Skip to content

Commit

Permalink
fix #180 "Wrapped Data Types"
Browse files Browse the repository at this point in the history
  • Loading branch information
carueda committed Oct 9, 2022
1 parent dff9180 commit f0ac232
Show file tree
Hide file tree
Showing 10 changed files with 368 additions and 9 deletions.
4 changes: 4 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
2022-10 - 1.0.1

- fix #180 "Wrapped Data Types"

2022-07

1.0.0
Expand Down
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ enablePlugins(BuildInfoPlugin)

organization := "com.github.carueda"
name := "tscfg"
version := "1.0.0"
version := "1.0.1"
scalaVersion := "3.1.3"
crossScalaVersions := Seq("2.13.8", "3.1.3")

Expand Down
4 changes: 4 additions & 0 deletions src/main/scala/tscfg/ModelBuilder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ class ModelBuilder(
conf: Config
): model.ObjectType = {
val memberStructs: List[Struct] = Struct.getMemberStructs(namespace, conf)
// Note: the returned order of this list is assumed to have taken into account any dependencies between
// the structs, in terms both of inheritance and member types.
// TODO a future revision may lessen this requirement by making the `namespace.resolveDefine` call below
// and associated handling more flexible.

val members: immutable.Map[String, model.AnnType] = memberStructs.map {
childStruct =>
Expand Down
69 changes: 61 additions & 8 deletions src/main/scala/tscfg/Struct.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package tscfg

import com.typesafe.config.Config
import com.typesafe.config.{Config, ConfigValueType}
import tscfg.DefineCase._
import tscfg.exceptions.ObjectDefinitionException
import tscfg.ns.Namespace
Expand All @@ -15,10 +15,14 @@ import scala.collection.{Map, mutable}
* Name of the config member
* @param members
* Nested config definitions
* @param tsStringValue
* Captures string value to support determining dependencies in terms of RHS
* names (that is, when such a string may be referring to a @define)
*/
case class Struct(
name: String,
members: mutable.HashMap[String, Struct] = mutable.HashMap.empty,
tsStringValue: Option[String] = None
) {

// Non-None when this is a `@define`
Expand All @@ -36,17 +40,28 @@ case class Struct(

def isLeaf: Boolean = members.isEmpty

def dependencies: Set[String] = {
tsStringValue.toSet ++ members.values.flatMap(_.dependencies)
}

// $COVERAGE-OFF$
def format(indent: String = ""): String = {
val defineStr = defineCaseOpt.map(dc => s" $dc").getOrElse("")
val nameStr = s"${if (name.isEmpty) "(root)" else name}$defineStr"

val dependenciesStr = dependencies.toList match {
case Nil => ""
case l => s" [dependencies=${l.mkString(", ")}]"
}

val nameHeading = nameStr + dependenciesStr

if (members.isEmpty) {
nameStr
nameHeading
}
else {
val indent2 = indent + " "
s"$nameStr ->\n" + indent2 + {
s"$nameHeading ->\n" + indent2 + {
members
.map(e => s"${e._1}: " + e._2.format(indent2))
.mkString("\n" + indent2)
Expand All @@ -73,14 +88,23 @@ object Struct {
val (defineStructs, nonDefineStructs) = memberStructs.partition(_.isDefine)
val sortedDefineStructs = sortDefineStructs(defineStructs)

val sortedStructs = {
// but also sort the "defines" by any name (member type) dependencies:
val definesSortedByNameDependencies = sortByNameDependencies(
sortedDefineStructs
)
definesSortedByNameDependencies ++ nonDefineStructs
}

if (namespace.isRoot) {
scribe.debug(
s"root struct=${struct.format()}\n" +
s"sortedDefineStructs=\n${sortedDefineStructs.map(_.format()).mkString("\n")}"
s"root\n" +
s"struct=${struct.format()}\n" +
s"sortedStructs=\n ${sortedStructs.map(_.format()).mkString("\n ")}"
)
}

sortedDefineStructs ++ nonDefineStructs
sortedStructs
}

private def sortDefineStructs(defineStructs: List[Struct]): List[Struct] = {
Expand Down Expand Up @@ -151,6 +175,26 @@ object Struct {
sorted.toList.map(_._2)
}

private def sortByNameDependencies(structs: List[Struct]): List[Struct] = {
structs.sortWith((a, b) => {
val aDeps = a.dependencies
val bDeps = b.dependencies

if (aDeps.contains(b.name)) {
// a depends on b, so b should come first:
false
}
else if (bDeps.contains(a.name)) {
// b depends on a, so a should come first:
true
}
else {
// no dependency, so sort by name:
a.name < b.name
}
})
}

/** Determines the joint set of all ancestor's members to allow proper
* overriding in child structs.
*
Expand Down Expand Up @@ -230,8 +274,17 @@ object Struct {

// Due to TS Config API, we traverse from the leaves to the ancestors:
conf.entrySet().asScala foreach { e =>
val path = e.getKey
val leaf = Struct(path)
val path = e.getKey
val configValue = e.getValue

// capture string value to determine possible "define" dependency
val tsStringValue: Option[String] = e.getValue.valueType() match {
case ConfigValueType.STRING => Some(configValue.unwrapped().toString)
case _ => None
}
scribe.debug(s"getStruct: path=$path, tsStringValue=$tsStringValue")
val leaf = Struct(path, tsStringValue = tsStringValue)

doAncestorsOf(path, leaf)

def doAncestorsOf(childKey: String, childStruct: Struct): Unit = {
Expand Down
16 changes: 16 additions & 0 deletions src/main/tscfg/example/issue180.spec.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#@define
TypeA {
fizz: String
buzz: String
}

#@define
TypeB {
foo: TypeA
bar: TypeA
}

cfg {
typeB: TypeB
additionalParam: String
}
80 changes: 80 additions & 0 deletions src/test/java/tscfg/example/JavaIssue180Cfg.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package tscfg.example;

public class JavaIssue180Cfg {
public final JavaIssue180Cfg.Cfg cfg;
public static class TypeA {
public final java.lang.String buzz;
public final java.lang.String fizz;

public TypeA(com.typesafe.config.Config c, java.lang.String parentPath, $TsCfgValidator $tsCfgValidator) {
this.buzz = $_reqStr(parentPath, c, "buzz", $tsCfgValidator);
this.fizz = $_reqStr(parentPath, c, "fizz", $tsCfgValidator);
}
private static java.lang.String $_reqStr(java.lang.String parentPath, com.typesafe.config.Config c, java.lang.String path, $TsCfgValidator $tsCfgValidator) {
if (c == null) return null;
try {
return c.getString(path);
}
catch(com.typesafe.config.ConfigException e) {
$tsCfgValidator.addBadPath(parentPath + path, e);
return null;
}
}

}

public static class TypeB {
public final TypeA bar;
public final TypeA foo;

public TypeB(com.typesafe.config.Config c, java.lang.String parentPath, $TsCfgValidator $tsCfgValidator) {
this.bar = c.hasPathOrNull("bar") ? new TypeA(c.getConfig("bar"), parentPath + "bar.", $tsCfgValidator) : new TypeA(com.typesafe.config.ConfigFactory.parseString("bar{}"), parentPath + "bar.", $tsCfgValidator);
this.foo = c.hasPathOrNull("foo") ? new TypeA(c.getConfig("foo"), parentPath + "foo.", $tsCfgValidator) : new TypeA(com.typesafe.config.ConfigFactory.parseString("foo{}"), parentPath + "foo.", $tsCfgValidator);
}
}

public static class Cfg {
public final java.lang.String additionalParam;
public final TypeB typeB;

public Cfg(com.typesafe.config.Config c, java.lang.String parentPath, $TsCfgValidator $tsCfgValidator) {
this.additionalParam = $_reqStr(parentPath, c, "additionalParam", $tsCfgValidator);
this.typeB = c.hasPathOrNull("typeB") ? new TypeB(c.getConfig("typeB"), parentPath + "typeB.", $tsCfgValidator) : new TypeB(com.typesafe.config.ConfigFactory.parseString("typeB{}"), parentPath + "typeB.", $tsCfgValidator);
}
private static java.lang.String $_reqStr(java.lang.String parentPath, com.typesafe.config.Config c, java.lang.String path, $TsCfgValidator $tsCfgValidator) {
if (c == null) return null;
try {
return c.getString(path);
}
catch(com.typesafe.config.ConfigException e) {
$tsCfgValidator.addBadPath(parentPath + path, e);
return null;
}
}

}

public JavaIssue180Cfg(com.typesafe.config.Config c) {
final $TsCfgValidator $tsCfgValidator = new $TsCfgValidator();
final java.lang.String parentPath = "";
this.cfg = c.hasPathOrNull("cfg") ? new JavaIssue180Cfg.Cfg(c.getConfig("cfg"), parentPath + "cfg.", $tsCfgValidator) : new JavaIssue180Cfg.Cfg(com.typesafe.config.ConfigFactory.parseString("cfg{}"), parentPath + "cfg.", $tsCfgValidator);
$tsCfgValidator.validate();
}
private static final class $TsCfgValidator {
private final java.util.List<java.lang.String> badPaths = new java.util.ArrayList<>();

void addBadPath(java.lang.String path, com.typesafe.config.ConfigException e) {
badPaths.add("'" + path + "': " + e.getClass().getName() + "(" + e.getMessage() + ")");
}

void validate() {
if (!badPaths.isEmpty()) {
java.lang.StringBuilder sb = new java.lang.StringBuilder("Invalid configuration:");
for (java.lang.String path : badPaths) {
sb.append("\n ").append(path);
}
throw new com.typesafe.config.ConfigException(sb.toString()) {};
}
}
}
}
56 changes: 56 additions & 0 deletions src/test/scala/tscfg/ModelBuilderSpec.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package tscfg

import org.scalatest.wordspec.AnyWordSpec
import org.scalatest.matchers.should.Matchers._
import tscfg.buildWarnings.{
DefaultListElemWarning,
MultElemListWarning,
Expand Down Expand Up @@ -324,4 +325,59 @@ class ModelBuilderSpec extends AnyWordSpec {
assert(e.getMessage contains "Unrecognized @define construct")
}
}

"#180 Wrapped Data Types" should {
val source = """
|#@define
|TypeA {
| fizz: String
| buzz: String
|}
|
|#@define
|TypeB {
| foo: TypeA
| bar: TypeA
|}
|
|cfg {
| typeB: TypeB
| additionalParam: String
|}
|""".stripMargin

val result = build(source)
// pprint.pprintln(result.objectType)

"capture expected TypeA model" in {
val TypeA = result.objectType.members("TypeA")
TypeA.t shouldBe a[ObjectType]
val ot = TypeA.t.asInstanceOf[ObjectType]
ot.members.keySet shouldBe Set("fizz", "buzz")
ot.members("fizz").t shouldBe STRING
}

"capture expected TypeB model" in {
val TypeB = result.objectType.members("TypeB")
TypeB.t shouldBe a[ObjectType]
val TypeB_ot = TypeB.t.asInstanceOf[ObjectType]
TypeB_ot.members.keySet shouldBe Set("foo", "bar")
val foo = TypeB_ot.members("foo")
// pprint.pprintln(foo)
foo.t shouldBe a[ObjectRefType]
val foo_ort = foo.t.asInstanceOf[ObjectRefType]
foo_ort.simpleName shouldBe "TypeA"
}

"capture expected cfg model" in {
val cfg = result.objectType.members("cfg")
cfg.t shouldBe a[ObjectType]
val cfg_ot = cfg.t.asInstanceOf[ObjectType]
cfg_ot.members.keySet shouldBe Set("typeB", "additionalParam")
val typeB = cfg_ot.members("typeB")
typeB.t shouldBe a[ObjectRefType]
val typeB_ort = typeB.t.asInstanceOf[ObjectRefType]
typeB_ort.simpleName shouldBe "TypeB"
}
}
}
Loading

0 comments on commit f0ac232

Please sign in to comment.