blob: b7472005387eac02842a9313fea547d1cf9be39c [file] [log] [blame]
#region License
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
#endregion
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using Gremlin.Net.Process.Traversal;
namespace Gremlin.Net.IntegrationTest.Gherkin.TraversalEvaluation
{
public class TraversalParser
{
private static readonly IDictionary<string, Func<GraphTraversalSource, ITraversal>> FixedTranslations =
new Dictionary<string, Func<GraphTraversalSource, ITraversal>>
{
{ "g.V().fold().count(Scope.local)", g => g.V().Fold().Count(Scope.Local)},
{ "g.inject(10,20,null,20,10,10).groupCount(\"x\").dedup().as(\"y\").project(\"a\",\"b\").by().by(__.select(\"x\").select(__.select(\"y\")))",
g => g.Inject<object>(10,20,null,20,10,10).GroupCount("x").Dedup().As("y").Project<object>("a","b").By().By(__.Select<int>("x").Select<object>(__.Select<int>("y")))},
{ "g.inject(m).select(\"name\",\"age\")", g => g.Inject(new Dictionary<string,object>() {{"name", "marko"}, {"age", null}}).Select<object>("name", "age") }
};
private static readonly Regex RegexNumeric =
new Regex(@"\d+(\.\d+)?(?:l|f)?", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex RegexEnum = new Regex(@"\w+\.\w+", RegexOptions.Compiled);
private static readonly Regex RegexIO = new Regex(@"IO.\w+", RegexOptions.Compiled);
private static readonly Regex RegexWithOptions = new Regex(@"WithOptions.\w+", RegexOptions.Compiled);
private static readonly Regex RegexParam = new Regex(@"\w+", RegexOptions.Compiled);
private static readonly HashSet<Type> NumericTypes = new HashSet<Type>
{
typeof(int), typeof(long), typeof(double), typeof(float), typeof(short), typeof(decimal), typeof(byte)
};
internal static ITraversal GetTraversal(string traversalText, GraphTraversalSource g,
IDictionary<string, object> contextParameterValues)
{
if (!FixedTranslations.TryGetValue(traversalText, out var traversalBuilder))
{
var tokens = ParseTraversal(traversalText);
return GetTraversalFromTokens(tokens, g, contextParameterValues, traversalText);
}
return traversalBuilder(g);
}
internal static ITraversal GetTraversalFromTokens(IList<Token> tokens, GraphTraversalSource g,
IDictionary<string, object> contextParameterValues,
string traversalText)
{
object instance;
Type instanceType;
if (tokens[0].Name == "g")
{
instance = g;
instanceType = g.GetType();
}
else if (tokens[0].Name == "__")
{
instance = null;
instanceType = typeof(__);
}
else
{
throw BuildException(traversalText);
}
for (var i = 1; i < tokens.Count; i++)
{
var token = tokens[i];
token.SetContextParameterValues(contextParameterValues);
var name = GetCsharpName(token.Name);
var methods = instanceType.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static)
.Where(m => m.Name == name).ToList();
var method = GetClosestMethod(methods, token.Parameters);
if (method == null)
{
throw new InvalidOperationException($"Traversal method '{tokens[i].Name}' not found for testing");
}
var parameterValues = BuildParameters(method, token, out var genericParameters);
method = BuildGenericMethod(method, genericParameters, parameterValues);
instance = method.Invoke(instance, parameterValues);
instanceType = instance.GetType();
}
return (ITraversal) instance;
}
/// <summary>
/// Find the method that supports the amount of parameters provided
/// </summary>
private static MethodInfo GetClosestMethod(IList<MethodInfo> methods, IList<ITokenParameter> tokenParameters)
{
if (methods.Count == 0)
{
return null;
}
if (methods.Count == 1)
{
return methods[0];
}
var ordered = methods.OrderBy(m => m.GetParameters().Length);
if (tokenParameters.Count == 0)
{
return ordered.First();
}
MethodInfo lastMethod = null;
var compatibleMethods = new Dictionary<int, MethodInfo>();
foreach (var method in ordered)
{
var methodParameters = method.GetParameters();
var requiredParameters = methodParameters.Length;
if (requiredParameters > 0 && IsParamsArray(methodParameters.Last()))
{
// Params array can be not provided
requiredParameters--;
}
if (tokenParameters.Count < requiredParameters)
{
continue;
}
lastMethod = method;
var matched = true;
var exactMatches = 0;
for (var i = 0; i < tokenParameters.Count; i++)
{
if (methodParameters.Length <= i)
{
// The method contains less parameters (and no params array) than provided
matched = false;
break;
}
var methodParameter = methodParameters[i];
var tokenParameterType = tokenParameters[i].GetParameterType();
// Match either the same parameter type
matched = methodParameter.ParameterType == tokenParameterType;
if (matched)
{
exactMatches++;
}
else if (IsParamsArray(methodParameter))
{
matched = methodParameter.ParameterType == typeof(object[]) ||
methodParameter.ParameterType.GetElementType() == tokenParameterType;
// The method has params array, no further parameters are going to be defined
break;
}
else
{
if (IsNumeric(methodParameter.ParameterType) && IsNumeric(tokenParameterType))
{
// Account for implicit conversion of numeric values as an exact match
exactMatches++;
}
else if (!methodParameter.ParameterType.GetTypeInfo().IsAssignableFrom(tokenParameterType))
{
// Not a match
break;
}
// Is assignable to the parameter type
matched = true;
}
}
if (matched)
{
compatibleMethods[exactMatches] = method;
}
}
// Attempt to use the method with the higher number of matches or the last one
return compatibleMethods.OrderByDescending(kv => kv.Key).Select(kv => kv.Value).FirstOrDefault() ??
lastMethod;
}
private static bool IsNumeric(Type t) => NumericTypes.Contains(t);
private static bool IsParamsArray(ParameterInfo methodParameter)
{
return methodParameter.IsDefined(typeof(ParamArrayAttribute), false);
}
private static MethodInfo BuildGenericMethod(MethodInfo method, IDictionary<string, Type> genericParameters,
object[] parameterValues)
{
if (!method.IsGenericMethod)
{
return method;
}
var genericArgs = method.GetGenericArguments();
var types = new Type[genericArgs.Length];
for (var i = 0; i < genericArgs.Length; i++)
{
var name = genericArgs[i].Name;
Type type;
if (!genericParameters.TryGetValue(name, out type))
{
// Try to infer it from the name based on modern graph
type = ModernGraphTypeInformation.GetTypeArguments(method, parameterValues, i);
}
if (type == null)
{
throw new InvalidOperationException(
$"Can not build traversal to test as '{method.Name}()' method is generic and type '{name}'" +
" can not be inferred");
}
types[i] = type;
}
return method.MakeGenericMethod(types);
}
private static object[] BuildParameters(MethodInfo method, Token token,
out IDictionary<string, Type> genericParameterTypes)
{
var paramsInfo = method.GetParameters();
var parameters = new object[paramsInfo.Length];
genericParameterTypes = new Dictionary<string, Type>();
for (var i = 0; i < paramsInfo.Length; i++)
{
var info = paramsInfo[i];
object value = null;
if (token.Parameters.Count > i)
{
var tokenParameter = token.Parameters[i];
value = tokenParameter.GetValue();
if (info.ParameterType.IsGenericParameter)
{
// We've provided a value for parameter of a generic type, we can infer the
// type of the generic argument based on the parameter.
// For example, in the case of `Constant<E2>(E2 value)`
// if we have the type of value we have the type of E2.
genericParameterTypes.Add(info.ParameterType.Name, tokenParameter.GetParameterType());
}
else if (IsParamsArray(info) && info.ParameterType.GetElementType().IsGenericParameter)
{
// Its a method where the type parameter comes from an params Array
// e.g., Inject<S>(params S[] value)
var type = tokenParameter.GetParameterType();
genericParameterTypes.Add(info.ParameterType.GetElementType().Name, type);
// Use a placeholder value
value = type.IsValueType ? Activator.CreateInstance(type) : new object();
}
if (info.ParameterType != tokenParameter.GetParameterType() && IsNumeric(info.ParameterType) &&
IsNumeric(tokenParameter.GetParameterType()))
{
// Numeric conversion
value = Convert.ChangeType(value, info.ParameterType);
}
}
if (IsParamsArray(info))
{
// For `params type[] value` we should provide an empty array
if (value == null)
{
value = Array.CreateInstance(info.ParameterType.GetElementType(), 0);
}
else if (!value.GetType().IsArray)
{
// An array with the parameter values
// No more method parameters after this one
var elementType = info.ParameterType.GetElementType();
if (elementType.IsGenericParameter)
{
// The Array element type is generic, so we use type of the value to specify it
elementType = value.GetType();
}
var arr = Array.CreateInstance(elementType, token.Parameters.Count - i);
for (var j = 0; j < token.Parameters.Count - i; j++)
{
arr.SetValue(token.Parameters[i + j].GetValue(), j);
}
value = arr;
}
}
parameters[i] = value ?? GetDefault(info.ParameterType);
}
return parameters;
}
public static object GetDefault(Type type)
{
return type.GetTypeInfo().IsValueType ? Activator.CreateInstance(type) : null;
}
internal static string GetCsharpName(string part)
{
// Transform to PascalCasing and remove the parenthesis
return char.ToUpper(part[0]) + part.Substring(1);
}
private static Exception BuildException(string traversalText)
{
return new InvalidOperationException($"Can not build a traversal to test from '{traversalText}'");
}
internal static IList<Token> ParseTraversal(string traversalText)
{
var index = 0;
return ParseTokens(traversalText, ref index);
}
private static IList<Token> ParseTokens(string text, ref int i)
{
// Parser issue: quotes are not normalized
text = text.Replace("\\\"", "\"");
var result = new List<Token>();
var startIndex = i;
var parsing = ParsingPart.Name;
var parameters = new List<ITokenParameter>();
string name = null;
while (i < text.Length)
{
switch (text[i])
{
case '.':
if (parsing == ParsingPart.Name)
{
// The previous token was an object property, not a method
result.Add(new Token(text.Substring(startIndex, i - startIndex)));
}
startIndex = i + 1;
parameters = new List<ITokenParameter>();
parsing = ParsingPart.Name;
break;
case '(':
{
name = text.Substring(startIndex, i - startIndex);
parsing = ParsingPart.StartParameters;
// Start parsing from the next index
i++;
var param = ParseParameter(text, ref i);
if (param == null)
{
// The next character was a ')', empty params
// Evaluate the current position
continue;
}
parameters.Add(param);
break;
}
case ',' when parsing == ParsingPart.StartParameters && text.Length > i + 1 && text[i+1] != ' ':
case ' ' when parsing == ParsingPart.StartParameters && text.Length > i + 1 && text[i+1] != ' ' &&
text[i+1] != ')':
{
i++;
var param = ParseParameter(text, ref i);
if (param == null)
{
// The next character was a ')', empty params
// Evaluate the current position
continue;
}
parameters.Add(param);
break;
}
case ',' when parsing != ParsingPart.StartParameters:
case ')' when parsing != ParsingPart.StartParameters:
// The current nested object already ended
if (parsing == ParsingPart.Name)
{
// The previous token was an object property, not a method and finished
result.Add(new Token(text.Substring(startIndex, i - startIndex)));
}
i--;
return result;
case ')':
parsing = ParsingPart.EndParameters;
result.Add(new Token(name, parameters));
break;
}
i++;
}
if (parsing == ParsingPart.Name)
{
// The previous token was an object property, not a method and finished
result.Add(new Token(text.Substring(startIndex, i - startIndex)));
}
return result;
}
private static ITokenParameter ParseParameter(string text, ref int i)
{
var firstChar = text[i];
while (char.IsWhiteSpace(firstChar))
{
firstChar = text[++i];
}
if (firstChar == ')')
{
return null;
}
if (firstChar == '"' || firstChar == '\'')
{
return StringParameter.Parse(text, firstChar, ref i);
}
if (char.IsDigit(firstChar))
{
return ParseNumber(text, ref i);
}
if (text.Length >= i + 3 && text.Substring(i, 3) == "__.")
{
var startIndex = i;
var tokens = ParseTokens(text, ref i);
return new StaticTraversalParameter(tokens, text.Substring(startIndex, i - startIndex));
}
if (text.Length >= i + 6 && text.Substring(i, 6) == "TextP.")
{
return new TextPParameter(ParseTokens(text, ref i));
}
if (text.Substring(i, 2).StartsWith("P."))
{
return new PParameter(ParseTokens(text, ref i));
}
var parameterText = text.Substring(i, text.IndexOf(')', i) - i);
var separatorIndex = parameterText.IndexOf(',');
if (separatorIndex >= 0)
{
parameterText = parameterText.Substring(0, separatorIndex);
}
parameterText = parameterText.Trim();
if (parameterText == "")
{
return null;
}
if (parameterText == "true" || parameterText == "false")
{
i += parameterText.Length - 1;
return LiteralParameter.Create(Convert.ToBoolean(parameterText));
}
if (parameterText == "null")
{
i += parameterText.Length - 1;
return LiteralParameter.Create<object>(null);
}
if (RegexIO.IsMatch(parameterText))
{
i += parameterText.Length - 1;
return new IOParameter(parameterText);
}
if (RegexWithOptions.IsMatch(parameterText))
{
i += parameterText.Length - 1;
return new WithOptionsParameter(parameterText);
}
if (RegexEnum.IsMatch(parameterText))
{
i += parameterText.Length - 1;
return new TraversalEnumParameter(parameterText);
}
if (RegexParam.IsMatch(parameterText))
{
i += parameterText.Length - 1;
return new ContextBasedParameter(parameterText);
}
throw new NotSupportedException($"Parameter {parameterText} not supported");
}
private static ITokenParameter ParseNumber(string text, ref int i)
{
var match = RegexNumeric.Match(text, i);
if (!match.Success)
{
throw new InvalidOperationException(
$"Could not parse numeric value from the beginning of {text.Substring(i)}");
}
var numericText = match.Value.ToUpper();
i += match.Value.Length - 1;
if (numericText.EndsWith("L"))
{
return LiteralParameter.Create(Convert.ToInt64(match.Value.Substring(0, match.Value.Length - 1)));
}
if (numericText.EndsWith("F"))
{
return LiteralParameter.Create(Convert.ToSingle(match.Value.Substring(0, match.Value.Length - 1),
CultureInfo.InvariantCulture));
}
if (match.Groups[1].Value != "")
{
// Captured text with the decimal separator
return LiteralParameter.Create(Convert.ToDecimal(match.Value, CultureInfo.InvariantCulture));
}
return LiteralParameter.Create(Convert.ToInt32(match.Value));
}
private enum ParsingPart
{
Name,
StartParameters,
EndParameters
}
}
}