aboutsummaryrefslogtreecommitdiff
path: root/Foundation
diff options
context:
space:
mode:
authorGravatar gtm.daemon <gtm.daemon@7dc7ac4e-7543-0410-b95c-c1676fc8e2a3>2010-10-14 19:30:33 +0000
committerGravatar gtm.daemon <gtm.daemon@7dc7ac4e-7543-0410-b95c-c1676fc8e2a3>2010-10-14 19:30:33 +0000
commit9ba6c2468a3e9c2638bfda785f38f7881c3e7f5d (patch)
tree9aada9758f79bd6a99d06a9fade941012a2abacf /Foundation
parent9d5523b1f41bb93b76fd23cff42576fd67500f36 (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.h44
-rw-r--r--Foundation/GTMURITemplate.m523
-rw-r--r--Foundation/GTMURITemplateTest.m133
-rw-r--r--Foundation/TestData/GTMURITemplateExtraTests.json222
-rw-r--r--Foundation/TestData/GTMURITemplateRFCTests.json131
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"]
+ ]
+ }
+}