Skip to content

Commit

Permalink
Start writing the README.
Browse files Browse the repository at this point in the history
  • Loading branch information
jverkoey committed Aug 29, 2011
1 parent 0042a71 commit 684f9a2
Show file tree
Hide file tree
Showing 4 changed files with 183 additions and 28 deletions.
59 changes: 57 additions & 2 deletions README.mdown
Original file line number Diff line number Diff line change
@@ -1,4 +1,59 @@
SOCKit
=====
======

String <-> Object Coder for Objective-C.
String <-> Object Coding for Objective-C.

With SOCKit and [SOCPattern][] you can easily transform objects into strings and vice versa.

## Two examples, cuz devs love examples.

```obj-c
SOCPattern* pattern = [SOCPattern patternWithString:@"api.github.com/users/(username)/gists"];
[pattern stringFromObject:githubUser];
> @"api.github.com/users/jverkoey/gists"
```
```obj-c
SOCPattern* pattern = [SOCPattern patternWithString:@"github.com/(initWithUsername:)"];
[pattern performPatternSelectorOnObject:[GithubUser class] sourceString:@"github.com/jverkoey"];
> <GithubUser> username = jverkoey
```

## Hey, this is really similar to defining routes in Rails

Damn straight it is. There are some differences, though. Parameters in Rails are denoted using
a colon, like `:this`. SOCKit parameters are denoted using parenthesis, like `(this)`. The reason
for this is that you can use dot notation in SOCKit to recursively access sub-objects
from the receiver. You can even perform [KVC collection operators][].

Let's consider a quick Rails example from the Rails docs:

```ruby
match "/patients/:id" => "patients#show"
```

The equivalent SOCKit pattern would be:

```obj-c
[SOCPattern patternWithString:@"/patients/(id)"]; // For creating URLs from patient objects

Patient* patient = [SOCPattern performSelector:@selector(initWithId:) onObject:[Patient class] sourceString:@"/patients/17"];
patient
> <Patient> id = 17
[SOCPattern stringFromObject:patient];
> @"/patients/17"
```
## Add SOCKit to your project
This lightweight library is built to be a dead-simple airdrop into your project. Contained
in SOCKit.h and SOCKit.m is all of the functionality you will need in order to start mapping
Strings <-> Objects. To start using SOCKit, simply download or `git checkout` the sockit repo
and drag SOCKit.h and SOCKit.m to your project's source tree. `#import "SOCKit.h"` where you want
to use SOCKit and start pumping out some mad String <-> Object coding.
## Hey, this is
[SOCPattern]: https://github.com/jverkoey/sockit/blob/master/SOCKit.h
[KVC collection operators]: http://developer.apple.com/library/ios/#documentation/cocoa/conceptual/KeyValueCoding/Articles/CollectionOperators.html#//apple_ref/doc/uid/20002176-BAJEAIEE
84 changes: 73 additions & 11 deletions SOCKit.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,21 @@

#import <Foundation/Foundation.h>

/**
* A convenience method for:
*
* SOCPattern* pattern = [SOCPattern patternWithString:string];
* NSString* result = [pattern stringFromObject:object];
*
* @see documentation for stringFromObject:
*/
NSString* SOCStringFromStringWithObject(NSString* string, id object);

/**
* String <-> Object Coding.
*
* Easily extract information from strings into objects and vice versa using KVC.
*/
@interface SOCPattern : NSObject {
@private
NSString* _patternString;
Expand All @@ -31,35 +44,84 @@ NSString* SOCStringFromStringWithObject(NSString* string, id object);
SEL _outboundSelector;
}

/**
* Initialize a newly allocated pattern object with the given pattern string.
*
* A pattern string is a string with parameter names wrapped in parenthesis. Pattern strings
* are classified into two categories: inbound and outbound.
*
* An inbound pattern can use stringFromObject: to create a string with a given object's
* values.
*
* An outbound pattern can use the perform methods to extract values from the string and call
* selectors on objects.
*
* Example inbound patterns:
*
* api.github.com/users/(username)/gists
* [pattern stringFromObject:githubUser];
* returns: @"api.github.com/users/jverkoey/gists"
*
* api.github.com/repos/(username)/(repo)/issues
* [pattern stringFromObject:githubRepo];
* returns: @"api.github.com/repos/jverkoey/sockit/issues"
*
* Example outbound patterns:
*
* github.com/(initWithUsername:)
* [pattern performPatternSelectorOnObject:[GithubUser class] sourceString:@"github.com/jverkoey"];
* returns: an allocated, initialized, and autoreleased GithubUser object with @"jverkoey" passed
* to the initWithUsername: method.
*
* github.com/(initWithUsername:)/(repoName:)
* [pattern performPatternSelectorOnObject:[GithubUser class] sourceString:@"github.com/jverkoey/sockit"];
* returns: an allocated, initialized, and autoreleased GithubUser object with @"jverkoey" and
* @"sockit" passed to the initWithUsername:repoName: method.
*
* github.com/(setUsername:)
* [pattern performPatternSelectorOnObject:githubUser sourceString:@"github.com/jverkoey"];
* returns: nil because setUsername: does not have a return value. githubUser's username property
* is now @"jverkoey".
*/
- (id)initWithString:(NSString *)string;
+ (id)patternWithString:(NSString *)string;

/**
* Returns YES if the given string conforms to this pattern.
*
* A conforming string must match all of the static portions of the pattern and provide values
* for each of the parenthesized portions.
* Returns YES if the given string can be used with this pattern's perform methods.
*
* A conforming string can be used as the conformingString in
* performSelectorWithObject:conformingString:.
* A conforming string must exactly match all of the static portions of the pattern and provide
* values for each of the parenthesized portions.
*
* @param string A string that may or may not conform to this pattern.
* @returns YES if the given string conforms to this pattern, NO otherwise.
*/
- (BOOL)doesStringConform:(NSString *)string;

/**
* Performs this pattern's selector on the object with the values retrieved from matchingString.
* Performs this pattern's selector on the object with the matching parameter values from
* sourceString.
*
* @param object This pattern's selector will be called on this object. If this
* pattern is an initializer pattern and object is a Class then the
* class will be allocated, the selector performed on the allocated
* object, and the initialized object returned.
* @param matchingString A string that conforms to this pattern.
* @returns The initialized object if this pattern is an initializer and object is a Class,
* otherwise nil.
* @param sourceString A string that conforms to this pattern.
* @returns The initialized, autoreleased object if this pattern is an initializer and
* object is a Class, otherwise the return value from invoking the selector.
*/
- (id)performPatternSelectorOnObject:(id)object sourceString:(NSString *)sourceString;

/**
* Performs the given selector on the object with the matching parameter values from sourceString.
*
* @param selector The selector to perform on the object.
* @param object The selector will be performed on this object.
* @param sourceString A string that conforms to this pattern. The parameter values from
* this string are used when performing the selector on the object.
* @returns The initialized, autoreleased object if the selector is an initializer and
* object is a Class, otherwise the return value from invoking the selector.
*/
- (id)performSelectorOnObject:(id)object string:(NSString *)matchingString;
- (id)performSelector:(SEL)selector onObject:(id)object sourceString:(NSString *)sourceString;

/**
* Returns a string with the parenthesized portions of this pattern replaced using
Expand Down
34 changes: 23 additions & 11 deletions SOCKit.m
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ - (void)_compilePattern {
}
}

// Enforce one of only inbound, only outbound, or no parameters.
// Enforce one of: only inbound, only outbound, or no parameters.
NSAssert([inboundParameters count] > 0 && [outboundParameters count] == 0
|| [inboundParameters count] == 0 && [outboundParameters count] > 0
|| [inboundParameters count] == 0 && [outboundParameters count] == 0,
Expand All @@ -168,8 +168,8 @@ - (void)_compilePattern {
lastWasParameter = NO;
}
}
// Compile the selector.

// Compile the outbound selector.
NSMutableString* selectorString = [[NSMutableString alloc] init];
for (SOCParameter* parameter in outboundParameters) {
[selectorString appendString:parameter.string];
Expand Down Expand Up @@ -207,7 +207,7 @@ - (BOOL)gatherParameterValues:(NSArray**)pValues fromString:(NSString *)string

NSMutableArray* values = nil;
if (nil != pValues) {
values = [NSMutableArray arrayWithCapacity:[_outboundParameters count]];
values = [NSMutableArray array];
}

NSInteger tokenIndex = 0;
Expand Down Expand Up @@ -326,13 +326,13 @@ - (void)setArgument:(NSString*)text withType:(SOCArgumentType)type atIndex:(NSIn

///////////////////////////////////////////////////////////////////////////////////////////////////
- (void)setArgumentsFromValues:(NSArray *)values forInvocation:(NSInvocation *)invocation {
Method method = class_getInstanceMethod([invocation.target class], _outboundSelector);
Method method = class_getInstanceMethod([invocation.target class], invocation.selector);
NSAssert(nil != method, @"The method must exist with the given invocation target.");

for (NSInteger ix = 0; ix < [values count]; ++ix) {
NSString* value = [values objectAtIndex:ix];

char argType[32];
char argType[4];
method_getArgumentType(method, ix + 2, argType, sizeof(argType) / sizeof(typeof(argType[0])));
SOCArgumentType type = SOCArgumentTypeForTypeAsChar(argType[0]);

Expand All @@ -342,17 +342,29 @@ - (void)setArgumentsFromValues:(NSArray *)values forInvocation:(NSInvocation *)i


///////////////////////////////////////////////////////////////////////////////////////////////////
- (id)performSelectorOnObject:(id)object string:(NSString *)matchingString {
- (id)performPatternSelectorOnObject:(id)object sourceString:(NSString *)sourceString {
return [self performSelector:_outboundSelector onObject:object sourceString:sourceString];
}


///////////////////////////////////////////////////////////////////////////////////////////////////
- (id)performSelector:(SEL)selector onObject:(id)object sourceString:(NSString *)sourceString {
BOOL isInitializer = [NSStringFromSelector(selector) hasPrefix:@"init"] && [object class] == object;

if (isInitializer) {
object = [[object alloc] autorelease];
}

NSArray* values = nil;
NSAssert([self gatherParameterValues:&values fromString:matchingString], @"The pattern can't be used with this string.");
NSAssert([self gatherParameterValues:&values fromString:sourceString], @"The pattern can't be used with this string.");

id returnValue = nil;

NSMethodSignature* sig = [object methodSignatureForSelector:_outboundSelector];
NSAssert(nil != sig, @"Object does not respond to selector: '%@'", NSStringFromSelector(_outboundSelector));
NSMethodSignature* sig = [object methodSignatureForSelector:selector];
NSAssert(nil != sig, @"%@ does not respond to selector: '%@'", object, NSStringFromSelector(selector));
NSInvocation* invocation = [NSInvocation invocationWithMethodSignature:sig];
[invocation setTarget:object];
[invocation setSelector:_outboundSelector];
[invocation setSelector:selector];
[self setArgumentsFromValues:values forInvocation:invocation];
[invocation invoke];

Expand Down
34 changes: 30 additions & 4 deletions tests/SOCKitTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
@interface SOCTestObject : NSObject

- (id)initWithId:(NSInteger)ident floatValue:(CGFloat)flv doubleValue:(double)dv longLongValue:(long long)llv stringValue:(NSString *)string;
- (id)initWithId:(NSInteger)ident floatValue:(CGFloat)flv doubleValue:(double)dv longLongValue:(long long)llv stringValue:(NSString *)string userInfo:(id)userInfo;

@property (nonatomic, readwrite, assign) NSInteger ident;
@property (nonatomic, readwrite, assign) CGFloat flv;
Expand Down Expand Up @@ -56,6 +57,10 @@ - (id)initWithId:(NSInteger)anIdent floatValue:(CGFloat)anFlv doubleValue:(doubl
return self;
}

- (id)initWithId:(NSInteger)anIdent floatValue:(CGFloat)anFlv doubleValue:(double)aDv longLongValue:(long long)anLlv stringValue:(NSString *)aString userInfo:(id)userInfo {
return [self initWithId:anIdent floatValue:anFlv doubleValue:aDv longLongValue:anLlv stringValue:aString];
}

@end

@interface SOCKitTests : SenTestCase
Expand Down Expand Up @@ -166,16 +171,37 @@ - (void)testOutboundParameters {


///////////////////////////////////////////////////////////////////////////////////////////////////
- (void)testStringDecoding {
- (void)testPerformSelectorOnObjectWithSourceString {
SOCPattern* pattern = [SOCPattern patternWithString:@"soc://(initWithId:)/(floatValue:)/(doubleValue:)/(longLongValue:)/(stringValue:)"];
SOCTestObject* testObject = [SOCTestObject alloc];
[pattern performSelectorOnObject:testObject string:@"soc://3/3.5/6.14/13413143124321/dilly"];
SOCTestObject* testObject = [pattern performPatternSelectorOnObject:[SOCTestObject class] sourceString:@"soc://3/3.5/6.14/13413143124321/dilly"];
STAssertEquals(testObject.ident, (NSInteger)3, @"Values should be equal.");
STAssertEquals(testObject.flv, (CGFloat)3.5, @"Values should be equal.");
STAssertEquals(testObject.dv, 6.14, @"Values should be equal.");
STAssertEquals(testObject.llv, (long long)13413143124321, @"Values should be equal.");
STAssertTrue([testObject.string isEqualToString:@"dilly"], @"Values should be equal.");

testObject = [pattern performSelector:@selector(initWithId:floatValue:doubleValue:longLongValue:stringValue:userInfo:) onObject:[SOCTestObject class] sourceString:@"soc://3/3.5/6.14/13413143124321/dilly"];
STAssertEquals(testObject.ident, (NSInteger)3, @"Values should be equal.");
STAssertEquals(testObject.flv, (CGFloat)3.5, @"Values should be equal.");
STAssertEquals(testObject.dv, 6.14, @"Values should be equal.");
STAssertEquals(testObject.llv, (long long)13413143124321, @"Values should be equal.");
STAssertTrue([testObject.string isEqualToString:@"dilly"], @"Values should be equal.");
[testObject release];

pattern = [SOCPattern patternWithString:@"soc://(id)/(flv)/(dv)/(llv)/(string)"];
testObject = [pattern performSelector:@selector(initWithId:floatValue:doubleValue:longLongValue:stringValue:) onObject:[SOCTestObject class] sourceString:@"soc://3/3.5/6.14/13413143124321/dilly"];
STAssertEquals(testObject.ident, (NSInteger)3, @"Values should be equal.");
STAssertEquals(testObject.flv, (CGFloat)3.5, @"Values should be equal.");
STAssertEquals(testObject.dv, 6.14, @"Values should be equal.");
STAssertEquals(testObject.llv, (long long)13413143124321, @"Values should be equal.");
STAssertTrue([testObject.string isEqualToString:@"dilly"], @"Values should be equal.");

pattern = [SOCPattern patternWithString:@"soc://(setIdent:)"];
[pattern performPatternSelectorOnObject:testObject sourceString:@"soc://6"];
STAssertEquals(testObject.ident, (NSInteger)6, @"Values should be equal.");

pattern = [SOCPattern patternWithString:@"soc://(setIdent:)"];
[pattern performSelector:@selector(setLlv:) onObject:testObject sourceString:@"soc://6"];
STAssertEquals(testObject.llv, (long long)6, @"Values should be equal.");
}

@end

0 comments on commit 684f9a2

Please sign in to comment.