Skip to content

Immutable DTO for hocon #106

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Oct 5, 2019
Merged

Conversation

Arkatufus
Copy link
Contributor

@Arkatufus Arkatufus commented Jul 29, 2019

  • Add immutable support library for materialized Hocon:
    • Class types are reduced to Object, Array, and Literal value type
    • Object value class emulates ImmutableDictionary, with a custom indexer that can directly take a Hocon path, with an ImmutableSortedDictionary as a backing data source
    • Array value class emulates ImmutableList, with an ImmutableArray as a backing data source.
    • Literal value is just a wrapper around a string to make it immutable.
  • Improved data access API for the immutable tree:
    • Objects can be accessed directly using string or HoconPath path inside the dictionary-like indexer. Eg. config["root.first-object.first-property"] will treat config as an object value and traverse to the "root.first-object.first-property" path.
    • The library is smart enough to deduce data access from indexer type, integer indexer will be automatically interpreted as array access, string indexer will be automatically interpreted as path traversal. Eg. config["root.my-array"][0] will treat the config instance as a Hocon object, traverse it to the "root.my-array" value, and then the [0] indexer will treat that value as an array. It will read array values directly, or if it is an object value, will try to convert it into an array using the "positively numeric keyed object to array convertion" rule. Accessing a literal value using any indexers will throw an exception.
    • Literal values are automatically converted/parsed when accessed. Eg. byte value = config["root.byte-value"], the value will be auto convert to byte and assigned to the variable. Auto convert will validate the conversion on the fly and throws an exception if the conversion is invalid.
    • Generic getter implementation. You can use the generic getter instead of relying on the auto type conversion. Eg. var myIntArray = config["root.int-array"].Get<int[]>() will return an array of ints from "root.int-array" path, or var myTimeSpan = config["root.time-span"].Get<TimeSpan>() will return a TimeSpan from "root.time-span" path.
    • Old style getters are still supported, you can still use myConfig.Value.GetInt(), myConfig.Value.GetInt("my-path") etc. but they will only work on the Literal class instance, you can not pass a Hocon path into them anymore. The .Value property is deprecated, it is included just to maintain backward compatibility with old code.
    • Auto-casting and generic getter only works for primitive types, there are no POCO class converter support yet.
    • Supported auto-cast and generic types: bool, byte, sbyte, short, ushort, int, uint, long, ulong, BigInteger, float, double, decimal, TimeSpan, string, and their array variant (eg. bool[] and byte[])

@Arkatufus Arkatufus changed the title Immutable DTO for hocon WIP: Immutable DTO for hocon Jul 29, 2019
@Arkatufus
Copy link
Contributor Author

I'm maintaining full backward compatibility with the old API. Should this be a separate library or should it be folded back into the core project? Ie, should config immutability be the default?

@iron9light
Copy link
Contributor

You wanna re-implemented with all immutable data struct?
That's a good idea. It will be easier to do things right and easier to test.
I think backward compatibility is not a must-have.
You can make it HOCON 2.0 and keep maintain a HOCON 1.x branch for bug fixing.

@Arkatufus
Copy link
Contributor Author

The original HOCON API are all immutable, I'm just trying to see if we should stick to the original material, plus immutability does have its advantages. Programmatic HOCON building/materialization is also supported using a builder pattern, instead of using the fluent .With pattern, which I kind of hate, because it is so memory intensive.

Copy link
Contributor Author

@Arkatufus Arkatufus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code documentation for builder pattern API, generic getter API, and API backward compatibility

.AddRange(value)
.Build();
case HoconType.Literal:
return new HoconImmutableLiteralBuilder()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sample for immutable Hocon literal builder pattern API, it acts/behaves exactly like a string builder.

.Merge(value.GetObject())
.Build();
case HoconType.Array:
return new HoconImmutableArrayBuilder()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sample for immutable Hocon Array builder pattern API, it inherits from List<HoconImmutableElement>, where HoconImmutableElement is the base class for immutable object, array, and literal. Usage acts/behaves exactly like a List

switch (value.Type)
{
case HoconType.Object:
return new HoconImmutableObjectBuilder()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sample for immutable Hocon Object builder pattern API, it inherits from SortedDictionary<string, HoconImmutableElement>, where HoconImmutableElement is the base class for immutable object, array, and literal. Usage acts/behaves exactly like a SortedDictionary


namespace Hocon.Immutable.Extensions
{
public static class HoconImmutableExtensions
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Contains all of the static ToHoconImmutable extension helper class. Converts the old Hocon classes into its immutable variant.


namespace Hocon.Immutable.Extensions
{
public static class TypeConverterExtensions
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Contains static helper extension functions to cast HoconImmutableElement class back into immutable object, array, or literal.


namespace Hocon.Immutable.Extensions
{
public static class GenericGetterExtensions
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generic value getter implementation. Converts an immutable Hocon literal string value to another primitive type. Usage: myHoconLiteral.Get<int>()


namespace Hocon.Immutable.Extensions
{
public static class ValueGetterExtensions
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Contains static extension convenience helper functions to convert an immutable hocon literal into another primitive type. Sample use: myHoconLiteral.GetDecimal().

Also contains backward compatible getter helper functions that follows the old myHoconObject.GetInt("my.hocon.path") API pattern.

Copy link
Contributor Author

@Arkatufus Arkatufus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code documentation for the immutable API

root_2 : 1234
}";
var config = Parser.Parse(hocon).ToHoconImmutable();
Assert.True(config["root.bool"].GetBoolean());
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sample use of the new API, it emulates how Microsoft.Extensions.Configuration works, where you can supply a Hocon path into an array indexer to access children values. In this case, config is an immutable Hocon object, and "root.bool" is the path to the "bool" property inside "root" object.

The getter function GetBoolean() acts directly on an ImmutableHoconLiteral object instance. Object instance will be checked during runtime, invalid instance will throw an exception.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

}";
var config = Parser.Parse(hocon).ToHoconImmutable();
Assert.True(config["root.bool"].GetBoolean());
Assert.True(config["root.bool"].Get<bool>());
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generic getter function version (syntactic sugar) of GetBoolean().

var config = Parser.Parse(hocon).ToHoconImmutable();
Assert.True(config["root.bool"].GetBoolean());
Assert.True(config["root.bool"].Get<bool>());
Assert.True(config["root.bool"]);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sample use of the auto-conversion getter. ImmutableHoconLiteral contains implicit cast operator overrides to automatically convert the string value into other primitive types. In this case, it automatically converts the string value to bool

@Arkatufus Arkatufus self-assigned this Sep 28, 2019
@Aaronontheweb
Copy link
Member

Have this on my to-do list this Friday

Copy link
Member

@Aaronontheweb Aaronontheweb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love this - there's a lot to it but it all looks very well tested and well done.

Question though - why ship it as a separate project? Why not make it part of the whole?

};
[Theory]
[MemberData(nameof(ObjectArrayData))]
public void CanIntelligentlyUseIndexerTypeToAccessMembers(string hocon)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very cool - this is a slick addition to the API

root_2 : 1234
}";
var config = Parser.Parse(hocon).ToHoconImmutable();
Assert.True(config["root.bool"].GetBoolean());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@Arkatufus
Copy link
Contributor Author

Arkatufus commented Oct 5, 2019

Question though - why ship it as a separate project? Why not make it part of the whole?

That was what I asked earlier, should I fold this back into the main package or should I keep it separate into an opt-in package.

The main reason I split it up is to make a proof of concept that it works without having to touch the main code; I was not planning on making these backward compatible, but it turns out that making it backward compatible was really trivial.

@Aaronontheweb
Copy link
Member

Got it - let’s keep it in its own project for now and do a POC while integrating it into Akka.NET

@Aaronontheweb Aaronontheweb merged commit 386f3c1 into akkadotnet:dev Oct 5, 2019
@Arkatufus Arkatufus changed the title WIP: Immutable DTO for hocon Immutable DTO for hocon Oct 5, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants