Fix various bugs, improve docs, allow 'null' in JSON, specify behaviour of must_match better

This commit is contained in:
Pieter Vander Vennet 2021-04-03 19:11:41 +02:00
parent 8e3383baec
commit e2cd6caa70
12 changed files with 172 additions and 102 deletions

View file

@ -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);
}
}
}

View file

@ -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<string, string> {
{"highway", "residential"}
})).Evaluate(new Context());
Assert.Equal("yes", residential);
var living = mm.Apply(new Constant(new Dictionary<string, string> {
{"highway", "living_street"}
})).Evaluate(new Context());
Assert.Equal("no", living);
var unknown = mm.Apply(new Constant(new Dictionary<string, string> {
{"highway", "unknown_type"}
})).Evaluate(new Context());
Assert.Equal("yes", unknown);
var missing = mm.Apply(new Constant(new Dictionary<string, string> {
{"proposed:highway", "unknown_type"}
})).Evaluate(new Context());
Assert.Equal("no", missing);
}
}
}

View file

@ -12,10 +12,10 @@
&lt;/TestAncestor&gt;
&lt;/SessionState&gt;</s:String>
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=a6a74f48_002D8456_002D43c7_002Dbbee_002Dd3da33a8a4be/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" Name="Integration_TestExamples" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=a6a74f48_002D8456_002D43c7_002Dbbee_002Dd3da33a8a4be/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" IsActive="True" Name="Integration_TestExamples" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;
&lt;Project Location="/home/pietervdvn/git/AspectedRouting/AspectedRouting.Test" Presentation="&amp;lt;AspectedRouting.Test&amp;gt;" /&gt;
&lt;/SessionState&gt;</s:String>
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=d2e3d58f_002Debff_002D4fb5_002D8d18_002Deafe85f4773d/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" IsActive="True" Name="SpecializeToCommonTypes_ValueAndFuncType_ShouldFail" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=d2e3d58f_002Debff_002D4fb5_002D8d18_002Deafe85f4773d/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" Name="SpecializeToCommonTypes_ValueAndFuncType_ShouldFail" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;
&lt;Project Location="/home/pietervdvn/git/AspectedRouting/AspectedRouting.Test" Presentation="&amp;lt;AspectedRouting.Test&amp;gt;" /&gt;
&lt;/SessionState&gt;</s:String>
</wpf:ResourceDictionary>

View file

@ -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<IExpression>();
var func = new List<IExpression>();
if (
UnApply(
UnApply(IsFunc(Funcs.Dot), Assign(func)),
UnApply(IsFunc(Funcs.ListDot),
Assign(collectedList))).Invoke(bare))
{
Assign(collectedList))).Invoke(bare)) {
var exprs = (IEnumerable<IExpression>) ((Constant) collectedList.First()).Evaluate(_context);
var luaExprs = new List<string>();
var funcName = func.First().ToString().TrimStart('$');
AddDep(funcName);
foreach (var expr in exprs)
{
foreach (var expr in exprs) {
var c = new List<IExpression>();
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<IExpression>();
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<string>();
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<IExpression>();
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;
}
/// <summary>
/// Neatly creates a value expression in lua, based on a constant
/// Neatly creates a value expression in lua, based on a constant
/// </summary>
/// <param name="c"></param>
/// <returns></returns>
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<string, string> unpack:
return unpack.Item1 + "[" + unpack.Item2 + "]";
case IEnumerable<object> 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;
}

View file

@ -6,6 +6,7 @@ namespace AspectedRouting.IO.itinero1
{
public static class LuaStringExtensions
{
public static string ToLuaTable(this Dictionary<string, string> tags)
{
var contents = tags.Select(kv =>

View file

@ -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);
}

View file

@ -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

View file

@ -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

View file

@ -7,6 +7,13 @@ using Type = AspectedRouting.Language.Typ.Type;
namespace AspectedRouting.Language.Functions
{
/// <summary>
/// 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.
/// </summary>
public class Mapping : Function
{
public readonly Dictionary<string, IExpression> StringToResultFunctions;

View file

@ -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<string> ArgNames { get; } = new List<string>
{
"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<Type> types) : base("mustMatch", types)
{
}
private MustMatch(IEnumerable<Type> 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<string> ArgNames { get; } = new List<string> {
"neededKeys (do not specify in source code -added automatically by the parser)",
"f"
};
public override IExpression Specialize(IEnumerable<Type> 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<object>) arguments[0].Evaluate(c);
var neededKeys = (IEnumerable<object>) arguments[0].Evaluate(c);
var function = arguments[1];
var tags = (Dictionary<string, string>) 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<object>) 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";
}
}
}

View file

@ -43,7 +43,7 @@ namespace AspectedRouting.Tests
var tags = new Dictionary<string, string>();
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("\"","\\\"");
}
}

View file

@ -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("\"","\\\"");
}
}