Configparser Interpolation in C#

I have only ever written a few hundred lines of Python, but I have appreciated its configparser library. One of the coolest features is its interpolation mechanism, which can be used to derive values using other values in an .ini file. Here's a quick example from the documentation:

[Paths]
home_dir: /Users
my_dir: %(home_dir)s/lumberjack
my_pictures: %(my_dir)s/Pictures

I was wondering how I could implement a similar feature in a JSON or YAML configuration file and I realized that I could use scriban. The trick is simple: we read the raw text, serialize it to an object and use this object on the raw text. We repeat this process until no more changes to the text input can be found (or we are reaching some predefined limit). Here is an implementation example:

public static class TemplateEngine
{
    public static T RenderTextAsModelLoop<T>(
        string text,
        Func<string, T> toModel)
        where T : notnull
    {
        var newText = text;

        for (var i = 0; i < 10; i++)
        {
            var model = toModel(newText);
            var tmp = Render(newText, model);

            if (tmp.Equals(newText))
            {
                return model;
            }

            newText = tmp;
        }

        throw new ArgumentException(
            "Exceeded loop limit while trying to render model");
    }

    public static string Render(string text, object model)
    {
        ArgumentNullException.ThrowIfNull(model);

        var template = Template.Parse(text);

        if (template.HasErrors)
        {
            var message = string.Join(Environment.NewLine, template.Messages);

            throw new ArgumentException(
                $"Could not parse template: {message}");
        }

        var scriptObject = new ScriptObject();

        scriptObject.Import(model);

        var context = new TemplateContext
        {
            StrictVariables = true,
            EnableRelaxedMemberAccess = false,
        };

        context.PushGlobal(scriptObject);

        try
        {
            return template.Render(context);
        }
        catch (Exception e)
        {
            throw new ArgumentException(
                $"Could not render template: {e.Message}");
        }
    }
}

Keep in mind that the overall templating features that you can use are rather limited, since the initial raw text must be deserializable. That means that you cannot perform structural changes, but changes to a particular text field/property is doable. A limited, but useful trick.

Published: 2022-12-23