diff options
-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 | ||||
-rw-r--r-- | GTM.xcodeproj/project.pbxproj | 30 | ||||
-rw-r--r-- | ReleaseNotes.txt | 2 |
7 files changed, 1084 insertions, 1 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"] + ] + } +} diff --git a/GTM.xcodeproj/project.pbxproj b/GTM.xcodeproj/project.pbxproj index 4f77c6c..0696e02 100644 --- a/GTM.xcodeproj/project.pbxproj +++ b/GTM.xcodeproj/project.pbxproj @@ -99,7 +99,7 @@ 8B1B49190E5F8E2100A08972 /* GTMExceptionalInlines.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B1B49170E5F8E2100A08972 /* GTMExceptionalInlines.m */; }; 8B1B49260E5F97C800A08972 /* GTMExceptionalInlinesTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B1B491B0E5F904C00A08972 /* GTMExceptionalInlinesTest.m */; }; 8B21BE9211532CDC00DD2686 /* GTMNSAnimatablePropertyContainerTest.xib in Resources */ = {isa = PBXBuildFile; fileRef = 8B21BE9111532CDC00DD2686 /* GTMNSAnimatablePropertyContainerTest.xib */; }; - 8B21DE56117E5CB7000E004F /* GTMLocalizedString.h in Headers */ = {isa = PBXBuildFile; fileRef = 8B21DE54117E5CB7000E004F /* GTMLocalizedString.h */; }; + 8B21DE56117E5CB7000E004F /* GTMLocalizedString.h in Headers */ = {isa = PBXBuildFile; fileRef = 8B21DE54117E5CB7000E004F /* GTMLocalizedString.h */; settings = {ATTRIBUTES = (Public, ); }; }; 8B2789960EF855FB00D68C01 /* GTMUnitTestingWindow.10.5.6.tiff in Resources */ = {isa = PBXBuildFile; fileRef = 8B2789950EF855FB00D68C01 /* GTMUnitTestingWindow.10.5.6.tiff */; }; 8B29078711F8D1BF0064F50F /* GTMNSFileHandle+UniqueName.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B29078511F8D1BF0064F50F /* GTMNSFileHandle+UniqueName.m */; }; 8B29078811F8D1BF0064F50F /* GTMNSFileHandle+UniqueNameTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B29078611F8D1BF0064F50F /* GTMNSFileHandle+UniqueNameTest.m */; }; @@ -303,6 +303,11 @@ F42597790E23FE3A003BEA3E /* GTMNSString+FindFolder.h in Headers */ = {isa = PBXBuildFile; fileRef = F42597760E23FE3A003BEA3E /* GTMNSString+FindFolder.h */; settings = {ATTRIBUTES = (Public, ); }; }; F425977A0E23FE3A003BEA3E /* GTMNSString+FindFolder.m in Sources */ = {isa = PBXBuildFile; fileRef = F42597770E23FE3A003BEA3E /* GTMNSString+FindFolder.m */; }; F425977F0E23FE43003BEA3E /* GTMNSString+FindFolderTest.m in Sources */ = {isa = PBXBuildFile; fileRef = F42597780E23FE3A003BEA3E /* GTMNSString+FindFolderTest.m */; }; + F42866B81267340A0090FE0F /* GTMURITemplate.h in Headers */ = {isa = PBXBuildFile; fileRef = F42866B51267340A0090FE0F /* GTMURITemplate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + F42866B91267340A0090FE0F /* GTMURITemplate.m in Sources */ = {isa = PBXBuildFile; fileRef = F42866B61267340A0090FE0F /* GTMURITemplate.m */; }; + F42866BC1267340A0090FE0F /* GTMURITemplateTest.m in Sources */ = {isa = PBXBuildFile; fileRef = F42866B71267340A0090FE0F /* GTMURITemplateTest.m */; }; + F428670312673C490090FE0F /* GTMURITemplateExtraTests.json in Resources */ = {isa = PBXBuildFile; fileRef = F428670112673C490090FE0F /* GTMURITemplateExtraTests.json */; }; + F428670412673C490090FE0F /* GTMURITemplateRFCTests.json in Resources */ = {isa = PBXBuildFile; fileRef = F428670212673C490090FE0F /* GTMURITemplateRFCTests.json */; }; F428FF030D48E55E00382ED1 /* GTMNSBezierPath+CGPath.h in Headers */ = {isa = PBXBuildFile; fileRef = F428FEFF0D48E55E00382ED1 /* GTMNSBezierPath+CGPath.h */; settings = {ATTRIBUTES = (Public, ); }; }; F428FF040D48E55E00382ED1 /* GTMNSBezierPath+CGPath.m in Sources */ = {isa = PBXBuildFile; fileRef = F428FF000D48E55E00382ED1 /* GTMNSBezierPath+CGPath.m */; }; F42E08330D19992100D5DDE0 /* GTMNSString+HTMLTest.m in Sources */ = {isa = PBXBuildFile; fileRef = F48FE2900D198D24009257D2 /* GTMNSString+HTMLTest.m */; }; @@ -779,6 +784,11 @@ F42597760E23FE3A003BEA3E /* GTMNSString+FindFolder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "GTMNSString+FindFolder.h"; sourceTree = "<group>"; }; F42597770E23FE3A003BEA3E /* GTMNSString+FindFolder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "GTMNSString+FindFolder.m"; sourceTree = "<group>"; }; F42597780E23FE3A003BEA3E /* GTMNSString+FindFolderTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "GTMNSString+FindFolderTest.m"; sourceTree = "<group>"; }; + F42866B51267340A0090FE0F /* GTMURITemplate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GTMURITemplate.h; sourceTree = "<group>"; }; + F42866B61267340A0090FE0F /* GTMURITemplate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTMURITemplate.m; sourceTree = "<group>"; }; + F42866B71267340A0090FE0F /* GTMURITemplateTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTMURITemplateTest.m; sourceTree = "<group>"; }; + F428670112673C490090FE0F /* GTMURITemplateExtraTests.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = GTMURITemplateExtraTests.json; sourceTree = "<group>"; }; + F428670212673C490090FE0F /* GTMURITemplateRFCTests.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = GTMURITemplateRFCTests.json; sourceTree = "<group>"; }; F428FEFF0D48E55E00382ED1 /* GTMNSBezierPath+CGPath.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "GTMNSBezierPath+CGPath.h"; sourceTree = "<group>"; }; F428FF000D48E55E00382ED1 /* GTMNSBezierPath+CGPath.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "GTMNSBezierPath+CGPath.m"; sourceTree = "<group>"; }; F428FF010D48E55E00382ED1 /* GTMNSBezierPath+CGPathTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "GTMNSBezierPath+CGPathTest.m"; sourceTree = "<group>"; }; @@ -1119,6 +1129,15 @@ path = TestData; sourceTree = "<group>"; }; + F428670012673C1D0090FE0F /* TestData */ = { + isa = PBXGroup; + children = ( + F428670112673C490090FE0F /* GTMURITemplateExtraTests.json */, + F428670212673C490090FE0F /* GTMURITemplateRFCTests.json */, + ); + path = TestData; + sourceTree = "<group>"; + }; F435E46C0DC8F23A0069CDE8 /* TestData */ = { isa = PBXGroup; children = ( @@ -1358,6 +1377,9 @@ F48FE2720D198CCE009257D2 /* Foundation */ = { isa = PBXGroup; children = ( + F42866B51267340A0090FE0F /* GTMURITemplate.h */, + F42866B61267340A0090FE0F /* GTMURITemplate.m */, + F42866B71267340A0090FE0F /* GTMURITemplateTest.m */, 0B1B9B8410FECD870084EE4B /* GTMStringEncoding.h */, 0B1B9B8510FECD870084EE4B /* GTMStringEncoding.m */, 0B1B9B8610FECD870084EE4B /* GTMStringEncodingTest.m */, @@ -1492,6 +1514,7 @@ 8B3AA9EF0E033E23007E31B5 /* GTMValidatingContainers.h */, 8B3AA9F00E033E23007E31B5 /* GTMValidatingContainers.m */, 8B3AA9F70E033E5F007E31B5 /* GTMValidatingContainersTest.m */, + F428670012673C1D0090FE0F /* TestData */, ); path = Foundation; sourceTree = "<group>"; @@ -1653,6 +1676,7 @@ 8BCB59F011C00ED6009B6C40 /* GTMNSScanner+Unsigned.h in Headers */, 8B29080A11F8E1670064F50F /* GTMNSFileHandle+UniqueName.h in Headers */, 8B414E891226FB1000D0064F /* GTMServiceManagement.h in Headers */, + F42866B81267340A0090FE0F /* GTMURITemplate.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1917,6 +1941,8 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + F428670312673C490090FE0F /* GTMURITemplateExtraTests.json in Resources */, + F428670412673C490090FE0F /* GTMURITemplateRFCTests.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2212,6 +2238,7 @@ 8B29078711F8D1BF0064F50F /* GTMNSFileHandle+UniqueName.m in Sources */, 8B29078811F8D1BF0064F50F /* GTMNSFileHandle+UniqueNameTest.m in Sources */, 8B414E8B1226FB1800D0064F /* GTMServiceManagementTest.m in Sources */, + F42866BC1267340A0090FE0F /* GTMURITemplateTest.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2288,6 +2315,7 @@ 8BCB59F111C00ED6009B6C40 /* GTMNSScanner+Unsigned.m in Sources */, 8B29080911F8E1630064F50F /* GTMNSFileHandle+UniqueName.m in Sources */, 8B414E881226FB1000D0064F /* GTMServiceManagement.c in Sources */, + F42866B91267340A0090FE0F /* GTMURITemplate.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ReleaseNotes.txt b/ReleaseNotes.txt index 2f26e92..435bda8 100644 --- a/ReleaseNotes.txt +++ b/ReleaseNotes.txt @@ -16,6 +16,8 @@ Changes since 1.6.0 - Removed Foundation/GTMBase64 and Foundation/GTMNSData+Hex in favor of Foundation/GTMStringEncoding. +- Added Foundation/GTMURITemplate to support the pending standard. + Release 1.6.0 Changes since 1.5.1 |