diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d9bd32..3eb01a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +2024-12 + +1.1.5 + +- Fixed #309 "Empty object not reflected in generated wrapper." + `Struct` construction now based on `Config.root.entrySet` (instead of `Config.entrySet`). + For model construction, two passes are now performed to have all `@define`s captured for resolution. + This also resolves the closely related issue of wrongly resolving a `SomeName` as string when it is + actually a `@define`. The two passes are in particular to address types like `[SomeName]`, that is, + when the name is referenced indirectly via some container type. + 2024-08 - 1.1.4: maintenance (incorporate some generated java files (related with records) previously skipped for CI) diff --git a/build.sbt b/build.sbt index f6a2c06..d040c9f 100644 --- a/build.sbt +++ b/build.sbt @@ -2,7 +2,7 @@ enablePlugins(BuildInfoPlugin) organization := "com.github.carueda" name := "tscfg" -version := "1.1.4" +version := "1.1.5" scalaVersion := "3.3.4" crossScalaVersions := Seq("2.13.9", "3.3.4") @@ -17,7 +17,7 @@ libraryDependencies ++= Seq( "com.google.code.gson" % "gson" % "2.10.1" ) -scalafmtOnCompile := true +scalafmtOnCompile := false scalacOptions ++= Seq("-deprecation", "-feature") diff --git a/src/main/scala/tscfg/ModelBuilder.scala b/src/main/scala/tscfg/ModelBuilder.scala index 7e8e350..c2093a4 100644 --- a/src/main/scala/tscfg/ModelBuilder.scala +++ b/src/main/scala/tscfg/ModelBuilder.scala @@ -31,9 +31,31 @@ class ModelBuilder( */ private def fromConfig( namespace: Namespace, - conf: Config + conf: Config, ): model.ObjectType = { - val memberStructs: List[Struct] = Struct.getMemberStructs(namespace, conf) + val struct = Struct.getStruct(conf) + + // A single pass may not be sufficient to populate the namespace and get complete + // name resolutions, in particular, when a name (referring to a `@define`) is + // within some container type (like `[SomeDef]`). The effect is that `SomeDef` + // would be considered a string instead of resolving to the definition. + // An overall redesign (and much more elegant implementation) is needed, + // but, for now, let's do two passes as a quick trick: + fromStruct(namespace, struct, firstPass = true) + // avoid spurious duplicate warnings for the next pass: + val definedNames = namespace.getAllDefines.keySet + scribe.debug(s"definedNames = ${pprint(definedNames)}") + namespace.setOkDuplicates(definedNames) + fromStruct(namespace, struct, firstPass = false) + } + + /** Gets the [[model.ObjectType]] corresponding to the given Struct. */ + private def fromStruct( + namespace: Namespace, + struct: Struct, + firstPass: Boolean, + ): model.ObjectType = { + val memberStructs = struct.members.values.toList // 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 @@ -42,46 +64,21 @@ class ModelBuilder( val members: immutable.Map[String, model.AnnType] = memberStructs.map { childStruct => val name = childStruct.name - val cv = conf.getValue(name) val (childType, optional, default) = { if (childStruct.isLeaf) { - val valueString = tscfg.util.escapeValue(cv.unwrapped().toString) - val isEnum = childStruct.isEnum - - getTypeFromConfigValue(namespace, cv, isEnum) match { - case typ: model.STRING.type => - namespace.resolveDefine(valueString) match { - case Some(ort) => - (ort, false, None) - - case None => - inferAnnBasicTypeFromString(valueString) match { - case Some(annBasicType) => - annBasicType - - case None => - (typ, true, Some(valueString)) - } - } - - case typ: model.BasicType => - (typ, true, Some(valueString)) - - case typ => - (typ, false, None) - } + fromLeafStruct(namespace, childStruct) } else { ( - fromConfig(namespace.extend(name), conf.getConfig(name)), + fromStruct(namespace.extend(name), childStruct, firstPass), false, None ) } } - val comments = cv.origin().comments().asScala.toList + val comments = childStruct.comments val optFromComments = comments.exists(_.trim.startsWith("@optional")) val commentsOpt = if (comments.isEmpty) None else Some(comments.mkString("\n")) @@ -100,21 +97,23 @@ class ModelBuilder( else (optional || optFromComments, default) - // println(s"ModelBuilder: effOptional=$effOptional effDefault=$effDefault " + - // s"assumeAllRequired=$assumeAllRequired optFromComments=$optFromComments " + - // s"adjName=$adjName") - /* Get a comprehensive view of members from _all_ ancestors */ val parentClassMembers = - Struct.ancestorClassMembers(childStruct, memberStructs, namespace) + Struct.ancestorClassMembers( + childStruct, + memberStructs, + namespace, + firstPass, + ) /* build the annType */ val annType = buildAnnType( childType, effOptional, effDefault, + childStruct.defineCaseOpt, commentsOpt, - parentClassMembers + parentClassMembers, ) annType.defineCase foreach { namespace.addDefine(name, annType.t, _) } @@ -131,10 +130,43 @@ class ModelBuilder( ) } + private def fromLeafStruct( + namespace: Namespace, + struct: Struct, + ): (Type, Boolean, Option[String]) = { + val cv = struct.cv + val valueString = tscfg.util.escapeValue(cv.unwrapped().toString) + val isEnum = struct.isEnum + + getTypeFromConfigValue(namespace, cv, isEnum) match { + case typ: model.STRING.type => + namespace.resolveDefine(valueString) match { + case Some(ort) => + (ort, false, None) + + case None => + inferAnnBasicTypeFromString(valueString) match { + case Some(annBasicType) => + annBasicType + + case None => + (typ, true, Some(valueString)) + } + } + + case typ: model.BasicType => + (typ, true, Some(valueString)) + + case typ => + (typ, false, None) + } + } + private def buildAnnType( childType: model.Type, effOptional: Boolean, effDefault: Option[String], + defineCase: Option[DefineCase], commentsOpt: Option[String], parentClassMembers: Option[Map[String, model.AnnType]] ): AnnType = { @@ -159,8 +191,9 @@ class ModelBuilder( updatedChildType, optional = effOptional, default = effDefault, + defineCase = defineCase, comments = commentsOpt, - parentClassMembers = parentClassMembers.map(_.toMap) + parentClassMembers = parentClassMembers.map(_.toMap), ) } diff --git a/src/main/scala/tscfg/Struct.scala b/src/main/scala/tscfg/Struct.scala index c105ac6..6159659 100644 --- a/src/main/scala/tscfg/Struct.scala +++ b/src/main/scala/tscfg/Struct.scala @@ -1,110 +1,84 @@ package tscfg -import com.typesafe.config.{Config, ConfigValueType} +import com.typesafe.config._ import tscfg.DefineCase._ import tscfg.exceptions.ObjectDefinitionException import tscfg.ns.Namespace -import scala.annotation.tailrec +import java.util.Map.Entry +import scala.collection.SeqMap import scala.collection.{Map, mutable} +import scala.jdk.CollectionConverters._ /** Supports a convenient next representation based on given TS Config object. * It supports nested member definitions utilizing the 'members' field * * @param name * Name of the config member + * @param cv + * Associated ConfigValue * @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 + cv: ConfigValue, + members: SeqMap[String, Struct] = SeqMap.empty, ) { + /* Captures string value to support determining dependencies in terms of RHS + * names (that is, when such a string may be referring to a @define) + */ + private val tsStringValue: Option[String] = + cv.valueType() match { + case ConfigValueType.STRING => Some(cv.unwrapped().toString) + case _ => None + } - // Non-None when this is a `@define` - var defineCaseOpt: Option[DefineCase] = None - - def isDefine: Boolean = defineCaseOpt.isDefined + val comments: List[String] = + cv.origin().comments().asScala.toList - def isExtends: Boolean = defineCaseOpt match { - case Some(_: ExtendsDefineCase) => true - case Some(_: ImplementsDefineCase) => true - case _ => false + // Non-None when this is a `@define` + val defineCaseOpt: Option[DefineCase] = { + val defineLines = comments.map(_.trim).filter(_.startsWith("@define")) + defineLines.length match { + case 0 => None + case 1 => DefineCase.getDefineCase(defineLines.head) + case _ => + throw new IllegalArgumentException(s"multiple @define lines for $name.") + } } def isEnum: Boolean = defineCaseOpt.exists(_.isEnum) def isLeaf: Boolean = members.isEmpty - private def dependencies: Set[String] = { + private 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 + private def isDefine: Boolean = defineCaseOpt.isDefined - if (members.isEmpty) { - nameHeading - } - else { - val indent2 = indent + " " - s"$nameHeading ->\n" + indent2 + { - members - .map(e => s"${e._1}: " + e._2.format(indent2)) - .mkString("\n" + indent2) - } - } + private def isExtends: Boolean = defineCaseOpt match { + case Some(_: ExtendsDefineCase) => true + case Some(_: ImplementsDefineCase) => true + case _ => false } - // $COVERAGE-ON$ } object Struct { - import scala.jdk.CollectionConverters._ - /** Gets all structs from the given TS Config object, sorted appropriately for - * subsequent processing in ModelBuilder. Any circular reference will throw a - * [[ObjectDefinitionException]]. + /** Gets struct corresponding to given TS Config object, with members sorted + * appropriately for subsequent processing in ModelBuilder. Any circular + * reference will throw a [[ObjectDefinitionException]]. */ - def getMemberStructs(namespace: Namespace, conf: Config): List[Struct] = { - val struct: Struct = getStruct(conf) - val memberStructs: List[Struct] = struct.members.values.toList - - // set any define to each struct: - memberStructs.flatMap { setDefineCase(conf, _) } + def getStruct(conf: Config): Struct = getStruct("", "", conf.root()) - val (defineStructs, nonDefineStructs) = memberStructs.partition(_.isDefine) + private def sortStructs(structs: List[Struct]): List[Struct] = { + val (defineStructs, nonDefineStructs) = structs.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\n" + - s"struct=${struct.format()}\n" + - s"sortedStructs=\n ${sortedStructs.map(_.format()).mkString("\n ")}" - ) - } - - sortedStructs + // but also sort the "defines" by any name (member type) dependencies: + val definesSortedByNameDependencies = + sortByNameDependencies(sortedDefineStructs) + definesSortedByNameDependencies ++ nonDefineStructs } private def sortDefineStructs(defineStructs: List[Struct]): List[Struct] = { @@ -113,7 +87,7 @@ object Struct { // / `othersBeingAdded` allows to check for circularity def addExtendStruct( s: Struct, - othersBeingAdded: List[Struct] = List.empty + othersBeingAdded: List[Struct] = List.empty, ): Unit = { def addExtendsOrImplements(name: String, isExternal: Boolean): Unit = { sorted.get(name) match { @@ -133,7 +107,7 @@ object Struct { .map("'" + _.name + "'") .mkString(" -> ") throw ObjectDefinitionException( - s"extension of struct '${s.name}' involves circular reference via $via" + s"extension of struct '${s.name}' involves circular reference via $via", ) } @@ -147,7 +121,7 @@ object Struct { case None => throw ObjectDefinitionException( - s"struct '${s.name}' with undefined extend '$name'" + s"struct '${s.name}' with undefined extend '$name'", ) } } @@ -169,7 +143,7 @@ object Struct { assert( defineStructs.size == sorted.size, - s"defineStructs.size=${defineStructs.size} != sorted.size=${sorted.size}" + s"defineStructs.size=${defineStructs.size} != sorted.size=${sorted.size}", ) sorted.toList.map(_._2) @@ -206,25 +180,34 @@ object Struct { * List to find referenced structs * @param namespace * Current known name space + * @param firstPass + * true for a first pass, which will be less strict in terms of name + * resolution * @return * Mapping from symbol to type definition if struct is an ExtendsDefineCase */ def ancestorClassMembers( struct: Struct, memberStructs: List[Struct], - namespace: Namespace + namespace: Namespace, + firstPass: Boolean, ): Option[Map[String, model.AnnType]] = { def handleExtends( parentName: String, - isExternal: Boolean + isExternal: Boolean, ): Option[Map[String, model.AnnType]] = { val defineStructs = memberStructs.filter(_.isDefine) val greatAncestorMembers = defineStructs.find(_.name == parentName) match { case Some(parentStruct) if parentStruct.isExtends => - ancestorClassMembers(parentStruct, memberStructs, namespace) + ancestorClassMembers( + parentStruct, + memberStructs, + namespace, + firstPass, + ) case Some(_) => None @@ -232,19 +215,18 @@ object Struct { case None => throw new RuntimeException( - s"struct '${struct.name}' with undefined extend '$parentName'" + s"struct '${struct.name}' with undefined extend '$parentName'", ) } val parentMembers = namespace.getRealDefine(parentName).map(_.members) match { case Some(parentMembers) => parentMembers - - case None if isExternal => None - + case None if isExternal => None + case None if firstPass => None case None => throw new IllegalArgumentException( - s"@define '${struct.name}' is invalid because '$parentName' is not @defined" + s"@define '${struct.name}' is invalid because '$parentName' is not @defined", ) } @@ -264,77 +246,49 @@ object Struct { } } - private def getStruct(conf: Config): Struct = { - val structs = mutable.HashMap[String, Struct]("" -> Struct("")) - - def resolve(key: String): Struct = { - if (!structs.contains(key)) structs.put(key, Struct(getSimple(key))) - structs(key) - } - - // Due to TS Config API, we traverse from the leaves to the ancestors: - conf.entrySet().asScala foreach { e => - 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 = { - val (parent, simple) = (getParent(childKey), getSimple(childKey)) - createParent(parent, simple, childStruct) - - @tailrec - def createParent( - parentKey: String, - simple: String, - child: Struct - ): Unit = { - val parentGroup = resolve(parentKey) - parentGroup.members.put(simple, child) - if (parentKey != "") { - createParent( - getParent(parentKey), - getSimple(parentKey), - parentGroup - ) - } - } - } - } - - def getParent(path: String): String = { - val idx = path.lastIndexOf('.') - if (idx >= 0) path.substring(0, idx) else "" + private def getStruct(pp: String, key: String, cv: ConfigValue): Struct = + cv match { + case co: ConfigObject => getStructForObject(pp, key, co) + case _ => getStructForLeaf(key, cv) } - def getSimple(path: String): String = { - val idx = path.lastIndexOf('.') - if (idx >= 0) path.substring(idx + 1) else path + private def getStructForObject( + pp: String, + key: String, + co: ConfigObject, + ): Struct = { + def doEntry(e: Entry[String, ConfigValue]): Struct = { + val key = e.getKey + val cv = e.getValue + val newPp = if (pp.isEmpty) key else s"$pp.$key" + getStruct(newPp, key, cv) } + val structs = co.entrySet().asScala.toList.map(doEntry) + val sortedStructs = sortStructs(structs) + val pairs = sortedStructs.map(s => s.name -> s) + val members = SeqMap(pairs: _*) + Struct(key, co, members) + } - structs("") + private def getStructForLeaf(key: String, cv: ConfigValue): Struct = { + val name = if (key.contains('$')) s"\"$key\"" else key + Struct(name, cv) } - private def setDefineCase(conf: Config, s: Struct): Option[DefineCase] = { - val cv = conf.getValue(s.name) - val comments = cv.origin().comments().asScala.toList - val defineLines = comments.map(_.trim).filter(_.startsWith("@define")) - s.defineCaseOpt = defineLines.length match { - case 0 => None - case 1 => DefineCase.getDefineCase(defineLines.head) - case _ => - throw new IllegalArgumentException( - s"multiple @define lines for ${s.name}." - ) - } - s.defineCaseOpt + // $COVERAGE-OFF$ + def main(args: Array[String]): Unit = { + import java.io.File + // tscfg.util.setLogMinLevel() + val filename = + args.headOption.getOrElse("src/main/tscfg/example/issue309b.spec.conf") + println(s"filename = $filename") + val file = new File(filename) + val bufSource = io.Source.fromFile(file) + val source = bufSource.mkString.trim + bufSource.close() + val conf = ConfigFactory.parseString(source).resolve() + val struct = getStruct(conf) + pprint.pprintln(struct) } + // $COVERAGE-ON$ } diff --git a/src/main/scala/tscfg/model.scala b/src/main/scala/tscfg/model.scala index bb46eba..872dbc3 100644 --- a/src/main/scala/tscfg/model.scala +++ b/src/main/scala/tscfg/model.scala @@ -86,13 +86,11 @@ object model { t: Type, optional: Boolean = false, default: Option[String] = None, + defineCase: Option[DefineCase] = None, comments: Option[String] = None, - parentClassMembers: Option[Map[String, model.AnnType]] = None + parentClassMembers: Option[Map[String, model.AnnType]] = None, ) { - val defineCase: Option[DefineCase] = - comments.flatMap(cs => getDefineCase(cs)) - val isDefine: Boolean = defineCase.isDefined def nameIsImplementsIsExternal: Option[(String, Boolean, Boolean)] = diff --git a/src/main/scala/tscfg/ns/Namespace.scala b/src/main/scala/tscfg/ns/Namespace.scala index 0252899..d741d72 100644 --- a/src/main/scala/tscfg/ns/Namespace.scala +++ b/src/main/scala/tscfg/ns/Namespace.scala @@ -50,6 +50,12 @@ class Namespace private[ns] ( private val defineNames = collection.mutable.HashSet[String]() private val defineAbstractClassNames = collection.mutable.HashSet[String]() + private var okDuplicates: Option[Set[String]] = None + + // to facilitate handling of 2nd pass + def setOkDuplicates(names: Set[String]): Unit = + okDuplicates = Some(names) + def addDefine( simpleName: String, t: Type, @@ -62,10 +68,9 @@ class Namespace private[ns] ( assert(!simpleName.contains(".")) assert(simpleName.nonEmpty) - if ( - defineNames - .contains(simpleName) || defineAbstractClassNames.contains(simpleName) - ) { + val isDuplicate = defineNames.contains(simpleName) || + defineAbstractClassNames.contains(simpleName) + if (isDuplicate && !okDuplicates.exists(_.contains(simpleName))) { val ns = if (getPath.nonEmpty) s"'$getPathString'" else "(root)" println( s"WARN: duplicate @define '$simpleName' in namespace $ns. Ignoring previous entry" diff --git a/src/main/tscfg/example/issue309a.spec.conf b/src/main/tscfg/example/issue309a.spec.conf new file mode 100644 index 0000000..d166964 --- /dev/null +++ b/src/main/tscfg/example/issue309a.spec.conf @@ -0,0 +1,4 @@ +# comment1 +emptyObj {} +# comment2 +other: int diff --git a/src/main/tscfg/example/issue309b.spec.conf b/src/main/tscfg/example/issue309b.spec.conf new file mode 100644 index 0000000..c2d75e2 --- /dev/null +++ b/src/main/tscfg/example/issue309b.spec.conf @@ -0,0 +1,10 @@ +#@define abstract +SomeAbstract { + something: string +} + +#@define extends SomeAbstract +SomeExtension { +} + +foo: SomeExtension diff --git a/src/test/java/tscfg/example/JavaIssue309aCfg.java b/src/test/java/tscfg/example/JavaIssue309aCfg.java new file mode 100644 index 0000000..45aa8fb --- /dev/null +++ b/src/test/java/tscfg/example/JavaIssue309aCfg.java @@ -0,0 +1,49 @@ +package tscfg.example; + +public class JavaIssue309aCfg { + public final JavaIssue309aCfg.EmptyObj emptyObj; + public final int other; + public static class EmptyObj { + + + public EmptyObj(com.typesafe.config.Config c, java.lang.String parentPath, $TsCfgValidator $tsCfgValidator) { + + } + } + + public JavaIssue309aCfg(com.typesafe.config.Config c) { + final $TsCfgValidator $tsCfgValidator = new $TsCfgValidator(); + final java.lang.String parentPath = ""; + this.emptyObj = c.hasPathOrNull("emptyObj") ? new JavaIssue309aCfg.EmptyObj(c.getConfig("emptyObj"), parentPath + "emptyObj.", $tsCfgValidator) : new JavaIssue309aCfg.EmptyObj(com.typesafe.config.ConfigFactory.parseString("emptyObj{}"), parentPath + "emptyObj.", $tsCfgValidator); + this.other = $_reqInt(parentPath, c, "other", $tsCfgValidator); + $tsCfgValidator.validate(); + } + private static int $_reqInt(java.lang.String parentPath, com.typesafe.config.Config c, java.lang.String path, $TsCfgValidator $tsCfgValidator) { + if (c == null) return 0; + try { + return c.getInt(path); + } + catch(com.typesafe.config.ConfigException e) { + $tsCfgValidator.addBadPath(parentPath + path, e); + return 0; + } + } + + private static final class $TsCfgValidator { + private final java.util.List 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()) {}; + } + } + } +} diff --git a/src/test/java/tscfg/example/JavaIssue309bCfg.java b/src/test/java/tscfg/example/JavaIssue309bCfg.java new file mode 100644 index 0000000..bd9560e --- /dev/null +++ b/src/test/java/tscfg/example/JavaIssue309bCfg.java @@ -0,0 +1,56 @@ +package tscfg.example; + +public class JavaIssue309bCfg { + public final SomeExtension foo; + public abstract static class SomeAbstract { + public final java.lang.String something; + + public SomeAbstract(com.typesafe.config.Config c, java.lang.String parentPath, $TsCfgValidator $tsCfgValidator) { + this.something = $_reqStr(parentPath, c, "something", $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 SomeExtension extends SomeAbstract { + + + public SomeExtension(com.typesafe.config.Config c, java.lang.String parentPath, $TsCfgValidator $tsCfgValidator) { + super(c, parentPath, $tsCfgValidator); + + } + } + + public JavaIssue309bCfg(com.typesafe.config.Config c) { + final $TsCfgValidator $tsCfgValidator = new $TsCfgValidator(); + final java.lang.String parentPath = ""; + this.foo = c.hasPathOrNull("foo") ? new SomeExtension(c.getConfig("foo"), parentPath + "foo.", $tsCfgValidator) : new SomeExtension(com.typesafe.config.ConfigFactory.parseString("foo{}"), parentPath + "foo.", $tsCfgValidator); + $tsCfgValidator.validate(); + } + private static final class $TsCfgValidator { + private final java.util.List 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()) {}; + } + } + } +} diff --git a/src/test/scala/tscfg/example/ScalaIssue309aCfg.scala b/src/test/scala/tscfg/example/ScalaIssue309aCfg.scala new file mode 100644 index 0000000..52da908 --- /dev/null +++ b/src/test/scala/tscfg/example/ScalaIssue309aCfg.scala @@ -0,0 +1,58 @@ +package tscfg.example + +final case class ScalaIssue309aCfg( + emptyObj : ScalaIssue309aCfg.EmptyObj, + other : scala.Int +) +object ScalaIssue309aCfg { + final case class EmptyObj( + + ) + object EmptyObj { + def apply(c: com.typesafe.config.Config, parentPath: java.lang.String, $tsCfgValidator: $TsCfgValidator): ScalaIssue309aCfg.EmptyObj = { + ScalaIssue309aCfg.EmptyObj( + + ) + } + } + + def apply(c: com.typesafe.config.Config): ScalaIssue309aCfg = { + val $tsCfgValidator: $TsCfgValidator = new $TsCfgValidator() + val parentPath: java.lang.String = "" + val $result = ScalaIssue309aCfg( + emptyObj = ScalaIssue309aCfg.EmptyObj(if(c.hasPathOrNull("emptyObj")) c.getConfig("emptyObj") else com.typesafe.config.ConfigFactory.parseString("emptyObj{}"), parentPath + "emptyObj.", $tsCfgValidator), + other = $_reqInt(parentPath, c, "other", $tsCfgValidator) + ) + $tsCfgValidator.validate() + $result + } + private def $_reqInt(parentPath: java.lang.String, c: com.typesafe.config.Config, path: java.lang.String, $tsCfgValidator: $TsCfgValidator): scala.Int = { + if (c == null) 0 + else try c.getInt(path) + catch { + case e:com.typesafe.config.ConfigException => + $tsCfgValidator.addBadPath(parentPath + path, e) + 0 + } + } + + final class $TsCfgValidator { + private val badPaths = scala.collection.mutable.ArrayBuffer[java.lang.String]() + + def addBadPath(path: java.lang.String, e: com.typesafe.config.ConfigException): Unit = { + badPaths += s"'$path': ${e.getClass.getName}(${e.getMessage})" + } + + def addInvalidEnumValue(path: java.lang.String, value: java.lang.String, enumName: java.lang.String): Unit = { + badPaths += s"'$path': invalid value $value for enumeration $enumName" + } + + def validate(): Unit = { + if (badPaths.nonEmpty) { + throw new com.typesafe.config.ConfigException( + badPaths.mkString("Invalid configuration:\n ", "\n ", "") + ){} + } + } + } +} diff --git a/src/test/scala/tscfg/example/ScalaIssue309bCfg.scala b/src/test/scala/tscfg/example/ScalaIssue309bCfg.scala new file mode 100644 index 0000000..85da83b --- /dev/null +++ b/src/test/scala/tscfg/example/ScalaIssue309bCfg.scala @@ -0,0 +1,60 @@ +package tscfg.example + +final case class ScalaIssue309bCfg( + foo : ScalaIssue309bCfg.SomeExtension +) +object ScalaIssue309bCfg { + sealed abstract class SomeAbstract ( + val something : java.lang.String + ) + + final case class SomeExtension( + override val something : java.lang.String + ) extends SomeAbstract(something) + object SomeExtension { + def apply(c: com.typesafe.config.Config, parentPath: java.lang.String, $tsCfgValidator: $TsCfgValidator): ScalaIssue309bCfg.SomeExtension = { + ScalaIssue309bCfg.SomeExtension( + something = $_reqStr(parentPath, c, "something", $tsCfgValidator) + ) + } + private def $_reqStr(parentPath: java.lang.String, c: com.typesafe.config.Config, path: java.lang.String, $tsCfgValidator: $TsCfgValidator): java.lang.String = { + if (c == null) null + else try c.getString(path) + catch { + case e:com.typesafe.config.ConfigException => + $tsCfgValidator.addBadPath(parentPath + path, e) + null + } + } + + } + + def apply(c: com.typesafe.config.Config): ScalaIssue309bCfg = { + val $tsCfgValidator: $TsCfgValidator = new $TsCfgValidator() + val parentPath: java.lang.String = "" + val $result = ScalaIssue309bCfg( + foo = ScalaIssue309bCfg.SomeExtension(if(c.hasPathOrNull("foo")) c.getConfig("foo") else com.typesafe.config.ConfigFactory.parseString("foo{}"), parentPath + "foo.", $tsCfgValidator) + ) + $tsCfgValidator.validate() + $result + } + final class $TsCfgValidator { + private val badPaths = scala.collection.mutable.ArrayBuffer[java.lang.String]() + + def addBadPath(path: java.lang.String, e: com.typesafe.config.ConfigException): Unit = { + badPaths += s"'$path': ${e.getClass.getName}(${e.getMessage})" + } + + def addInvalidEnumValue(path: java.lang.String, value: java.lang.String, enumName: java.lang.String): Unit = { + badPaths += s"'$path': invalid value $value for enumeration $enumName" + } + + def validate(): Unit = { + if (badPaths.nonEmpty) { + throw new com.typesafe.config.ConfigException( + badPaths.mkString("Invalid configuration:\n ", "\n ", "") + ){} + } + } + } +} diff --git a/src/test/scala/tscfg/generators/java/JavaMainSpec.scala b/src/test/scala/tscfg/generators/java/JavaMainSpec.scala index 3b0e24b..1b1dec0 100644 --- a/src/test/scala/tscfg/generators/java/JavaMainSpec.scala +++ b/src/test/scala/tscfg/generators/java/JavaMainSpec.scala @@ -1458,4 +1458,41 @@ class JavaMainSpec extends AnyWordSpec { assert(c.cfg.additionalParam === "additionalParamValue") } } + + "(scala) issue 309a" should { + "generate class for empty object EmptyObj" in { + val r = JavaGen.generate("example/issue309a.spec.conf") + assert(r.classNames === Set("JavaIssue309aCfg", "EmptyObj")) + } + + "be exercised ok" in { + val c = JavaIssue309aCfg(ConfigFactory.parseString(""" + |other = 42 + |""".stripMargin)) + + assert(c.other === 42) + assert(c.emptyObj.getClass.getSimpleName === "EmptyObj") + } + } + + "(scala) issue 309b" should { + "generate class for empty object SomeExtension extending SomeAbstract" in { + val r = JavaGen.generate("example/issue309b.spec.conf") + assert( + r.classNames === Set( + "JavaIssue309bCfg", + "SomeAbstract", + "SomeExtension" + ) + ) + } + + "be exercised ok" in { + val c = JavaIssue309bCfg(ConfigFactory.parseString(""" + |foo.something = howdy + |""".stripMargin)) + + assert(c.foo.something === "howdy") + } + } } diff --git a/src/test/scala/tscfg/generators/scala/ScalaMainSpec.scala b/src/test/scala/tscfg/generators/scala/ScalaMainSpec.scala index 6ca340b..75e7728 100644 --- a/src/test/scala/tscfg/generators/scala/ScalaMainSpec.scala +++ b/src/test/scala/tscfg/generators/scala/ScalaMainSpec.scala @@ -469,7 +469,7 @@ class ScalaMainSpec extends AnyWordSpec { val r = ScalaGen.generate("example/issue19.spec.conf") assert(r.classNames === Set("ScalaIssue19Cfg")) assert( - r.fields === Map( + r.fields.toSet === Set( "do_log" -> "scala.Boolean", "_$_foo_" -> "java.lang.String" ) @@ -1307,4 +1307,41 @@ class ScalaMainSpec extends AnyWordSpec { assert(c.cfg.additionalParam === "additionalParamValue") } } + + "(scala) issue 309a" should { + "generate class for empty object EmptyObj" in { + val r = ScalaGen.generate("example/issue309a.spec.conf") + assert(r.classNames === Set("ScalaIssue309aCfg", "EmptyObj")) + } + + "be exercised ok" in { + val c = ScalaIssue309aCfg(ConfigFactory.parseString(""" + |other = 42 + |""".stripMargin)) + + assert(c.other === 42) + assert(c.emptyObj.getClass.getSimpleName === "EmptyObj") + } + } + + "(scala) issue 309b" should { + "generate class for empty object SomeExtension extending SomeAbstract" in { + val r = ScalaGen.generate("example/issue309b.spec.conf") + assert( + r.classNames === Set( + "ScalaIssue309bCfg", + "SomeAbstract", + "SomeExtension" + ) + ) + } + + "be exercised ok" in { + val c = ScalaIssue309bCfg(ConfigFactory.parseString(""" + |foo.something = howdy + |""".stripMargin)) + + assert(c.foo.something === "howdy") + } + } }