As of iOS 13.3.1, apps installed with free developer accounts that contain embedded frameworks fail to launch. To work around this, we now link all dependencies via Cocoapods as static libraries.
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
|