diff options
author | gtm.daemon <gtm.daemon@7dc7ac4e-7543-0410-b95c-c1676fc8e2a3> | 2010-10-14 19:30:33 +0000 |
---|---|---|
committer | gtm.daemon <gtm.daemon@7dc7ac4e-7543-0410-b95c-c1676fc8e2a3> | 2010-10-14 19:30:33 +0000 |
commit | 9ba6c2468a3e9c2638bfda785f38f7881c3e7f5d (patch) | |
tree | 9aada9758f79bd6a99d06a9fade941012a2abacf /Foundation | |
parent | 9d5523b1f41bb93b76fd23cff42576fd67500f36 (diff) |
[Author: thomasvl]
Add URITemplate support.
R=dmaclach
DELTA=1084 (1083 added, 0 deleted, 1 changed)
Diffstat (limited to 'Foundation')
-rw-r--r-- | Foundation/GTMURITemplate.h | 44 | ||||
-rw-r--r-- | Foundation/GTMURITemplate.m | 523 | ||||
-rw-r--r-- | Foundation/GTMURITemplateTest.m | 133 | ||||
-rw-r--r-- | Foundation/TestData/GTMURITemplateExtraTests.json | 222 | ||||
-rw-r--r-- | Foundation/TestData/GTMURITemplateRFCTests.json | 131 |
5 files changed, 1053 insertions, 0 deletions
diff --git a/Foundation/GTMURITemplate.h b/Foundation/GTMURITemplate.h new file mode 100644 index 0000000..d0e9cea --- /dev/null +++ b/Foundation/GTMURITemplate.h @@ -0,0 +1,44 @@ +/* Copyright (c) 2010 Google Inc. + * + * Licensed 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. + */ + +#import <Foundation/Foundation.h> + +#if MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 + +// +// URI Template +// +// http://tools.ietf.org/html/draft-gregorio-uritemplate-04 +// +// NOTE: This implemention is only a subset of the spec. It should be able +// to parse any template that matches the spec, but if the template makes use +// of a feature that is not supported, it will fail with an error. +// + +@interface GTMURITemplate : NSObject + +// Process the template. If the template uses an unsupported feature, it will +// throw an exception to help catch that limitation. Currently unsupported +// feature is partial result modifiers (prefix/suffix). +// +// valueProvider should be anything that implements -objectForKey:. At the +// simplest level, this can be an NSDictionary. However, a custom class that +// implements valueForKey may be better for some uses (like if the values +// are coming out of some other structure). ++ (NSString *)expandTemplate:(NSString *)uriTemplate values:(id)valueProvider; + +@end + +#endif // MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 diff --git a/Foundation/GTMURITemplate.m b/Foundation/GTMURITemplate.m new file mode 100644 index 0000000..3240dee --- /dev/null +++ b/Foundation/GTMURITemplate.m @@ -0,0 +1,523 @@ +/* Copyright (c) 2010 Google Inc. + * + * Licensed 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. + */ + +#import "GTMURITemplate.h" + +#import "GTMGarbageCollection.h" + +#if MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 + +// Key constants for handling variables. +static NSString *const kVariable = @"variable"; // NSString +static NSString *const kExplode = @"explode"; // NSString +static NSString *const kPartial = @"partial"; // NSString +static NSString *const kPartialValue = @"partialValue"; // NSNumber + +// Help for passing the Expansion info in one shot. +struct ExpansionInfo { + // Constant for the whole expansion. + unichar expressionOperator; + NSString *joiner; + BOOL allowReservedInEscape; + + // Update for each variable. + NSString *explode; +}; + +// Helper just to shorten the lines when needed. +static NSString *UnescapeString(NSString *str) { + return [str stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; +} + +static NSString *EscapeString(NSString *str, BOOL allowReserved) { + static CFStringRef kReservedChars = CFSTR(":/?#[]@!$&'()*+,;="); + CFStringRef allowedChars = allowReserved ? kReservedChars : NULL; + + // NSURL's stringByAddingPercentEscapesUsingEncoding: does not escape + // some characters that should be escaped in URL parameters, like / and ?; + // we'll use CFURL to force the encoding of those + // + // Reference: http://www.ietf.org/rfc/rfc3986.txt + static CFStringRef kCharsToForceEscape = CFSTR("!*'();:@&=+$,/?%#[]"); + static CFStringRef kCharsToForceEscapeSansReserved = CFSTR("%"); + CFStringRef forceEscapedChars = + allowReserved ? kCharsToForceEscapeSansReserved : kCharsToForceEscape; + + NSString *resultStr = str; + CFStringRef escapedStr = + CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, + (CFStringRef)str, + allowedChars, + forceEscapedChars, + kCFStringEncodingUTF8); + if (escapedStr) { + resultStr = GTMCFAutorelease(escapedStr); + } + return resultStr; +} + +@interface GTMURITemplate () ++ (BOOL)parseExpression:(NSString *)expression + expressionOperator:(unichar*)outExpressionOperator + variables:(NSMutableArray **)outVariables + defaultValues:(NSMutableDictionary **)outDefaultValues; + ++ (NSString *)expandVariables:(NSArray *)variables + expressionOperator:(unichar)expressionOperator + values:(id)valueProvider + defaultValues:(NSMutableDictionary *)defaultValues; + ++ (NSString *)expandString:(NSString *)valueStr + variableName:(NSString *)variableName + expansionInfo:(struct ExpansionInfo *)expansionInfo; ++ (NSString *)expandArray:(NSArray *)valueArray + variableName:(NSString *)variableName + expansionInfo:(struct ExpansionInfo *)expansionInfo; ++ (NSString *)expandDictionary:(NSDictionary *)valueDict + variableName:(NSString *)variableName + expansionInfo:(struct ExpansionInfo *)expansionInfo; +@end + +@implementation GTMURITemplate + +#pragma mark Internal Helpers + ++ (BOOL)parseExpression:(NSString *)expression + expressionOperator:(unichar*)outExpressionOperator + variables:(NSMutableArray **)outVariables + defaultValues:(NSMutableDictionary **)outDefaultValues { + + // Please see the spec for full details, but here are the basics: + // + // URI-Template = *( literals / expression ) + // expression = "{" [ operator ] variable-list "}" + // variable-list = varspec *( "," varspec ) + // varspec = varname [ modifier ] [ "=" default ] + // varname = varchar *( varchar / "." ) + // modifier = explode / partial + // explode = ( "*" / "+" ) + // partial = ( substring / remainder ) offset + // + // Examples: + // http://www.example.com/foo{?query,number} + // http://maps.com/mapper{?address*} + // http://directions.org/directions{?from+,to+} + // http://search.org/query{?terms+=none} + // + + // http://tools.ietf.org/html/draft-gregorio-uritemplate-04#section-2.2 + // Operator and op-reserve characters + static NSCharacterSet *operatorSet = nil; + // http://tools.ietf.org/html/draft-gregorio-uritemplate-04#section-2.4.1 + // Explode characters + static NSCharacterSet *explodeSet = nil; + // http://tools.ietf.org/html/draft-gregorio-uritemplate-04#section-2.4.2 + // Partial (prefix/subset) characters + static NSCharacterSet *partialSet = nil; + + @synchronized(self) { + if (operatorSet == nil) { + operatorSet = [[NSCharacterSet characterSetWithCharactersInString:@"+./;?|!@"] retain]; + } + if (explodeSet == nil) { + explodeSet = [[NSCharacterSet characterSetWithCharactersInString:@"*+"] retain]; + } + if (partialSet == nil) { + partialSet = [[NSCharacterSet characterSetWithCharactersInString:@":^"] retain]; + } + } + + // http://tools.ietf.org/html/draft-gregorio-uritemplate-04#section-3.3 + // Empty expression inlines the expression. + if ([expression length] == 0) return NO; + + // Pull off any operator. + *outExpressionOperator = 0; + unichar firstChar = [expression characterAtIndex:0]; + if ([operatorSet characterIsMember:firstChar]) { + *outExpressionOperator = firstChar; + expression = [expression substringFromIndex:1]; + } + + if ([expression length] == 0) return NO; + + // Need to find at least one varspec for the expresssion to be considered + // valid. + BOOL gotAVarspec = NO; + + // Split the variable list. + NSArray *varspecs = [expression componentsSeparatedByString:@","]; + + // Extract the defaults, explodes and modifiers from the varspecs. + *outVariables = [NSMutableArray arrayWithCapacity:[varspecs count]]; + for (NSString *varspec in varspecs) { + NSString *defaultValue = nil; + + if ([varspec length] == 0) continue; + + NSMutableDictionary *varInfo = + [NSMutableDictionary dictionaryWithCapacity:4]; + + // Check for a default (foo=bar). + NSRange range = [varspec rangeOfString:@"="]; + if (range.location != NSNotFound) { + defaultValue = + UnescapeString([varspec substringFromIndex:range.location + 1]); + varspec = [varspec substringToIndex:range.location]; + + if ([varspec length] == 0) continue; + } + + // Check for explode (foo*). + NSUInteger lenLessOne = [varspec length] - 1; + if ([explodeSet characterIsMember:[varspec characterAtIndex:lenLessOne]]) { + [varInfo setObject:[varspec substringFromIndex:lenLessOne] forKey:kExplode]; + varspec = [varspec substringToIndex:lenLessOne]; + if ([varspec length] == 0) continue; + } else { + // Check for partial (prefix/suffix) (foo:12). + range = [varspec rangeOfCharacterFromSet:partialSet]; + if (range.location != NSNotFound) { + NSString *partialMode = [varspec substringWithRange:range]; + NSString *valueStr = [varspec substringFromIndex:range.location + 1]; + // If there wasn't a value for the partial, ignore it. + if ([valueStr length] > 0) { + [varInfo setObject:partialMode forKey:kPartial]; + // TODO: Should validate valueStr is just a number... + [varInfo setObject:[NSNumber numberWithInteger:[valueStr integerValue]] + forKey:kPartialValue]; + } + varspec = [varspec substringToIndex:range.location]; + if ([varspec length] == 0) continue; + } + } + + // Spec allows percent escaping in names, so undo that. + varspec = UnescapeString(varspec); + + // Save off the cleaned up variable name. + [varInfo setObject:varspec forKey:kVariable]; + [*outVariables addObject:varInfo]; + gotAVarspec = YES; + + // Now that the variable has been cleaned up, store its default. + if (defaultValue) { + if (*outDefaultValues == nil) { + *outDefaultValues = [NSMutableDictionary dictionary]; + } + [*outDefaultValues setObject:defaultValue forKey:varspec]; + } + } + // All done. + return gotAVarspec; +} + ++ (NSString *)expandVariables:(NSArray *)variables + expressionOperator:(unichar)expressionOperator + values:(id)valueProvider + defaultValues:(NSMutableDictionary *)defaultValues { + NSString *prefix = nil; + struct ExpansionInfo expansionInfo; + expansionInfo.expressionOperator = expressionOperator; + expansionInfo.joiner = nil; + expansionInfo.allowReservedInEscape = NO; + switch (expressionOperator) { + case 0: + expansionInfo.joiner = @","; + prefix = @""; + break; + case '+': + expansionInfo.joiner = @","; + prefix = @""; + // The reserved character are safe from escaping. + expansionInfo.allowReservedInEscape = YES; + break; + case '.': + expansionInfo.joiner = @"."; + prefix = @"."; + break; + case '/': + expansionInfo.joiner = @"/"; + prefix = @"/"; + break; + case ';': + expansionInfo.joiner = @";"; + prefix = @";"; + break; + case '?': + expansionInfo.joiner = @"&"; + prefix = @"?"; + break; + default: + [NSException raise:@"GTMURITemplateUnsupported" + format:@"Unknown expression operator '%C'", expressionOperator]; + break; + } + + NSMutableArray *results = [NSMutableArray arrayWithCapacity:[variables count]]; + + for (NSDictionary *varInfo in variables) { + NSString *variable = [varInfo objectForKey:kVariable]; + + expansionInfo.explode = [varInfo objectForKey:kExplode]; + // Look up the variable value. + id rawValue = [valueProvider objectForKey:variable]; + + // If the value is an empty array or dictionary, the default is still used. + if (([rawValue isKindOfClass:[NSArray class]] + || [rawValue isKindOfClass:[NSDictionary class]]) + && [rawValue count] == 0) { + rawValue = nil; + } + + // Got nothing? Check defaults. + if (rawValue == nil) { + rawValue = [defaultValues objectForKey:variable]; + } + + // If we didn't get any value, on to the next thing. + if (!rawValue) { + continue; + } + + // Time do to the work... + NSString *result = nil; + if ([rawValue isKindOfClass:[NSString class]]) { + result = [self expandString:rawValue + variableName:variable + expansionInfo:&expansionInfo]; + } else if ([rawValue isKindOfClass:[NSNumber class]]) { + // Turn the number into a string and send it on its way. + result = [self expandString:[rawValue stringValue] + variableName:variable + expansionInfo:&expansionInfo]; + } else if ([rawValue isKindOfClass:[NSArray class]]) { + result = [self expandArray:rawValue + variableName:variable + expansionInfo:&expansionInfo]; + } else if ([rawValue isKindOfClass:[NSDictionary class]]) { + result = [self expandDictionary:rawValue + variableName:variable + expansionInfo:&expansionInfo]; + } else { + [NSException raise:@"GTMURITemplateUnsupported" + format:@"Variable returned unsupported type (%@)", + NSStringFromClass([rawValue class])]; + } + + // Did it generate anything? + if (!result) + continue; + + // Apply partial. + // Defaults should get partial applied? + // ( http://tools.ietf.org/html/draft-gregorio-uritemplate-04#section-2.5 ) + NSString *partial = [varInfo objectForKey:kPartial]; + if ([partial length] > 0) { + [NSException raise:@"GTMURITemplateUnsupported" + format:@"Unsupported partial on expansion %@", partial]; + } + + // Add the result + [results addObject:result]; + } + + // Join and add any needed prefix. + NSString *joinedResults = + [results componentsJoinedByString:expansionInfo.joiner]; + if (([prefix length] > 0) && ([joinedResults length] > 0)) { + return [prefix stringByAppendingString:joinedResults]; + } + return joinedResults; +} + ++ (NSString *)expandString:(NSString *)valueStr + variableName:(NSString *)variableName + expansionInfo:(struct ExpansionInfo *)expansionInfo { + NSString *escapedValue = + EscapeString(valueStr, expansionInfo->allowReservedInEscape); + switch (expansionInfo->expressionOperator) { + case ';': + case '?': + if ([valueStr length] > 0) { + return [NSString stringWithFormat:@"%@=%@", variableName, escapedValue]; + } + return variableName; + default: + return escapedValue; + } +} + ++ (NSString *)expandArray:(NSArray *)valueArray + variableName:(NSString *)variableName + expansionInfo:(struct ExpansionInfo *)expansionInfo { + NSMutableArray *results = [NSMutableArray arrayWithCapacity:[valueArray count]]; + // When joining variable with value, use "var.val" except for 'path' and + // 'form' style expression, use 'var=val' then. + char variableValueJoiner = '.'; + char expressionOperator = expansionInfo->expressionOperator; + if ((expressionOperator == ';') || (expressionOperator == '?')) { + variableValueJoiner = '='; + } + // Loop over the values. + for (NSString *value in valueArray) { + // Escape it. + value = EscapeString(value, expansionInfo->allowReservedInEscape); + // Should variable names be used? + if ([expansionInfo->explode isEqual:@"+"]) { + value = [NSString stringWithFormat:@"%@%c%@", + variableName, variableValueJoiner, value]; + } + [results addObject:value]; + } + if ([results count] > 0) { + // Use the default joiner unless there was no explode request, then a list + // always gets comma seperated. + NSString *joiner = expansionInfo->joiner; + if (expansionInfo->explode == nil) { + joiner = @","; + } + // Join the values. + NSString *joined = [results componentsJoinedByString:joiner]; + // 'form' style without an explode gets the variable name set to the + // joined list of values. + if ((expressionOperator == '?') && (expansionInfo->explode == nil)) { + return [NSString stringWithFormat:@"%@=%@", variableName, joined]; + } + return joined; + } + return nil; +} + ++ (NSString *)expandDictionary:(NSDictionary *)valueDict + variableName:(NSString *)variableName + expansionInfo:(struct ExpansionInfo *)expansionInfo { + NSMutableArray *results = [NSMutableArray arrayWithCapacity:[valueDict count]]; + // When joining variable with value: + // - Default to the joiner... + // - No explode, always comma... + // - For 'path' and 'form' style expression, use 'var=val'. + NSString *keyValueJoiner = expansionInfo->joiner; + char expressionOperator = expansionInfo->expressionOperator; + if (!expansionInfo->explode) { + keyValueJoiner = @","; + } else if ((expressionOperator == ';') || (expressionOperator == '?')) { + keyValueJoiner = @"="; + } + // Loop over the sorted keys. + NSArray *sortedKeys = + [[valueDict allKeys] sortedArrayUsingSelector:@selector(compare:)]; + for (NSString *key in sortedKeys) { + NSString *value = [valueDict objectForKey:key]; + // Escape them. + key = EscapeString(key, expansionInfo->allowReservedInEscape); + value = EscapeString(value, expansionInfo->allowReservedInEscape); + // Should variable names be used? + if ([expansionInfo->explode isEqual:@"+"]) { + key = [NSString stringWithFormat:@"%@.%@", variableName, key]; + } + if ((expressionOperator == '?' || expressionOperator == ';') + && ([value length] == 0)) { + [results addObject:key]; + } else { + NSString *pair = [NSString stringWithFormat:@"%@%@%@", + key, keyValueJoiner, value]; + [results addObject:pair]; + } + } + if ([results count]) { + // Use the default joiner unless there was no explode request, then a list + // always gets comma seperated. + NSString *joiner = expansionInfo->joiner; + if (!expansionInfo->explode) { + joiner = @","; + } + // Join the values. + NSString *joined = [results componentsJoinedByString:joiner]; + // 'form' style without an explode gets the variable name set to the + // joined list of values. + if ((expressionOperator == '?') && (expansionInfo->explode == nil)) { + return [NSString stringWithFormat:@"%@=%@", variableName, joined]; + } + return joined; + } + return nil; +} + +#pragma mark Public API + ++ (NSString *)expandTemplate:(NSString *)uriTemplate values:(id)valueProvider { + NSMutableString *result = + [NSMutableString stringWithCapacity:[uriTemplate length]]; + + NSScanner *scanner = [NSScanner scannerWithString:uriTemplate]; + [scanner setCharactersToBeSkipped:nil]; + + // Defaults have to live through the full evaluation, so if any are encoured + // they are reused throughout the expansion calls. + NSMutableDictionary *defaultValues = nil; + + // Pull out the expressions for processing. + while (![scanner isAtEnd]) { + NSString *skipped = nil; + // Find the next '{'. + if ([scanner scanUpToString:@"{" intoString:&skipped]) { + // Add anything before it to the result. + [result appendString:skipped]; + } + // Advance over the '{'. + [scanner scanString:@"{" intoString:nil]; + // Collect the expression. + NSString *expression = nil; + if ([scanner scanUpToString:@"}" intoString:&expression]) { + // Collect the trailing '}' on the expression. + BOOL hasTrailingBrace = [scanner scanString:@"}" intoString:nil]; + + // Parse the expression. + NSMutableArray *variables = nil; + unichar expressionOperator = 0; + if ([self parseExpression:expression + expressionOperator:&expressionOperator + variables:&variables + defaultValues:&defaultValues]) { + // Do the expansion. + NSString *substitution = [self expandVariables:variables + expressionOperator:expressionOperator + values:valueProvider + defaultValues:defaultValues]; + if (substitution) { + [result appendString:substitution]; + } + } else { + // Failed to parse, add the raw expression to the output. + if (hasTrailingBrace) { + [result appendFormat:@"{%@}", expression]; + } else { + [result appendFormat:@"{%@", expression]; + } + } + } else if (![scanner isAtEnd]) { + // Empty expression ('{}'). Copy over the opening brace and the trailing + // one will be copied by the next cycle of the loop. + [result appendString:@"{"]; + } + } + + return result; +} + +@end + +#endif // MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 diff --git a/Foundation/GTMURITemplateTest.m b/Foundation/GTMURITemplateTest.m new file mode 100644 index 0000000..ba2c8fb --- /dev/null +++ b/Foundation/GTMURITemplateTest.m @@ -0,0 +1,133 @@ +/* Copyright (c) 2010 Google Inc. + * + * Licensed 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. + */ + +#import "GTMURITemplate.h" + +#if MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 + +#import "GTMSenTestCase.h" +#import "GTMScriptRunner.h" + +@interface GTMURITemplateTest : GTMTestCase +- (NSDictionary *)loadTestSuitesNamed:(NSString *)testSuitesName; +- (NSDictionary *)parseJSONString:(NSString *)json error:(NSError **)error; +- (void)runTestSuites:(NSDictionary *)testSuites; +@end + +@implementation GTMURITemplateTest + +- (NSDictionary *)parseJSONString:(NSString *)json error:(NSError **)error { + NSDictionary *result = nil; + + // If we ever get a JSON parser in GTM (or the system gets one, next cat?), + // then we can skip this conversion dance. + + NSString *fileName = [NSString stringWithFormat:@"URITemplate_%u.plist", arc4random()]; + NSString *tempOutPath = [NSTemporaryDirectory() stringByAppendingPathComponent:fileName]; + + GTMScriptRunner *runner = [GTMScriptRunner runnerWithPython]; + NSString *command = [NSString stringWithFormat: + @"import Foundation\n" + @"import json\n" + @"str_of_json = \"\"\"%@\"\"\"\n" + @"Foundation.NSDictionary.dictionaryWithDictionary_(json.loads(str_of_json)).writeToFile_atomically_('%@', True)\n", + json, tempOutPath]; + NSString *errStr = nil; + NSString *outStr = [runner run:command standardError:&errStr]; + + STAssertNil(outStr, @"got something on stdout: %@", outStr); + STAssertNil(errStr, @"got something on stderr: %@", errStr); + result = [NSDictionary dictionaryWithContentsOfFile:tempOutPath]; + + [[NSFileManager defaultManager] removeItemAtPath:tempOutPath + error:NULL]; + + return result; +} + +- (NSDictionary *)loadTestSuitesNamed:(NSString *)testSuitesName { + NSBundle *testBundle = [NSBundle bundleForClass:[self class]]; + STAssertNotNil(testBundle, nil); + + NSString *testSuitesPath = [testBundle pathForResource:testSuitesName + ofType:nil]; + STAssertNotNil(testSuitesPath, @"%@ not found", testSuitesName); + + NSError *error = nil; + NSString *testSuitesStr = [NSString stringWithContentsOfFile:testSuitesPath + encoding:NSUTF8StringEncoding + error:&error]; + STAssertNil(error, @"Loading %@, error %@", testSuitesName, error); + STAssertNotNil(testSuitesStr, @"Loading %@", testSuitesName); + + NSDictionary *testSuites = [self parseJSONString:testSuitesStr + error:&error]; + STAssertNil(error, @"Parsing %@, error %@", testSuitesName, error); + STAssertNotNil(testSuites, @"failed to parse"); + + return testSuites; +} + +- (void)runTestSuites:(NSDictionary *)testSuites { + // The file holds a set of named suites... + for (NSString *suiteName in testSuites) { + NSDictionary *suite = [testSuites objectForKey:suiteName]; + // Each suite has variables and test cases... + NSDictionary *vars = [suite objectForKey:@"variables"]; + NSArray *testCases = [suite objectForKey:@"testcases"]; + STAssertTrue([vars count] != 0, @"'%@' no variables?", suiteName); + STAssertTrue([testCases count] != 0, @"'%@' no testcases?", suiteName); + NSUInteger idx = 0; + for (NSArray *testCase in testCases) { + // Each case is an array of the template and value... + STAssertEquals([testCase count], (NSUInteger)2, + @" test index %lu of '%@'", (unsigned long)idx, suiteName); + + NSString *testTemplate = [testCase objectAtIndex:0]; + NSString *expectedResult = [testCase objectAtIndex:1]; + + NSString *result = [GTMURITemplate expandTemplate:testTemplate + values:vars]; + STAssertEqualObjects(result, expectedResult, + @"template was '%@' (index %lu of '%@')", + testTemplate, (unsigned long)idx, suiteName); + ++idx; + } + } +} + +- (void)testRFCSuite { + // All of the examples from the RFC are in the python impl source as json + // test data. A copy is in the GTM tree as GTMURITemplateJSON.txt. The + // original can be found at: + // http://code.google.com/p/uri-templates/source/browse/trunk/testdata.json + NSDictionary *testSuites = [self loadTestSuitesNamed:@"GTMURITemplateRFCTests.json"]; + STAssertNotNil(testSuites, nil); + [self runTestSuites:testSuites]; +} + +- (void)testExtraSuite { + // These are follow up cases not explictly listed in the spec, but does + // as cases to confirm behaviors. The list was sent to the w3c uri list + // for confirmation: + // http://lists.w3.org/Archives/Public/uri/2010Sep/thread.html + NSDictionary *testSuites = [self loadTestSuitesNamed:@"GTMURITemplateExtraTests.json"]; + STAssertNotNil(testSuites, nil); + [self runTestSuites:testSuites]; +} + +@end + +#endif // MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 diff --git a/Foundation/TestData/GTMURITemplateExtraTests.json b/Foundation/TestData/GTMURITemplateExtraTests.json new file mode 100644 index 0000000..e84ab90 --- /dev/null +++ b/Foundation/TestData/GTMURITemplateExtraTests.json @@ -0,0 +1,222 @@ +{ + "No varspec (section 3.3, paragraph 3)" : + { + "variables": { + "var" : "value" + }, + "testcases" : [ + ["{}", "{}"], + ["{,}", "{,}"], + ["{,,}", "{,,}"] + ] + }, + "Missing closing brace (section 3.3 paragraph 4)" : + { + "variables": { + "var" : "value", + "hello" : "Hello World!", + "list" : [ "val1", "val2", "val3" ], + "keys" : {"key1": "val1", "key2": "val2"}, + "x" : "1024", + "y" : "768" + }, + "testcases" : [ + ["{var", "value"], + ["{hello", "Hello%20World%21"], + ["{x,y", "1024,768"], + ["{var=default", "value"], + ["{undef=default", "default"], + ["{list", "val1,val2,val3"], + ["{list*", "val1,val2,val3"], + ["{list+", "list.val1,list.val2,list.val3"], + ["{keys", "key1,val1,key2,val2"], + ["{keys*", "key1,val1,key2,val2"], + ["{keys+", "keys.key1,val1,keys.key2,val2"] + ] + }, + "varspec of only operator and explodes (section 3.3?)" : + { + "variables": { + "var" : "value" + }, + "testcases" : [ + ["{+}", "{+}"], + ["{;}", "{;}"], + ["{?}", "{?}"], + ["{/}", "{/}"], + ["{.}", "{.}"], + ["{+,}", "{+,}"], + ["{;,}", "{;,}"], + ["{?,}", "{?,}"], + ["{/,}", "{/,}"], + ["{.,}", "{.,}"], + ["{++}", "{++}"], + ["{;+}", "{;+}"], + ["{?+}", "{?+}"], + ["{/+}", "{/+}"], + ["{.+}", "{.+}"], + ["{+*}", "{+*}"], + ["{;*}", "{;*}"], + ["{?*}", "{?*}"], + ["{/*}", "{/*}"], + ["{.*}", "{.*}"] + ] + }, + "One good varspec and bad varspecs (section 3.3, paragraph 3?)" : + { + "variables": { + "var" : "value" + }, + "testcases" : [ + ["{var,}", "value"], + ["{,var}", "value"], + ["{,var,,}", "value"], + ["{+var,,}", "value"], + ["{;var,,}", ";var=value"], + ["{?var,,}", "?var=value"], + ["{/var,,}", "/value"], + ["{.var,,}", ".value"], + ["{+,var,}", "value"], + ["{;,var,}", ";var=value"], + ["{?,var,}", "?var=value"], + ["{/,var,}", "/value"], + ["{.,var,}", ".value"], + ["{+,,var}", "value"], + ["{;,,var}", ";var=value"], + ["{?,,var}", "?var=value"], + ["{/,,var}", "/value"], + ["{.,,var}", ".value"] + ] + }, + "Multiple undefined variables (section 3.4)" : + { + "variables": { + "var" : "value" + }, + "testcases" : [ + ["{undef1,undef2}", ""], + ["{+undef1,undef2}", ""], + ["{;undef1,undef2}", ""], + ["{?undef1,undef2}", ""], + ["{/undef1,undef2}", ""], + ["{.undef1,undef2}", ""] + ] + }, + "Default with variable in varspec (just reported like above cases)" : + { + "variables": { + "var" : "value" + }, + "testcases" : [ + ["{=foo}", "{=foo}"] + ] + }, + "varspec with bad partial (partial gets ignored)" : + { + "variables": { + "var" : "value" + }, + "testcases" : [ + ["{var:}", "value"], + ["{var^}", "value"] + ] + }, + "Default of empty string and edge cases with empty strings" : + { + "variables": { + "empty" : "", + "x" : "1024", + "y" : "768" + }, + "testcases" : [ + ["{empty}", ""], + ["{;x,empty,y}", ";x=1024;empty;y=768"], + ["{?x,empty,y}", "?x=1024&empty&y=768"], + ["{x,empty,y}", "1024,,768"], + ["{+x,empty,y}", "1024,,768"], + ["{/x,empty,y}", "/1024//768"], + ["{.x,empty,y}", ".1024..768"], + ["{undef=}", ""], + ["{;x,undef=,y}", ";x=1024;undef;y=768"], + ["{?x,undef=,y}", "?x=1024&undef&y=768"], + ["{x,undef=,y}", "1024,,768"], + ["{+x,undef=,y}", "1024,,768"], + ["{/x,undef=,y}", "/1024//768"], + ["{.x,undef=,y}", ".1024..768"] + ] + }, + "Two defaults for one variable" : + { + "variables": { + "y" : "768" + }, + "testcases" : [ + ["1{undef=a}2{undef=b}3", "1a2b3"], + ["0{undef}1{undef=a}2{undef}3{undef=b}4{undef}5", "01a2a3b4b5"] + ] + }, + "Empty strings within arrays and associative arrays" : + { + "variables": { + "list" : [ "val1", "", "val3" ], + "keysA" : {"key1": "", "key2": "val2"}, + "keysB" : {"key1": "val1", "": "val2"} + }, + "testcases" : [ + ["{list}", "val1,,val3"], + ["{list*}", "val1,,val3"], + ["{list+}", "list.val1,list.,list.val3"], + ["{keysA}", "key1,,key2,val2"], + ["{keysA*}", "key1,,key2,val2"], + ["{keysA+}", "keysA.key1,,keysA.key2,val2"], + ["{keysB}", ",val2,key1,val1"], + ["{keysB*}", ",val2,key1,val1"], + ["{keysB+}", "keysB.,val2,keysB.key1,val1"], + ["{+list}", "val1,,val3"], + ["{+list*}", "val1,,val3"], + ["{+list+}", "list.val1,list.,list.val3"], + ["{+keysA}", "key1,,key2,val2"], + ["{+keysA*}", "key1,,key2,val2"], + ["{+keysA+}", "keysA.key1,,keysA.key2,val2"], + ["{+keysB}", ",val2,key1,val1"], + ["{+keysB*}", ",val2,key1,val1"], + ["{+keysB+}", "keysB.,val2,keysB.key1,val1"], + ["{;list}", ";val1,,val3"], + ["{;list*}", ";val1;;val3"], + ["{;list+}", ";list=val1;list=;list=val3"], + ["{;keysA}", ";key1,key2,val2"], + ["{;keysA*}", ";key1;key2=val2"], + ["{;keysA+}", ";keysA.key1;keysA.key2=val2"], + ["{;keysB}", ";,val2,key1,val1"], + ["{;keysB*}", ";=val2;key1=val1"], + ["{;keysB+}", ";keysB.=val2;keysB.key1=val1"], + ["{?list}", "?list=val1,,val3"], + ["{?list*}", "?val1&&val3"], + ["{?list+}", "?list=val1&list=&list=val3"], + ["{?keysA}", "?keysA=key1,key2,val2"], + ["{?keysA*}", "?key1&key2=val2"], + ["{?keysA+}", "?keysA.key1&keysA.key2=val2"], + ["{?keysB}", "?keysB=,val2,key1,val1"], + ["{?keysB*}", "?=val2&key1=val1"], + ["{?keysB+}", "?keysB.=val2&keysB.key1=val1"], + ["{/list}", "/val1,,val3"], + ["{/list*}", "/val1//val3"], + ["{/list+}", "/list.val1/list./list.val3"], + ["{/keysA}", "/key1,,key2,val2"], + ["{/keysA*}", "/key1//key2/val2"], + ["{/keysA+}", "/keysA.key1//keysA.key2/val2"], + ["{/keysB}", "/,val2,key1,val1"], + ["{/keysB*}", "//val2/key1/val1"], + ["{/keysB+}", "/keysB./val2/keysB.key1/val1"], + ["X{.list}", "X.val1,,val3"], + ["X{.list*}", "X.val1..val3"], + ["X{.list+}", "X.list.val1.list..list.val3"], + ["X{.keysA}", "X.key1,,key2,val2"], + ["X{.keysA*}", "X.key1..key2.val2"], + ["X{.keysA+}", "X.keysA.key1..keysA.key2.val2"], + ["X{.keysB}", "X.,val2,key1,val1"], + ["X{.keysB*}", "X..val2.key1.val1"], + ["X{.keysB+}", "X.keysB..val2.keysB.key1.val1"] + ] + } +} diff --git a/Foundation/TestData/GTMURITemplateRFCTests.json b/Foundation/TestData/GTMURITemplateRFCTests.json new file mode 100644 index 0000000..03fa22d --- /dev/null +++ b/Foundation/TestData/GTMURITemplateRFCTests.json @@ -0,0 +1,131 @@ +{ + "Test Suite 1" : + { + "variables": { + "var" : "value", + "hello" : "Hello World!", + "empty" : "", + "list" : [ "val1", "val2", "val3" ], + "keys" : {"key1": "val1", "key2": "val2"}, + "path" : "/foo/bar", + "x" : "1024", + "y" : "768" + }, + "testcases" : [ + ["{var}", "value"], + ["{hello}", "Hello%20World%21"], + ["{path}/here", "%2Ffoo%2Fbar/here"], + ["{x,y}", "1024,768"], + ["{var=default}", "value"], + ["{undef=default}", "default"], + ["{list}", "val1,val2,val3"], + ["{list*}", "val1,val2,val3"], + ["{list+}", "list.val1,list.val2,list.val3"], + ["{keys}", "key1,val1,key2,val2"], + ["{keys*}", "key1,val1,key2,val2"], + ["{keys+}", "keys.key1,val1,keys.key2,val2"], + ["{+var}", "value"], + ["{+hello}", "Hello%20World!"], + ["{+path}/here", "/foo/bar/here"], + ["{+path,x}/here", "/foo/bar,1024/here"], + ["{+path}{x}/here", "/foo/bar1024/here"], + ["{+empty}/here", "/here"], + ["{+undef}/here", "/here"], + ["{+list}", "val1,val2,val3"], + ["{+list*}", "val1,val2,val3"], + ["{+list+}", "list.val1,list.val2,list.val3"], + ["{+keys}", "key1,val1,key2,val2"], + ["{+keys*}", "key1,val1,key2,val2"], + ["{+keys+}", "keys.key1,val1,keys.key2,val2"], + ["{;x,y}", ";x=1024;y=768"], + ["{;x,y,empty}", ";x=1024;y=768;empty"], + ["{;x,y,undef}", ";x=1024;y=768"], + ["{;list}", ";val1,val2,val3"], + ["{;list*}", ";val1;val2;val3"], + ["{;list+}", ";list=val1;list=val2;list=val3"], + ["{;keys}", ";key1,val1,key2,val2"], + ["{;keys*}", ";key1=val1;key2=val2"], + ["{;keys+}", ";keys.key1=val1;keys.key2=val2"], + ["{?x,y}", "?x=1024&y=768"], + ["{?x,y,empty}", "?x=1024&y=768&empty"], + ["{?x,y,undef}", "?x=1024&y=768"], + ["{?list}", "?list=val1,val2,val3"], + ["{?list*}", "?val1&val2&val3"], + ["{?list+}", "?list=val1&list=val2&list=val3"], + ["{?keys}", "?keys=key1,val1,key2,val2"], + ["{?keys*}", "?key1=val1&key2=val2"], + ["{?keys+}", "?keys.key1=val1&keys.key2=val2"], + ["{/var}", "/value"], + ["{/var,empty}", "/value/"], + ["{/var,undef}", "/value"], + ["{/list}", "/val1,val2,val3"], + ["{/list*}", "/val1/val2/val3"], + ["{/list*,x}", "/val1/val2/val3/1024"], + ["{/list+}", "/list.val1/list.val2/list.val3"], + ["{/keys}", "/key1,val1,key2,val2"], + ["{/keys*}", "/key1/val1/key2/val2"], + ["{/keys+}", "/keys.key1/val1/keys.key2/val2"], + ["X{.var}", "X.value"], + ["X{.empty}", "X"], + ["X{.undef}", "X"], + ["X{.list}", "X.val1,val2,val3"], + ["X{.list*}", "X.val1.val2.val3"], + ["X{.list*,x}", "X.val1.val2.val3.1024"], + ["X{.list+}", "X.list.val1.list.val2.list.val3"], + ["X{.keys}", "X.key1,val1,key2,val2"], + ["X{.keys*}", "X.key1.val1.key2.val2"], + ["X{.keys+}", "X.keys.key1.val1.keys.key2.val2"] + ] + }, + "Test Suite 2" : + { + "variables": { + "var" : "value", + "empty" : "", + "name" : [ "Fred", "Wilma", "Pebbles" ], + "favs" : {"color":"red", "volume": "high"}, + "empty_list" : [], + "empty_keys" : {} + }, + "testcases" : [ + ["{var=default}", "value"], + ["{undef=default}", "default"], + ["x{empty}y", "xy"], + ["x{empty=_}y", "xy"], + ["x{undef}y", "xy"], + ["x{undef=_}y", "x_y"], + ["x{empty_list}y", "xy"], + ["x{empty_list=_}y", "x_y"], + ["x{empty_list*}y", "xy"], + ["x{empty_list*=_}y", "x_y"], + ["x{empty_list+}y", "xy"], + ["x{empty_list+=_}y", "x_y"], + ["x{empty_keys}y", "xy"], + ["x{empty_keys=_}y", "x_y"], + ["x{empty_keys*}y", "xy"], + ["x{empty_keys*=_}y", "x_y"], + ["x{empty_keys+}y", "xy"], + ["x{empty_keys+=_}y", "x_y"], + ["x{?name=none}", "x?name=Fred,Wilma,Pebbles"], + ["x{?favs=none}", "x?favs=color,red,volume,high"], + ["x{?favs*=none}", "x?color=red&volume=high"], + ["x{?favs+=none}", "x?favs.color=red&favs.volume=high"], + ["x{?undef}", "x"], + ["x{?undef=none}", "x?undef=none"], + ["x{?empty}", "x?empty"], + ["x{?empty=none}", "x?empty"], + ["x{?empty_list}", "x"], + ["x{?empty_list=none}", "x?empty_list=none"], + ["x{?empty_list*}", "x"], + ["x{?empty_list*=none}", "x?empty_list=none"], + ["x{?empty_list+}", "x"], + ["x{?empty_list+=none}", "x?empty_list=none"], + ["x{?empty_keys}", "x"], + ["x{?empty_keys=none}", "x?empty_keys=none"], + ["x{?empty_keys*}", "x"], + ["x{?empty_keys*=none}", "x?empty_keys=none"], + ["x{?empty_keys+}", "x"], + ["x{?empty_keys+=none}", "x?empty_keys=none"] + ] + } +} |