From ca289e081e6ac485fa65ba7e35dd2ccbe971b579 Mon Sep 17 00:00:00 2001 From: cacaodev Date: Sat, 6 Nov 2010 23:37:15 +0100 Subject: [PATCH] CPPredicate parsing support: "($)path.($)variable" "SUBQUERY(collection, $x,$x = 2)" "sum:(arg1, ...)" "FUNCTION(target, selector, arg1, ...) "object[expression]" "set UNION|INTERSECT|MINUS otherset" Fixed a bug where CPExpression_set was evaluating to an expression instead of a set. Replaced parsing exceptions with a generic function indicating where it failed. --- .../CPPredicate/CPComparisonPredicate.j | 1 + Foundation/CPPredicate/CPCompoundPredicate.j | 3 +- Foundation/CPPredicate/CPExpression.j | 12 +- .../CPPredicate/CPExpression_aggregate.j | 4 +- .../CPPredicate/CPExpression_constant.j | 2 +- .../CPPredicate/CPExpression_function.j | 41 ++-- Foundation/CPPredicate/CPExpression_keypath.j | 21 ++- Foundation/CPPredicate/CPExpression_self.j | 18 +- Foundation/CPPredicate/CPExpression_set.j | 4 +- .../CPPredicate/CPExpression_subquery.j | 52 +++-- .../CPPredicate/CPExpression_variable.j | 12 +- Foundation/CPPredicate/CPPredicate.j | 178 +++++++++++------- Tests/Foundation/CPPredicateTest.j | 94 ++++++--- 13 files changed, 286 insertions(+), 156 deletions(-) diff --git a/Foundation/CPPredicate/CPComparisonPredicate.j b/Foundation/CPPredicate/CPComparisonPredicate.j index 2e3ada1a7..434fee310 100644 --- a/Foundation/CPPredicate/CPComparisonPredicate.j +++ b/Foundation/CPPredicate/CPComparisonPredicate.j @@ -1,3 +1,4 @@ + @import "CPArray.j" @import "CPNull.j" @import "CPString.j" diff --git a/Foundation/CPPredicate/CPCompoundPredicate.j b/Foundation/CPPredicate/CPCompoundPredicate.j index 0ea197261..964fda633 100644 --- a/Foundation/CPPredicate/CPCompoundPredicate.j +++ b/Foundation/CPPredicate/CPCompoundPredicate.j @@ -1,6 +1,5 @@ + @import "CPPredicate.j" -@import -@import /*! A predicate to compare directly the left and right hand sides. diff --git a/Foundation/CPPredicate/CPExpression.j b/Foundation/CPPredicate/CPExpression.j index 9c339ab50..6caf1a8db 100644 --- a/Foundation/CPPredicate/CPExpression.j +++ b/Foundation/CPPredicate/CPExpression.j @@ -1,8 +1,8 @@ -@import -@import -@import -@import -@import + +@import "CPString.j" +@import "CPArray.j" +@import "CPKeyValueCoding.j" +@import "CPDictionary.j" /*! An expression that always returns the same value. @@ -90,7 +90,7 @@ CPMinusSetExpressionType = 9; */ + (CPExpression)expressionForEvaluatedObject { - return [[CPExpression_self alloc] init]; + return [CPExpression_self evaluatedObject]; } /*! diff --git a/Foundation/CPPredicate/CPExpression_aggregate.j b/Foundation/CPPredicate/CPExpression_aggregate.j index fed253883..47be66fc3 100644 --- a/Foundation/CPPredicate/CPExpression_aggregate.j +++ b/Foundation/CPPredicate/CPExpression_aggregate.j @@ -1,7 +1,7 @@ @import "CPExpression.j" -@import -@import +@import "CPArray.j" +@import "CPString.j" @implementation CPExpression_aggregate : CPExpression { diff --git a/Foundation/CPPredicate/CPExpression_constant.j b/Foundation/CPPredicate/CPExpression_constant.j index 66cfa1ca0..01ca5084b 100644 --- a/Foundation/CPPredicate/CPExpression_constant.j +++ b/Foundation/CPPredicate/CPExpression_constant.j @@ -1,6 +1,6 @@ @import "CPExpression.j" -@import +@import "CPDictionary.j" @implementation CPExpression_constant : CPExpression { diff --git a/Foundation/CPPredicate/CPExpression_function.j b/Foundation/CPPredicate/CPExpression_function.j index 31d92f1ac..72c8762dd 100644 --- a/Foundation/CPPredicate/CPExpression_function.j +++ b/Foundation/CPPredicate/CPExpression_function.j @@ -1,8 +1,8 @@ @import "CPExpression.j" -@import -@import -@import +@import "CPString.j" +@import "CPArray.j" +@import "CPDictionary.j" @implementation CPExpression_function : CPExpression { @@ -10,6 +10,7 @@ SEL _selector; CPArray _arguments; int _argc; + int _maxargs; } - (id)initWithSelector:(SEL)aSelector arguments:(CPArray)parameters @@ -37,6 +38,7 @@ _operand = operand; _arguments = parameters; _argc = [parameters count]; + _maxargs = [[CPStringFromSelector(_selector) componentsSeparatedByString:@":"] count] - 1; return self; } @@ -84,30 +86,45 @@ objj_args.push(arg); } + // If we have too much arguments, concatenate remaining args on the last one. + if (_argc > _maxargs) + { + var r = MAX(_maxargs + 1, 2); + objj_args = objj_args.slice(0, r).concat([objj_args.slice(r)]); + } + return objj_msgSend.apply(this, objj_args); } - (CPString)description { - var result = _operand + [self _function] + "("; + var result = ""; + if ([_operand isEqual:[CPExpression expressionForConstantValue:[CPPredicateUtilities class]]]) + result += CPStringFromSelector(_selector) + "("; + else + { + result += "FUNCTION("; + result += _operand ? [_operand description] + ", ":""; + result += _selector ? CPStringFromSelector(_selector) + ", ":""; + } for (var i = 0; i < _argc; i++) result = result + [_arguments[i] description] + ((i + 1 < _argc) ? ", " : ""); - result = result + ")"; + result += ")"; return result ; } - (CPExpression)_expressionWithSubstitutionVariables:(CPDictionary)variables { - var array = [CPArray array], - i; - // should we also allow variables for target and selectors ? - for (i = 0; i < _argc; i++) - [array addObject:[[_arguments objectAtIndex:i] _expressionWithSubstitutionVariables:variables]]; + var operand = [[self operand] _expressionWithSubstitutionVariables:variables], + args = [CPArray array]; - return [CPExpression expressionForFunction:[self operand] selectorName:[self _function] arguments:array]; + for (var i = 0; i < _argc; i++) + [args addObject:[_arguments[i] _expressionWithSubstitutionVariables:variables]]; + + return [CPExpression expressionForFunction:operand selectorName:[self _function] arguments:args]; } @end @@ -255,7 +272,7 @@ var CPSelectorNameKey = @"CPSelectorName", return ROUND(RAND() * num); } -+ (int)modulus:(int)n by:(int)n ++ (int)modulus:(int)n by:(int)m { return n % m; } diff --git a/Foundation/CPPredicate/CPExpression_keypath.j b/Foundation/CPPredicate/CPExpression_keypath.j index 9abf3b90f..53878833c 100644 --- a/Foundation/CPPredicate/CPExpression_keypath.j +++ b/Foundation/CPPredicate/CPExpression_keypath.j @@ -1,8 +1,8 @@ @import "CPExpression.j" -@import -@import @import "CPExpression_function.j" +@import "CPString.j" +@import "CPKeyValueCoding.j" @implementation CPExpression_keypath : CPExpression_function { @@ -37,13 +37,26 @@ - (CPString)keyPath { - return [[self pathExpression] constantValue]; + return [[self pathExpression] keyPath]; } - (CPString)description { - return [self keyPath]; + var result = ""; + if ([_operand expressionType] != CPEvaluatedObjectExpressionType) + result += [_operand description] + "."; + result += [self keyPath]; + + return result; } @end +@implementation CPExpression_constant (KeyPath) + +- (CPString)keyPath +{ + return [self constantValue]; +} + +@end diff --git a/Foundation/CPPredicate/CPExpression_self.j b/Foundation/CPPredicate/CPExpression_self.j index 5e440519d..a313fe8ba 100644 --- a/Foundation/CPPredicate/CPExpression_self.j +++ b/Foundation/CPPredicate/CPExpression_self.j @@ -1,13 +1,21 @@ @import "CPExpression.j" -@import -@import -@import +@import "CPString.j" +@import "CPDictionary.j" +var evaluatedObject = nil; @implementation CPExpression_self : CPExpression { } ++ (id)evaluatedObject +{ + if (evaluatedObject == nil) + evaluatedObject = [CPExpression_self new]; + + return evaluatedObject; +} + - (id)init { [super initWithExpressionType:CPEvaluatedObjectExpressionType]; @@ -17,7 +25,7 @@ - (id)initWithCoder:(CPCoder)coder { - return [self init]; + return [CPExpression_self evaluatedObject]; } - (void)encodeWithCoder:(CPCoder)coder @@ -26,7 +34,7 @@ - (BOOL)isEqual:(id)object { - return (_type == CPEvaluatedObjectExpressionType); + return (object === self); } - (id)expressionValueWithObject:(id)object context:(CPDictionary)context diff --git a/Foundation/CPPredicate/CPExpression_set.j b/Foundation/CPPredicate/CPExpression_set.j index 2f1ddbcba..9be28e076 100644 --- a/Foundation/CPPredicate/CPExpression_set.j +++ b/Foundation/CPPredicate/CPExpression_set.j @@ -30,7 +30,7 @@ - (id)expressionValueWithObject:object context:(CPDictionary)context { var right = [_right expressionValueWithObject:object context:context]; - if ([right isKindOfClass:[CPArray class]]) // Or we could do [[right objectEnumerator] allObjects] + if ([right isKindOfClass:[CPArray class]]) right = [CPSet setWithArray:right]; else if ([right isKindOfClass:[CPDictionary class]]) right = [CPSet setWithArray:[right allValues]]; @@ -53,7 +53,7 @@ default: } - return [CPExpression expressionForConstantValue:result]; + return result; } - (CPExpression )_expressionWithSubstitutionVariables:(CPDictionary )variables diff --git a/Foundation/CPPredicate/CPExpression_subquery.j b/Foundation/CPPredicate/CPExpression_subquery.j index 1e5e94171..912f50d8e 100644 --- a/Foundation/CPPredicate/CPExpression_subquery.j +++ b/Foundation/CPPredicate/CPExpression_subquery.j @@ -1,3 +1,4 @@ + @import "CPExpression.j" @implementation CPExpression_subquery : CPExpression @@ -8,13 +9,19 @@ } - (id)initWithExpression:(CPExpression)collection usingIteratorVariable:(CPString)variable predicate:(CPPredicate)subpredicate +{ + var variableExpression = [CPExpression expressionForVariable:variable]; + return [self initWithExpression:collection usingIteratorExpression:variableExpression predicate:subpredicate]; +} + +- (id)initWithExpression:(CPExpression)collection usingIteratorExpression:(CPExpression)variableExpression predicate:(CPPredicate)subpredicate { [super initWithExpressionType:CPSubqueryExpressionType]; - + _subpredicate = subpredicate; _collection = collection; - _variableExpression = [CPExpression expressionForVariable:variable]; - + _variableExpression = variableExpression; + return self; } @@ -23,16 +30,16 @@ var collection = [_collection expressionValueWithObject:object context:context], count = [collection count], result = [CPArray array], - + bindings = [CPDictionary dictionaryWithObject:[CPExpression expressionForEvaluatedObject] forKey:[self variable]]; - + for (var i = 0; i < count; i++) { var item = [collection objectAtIndex:i]; if ([_subpredicate evaluateWithObject:item substitutionVariables:bindings]) [result addObject:item]; } - + return result; } @@ -40,14 +47,14 @@ { if (self === object) return YES; - + if (![_collection isEqual:[object collection]] || ![_subpredicate isEqual:[object predicate]]) return NO; - + return YES; } -- (id)collection +- (CPExpression)collection { return _collection; } @@ -57,22 +64,27 @@ return [[CPExpression_subquery alloc] initWithExpression:[_collection copy] usingIteratorExpression:[_variableExpression copy] predicate:[_subpredicate copy]]; } -- (id)predicate +- (CPPredicate)predicate { return _subpredicate; } -- (id)predicateFormat +- (CPString)description +{ + return [self predicateFormat]; +} + +- (CPString)predicateFormat { return @"SUBQUERY(" + [_collection description] + ", " + [_variableExpression description] + ", " + [_subpredicate predicateFormat] + ")"; } -- (id)variable +- (CPString)variable { return [_variableExpression variable]; } -- (id)variableExpression +- (CPExpression)variableExpression { return _variableExpression; } @@ -81,23 +93,23 @@ var CPExpressionKey = @"CPExpression", CPSubpredicateKey = @"CPSubpredicate", CPVariableKey = @"CPVariable"; - + @implementation CPExpression_subquery (CPCoding) - (id)initWithCoder:(CPCoder)coder { - var collection = [coder decodeObjectForKey:CPExpressionKey], + var collection = [coder decodeObjectForKey:CPExpressionKey], subpredicate = [coder decodeObjectForKey:CPSubpredicateKey], - variable = [coder decodeObjectForKey:CPVariableKey]; - - return [self initWithExpression:collection usingIteratorVariable:variable predicate:subpredicate]; + variableExpression = [coder decodeObjectForKey:CPVariableKey]; + + return [self initWithExpression:collection usingIteratorExpression:variableExpression predicate:subpredicate]; } - (void)encodeWithCoder:(CPCoder)coder { [coder encodeObject:_collection forKey:CPExpressionKey]; [coder encodeObject:_subpredicate forKey:CPSubpredicateKey]; - [coder encodeObject:[variableExpression variable] forKey:CPVariableKey]; + [coder encodeObject:_variableExpression forKey:CPVariableKey]; } -@end \ No newline at end of file +@end diff --git a/Foundation/CPPredicate/CPExpression_variable.j b/Foundation/CPPredicate/CPExpression_variable.j index 866c4f61a..340c858b2 100644 --- a/Foundation/CPPredicate/CPExpression_variable.j +++ b/Foundation/CPPredicate/CPExpression_variable.j @@ -1,7 +1,7 @@ @import "CPExpression.j" -@import -@import +@import "CPString.j" +@import "CPDictionary.j" @implementation CPExpression_variable : CPExpression { @@ -35,8 +35,8 @@ - (id)expressionValueWithObject:object context:(CPDictionary)context { var expression = [self _expressionWithSubstitutionVariables:context]; - - return [expression expressionValueWithObject:object context:context]; + + return [expression expressionValueWithObject:object context:context]; } - (CPString)description @@ -49,10 +49,10 @@ var value = [variables objectForKey:_variable]; if (value == nil) [CPException raise:CPInvalidArgumentException reason:@"Can't get value for '" + _variable + "' in bindings" + variables]; - + if ([value isKindOfClass:[CPExpression class]]) return value; - + return [CPExpression expressionForConstantValue:value]; } diff --git a/Foundation/CPPredicate/CPPredicate.j b/Foundation/CPPredicate/CPPredicate.j index 2041e7e71..7f323b047 100644 --- a/Foundation/CPPredicate/CPPredicate.j +++ b/Foundation/CPPredicate/CPPredicate.j @@ -1,9 +1,9 @@ -@import -@import -@import -@import -@import +@import "CPValue.j" +@import "CPArray.j" +@import "CPSet.j" +@import "CPNull.j" +@import "CPScanner.j" /*! @ingroup foundation @@ -285,12 +285,16 @@ function(newValue)\ } catch(error) { - CPLogConsole(@"Parsing failed for "+[self string]+" with " + error); + CPLogConsole(@"Unable to parse predicate '"+[self string]+"' with " + error); } finally { if (![self isAtEnd]) - CPLogConsole(@"Format string contains extra characters: \""+[self string]+"\""); + { + var pstr = [self string], + loc = [self scanLocation]; + CPLogConsole(@"Format string contains extra characters: '" + [pstr substringToIndex:loc] + "**" + [pstr substringFromIndex:loc] + "**'"); + } } return r; @@ -340,7 +344,7 @@ function(newValue)\ var r = [self parsePredicate]; if (![self scanString:@")" intoString:NULL]) - [CPException raise:CPInvalidArgumentException reason:@"Missing ) in compound predicate"]; + CPRaiseParseError(self, @"predicate"); return r; } @@ -475,7 +479,7 @@ function(newValue)\ type = CPBetweenPredicateOperatorType; } else - [CPException raise:CPInvalidArgumentException reason:@"Invalid comparison predicate: "+ [[self string] substringFromIndex: [self scanLocation]]]; + CPRaiseParseError(self, @"comparison predicate"); if ([self scanString:@"[cd]" intoString:NULL]) { @@ -525,7 +529,7 @@ function(newValue)\ var arg = [self parseExpression]; if (![self scanString:@")" intoString:NULL]) - [CPException raise:CPInvalidArgumentException reason:@"Missing ) in expression"]; + CPRaiseParseError(self, @"expression"); return arg; } @@ -542,7 +546,7 @@ function(newValue)\ [a addObject:[self parseExpression]]; if (![self scanString:@"}" intoString:NULL]) - [CPException raise:CPInvalidArgumentException reason:@"Missing } in aggregate"]; + CPRaiseParseError(self, @"expression"); return [CPExpression expressionForAggregate:a]; } @@ -566,12 +570,12 @@ function(newValue)\ if ([self scanString:@"$" intoString:NULL]) { - var variable = [self parseExpression]; + var variable = [self parseSimpleExpression]; if (![variable keyPath]) - [CPException raise:CPInvalidArgumentException reason:@"Invalid variable identifier: " + variable]; + CPRaiseParseError(self, @"expression"); - return [CPExpression expressionForVariable:[variable keyPath]]; + return [CPExpression expressionForVariable:variable]; } location = [self scanLocation]; @@ -648,10 +652,7 @@ function(newValue)\ [self scanUpToString:@"\"" intoString:REFERENCE(str)]; if ([self scanString:@"\"" intoString:NULL] == NO) - { - [self setCharactersToBeSkipped:skip]; - [CPException raise:CPInvalidArgumentException reason:@"Invalid double quoted literal at "+location]; - } + CPRaiseParseError(self, @"expression"); [self setCharactersToBeSkipped:skip]; @@ -667,10 +668,7 @@ function(newValue)\ [self scanUpToString:@"'" intoString:REFERENCE(str)]; if ([self scanString:@"'" intoString:NULL] == NO) - { - [self setCharactersToBeSkipped:skip]; - [CPException raise:CPInvalidArgumentException reason:@"Invalid double quoted literal at " + location]; - } + CPRaiseParseError(self, @"expression"); [self setCharactersToBeSkipped:skip]; @@ -682,17 +680,55 @@ function(newValue)\ var e = [self parseExpression]; if (![e keyPath]) - [CPException raise:CPInvalidArgumentException reason:@"Invalid keypath identifier: " + e]; + CPRaiseParseError(self, @"expression"); return [CPExpression expressionForKeyPath:[e keyPath] + "@"]; } + if ([self scanString:@"SUBQUERY" intoString:NULL]) + { + if (![self scanString:@"(" intoString:NULL]) + CPRaiseParseError(self, @"expression"); + + var collection = [self parseExpression], + variableExpression, + subpredicate; + + if (![self scanString:@"," intoString:NULL]) + CPRaiseParseError(self, @"expression"); + variableExpression = [self parseExpression]; + + if (![self scanString:@"," intoString:NULL]) + CPRaiseParseError(self, @"expression"); + subpredicate = [self parsePredicate]; + + if (![self scanString:@")" intoString:NULL]) + CPRaiseParseError(self, @"expression"); + + return [[CPExpression_subquery alloc] initWithExpression:collection usingIteratorExpression:variableExpression predicate:subpredicate]; + } + + if ([self scanString:@"FUNCTION" intoString:NULL]) + { + if (![self scanString:@"(" intoString:NULL]) + CPRaiseParseError(self, @"expression"); + + var args = [CPArray arrayWithObject:[self parseExpression]]; + while ([self scanString:@"," intoString:NULL]) + [args addObject:[self parseExpression]]; + + if (![self scanString:@")" intoString:NULL] || [args count] < 2 || [args[1] expressionType] != CPConstantValueExpressionType) + CPRaiseParseError(self, @"expression"); + + return [CPExpression expressionForFunction:args[0] selectorName:[args[1] constantValue] arguments:args.slice(2)]; + } + [self scanString:@"#" intoString:NULL]; if (!identifier) identifier = [CPCharacterSet characterSetWithCharactersInString:@"_$abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"]; if (![self scanCharactersFromSet:identifier intoString:REFERENCE(ident)]) - [CPException raise:CPInvalidArgumentException reason:@"Missing identifier: "+[[self string] substringFromIndex:[self scanLocation]]]; + CPRaiseParseError(self, @"expression"); return [CPExpression expressionForKeyPath:ident]; } @@ -703,48 +739,17 @@ function(newValue)\ while (YES) { - if ([self scanString:@"(" intoString:NULL]) + if ([self scanString:@"." intoString:NULL]) { - // function - this parser allows for (max)(a, b, c) to be properly - // recognized and even (%K)(a, b, c) if %K evaluates to "max" - var args = [CPMutableArray arrayWithCapacity:5]; - - if (![left keyPath]) - [CPException raise:CPInvalidArgumentException reason:@"Invalid function identifier: " + left]; - - if (![self scanString:@")" intoString:NULL]) - { - // any arguments - // first argument - [args addObject:[self parseExpression]]; - while ([self scanString:@"," intoString:NULL]) - { - // more arguments - [args addObject:[self parseExpression]]; - } - - if (![self scanString:@")" intoString:NULL]) - [CPException raise:CPInvalidArgumentException reason:@"Missing ) in function arguments"]; - } - left = [CPExpression expressionForFunction:[left keyPath] arguments:args]; - } - else if ([self scanString:@"." intoString:NULL]) - { - // keypath - this parser allows for (a).(b.c) - // to be properly recognized - // and even %K.((%K)) if the first %K evaluates to "a" and the - // second %K to "b.c" - - if (![left keyPath]) - [CPException raise:CPInvalidArgumentException reason:@"Invalid left keypath:" + left]; - - var right = [self parseSimpleExpression]; - if (![right keyPath]) - [CPException raise:CPInvalidArgumentException reason:@"Invalid right keypath:" + right]; - - // concatenate - left = [CPExpression expressionForKeyPath:[left keyPath]+ "." + [right keyPath]]; + var right = [self parseSimpleExpression], + expressionType = [right expressionType]; + if (expressionType == CPKeyPathExpressionType) + left = [[CPExpression_keypath alloc] initWithOperand:left andKeyPath:[right keyPath]]; + else if (expressionType == CPVariableExpressionType) + left = [CPExpression expressionForFunction:left selectorName:@"valueForKey:" arguments:[right]]; + else + CPRaiseParseError(self, @"expression"); } else if ([self scanString:@"[" intoString:NULL]) { @@ -763,13 +768,44 @@ function(newValue)\ } else { - var integer; - if ([self scanInt:REFERENCE(integer)]) - left = [CPExpression expressionForFunction:@"fromObject:index:" arguments:[CPArray arrayWithObjects:left, [CPExpression expressionForConstantValue:integer]]]; + var index = [self parseExpression]; + left = [CPExpression expressionForFunction:@"fromObject:index:" arguments:[CPArray arrayWithObjects:left, index]]; } if (![self scanString:@"]" intoString:NULL]) - [CPException raise:CPInvalidArgumentException reason:@"Missing ] in index argument"]; + CPRaiseParseError(self, @"expression"); + } + else if ([self scanString:@":(" intoString:NULL]) + { + // function - this parser allows for (max)(a, b, c) to be properly + // recognized and even (%K)(a, b, c) if %K evaluates to "max" + var args = [CPMutableArray arrayWithCapacity:5]; + + if (![left keyPath]) + CPRaiseParseError(self, @"expression"); + + if (![self scanString:@")" intoString:NULL]) + { + [args addObject:[self parseExpression]]; + while ([self scanString:@"," intoString:NULL]) + [args addObject:[self parseExpression]]; + + if (![self scanString:@")" intoString:NULL]) + CPRaiseParseError(self, @"expression"); + } + left = [CPExpression expressionForFunction:([left keyPath] + ":") arguments:args]; + } + else if ([self scanString:@"UNION" intoString:NULL]) + { + left = [CPExpression expressionForUnionSet:left with:[self parseExpression]]; + } + else if ([self scanString:@"INTERSECT" intoString:NULL]) + { + left = [CPExpression expressionForIntersectSet:left with:[self parseExpression]]; + } + else if ([self scanString:@"MINUS" intoString:NULL]) + { + left = [CPExpression expressionForMinusSet:left with:[self parseExpression]]; } else { @@ -872,7 +908,11 @@ function(newValue)\ @end +var CPRaiseParseError = function CPRaiseParseError(aScanner, target) +{ + [CPException raise:CPInvalidArgumentException reason:@"unable to parse " + target + " at index " + [aScanner scanLocation]]; +} + @import "CPCompoundPredicate.j" @import "CPComparisonPredicate.j" - @import "CPExpression.j" diff --git a/Tests/Foundation/CPPredicateTest.j b/Tests/Foundation/CPPredicateTest.j index 718cb6edf..62863c887 100644 --- a/Tests/Foundation/CPPredicateTest.j +++ b/Tests/Foundation/CPPredicateTest.j @@ -36,7 +36,7 @@ var expression_keypath = [CPExpression expressionForKeyPath:@"name"]; [self assertNotNull:expression_keypath message:"KeyPath Expression should not be nil"]; [self assertTrue:[expression_keypath keyPath] == @"name" message:"-keyPath should not be \"name\""]; - + var expression_str = [CPExpression expressionForConstantValue:@"j[a-z]an"]; [self assertNotNull:expression_str message:"ConstantValue Expression should not be nil"]; @@ -83,26 +83,28 @@ var right = [CPExpression expressionForConstantValue:[CPArray arrayWithObjects:@"a",@"b",@"d"]]; var expression = [CPExpression expressionForIntersectSet:left with:right]; - var eval = [[expression expressionValueWithObject:nil context:nil] constantValue]; + var eval = [expression expressionValueWithObject:nil context:nil]; [self assertTrue:[eval isEqualToSet:[CPSet setWithObjects:@"a",@"b"]] message:"Result should be {(a, b)}, is " + eval]; expression = [CPExpression expressionForUnionSet:left with:right]; - eval = [[expression expressionValueWithObject:nil context:nil] constantValue]; + eval = [expression expressionValueWithObject:nil context:nil]; [self assertTrue:[eval isEqualToSet:[CPSet setWithObjects:@"a",@"b",@"c",@"d"]] message:"Result should be {(a, b, c, d)}, is " + eval]; - + expression = [CPExpression expressionForMinusSet:left with:right]; - eval = [[expression expressionValueWithObject:nil context:nil] constantValue]; + eval = [expression expressionValueWithObject:nil context:nil]; [self assertTrue:[eval isEqualToSet:[CPSet setWithObjects:@"c"]] message:"Result should be {(c)}, is " + eval]; } - (void)testFunctionExpressionEvaluation { - var expression = [CPExpression expressionForConstantValue:[1,2,3]]; - var function_exp = [CPExpression expressionForFunction:"sum:" arguments:[expression]]; +// Built-in function + var args = [[CPExpression expressionForConstantValue:1], [CPExpression expressionForConstantValue:2], [CPExpression expressionForConstantValue:3]]; + var function_exp = [CPExpression expressionForFunction:"sum:" arguments:args]; var pred = [[CPComparisonPredicate alloc] initWithLeftExpression:function_exp rightExpression:[CPExpression expressionForConstantValue:6] modifier:CPDirectPredicateModifier type:CPEqualToPredicateOperatorType options:0]; [self assertTrue:[pred evaluateWithObject:nil] message:[pred description] + " should be true"]; - + +// Custom function var operand = [CPExpression expressionForConstantValue:@"text"]; var arg = [CPExpression expressionForConstantValue:2]; function_exp = [CPExpression expressionForFunction:operand selectorName:@"substringFromIndex:" arguments:[arg]]; @@ -115,7 +117,7 @@ // Replace with constant var expression = [CPExpression expressionForVariable:@"variable"], bindings = [CPDictionary dictionaryWithObject:20 forKey:@"variable"], - eval = [expression expressionValueWithObject:@"variable" context:bindings]; + eval = [expression expressionValueWithObject:@"variable" context:bindings]; [self assertTrue:(eval == 20) message:"'"+ eval + "' should be 20"]; // Replace with constant expression @@ -132,11 +134,11 @@ - (void)testSubqueryExpressionEvaluation { var collection = [CPExpression expressionForKeyPath:@"Record1.Children"], - iteratorVariable = @"x", + iteratorVariable = @"x", predicate = [CPPredicate predicateWithFormat:@"$x BEGINSWITH 'Kid'"]; - + var expression = [CPExpression expressionForSubquery:collection usingIteratorVariable:iteratorVariable predicate:predicate]; - + var eval = [expression expressionValueWithObject:dict context:nil]; var expected = [CPArray arrayWithObjects:"Kid1", "Kid2"]; [self assertTrue:([eval isEqual:expected]) message:"'"+ [expression predicateFormat] + "' result is "+ eval + "but should be " + expected]; @@ -197,9 +199,9 @@ pred = [[CPComparisonPredicate alloc] initWithLeftExpression:[CPExpression expressionForConstantValue:nil] rightExpression:[CPExpression expressionForConstantValue:nil] customSelector:@selector(yes:)]; [self assertFalse:[pred evaluateWithObject:dict] message:"'"+ [pred description] + "' should be false"]; - + pred = [[CPComparisonPredicate alloc] initWithLeftExpression:[CPExpression expressionForKeyPath:@"Record1.Name"] rightExpression:[CPExpression expressionForConstantValue:nil] modifier:CPDirectPredicateModifier type:CPBeginsWithPredicateOperatorType options:0]; - + [self assertFalse:[pred evaluateWithObject:dict] message:"'"+ [pred description] + "' should be false"]; // Predicates with operators @@ -210,9 +212,9 @@ pred = [[CPComparisonPredicate alloc] initWithLeftExpression:[CPExpression expressionForConstantValue:nil] rightExpression:[CPExpression expressionForConstantValue:nil] modifier:CPDirectPredicateModifier type:CPGreaterThanOrEqualToPredicateOperatorType options:0]; [self assertTrue:[pred evaluateWithObject:dict] message:"'"+ [pred description] + "' should be true"]; - + pred = [[CPComparisonPredicate alloc] initWithLeftExpression:[CPExpression expressionForConstantValue:nil] rightExpression:[CPExpression expressionForConstantValue:nil] modifier:CPDirectPredicateModifier type:CPBeginsWithPredicateOperatorType options:0]; - + [self assertFalse:[pred evaluateWithObject:dict] message:"'"+ [pred description] + "' should be false"]; } @@ -266,16 +268,19 @@ [self assertTrue:[predicate evaluateWithObject:dict] message:[predicate description] + " should be true"]; // Test Aggregate - predicate = [CPPredicate predicateWithFormat:@"{Record1 .Name, Record1.Age} = {'John',34}"]; + predicate = [CPPredicate predicateWithFormat:@"{Record1.Name, Record1.Age} = {'John',34}"]; [self assertTrue:[predicate evaluateWithObject:dict] message:[predicate description] + " should be true"]; // Test Symbolic token predicate = [CPPredicate predicateWithFormat:@"Record1.Children[FIRST] = 'Kid1'"]; [self assertTrue:[predicate evaluateWithObject:dict] message:[predicate description] + " should be true"]; - + predicate = [CPPredicate predicateWithFormat:@"Record1.Children[1] = 'Kid2'"]; [self assertTrue:[predicate evaluateWithObject:dict] message:[predicate description] + " should be true"]; + predicate = [CPPredicate predicateWithFormat:@"Record1.Children[count:(Record1.Children) - 1] = 'Kid2'"]; + [self assertTrue:[predicate evaluateWithObject:dict] message:[predicate description] + " should be true"]; + // Test arithm var n = 2; predicate = [CPPredicate predicateWithFormat:@"SELF +1 = 3"]; @@ -296,26 +301,61 @@ predicate = [CPPredicate predicateWithFormat:@"SELF** 3 = 8"]; [self assertTrue:[predicate evaluateWithObject:n] message:[predicate description] + " should be true"]; -// TEST Operator type +// TEST Operator type predicate = [CPPredicate predicateWithFormat: @"a CONTAINS[c] \"b\""]; [self assertTrue:([predicate predicateOperatorType] == CPContainsPredicateOperatorType) message:[predicate description] + " operator should be a CPContainsPredicateOperatorType"]; - + predicate = [CPPredicate predicateWithFormat: @"a BETWEEN {%f,%f}", 20, 40]; [self assertTrue:([predicate predicateOperatorType] == CPBetweenPredicateOperatorType) message:[predicate description] + " operator should be a CPBetweenPredicateOperatorType"]; // TEST Empty string predicate = [CPPredicate predicateWithFormat: @"a CONTAINS \"\""]; [self assertNotNull:predicate message:[predicate description] + " should not be nil"]; - + // TEST variable predicate = [CPPredicate predicateWithFormat: @"$x CONTAINS \"\""]; [self assertTrue:[[predicate leftExpression] expressionType] == CPVariableExpressionType message:"Left Expression should be a CPVariableExpressionType"]; -/* - Variable in multiple paths will fail. Left exp should be FUNCTION(keyPathExp"keypath", valueForKey:, variableExp"variable") - predicate = [CPPredicate predicateWithFormat: @"keypath.$variable CONTAINS \"\""]; - var type = [[predicate leftExpression] expressionType]; - [self assertTrue: type == CPFunctionExpressionType message:"Left Expression should be a CPFunctionExpressionType is " + type]; -*/ + +// TEST variable inside keypath + predicate = [CPPredicate predicateWithFormat: @"Record1.$age = 34"]; + var bindings = [CPDictionary dictionaryWithObject:@"Age" forKey:@"age"]; + [self assertTrue:[predicate evaluateWithObject:dict substitutionVariables:bindings] message:"Predicate " + predicate + " should evaluate to TRUE"]; + + predicate = [CPPredicate predicateWithFormat: @"$record.Age = 34"]; + [bindings setObject:[CPExpression expressionForKeyPath:@"Record1"] forKey:@"record"]; + [self assertTrue:[predicate evaluateWithObject:dict substitutionVariables:bindings] message:"Predicate " + predicate + " should evaluate to TRUE"]; + + predicate = [CPPredicate predicateWithFormat: @"$record.$age = 34"]; + [self assertTrue:[predicate evaluateWithObject:dict substitutionVariables:bindings] message:"Predicate " + predicate + " should evaluate to TRUE"]; + +// TEST built-in functions + predicate = [CPPredicate predicateWithFormat: @"sum:(1,1) = 2"]; + [self assertTrue:[predicate evaluateWithObject:nil] message:"Predicate " + predicate + " should evaluate to TRUE"]; + +// predicate = [CPPredicate predicateWithFormat: @"multiply:by:(5,3) = 15"]; +// [self assertTrue:[predicate evaluateWithObject:nil] message:"Predicate " + predicate + " should evaluate to TRUE"]; + +// TEST custom functions + predicate = [CPPredicate predicateWithFormat:@"FUNCTION('a/path', 'lastPathComponent') = 'path'"]; + [self assertTrue:[predicate evaluateWithObject:nil] message:"Predicate " + predicate + " should evaluate to TRUE"]; + + predicate = [CPPredicate predicateWithFormat:@"FUNCTION('a/path', 'substringFromIndex:', 2) = 'path'"]; + [self assertTrue:[predicate evaluateWithObject:nil] message:"Predicate " + predicate + " should evaluate to TRUE"]; + +// TEST Subquery -- This means: search people who have 2 boys. + predicate = [CPPredicate predicateWithFormat: @"SUBQUERY(Record1.Children, $x, $x BEGINSWITH 'Kid')[SIZE] = 2"]; + [self assertTrue:[predicate evaluateWithObject:dict] message:"Predicate " + predicate + " should evaluate to TRUE"]; + +// Test Set expressions +// Parsing is ok but the evaluation of this predicate will return NO for 2 reasons: +// 1- CPSet -isEqual: is unimplemented. +// 2- lhs will evaluate to a CPSet and rhs to a CPArray (aggregate exp). Comparing sets against arrays will always fail in CPComparisonPredicate. This is also cocoa behavior but i guess it's for historical reasons (set expressions are 10.5+) and should be changed in capp in my opinion. + var object = [CPDictionary dictionaryWithObject:[CPSet setWithObjects:@"a"] forKey:"a"], + result = [CPSet setWithObjects:@"a",@"b"]; + + predicate = [CPPredicate predicateWithFormat:@"a UNION {'b'} = {'a','b'}"]; + var left = [[predicate leftExpression] expressionValueWithObject:object context:nil]; + [self assertTrue:[left isEqualToSet:result] message:"Expression eval " + left + " should be " + result]; } @end