diff --git a/build.sbt b/build.sbt index cb8d11d..d31d8ad 100644 --- a/build.sbt +++ b/build.sbt @@ -1,4 +1,4 @@ -lazy val tscfgVersion = setVersion("0.8.4") +lazy val tscfgVersion = setVersion("0.9.0") organization := "com.github.carueda" name := "tscfg" diff --git a/changelog.md b/changelog.md index 719df4d..a2d35a7 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,11 @@ +2018-02-11 - 0.9.0 + +- resolve #30 "scala: option to use back ticks" +- Adjustments regarding keys with $ and quoted strings: + - key containing $ is left alone (even if it's quoted). + This mainly due to Config restrictions on keys involving $ + - otherwise, the key is unquoted (if quoted of course) + 2018-02-11 - 0.8.4 - internal: use scala 2.12 (but cross compile to 2.11 too) diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf index e61c4c8..d9a15b8 100644 --- a/src/main/resources/application.conf +++ b/src/main/resources/application.conf @@ -1 +1 @@ -tscfg.version = 0.8.4 +tscfg.version = 0.9.0 diff --git a/src/main/scala/tscfg/Main.scala b/src/main/scala/tscfg/Main.scala index c0b2a6b..ea55edc 100644 --- a/src/main/scala/tscfg/Main.scala +++ b/src/main/scala/tscfg/Main.scala @@ -34,6 +34,7 @@ object Main { | --dd ($defaultDestDir) | --j7 generate code for java <= 7 (8) | --scala generate scala code (java) + | --scala:` use backsticks (false) | --java generate java code (the default) | --tpl generate config template (no default) | --tpl.ind template indentation string ("${templateOpts.indent}") @@ -47,6 +48,7 @@ object Main { destDir: String = defaultDestDir, j7: Boolean = false, language: String = "java", + useBacksticks: Boolean = false, tplFilename: Option[String] = None ) @@ -86,6 +88,9 @@ object Main { case "--scala" :: rest => traverseList(rest, opts.copy(language = "scala")) + case "--scala:`" :: rest => + traverseList(rest, opts.copy(useBacksticks = true)) + case "--java" :: rest => traverseList(rest, opts.copy(language = "java")) @@ -127,7 +132,8 @@ object Main { val destFile = new File(destFilename) val out = new PrintWriter(destFile) - val genOpts = GenOpts(opts.packageName, opts.className, opts.j7) + val genOpts = GenOpts(opts.packageName, opts.className, opts.j7, + useBacksticks = opts.useBacksticks) println(s"parsing: $inputFilename") val source = io.Source.fromFile(new File(inputFilename)).mkString.trim diff --git a/src/main/scala/tscfg/ModelBuilder.scala b/src/main/scala/tscfg/ModelBuilder.scala index 0f0943c..05f9417 100644 --- a/src/main/scala/tscfg/ModelBuilder.scala +++ b/src/main/scala/tscfg/ModelBuilder.scala @@ -73,7 +73,11 @@ class ModelBuilder { val optFromComments = comments.exists(_.trim.startsWith("@optional")) val commentsOpt = if (comments.isEmpty) None else Some(comments.mkString("\n")) - name -> model.AnnType(childType, + // per Lightbend Config restrictions involving $, leave the key alone if + // contains $, otherwise unquote the key in case is quoted. + val adjName = if (name.contains("$")) name else name.replaceAll("^\"|\"$", "") + + adjName -> model.AnnType(childType, optional = optional || optFromComments, default = default, comments = commentsOpt diff --git a/src/main/scala/tscfg/generators/Generator.scala b/src/main/scala/tscfg/generators/Generator.scala index 6abae4f..152a06e 100644 --- a/src/main/scala/tscfg/generators/Generator.scala +++ b/src/main/scala/tscfg/generators/Generator.scala @@ -25,7 +25,8 @@ abstract class Generator(genOpts: GenOpts) { */ case class GenOpts(packageName: String, className: String, - j7: Boolean + j7: Boolean, + useBacksticks: Boolean = false ) case class GenResult(code: String = "?", diff --git a/src/main/scala/tscfg/generators/scala/ScalaGen.scala b/src/main/scala/tscfg/generators/scala/ScalaGen.scala index 41ab117..d4c6423 100644 --- a/src/main/scala/tscfg/generators/scala/ScalaGen.scala +++ b/src/main/scala/tscfg/generators/scala/ScalaGen.scala @@ -1,7 +1,6 @@ package tscfg.generators.scala import tscfg.{ModelBuilder, model} -import tscfg.generators.scala.scalaUtil.scalaIdentifier import tscfg.generators._ import tscfg.model._ import tscfg.util.escapeString @@ -14,6 +13,9 @@ class ScalaGen(genOpts: GenOpts) extends Generator(genOpts) { val getter = Getter(hasPath, accessors, methodNames) import methodNames._ + val scalaUtil: ScalaUtil = new ScalaUtil(useBacksticks = genOpts.useBacksticks) + import scalaUtil.{scalaIdentifier, getClassName} + def generate(objectType: ObjectType): GenResult = { genResults = GenResult() @@ -53,11 +55,10 @@ class ScalaGen(genOpts: GenOpts) extends Generator(genOpts) { def padId(id: String) = id + (" " * (padScalaIdLength - id.length)) val results = symbols.map { symbol ⇒ - val scalaId = scalaIdentifier(symbol) val a = ot.members(symbol) val res = generate(a.t, classNamesPrefix = className+"." :: classNamesPrefix, - className = scalaUtil.getClassName(symbol) + className = getClassName(symbol) ) (symbol, res) } @@ -158,7 +159,10 @@ object ScalaGen { import tscfg.util // $COVERAGE-OFF$ - def generate(filename: String, showOut: Boolean = false): GenResult = { + def generate(filename: String, + showOut: Boolean = false, + useBacksticks: Boolean = false + ): GenResult = { val file = new File("src/main/tscfg/" + filename) val source = io.Source.fromFile(file).mkString.trim @@ -182,7 +186,8 @@ object ScalaGen { } } - val genOpts = GenOpts("tscfg.example", className, j7 = false) + val genOpts = GenOpts("tscfg.example", className, j7 = false, + useBacksticks = useBacksticks) val generator = new ScalaGen(genOpts) @@ -440,7 +445,7 @@ private[scala] class Accessors { val methodDef = s""" |private def $methodName(cl:com.typesafe.config.ConfigList): scala.List[$scalaType] = { - | import scala.collection.JavaConverters._ + | import scala.collection.JavaConverters._ | cl.asScala.map(cv => $elem).toList |}""".stripMargin.trim (methodName, methodDef) diff --git a/src/main/scala/tscfg/generators/scala/ScalaUtil.scala b/src/main/scala/tscfg/generators/scala/ScalaUtil.scala new file mode 100644 index 0000000..32c6fa2 --- /dev/null +++ b/src/main/scala/tscfg/generators/scala/ScalaUtil.scala @@ -0,0 +1,101 @@ +package tscfg.generators.scala + +import tscfg.generators.java.javaUtil +import tscfg.util + +/** + * By default [[scalaIdentifier]] uses underscores as needed for various cases + * that need translation of the given identifier to make it valid Scala. + * Similarly, [[getClassName]] also does some related logic. + * + * With this flag set to true, both methods will change their logic to uses + * backsticks instead of replacing or removing the characters that would + * make the resulting identifiers invalid. + * + * @param useBacksticks False by default + */ +class ScalaUtil(useBacksticks: Boolean = false) { + import ScalaUtil._ + + /** + * Returns a valid scala identifier from the given symbol: + * + * - encloses the symbol in backticks if the symbol is a scala reserved word; + * - appends an underscore if the symbol corresponds to a no-arg method in scope; + * - otherwise: + * - returns symbol if it is a valid java identifier + * - otherwise: + * if useBacksticks is true, enclose symbol in backsticks + * otherwise, returns `javaGenerator.javaIdentifier(symbol)` + */ + def scalaIdentifier(symbol: String): String = { + if (scalaReservedWords.contains(symbol)) "`" + symbol + "`" + else if (noArgMethodInScope.contains(symbol)) symbol + "_" + else if (javaUtil.isJavaIdentifier(symbol)) symbol + else if (useBacksticks) "`" + symbol + "`" + else javaUtil.javaIdentifier(symbol) + } + + /** + * Returns a class name from the given symbol. + * If useBacksticks: + * This is basically capitalizing the first character that + * can be capitalized. If none, then a `U` is prepended. + * Otherwise: + * Since underscores are specially used in generated code, + * this method camelizes the symbol in case of any underscores. + */ + def getClassName(symbol: String): String = { + if (useBacksticks) { + val scalaId = scalaIdentifier(symbol) + val search = scalaId.zipWithIndex.find { case (c, _) ⇒ c.toUpper != c } + search match { + case Some((c, index)) ⇒ + scalaId.substring(0, index) + c.toUpper + scalaId.substring(index + 1) + + case None ⇒ + if (scalaId.head == '`') + "`U" + scalaId.substring(1) + else + "U" + scalaId + } + } + else { // preserve behavior until v0.8.4 + // regular javaUtil.javaIdentifier as it might generate underscores: + val id = javaUtil.javaIdentifier(symbol) + // note: not scalaIdentifier because we are going to camelize anyway + val parts = id.split("_") + val name = parts.map(util.upperFirst).mkString + if (name.nonEmpty) scalaIdentifier(name) + else "U" + id.count(_ == '_') // named based on # of underscores ;) + } + } +} + +object ScalaUtil { + /** + * The ones from Sect 1.1 of the Scala Language Spec, v2.9 + * plus `_` + */ + val scalaReservedWords: List[String] = List( + "_", + "abstract", "case", "catch", "class", "def", + "do", "else", "extends", "false", "final", + "finally", "for", "forSome", "if", "implicit", + "import", "lazy", "match", "new", "null", + "object", "override", "package", "private", "protected", + "return", "sealed", "super", "this", "throw", + "trait", "try", "true", "type", "val", + "var", "while", "with", "yield" + ) + + val noArgMethodInScope: List[String] = List( + "clone", + "finalize", + "getClass", + "notify", + "notifyAll", + "toString", + "wait" + ) +} diff --git a/src/main/scala/tscfg/generators/scala/scalaUtil.scala b/src/main/scala/tscfg/generators/scala/scalaUtil.scala deleted file mode 100644 index 2848e65..0000000 --- a/src/main/scala/tscfg/generators/scala/scalaUtil.scala +++ /dev/null @@ -1,63 +0,0 @@ -package tscfg.generators.scala - -import tscfg.generators.java.javaUtil -import tscfg.util - -object scalaUtil { - - /** - * Returns a valid scala identifier from the given symbol: - * - * - encloses the symbol in backticks if the symbol is a scala reserved word; - * - appends an underscore if the symbol corresponds to a no-arg method in scope; - * - otherwise, returns symbol if it is a valid java identifier - * - otherwise, returns `javaGenerator.javaIdentifier(symbol)` - */ - def scalaIdentifier(symbol: String): String = { - if (scalaReservedWords.contains(symbol)) "`" + symbol + "`" - else if (noArgMethodInScope.contains(symbol)) symbol + "_" - else if (javaUtil.isJavaIdentifier(symbol)) symbol - else javaUtil.javaIdentifier(symbol) - } - - /** - * Returns a class name from the given symbol. - * Since underscores are specially used in generated code, - * this method camelizes the symbol in case of any underscores. - */ - def getClassName(symbol: String): String = { - // regular javaUtil.javaIdentifier as it might generate underscores: - val id = javaUtil.javaIdentifier(symbol) - // note: not scalaIdentifier because we are going to camelize anyway - val parts = id.split("_") - val name = parts.map(util.upperFirst).mkString - if (name.nonEmpty) scalaIdentifier(name) - else "U" + id.count(_ == '_') // named based on # of underscores ;) - } - - /** - * The ones from Sect 1.1 of the Scala Language Spec, v2.9 - * plus `_` - */ - val scalaReservedWords: List[String] = List( - "_", - "abstract", "case", "catch", "class", "def", - "do", "else", "extends", "false", "final", - "finally", "for", "forSome", "if", "implicit", - "import", "lazy", "match", "new", "null", - "object", "override", "package", "private", "protected", - "return", "sealed", "super", "this", "throw", - "trait", "try", "true", "type", "val", - "var", "while", "with", "yield" - ) - - val noArgMethodInScope: List[String] = List( - "clone", - "finalize", - "getClass", - "notify", - "notifyAll", - "toString", - "wait" - ) -} diff --git a/src/main/scala/tscfg/model.scala b/src/main/scala/tscfg/model.scala index ea847d0..4697889 100644 --- a/src/main/scala/tscfg/model.scala +++ b/src/main/scala/tscfg/model.scala @@ -74,7 +74,14 @@ object model { def apply(elems: (String, AnnType)*): ObjectType = { val x = elems.groupBy(_._1).mapValues(_.length).filter(_._2 > 1) if (x.nonEmpty) throw new RuntimeException(s"key repeated in object: ${x.head}") - ObjectType(Map(elems : _*)) + + val noQuotes = elems map { case (k, v) ⇒ + // per Lightbend Config restrictions involving $, leave the key alone if + // contains $, otherwise unquote the key in case is quoted. + val adjName = if (k.contains("$")) k else k.replaceAll("^\"|\"$", "") + adjName.replaceAll("^\"|\"$", "") -> v + } + ObjectType(Map(noQuotes : _*)) } } diff --git a/src/main/tscfg/example/issue30.spec.conf b/src/main/tscfg/example/issue30.spec.conf new file mode 100644 index 0000000..6eb70fc --- /dev/null +++ b/src/main/tscfg/example/issue30.spec.conf @@ -0,0 +1,5 @@ +foo-object { + bar-baz: string + 0: string +} +"other#stuff": int diff --git a/src/test/scala/tscfg/generators/java/JavaMainSpec.scala b/src/test/scala/tscfg/generators/java/JavaMainSpec.scala index 15dc940..3e2ad36 100644 --- a/src/test/scala/tscfg/generators/java/JavaMainSpec.scala +++ b/src/test/scala/tscfg/generators/java/JavaMainSpec.scala @@ -299,12 +299,12 @@ class JavaMainSpec extends Specification { } "issue19" should { - """replace leading and trailing " with _""" in { + """put underscores for key having $""" in { val r = JavaGen.generate("example/issue19.spec.conf") r.classNames === Set("JavaIssue19Cfg") r.fields === Map( - "_do_log_" → "boolean", - "_$_foo_" → "java.lang.String" + "do_log" → "boolean", + "_$_foo_" → "java.lang.String" ) } @@ -315,8 +315,8 @@ class JavaMainSpec extends Specification { |"$_foo" : some string """.stripMargin )) - c._do_log_ === true - c._$_foo_ === "some string" + c.do_log === true + c._$_foo_ === "some string" } } diff --git a/src/test/scala/tscfg/generators/scala/ScalaMainSpec.scala b/src/test/scala/tscfg/generators/scala/ScalaMainSpec.scala index b863589..f31c5c7 100644 --- a/src/test/scala/tscfg/generators/scala/ScalaMainSpec.scala +++ b/src/test/scala/tscfg/generators/scala/ScalaMainSpec.scala @@ -303,12 +303,12 @@ class ScalaMainSpec extends Specification { } "issue19" should { - """replace leading and trailing " with _""" in { + """put underscores for key having $""" in { val r = ScalaGen.generate("example/issue19.spec.conf") r.classNames === Set("ScalaIssue19Cfg") r.fields === Map( - "_do_log_" → "scala.Boolean", - "_$_foo_" → "java.lang.String" + "do_log" → "scala.Boolean", + "_$_foo_" → "java.lang.String" ) } @@ -319,8 +319,8 @@ class ScalaMainSpec extends Specification { |"$_foo" : some string """.stripMargin )) - c._do_log_ === true - c._$_foo_ === "some string" + c.do_log === true + c._$_foo_ === "some string" } } @@ -405,4 +405,18 @@ class ScalaMainSpec extends Specification { List(16*1000)) } } + + "issue30" should { + "generate as indicated for useBacksticks" in { + val r = ScalaGen.generate("example/issue30.spec.conf", + useBacksticks = true) + + r.classNames === Set("ScalaIssue30Cfg", "`Foo-object`") + r.fields.size === 4 + r.fields("`foo-object`" ) === "ScalaIssue30Cfg.`Foo-object`" + r.fields("`bar-baz`" ) === "java.lang.String" + r.fields("`0`" ) === "java.lang.String" + r.fields("`other#stuff`") === "scala.Int" + } + } } diff --git a/src/test/scala/tscfg/scalaIdentifierSpec.scala b/src/test/scala/tscfg/scalaIdentifierSpec.scala index c448b76..f791e33 100644 --- a/src/test/scala/tscfg/scalaIdentifierSpec.scala +++ b/src/test/scala/tscfg/scalaIdentifierSpec.scala @@ -2,12 +2,17 @@ package tscfg import org.specs2.mutable.Specification import org.specs2.specification.core.Fragments -import tscfg.generators.scala.scalaUtil.{scalaReservedWords, scalaIdentifier} +import tscfg.generators.scala.ScalaUtil +import tscfg.generators.scala.ScalaUtil.scalaReservedWords + import scala.util.Random object scalaIdentifierSpec extends Specification { """scalaIdentifier""" should { + val scalaUtil: ScalaUtil = new ScalaUtil() + import scalaUtil.scalaIdentifier + List("foo", "bar_3", "$baz").foldLeft(Fragments.empty) { (res, id) => res.append(s"""keep valid identifier "$id"""" in { @@ -32,4 +37,21 @@ object scalaIdentifierSpec extends Specification { scalaIdentifier("21") must_== "_21" } } + + """scalaIdentifier with useBacksticks=true""" should { + val scalaUtil: ScalaUtil = new ScalaUtil(useBacksticks = true) + import scalaUtil.scalaIdentifier + + List("foo-bar", "foo:bar", "foo#bar").foldLeft(Fragments.empty) { (res, id) => + res.append(s"""put non scala id with backsticks: "$id" -> "`$id`"""" in { + scalaIdentifier(id) must_== s"`$id`" + }) + } + + List("0", "1", "3").foldLeft(Fragments.empty) { (res, id) => + res.append(s"""put literal with backsticks: "$id" -> "`$id`"""" in { + scalaIdentifier(id) must_== s"`$id`" + }) + } + } }