Files
gmconfig/README.md
2026-05-14 11:32:17 +02:00

4.2 KiB

Gmconfig - A JSON-SCHEMA-based configuration management support library

Introduction

This library offers the basic support functions for the Gajumaru configuration management subsystem. It is based on JSON-Schema, and includes, among other things:

  • A reasonably complete JSON-Schema validator
  • Validating user configurations against the schema
  • In-service update of the user config
  • Caching of the user config (and schema) as persistent terms
  • Fast config lookups using key paths
  • Lookups can handle both schema defaults and user-provided defaults
  • Optional type coercion during validation
  • Optional conversion of enums to atoms
  • Optional extensions with custom validator funs

JSON-Schema validator

The main thing the validator currently doesn't support is proper management of complex sets of schemas and subSchemas. There are some preparations made for supporting "$id" and uris, but this is still incomplete. "definitions" and "$ref" properties are recognized.

As almost anything is theoretically possible with JSON-Schema, there are surely other things that are unsupported.

All standard data types

  • "null"
  • "boolean"
  • "integer"
  • "number"
  • "boolean"
  • "string"
  • "array"
  • "object"

Dynamic properties

  • "if"
  • "not"
  • "oneOf"
  • "anyOf"
  • "allOf"

Static Properties

Array

  • "maxItems"
  • "minItems"
  • "uniqueItems"
  • "prefixItems"
  • "contains" ("minContains", "maxContains")

Object

  • "properties" ("minProperties", "maxProperties")
  • "patternProperties"
  • "additionalProperties"
  • "required"

String

  • "pattern"
  • "minLength"
  • "maxLength"

Number, Integer

  • "minimum"
  • "maximum"
  • "exclusiveMinimum"
  • "exclusiveMinimum"
  • "multipleOf" (only if object is an integer)

"$updateSemantics"

  • "replace" (fully replaces any old data)
  • "merge" (for objects, keeps and/or updates existing values)
  • "suggest" (adds value if not already present)

Custom validation options

Using gmconfig_schema_utils:validate(Json, Schema, Opts), a few options are supported to further enhance the validation (Opts is of type map()):

coerce => boolean() converts strings to integers, null and booleans when needed. enums_to_atoms => boolean() converts enum strings to atoms extensions => map() supports mapping x-... properties to custom validators.

Validator extensions

See the following test case:

t_nested_refs() ->
    S = read("data/nested_refs_schema.json"),
    F = fun(Str, #{<<"tags">> := Tags}) ->
                true = lists:any(
                         fun(T) ->
                                 nomatch =/= string:prefix(Str, T)
                         end, Tags)
        end,
    Opts = #{extensions => #{<<"x-serialization">> => F}},
    Vs = #{<<"tx">> => #{<<"from">> => <<"ak_good">>}},
    Vf = #{<<"tx">> => #{<<"from">> => <<"ac_bad">>}},
    validate(Vs, S, Opts),
    fails(Vf, S, Opts, #{e => failing_schemas}),
    ok.

This simulates an encoding extension, where the example fun here simply checks if tags specified under the x-serialization property are prefixes of the given string value.

In the test schema, we can see the following definition:

        "properties": {
            "from": {
                "allOf": [
                    { "$ref": "#/components/schemas/Pubkey" },
                    { "x-serialization": {
                        "tags": ["ak"]
                    }}
                ]
            }
        }
...
    "Pubkey": {
        "type": "string",
        "x-serialization": {
            "tags": ["ak", "ct"]
        }

Whenever the validator encounters an x-... property mapped to a validator fun, this fun is called with the value and the schema part of the property. The return value of the fun is ignored, and any normal return is treated as a validation success.

The example illustrates a common pattern in OpenAPI specs, where entity references are used extensively. The Pubkey data type can have a more general x-serialization definition, where multiple key types are accepted, whereas a specialized use of the type can narrow the scope by accepting only a subset of the possible types.