diff --git a/README.md b/README.md index 88a43b2..00f0194 100644 --- a/README.md +++ b/README.md @@ -80,12 +80,33 @@ A name like `root.a` will be translated to a search for an environment variable named `ROOT__A`. +Read the [API](https://hummus.dpldocs.info/hummus.providers.env.html). + +### `JSONProvider` + +A provider which will look for JSON key-value pairs +based on matching them to the names of the fields in +the provided struct. + +Struct fields which are of a struct-type themselves +are supported and are filled whenever json such as +`x.y` is encountered. This means `x` is some field +in the "outer" struct. Then because we have `x.y`, +`x` MUST be of a struct type. Then we access the field +named `y` in this "inner" struct. + +Hence a name like `root.a` will be translated to +an access at `jsonObject["root"].a` (where `jsonObject` +is the root document). + +Read the [API](https://hummus.dpldocs.info/hummus.providers.json.html). + # Development ## Testing In order to run the full test suite use the following -command: +command (or just `./test.sh`): ```d V=1 I__Z=2 dub test @@ -97,6 +118,13 @@ is for the `EnvironmentProvider` which searches for environment variables, and in particular its unittest looks for those two. +### Build configurations + +1. `debug` + * In this mode the `gogga` logging library is + brought in and various flags are set to enable + verbose logging that aids debugging + ## License Licensed under the LGPL-2.0-only . diff --git a/dub.json b/dub.json index d08a148..ce31a7d 100644 --- a/dub.json +++ b/dub.json @@ -4,7 +4,7 @@ ], "copyright": "Copyright © 2025, Tristan Brice Velloza Kildaire", "dependencies": { - "niknaks": ">=0.26.2" + "niknaks": ">=0.27.0" }, "description": "Pluggable configuration engine", "license": "LGPL-2.0-only", @@ -12,10 +12,10 @@ "targetType": "library", "configurations": { "debug": { - "dependencies": { - "gogga": ">=3.2.0" - }, - "versions": ["DBG_DEBUG_LOGGING", "DBG_VERBOSE_LOGGING"] + "dependencies": { + "gogga": ">=3.2.0" + }, + "versions": ["DBG_DEBUG_LOGGING", "DBG_VERBOSE_LOGGING"] } } -} \ No newline at end of file +} diff --git a/source/hummus/providers/json.d b/source/hummus/providers/json.d new file mode 100644 index 0000000..4242cd1 --- /dev/null +++ b/source/hummus/providers/json.d @@ -0,0 +1,167 @@ +/** + * JSON-based provider + * + * You should use this when + * you want to fill up your + * config with values stored + * in a string containing + * JSON-encoded data + * + * Authors: Tristan Brice Velloza Kildaire (deavmi) + */ +module hummus.providers.json; + +import hummus.provider : Provider; +import std.json : JSONValue, JSONType; +import niknaks.json : traverseTo; + +version(unittest) +{ + import gogga.mixins; +} + +/** + * A provider which will look for + * JSON key-value pairs based on + * matching them to the names of the + * fields in the provided struct. + * + * Struct fields which are of + * a struct-type themselves are + * supported and are filled + * whenever json such as `x.y` + * is encountered. This means `x` + * is some field in the "outer" + * struct. Then because we have + * `x.y`, `x` MUST be of a struct + * type. Then we access the field + * named `y` in this "inner" struct + */ +public class JSONProvider : Provider +{ + import std.json : parseJSON, JSONException; + + private JSONValue _j; + + /** + * Constructs a new JSON provider + * with the given input JSON to + * parse + * + * Params: + * json = the input JSON + * Throws: + * JSONException when parsing + * fails + */ + this(string json) + { + // todo: handle exceptions in non-library specific way + // OR require they parse in the JSONValue - that is + // library-dependent tho + this._j = parseJSON(json); + } + + /** + * Implementation + */ + protected bool provideImpl(string n, ref string v) + { + // todo: check return value for nullity + JSONValue* f_node = traverseTo(n, &this._j); + + // todo: value conversion here + if(f_node is null) + { + return false; + } + + string s_out; + if(jsonNormal(f_node, s_out)) + { + version(unittest) + DEBUG("found JSON node toString(): ", s_out); + + v = s_out; + return true; + } + else + { + return false; + } + } +} + +private bool jsonNormal(JSONValue* i, ref string o) +{ + auto t = i.type(); + if(t == JSONType.string) + { + o = i.str(); + return true; + } + else if(t == JSONType.ARRAY) + { + version(unittest) + DEBUG("'", i, "' is an array type, these are unsupported"); + + return false; + } + // todo: disallow array types and object types + else + { + o = i.toString(); + return true; + } +} + +private version(unittest) +{ + import hummus.cfg : fieldsOf; + import std.stdio : writeln; +} + +unittest +{ + struct Inner + { + int prop; + int k; + } + + struct Basic + { + string name; + ulong age; + Inner x; + string bad; + } + + auto cfg = Basic(); + writeln("Before provisioning: ", cfg); + + // input json + string json = ` + { + "name": "Tristan Brice Velloza Kildaire", + "age": 25, + "x": { + "prop": 2 + }, + "bad": ["", 2] + } + `; + + // create a new JSON provider with the + // input JSON + fieldsOf(cfg, new JSONProvider(json)); + + assert(cfg.name == "Tristan Brice Velloza Kildaire"); + assert(cfg.age == 25); + assert(cfg.x.prop == 2); + + // it is not present (in the JSON) hence it should + // never be set in our struct + assert(cfg.x.k == cfg.x.k.init); + assert(cfg.bad.length == 0); +} \ No newline at end of file diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..75471ab --- /dev/null +++ b/test.sh @@ -0,0 +1,2 @@ +#!/bin/bash +V=1 I__Z=2 dub test