codecutout / StringToExpression

Convert strings into .NET expressions
MIT License
95 stars 22 forks source link

oData Contains, StartsWith, EndsWith? #9

Open JohnGalt1717 opened 4 years ago

JohnGalt1717 commented 4 years ago

Any plans to add these?

Looks like StartsWIth and EndsWith are anbled. substringof also is enabled but I think the parameters are in the wrong order. Adding contains should just be the same as substringof with the corrected parameters I would think?

JohnGalt1717 commented 4 years ago

The following code makes contains work properly:

        protected override IEnumerable<FunctionCallDefinition> FunctionDefinitions()
        {
            var defs = new List<FunctionCallDefinition>(base.FunctionDefinitions())
            {
                new FunctionCallDefinition(
                    name: "FN_CONTAINS",
                    regex: @"contains\(",
                    argumentTypes: new[] { typeof(string), typeof(string) },
                    expressionBuilder: (parameters) =>
                    {
                        return Expression.Call(
                            instance: parameters[0],
                            method: StringMembers.Contains,
                            arguments: new[] { parameters[1] });
                    })
            };

            return defs;
        }

Happy to do a pull request if that would help.

alex-davies commented 4 years ago

substringof is OData2 and, as unintuitive as it feels, the parameters are defined in that order. contains is OData4 which is why it was never implemented

It may make sense to make a OData4FilterLanguage and rename the existing one to OData2FilterLanguage.

Would need to go through the spec thoroughly to determine what other parts have changed. As it looks like there are a number of changes with dates and durations

Would also need setup test cases a bit differently. Currently I run another OData2 parser and ensure the results are the same. Would have to write out each case seperatly for OData4

I am more than happy to accept PRs, or if there is only a subset of OData4 you need to support you can create the provider in your own project. Most of the changes you should be able to do by defining new definitions

LandryDubus commented 4 years ago

Thanks for the example to support the 'Contains' canonic function of OData4. I was missing this as well as the 'In' operator of OData4.

Any ideas on how to support this one? It seems the in operator should have an orderOfPrecedence higher than anything else already implemented (Odata Doc) so we would need to remove all existing operator definition to recreate them with a new order of precedence? Or we could use the 0 value for orderOfPrecedence. We also have to be able to differentiate betweens parenthesis (brackets) used for precedence grouping or functions from those used for list of values.

The easy way around this is to rewrite the filter expression by replacing any A in (B,C) by (A eq B or A eq C) prior to parsing the expression with the library.

JohnGalt1717 commented 4 years ago

If it helps (I don't have a use for this) this should be able to be implemented as a simple expression of Contains which EF translates to IN (SELECT)

LandryDubus commented 4 years ago

Another workaround would be to handle the IN operator as a function instead of an operator. Instead of writing Property in ('B','C') like in the OData Specification we would write in(Property,'B','C') as in other functions like contains.

Here is an implementation:

        /// <summary>
        /// Returns the definitions for functions used within the language while adding a few new ones.
        /// </summary>
        protected override IEnumerable<FunctionCallDefinition> FunctionDefinitions()
        {
            var defs = new List<FunctionCallDefinition>(base.FunctionDefinitions())
            {
                new FunctionCallDefinition(
                    name: "FN_CONTAINS",
                    regex: @"contains\(",
                    argumentTypes: new[] { typeof(string), typeof(string) },
                    expressionBuilder: (parameters) =>
                    {
                        return Expression.Call(
                            instance: parameters[0],
                            method: StringMembers.Contains,
                            arguments: new[] { parameters[1] });
                    }),
                new FunctionCallDefinition(
                    name: "FN_IN",
                    regex: @"in\(",
                    expressionBuilder: (parameters) =>
                    {
                        Type valueType = parameters[1].Type;
                        List<ElementInit> elementInits = new List<ElementInit>();
                        foreach(var parameter in parameters.Skip(1)){
                            elementInits.Add(Expression.ElementInit(typeof(List<>).MakeGenericType(valueType).GetMethod("Add"), parameter));
                        }
                        NewExpression newListExpression = Expression.New(typeof(List<>).MakeGenericType(valueType));
                        ListInitExpression values = Expression.ListInit(newListExpression, elementInits);
                        return Expression.Call(
                            instance: values,
                            method: typeof(List<>).MakeGenericType(valueType).GetMethod("Contains"),
                            arguments: new[] { parameters[0] });
                    })
            };

            return defs;
        }
    }

We can provide any number of values but in order to do that we cannot use the argumentTypes parameter in the FunctionCallDefinition so no type validation is done while parsing.

LandryDubus commented 4 years ago

Here is a working implementation for the IN operator that conforms to the OData syntax Property in (value1,value2). This works for any data type:

    /// <summary>
    /// Represents an extended opening bracket.
    /// </summary>
    /// <seealso cref="StringToExpression.GrammerDefinitions.GrammerDefinition" />
    public class ExtendedBracketOpenDefinition : BracketOpenDefinition
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="ExtendedBracketOpenDefinition"/> class.
        /// </summary>
        /// <param name="name">The name of the definition.</param>
        /// <param name="regex">The regex to match tokens.</param>
        public ExtendedBracketOpenDefinition(string name, string regex)
            : base(name, regex)
        {
        }

        /// <summary>
        /// Applies the bracket operands. Adds the evaluated operand within the bracket to the state.
        /// </summary>
        /// <param name="bracketOpen">The operator that opened the bracket.</param>
        /// <param name="bracketOperands">The list of operands within the brackets.</param>
        /// <param name="bracketClose">The operator that closed the bracket.</param>
        /// <param name="state">The current parse state.</param>
        /// <exception cref="OperandExpectedException">When brackets are empty.</exception>
        public override void ApplyBracketOperands(Operator bracketOpen, Stack<Operand> bracketOperands, Operator bracketClose, ParseState state)
        {
            if (state.Operators.Count > 0 && state.Operators.Peek().Definition.Name == "IN")
            {
                Type valueType = bracketOperands.First().Expression.Type;

                if (!bracketOperands.Skip(1).All(o => o.Expression.Type == valueType))
                {
                    var operandSpan = StringSegment.Encompass(bracketOperands.Skip(1).Select(x => x.SourceMap));
                    throw new OperandUnexpectedException(operandSpan);
                }

                List<StringSegment> sourceMapSegments = new List<StringSegment>() { bracketOpen.SourceMap };

                List<ElementInit> elementInits = new List<ElementInit>();
                foreach (var bracketOperand in bracketOperands)
                {
                    elementInits.Add(Expression.ElementInit(typeof(List<>).MakeGenericType(valueType).GetMethod("Add"), bracketOperand.Expression));
                    sourceMapSegments.Add(bracketOperand.SourceMap);
                }
                NewExpression newListExpression = Expression.New(typeof(List<>).MakeGenericType(valueType));
                ListInitExpression values = Expression.ListInit(newListExpression, elementInits);

                sourceMapSegments.Add(bracketClose.SourceMap);
                var sourceMap = StringSegment.Encompass(sourceMapSegments);

                state.Operands.Push(new Operand(values, sourceMap));
            }
            else
                base.ApplyBracketOperands(bracketOpen, bracketOperands, bracketClose, state);
        }
    }

    /// <summary>
    /// Extends the base class for parsing OData filter parameters by adding new function and logic operator definitions and by replacing default bracket definitions to use the <see cref="ExtendedBracketOpenDefinition"/>.
    /// </summary>
    public class ExtendedODataFilterLanguage : ODataFilterLanguage
    {
        /// <summary>
        /// Returns the definitions for functions used within the language while adding a few new ones.
        /// </summary>
        protected override IEnumerable<FunctionCallDefinition> FunctionDefinitions()
        {
            var defs = new List<FunctionCallDefinition>(base.FunctionDefinitions())
            {
                new FunctionCallDefinition(
                    name: "FN_CONTAINS",
                    regex: @"contains\(",
                    argumentTypes: new[] { typeof(string), typeof(string) },
                    expressionBuilder: (parameters) =>
                    {
                        return Expression.Call(
                            instance: parameters[0],
                            method: StringMembers.Contains,
                            arguments: new[] { parameters[1] });
                    })
            };

            return defs;
        }

        /// <summary>
        /// Returns the definitions for logic operators used within the language while adding a few new ones.
        /// </summary>
        /// <returns></returns>
        protected override IEnumerable<GrammerDefinition> LogicalOperatorDefinitions()
        {
            var defs = new List<GrammerDefinition>(base.LogicalOperatorDefinitions())
            {
                new BinaryOperatorDefinition(
                    name: "IN",
                    regex: @"in",
                    orderOfPrecedence: 0,
                    expressionBuilder: (left, right) => {
                        Type valueType = left.Type;
                        return Expression.Call(
                            instance: right,
                            method: typeof(List<>).MakeGenericType(valueType).GetMethod("Contains"),
                            arguments: new[] { left });
                    })
            };

            return defs;
        }

        /// <summary>
        /// Returns the definitions for brackets used within the language using the <see cref="ExtendedBracketOpenDefinition"/>
        /// </summary>
        /// <param name="functionCalls">The function calls in the language. (used as opening brackets)</param>
        protected override IEnumerable<GrammerDefinition> BracketDefinitions(IEnumerable<FunctionCallDefinition> functionCalls)
        {
            ExtendedBracketOpenDefinition openBracket = new ExtendedBracketOpenDefinition(
                    name: "OPEN_BRACKET",
                    regex: @"\(");
            ListDelimiterDefinition delimeter = new ListDelimiterDefinition(
                    name: "COMMA",
                    regex: ",");
            return new GrammerDefinition[] {
                openBracket,
                delimeter,
                new BracketCloseDefinition(
                    name: "CLOSE_BRACKET",
                    regex: @"\)",
                    bracketOpenDefinitions: new BracketOpenDefinition[] { openBracket }.Concat(functionCalls),
                    listDelimeterDefinition: delimeter)
            };
        }
    }

@alex-davies I could create a pull-request on the original code base in order to do the same to support both the 'FN_CONTAINS' canonic function and the 'IN' operator in the ODataFilterLanguage but I don't quite like the fact that the new generic BracketOpenDefinition knows and look for the 'IN' operator explicitly as it is not a known operator in all possible languages. Any idea on this? I think the best approach would be to allow the BracketOpenDefinition to receive a list of operator definitions or name to look for in order to handle the operands as a list of values. This list of operator would then be passed to the BracketOpenDefinition in the language definition.

devalugbin commented 3 years ago

can any one please help with a sample usage of the Contains🥺🥺