Skip to content

Commit ca230b7

Browse files
authored
Resolve #54 Shared config objects (#56)
1 parent cea3be1 commit ca230b7

18 files changed

+548
-69
lines changed

build.sbt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
lazy val tscfgVersion = setVersion("0.9.93")
1+
lazy val tscfgVersion = setVersion("0.9.94")
22

33
organization := "com.github.carueda"
44
name := "tscfg"

changelog.md

+9
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
2019-09-14 - 0.9.94
2+
3+
- resolve #54 "Shared config objects"
4+
- initial implementation
5+
- only exercised with the explicit use of `@define` annotation
6+
- scoping should be handled already
7+
- no support for recursive type
8+
(example spec would look like issue54c.spec.conf)
9+
110
2019-09-03 - 0.9.93
211

312
- fix #55 "Regex not properly captured".

readme.md

+26
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ The generated code only depends on the Typesafe Config library.
2626
- [list type](#list-type)
2727
- [object type](#object-type)
2828
- [optional object or list](#optional-object-or-list)
29+
- [shared object](#shared-object)
2930
- [configuration template](#configuration-template)
3031
- [FAQ](#faq)
3132
- [tests](#tests)
@@ -431,6 +432,31 @@ As with basic types, the meaning of an optional object or list is that the corre
431432
value will be `null` (or `Optional.empty()`) (`None` in Scala) when the corresponding actual entry is missing in
432433
a given configuration instance.
433434
435+
### shared object
436+
437+
As of 0.9.94 there's initial, experimental support for shared objects (#54).
438+
This is exercised by using the `@define` annotation:
439+
440+
```properties
441+
#@define
442+
Struct {
443+
c: string
444+
d: int
445+
}
446+
447+
example {
448+
a: Struct
449+
b: [ Struct ]
450+
}
451+
```
452+
453+
In this example, the annotation will only generate the definition of the
454+
corresponding class `Struct` in the wrapper but not the member of that
455+
type itself. Then, the type can be referenced for other definitions.
456+
457+
> Note: current support is in terms of the referenced object being always
458+
> *required*. `a: "Struct?"`, for example, is not supported.
459+
> Also, `@define` is only supported for an object, not for a basic type or list.
434460

435461
## configuration template
436462

src/main/resources/application.conf

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
tscfg.version = 0.9.93
1+
tscfg.version = 0.9.94

src/main/scala/tscfg/ModelBuilder.scala

+95-35
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package tscfg
22

33
import com.typesafe.config._
44
import tscfg.generators.tsConfigUtil
5-
import tscfg.model.{DURATION, SIZE}
5+
import tscfg.model.{AnnType, DURATION, ObjectType, SIZE}
66
import tscfg.model.durations.ms
77

88
import scala.collection.JavaConverters._
@@ -35,37 +35,53 @@ class ModelBuilder(assumeAllRequired: Boolean = false) {
3535

3636
def build(conf: Config): ModelBuildResult = {
3737
warns.clear()
38-
ModelBuildResult(objectType = fromConfig(conf),
38+
ModelBuildResult(objectType = fromConfig(Namespace.root, conf),
3939
warnings = warns.toList.sortBy(_.line))
4040
}
4141

4242
private val warns = collection.mutable.ArrayBuffer[Warning]()
4343

44-
private def fromConfig(conf: Config): model.ObjectType = {
44+
private def fromConfig(namespace: Namespace, conf: Config): model.ObjectType = {
45+
// do two passes as lightbend config does not necessarily preserve member order:
46+
val ot = fromConfig1(namespace, conf)
47+
fromConfig2(namespace, ot)
48+
}
49+
50+
private def fromConfig1(namespace: Namespace, conf: Config): model.ObjectType = {
4551
val memberStructs = getMemberStructs(conf)
4652
val members: immutable.Map[String, model.AnnType] = memberStructs.map { childStruct
4753
val name = childStruct.name
4854
val cv = conf.getValue(name)
4955

5056
val (childType, optional, default) = {
5157
if (childStruct.isLeaf) {
52-
val typ = fromConfigValue(cv)
5358
val valueString = util.escapeValue(cv.unwrapped().toString)
54-
if (typ == model.STRING) {
55-
toAnnBasicType(valueString) match {
56-
case Some(annBasicType)
57-
annBasicType
58-
case None
59-
(typ, true, Some(valueString))
60-
}
59+
60+
getTypeFromConfigValue(namespace, cv, valueString) match {
61+
case typ: model.STRING.type
62+
namespace.resolveDefine(valueString) match {
63+
case Some(ort)
64+
(ort, false, None)
65+
66+
case None
67+
toAnnBasicType(valueString) match {
68+
case Some(annBasicType)
69+
annBasicType
70+
71+
case None
72+
(typ, true, Some(valueString))
73+
}
74+
}
75+
76+
case typ: model.BasicType
77+
(typ, true, Some(valueString))
78+
79+
case typ
80+
(typ, false, None)
6181
}
62-
else if (typ.isInstanceOf[model.BasicType])
63-
(typ, true, Some(valueString))
64-
else
65-
(typ, false, None)
6682
}
6783
else {
68-
(fromConfig(conf.getConfig(name)), false, None)
84+
(fromConfig(namespace.extend(name), conf.getConfig(name)), false, None)
6985
}
7086
}
7187

@@ -89,15 +105,51 @@ class ModelBuilder(assumeAllRequired: Boolean = false) {
89105
// s"assumeAllRequired=$assumeAllRequired optFromComments=$optFromComments " +
90106
// s"adjName=$adjName")
91107

92-
adjName -> model.AnnType(childType,
108+
val annType = model.AnnType(childType,
93109
optional = effOptional,
94110
default = effDefault,
95111
comments = commentsOpt
96112
)
113+
114+
if (annType.isDefine) {
115+
namespace.addDefine(name, childType)
116+
}
117+
118+
adjName -> annType
119+
97120
}.toMap
98121
model.ObjectType(members)
99122
}
100123

124+
private def fromConfig2(namespace: Namespace, ot: model.ObjectType): model.ObjectType = {
125+
val resolvedMembers = ot.members.map { case (name, annType)
126+
val modAnnType = annType.t match {
127+
128+
case _:model.STRING.type
129+
annType.default match {
130+
case Some(strValue)
131+
namespace.resolveDefine(strValue) match {
132+
case Some(ort) AnnType(ort)
133+
case _ annType
134+
}
135+
136+
case None annType
137+
}
138+
139+
//// the following would be part of changes to allow recursive type
140+
//case ot:ObjectType ⇒
141+
// val ot2 = fromConfig2(namespace, ot)
142+
// AnnType(ot2)
143+
144+
case _ annType
145+
}
146+
147+
name modAnnType
148+
}
149+
150+
model.ObjectType(resolvedMembers)
151+
}
152+
101153
private case class Struct(name: String, members: mutable.HashMap[String, Struct] = mutable.HashMap.empty) {
102154
def isLeaf: Boolean = members.isEmpty
103155
// $COVERAGE-OFF$
@@ -151,14 +203,14 @@ class ModelBuilder(assumeAllRequired: Boolean = false) {
151203
structs("").members.values.toList
152204
}
153205

154-
private def fromConfigValue(cv: ConfigValue): model.Type = {
206+
private def getTypeFromConfigValue(namespace: Namespace, cv: ConfigValue, valueString: String): model.Type = {
155207
import ConfigValueType._
156208
cv.valueType() match {
157209
case STRING => model.STRING
158210
case BOOLEAN => model.BOOLEAN
159211
case NUMBER => numberType(cv.unwrapped().toString)
160-
case LIST => listType(cv.asInstanceOf[ConfigList])
161-
case OBJECT => objType(cv.asInstanceOf[ConfigObject])
212+
case LIST => listType(namespace, cv.asInstanceOf[ConfigList])
213+
case OBJECT => objType(namespace, cv.asInstanceOf[ConfigObject])
162214
case NULL => throw new AssertionError("null unexpected")
163215
}
164216
}
@@ -200,7 +252,7 @@ class ModelBuilder(assumeAllRequired: Boolean = false) {
200252
}
201253
}
202254

203-
private def listType(cv: ConfigList): model.ListType = {
255+
private def listType(namespace: Namespace, cv: ConfigList): model.ListType = {
204256
if (cv.isEmpty) throw new IllegalArgumentException("list with one element expected")
205257

206258
if (cv.size() > 1) {
@@ -211,24 +263,31 @@ class ModelBuilder(assumeAllRequired: Boolean = false) {
211263
}
212264

213265
val cv0: ConfigValue = cv.get(0)
214-
val typ = fromConfigValue(cv0)
266+
val valueString = util.escapeValue(cv0.unwrapped().toString)
267+
val typ = getTypeFromConfigValue(namespace, cv0, valueString)
215268

216269
val elemType = {
217-
val valueString = util.escapeValue(cv0.unwrapped().toString)
218270
if (typ == model.STRING) {
219-
// see possible type from the string literal:
220-
toAnnBasicType(valueString) match {
221-
case Some((basicType, isOpt, defaultValue))
222-
if (isOpt)
223-
warns += OptListElemWarning(cv0.origin().lineNumber(), valueString)
224-
225-
if (defaultValue.isDefined)
226-
warns += DefaultListElemWarning(cv0.origin().lineNumber(), defaultValue.get, valueString)
227271

228-
basicType
272+
namespace.resolveDefine(valueString) match {
273+
case Some(ort)
274+
ort
229275

230276
case None
231-
typ
277+
// see possible type from the string literal:
278+
toAnnBasicType(valueString) match {
279+
case Some((basicType, isOpt, defaultValue))
280+
if (isOpt)
281+
warns += OptListElemWarning(cv0.origin().lineNumber(), valueString)
282+
283+
if (defaultValue.isDefined)
284+
warns += DefaultListElemWarning(cv0.origin().lineNumber(), defaultValue.get, valueString)
285+
286+
basicType
287+
288+
case None
289+
typ
290+
}
232291
}
233292
}
234293
else typ
@@ -237,7 +296,8 @@ class ModelBuilder(assumeAllRequired: Boolean = false) {
237296
model.ListType(elemType)
238297
}
239298

240-
private def objType(cv: ConfigObject): model.ObjectType = fromConfig(cv.toConfig)
299+
private def objType(namespace: Namespace, cv: ConfigObject): model.ObjectType =
300+
fromConfig(namespace, cv.toConfig)
241301

242302
private def numberType(valueString: String): model.BasicType = {
243303
try {
@@ -279,7 +339,7 @@ object ModelBuilder {
279339
val filename = args(0)
280340
val file = new File(filename)
281341
val source = io.Source.fromFile(file).mkString.trim
282-
println("source:\n |" + source.replaceAll("\n", "\n |"))
342+
//println("source:\n |" + source.replaceAll("\n", "\n |"))
283343
val result = ModelBuilder(source)
284344
println("objectType:")
285345
println(model.util.format(result.objectType))

src/main/scala/tscfg/Namespace.scala

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package tscfg
2+
3+
import tscfg.model.{ObjectRefType, Type}
4+
5+
object Namespace {
6+
val root: Namespace = new Namespace("", None)
7+
8+
def getAllDefines: Map[String, Type] = {
9+
allDefines.toMap
10+
}
11+
12+
private def addToAllDefines(defineFullPath: String, t: Type): Unit = {
13+
allDefines.update(defineFullPath, t)
14+
}
15+
16+
private val allDefines = collection.mutable.HashMap[String, Type]()
17+
}
18+
19+
class Namespace private(simpleName: String, parent: Option[Namespace]) {
20+
import Namespace.addToAllDefines
21+
22+
def getPath: Seq[String] = parent match {
23+
case None Seq.empty
24+
case Some(ns) ns.getPath ++ Seq(simpleName)
25+
}
26+
27+
def getPathString: String = getPath.mkString(".")
28+
29+
def extend(simpleName: String): Namespace = new Namespace(simpleName, Some(this))
30+
31+
private val defineNames = collection.mutable.HashSet[String]()
32+
33+
def addDefine(simpleName: String, t: Type): Unit = {
34+
assert(!simpleName.contains("."))
35+
assert(simpleName.nonEmpty)
36+
37+
if (defineNames.contains(simpleName)) {
38+
println(s"WARN: duplicate @define '$simpleName' in namespace $getPathString. Ignoring previous entry")
39+
// TODO include in build warnings
40+
}
41+
42+
defineNames.add(simpleName)
43+
44+
addToAllDefines(resolvedFullPath(simpleName), t)
45+
}
46+
47+
private def resolvedFullPath(simpleName: String): String = parent match {
48+
case None simpleName
49+
case Some(_) s"$getPathString.$simpleName"
50+
}
51+
52+
def resolveDefine(name: String): Option[ObjectRefType] = {
53+
if (defineNames.contains(name))
54+
Some(ObjectRefType(this, name))
55+
else
56+
parent.flatMap(_.resolveDefine(name))
57+
}
58+
}

src/main/scala/tscfg/gen4tests.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ object gen4tests {
5757
// $COVERAGE-OFF$
5858
if (true||confFile.lastModified >= targetFile.lastModified) {
5959
val genOpts = baseGenOpts.copy(className = className)
60-
println(s"generating for $name -> $fileName")
60+
//println(s"generating for $name -> $fileName")
6161
val generator: Generator = lang match {
6262
case "Scala" new ScalaGen(genOpts)
6363
case "Java" new JavaGen(genOpts)

src/main/scala/tscfg/generators/Generator.scala

+4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ abstract class Generator(genOpts: GenOpts) {
1414
protected val className: String = genOpts.className
1515
protected val hasPath: String = if (genOpts.j7) "hasPath" else "hasPathOrNull"
1616
protected var genResults = GenResult()
17+
18+
protected def dbg(s: String, in: Boolean = true): String =
19+
""
20+
//if (in) s" /*$s*/ " else s" // $s"
1721
}
1822

1923
/**

src/main/scala/tscfg/generators/TemplateGenerator.scala

+4
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ class TemplateGenerator(opts: TemplateOpts) {
6767
case o:ObjectType
6868
(genObjectType(o), abbrevObject)
6969

70+
case ort:ObjectRefType
71+
(ort.toString, abbrevObjectRef)
72+
7073
case b:BasicType
7174
val lc = b.toString.toLowerCase
7275
(lc, lc)
@@ -80,6 +83,7 @@ class TemplateGenerator(opts: TemplateOpts) {
8083
abbrev != abbrevObject && !abbrev.startsWith(abbrevListOf)
8184

8285
private val abbrevObject = "object"
86+
private val abbrevObjectRef = "ref to object"
8387
private val abbrevListOf = "list of"
8488

8589
private val envVarAnn = "@envvar"

0 commit comments

Comments
 (0)