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;/TestAncestor&gt;
&lt;/SessionState&gt;</s:String> &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;Project Location="/home/pietervdvn/git/AspectedRouting/AspectedRouting.Test" Presentation="&amp;lt;AspectedRouting.Test&amp;gt;" /&gt;
&lt;/SessionState&gt;</s:String> &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;Project Location="/home/pietervdvn/git/AspectedRouting/AspectedRouting.Test" Presentation="&amp;lt;AspectedRouting.Test&amp;gt;" /&gt;
&lt;/SessionState&gt;</s:String> &lt;/SessionState&gt;</s:String>
</wpf:ResourceDictionary> </wpf:ResourceDictionary>

View file

@ -26,8 +26,7 @@ namespace AspectedRouting.IO.LuaSkeleton
, UnApply( , UnApply(
IsFunc(Funcs.StringStringToTags), IsFunc(Funcs.StringStringToTags),
Assign(collectedMapping)) Assign(collectedMapping))
).Invoke(bare)) ).Invoke(bare)) {
{
AddDep(Funcs.FirstOf.Name); AddDep(Funcs.FirstOf.Name);
return "first_match_of(tags, result, \n" + return "first_match_of(tags, result, \n" +
" " + ToLua(order.First(), key) + "," + " " + ToLua(order.First(), key) + "," +
@ -42,8 +41,7 @@ namespace AspectedRouting.IO.LuaSkeleton
, UnApply( , UnApply(
IsFunc(Funcs.StringStringToTags), IsFunc(Funcs.StringStringToTags),
Assign(collectedMapping)) Assign(collectedMapping))
).Invoke(bare)) ).Invoke(bare)) {
{
AddDep(Funcs.MustMatch.Name); AddDep(Funcs.MustMatch.Name);
return "must_match(tags, result, \n" + return "must_match(tags, result, \n" +
" " + ToLua(order.First(), key) + "," + " " + ToLua(order.First(), key) + "," +
@ -54,37 +52,32 @@ namespace AspectedRouting.IO.LuaSkeleton
if (UnApply( if (UnApply(
IsFunc(Funcs.MemberOf), IsFunc(Funcs.MemberOf),
Any Any
).Invoke(bare)) ).Invoke(bare)) {
{
AddDep("memberOf"); AddDep("memberOf");
return "memberOf(funcName, parameters, tags, result)"; return "memberOf(funcName, parameters, tags, result)";
} }
var collectedList = new List<IExpression>(); var collectedList = new List<IExpression>();
var func = new List<IExpression>(); var func = new List<IExpression>();
if ( if (
UnApply( UnApply(
UnApply(IsFunc(Funcs.Dot), Assign(func)), UnApply(IsFunc(Funcs.Dot), Assign(func)),
UnApply(IsFunc(Funcs.ListDot), UnApply(IsFunc(Funcs.ListDot),
Assign(collectedList))).Invoke(bare)) Assign(collectedList))).Invoke(bare)) {
{
var exprs = (IEnumerable<IExpression>) ((Constant) collectedList.First()).Evaluate(_context); var exprs = (IEnumerable<IExpression>) ((Constant) collectedList.First()).Evaluate(_context);
var luaExprs = new List<string>(); var luaExprs = new List<string>();
var funcName = func.First().ToString().TrimStart('$'); var funcName = func.First().ToString().TrimStart('$');
AddDep(funcName); AddDep(funcName);
foreach (var expr in exprs) foreach (var expr in exprs) {
{
var c = new List<IExpression>(); 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)); luaExprs.Add(ToLua(c.First(), key));
continue; continue;
} }
if (expr.Types.First() is Curry curry if (expr.Types.First() is Curry curry
&& curry.ArgType.Equals(Typs.Tags)) && curry.ArgType.Equals(Typs.Tags)) {
{
var lua = ToLua(expr, key); var lua = ToLua(expr, key);
luaExprs.Add(lua); luaExprs.Add(lua);
} }
@ -93,6 +86,7 @@ namespace AspectedRouting.IO.LuaSkeleton
return "\n " + funcName + "({\n " + string.Join(",\n ", luaExprs) + return "\n " + funcName + "({\n " + string.Join(",\n ", luaExprs) +
"\n })"; "\n })";
} }
collectedMapping.Clear(); collectedMapping.Clear();
var dottedFunction = new List<IExpression>(); var dottedFunction = new List<IExpression>();
dottedFunction.Clear(); dottedFunction.Clear();
@ -104,8 +98,7 @@ namespace AspectedRouting.IO.LuaSkeleton
UnApply( UnApply(
IsFunc(Funcs.StringStringToTags), IsFunc(Funcs.StringStringToTags),
Assign(collectedMapping))).Invoke(bare) Assign(collectedMapping))).Invoke(bare)
) ) {
{
var mapping = (Mapping) collectedMapping.First(); var mapping = (Mapping) collectedMapping.First();
var baseFunc = (Function) dottedFunction.First(); var baseFunc = (Function) dottedFunction.First();
AddDep(baseFunc.Name); 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 // The expression might be a function which still expects a string (the value from the tag) as argument
if (!(bare is Mapping) && if (!(bare is Mapping) &&
bare.Types.First() is Curry curr && 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 + "\""))); var applied = new Apply(bare, new Constant(curr.ArgType, ("tags", "\"" + key + "\"")));
return ToLua(applied.Optimize(), key); return ToLua(applied.Optimize(), key);
} }
@ -130,37 +122,31 @@ namespace AspectedRouting.IO.LuaSkeleton
// The expression might consist of multiple nested functions // The expression might consist of multiple nested functions
var fArgs = bare.DeconstructApply(); var fArgs = bare.DeconstructApply();
if (fArgs != null) if (fArgs != null) {
{
var (f, args) = fArgs.Value; var (f, args) = fArgs.Value;
var baseFunc = (Function) f; var baseFunc = (Function) f;
if (baseFunc.Name.Equals(Funcs.Id.Name)) if (baseFunc.Name.Equals(Funcs.Id.Name)) {
{
// This is an ugly hack // This is an ugly hack
return ToLua(args.First()); 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]); return ToLua(args[0]);
} }
var argsAsLua = args.Select(arg => ToLua(arg, key)).ToList(); var argsAsLua = args.Select(arg => ToLua(arg, key)).ToList();
var fName = argsAsLua[0]; var fName = argsAsLua[0];
var actualArgs = var actualArgs =
string.Join(",",argsAsLua.GetRange(1, argsAsLua.Count - 1)); string.Join(",", argsAsLua.GetRange(1, argsAsLua.Count - 1));
return $"{fName}({actualArgs})"; return $"{fName}({actualArgs})";
} }
AddDep(baseFunc.Name); AddDep(baseFunc.Name);
var argExpressions = new List<string>(); var argExpressions = new List<string>();
foreach (var arg in args) foreach (var arg in args) {
{
argExpressions.Add(ToLua(arg, key)); argExpressions.Add(ToLua(arg, key));
} }
@ -169,14 +155,12 @@ namespace AspectedRouting.IO.LuaSkeleton
var collected = new List<IExpression>(); var collected = new List<IExpression>();
switch (bare) switch (bare) {
{
case LuaLiteral lua: case LuaLiteral lua:
return lua.Lua; return lua.Lua;
case FunctionCall fc: case FunctionCall fc:
var called = _context.DefinedFunctions[fc.CalledFunctionName]; var called = _context.DefinedFunctions[fc.CalledFunctionName];
if (called.ProfileInternal) if (called.ProfileInternal) {
{
return called.Name; return called.Name;
} }
@ -189,15 +173,12 @@ namespace AspectedRouting.IO.LuaSkeleton
return MappingToLua(m).Indent(); return MappingToLua(m).Indent();
case Function f: case Function f:
var fName = f.Name.TrimStart('$'); var fName = f.Name.TrimStart('$');
if (Funcs.Builtins.ContainsKey(fName)) if (Funcs.Builtins.ContainsKey(fName)) {
{
AddDep(f.Name); AddDep(f.Name);
} }
else else {
{
var definedFunc = _context.DefinedFunctions[fName]; var definedFunc = _context.DefinedFunctions[fName];
if (definedFunc.ProfileInternal) if (definedFunc.ProfileInternal) {
{
return f.Name; return f.Name;
} }
@ -219,13 +200,11 @@ namespace AspectedRouting.IO.LuaSkeleton
public string MappingToLua(Mapping m) public string MappingToLua(Mapping m)
{ {
var isConstant = true; var isConstant = true;
var contents = m.StringToResultFunctions.Select(kv => var contents = m.StringToResultFunctions.Select(kv => {
{
var (key, expr) = kv; var (key, expr) = kv;
var left = "[\"" + key + "\"]"; 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; left = key;
} }
@ -233,7 +212,8 @@ namespace AspectedRouting.IO.LuaSkeleton
if (luaExpr.Contains("tags")) { if (luaExpr.Contains("tags")) {
isConstant = false; isConstant = false;
} }
return left + " = " + luaExpr ;
return left + " = " + luaExpr;
} }
); );
var mapping = var mapping =
@ -245,19 +225,17 @@ namespace AspectedRouting.IO.LuaSkeleton
} }
return mapping; return mapping;
} }
/// <summary> /// <summary>
/// Neatly creates a value expression in lua, based on a constant /// Neatly creates a value expression in lua, based on a constant
/// </summary> /// </summary>
/// <param name="c"></param> /// <param name="c"></param>
/// <returns></returns> /// <returns></returns>
private string ConstantToLua(Constant c) private string ConstantToLua(Constant c)
{ {
var o = c.Evaluate(_context); var o = c.Evaluate(_context);
switch (o) switch (o) {
{
case LuaLiteral lua: case LuaLiteral lua:
return lua.Lua; return lua.Lua;
case IExpression e: case IExpression e:
@ -268,15 +246,15 @@ namespace AspectedRouting.IO.LuaSkeleton
return "" + d; return "" + d;
case string s: case string s:
return '"' + s.Replace("\"", "\\\"") + '"'; return '"' + s.Replace("\"", "\\\"") + '"';
case null:
return "nil";
case ValueTuple<string, string> unpack: case ValueTuple<string, string> unpack:
return unpack.Item1 + "[" + unpack.Item2 + "]"; return unpack.Item1 + "[" + unpack.Item2 + "]";
case IEnumerable<object> ls: case IEnumerable<object> ls:
var t = ((ListType) c.Types.First()).InnerType; 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); var objInConstant = new Constant(t, obj);
if (obj is Constant asConstant) if (obj is Constant asConstant) {
{
objInConstant = asConstant; objInConstant = asConstant;
} }

View file

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

View file

@ -301,6 +301,9 @@ namespace AspectedRouting.IO.jsonParser
return new Constant(s); return new Constant(s);
} }
if (e.ValueKind == JsonValueKind.Null) {
return new Constant(new Var("a"), null);
}
throw new Exception("Could not parse " + e); throw new Exception("Could not parse " + e);
} }

View file

@ -1,5 +1,5 @@
function if_then_else(condition, thn, els) 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 return thn
else else
return els -- if no third parameter is given, 'els' will be nil 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! 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) function must_match(tags, result, needed_keys, table)
for _, key in ipairs(needed_keys) do 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 if (v == nil) then
-- a key is missing...
return false return false
end end
local mapping = table[key] local mapping = table[key]
if (type(mapping) == "table") then if (type(mapping) == "table") then
-- we have to map the value with a function:
local resultValue = mapping[v] 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 == false or
resultValue == "no" or resultValue == "no" or
resultValue == "false") then resultValue == "false") then

View file

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

View file

@ -2,47 +2,44 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using AspectedRouting.Language.Expression; using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Typ; using AspectedRouting.Language.Typ;
using Type = AspectedRouting.Language.Typ.Type;
namespace AspectedRouting.Language.Functions namespace AspectedRouting.Language.Functions
{ {
public class MustMatch : Function 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, public MustMatch() : base("mustMatch", true,
new[] new[] {
{
// [String] -> (Tags -> [string]) -> Tags -> bool // [String] -> (Tags -> [string]) -> Tags -> bool
Curry.ConstructFrom(Typs.Bool, // Result type on top! Curry.ConstructFrom(Typs.Bool, // Result type on top!
new ListType(Typs.String), // List of keys to check for 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 new Curry(Typs.Tags, new ListType(Typs.String)), // The function to execute on every key
Typs.Tags // The tags to apply this on 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) public override IExpression Specialize(IEnumerable<Type> allowedTypes)
{ {
var unified = Types.SpecializeTo(allowedTypes); var unified = Types.SpecializeTo(allowedTypes);
if (unified == null) if (unified == null) {
{
return null; return null;
} }
@ -51,36 +48,38 @@ namespace AspectedRouting.Language.Functions
public override object Evaluate(Context c, params IExpression[] arguments) 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 function = arguments[1];
var tags = (Dictionary<string, string>) arguments[2].Evaluate(c); var tags = (Dictionary<string, string>) arguments[2].Evaluate(c);
foreach (var oo in neededKeys) foreach (var oo in neededKeys) {
{
var o = oo; var o = oo;
while (o is IExpression e) while (o is IExpression e) {
{
o = e.Evaluate(c); o = e.Evaluate(c);
} }
if (!(o is string tagKey))
{ if (!(o is string tagKey)) {
continue; 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)); var result = (IEnumerable<object>) function.Evaluate(c, new Constant(tags));
if (!result.Any(o => if (result.Any(o =>
o == null || o == null ||
(o is string s && (s.Equals("no") || s.Equals("false"))))) 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 "yes"; return "no";
} }
return "no"; return "yes";
} }
} }
} }

View file

@ -43,7 +43,7 @@ namespace AspectedRouting.Tests
var tags = new Dictionary<string, string>(); var tags = new Dictionary<string, string>();
for (var i = 0; i < keys.Count; i++) { for (var i = 0; i < keys.Count; i++) {
if (i < vals.Count && !string.IsNullOrEmpty(vals[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])) if (i < vals.Count && !string.IsNullOrEmpty(vals[i]))
{ {
tags[keys[i]] = vals[i]; tags[keys[i]] = vals[i].Trim(new []{'\"'}).Replace("\"","\\\"");
} }
} }