< Summary

Class:Itinero.Instructions.Configuration.ConfigurationParser
Assembly:Itinero.Instructions
File(s):/home/runner/work/routing2/routing2/src/Itinero.Instructions/Configuration/ConfigurationParser.cs
Covered lines:113
Uncovered lines:10
Coverable lines:123
Total lines:246
Line coverage:91.8% (113 of 123)
Covered branches:31
Total branches:42
Branch coverage:73.8% (31 of 42)
Tag:224_14471318300

Metrics

MethodBranch coverage Cyclomatic complexity Line coverage
.cctor()25%892.3%
ParseRouteToInstructions(...)50%480%
ParseInstructionToText(...)100%10100%
ParseSubObj(...)100%2100%
BothDouble(...)50%483.33%
ParseCondition(...)91.66%1290.9%
ParseRenderValue(...)100%2100%

File(s)

/home/runner/work/routing2/routing2/src/Itinero.Instructions/Configuration/ConfigurationParser.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.Linq;
 4using System.Runtime.CompilerServices;
 5using System.Text.Json;
 6using System.Text.RegularExpressions;
 7using Itinero.Instructions.ToText;
 8using Itinero.Instructions.Types;
 9using Itinero.Instructions.Types.Generators;
 10
 11[assembly: InternalsVisibleTo("Itinero.Tests.Instructions")]
 12
 13namespace Itinero.Instructions.Configuration;
 14
 15internal static class ConfigurationParser
 16{
 117    private static readonly Regex RenderValueRegex =
 118        new(@"^(\${[.+-]?[a-zA-Z0-9_]+}|\$[.+-]?[a-zA-Z0-9_]+|[^\$]+)*$");
 19
 120    private static readonly List<(string, Predicate<(string? a, string? b)>)> Operators =
 121        new()
 122        {
 123            // This is a list, as we first need to match '<=' and '>=', otherwise we might think the match is "abc<" = "
 4424            ("<=", t => BothDouble(t, d => d.a <= d.b)),
 2425            (">=", t => BothDouble(t, d => d.a >= d.b)),
 026            ("!=", t => t.a != null && t.b != null && t.a != t.b),
 227            ("=", t => t.a != null && t.b != null && t.a == t.b),
 2228            ("<", t => BothDouble(t, d => d.a < d.b)),
 4629            (">", t => BothDouble(t, d => d.a > d.b))
 130        };
 31
 32    /// <summary>
 33    ///     Parses the full pipeline
 34    /// </summary>
 35    /// <param name="jsonElement">The json element to start from.</param>
 36    /// <param name="knownGenerators">
 37    ///     The instruction generators that can be used during the construction - should include them
 38    ///     all explicitely
 39    /// </param>
 40    /// <returns>The instruction generator and the to text translators.</returns>
 41    public static (IReadOnlyList<IInstructionGenerator> generator, Dictionary<string, IInstructionToText> toTexts)
 42        ParseRouteToInstructions(JsonElement jsonElement, Dictionary<string, IInstructionGenerator> knownGenerators)
 243    {
 44        // parse generator names and instantiate generators.
 245        var generatorNames = jsonElement.GetProperty("generators").EnumerateArray().Select(v => v.GetString())
 246            .ToList();
 247        var generators = generatorNames.Select(name =>
 048        {
 049            if (knownGenerators.TryGetValue(name, out var g))
 050            {
 051                return g;
 252            }
 253
 054            throw new Exception($"Generator not found: {name}");
 255        }).ToList();
 56
 57        // parse instructions to text configurations.
 258        var languages = jsonElement.GetProperty("languages");
 259        var toTexts = new Dictionary<string, IInstructionToText>();
 1060        foreach (var obj in languages.EnumerateObject())
 261        {
 262            var langCode = obj.Name;
 263            var wholeToText = new Box<IInstructionToText>();
 264            var whole = ParseInstructionToText(obj.Value, wholeToText,
 265                new Dictionary<string, IInstructionToText>(), "/");
 266            wholeToText.Content = whole;
 267            toTexts[langCode] = whole;
 268        }
 69
 270        return (generators, toTexts);
 271    }
 72
 73    /**
 74     * Parses a JSON-file and converts it into a InstructionToText.
 75     * This is done as following:
 76     *
 77     * A hash is interpreted as being "condition":"rendervalue"
 78     *
 79     * A condition is parsed in the following way:
 80     *
 81     * "InstructionType": the 'type' of the instruction must match the given string, e.g. 'Roundabout', 'Start', 'Base',
 82     * These are the same as the classnames of 'Instruction/*.cs' (but not case sensitive and the Instruction can be dro
 83     *
 84     * "extensions": is a special key. The containing object's top levels have keys which are calculated and 'injected' 
 85     *
 86     * "$someVariable": some variable has to exist.
 87     *
 88     * "condition1&condition2": all the conditions have to match
 89     *
 90     * If the condition contains a "=","
 91     * <
 92     * ","
 93     * >
 94     * ","
 95     * <
 96     * =
 97     * " or "
 98     * >
 99     * =" then both parts are interpreted as renderValues and should satisfy the condition.
 100     * (If a substitution fails in any part, the condition is considered failed)
 101     * (Note that = compares the string values, whereas the comparators first parse to a double. If parsing fails, the c
 102     *
 103     * If the condition equals "*", then this is considered the 'fallback'-value. This implies that if every other condi
 104     *
 105     *
 106     * A rendervalue is a string such as "Take the {exitNumber}th exit", where 'exitNumber' is substituted by the corres
 107     * If that substitution fails, the result will be null which will either cause an error in rendering or a condition 
 108     *
 109     * Other notes:
 110     * A POSITIVE angle is going left,
 111     * A NEGATIVE angle is going right
 112     */
 113    internal static IInstructionToText ParseInstructionToText(JsonElement jobj,
 114        Box<IInstructionToText>? wholeToText = null,
 115        Dictionary<string, IInstructionToText>? extensions = null, string context = "")
 17116    {
 17117        extensions ??= new Dictionary<string, IInstructionToText>();
 17118        var conditions = new List<(Predicate<BaseInstruction>, IInstructionToText)>();
 17119        var lowPriority = new List<(Predicate<BaseInstruction>, IInstructionToText)>();
 120
 157121        foreach (var obj in jobj.EnumerateObject())
 53122        {
 53123            var key = obj.Name;
 53124            var value = obj.Value;
 125
 53126            if (key == "extensions")
 2127            {
 2128                var extensionsSource = obj.Value;
 10129                foreach (var ext in extensionsSource.EnumerateObject())
 2130                {
 2131                    extensions.Add(ext.Name,
 2132                        ParseSubObj(ext.Value, context + ".extensions." + ext.Name, extensions, wholeToText)
 2133                    );
 2134                }
 135
 2136                continue;
 137            }
 138
 51139            var (p, isLowPriority) = ParseCondition(key, wholeToText, context + "." + key, extensions);
 51140            var sub = ParseSubObj(value, context + "." + key, extensions, wholeToText);
 51141            (isLowPriority ? lowPriority : conditions).Add((p, sub));
 51142        }
 143
 17144        return new ConditionalToText(conditions.Concat(lowPriority).ToList(), context);
 17145    }
 146
 147    private static IInstructionToText ParseSubObj(JsonElement j, string context,
 148        Dictionary<string, IInstructionToText> extensions, Box<IInstructionToText>? wholeToText)
 53149    {
 53150        if (j.ValueKind == JsonValueKind.String)
 45151        {
 45152            return ParseRenderValue(j.GetString(), extensions, wholeToText, context);
 153        }
 154
 8155        return ParseInstructionToText(j, wholeToText, extensions, context);
 53156    }
 157
 158    private static bool BothDouble((string? a, string? b) t, Predicate<(double a, double b)> p)
 68159    {
 68160        if (double.TryParse(t.a, out var a) && double.TryParse(t.b, out var b))
 68161        {
 68162            return p.Invoke((a, b));
 163        }
 164
 0165        return false;
 68166    }
 167
 168    internal static (Predicate<BaseInstruction> predicate, bool lowPriority) ParseCondition(string condition,
 169        Box<IInstructionToText>? wholeToText = null,
 170        string context = "",
 171        Dictionary<string, IInstructionToText>? extensions = null)
 98172    {
 98173        if (condition == "*")
 12174        {
 25175            return (_ => true, true);
 176        }
 177
 86178        if (condition.IndexOf("&", StringComparison.Ordinal) >= 0)
 18179        {
 18180            var cs = condition.Split("&")
 40181                .Select((condition1, i) => ParseCondition(condition1, wholeToText, context + "&" + i, extensions))
 58182                .Select(t => t.predicate);
 78183            return (instruction => cs.All(p => p.Invoke(instruction)), false);
 184        }
 185
 696186        foreach (var (key, op) in Operators)
 273187        {
 188            // We iterate over all the possible operator keywords: '=', '<=', '>=', ...
 189            // If they are found, we apply the actual operator
 273190            if (condition.IndexOf(key, StringComparison.Ordinal) < 0)
 219191            {
 219192                continue;
 193            }
 194
 195            // Get the two parts of the condition...
 54196            var parts = condition.Split(key).Select(renderValue =>
 108197                    ParseRenderValue(renderValue, extensions, wholeToText, context + "." + key, false))
 54198                .ToList();
 54199            if (parts.Count != 2)
 0200            {
 0201                throw new ArgumentException("Parsing condition " + condition +
 0202                                            " failed, it has an operator, but to much matches. Maybe you forgot to add a
 203            }
 204
 205            // And apply the instruction on it!
 206            // We pull the instruction from thin air by returning a lambda instead
 54207            return (
 70208                instruction => op.Invoke((parts[0].ToText(instruction), parts[1].ToText(instruction))),
 54209                false);
 210        }
 211
 212        // At this point, the condition is a single string
 213        // This could either be a type matching or a substitution that has to exist
 14214        var rendered = ParseRenderValue(condition, extensions, wholeToText, context, false);
 14215        if (rendered.SubstitutedValueCount() > 0)
 3216        {
 8217            return (instruction => rendered.ToText(instruction) != null, false);
 218        }
 219
 11220        return (
 27221            instruction => string.Equals(instruction.Type, condition, StringComparison.CurrentCultureIgnoreCase),
 11222            false);
 98223    }
 224
 225    internal static SubstituteText ParseRenderValue(string value,
 226        Dictionary<string, IInstructionToText>? extensions = null,
 227        Box<IInstructionToText>? wholeToText = null,
 228        string context = "",
 229        bool crashOnNotFound = true)
 169230    {
 169231        var parts = RenderValueRegex.Match(value).Groups[1].Captures
 169232                .Select(m =>
 211233                {
 211234                    var v = m.Value;
 211235                    if (!v.StartsWith("$"))
 119236                    {
 119237                        return (m.Value, false);
 169238                    }
 169239
 92240                    v = v.Substring(1).Trim('{', '}').ToLower();
 92241                    return (v, true);
 211242                }).ToList()
 169243            ;
 169244        return new SubstituteText(parts, wholeToText, context, extensions, crashOnNotFound);
 169245    }
 246}