From 2368ac4961daedd2f26466ef4c25a9cb895c700b Mon Sep 17 00:00:00 2001 From: Carlos Rueda Date: Tue, 17 Dec 2024 22:07:10 -0800 Subject: [PATCH] Good part of #312 (doc comments) implemented (#314) implement good part of #312 "doc comments" --- CHANGELOG.md | 11 ++ build.sbt | 2 +- .../java/tscfg/example/JavaExampleCfg.java | 45 ++++++- src/main/scala/tscfg/Main.scala | 6 + src/main/scala/tscfg/ModelBuilder.scala | 5 +- src/main/scala/tscfg/Struct.scala | 7 + .../scala/tscfg/example/ScalaExampleCfg.scala | 26 +++- src/main/scala/tscfg/files.scala | 15 +++ src/main/scala/tscfg/gen4tests.scala | 11 ++ .../scala/tscfg/generators/Generator.scala | 1 + src/main/scala/tscfg/generators/docUtil.scala | 72 ++++++++++ .../scala/tscfg/generators/java/JavaGen.scala | 21 ++- .../tscfg/generators/scala/ScalaGen.scala | 8 +- src/main/scala/tscfg/model.scala | 1 + src/main/tscfg/example/example.spec.conf | 7 +- src/main/tscfg/example/issue312a.spec.conf | 24 ++++ src/main/tscfg/example/issue312b.spec.conf | 25 ++++ .../java/tscfg/example/JavaDuration2Cfg.java | 16 +++ .../java/tscfg/example/JavaDuration3Cfg.java | 16 +++ .../java/tscfg/example/JavaDurationCfg.java | 16 +++ .../java/tscfg/example/JavaExample1Cfg.java | 4 + .../tscfg/example/JavaExample4tplCfg.java | 41 ++++++ .../java/tscfg/example/JavaExampleCfg.java | 42 +++++- .../java/tscfg/example/JavaIssue10Cfg.java | 8 ++ .../java/tscfg/example/JavaIssue23Cfg.java | 20 +++ .../java/tscfg/example/JavaIssue29Cfg.java | 4 + .../java/tscfg/example/JavaIssue309aCfg.java | 12 ++ .../java/tscfg/example/JavaIssue312aCfg.java | 117 +++++++++++++++++ .../java/tscfg/example/JavaIssue312bCfg.java | 124 ++++++++++++++++++ .../java/tscfg/example/JavaIssue50Cfg.java | 4 + .../tscfg/example/ScalaDuration2Cfg.scala | 13 ++ .../tscfg/example/ScalaDurationCfg.scala | 13 ++ .../tscfg/example/ScalaExample4tplCfg.scala | 25 ++++ .../scala/tscfg/example/ScalaExampleCfg.scala | 24 +++- .../scala/tscfg/example/ScalaIssue10Cfg.scala | 8 ++ .../tscfg/example/ScalaIssue309aCfg.scala | 3 + .../tscfg/example/ScalaIssue312aCfg.scala | 116 ++++++++++++++++ .../generators/java/JavaExampleSpec.scala | 2 +- .../tscfg/generators/java/JavaMainSpec.scala | 104 ++++++++++++++- .../generators/scala/ScalaExampleSpec.scala | 2 +- .../generators/scala/ScalaMainSpec.scala | 41 ++++++ 41 files changed, 1038 insertions(+), 24 deletions(-) create mode 100644 src/main/scala/tscfg/files.scala create mode 100644 src/main/scala/tscfg/generators/docUtil.scala create mode 100644 src/main/tscfg/example/issue312a.spec.conf create mode 100644 src/main/tscfg/example/issue312b.spec.conf create mode 100644 src/test/java/tscfg/example/JavaIssue312aCfg.java create mode 100644 src/test/java/tscfg/example/JavaIssue312bCfg.java create mode 100644 src/test/scala/tscfg/example/ScalaIssue312aCfg.scala diff --git a/CHANGELOG.md b/CHANGELOG.md index 3eb01a32..ca902310 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ 2024-12 +1.2.0 + +- Advancing #312 "Reflect doc comments in generated code" + - Most cases already covered, both for scala and java records/POJOs + - Note: `@define`s not yet addressed. + - Comments used for annotations, i.e., starting with `@`, are not considered. + - Also ignored are comments starting with `!`. + - Doc generation processing is always performed. + (In retrospect, this should have been implemented long ago, but better late than never 😅) + - however, for now, `--no-doc` can be used to opt out of doc generation + 1.1.5 - Fixed #309 "Empty object not reflected in generated wrapper." diff --git a/build.sbt b/build.sbt index 703be440..69fb143f 100644 --- a/build.sbt +++ b/build.sbt @@ -2,7 +2,7 @@ enablePlugins(BuildInfoPlugin) organization := "com.github.carueda" name := "tscfg" -version := "1.1.5" +version := "1.2.0" scalaVersion := "3.3.4" crossScalaVersions := Seq("2.13.9", "3.3.4") diff --git a/src/main/java/tscfg/example/JavaExampleCfg.java b/src/main/java/tscfg/example/JavaExampleCfg.java index f82e8fc3..142ba95c 100644 --- a/src/main/java/tscfg/example/JavaExampleCfg.java +++ b/src/main/java/tscfg/example/JavaExampleCfg.java @@ -1,18 +1,58 @@ -// generated by tscfg 0.9.92 on Thu Aug 08 12:07:41 PDT 2019 +// generated by tscfg 1.2.0 on Tue Dec 17 21:46:38 PST 2024 // source: src/main/tscfg/example/example.spec.conf package tscfg.example; public class JavaExampleCfg { + + /** + * Description of the required endpoint section. + */ public final JavaExampleCfg.Endpoint endpoint; + + /** + * Description of the required endpoint section. + */ public static class Endpoint { + + /** + * a required int + */ public final int intReq; + + /** + * Interface definition + */ public final Endpoint.Interface_ interface_; + + /** + * a required String + */ public final java.lang.String path; + + /** + * an optional Integer with default value null + */ public final java.lang.Integer serial; + + /** + * a String with default value "https://example.net" + */ public final java.lang.String url; + + /** + * Interface definition + */ public static class Interface_ { + + /** + * an int with default value 8080 + */ public final int port; + + /** + * Interface type + */ public final java.lang.String type; public Interface_(com.typesafe.config.Config c, java.lang.String parentPath, $TsCfgValidator $tsCfgValidator) { @@ -26,7 +66,7 @@ public Endpoint(com.typesafe.config.Config c, java.lang.String parentPath, $TsCf this.interface_ = c.hasPathOrNull("interface") ? new Endpoint.Interface_(c.getConfig("interface"), parentPath + "interface.", $tsCfgValidator) : new Endpoint.Interface_(com.typesafe.config.ConfigFactory.parseString("interface{}"), parentPath + "interface.", $tsCfgValidator); this.path = $_reqStr(parentPath, c, "path", $tsCfgValidator); this.serial = c.hasPathOrNull("serial") ? c.getInt("serial") : null; - this.url = c.hasPathOrNull("url") ? c.getString("url") : "http://example.net"; + this.url = c.hasPathOrNull("url") ? c.getString("url") : "https://example.net"; } private static int $_reqInt(java.lang.String parentPath, com.typesafe.config.Config c, java.lang.String path, $TsCfgValidator $tsCfgValidator) { if (c == null) return 0; @@ -76,4 +116,3 @@ void validate() { } } } - diff --git a/src/main/scala/tscfg/Main.scala b/src/main/scala/tscfg/Main.scala index 70e02f3a..000236a4 100644 --- a/src/main/scala/tscfg/Main.scala +++ b/src/main/scala/tscfg/Main.scala @@ -42,6 +42,7 @@ object Main { | --scala generate scala code (java) | --scala:bt use backticks (see #30) (false) | --durations use java.time.Duration (false) + | --no-doc do not capture doc comments (see #312) | --all-required assume all properties are required (see #47) | --tpl generate config template (no default) | --tpl.ind template indentation string ("${templateOpts.indent}") @@ -57,6 +58,7 @@ object Main { packageName: String = defaultGenOpts.packageName, className: String = defaultGenOpts.className, destDir: String = defaultDestDir, + genDoc: Boolean = true, assumeAllRequired: Boolean = false, language: String = "java", useBackticks: Boolean = false, @@ -121,6 +123,9 @@ object Main { traverseList(rest, opts.copy(destDir = destDir)) else None + case "--no-doc" :: rest => + traverseList(rest, opts.copy(genDoc = false)) + case "--all-required" :: rest => traverseList(rest, opts.copy(assumeAllRequired = true)) @@ -197,6 +202,7 @@ object Main { val genOpts = GenOpts( opts.packageName, opts.className, + genDoc = opts.genDoc, assumeAllRequired = opts.assumeAllRequired, useBackticks = opts.useBackticks, genGetters = opts.genGetters, diff --git a/src/main/scala/tscfg/ModelBuilder.scala b/src/main/scala/tscfg/ModelBuilder.scala index efd5f1dc..8de966c3 100644 --- a/src/main/scala/tscfg/ModelBuilder.scala +++ b/src/main/scala/tscfg/ModelBuilder.scala @@ -112,6 +112,7 @@ class ModelBuilder( effOptional, effDefault, childStruct.defineCaseOpt, + docComments = childStruct.docComments, commentsOpt, parentClassMembers, ) @@ -167,6 +168,7 @@ class ModelBuilder( effOptional: Boolean, effDefault: Option[String], defineCase: Option[DefineCase], + docComments: List[String], commentsOpt: Option[String], parentClassMembers: Option[Map[String, model.AnnType]] ): AnnType = { @@ -192,6 +194,7 @@ class ModelBuilder( optional = effOptional, default = effDefault, defineCase = defineCase, + docComments = docComments, comments = commentsOpt, parentClassMembers = parentClassMembers.map(_.toMap), ) @@ -369,7 +372,7 @@ object ModelBuilder { } /** build model from TS Config object */ - def fromConfig( + private def fromConfig( rootNamespace: NamespaceMan, config: Config, assumeAllRequired: Boolean = false diff --git a/src/main/scala/tscfg/Struct.scala b/src/main/scala/tscfg/Struct.scala index 6159659e..c36850ce 100644 --- a/src/main/scala/tscfg/Struct.scala +++ b/src/main/scala/tscfg/Struct.scala @@ -37,6 +37,13 @@ case class Struct( val comments: List[String] = cv.origin().comments().asScala.toList + val docComments: List[String] = + comments + .map(_.trim) + .filterNot(c => + c.startsWith("@") || c.startsWith("!") || c.startsWith("GenOpts:") + ) + // Non-None when this is a `@define` val defineCaseOpt: Option[DefineCase] = { val defineLines = comments.map(_.trim).filter(_.startsWith("@define")) diff --git a/src/main/scala/tscfg/example/ScalaExampleCfg.scala b/src/main/scala/tscfg/example/ScalaExampleCfg.scala index efa1a056..1c83c530 100644 --- a/src/main/scala/tscfg/example/ScalaExampleCfg.scala +++ b/src/main/scala/tscfg/example/ScalaExampleCfg.scala @@ -1,4 +1,4 @@ -// generated by tscfg 0.9.994 on Sat Oct 30 17:07:40 PDT 2021 +// generated by tscfg 1.2.0 on Tue Dec 17 21:46:40 PST 2024 // source: src/main/tscfg/example/example.spec.conf package tscfg.example @@ -7,6 +7,20 @@ final case class ScalaExampleCfg( endpoint: ScalaExampleCfg.Endpoint ) object ScalaExampleCfg { + + /** Description of the required endpoint section. + * + * @param serial + * an optional Integer with default value null + * @param path + * a required String + * @param url + * a String with default value "https://example.net" + * @param interface + * Interface definition + * @param intReq + * a required int + */ final case class Endpoint( intReq: scala.Int, interface: ScalaExampleCfg.Endpoint.Interface, @@ -15,6 +29,14 @@ object ScalaExampleCfg { url: java.lang.String ) object Endpoint { + + /** Interface definition + * + * @param `type` + * Interface type + * @param port + * an int with default value 8080 + */ final case class Interface( port: scala.Int, `type`: scala.Option[java.lang.String] @@ -51,7 +73,7 @@ object ScalaExampleCfg { if (c.hasPathOrNull("serial")) Some(c.getInt("serial")) else None, url = if (c.hasPathOrNull("url")) c.getString("url") - else "http://example.net" + else "https://example.net" ) } private def $_reqInt( diff --git a/src/main/scala/tscfg/files.scala b/src/main/scala/tscfg/files.scala new file mode 100644 index 00000000..43c726c1 --- /dev/null +++ b/src/main/scala/tscfg/files.scala @@ -0,0 +1,15 @@ +package tscfg + +import java.io.File +import java.nio.charset.StandardCharsets +import java.nio.file.{Files, Path} +import scala.util.{Try, Using} + +object files { + def readFile(file: File): Try[String] = + Using(io.Source.fromFile(file))(_.mkString) + + def writeFile(path: Path, content: String): Try[Unit] = Try { + Files.writeString(path, content, StandardCharsets.UTF_8) + } +} diff --git a/src/main/scala/tscfg/gen4tests.scala b/src/main/scala/tscfg/gen4tests.scala index 4d175c13..53f1231e 100644 --- a/src/main/scala/tscfg/gen4tests.scala +++ b/src/main/scala/tscfg/gen4tests.scala @@ -74,6 +74,9 @@ object gen4tests { case opt @ "--java:getters" => OptFromFile(opt, Some("java")) + case opt @ "--java" => + OptFromFile(opt, Some("java")) + case opt @ "--java:records" => OptFromFile(opt, Some("java")) @@ -83,6 +86,9 @@ object gen4tests { case opt @ "--durations" => OptFromFile(opt, None) + case opt @ "--no-doc" => + OptFromFile(opt, None) + case opt @ "--all-required" => OptFromFile(opt, None) @@ -111,6 +117,8 @@ object gen4tests { case "--scala:bt" => genOpts = genOpts.copy(useBackticks = true) + case "--java" => () + case "--java:getters" => genOpts = genOpts.copy(genGetters = true) case "--java:records" => genOpts = genOpts.copy(genRecords = true) @@ -122,6 +130,9 @@ object gen4tests { case "--all-required" => genOpts = genOpts.copy(assumeAllRequired = true) + case "--no-doc" => + genOpts = genOpts.copy(genDoc = false) + case opt => warn(s"$confFile: ignoring unrecognized GenOpts argument: `$opt'") } diff --git a/src/main/scala/tscfg/generators/Generator.scala b/src/main/scala/tscfg/generators/Generator.scala index b2fe4de4..f1ed48c3 100644 --- a/src/main/scala/tscfg/generators/Generator.scala +++ b/src/main/scala/tscfg/generators/Generator.scala @@ -27,6 +27,7 @@ abstract class Generator(genOpts: GenOpts) { case class GenOpts( packageName: String, className: String, + genDoc: Boolean = true, assumeAllRequired: Boolean = false, useBackticks: Boolean = false, genGetters: Boolean = false, diff --git a/src/main/scala/tscfg/generators/docUtil.scala b/src/main/scala/tscfg/generators/docUtil.scala new file mode 100644 index 00000000..2bd65d18 --- /dev/null +++ b/src/main/scala/tscfg/generators/docUtil.scala @@ -0,0 +1,72 @@ +package tscfg.generators + +import tscfg.model.{AnnType, ObjectType} + +object docUtil { + def getDoc( + a: AnnType, + genOpts: GenOpts, + symbol2id: String => String, + onlyField: Boolean = false, + genScala: Boolean = false, + indent: String = "", + ): String = { + if (!genOpts.genDoc) "" + else { + val withParams = genScala || genOpts.genRecords + a.t match { + case ot: ObjectType => + val paramDocs = if (withParams) { + // only reflect params with comments + ot.members.toList.flatMap { case (k, memberAnnType) => + val paramComments = memberAnnType.docComments + if (paramComments.isEmpty) None + else Some(ParamDoc(symbol2id(k), paramComments)) + } + } + else Nil + if (a.docComments.isEmpty && paramDocs.isEmpty) "" + else formatDocComment(a.docComments, paramDocs, genScala, indent) + + case _ if onlyField => + if (a.docComments.isEmpty) "" + else formatDocComment(a.docComments, Nil, genScala, indent) + + case _ => "" + } + } + } + + private case class ParamDoc(name: String, docLines: List[String]) + + private def formatDocComment( + docComments: List[String], + paramDocs: List[ParamDoc], + genScala: Boolean = false, + indent: String, + ): String = { + val lines = collection.mutable.ArrayBuffer[String]() + val (start, sep, end) = if (genScala) { + ("\n/** ", "\n * ", "\n */\n") + } + else { + lines += "" + ("\n/**", "\n * ", "\n */\n") + } + lines.addAll(docComments.map(_.trim).filter(_.nonEmpty)) + if (paramDocs.nonEmpty) { + lines += "" + paramDocs foreach { pd => + lines += s"@param ${pd.name}" + pd.docLines.foreach(lines += " " + _) + } + } + val res = lines.map(escapeForDoc).mkString(start, sep, end) + if (res.isEmpty) "" + else indent + res.replaceAll("\n", "\n" + indent) + } + + private def escapeForDoc(content: String): String = content + .replace("/*", "/\\*") // start of comment + .replace("*/", "*\\/") // end of comment +} diff --git a/src/main/scala/tscfg/generators/java/JavaGen.scala b/src/main/scala/tscfg/generators/java/JavaGen.scala index 69e58001..4f0523c3 100644 --- a/src/main/scala/tscfg/generators/java/JavaGen.scala +++ b/src/main/scala/tscfg/generators/java/JavaGen.scala @@ -80,13 +80,15 @@ class JavaGen( symbols.foreach(methodNames.checkUserSymbol) val results = symbols.map { symbol => - val a = ot.members(symbol) - val res = generate( + val a = ot.members(symbol) + val doc = docUtil.getDoc(a, genOpts, javaIdentifier) + var res = generate( a.t, classNamePrefixOpt = Some(classNameAdjusted + "."), className = javaUtil.getClassName(symbol), annTypeForAbstractClassName = Some(a) ) + res = res.copy(definition = doc + res.definition) (symbol, res, a) } @@ -121,8 +123,17 @@ class JavaGen( .copy(fields = genResults.fields + (javaId -> type_)) if (genOpts.genRecords) s"$type_ $javaId" + dbg("") - else - s"public final $type_ $javaId;" + dbg("") + else { + val doc = + docUtil.getDoc( + a, + genOpts, + javaIdentifier, + onlyField = true, + indent = " " + ) + doc + s"public final $type_ $javaId;" + dbg("") + } } } .mkString(if (genOpts.genRecords) ",\n " else "\n ") @@ -637,6 +648,7 @@ object JavaGen { // $COVERAGE-OFF$ def generate( filename: String, + genDoc: Boolean = true, showOut: Boolean = false, assumeAllRequired: Boolean = false, genGetters: Boolean = false, @@ -679,6 +691,7 @@ object JavaGen { val genOpts = GenOpts( "tscfg.example", className, + genDoc = genDoc, genGetters = genGetters, genRecords = genRecords, useOptionals = useOptionals, diff --git a/src/main/scala/tscfg/generators/scala/ScalaGen.scala b/src/main/scala/tscfg/generators/scala/ScalaGen.scala index 78adcf21..09f555ff 100644 --- a/src/main/scala/tscfg/generators/scala/ScalaGen.scala +++ b/src/main/scala/tscfg/generators/scala/ScalaGen.scala @@ -132,14 +132,16 @@ class ScalaGen( symbols.foreach(checkUserSymbol) val results = symbols.map { symbol => - val a = ot.members(symbol) - val res = generate( + val a = ot.members(symbol) + val doc = docUtil.getDoc(a, genOpts, scalaIdentifier, genScala = true) + var res = generate( a.t, classNamesPrefix = className + "." :: classNamesPrefix, className = getClassName(symbol), Some(a), a.parentClassMembers, ) + res = res.copy(definition = doc + res.definition) (symbol, res, a, false) } @@ -474,6 +476,7 @@ object ScalaGen { // $COVERAGE-OFF$ def generate( filename: String, + genDoc: Boolean = true, assumeAllRequired: Boolean = false, showOut: Boolean = false, useDurations: Boolean = false, @@ -520,6 +523,7 @@ object ScalaGen { val genOpts = GenOpts( "tscfg.example", className, + genDoc = genDoc, useBackticks = useBackticks, useDurations = useDurations, ) diff --git a/src/main/scala/tscfg/model.scala b/src/main/scala/tscfg/model.scala index 872dbc34..964a9e40 100644 --- a/src/main/scala/tscfg/model.scala +++ b/src/main/scala/tscfg/model.scala @@ -87,6 +87,7 @@ object model { optional: Boolean = false, default: Option[String] = None, defineCase: Option[DefineCase] = None, + docComments: List[String] = Nil, comments: Option[String] = None, parentClassMembers: Option[Map[String, model.AnnType]] = None, ) { diff --git a/src/main/tscfg/example/example.spec.conf b/src/main/tscfg/example/example.spec.conf index 4803e18b..8316fdb7 100644 --- a/src/main/tscfg/example/example.spec.conf +++ b/src/main/tscfg/example/example.spec.conf @@ -1,3 +1,4 @@ +# Description of the required endpoint section. endpoint { # a required String path = "string" @@ -5,16 +6,18 @@ endpoint { # a required int intReq = "int" - # a String with default value "http://example.net" - url = "String | http://example.net" + # a String with default value "https://example.net" + url = "String | https://example.net" # an optional Integer with default value null serial = "int?" + # Interface definition interface { # an int with default value 8080 port = "int | 8080" + # Interface type type = "string?" } } diff --git a/src/main/tscfg/example/issue312a.spec.conf b/src/main/tscfg/example/issue312a.spec.conf new file mode 100644 index 00000000..6b220193 --- /dev/null +++ b/src/main/tscfg/example/issue312a.spec.conf @@ -0,0 +1,24 @@ +// GenOpts: --java:records +#! Comments starting with `!`, `@`, or `GenOpts` are not transferred to output. + +# Description of the required endpoint. +# /* nested doc comment delimiters */ escaped. +endpoint { + # The associated path. + # For example, "/home/foo/bar" + path = "string" + + # Port for the endpoint service. + port = "int | 8080" + + # Configuration for notifications. + notification { + # Emails to send notifications to. + emails = [{ + # The email address. + email = "string" + # The name of the recipient. + name = "string?" + }] + } +} diff --git a/src/main/tscfg/example/issue312b.spec.conf b/src/main/tscfg/example/issue312b.spec.conf new file mode 100644 index 00000000..bc4a88a2 --- /dev/null +++ b/src/main/tscfg/example/issue312b.spec.conf @@ -0,0 +1,25 @@ +# Like issue312a but only java generation and with regular classes (no records). +// GenOpts: --java +#! Comments starting with `!`, `@`, or `GenOpts` are not transferred to output. + +# Description of the required endpoint. +# /* nested doc comment delimiters */ escaped. +endpoint { + # The associated path. + # For example, "/home/foo/bar" + path = "string" + + # Port for the endpoint service. + port = "int | 8080" + + # Configuration for notifications. + notification { + # Emails to send notifications to. + emails = [{ + # The email address. + email = "string" + # The name of the recipient. + name = "string?" + }] + } +} diff --git a/src/test/java/tscfg/example/JavaDuration2Cfg.java b/src/test/java/tscfg/example/JavaDuration2Cfg.java index 88f73e8c..61531472 100644 --- a/src/test/java/tscfg/example/JavaDuration2Cfg.java +++ b/src/test/java/tscfg/example/JavaDuration2Cfg.java @@ -3,6 +3,11 @@ public class JavaDuration2Cfg { public final JavaDuration2Cfg.Durations durations; public static class Durations { + + /** + * optional duration; reported Long (Option[Long] in scala) is null (None) if value is missing + * or whatever is provided converted to days + */ public final java.time.Duration days; public final java.time.Duration duration_dy; public final java.time.Duration duration_hr; @@ -11,7 +16,18 @@ public static class Durations { public final java.time.Duration duration_ns; public final java.time.Duration duration_se; public final java.time.Duration duration_µs; + + /** + * required duration; reported long (Long) is whatever is provided + * converted to hours + */ public final java.time.Duration hours; + + /** + * optional duration with default value; + * reported long (Long) is in milliseconds, either 550,000 if value is missing + * or whatever is provided converted to millis + */ public final java.time.Duration millis; public Durations(com.typesafe.config.Config c, java.lang.String parentPath, $TsCfgValidator $tsCfgValidator) { diff --git a/src/test/java/tscfg/example/JavaDuration3Cfg.java b/src/test/java/tscfg/example/JavaDuration3Cfg.java index 6fb88ddc..e1a611d7 100644 --- a/src/test/java/tscfg/example/JavaDuration3Cfg.java +++ b/src/test/java/tscfg/example/JavaDuration3Cfg.java @@ -4,6 +4,11 @@ public class JavaDuration3Cfg { public final JavaDuration3Cfg.Durations durations; public final JavaDuration3Cfg.Durations getDurations() { return durations; } public static class Durations { + + /** + * optional duration; reported Long (Option[Long] in scala) is null (None) if value is missing + * or whatever is provided converted to days + */ public final java.time.Duration days; public final java.time.Duration duration_dy; public final java.time.Duration duration_hr; @@ -12,7 +17,18 @@ public static class Durations { public final java.time.Duration duration_ns; public final java.time.Duration duration_se; public final java.time.Duration duration_µs; + + /** + * required duration; reported long (Long) is whatever is provided + * converted to hours + */ public final java.time.Duration hours; + + /** + * optional duration with default value; + * reported long (Long) is in milliseconds, either 550,000 if value is missing + * or whatever is provided converted to millis + */ public final java.time.Duration millis; public final java.time.Duration getDays() { return days; } public final java.time.Duration getDuration_dy() { return duration_dy; } diff --git a/src/test/java/tscfg/example/JavaDurationCfg.java b/src/test/java/tscfg/example/JavaDurationCfg.java index 3ccd3b27..1326b288 100644 --- a/src/test/java/tscfg/example/JavaDurationCfg.java +++ b/src/test/java/tscfg/example/JavaDurationCfg.java @@ -3,6 +3,11 @@ public class JavaDurationCfg { public final JavaDurationCfg.Durations durations; public static class Durations { + + /** + * optional duration; reported Long (Option[Long] in scala) is null (None) if value is missing + * or whatever is provided converted to days + */ public final java.lang.Long days; public final long duration_dy; public final long duration_hr; @@ -11,7 +16,18 @@ public static class Durations { public final long duration_ns; public final long duration_se; public final long duration_µs; + + /** + * required duration; reported long (Long) is whatever is provided + * converted to hours + */ public final long hours; + + /** + * optional duration with default value; + * reported long (Long) is in milliseconds, either 550,000 if value is missing + * or whatever is provided converted to millis + */ public final long millis; public Durations(com.typesafe.config.Config c, java.lang.String parentPath, $TsCfgValidator $tsCfgValidator) { diff --git a/src/test/java/tscfg/example/JavaExample1Cfg.java b/src/test/java/tscfg/example/JavaExample1Cfg.java index 87a90d47..774e10d0 100644 --- a/src/test/java/tscfg/example/JavaExample1Cfg.java +++ b/src/test/java/tscfg/example/JavaExample1Cfg.java @@ -1,6 +1,10 @@ package tscfg.example; public class JavaExample1Cfg { + + /** + * same as: "string | hello" + */ public final java.lang.String bazOptionalWithDefault; public final java.lang.String bazOptionalWithNoDefault; public final java.lang.String fooRequired; diff --git a/src/test/java/tscfg/example/JavaExample4tplCfg.java b/src/test/java/tscfg/example/JavaExample4tplCfg.java index bb79f213..be7fed49 100644 --- a/src/test/java/tscfg/example/JavaExample4tplCfg.java +++ b/src/test/java/tscfg/example/JavaExample4tplCfg.java @@ -1,13 +1,46 @@ package tscfg.example; public class JavaExample4tplCfg { + + /** + * Description of the required endpoint section. + */ public final JavaExample4tplCfg.Endpoint endpoint; + + /** + * Description of the required endpoint section. + */ public static class Endpoint { + + /** + * Configuration for notifications + */ public final Endpoint.Notifications notifications; + + /** + * The path associated with the endpoint. + * For example, "/home/foo/bar" + */ public final java.lang.String path; + + /** + * Port for the endpoint service. + */ public final int port; + + /** + * Some optional stuff. + */ public final Endpoint.Stuff stuff; + + /** + * Configuration for notifications + */ public static class Notifications { + + /** + * Emails to send notifications to. + */ public final java.util.List emails; public static class Emails$Elm { public final java.lang.String email; @@ -42,7 +75,15 @@ public Notifications(com.typesafe.config.Config c, java.lang.String parentPath, } } + + /** + * Some optional stuff. + */ public static class Stuff { + + /** + * Coeficient matrix + */ public final java.util.List> coefs; public final int port2; diff --git a/src/test/java/tscfg/example/JavaExampleCfg.java b/src/test/java/tscfg/example/JavaExampleCfg.java index 15b2fb1a..c513775d 100644 --- a/src/test/java/tscfg/example/JavaExampleCfg.java +++ b/src/test/java/tscfg/example/JavaExampleCfg.java @@ -1,15 +1,55 @@ package tscfg.example; public class JavaExampleCfg { + + /** + * Description of the required endpoint section. + */ public final JavaExampleCfg.Endpoint endpoint; + + /** + * Description of the required endpoint section. + */ public static class Endpoint { + + /** + * a required int + */ public final int intReq; + + /** + * Interface definition + */ public final Endpoint.Interface_ interface_; + + /** + * a required String + */ public final java.lang.String path; + + /** + * an optional Integer with default value null + */ public final java.lang.Integer serial; + + /** + * a String with default value "https://example.net" + */ public final java.lang.String url; + + /** + * Interface definition + */ public static class Interface_ { + + /** + * an int with default value 8080 + */ public final int port; + + /** + * Interface type + */ public final java.lang.String type; public Interface_(com.typesafe.config.Config c, java.lang.String parentPath, $TsCfgValidator $tsCfgValidator) { @@ -23,7 +63,7 @@ public Endpoint(com.typesafe.config.Config c, java.lang.String parentPath, $TsCf this.interface_ = c.hasPathOrNull("interface") ? new Endpoint.Interface_(c.getConfig("interface"), parentPath + "interface.", $tsCfgValidator) : new Endpoint.Interface_(com.typesafe.config.ConfigFactory.parseString("interface{}"), parentPath + "interface.", $tsCfgValidator); this.path = $_reqStr(parentPath, c, "path", $tsCfgValidator); this.serial = c.hasPathOrNull("serial") ? c.getInt("serial") : null; - this.url = c.hasPathOrNull("url") ? c.getString("url") : "http://example.net"; + this.url = c.hasPathOrNull("url") ? c.getString("url") : "https://example.net"; } private static int $_reqInt(java.lang.String parentPath, com.typesafe.config.Config c, java.lang.String path, $TsCfgValidator $tsCfgValidator) { if (c == null) return 0; diff --git a/src/test/java/tscfg/example/JavaIssue10Cfg.java b/src/test/java/tscfg/example/JavaIssue10Cfg.java index aa948414..fc02dcdc 100644 --- a/src/test/java/tscfg/example/JavaIssue10Cfg.java +++ b/src/test/java/tscfg/example/JavaIssue10Cfg.java @@ -3,8 +3,16 @@ public class JavaIssue10Cfg { public final JavaIssue10Cfg.Main main; public static class Main { + + /** + * Mail server properties if you want to enable notifications to users + */ public final Main.Email email; public final java.util.List reals; + + /** + * Mail server properties if you want to enable notifications to users + */ public static class Email { public final java.lang.String password; public final java.lang.String server; diff --git a/src/test/java/tscfg/example/JavaIssue23Cfg.java b/src/test/java/tscfg/example/JavaIssue23Cfg.java index 3dd4dfbe..a7853399 100644 --- a/src/test/java/tscfg/example/JavaIssue23Cfg.java +++ b/src/test/java/tscfg/example/JavaIssue23Cfg.java @@ -1,10 +1,30 @@ package tscfg.example; public class JavaIssue23Cfg { + + /** + * optional size, no default + */ public final java.lang.Long sizeOpt; + + /** + * optional size with default value 1024 bytes + */ public final long sizeOptDef; + + /** + * required size + */ public final long sizeReq; + + /** + * list of sizes + */ public final java.util.List sizes; + + /** + * list of lists of sizes + */ public final java.util.List> sizes2; public JavaIssue23Cfg(com.typesafe.config.Config c) { diff --git a/src/test/java/tscfg/example/JavaIssue29Cfg.java b/src/test/java/tscfg/example/JavaIssue29Cfg.java index 618063c3..eec265f9 100644 --- a/src/test/java/tscfg/example/JavaIssue29Cfg.java +++ b/src/test/java/tscfg/example/JavaIssue29Cfg.java @@ -1,6 +1,10 @@ package tscfg.example; public class JavaIssue29Cfg { + + /** + * 测试 is test in Chinese. + */ public final java.lang.String test; public JavaIssue29Cfg(com.typesafe.config.Config c) { diff --git a/src/test/java/tscfg/example/JavaIssue309aCfg.java b/src/test/java/tscfg/example/JavaIssue309aCfg.java index 45aa8fb2..edf4caf9 100644 --- a/src/test/java/tscfg/example/JavaIssue309aCfg.java +++ b/src/test/java/tscfg/example/JavaIssue309aCfg.java @@ -1,8 +1,20 @@ package tscfg.example; public class JavaIssue309aCfg { + + /** + * comment1 + */ public final JavaIssue309aCfg.EmptyObj emptyObj; + + /** + * comment2 + */ public final int other; + + /** + * comment1 + */ public static class EmptyObj { diff --git a/src/test/java/tscfg/example/JavaIssue312aCfg.java b/src/test/java/tscfg/example/JavaIssue312aCfg.java new file mode 100644 index 00000000..9fd59aa6 --- /dev/null +++ b/src/test/java/tscfg/example/JavaIssue312aCfg.java @@ -0,0 +1,117 @@ +package tscfg.example; + +public record JavaIssue312aCfg( + JavaIssue312aCfg.Endpoint endpoint +) { + static final $TsCfgValidator $tsCfgValidator = new $TsCfgValidator(); + + + /** + * Description of the required endpoint. + * /\* nested doc comment delimiters *\/ escaped. + * + * @param notification + * Configuration for notifications. + * @param path + * The associated path. + * For example, "/home/foo/bar" + * @param port + * Port for the endpoint service. + */ + public static record Endpoint( + Endpoint.Notification notification, + java.lang.String path, + int port + ) { + + /** + * Configuration for notifications. + * + * @param emails + * Emails to send notifications to. + */ + public static record Notification( + java.util.List emails + ) { + public static record Emails$Elm( + java.lang.String email, + java.lang.String name + ) { + + public Emails$Elm(com.typesafe.config.Config c, java.lang.String parentPath, $TsCfgValidator $tsCfgValidator) { + this( + $_reqStr(parentPath, c, "email", $tsCfgValidator), + c.hasPathOrNull("name") ? c.getString("name") : null + ); + } + 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 Notification(com.typesafe.config.Config c, java.lang.String parentPath, $TsCfgValidator $tsCfgValidator) { + this( + $_LNotification_Emails$Elm(c.getList("emails"), parentPath, $tsCfgValidator) + ); + } + private static java.util.List $_LNotification_Emails$Elm(com.typesafe.config.ConfigList cl, java.lang.String parentPath, $TsCfgValidator $tsCfgValidator) { + java.util.ArrayList al = new java.util.ArrayList<>(); + for (com.typesafe.config.ConfigValue cv: cl) { + al.add(new Notification.Emails$Elm(((com.typesafe.config.ConfigObject)cv).toConfig(), parentPath, $tsCfgValidator)); + } + return java.util.Collections.unmodifiableList(al); + } + } + + public Endpoint(com.typesafe.config.Config c, java.lang.String parentPath, $TsCfgValidator $tsCfgValidator) { + this( + c.hasPathOrNull("notification") ? new Endpoint.Notification(c.getConfig("notification"), parentPath + "notification.", $tsCfgValidator) : new Endpoint.Notification(com.typesafe.config.ConfigFactory.parseString("notification{}"), parentPath + "notification.", $tsCfgValidator), + $_reqStr(parentPath, c, "path", $tsCfgValidator), + c.hasPathOrNull("port") ? c.getInt("port") : 8080 + ); + } + 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 JavaIssue312aCfg(com.typesafe.config.Config c) { + this( + c.hasPathOrNull("endpoint") ? new JavaIssue312aCfg.Endpoint(c.getConfig("endpoint"), "endpoint.", $tsCfgValidator) : new JavaIssue312aCfg.Endpoint(com.typesafe.config.ConfigFactory.parseString("endpoint{}"), "endpoint.", $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/java/tscfg/example/JavaIssue312bCfg.java b/src/test/java/tscfg/example/JavaIssue312bCfg.java new file mode 100644 index 00000000..7afe83f0 --- /dev/null +++ b/src/test/java/tscfg/example/JavaIssue312bCfg.java @@ -0,0 +1,124 @@ +package tscfg.example; + +public class JavaIssue312bCfg { + + /** + * Description of the required endpoint. + * /\* nested doc comment delimiters *\/ escaped. + */ + public final JavaIssue312bCfg.Endpoint endpoint; + + /** + * Description of the required endpoint. + * /\* nested doc comment delimiters *\/ escaped. + */ + public static class Endpoint { + + /** + * Configuration for notifications. + */ + public final Endpoint.Notification notification; + + /** + * The associated path. + * For example, "/home/foo/bar" + */ + public final java.lang.String path; + + /** + * Port for the endpoint service. + */ + public final int port; + + /** + * Configuration for notifications. + */ + public static class Notification { + + /** + * Emails to send notifications to. + */ + public final java.util.List emails; + public static class Emails$Elm { + + /** + * The email address. + */ + public final java.lang.String email; + + /** + * The name of the recipient. + */ + public final java.lang.String name; + + public Emails$Elm(com.typesafe.config.Config c, java.lang.String parentPath, $TsCfgValidator $tsCfgValidator) { + this.email = $_reqStr(parentPath, c, "email", $tsCfgValidator); + this.name = c.hasPathOrNull("name") ? c.getString("name") : null; + } + 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 Notification(com.typesafe.config.Config c, java.lang.String parentPath, $TsCfgValidator $tsCfgValidator) { + this.emails = $_LNotification_Emails$Elm(c.getList("emails"), parentPath, $tsCfgValidator); + } + private static java.util.List $_LNotification_Emails$Elm(com.typesafe.config.ConfigList cl, java.lang.String parentPath, $TsCfgValidator $tsCfgValidator) { + java.util.ArrayList al = new java.util.ArrayList<>(); + for (com.typesafe.config.ConfigValue cv: cl) { + al.add(new Notification.Emails$Elm(((com.typesafe.config.ConfigObject)cv).toConfig(), parentPath, $tsCfgValidator)); + } + return java.util.Collections.unmodifiableList(al); + } + } + + public Endpoint(com.typesafe.config.Config c, java.lang.String parentPath, $TsCfgValidator $tsCfgValidator) { + this.notification = c.hasPathOrNull("notification") ? new Endpoint.Notification(c.getConfig("notification"), parentPath + "notification.", $tsCfgValidator) : new Endpoint.Notification(com.typesafe.config.ConfigFactory.parseString("notification{}"), parentPath + "notification.", $tsCfgValidator); + this.path = $_reqStr(parentPath, c, "path", $tsCfgValidator); + this.port = c.hasPathOrNull("port") ? c.getInt("port") : 8080; + } + 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 JavaIssue312bCfg(com.typesafe.config.Config c) { + final $TsCfgValidator $tsCfgValidator = new $TsCfgValidator(); + final java.lang.String parentPath = ""; + this.endpoint = c.hasPathOrNull("endpoint") ? new JavaIssue312bCfg.Endpoint(c.getConfig("endpoint"), parentPath + "endpoint.", $tsCfgValidator) : new JavaIssue312bCfg.Endpoint(com.typesafe.config.ConfigFactory.parseString("endpoint{}"), parentPath + "endpoint.", $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/java/tscfg/example/JavaIssue50Cfg.java b/src/test/java/tscfg/example/JavaIssue50Cfg.java index 2a969cdb..09fae531 100644 --- a/src/test/java/tscfg/example/JavaIssue50Cfg.java +++ b/src/test/java/tscfg/example/JavaIssue50Cfg.java @@ -1,6 +1,10 @@ package tscfg.example; public class JavaIssue50Cfg { + + /** + * not supported yet, see #50 + */ public final int parameter; public JavaIssue50Cfg(com.typesafe.config.Config c) { diff --git a/src/test/scala/tscfg/example/ScalaDuration2Cfg.scala b/src/test/scala/tscfg/example/ScalaDuration2Cfg.scala index fd57e1a2..21f96610 100644 --- a/src/test/scala/tscfg/example/ScalaDuration2Cfg.scala +++ b/src/test/scala/tscfg/example/ScalaDuration2Cfg.scala @@ -4,6 +4,19 @@ final case class ScalaDuration2Cfg( durations : ScalaDuration2Cfg.Durations ) object ScalaDuration2Cfg { + + /** + * @param millis + * optional duration with default value; + * reported long (Long) is in milliseconds, either 550,000 if value is missing + * or whatever is provided converted to millis + * @param days + * optional duration; reported Long (Option[Long] in scala) is null (None) if value is missing + * or whatever is provided converted to days + * @param hours + * required duration; reported long (Long) is whatever is provided + * converted to hours + */ final case class Durations( days : scala.Option[java.time.Duration], duration_dy : java.time.Duration, diff --git a/src/test/scala/tscfg/example/ScalaDurationCfg.scala b/src/test/scala/tscfg/example/ScalaDurationCfg.scala index 5b2033e4..2a286ce9 100644 --- a/src/test/scala/tscfg/example/ScalaDurationCfg.scala +++ b/src/test/scala/tscfg/example/ScalaDurationCfg.scala @@ -4,6 +4,19 @@ final case class ScalaDurationCfg( durations : ScalaDurationCfg.Durations ) object ScalaDurationCfg { + + /** + * @param millis + * optional duration with default value; + * reported long (Long) is in milliseconds, either 550,000 if value is missing + * or whatever is provided converted to millis + * @param days + * optional duration; reported Long (Option[Long] in scala) is null (None) if value is missing + * or whatever is provided converted to days + * @param hours + * required duration; reported long (Long) is whatever is provided + * converted to hours + */ final case class Durations( days : scala.Option[scala.Long], duration_dy : scala.Long, diff --git a/src/test/scala/tscfg/example/ScalaExample4tplCfg.scala b/src/test/scala/tscfg/example/ScalaExample4tplCfg.scala index db08f9b7..195a67f8 100644 --- a/src/test/scala/tscfg/example/ScalaExample4tplCfg.scala +++ b/src/test/scala/tscfg/example/ScalaExample4tplCfg.scala @@ -4,6 +4,19 @@ final case class ScalaExample4tplCfg( endpoint : ScalaExample4tplCfg.Endpoint ) object ScalaExample4tplCfg { + + /** Description of the required endpoint section. + * + * @param path + * The path associated with the endpoint. + * For example, "/home/foo/bar" + * @param stuff + * Some optional stuff. + * @param port + * Port for the endpoint service. + * @param notifications + * Configuration for notifications + */ final case class Endpoint( notifications : ScalaExample4tplCfg.Endpoint.Notifications, path : java.lang.String, @@ -11,6 +24,12 @@ object ScalaExample4tplCfg { stuff : scala.Option[ScalaExample4tplCfg.Endpoint.Stuff] ) object Endpoint { + + /** Configuration for notifications + * + * @param emails + * Emails to send notifications to. + */ final case class Notifications( emails : scala.List[ScalaExample4tplCfg.Endpoint.Notifications.Emails$Elm] ) @@ -49,6 +68,12 @@ object ScalaExample4tplCfg { } } + + /** Some optional stuff. + * + * @param coefs + * Coeficient matrix + */ final case class Stuff( coefs : scala.List[scala.List[scala.Double]], port2 : scala.Int diff --git a/src/test/scala/tscfg/example/ScalaExampleCfg.scala b/src/test/scala/tscfg/example/ScalaExampleCfg.scala index 5230f0c6..27e46bc5 100644 --- a/src/test/scala/tscfg/example/ScalaExampleCfg.scala +++ b/src/test/scala/tscfg/example/ScalaExampleCfg.scala @@ -4,6 +4,20 @@ final case class ScalaExampleCfg( endpoint : ScalaExampleCfg.Endpoint ) object ScalaExampleCfg { + + /** Description of the required endpoint section. + * + * @param serial + * an optional Integer with default value null + * @param path + * a required String + * @param url + * a String with default value "https://example.net" + * @param interface + * Interface definition + * @param intReq + * a required int + */ final case class Endpoint( intReq : scala.Int, interface : ScalaExampleCfg.Endpoint.Interface, @@ -12,6 +26,14 @@ object ScalaExampleCfg { url : java.lang.String ) object Endpoint { + + /** Interface definition + * + * @param `type` + * Interface type + * @param port + * an int with default value 8080 + */ final case class Interface( port : scala.Int, `type` : scala.Option[java.lang.String] @@ -31,7 +53,7 @@ object ScalaExampleCfg { interface = ScalaExampleCfg.Endpoint.Interface(if(c.hasPathOrNull("interface")) c.getConfig("interface") else com.typesafe.config.ConfigFactory.parseString("interface{}"), parentPath + "interface.", $tsCfgValidator), path = $_reqStr(parentPath, c, "path", $tsCfgValidator), serial = if(c.hasPathOrNull("serial")) Some(c.getInt("serial")) else None, - url = if(c.hasPathOrNull("url")) c.getString("url") else "http://example.net" + url = if(c.hasPathOrNull("url")) c.getString("url") else "https://example.net" ) } private def $_reqInt(parentPath: java.lang.String, c: com.typesafe.config.Config, path: java.lang.String, $tsCfgValidator: $TsCfgValidator): scala.Int = { diff --git a/src/test/scala/tscfg/example/ScalaIssue10Cfg.scala b/src/test/scala/tscfg/example/ScalaIssue10Cfg.scala index 17c74a13..8d961f76 100644 --- a/src/test/scala/tscfg/example/ScalaIssue10Cfg.scala +++ b/src/test/scala/tscfg/example/ScalaIssue10Cfg.scala @@ -4,11 +4,19 @@ final case class ScalaIssue10Cfg( main : ScalaIssue10Cfg.Main ) object ScalaIssue10Cfg { + + /** + * @param email + * Mail server properties if you want to enable notifications to users + */ final case class Main( email : scala.Option[ScalaIssue10Cfg.Main.Email], reals : scala.Option[scala.List[ScalaIssue10Cfg.Main.Reals$Elm]] ) object Main { + + /** Mail server properties if you want to enable notifications to users + */ final case class Email( password : java.lang.String, server : java.lang.String diff --git a/src/test/scala/tscfg/example/ScalaIssue309aCfg.scala b/src/test/scala/tscfg/example/ScalaIssue309aCfg.scala index 52da9080..799ab4f1 100644 --- a/src/test/scala/tscfg/example/ScalaIssue309aCfg.scala +++ b/src/test/scala/tscfg/example/ScalaIssue309aCfg.scala @@ -5,6 +5,9 @@ final case class ScalaIssue309aCfg( other : scala.Int ) object ScalaIssue309aCfg { + + /** comment1 + */ final case class EmptyObj( ) diff --git a/src/test/scala/tscfg/example/ScalaIssue312aCfg.scala b/src/test/scala/tscfg/example/ScalaIssue312aCfg.scala new file mode 100644 index 00000000..ca63fa27 --- /dev/null +++ b/src/test/scala/tscfg/example/ScalaIssue312aCfg.scala @@ -0,0 +1,116 @@ +package tscfg.example + +final case class ScalaIssue312aCfg( + endpoint : ScalaIssue312aCfg.Endpoint +) +object ScalaIssue312aCfg { + + /** Description of the required endpoint. + * /\* nested doc comment delimiters *\/ escaped. + * + * @param notification + * Configuration for notifications. + * @param path + * The associated path. + * For example, "/home/foo/bar" + * @param port + * Port for the endpoint service. + */ + final case class Endpoint( + notification : ScalaIssue312aCfg.Endpoint.Notification, + path : java.lang.String, + port : scala.Int + ) + object Endpoint { + + /** Configuration for notifications. + * + * @param emails + * Emails to send notifications to. + */ + final case class Notification( + emails : scala.List[ScalaIssue312aCfg.Endpoint.Notification.Emails$Elm] + ) + object Notification { + final case class Emails$Elm( + email : java.lang.String, + name : scala.Option[java.lang.String] + ) + object Emails$Elm { + def apply(c: com.typesafe.config.Config, parentPath: java.lang.String, $tsCfgValidator: $TsCfgValidator): ScalaIssue312aCfg.Endpoint.Notification.Emails$Elm = { + ScalaIssue312aCfg.Endpoint.Notification.Emails$Elm( + email = $_reqStr(parentPath, c, "email", $tsCfgValidator), + name = if(c.hasPathOrNull("name")) Some(c.getString("name")) else None + ) + } + 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, parentPath: java.lang.String, $tsCfgValidator: $TsCfgValidator): ScalaIssue312aCfg.Endpoint.Notification = { + ScalaIssue312aCfg.Endpoint.Notification( + emails = $_LScalaIssue312aCfg_Endpoint_Notification_Emails$Elm(c.getList("emails"), parentPath, $tsCfgValidator) + ) + } + private def $_LScalaIssue312aCfg_Endpoint_Notification_Emails$Elm(cl:com.typesafe.config.ConfigList, parentPath: java.lang.String, $tsCfgValidator: $TsCfgValidator): scala.List[ScalaIssue312aCfg.Endpoint.Notification.Emails$Elm] = { + import scala.jdk.CollectionConverters._ + cl.asScala.map(cv => ScalaIssue312aCfg.Endpoint.Notification.Emails$Elm(cv.asInstanceOf[com.typesafe.config.ConfigObject].toConfig, parentPath, $tsCfgValidator)).toList + } + } + + def apply(c: com.typesafe.config.Config, parentPath: java.lang.String, $tsCfgValidator: $TsCfgValidator): ScalaIssue312aCfg.Endpoint = { + ScalaIssue312aCfg.Endpoint( + notification = ScalaIssue312aCfg.Endpoint.Notification(if(c.hasPathOrNull("notification")) c.getConfig("notification") else com.typesafe.config.ConfigFactory.parseString("notification{}"), parentPath + "notification.", $tsCfgValidator), + path = $_reqStr(parentPath, c, "path", $tsCfgValidator), + port = if(c.hasPathOrNull("port")) c.getInt("port") else 8080 + ) + } + 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): ScalaIssue312aCfg = { + val $tsCfgValidator: $TsCfgValidator = new $TsCfgValidator() + val parentPath: java.lang.String = "" + val $result = ScalaIssue312aCfg( + endpoint = ScalaIssue312aCfg.Endpoint(if(c.hasPathOrNull("endpoint")) c.getConfig("endpoint") else com.typesafe.config.ConfigFactory.parseString("endpoint{}"), parentPath + "endpoint.", $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/JavaExampleSpec.scala b/src/test/scala/tscfg/generators/java/JavaExampleSpec.scala index 7d18fc00..1dd1cb0b 100644 --- a/src/test/scala/tscfg/generators/java/JavaExampleSpec.scala +++ b/src/test/scala/tscfg/generators/java/JavaExampleSpec.scala @@ -29,7 +29,7 @@ class JavaExampleSpec extends AnyWordSpec { } "capture default values" in { - assert(cfg.endpoint.url === "http://example.net") + assert(cfg.endpoint.url === "https://example.net") assert(cfg.endpoint.serial === null) } } diff --git a/src/test/scala/tscfg/generators/java/JavaMainSpec.scala b/src/test/scala/tscfg/generators/java/JavaMainSpec.scala index c3fcdeba..33b7eb07 100644 --- a/src/test/scala/tscfg/generators/java/JavaMainSpec.scala +++ b/src/test/scala/tscfg/generators/java/JavaMainSpec.scala @@ -1093,7 +1093,7 @@ class JavaMainSpec extends AnyWordSpec { .resolve() ) - "result in a valid config for scala" in { + "result in a valid config for java" in { val r = JavaGen.generate("example/issue67.spec.conf") assert( r.classNames === Set("JavaIssue67Cfg", "AbstractA", "ImplA", "Test") @@ -1120,7 +1120,7 @@ class JavaMainSpec extends AnyWordSpec { .resolve() ) - "result in a valid config for scala" in { + "result in a valid config for java" in { val r = JavaGen.generate("example/issue67a.spec.conf") assert( r.classNames === Set( @@ -1159,7 +1159,7 @@ class JavaMainSpec extends AnyWordSpec { .resolve() ) - "result in a valid config for scala" in { + "result in a valid config for java" in { val r = JavaGen.generate("example/issue67b.spec.conf") assert( r.classNames === Set( @@ -1459,7 +1459,7 @@ class JavaMainSpec extends AnyWordSpec { } } - "(scala) issue 309a" should { + "(java) issue 309a" should { "generate class for empty object EmptyObj" in { val r = JavaGen.generate("example/issue309a.spec.conf") assert(r.classNames === Set("JavaIssue309aCfg", "EmptyObj")) @@ -1475,7 +1475,7 @@ class JavaMainSpec extends AnyWordSpec { } } - "(scala) issue 309b" should { + "(java) issue 309b" should { "generate class for empty object SomeExtension extending SomeAbstract" in { val r = JavaGen.generate("example/issue309b.spec.conf") assert( @@ -1495,4 +1495,98 @@ class JavaMainSpec extends AnyWordSpec { assert(c.foo.something === "howdy") } } + + "(java) issue 312a - javadoc (record)" should { + "generate expected classes" in { + val r = + JavaGen.generate( + "example/issue312a.spec.conf", + genDoc = true, + genRecords = true + ) + assert( + r.classNames === Set( + "JavaIssue312aCfg", + "Endpoint", + "Notification", + "Emails$Elm", + ) + ) + } + "generate expected javadoc" in { + val file = + new File("src/test/java/tscfg/example/JavaIssue312aCfg.java") + val src = tscfg.files.readFile(file).get + assert(src.contains("* Description of the required endpoint.")) + assert(src.contains("* /\\* nested doc comment delimiters *\\/ escaped.")) + assert(src.contains("* @param notification")) + assert(src.contains("* @param path")) + assert(src.contains("* @param path")) + assert(src.contains("* @param emails")) + } + + "be exercised ok" in { + val c = new JavaIssue312aCfg(ConfigFactory.parseString(""" + |endpoint.path = /api/v1 + |endpoint.notification.emails = [ {email = "foo@x.net"} ] + |""".stripMargin)) + + assert(c.endpoint.path === "/api/v1") + assert(c.endpoint.port === 8080) + val emails = c.endpoint.notification.emails.asScala + assert( + emails === List( + new JavaIssue312aCfg.Endpoint.Notification.Emails$Elm( + "foo@x.net", + null + ) + ) + ) + } + } + + "(java) issue 312b - javadoc (regular class)" should { + "generate expected classes" in { + val r = + JavaGen.generate( + "example/issue312b.spec.conf", + genDoc = true, + genRecords = false + ) + assert( + r.classNames === Set( + "JavaIssue312bCfg", + "Endpoint", + "Notification", + "Emails$Elm", + ) + ) + } + "generate expected javadoc" in { + val file = + new File("src/test/java/tscfg/example/JavaIssue312bCfg.java") + val src = tscfg.files.readFile(file).get + assert(src.contains("* Description of the required endpoint.")) + assert(src.contains("* /\\* nested doc comment delimiters *\\/ escaped.")) + assert(src.contains("* The associated path.")) + assert(src.contains("* For example, \"/home/foo/bar\"")) + assert(src.contains("* Port for the endpoint service.")) + assert(src.contains("* Configuration for notifications.")) + } + + "be exercised ok" in { + val c = new JavaIssue312bCfg(ConfigFactory.parseString(""" + |endpoint.path = /api/v1 + |endpoint.notification.emails = [ {email = "foo@x.net"} ] + |""".stripMargin)) + + assert(c.endpoint.path === "/api/v1") + assert(c.endpoint.port === 8080) + val emails = c.endpoint.notification.emails.asScala + assert(emails.length === 1) + val email = emails.head + assert(email.email === "foo@x.net") + assert(email.name === null) + } + } } diff --git a/src/test/scala/tscfg/generators/scala/ScalaExampleSpec.scala b/src/test/scala/tscfg/generators/scala/ScalaExampleSpec.scala index b901ed64..3261ca11 100644 --- a/src/test/scala/tscfg/generators/scala/ScalaExampleSpec.scala +++ b/src/test/scala/tscfg/generators/scala/ScalaExampleSpec.scala @@ -28,7 +28,7 @@ class ScalaExampleSpec extends AnyWordSpec { } "capture default values" in { - assert(cfg.endpoint.url === "http://example.net") + assert(cfg.endpoint.url === "https://example.net") assert(cfg.endpoint.serial.isEmpty) } } diff --git a/src/test/scala/tscfg/generators/scala/ScalaMainSpec.scala b/src/test/scala/tscfg/generators/scala/ScalaMainSpec.scala index 75e77288..06b46edc 100644 --- a/src/test/scala/tscfg/generators/scala/ScalaMainSpec.scala +++ b/src/test/scala/tscfg/generators/scala/ScalaMainSpec.scala @@ -1344,4 +1344,45 @@ class ScalaMainSpec extends AnyWordSpec { assert(c.foo.something === "howdy") } } + + "(scala) issue 312a - scaladoc" should { + "generate expected classes" in { + val r = + ScalaGen.generate("example/issue312a.spec.conf", genDoc = true) + assert( + r.classNames === Set( + "ScalaIssue312aCfg", + "Endpoint", + "Notification", + "Emails$Elm", + ) + ) + } + "generate expected scaladoc" in { + val file = + new File("src/test/scala/tscfg/example/ScalaIssue312aCfg.scala") + val src = tscfg.files.readFile(file).get + assert(src.contains("/** Description of the required endpoint.")) + assert(src.contains("* /\\* nested doc comment delimiters *\\/ escaped.")) + assert(src.contains("* @param notification")) + assert(src.contains("* @param path")) + assert(src.contains("* @param path")) + assert(src.contains("* @param emails")) + } + + "be exercised ok" in { + val c = ScalaIssue312aCfg(ConfigFactory.parseString(""" + |endpoint.path = /api/v1 + |endpoint.notification.emails = [ {email = "foo@x.net"} ] + |""".stripMargin)) + + assert(c.endpoint.path === "/api/v1") + assert(c.endpoint.port === 8080) + assert( + c.endpoint.notification.emails === List( + ScalaIssue312aCfg.Endpoint.Notification.Emails$Elm("foo@x.net", None) + ) + ) + } + } }