512 lines
18 KiB
Objective-C
512 lines
18 KiB
Objective-C
/* 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.
|
|
*/
|
|
|
|
#if !__has_feature(objc_arc)
|
|
#error "This file needs to be compiled with ARC enabled."
|
|
#endif
|
|
|
|
#import "GTLRURITemplate.h"
|
|
|
|
// 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;
|
|
__unsafe_unretained NSString *joiner;
|
|
BOOL allowReservedInEscape;
|
|
|
|
// Update for each variable.
|
|
__unsafe_unretained NSString *explode;
|
|
};
|
|
|
|
// Helper just to shorten the lines when needed.
|
|
static NSString *UnescapeString(NSString *str) {
|
|
return [str stringByRemovingPercentEncoding];
|
|
}
|
|
|
|
static NSString *EscapeString(NSString *str, BOOL allowReserved) {
|
|
// The spec is a little hard to map onto the charsets, so force
|
|
// reserved bits in/out.
|
|
NSMutableCharacterSet *cs = [[NSCharacterSet URLQueryAllowedCharacterSet] mutableCopy];
|
|
NSString * const kReservedChars = @":/?#[]@!$&'()*+,;=";
|
|
if (allowReserved) {
|
|
[cs addCharactersInString:kReservedChars];
|
|
} else {
|
|
[cs removeCharactersInString:kReservedChars];
|
|
}
|
|
NSString *resultStr = [str stringByAddingPercentEncodingWithAllowedCharacters:cs];
|
|
return resultStr;
|
|
}
|
|
|
|
static NSString *StringFromNSNumber(NSNumber *rawValue) {
|
|
NSString *strValue;
|
|
// NSNumber doesn't expose a way to tell if it is holding a BOOL or something
|
|
// else. -[NSNumber objCType] for a BOOL is the same as @encoding(char), but
|
|
// in the 64bit runtine @encoding(BOOL) (or for "bool") won't match that as
|
|
// the 64bit runtime actually has a true boolean type. Instead we reply on
|
|
// checking if the numbers are the CFBoolean constants to force true/value
|
|
// values.
|
|
if ((rawValue == (NSNumber *)kCFBooleanTrue) ||
|
|
(rawValue == (NSNumber *)kCFBooleanFalse)) {
|
|
strValue = (rawValue.boolValue ? @"true" : @"false");
|
|
} else {
|
|
strValue = [rawValue stringValue];
|
|
}
|
|
return strValue;
|
|
}
|
|
|
|
@implementation GTLRURITemplate
|
|
|
|
#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;
|
|
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^{
|
|
operatorSet = [NSCharacterSet characterSetWithCharactersInString:@"+./;?|!@"];
|
|
explodeSet = [NSCharacterSet characterSetWithCharactersInString:@"*+"];
|
|
partialSet = [NSCharacterSet characterSetWithCharactersInString:@":^"];
|
|
});
|
|
|
|
// 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 atleast 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 (__strong 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:(NSDictionary *)valueProvider
|
|
defaultValues:(NSMutableDictionary *)defaultValues {
|
|
NSString *prefix = nil;
|
|
struct ExpansionInfo expansionInfo = {
|
|
.expressionOperator = expressionOperator,
|
|
.joiner = nil,
|
|
.allowReservedInEscape = NO,
|
|
.explode = nil,
|
|
};
|
|
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:@"GTLRURITemplateUnsupported"
|
|
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]])
|
|
&& ((NSArray *)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.
|
|
NSString *strValue = StringFromNSNumber(rawValue);
|
|
result = [self expandString:strValue
|
|
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:@"GTLRURITemplateUnsupported"
|
|
format:@"Variable returned unsupported type (%@)",
|
|
NSStringFromClass([rawValue class])];
|
|
}
|
|
|
|
// Did it generate anything?
|
|
if (result == nil)
|
|
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:@"GTLRURITemplateUnsupported"
|
|
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 = '.';
|
|
unichar expressionOperator = expansionInfo->expressionOperator;
|
|
if ((expressionOperator == ';') || (expressionOperator == '?')) {
|
|
variableValueJoiner = '=';
|
|
}
|
|
// Loop over the values.
|
|
for (id rawValue in valueArray) {
|
|
NSString *value;
|
|
if ([rawValue isKindOfClass:[NSNumber class]]) {
|
|
value = StringFromNSNumber((id)rawValue);
|
|
} else if ([rawValue isKindOfClass:[NSString class]]) {
|
|
value = rawValue;
|
|
} else {
|
|
[NSException raise:@"GTLRURITemplateUnsupported"
|
|
format:@"Variable '%@' returned NSArray with unsupported type (%@), array: %@",
|
|
variableName, NSStringFromClass([rawValue class]), 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;
|
|
unichar expressionOperator = expansionInfo->expressionOperator;
|
|
if (expansionInfo->explode == nil) {
|
|
keyValueJoiner = @",";
|
|
} else if ((expressionOperator == ';') || (expressionOperator == '?')) {
|
|
keyValueJoiner = @"=";
|
|
}
|
|
// Loop over the sorted keys.
|
|
NSArray *sortedKeys = [valueDict.allKeys sortedArrayUsingSelector:@selector(compare:)];
|
|
for (__strong 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 == 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;
|
|
}
|
|
|
|
#pragma mark Public API
|
|
|
|
+ (NSString *)expandTemplate:(NSString *)uriTemplate
|
|
values:(NSDictionary *)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
|