From e2cd6caa703b7c7d868921adb94dc1b7d0d303b2 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Sat, 3 Apr 2021 19:11:41 +0200 Subject: [PATCH] Fix various bugs, improve docs, allow 'null' in JSON, specify behaviour of must_match better --- AspectedRouting.Test/MappingTest.cs | 26 +++++ AspectedRouting.Test/MustMatchTest.cs | 48 ++++++++++ AspectedRouting.sln.DotSettings.user | 4 +- .../IO/LuaSkeleton/LuaSkeleton.Expressions.cs | 94 +++++++------------ .../IO/LuaSkeleton/LuaStringExtensions.cs | 1 + .../JsonParser.ParseAspectProfile.cs | 3 + AspectedRouting/IO/lua/if_then_else.lua | 2 +- AspectedRouting/IO/lua/mustMatch.lua | 12 ++- AspectedRouting/Language/Functions/Mapping.cs | 7 ++ .../Language/Functions/MustMatch.cs | 73 +++++++------- AspectedRouting/Tests/FunctionTestSuite.cs | 2 +- AspectedRouting/Tests/ProfileTestSuite.cs | 2 +- 12 files changed, 172 insertions(+), 102 deletions(-) create mode 100644 AspectedRouting.Test/MappingTest.cs create mode 100644 AspectedRouting.Test/MustMatchTest.cs diff --git a/AspectedRouting.Test/MappingTest.cs b/AspectedRouting.Test/MappingTest.cs new file mode 100644 index 0000000..df92b98 --- /dev/null +++ b/AspectedRouting.Test/MappingTest.cs @@ -0,0 +1,26 @@ +using AspectedRouting.Language; +using AspectedRouting.Language.Functions; +using Xunit; + +namespace AspectedRouting.Test +{ + public class MappingTest + { + [Fact] + public static void SimpleMapping_SimpleHighway_GivesResult() + { + var maxspeed = new Mapping(new[] {"residential", "living_street"}, + new[] { + new Constant(30), + new Constant(20) + } + ); + var resMaxspeed= maxspeed.Evaluate(new Context(), new Constant("residential")); + Assert.Equal(30, resMaxspeed); + var livingStreetMaxspeed= maxspeed.Evaluate(new Context(), new Constant("living_street")); + Assert.Equal(20, livingStreetMaxspeed); + var undefinedSpeed = maxspeed.Evaluate(new Context(), new Constant("some_unknown_highway_type")); + Assert.Null(undefinedSpeed); + } + } +} \ No newline at end of file diff --git a/AspectedRouting.Test/MustMatchTest.cs b/AspectedRouting.Test/MustMatchTest.cs new file mode 100644 index 0000000..fce7ff4 --- /dev/null +++ b/AspectedRouting.Test/MustMatchTest.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using AspectedRouting.Language; +using AspectedRouting.Language.Functions; +using Xunit; + +namespace AspectedRouting.Test +{ + public class MustMatchTest + { + [Fact] + public void MustMatch_SimpleInput() + { + var mapValue = new Mapping(new[] {"residential", "living_street"}, + new[] { + new Constant("yes"), + new Constant("no") + }); + var mapTag = new Mapping(new[] {"highway"}, new[] {mapValue}); + var mm = Funcs.MustMatch + .Apply( + new Constant(new[] {new Constant("highway")}), + Funcs.StringStringToTags.Apply(mapTag) + ) + ; + + + var residential = mm.Apply(new Constant(new Dictionary { + {"highway", "residential"} + })).Evaluate(new Context()); + Assert.Equal("yes", residential); + + var living = mm.Apply(new Constant(new Dictionary { + {"highway", "living_street"} + })).Evaluate(new Context()); + Assert.Equal("no", living); + + var unknown = mm.Apply(new Constant(new Dictionary { + {"highway", "unknown_type"} + })).Evaluate(new Context()); + Assert.Equal("yes", unknown); + + var missing = mm.Apply(new Constant(new Dictionary { + {"proposed:highway", "unknown_type"} + })).Evaluate(new Context()); + Assert.Equal("no", missing); + } + } +} \ No newline at end of file diff --git a/AspectedRouting.sln.DotSettings.user b/AspectedRouting.sln.DotSettings.user index 116d60a..3a4c3c8 100644 --- a/AspectedRouting.sln.DotSettings.user +++ b/AspectedRouting.sln.DotSettings.user @@ -12,10 +12,10 @@ </TestAncestor> </SessionState> - <SessionState ContinuousTestingMode="0" Name="Integration_TestExamples" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + <SessionState ContinuousTestingMode="0" IsActive="True" Name="Integration_TestExamples" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> <Project Location="/home/pietervdvn/git/AspectedRouting/AspectedRouting.Test" Presentation="&lt;AspectedRouting.Test&gt;" /> </SessionState> - <SessionState ContinuousTestingMode="0" IsActive="True" Name="SpecializeToCommonTypes_ValueAndFuncType_ShouldFail" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + <SessionState ContinuousTestingMode="0" Name="SpecializeToCommonTypes_ValueAndFuncType_ShouldFail" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> <Project Location="/home/pietervdvn/git/AspectedRouting/AspectedRouting.Test" Presentation="&lt;AspectedRouting.Test&gt;" /> </SessionState> \ No newline at end of file diff --git a/AspectedRouting/IO/LuaSkeleton/LuaSkeleton.Expressions.cs b/AspectedRouting/IO/LuaSkeleton/LuaSkeleton.Expressions.cs index 7a834b7..ced1f46 100644 --- a/AspectedRouting/IO/LuaSkeleton/LuaSkeleton.Expressions.cs +++ b/AspectedRouting/IO/LuaSkeleton/LuaSkeleton.Expressions.cs @@ -26,8 +26,7 @@ namespace AspectedRouting.IO.LuaSkeleton , UnApply( IsFunc(Funcs.StringStringToTags), Assign(collectedMapping)) - ).Invoke(bare)) - { + ).Invoke(bare)) { AddDep(Funcs.FirstOf.Name); return "first_match_of(tags, result, \n" + " " + ToLua(order.First(), key) + "," + @@ -42,8 +41,7 @@ namespace AspectedRouting.IO.LuaSkeleton , UnApply( IsFunc(Funcs.StringStringToTags), Assign(collectedMapping)) - ).Invoke(bare)) - { + ).Invoke(bare)) { AddDep(Funcs.MustMatch.Name); return "must_match(tags, result, \n" + " " + ToLua(order.First(), key) + "," + @@ -54,37 +52,32 @@ namespace AspectedRouting.IO.LuaSkeleton if (UnApply( IsFunc(Funcs.MemberOf), Any - ).Invoke(bare)) - { + ).Invoke(bare)) { AddDep("memberOf"); return "memberOf(funcName, parameters, tags, result)"; } - + var collectedList = new List(); var func = new List(); if ( UnApply( UnApply(IsFunc(Funcs.Dot), Assign(func)), UnApply(IsFunc(Funcs.ListDot), - Assign(collectedList))).Invoke(bare)) - { + Assign(collectedList))).Invoke(bare)) { var exprs = (IEnumerable) ((Constant) collectedList.First()).Evaluate(_context); var luaExprs = new List(); var funcName = func.First().ToString().TrimStart('$'); AddDep(funcName); - foreach (var expr in exprs) - { + foreach (var expr in exprs) { var c = new List(); - if (UnApply(IsFunc(Funcs.Const), Assign(c)).Invoke(expr)) - { + if (UnApply(IsFunc(Funcs.Const), Assign(c)).Invoke(expr)) { luaExprs.Add(ToLua(c.First(), key)); continue; } if (expr.Types.First() is Curry curry - && curry.ArgType.Equals(Typs.Tags)) - { + && curry.ArgType.Equals(Typs.Tags)) { var lua = ToLua(expr, key); luaExprs.Add(lua); } @@ -93,6 +86,7 @@ namespace AspectedRouting.IO.LuaSkeleton return "\n " + funcName + "({\n " + string.Join(",\n ", luaExprs) + "\n })"; } + collectedMapping.Clear(); var dottedFunction = new List(); dottedFunction.Clear(); @@ -104,8 +98,7 @@ namespace AspectedRouting.IO.LuaSkeleton UnApply( IsFunc(Funcs.StringStringToTags), Assign(collectedMapping))).Invoke(bare) - ) - { + ) { var mapping = (Mapping) collectedMapping.First(); var baseFunc = (Function) dottedFunction.First(); AddDep(baseFunc.Name); @@ -121,8 +114,7 @@ namespace AspectedRouting.IO.LuaSkeleton // The expression might be a function which still expects a string (the value from the tag) as argument if (!(bare is Mapping) && bare.Types.First() is Curry curr && - curr.ArgType.Equals(Typs.String)) - { + curr.ArgType.Equals(Typs.String)) { var applied = new Apply(bare, new Constant(curr.ArgType, ("tags", "\"" + key + "\""))); return ToLua(applied.Optimize(), key); } @@ -130,37 +122,31 @@ namespace AspectedRouting.IO.LuaSkeleton // The expression might consist of multiple nested functions var fArgs = bare.DeconstructApply(); - if (fArgs != null) - { + if (fArgs != null) { var (f, args) = fArgs.Value; var baseFunc = (Function) f; - if (baseFunc.Name.Equals(Funcs.Id.Name)) - { + if (baseFunc.Name.Equals(Funcs.Id.Name)) { // This is an ugly hack return ToLua(args.First()); } - - if(baseFunc.Name.Equals(Funcs.Dot.Name)) - { - if (args.Count == 1 || forceFirstArgInDot) - { + if (baseFunc.Name.Equals(Funcs.Dot.Name)) { + if (args.Count == 1 || forceFirstArgInDot) { return ToLua(args[0]); } - + var argsAsLua = args.Select(arg => ToLua(arg, key)).ToList(); var fName = argsAsLua[0]; - var actualArgs = - string.Join(",",argsAsLua.GetRange(1, argsAsLua.Count - 1)); - return $"{fName}({actualArgs})"; + var actualArgs = + string.Join(",", argsAsLua.GetRange(1, argsAsLua.Count - 1)); + return $"{fName}({actualArgs})"; } - + AddDep(baseFunc.Name); var argExpressions = new List(); - foreach (var arg in args) - { + foreach (var arg in args) { argExpressions.Add(ToLua(arg, key)); } @@ -169,14 +155,12 @@ namespace AspectedRouting.IO.LuaSkeleton var collected = new List(); - switch (bare) - { + switch (bare) { case LuaLiteral lua: return lua.Lua; case FunctionCall fc: var called = _context.DefinedFunctions[fc.CalledFunctionName]; - if (called.ProfileInternal) - { + if (called.ProfileInternal) { return called.Name; } @@ -189,15 +173,12 @@ namespace AspectedRouting.IO.LuaSkeleton return MappingToLua(m).Indent(); case Function f: var fName = f.Name.TrimStart('$'); - if (Funcs.Builtins.ContainsKey(fName)) - { + if (Funcs.Builtins.ContainsKey(fName)) { AddDep(f.Name); } - else - { + else { var definedFunc = _context.DefinedFunctions[fName]; - if (definedFunc.ProfileInternal) - { + if (definedFunc.ProfileInternal) { return f.Name; } @@ -219,13 +200,11 @@ namespace AspectedRouting.IO.LuaSkeleton public string MappingToLua(Mapping m) { var isConstant = true; - var contents = m.StringToResultFunctions.Select(kv => - { + var contents = m.StringToResultFunctions.Select(kv => { var (key, expr) = kv; var left = "[\"" + key + "\"]"; - if (Regex.IsMatch(key, "^[a-zA-Z][_a-zA-Z-9]*$")) - { + if (Regex.IsMatch(key, "^[a-zA-Z][_a-zA-Z-9]*$")) { left = key; } @@ -233,7 +212,8 @@ namespace AspectedRouting.IO.LuaSkeleton if (luaExpr.Contains("tags")) { isConstant = false; } - return left + " = " + luaExpr ; + + return left + " = " + luaExpr; } ); var mapping = @@ -245,19 +225,17 @@ namespace AspectedRouting.IO.LuaSkeleton } return mapping; - } /// - /// Neatly creates a value expression in lua, based on a constant + /// Neatly creates a value expression in lua, based on a constant /// /// /// private string ConstantToLua(Constant c) { var o = c.Evaluate(_context); - switch (o) - { + switch (o) { case LuaLiteral lua: return lua.Lua; case IExpression e: @@ -268,15 +246,15 @@ namespace AspectedRouting.IO.LuaSkeleton return "" + d; case string s: return '"' + s.Replace("\"", "\\\"") + '"'; + case null: + return "nil"; case ValueTuple unpack: return unpack.Item1 + "[" + unpack.Item2 + "]"; case IEnumerable ls: var t = ((ListType) c.Types.First()).InnerType; - return "{" + string.Join(", ", ls.Select(obj => - { + return "{" + string.Join(", ", ls.Select(obj => { var objInConstant = new Constant(t, obj); - if (obj is Constant asConstant) - { + if (obj is Constant asConstant) { objInConstant = asConstant; } diff --git a/AspectedRouting/IO/LuaSkeleton/LuaStringExtensions.cs b/AspectedRouting/IO/LuaSkeleton/LuaStringExtensions.cs index 5f28130..22e360b 100644 --- a/AspectedRouting/IO/LuaSkeleton/LuaStringExtensions.cs +++ b/AspectedRouting/IO/LuaSkeleton/LuaStringExtensions.cs @@ -6,6 +6,7 @@ namespace AspectedRouting.IO.itinero1 { public static class LuaStringExtensions { + public static string ToLuaTable(this Dictionary tags) { var contents = tags.Select(kv => diff --git a/AspectedRouting/IO/jsonParser/JsonParser.ParseAspectProfile.cs b/AspectedRouting/IO/jsonParser/JsonParser.ParseAspectProfile.cs index ac5fda2..f681bdc 100644 --- a/AspectedRouting/IO/jsonParser/JsonParser.ParseAspectProfile.cs +++ b/AspectedRouting/IO/jsonParser/JsonParser.ParseAspectProfile.cs @@ -301,6 +301,9 @@ namespace AspectedRouting.IO.jsonParser return new Constant(s); } + if (e.ValueKind == JsonValueKind.Null) { + return new Constant(new Var("a"), null); + } throw new Exception("Could not parse " + e); } diff --git a/AspectedRouting/IO/lua/if_then_else.lua b/AspectedRouting/IO/lua/if_then_else.lua index 02fad15..8330772 100644 --- a/AspectedRouting/IO/lua/if_then_else.lua +++ b/AspectedRouting/IO/lua/if_then_else.lua @@ -1,5 +1,5 @@ function if_then_else(condition, thn, els) - if (condition ~= nil and (condition == "yes" or condition == true or condition == "true") then + if (condition ~= nil and (condition == "yes" or condition == true or condition == "true")) then return thn else return els -- if no third parameter is given, 'els' will be nil diff --git a/AspectedRouting/IO/lua/mustMatch.lua b/AspectedRouting/IO/lua/mustMatch.lua index 25d0b36..66ab3a4 100644 --- a/AspectedRouting/IO/lua/mustMatch.lua +++ b/AspectedRouting/IO/lua/mustMatch.lua @@ -15,18 +15,26 @@ When applied on the tags {"a" : "X"}, this yields the table {"a":"yes", "b":"yes MustMatch checks that every key in this last table yields yes - even if it is not in the original tags! +Arguments: +- The tags of the feature +- The result table, where 'attributes_to_keep' might be set +- needed_keys which indicate which keys must be present in 'tags' +- table which is the table to match + ]] function must_match(tags, result, needed_keys, table) for _, key in ipairs(needed_keys) do - local v = table[key] -- use the table here, as a tag that must _not_ match might be 'nil' in the tags + local v = tags[key] if (v == nil) then + -- a key is missing... return false end local mapping = table[key] if (type(mapping) == "table") then + -- we have to map the value with a function: local resultValue = mapping[v] - if (resultValue == nil or + if (resultValue ~= nil or -- actually, having nil for a mapping is fine for this function!. resultValue == false or resultValue == "no" or resultValue == "false") then diff --git a/AspectedRouting/Language/Functions/Mapping.cs b/AspectedRouting/Language/Functions/Mapping.cs index dcda7c2..de745d6 100644 --- a/AspectedRouting/Language/Functions/Mapping.cs +++ b/AspectedRouting/Language/Functions/Mapping.cs @@ -7,6 +7,13 @@ using Type = AspectedRouting.Language.Typ.Type; namespace AspectedRouting.Language.Functions { + /// + /// The constructor takes a dictionary "key --> expression". + /// If a string is given as argument, the respective argument is returned. If the key is not found, 'null' is returned + /// + /// If a table/dictionary/collection of tags is given, the key of the arguments is used and the expression is used as function on every respective value of the argument. + /// If the key is not found, null is returned for this expression instead. + /// public class Mapping : Function { public readonly Dictionary StringToResultFunctions; diff --git a/AspectedRouting/Language/Functions/MustMatch.cs b/AspectedRouting/Language/Functions/MustMatch.cs index 8ed286d..29668d5 100644 --- a/AspectedRouting/Language/Functions/MustMatch.cs +++ b/AspectedRouting/Language/Functions/MustMatch.cs @@ -2,47 +2,44 @@ using System.Collections.Generic; using System.Linq; using AspectedRouting.Language.Expression; using AspectedRouting.Language.Typ; -using Type = AspectedRouting.Language.Typ.Type; namespace AspectedRouting.Language.Functions { public class MustMatch : Function { - public override string Description { get; } = - "Checks that every specified key is present and gives a non-false value\n." + - "" + - "\n" + - "If, on top, a value is present with a mapping, every key/value will be executed and must return a value that is not 'no' or 'false'\n" + - "Note that this is a privileged builtin function, as the parser will automatically inject the keys used in the called function."; - - public override List ArgNames { get; } = new List - { - "neededKeys (filled in by parser)", - "f" - }; - public MustMatch() : base("mustMatch", true, - new[] - { + new[] { // [String] -> (Tags -> [string]) -> Tags -> bool Curry.ConstructFrom(Typs.Bool, // Result type on top! new ListType(Typs.String), // List of keys to check for new Curry(Typs.Tags, new ListType(Typs.String)), // The function to execute on every key Typs.Tags // The tags to apply this on ) - }) - { - } + }) { } - private MustMatch(IEnumerable types) : base("mustMatch", types) - { - } + private MustMatch(IEnumerable types) : base("mustMatch", types) { } + + public override string Description { get; } = Utils.Lines( + "Checks that every specified key is present and gives a non-false value.\n", + "If, on top, a value is present with a mapping, every key/value will be executed and must return a value that is not 'no' or 'false'. Note that 'null' is considered as true here too!", + "Note that this is a privileged builtin function, as the parser will automatically inject the keys used in the called function.\n", + "", + "Usage example", + "-------------", + "", + "`\'mustMatchKeys\': { \"highway\": { \"proposed\": \"no\", \"undefined\":null }}`", + "which will return 'yes' for {highway=residential}, {highway=living_street}, ..., but return 'no' for {highway=proposed}, but also for {some_other_key=xxx}", + "Also note that {highway=undefined} will return (somewhat surprisingly) 'yes' too - as null-values are considered as true here too!"); + + public override List ArgNames { get; } = new List { + "neededKeys (do not specify in source code -added automatically by the parser)", + "f" + }; public override IExpression Specialize(IEnumerable allowedTypes) { var unified = Types.SpecializeTo(allowedTypes); - if (unified == null) - { + if (unified == null) { return null; } @@ -51,36 +48,38 @@ namespace AspectedRouting.Language.Functions public override object Evaluate(Context c, params IExpression[] arguments) { - var neededKeys = (IEnumerable) arguments[0].Evaluate(c); + var neededKeys = (IEnumerable) arguments[0].Evaluate(c); var function = arguments[1]; var tags = (Dictionary) arguments[2].Evaluate(c); - foreach (var oo in neededKeys) - { + foreach (var oo in neededKeys) { var o = oo; - while (o is IExpression e) - { + while (o is IExpression e) { o = e.Evaluate(c); } - if (!(o is string tagKey)) - { + + if (!(o is string tagKey)) { continue; } - if (!tags.ContainsKey(tagKey)) return "no"; + if (!tags.ContainsKey(tagKey)) { + // A required key is missing: return 'no' + return "no"; + } } var result = (IEnumerable) function.Evaluate(c, new Constant(tags)); - if (!result.Any(o => + if (result.Any(o => o == null || - (o is string s && (s.Equals("no") || s.Equals("false"))))) - { - return "yes"; + o is string s && (s.Equals("no") || s.Equals("false")))) { + // The mapped function is executed. If the mapped function gives 'no', null or 'false' for any value, "no" is returned + return "no"; } - return "no"; + return "yes"; + } } } \ No newline at end of file diff --git a/AspectedRouting/Tests/FunctionTestSuite.cs b/AspectedRouting/Tests/FunctionTestSuite.cs index 4834280..3bcfd19 100644 --- a/AspectedRouting/Tests/FunctionTestSuite.cs +++ b/AspectedRouting/Tests/FunctionTestSuite.cs @@ -43,7 +43,7 @@ namespace AspectedRouting.Tests var tags = new Dictionary(); for (var i = 0; i < keys.Count; i++) { if (i < vals.Count && !string.IsNullOrEmpty(vals[i])) { - tags[keys[i]] = vals[i]; + tags[keys[i]] = vals[i].Trim(new []{'"'}).Replace("\"","\\\""); } } diff --git a/AspectedRouting/Tests/ProfileTestSuite.cs b/AspectedRouting/Tests/ProfileTestSuite.cs index 325034a..ab71a11 100644 --- a/AspectedRouting/Tests/ProfileTestSuite.cs +++ b/AspectedRouting/Tests/ProfileTestSuite.cs @@ -97,7 +97,7 @@ namespace AspectedRouting.Tests { if (i < vals.Count && !string.IsNullOrEmpty(vals[i])) { - tags[keys[i]] = vals[i]; + tags[keys[i]] = vals[i].Trim(new []{'\"'}).Replace("\"","\\\""); } }