Git Product home page Git Product logo

tscfg's Introduction

Build Status Coverage Status Known Vulnerabilities PRs Welcome Scala Steward badge

tscfg

tscfg is a command line tool that takes a configuration specification parseable by Typesafe Config and generates all the boiler-plate to make the definitions available in type-safe, immutable objects (POJOs/records for Java, case classes for Scala).

The generated code only depends on the Typesafe Config library.

status

The tool supports all types handled by Typesafe Config (string, int, long, double, boolean, duration, size-in-bytes, list, object) and has great test coverage. Possible improvements include a more standard command line interface, a proper tscfg library, and perhaps a revision of the syntax for types. Feel free to fork, enter issues/reactions, submit PRs, etc.

configuration spec

In tscfg's approach, the configuration spec itself is any source parseable by Typesafe Config, so the familiar syntax/format and loading mechanisms are used.

For example, from this configuration:

service {
  url = "http://example.net/rest"
  poolSize = 32
  debug = true
  factor = 0.75
}

tscfg will generate the following (constructors and other methods omitted):

  • Java:

    public class Cfg {
      public final Service service;
      public static class Service {
        public final boolean debug;
        public final double factor;
        public final int poolSize;
        public final String url;
      }
    }

    Nesting of configuration properties is captured via inner static classes.

  • Scala:

    case class Cfg(
      service : Cfg.Service
    )
    object Cfg {
      case class Service(
        debug : Boolean,
        factor : Double,
        poolSize : Int,
        url : String
      )
    }

    Nesting of configuration properties is captured via nested companion objects.

The tool determines the type of each field according to the given value in the input configuration. Used in this way, all fields are considered optional, with the given value as the default.

But this wouldn't be flexible enough! To allow the specification of required fields, explicit types, and default values, a string with a simple syntax as follows can be used (illustrated with the integer type):

field spec meaning java type / default scala type / default
a: "int" required integer int / no default Int / no default
a: "int | 3" optional integer with default value 3 int / 3 Int/ 3
a: "int?" optional integer Integer / null(*) Option[Int] / None

NOTE

  • (*) You can use the --java:optionals flag to generate Optional<T> instead of null.
  • The type syntax is still subject to change.

The following is a complete example exercising this mechanism.

endpoint {
  path: "string"
  url: "String | http://example.net"
  serial: "int?"
  interface {
    port: "int | 8080"
  }
}

For Java, this basically becomes the immutable class:

public class JavaExampleCfg {
  public final Endpoint endpoint;

  public static class Endpoint {
    public final int intReq;
    public final Interface_ interface_;
    public final String path;
    public final Integer serial;
    public final String url;

    public static class Interface_ {
      public final int port;
      public final String type;
    }
  }
}

And for Scala:

case class ScalaExampleCfg(
  endpoint : ScalaExampleCfg.Endpoint
)

object ScalaExampleCfg {
  case class Endpoint(
    intReq    : Int,
    interface : Endpoint.Interface,
    path      : String,
    serial    : Option[Int],
    url       : String
  )

  object Endpoint {
    case class Interface(
      port   : Int,
      `type` : Option[String]
    )
  }
}

running tscfg

You will need a JRE 8+ and the latest fat JAR (tscfg-x.y.z.jar) from the releases.

Or run sbt assembly (or sbt ++2.13.7 assembly) under a clone of this repo to generate the fat jar.

$ java -jar tscfg-x.y.z.jar

tscfg x.y.z
Usage:  tscfg.Main --spec inputFile [options]
Options (default):
  --pn <packageName>                                     (tscfg.example)
  --cn <className>                                       (ExampleCfg)
  --dd <destDir>                                         (/tmp if existing or OS dependent temp dir)
  --java                generate java code               (the default)
  --j7                  generate code for java <= 7      (>= 8)
  --java:getters        generate getters (see #31)       (false)
  --java:records        generate records                 (false)
  --java:optionals      use optionals                    (false)
  --scala               generate scala code              (java)
  --scala:2.12          generate code for scala 2.12     (2.13)
  --scala:bt            use backticks (see #30)          (false)
  --durations           use java.time.Duration           (false)
  --all-required        assume all properties are required (see #47)
  --tpl <filename>      generate config template         (no default)
  --tpl.ind <string>    template indentation string      ("  ")
  --tpl.cp <string>     prefix for template comments     ("##")
  --withoutTimestamp    generate header w/out timestamp  (false)
Output is written to $destDir/$className.ext

So, to generate the Java class tscfg.example.ExampleCfg with the example above saved in a file example.spec.conf, you can run:

$ java -jar tscfg-x.y.z.jar --spec example.spec.conf

parsing: example.spec.conf
generating: /tmp/ExampleCfg.java

maven plugin

Please see tscfg-maven-plugin. Thanks @timvlaer!

gradle plugin

Please see tscfg-plugin-gradle.

configuration access

Access to a configuration instance is via usual Typesafe Config mechanism as appropriate for your application, for example, to load the default configuration:

Config tsConfig = ConfigFactory.load().resolve()

or from a given file:

Config tsConfig = ConfigFactory.parseFile(new File("my.conf")).resolve();

Now, to access the configuration fields, instead of, for example:

Config endpoint = tsConfig.getConfig("endpoint");
String path    = endpoint.getString("path");
Integer serial = endpoint.hasPathOrNull("serial") ? endpoint.getInt("serial") : null;
int port       = endpoint.hasPathOrNull("port")   ? endpoint.getInt("interface.port") : 8080;

you can:

  1. Create the tscfg generated wrapper:

    ExampleCfg cfg = new ExampleCfg(tsConfig);

    which will make all verifications about required settings and associated types. In particular, as is typical with Config use, an exception will be thrown if this verification fails.

  2. Then, while enjoying full type safety and the code completion and navigation capabilities of your editor or IDE:

    String path    = cfg.endpoint.path;
    Integer serial = cfg.endpoint.serial;
    int port       = cfg.endpoint.interface_.port;

An object reference will never be null (or Optional.empty()) (None in Scala) if the corresponding field is required according to the specification. It will only be null (or Optional.empty()) (None in Scala) if it is marked optional with no default value and has been omitted in the input configuration.

With this example spec, the generated Java code looks like this and an example of use like this.

For Scala the generated code looks like this and an example of use like this.

supported types

basic types

The following basic types are supported:

type in spec java type:
req / opt
scala type:
req / opt
string String / String String / Option[String]
int int / Integer Int / Option[Int]
long long / Long Long / Option[Long]
double double / Double Double / Option[Double]
boolean boolean / Boolean Boolean / Option[Boolean]
size long / Long Long / Option[Long]
duration long / Long Long / Option[Long]
duration (using --durations flag) Duration / Duration Duration / Option[Duration]

NOTE

  • please read Optional<T> instead of the T values in the java "opt" column above if using the --java:optionals flag.
  • using the --durations flag, java.time.Duration is used instead of long / Long. See durations for further information.

size-in-bytes

The size type corresponds to the size-in-bytes formats supported by the Typesafe library. See #23 for various examples.

NOTE: As of 0.0.984, a setting with a default value like memory: 50G (or memory: "50G") will no longer be inferred as with size type, but just as a string (with default value "50G"). For the size type effect, you will need to be explicit: memory: "size | 50G". See #42 and #41.

durations

A duration type can be further qualified with a suffix consisting of a colon and a desired time unit for the reported value. For example, with the type "duration:day", the reported long value will be in day units, with conversion automatically performed if the actual configuration value is given in any other unit as supported by Typesafe Config according to the duration format.

A more complete example with some additional explanation:

durations {
  # optional duration; reported Long (Option[Long] in scala) is null (None) if value is missing
  # or whatever is provided converted to days
  days: "duration:day?"

  # required duration; reported long (Long) is whatever is provided
  # converted to hours
  hours: "duration:hour"

  # 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
  millis: "duration:ms | 550s"

  ...
}

Using the --durations flag, the reported value will be a java.time.Duration instead of a long / Long and the suffix will be ignored: "duration:hours | 3day" is java.time.Duration.ofDays(3) if value is missing or whatever is provided converted to a java.time.Duration

list type

With t denoting a handled type, a list of elements of that type is denoted [ t ]. The corresponding types in Java and Scala are:

type in spec java type:
req / opt
scala type:
req / opt
[ t ] List<T> / List<T> List[T] / Option[List[T]]

where T is the corresponding translation of t in the target language, with List<T> corresponding to an unmodifiable list in Java, and List[T] corresponding to an immutable list in Scala.

object type

As seen in examples above, each object in the given configuration spec becomes a class.

It is of course possible to specify a field as a list of objects, for example:

positions: [
  {
    lat: double
    lon: double
  }
]

In Java this basically becomes:

public class Cfg {
  public final java.util.List<Cfg.Positions$Elm> positions;

  public static class Positions$Elm {
    public final double lat;
    public final double lon;
  }
}

and in Scala:

case class Cfg(
  positions : List[Cfg.Positions$Elm]
)

object Cfg {
  case class Positions$Elm(
    lat : Double,
    lon : Double
  )
}

optional object or list

An object or a list in the input configuration can be marked optional with the @optional annotation (in a comment):

#@optional
email {
  server: string
  password: string
}

#@optional
reals: [ { foo: double } ]

In Scala this basically becomes:

case class Cfg(
  email : Option[Cfg.Email],
  reals : Option[List[Cfg.Reals$Elm]]
)

object Cfg {
  case class Email(
    password : String,
    server   : String
  )
  case class Reals$Elm(
    foo : Double
  )
}

As with basic types, the meaning of an optional object or list is that the corresponding value will be null (or Optional.empty()) (None in Scala) when the corresponding actual entry is missing in a given configuration instance.

shared objects

Since version 0.9.94 we started adding support for "shared objects" (#54), a feature that has been enhanced in later versions. This is exercised by using the @define annotation:

#@define
Struct {
  c: string
  d: int
}

example {
  a: Struct
  b: [ Struct ]
}

In this example, the annotation will only generate the definition of the corresponding class Struct in the wrapper but not the member of that type itself. Then, the type can be referenced for other definitions.

Note: The @define annotation is only supported for objects and enumerations (see below), not for a basic types or lists.

shared object inheritance

As of 0.9.98 shared objects now support simple inheritance by an abstract superclass. The following syntax can be used to define a simple inheritance:

#@define abstract
BaseStruct {
  a: [string]
  b: double
}

#@define extends BaseStruct
ChildStruct {
  c: string
  d: string
}

example {
  child: ChildStruct
}

In this example, the annotation will generate an abstract class definition of the BaseStruct as well as a definition of ChildStruct which extends BaseStruct. This inheritance structure simplifies processing of the config with structs that have multiple common fields.

Only leaf members of the inheritance tree may be instantiable instances, all other shared objects in between have to be abstract classes. The following are valid definition comments:

Comment Meaning
#@define abstract Root of an inheritance tree
#@define abstract extends Foo Intermediate member of the tree, that extends the shared object Foo
#@define extends Bla Leaf member of the inheritance tree, that extends the (abstract) shared object Bla

known issues with shared objects

  • the current support for shared objects as field types in another shared object is unstable and not yet fully supported
  • empty structs without any fields are treated as strings. Hence, having a child struct without new fields in addition to its superclass is not supported yet

enum

Enumerations can also be defined and this is done through the @define enum annotation:

#@define enum
FruitType = [apple, banana, pineapple]

fruit: FruitType

someFruits: [FruitType]

other: {
  aFruit: FruitType
}

As with other uses of @define, the enumeration annotation will only generate the enumeration type itself, but the associated name can then be used for other field definitions in your configuration schema.

The type defined in the example above basically gets translated into Java and Scala as follows:

public enum FruitType {
    apple,
    banana,
    pineapple;
}
sealed trait FruitType
object FruitType {
  object apple     extends FruitType
  object banana    extends FruitType
  object pineapple extends FruitType
}

configuration template

tscfg can also generate user-oriented configuration templates from the given configuration schema. See this wiki.

FAQ

But I can just access the configuration values directly with Typesafe Config and even put them in my own classes

Sure. However, as the number of configuration properties and levels of nesting increase, the benefits of automated generation of the typesafe, immutable objects, along with the centralized verification, shall become more apparent. All of this –worth emphasizing– based on an explicit schema for the configuration.

Any tscfg best practice for my development workflow?

Please see this wiki.

Is there any sbt plugin for tscfg that I can use as part of the build for my project?

Not implemented yet. The issue is #21 if you want to add comments or reactions. PRs are also welcome.

Can tscfg generate Optional<T> for optional fields?

Use the --java:optionals flag for enabling Optional<T> instead of null for optional fields in java.

What happened with the generated toString method?

We think it's more flexible to let client code decide how to render configuration instances while also recognizing that very likely typical serialization libraries are already being used in the application. For example, the demo programs JavaUse and scalaUse use Gson and pprint, respectively. Although you could also use Typesafe Config itself for rendering purposes, you would be using the original Typesafe Config parsed configuration object, so the rendering won't necessarily be restricted only to the elements captured in the configuration specification used by tscfg for the generated wrapper.

tests

https://github.com/carueda/tscfg/tree/main/src/test/scala/tscfg

tscfg's People

Contributors

aleris avatar carueda avatar ckittl avatar johanneshiry avatar justinpihony avatar qux42 avatar scala-steward avatar sebastian-peter avatar zishanbilal avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

tscfg's Issues

Report full path for missing required parameter

This is a follow-up of #33

Upon an absent parameter corresponding to a required nested parameter in the config spec, e.g.:

obj {
  sub {
    req: int
  }
}

the corresponding exception message will only refer to the simple path req:

com.typesafe.config.ConfigException$Missing: No configuration setting found for key 'req'

Certainly not critical, but it would be convenient for such exception to instead refer to the full path (obj.sub.req), which would be especially handy in cases where the overall configuration is complex.

Ability to set enums in config

It would be cool to have an ability to set a field which might have only limited set of values for example, let's say fruitType can be only apple, banana or pineapple.
In the config it could look like this:

app.fruitType = "string E ["apple", "banana", "pineapple"]"

After it, sealed trait with three classes could be generated:

trait FruitType 
object FruitType {
   object Apple extends FruitType
   object Banana extends FruitType
   object Pineapple extends FruitType
}

Consider inheritance tree in traversing members

I noticed some difficulties in traversing member structs when using the new inheritance feature (many thanks for that!). Think of my following example config:

#@define abstract
AbstractA {
  a: string
}

#@define extends AbstractA
ImplA {
  b: string
}

Parsing this config to Scala config class aborts with IllegalArgumentException thrown in ModelBuilder:210, as the parent of ImplA cannot be found.

Here is, what is happening: In ModelBuilder:85 it is ensured, that at first the shared objects are processed and added to the name space, without any further distinction among the shared objects. In my case by incident at first ImplA definition and then AbstractA definition should be traversed. Therefore AbstractA is not known to the name space on attempt to add ImplA leading to the described exception.

I would propose to extend the ordering logic in ModelBuilder:85 to utilize an inheritance tree of shared objects. This would also allow inheritance of abstract shared objects then, e.g.

AbstractA
-- AbstractAA
---- ImplAA
-- AbstractAB
---- ImplAB
...

and the logic wouldn't rely on the order in which the user provides the shared object definitions.

Additionally, this might come in handy for #66 of @johanneshiry as then case class members (leaf nodes) can easily be identified.

If I can provide you anything further for a better understanding of my issue, please just let me know. If you two wish for some support on this, please also just give me a little hint. 😉

Regex not properly captured

Hello,
I am having some problem with regex inside string, so I would like to know if is there any way to exclude some keys from building?

My config file has this key:
regex = ">(RUS00),(\\d{12})(.\\d{7})(.\\d{8})(\\d{3})(\\d{3}),(\\d{1,10})((\\.)(\\d{3}))?"

It works fine in my project, but i got this error on tscfg:
String: 1: Expecting a value but got wrong token: 'd' (backslash followed by 'd', this is not a valid escape sequence (quoted strings use JSON escaping, so use double-backslash \ for literal backslash)) (if you intended 'd' (backslash followed by 'd', this is not a valid escape sequence (quoted strings use JSON escaping, so use double-backslash \ for literal backslash)) to be part of a key or string value, try enclosing the key or value in double quotes, or you may be able to rename the file .properties rather than .conf)

It works only if my regex is commented, so actually I am commenting my regex line every time I use tscfg.

Decouple config spec and type spec

It would be helpful to decouple typesafe config files from tscfg's type spec. This would help ensure that the original typesafe config is not modified in any way with special syntax that is not recognized by Typesafe Config or any of its other wrappers.

Example:

Before
application.conf:

endpoint {
  path: "string"
  url: "string | http://example.net"
  serial: "int?"
  interface {
    port: "int | 8080"
  }
}

Instead of specifying tscfg types in this typesafe config file itself, we can decouple the type spec by introducing an optional additional file.

After
application.conf:

endpoint {
  path: "string"
  url: "http://example.net"
  serial: ""
  interface {
    port: "8080"
  }
}

tscfg.conf:

endpoint {
  serial: "int?"
  interface {
    port: int
  }
}

Moreover, since all values will now be strings by default unless explicitly converted (#42), the string type specification becomes redundant and unnecessary. The result is a much simpler typespec.

The user can then supply this optional typespec file like so:
java -jar tscfg-x.y.z.jar --spec example.spec.conf --typespec example.typespec.conf

If the user does not specify the --typespec parameter, tscfg can look for a default tscfg.conf file in the same directory. If one does not exist, tscfg can proceed with default type conversions (i.e., all String).

Let me know what you think!

support size-in-bytes type

Support a size type corresponding to the size-in-bytes formats supported by Typesafe-Config as specified in https://github.com/typesafehub/config/blob/master/HOCON.md#size-in-bytes-format

Example config spec for tscfg to process:

# required size
sizeReq: "size"

# optional size, no default
sizeOpt: "size?"

# optional size with default value 1024 bytes
sizeOptDef: "size | 1K"

# list of sizes
sizes: [ size ]

# list of lists of sizes
sizes2: [ [ size ] ]

Input config example:

sizeReq = "2048K"
sizeOpt = "1024000"
sizes = [ 1000, "64G", "16kB" ]
sizes2  = [[ 1000, "64G" ], [ "16kB" ] ]

incorrect reference with "config" fragment in path

(thanks P. Ambrose for reporting)

if one of the properties is named foo.config.bar, a Config class is created by tscfg. The problem is
all references to com.typesafe.config.Config are incorrectly referencing the
generated class. I think the problem will be easily fixed by fully qualified references
to com.typesafe.config.Config in the method args.

scala: option to use back ticks

Currently (0.8.4):

From:

foo-object {
  bar-baz: string
}
other-stuff: int

the generated Scala is:

case class ScalaIssue30Cfg(
  foo_object  : ScalaIssue30Cfg.FooObject,
  other_stuff : scala.Int
)
object ScalaIssue30Cfg {
  case class FooObject(
    bar_baz : java.lang.String
  )
  ...

That is, regarding the characters that would make the identifiers invalid in Scala, tscfg either removes them (foo-object becomes FooObject) or replace them with underscores (other-stuff -> other_stuff; bar-baz -> bar_baz).

So, the suggested idea here is to add an option to instead use back ticks for such translations (which should probably have been the default behavior since this was implemented):

case class ScalaIssue30Cfg(
  `foo-object`  : ScalaIssue30Cfg.`Foo-Object`,
  `other-stuff` : scala.Int
)
object ScalaIssue30Cfg {
  case class `Foo-Object`(
    `bar-baz` : java.lang.String
  )
...

Note that back sticks have already been used but only for Scala reserved words.

literal duration captured as string

With

idleTimeout = 75 seconds

idleTimeout gets typed as STRING. The ObjectType is

ObjectType(Map(idleTimeout -> AnnType(STRING,true,Some(75 seconds),None)))

(captured as optional and with default value "75 seconds" is ok.)

But idleTimeout should get the type DURATION(ms) instead of STRING.

Base class shouldn't necessarily be abstract

Related with #64, the base class shouldn't necessarily be abstract, but this is currently required per the issue 64b test, which refers to this spec:

    #@define
    BaseModelConfig {
      uuids: [string]
      scaling: double
    }
    
    #@define extends BaseModelConfig
    LoadModelConfig {
      modelBehaviour: string
      reference: string
    }
    
    test.loadModelConfig = LoadModelConfig

Shared config objects

Is there any way to use an object multiple times in a spec and have it generate a shared config type?

E.g.

example {
  a: ${shared}
  b: [${shared}]
}

Where shared could be something like:

shared {
  c: string
  d: {
    e: int
    f: double
  }
}

I often run into this use case, where different parts of the config share a common object.
The above generates two differently named types for shared, forcing you to implement whatever logic consumes the shared object twice.

Is there a way around this, or do you have a recommended approach for such scenarios?

Proper schema syntax

The use of the Lightbend/Typesafe Config library itself for purposes of parsing the input used to generate the wrapper has a number of limitations and counter-intuitive effects, in concrete when doing the automatic type detection from given values (NOTE: explicit types are not an issue). See for example #41, #50, #46.

This is not a fault of that library but rather a consequence of using it to infer the types. In particular, note that Lightbend/Typesafe Config:

  • i) deals with concrete config input (as opposed to a schema as used in tscfg)
  • ii) it complementarily offers multiple ways to get the value of the particular parameter, including getInt, getDouble, etc., so, in other words, it defers to the user to decide how to access the value.

The proposal here is to define a proper syntax/language more appropriate for schema definition purposes and, along with that, implement/use a corresponding parsing mechanism. Although tscfg is not yet v1, perhaps consider forcing the new proper syntax as the default in some stable version, while also adding an option to indicate the particular input language.

Reactions/comments welcome.

Two shared object leading to string coversion.

I am trying to declare and use two different shared objects.

sample conf

#@define
Shared {
  c: string
  d: {
    e: int
  }
}

#@define
Shared2 {
  dd: string
  dddd: {
    eeee: int
  }
}

example {
  a: Shared
  b: [ Shared ]
  c: [ Shared2 ]
}

command used

java -jar tscfg-0.9.981.jar --dd . --spec sample.conf

Generated file

public class ExampleCfg {
  public final ExampleCfg.Example example;

  public static class Example {
    public final java.lang.String a;
    public final java.util.List<java.lang.String> b;
    public final java.util.List<java.lang.String> c;

    public Example(
        com.typesafe.config.Config c,
        java.lang.String parentPath,
        $TsCfgValidator $tsCfgValidator) {
      this.a = c.hasPathOrNull("a") ? c.getString("a") : "Shared";
      this.b = $_L$_str(c.getList("b"), parentPath, $tsCfgValidator);
      this.c = $_L$_str(c.getList("c"), parentPath, $tsCfgValidator);
    }
  }

  public ExampleCfg(com.typesafe.config.Config c) {
    final $TsCfgValidator $tsCfgValidator = new $TsCfgValidator();
    final java.lang.String parentPath = "";
    this.example =
        c.hasPathOrNull("example")
            ? new ExampleCfg.Example(
                c.getConfig("example"), parentPath + "example.", $tsCfgValidator)
            : new ExampleCfg.Example(
                com.typesafe.config.ConfigFactory.parseString("example{}"),
                parentPath + "example.",
                $tsCfgValidator);
    $tsCfgValidator.validate();
  }

  private static java.util.List<java.lang.String> $_L$_str(
      com.typesafe.config.ConfigList cl,
      java.lang.String parentPath,
      $TsCfgValidator $tsCfgValidator) {
    java.util.ArrayList<java.lang.String> al = new java.util.ArrayList<>();
    for (com.typesafe.config.ConfigValue cv : cl) {
      al.add($_str(cv));
    }
    return java.util.Collections.unmodifiableList(al);
  }

  private static java.lang.RuntimeException $_expE(
      com.typesafe.config.ConfigValue cv, java.lang.String exp) {
    java.lang.Object u = cv.unwrapped();
    return new java.lang.RuntimeException(
        cv.origin().lineNumber()
            + ": expecting: "
            + exp
            + " got: "
            + (u instanceof java.lang.String ? "\"" + u + "\"" : u));
  }

  private static java.lang.String $_str(com.typesafe.config.ConfigValue cv) {
    return java.lang.String.valueOf(cv.unwrapped());
  }

  private static final class $TsCfgValidator {
    private final java.util.List<java.lang.String> badPaths = new java.util.ArrayList<>();

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

    void validate() {
      if (!badPaths.isEmpty()) {
        java.lang.StringBuilder sb = new java.lang.StringBuilder("Invalid configuration:");
        for (java.lang.String path : badPaths) {
          sb.append("\n    ").append(path);
        }
        throw new com.typesafe.config.ConfigException(sb.toString()) {};
      }
    }
  }
}

Ideally, a, b type should be Shared and c should be Shared2. But generated code contains all three variables as strings.

Size-in-bytes should be explicitly indicated in config spec

Size-in-bytes conversion should be explicitly (not optionally) specified by user using size | <size>. Unless this is explicitly specified, tscfg should assume it is a string by default; no implicit conversion should be done. (This is also the default behaviour of Typesafe Config.)

Example:

In each of the following cases, tscfg currently generates a variable memory of type Long, with value 53687091200.

memory: 50G
memory: "50G"

To be consistent with the default behavior of Typesafe Config, which doesn't make assumptions about types unless explicitly requested (e.g., using methods such as getBytes, getInt, etc), tscfg should instead generate a variable memory of type String, with value "50G". This should be the default behavior, unless the user explicitly requests a size-in-bytes comparison as follows:

memory: "size | 50G"

(This has been discussed at length in #41).

infer type

When no explicit type is given in the input config spec, infer the type according to the given value. This is mainly intended to facilitate generation solely based on existing configuration files.

Incorrect format of double with with input 1.0

When generating a java class from the following input:

my.parameter = 1.1
I get:

public static class my {
public final double parameter;
}

All is well, but:
my.parameter = 1.0
I get:

public static class my {
public final int parameter;
}

Which i think should be a double. The test in https://github.com/carueda/tscfg/blob/master/src/test/scala/tscfg/generators/java/JavaMainSpec.scala :

"example 1" in {
  val c = new JavaIssue15bCfg(ConfigFactory.parseString(
    """
      |strings:  [hello, world, true]
      |integers: [[1, 2, 3], [4, 5]]
      |doubles:  [3.14, 2.7182, 1.618]
      |longs:    [1, 9999999999]
      |booleans: [true, false]
      |""".stripMargin
  ))

is missing e.g. 4.0 in doubles to test this case.

Option to generate singleton constructor or static instance

There's a couple different patterns here I'd like to discuss; we've been switching between them at work in our handwritten boilerplate for Config and I was hoping this code generator could be extended to add one or the other or both.

Our apps just load their config once on startup, so it made sense to have the code executed in a static so it can be available everywhere; this does cause quite a bit of coupling which we will have to deal with later but it's great for rapid prototyping, especially if you don't expect the config to change.

Singleton Pattern

Our first attempt just used a singleton pattern, so with the codegen example from README it just looks like this:

public class Cfg {
  public final Service service;
  public static class Service {
    public final boolean debug;
    public final double factor;
    public final int poolSize;
    public final String url;
  }

  private static Cfg INSTANCE;
  public static final getInstance() {
    if (INSTANCE == null) INSTANCE = new Cfg(ConfigFactory.load());
    return INSTANCE;  
  }
}

Since we can't modify generated code because it can be overwritten at any point, it would be nice if the generator added something like this for us so we don't have to stick it in another class.

Statics Pattern

An alternative pattern we came up with that I've personally become more attached to is to just have a hierarchy of classes with static final fields, e.g. with the same example:

public class Cfg {
  private Cfg() {}

  private static final Config config = ConfigFactory.load();

  public static class Service {
    private Service() {}

    public static final boolean debug = config.getBoolean("service.debug");
    public static final double factor = config.getDouble("service.factor");
    public static final int poolSize = config.getInt("service.poolSize");
    public static final String url = Config.getString("service.url");
  }
}

It's kind of bad style but it looks really nice in usage:

Service.bind(Cfg.Service.url);

This avoids calling getInstance() everywhere which is really just line noise.

Ability to extend or implement external type that is an interface or class with no-arg, default constructor

Hello again :-)

it has been a while but we still make extensive use of this library.
Today we discovered some flaws that relate to the implementation of shared objects / inheritance in the config generation.
The main issue is, that defined parent classes using #@define abstract do not implement java.io.Serializable and hence their serialization is prevented which might be required in some situations (which we are currently facing when we want to setup an akka cluster and send the configuration around).

So when we discussed this in our team we came up with three different approaches that would solve our issue, but we would love to hear your @carueda opinion on this and which one you would prefer to have handed in as a PR.

  1. Always extend abstract class with java.io.Serializable
    The most easiest approach is to just add java.io.Serializable to all classes marked as #@define abstract. AFAIK this wouldn't break anything but just adds serialization capabilities to all abstract superclasses.

  2. Extend the check to allow java.io.Serializable as parent in ModelBuilder:168
    This would be another way to allow java.io.Serializable by exclude it from the superclass check. With this adaption, we would allow something like

#@define extends java.io.Serializable
ChildStruct {
  c: string
  d: string
}

if a struct needs to be serializable.

  1. Fully remove the check on existing parent classes in ModelBuilder:168
    This would be the biggest possible change that would not only solve our problem but somehow introduce a new feature. Without this check it would be possible to mix in any trait that is available in the existing codebase because we do not check anymore if the superclass is also defined within the config definition template. This would leverage the capabilities of superclasses to a new level and would allow for a huge flexibility. However, this would also introduce the possibility of non-compiling code as we lose control over the resulting code and cannot guarantee anymore that the resulting config class compiles as it may depend on third-party code at the specific project.

That said I'm open to provide a PR for one these solutions if you would like to.

Looking forward on your feedback and further discussions.

Best,

Johannes

switch to scala 2.13 and remove deprecation warning for JavaConverters

We are using tscfg with scala 2.13.x and we get lot's of deprecation warnings:

object JavaConverters in package collection is deprecated (since 2.13.0): Use `scala.jdk.CollectionConverters` instead

I prepared a small adaptions to use the (in 2.13 newly introduced) scala.jdk.CollectionConverters to get rid of this warnings. But this would be a breaking change as it would require 2.13. Is there any interest in updating to 2.13 or is the intended way to stay with 2.12 and ignore the warnings for now?

I know this is just a small benefit that comes with a breaking change, but I thought I would ask it anyway as it has become a little bit annoying in our projects to have these deprecation messages everywhere.

Use consistent formatter for the generated code

This will allow the tool implementation to evolve in a more flexible manner while avoiding/minimizing unneeded formatting changes in generated code.

Basically these benefits:

  • development can focus more on the correctness of the semantics and much less on dealing with the resulting formatting. Of course, changes in the output are to be expected when actually dealing with intended additions or changes.
  • users can upgrade to new versions of the library with less concern about impact in terms of changes in the generated code. Some users also commit the generated code to version control, so, in general, better to avoid unnecessary formatting changes.

An initial approach is to just incorporate (as libraries) scalafmt for Scala, and probably google-java-format for Java; in general with whatever defaults they have. (Note: initially at least, the goal is just some consistent/automated formatting, not necessarily trying to follow any formatting rules that users may have for their own projects -- but this could be reviewed later if needed.)

Longer term, for Scala, we could also consider incorporating https://scalameta.org/ in the pipeline.

Quoted string in spec value should determine string type

Suppose I have the following property in my typesafe-config file:

memory: "string | 50G"

the generated code using tscfg is:

memory = if(c.hasPathOrNull("memory")) c.getString("memory") else "50G"

However, if I load the config first using Typesafe's ConfigFactory.load() method, and if I then initialize the generated class using this Config object, I see this in the initialized object:

memory = "string | 50G"

Instead, I expect:

memory = "50G"

So it looks like despite generating the code correctly, initializing the generated class using a Typesafe Config object does not seem to parse the string correctly.

I highly recommend that if a key has a quoted string value in the Typesafe config file, the code generator should not implicitly convert it to bytes. Of course, if it is an unquoted string, it should convert it to bytes as is done currently.

Maven central

Hey,

Would it be possible to have the releases available in maven central?

Best,
Niklas

Default value for List type

It's not available to create spec contains List type with default value.
For example, spec intParams = [int] generate list of integers.
But default value for list will be ignored intParams = [int] | [1,2,3]. As workaround optional annotation can be used with default value in the code.

support durations

Definite syntax still TBD but the idea for the spec would be something like:

durations {
  # optional duration; reported Long (Option[Long] in scala) is null (None) if value is missing
  # or whatever is provided converted to days
  days = "duration:day?"

  # required duration; reported long (Long) is whatever is provided 
  # converted to hours
  hours = "duration:hour"

  # 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
  millis = "duration:ms | 550s"
}

Generate default config objects if non-existent

Right now you can enter a default config such as this:

endpoint {
  url: "String | http://example.net"
}

However, if endpoint is not entered into the final config then you end up with a missing config exception. And optional does not work because it results in a None, which cannot reference the url property.

So, my proposed solution is to add a top level annotation to denote that objects themselves should use defaults if they exist.

Instead of config.endpoint.url throwing an exception because endpoint doesn't exist it will instead create the endpoint object and let the final property referenced determine if an exception or default should be used.

capture other types

capture other types as supported by Config, perhaps even add some like "regex", etc.

Fully validate given config on construction

Looking through the codegen it appears that it just calls Config.get*() for each of the config object's fields, so if any keys are missing or mistyped it will error out on the first one.

However, you can call Config.checkValid() with a prototype config and it will report errors for all missing or mistyped keys in the config with a runtime exception; we've been specifying a schema manually in our config static class (which kind of invalidates my point in #47 about not having two different sources of truth for the config schema but this way we would still only have a single spec, our default config) but it would be nice when using this library to automatically generate the prototype config and validate the config being passed to the constructor, e.g. for the example in README:

public class Cfg {
  public final Service service;
  public static class Service {
    public final boolean debug;
    public final double factor;
    public final int poolSize;
    public final String url;
  }
  
  public Cfg(Config config) {
    config.checkValid(Config.parseMap(
      Map.of(
        "service", Map.of(
          "debug", false, // the value doesn't matter, only the type
          factor, 0.0,
          poolSize, (int) 0,
          url, "",
        )
    ));

    // initialize `Cfg` if an exception was not thrown
  }
}

annotations

Broadly speaking, besides specific types for the fields, a variety of other attributes could be associated to the elements in a configuration spec:

  • mark a section as optional
  • indicate that the value for a field is a password or similar sensitive piece of information such that it's omitted (or shown in some special fashion) by the generated toString method
  • have the generated toString method to skip a field or section
  • indicate restrictions (like valid ranges for numeric values)
  • etc.

Although some of this could be addressed via some extended syntax to the type (in the case of fields at least), a typical "annotation" approach would be more flexible.

The annotation syntax would be special comment lines, for example:

main {
  # Mail server properties if you want to enable notifications to users
  #@optional
  email {
    server: string

    #@password
    password
  }
  ...
}

Cannot use non ascii word in conf file.

For example I use non ascii

// 测试 is test in Chinese.
test = "测试" 

The generator complains:

Exception in thread "main" java.nio.charset.MalformedInputException: Input length = 1
	at java.nio.charset.CoderResult.throwException(CoderResult.java:281)
	at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:339)
	at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
	at java.io.InputStreamReader.read(InputStreamReader.java:184)
	at java.io.BufferedReader.read1(BufferedReader.java:210)
	at java.io.BufferedReader.read(BufferedReader.java:286)
	at java.io.Reader.read(Reader.java:140)
	at scala.io.BufferedSource.mkString(BufferedSource.scala:96)
	at tscfg.Main$.generate(Main.scala:133)
	at tscfg.Main$.main(Main.scala:54)
	at tscfg.Main.main(Main.scala)

Maybe something wrong with the default charset tscfg uses?
My file is in UTF-8.

support list

Example:

lists {
  names: [ string ]
  positions: [
    {
      lat: "double"
      lon: "double"
    }
  ]
}

denotes that:

  • names is a list of strings
  • positions is a list of objects {lat, lon}.

Since this is for purposes of the spec itself, the given list should have only one element. A warning can be generated in case additional elements (which will be ignored) are included.

Ability to allow shared object inheritance

After their introduction, we found shared objects very useful in our configuration setup. Based on our experience working with shared objects, we would highly appreciate the availability of some kind of basic inheritance for shared object definition. Especially from the scala config perspective, this would reduce code duplicates when working with several shared object definitions that have one or more fields in common because one could pattern match against them.

I have not thought about all details yet, because I'm unsure if this a feature that could be of interested to be included, but my first idea was something like:

#@define_pa
BaseModelConfig {
  uuids: [string]         
  scaling: double  
}

#@define
LoadModelConfig ex BaseModelConfig{
  modelBehaviour: string  
  reference: string       
}

#@define
FixedFeedInModelConfig ex BaseModelConfig

This would end up in either a sealed trait or an abstract class (whatever is easier) in the generated code (only thought about scala for now, maybe slightly different concept needs to be developed for java code):

  sealed trait BaseModelConfig {
    val scaling: scala.Double
    val uuids: scala.List[java.lang.String]
  }

  final case class LoadModelConfig(
                              scaling: Double,
                              uuids: List[String],
                              modelBehaviour: java.lang.String,
                              reference: java.lang.String
                            ) extends BaseModelConfig

  object LoadModelConfig {
    def apply(
               c: com.typesafe.config.Config,
               parentPath: java.lang.String,
               $tsCfgValidator: $TsCfgValidator
             ): SimonaConfig.LoadModelConfig = {
      SimonaConfig.LoadModelConfig(
        scaling = $_reqDbl(parentPath, c, "scaling", $tsCfgValidator),
        uuids = $_L$_str(c.getList("uuids"), parentPath, $tsCfgValidator),
        modelBehaviour =
          $_reqStr(parentPath, c, "modelBehaviour", $tsCfgValidator),
        reference = $_reqStr(parentPath, c, "reference", $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
        }
    }

    private def $_reqDbl(
                          parentPath: java.lang.String,
                          c: com.typesafe.config.Config,
                          path: java.lang.String,
                          $tsCfgValidator: $TsCfgValidator
                        ): scala.Double = {
      if (c == null) 0
      else
        try c.getDouble(path)
        catch {
          case e: com.typesafe.config.ConfigException =>
            $tsCfgValidator.addBadPath(parentPath + path, e)
            0
        }
    }
  }

final case class FixedFeedInModelConfig(scaling: Double, uuids: List[String]) extends BaseModelConfig

  object FixedFeedInModelConfig {
    def apply(
               c: com.typesafe.config.Config,
               parentPath: java.lang.String,
               $tsCfgValidator: $TsCfgValidator
             ): SimonaConfig.FixedFeedInModelConfig = {
      SimonaConfig.FixedFeedInModelConfig(
        scaling = $_reqDbl(parentPath, c, "scaling", $tsCfgValidator),
        uuids = $_L$_str(c.getList("uuids"), parentPath, $tsCfgValidator),
      )
    }

    private def $_reqDbl(
                          parentPath: java.lang.String,
                          c: com.typesafe.config.Config,
                          path: java.lang.String,
                          $tsCfgValidator: $TsCfgValidator
                        ): scala.Double = {
      if (c == null) 0
      else
        try c.getDouble(path)
        catch {
          case e: com.typesafe.config.ConfigException =>
            $tsCfgValidator.addBadPath(parentPath + path, e)
            0
        }
    }
  }

This is a first draft of the concept for now but I hope the general idea becomes clear. Looking forward to read what you think about it.

Cheers,

Johannes

tl;dr

  • inheritance for shared objects
  • added final modifier to scala case classes for scala best practices 🤓

UX improvement: using real config file as spec and with --all-required flag

We've been using Typesafe-Config at work in a few projects now and we really like it; we've actually written very similar boilerplate code to what this library generates, and now having stumbled across it I'd like to use it instead.

However, because we've been using Config directly, all we have are actual config files that act as both reference/template and default; I am aware they can be used directly as specs for codegen with this tool but it generates code that assumes any given values in the config files are the defaults when I would like to just have them all be required values (so if they're modified later but a key is omitted or mistyped it throws an error instead of falling back to default).

I see this has already been sort-of discussed in #43 but I would say this is the "application" workflow that was mentioned; I would like to just use the default config as the spec, otherwise it's two separate files to keep in sync with each other with duplicated structure but one contains types while the other contains values.

I think this could be implemented as a flag like --assume-all-required or something which doesn't pull default values from the spec; it just uses them to infer the types of the fields.

proper tscfg library

with API documentation, etc. So far, that "API" is the tscfg.Main.main method with documentation being the usage message printed out by that method.

Related: #24

No main manifest attribute error

When trying to use v0.9.95, I am getting a no main manifest attribute error.

I am invoking it the same way as v0.9.94, which works fine:
java -jar /tscfg-0.9.95.jar

rewrite code

Make it more modularized, readable, easily expandable.

scala: double definition

With

issue {
  optionalFoo = "string?"
}

the generated scala code:

// generated by tscfg 0.3.3 on Tue Aug 30 16:10:01 PDT 2016
// source: example/issue13.conf

package tscfg.example

object ScalaIssue13Cfg {
  object Issue {
    def apply(c: scala.Option[com.typesafe.config.Config]): Issue = {
      Issue(
        c.map(c => if(c.hasPathOrNull("optionalFoo")) Some(c.getString("optionalFoo")) else None).get
      )
    }
  }
  case class Issue(
    optionalFoo : scala.Option[java.lang.String]
  ) {
    override def toString: java.lang.String = toString("")
    def toString(i:java.lang.String): java.lang.String = {
      i+ "optionalFoo = " + this.optionalFoo + "\n"
    }
  }
...

does not compile:

[error] ...src/main/scala/tscfg/example/ScalaIssue13Cfg.scala:14: double definition:
[error] def apply(c: Option[com.typesafe.config.Config]): tscfg.example.ScalaIssue13Cfg.Issue at line 8 and
[error] case def apply(optionalFoo: Option[String]): tscfg.example.ScalaIssue13Cfg.Issue at line 14
[error] have same type after erasure: (c: Option)tscfg.example.ScalaIssue13Cfg.Issue
[error]   case class Issue(
[error]              ^
[error] one error found

name collision with no-arg methods in scope

With a configuration "spec" input like this

foo {
  clone     = ".."
  finalize  = ".."
  getClass  = ".."
  notify    = ".."
  notifyAll = ".."
  toString  = ".."
  wait      = ".."
}

The Scala output would be:

...
case class Foo(
    clone     : String,
    finalize  : String,
    getClass  : String,
    notify    : String,
    notifyAll : String,
    toString  : String,
    wait      : String
  ) { ...

This does not compile:

  • these names collide with corresponding no-args methods in Object class
  • actually for toString this is a method already being overridden in the body of the case class

I ran across this issue when running the tool on a real application having notify in its configuration. So, in general, these "special" names should be transformed in some way in the generated code to avoid the collision (e.g., with a trailing underscore).

(Note that this is not an issue in the Java output case.)

quoted keys

Quoted keys are not properly handled, eg:

"do log" : boolean
"$_foo"  : baz

generate conf template

Configuration templates are often a means to provide end users with a description of the properties that should be set for the proper operation of an application or library. Based on the template, end users will then enter the concrete settings that are appropriate.

On the other hand, configuration "specs," as used for tscfg generation, are mainly intended to be used by the developer of such application or library.

The proposal here is about adding an option to also generate a template configuration file from the given spec so the developer does not have to manually create/edit such templates.

For example, from the spec

# Some description of the endpoint section 
endpoint {
  # The path associated with the endpoint
  path = "string"

  # Port where the endpoint service is running
  port = "int | 8080"

  # Email to send notifications to. If missing, no emails are sent.
  email = "string?"
}

the generated template could look something like

# Some description of the endpoint section 
endpoint {
  # 'path': Required string.
  # The path associated with the endpoint
  path =   

  # 'port': Optional integer. Default value: 8080
  # Port where the endpoint service is running
  #port =

  # 'email': Optional string.
  # Email to send notifications to. If missing, no emails are sent.
  #email = 
}

Generating java.time.Duration instead of long.

Mit config.getDuration("path") bekommt man eine java.time.Duration.
Es wäre schön, wenn tscfg darauf erweitert werden könnte und anstatt eines Longs eben die Duration (java8 oder neuer) zurückliefert.

String-to-bytes conversion results in compilation error

First of all, I'd like to thank you for this library. This makes mapping Typesafe config files to objects a breeze!

I'm noticing an issue with the string-to-bytes conversion. If I specify a large number, such as:

memory: 50G

The resulting code is generated as:

memory = if(c.hasPathOrNull("memory")) c.getBytes("memory") else 53687091200

The compiler then throws an error saying 53687091200 is outside the range of Int. If I convert this to a long (53687091200L), it works. But this adds a manual element to the code generation; I must now manually fix this every time I use tscfg regenerate the code.

Option to generate getters

I am unable to mock generated Conf class for testing with Mockito even after enabling mocking final classes/methods in Mockito.

This is failing with Mockito.when(...).thenReturn(...);
I am guessing this is because Mockito can mock only method calls.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.