├── .gitignore ├── .autotest ├── package.json ├── Tests ├── TestHelper.j ├── CRSupportTest.j └── CRBaseTest.j ├── Jakefile ├── Framework └── CappuccinoResource │ ├── CRSupport.j │ └── CRBase.j ├── README.md └── common.jake /.gitignore: -------------------------------------------------------------------------------- 1 | Build -------------------------------------------------------------------------------- /.autotest: -------------------------------------------------------------------------------- 1 | class Autotest 2 | def make_test_cmd files_to_test 3 | return "jake test" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "CappuccinoResource", 3 | "dependencies": ["narwhal", "objective-j", "cappuccino"], 4 | "author": "Jerod Santo", 5 | "description": "Cappuccino on Rails", 6 | "keywords": ["objective-j", "cappuccino", "rails", "resource"], 7 | "objj-frameworks": ["Framework"] 8 | } 9 | -------------------------------------------------------------------------------- /Tests/TestHelper.j: -------------------------------------------------------------------------------- 1 | // import all the necessary stuff to run tests 2 | @import 3 | @import 4 | @import "../Framework/CappuccinoResource/CRBase.j" 5 | 6 | @implementation Observer : CPObject 7 | { 8 | CPArray _postedNotifications; 9 | } 10 | 11 | - (id)init 12 | { 13 | if (self = [super init]) 14 | { 15 | _postedNotifications = [CPArray array]; 16 | } 17 | return self; 18 | } 19 | 20 | - (void)startObserving:(CPString)aNotificationName 21 | { 22 | [[CPNotificationCenter defaultCenter] addObserver:self 23 | selector:@selector(notificationPosted:) 24 | name:aNotificationName 25 | object:nil]; 26 | } 27 | 28 | - (void)notificationPosted:(id)sender 29 | { 30 | [_postedNotifications addObject:[sender name]]; 31 | } 32 | 33 | - (BOOL)didObserve:(CPString)aNotificationName 34 | { 35 | return [_postedNotifications containsObject:aNotificationName]; 36 | } 37 | 38 | @end 39 | 40 | // define some classes which inherit from CR to use in testing 41 | 42 | @implementation User : CappuccinoResource 43 | { 44 | CPString email @accessors; 45 | CPString password @accessors; 46 | int age @accessors; 47 | BOOL isAlive @accessors; 48 | } 49 | 50 | - (JSObject)attributes 51 | { 52 | return {'email':email,'password':password, 'age':age}; 53 | } 54 | 55 | @end 56 | 57 | @implementation UserSession : CappuccinoResource 58 | { 59 | CPString userName @accessors; 60 | CPDate startDate @accessors; 61 | } 62 | 63 | - (JSObject)attributes 64 | { 65 | return {'user_name':userName,'start_date':[startDate toDateString]}; 66 | } 67 | 68 | 69 | + (CPString)identifierKey 70 | { 71 | return @"token"; 72 | } 73 | 74 | @end 75 | -------------------------------------------------------------------------------- /Jakefile: -------------------------------------------------------------------------------- 1 | /* 2 | * Jakefile 3 | * test 4 | * 5 | */ 6 | 7 | require('./common.jake'); 8 | 9 | var ENV = require("system").env, 10 | FILE = require("file"), 11 | OS = require("os"), 12 | JAKE = require("jake"), 13 | task = JAKE.task, 14 | CLEAN = require("jake/clean").CLEAN, 15 | FileList = JAKE.FileList, 16 | stream = require("narwhal/term").stream, 17 | framework = require("cappuccino/jake").framework, 18 | configuration = ENV["CONFIG"] || ENV["CONFIGURATION"] || ENV["c"] || "Release"; 19 | 20 | framework ("CappuccinoResource", function(task) 21 | { 22 | task.setBuildIntermediatesPath(FILE.join("Build", "CappuccinoResource.build", configuration)); 23 | task.setBuildPath(FILE.join("Build", configuration)); 24 | 25 | task.setProductName("CappuccinoResource"); 26 | task.setIdentifier("org.sant0sk1.cappuccinoResource"); 27 | task.setVersion("1.0"); 28 | task.setAuthor("Jerod Santo"); 29 | task.setEmail("nospam @nospam@ jerodsanto.net"); 30 | task.setSummary("CappuccinoResource"); 31 | task.setSources(new FileList("Framework/CappuccinoResource/*.j")); 32 | task.setInfoPlistPath("Info.plist"); 33 | 34 | if (configuration === "Debug") 35 | task.setCompilerFlags("-DDEBUG -g"); 36 | else 37 | task.setCompilerFlags("-O"); 38 | }); 39 | 40 | task("build", ["CappuccinoResource"]); 41 | 42 | task("debug", function() 43 | { 44 | ENV["CONFIG"] = "Debug" 45 | JAKE.subjake(["."], "build", ENV); 46 | }); 47 | 48 | task("release", function() 49 | { 50 | ENV["CONFIG"] = "Release" 51 | JAKE.subjake(["."], "build", ENV); 52 | }); 53 | 54 | task ("test", function() 55 | { 56 | var tests = new FileList('Tests/*Test.j'); 57 | var cmd = ["ojtest"].concat(tests.items()); 58 | var cmdString = cmd.map(OS.enquote).join(" "); 59 | 60 | var code = OS.system(cmdString); 61 | if (code !== 0) 62 | OS.exit(code); 63 | }); 64 | -------------------------------------------------------------------------------- /Tests/CRSupportTest.j: -------------------------------------------------------------------------------- 1 | @import "TestHelper.j" 2 | 3 | @implementation CRSupportTest : OJTestCase 4 | 5 | - (void)testCPStringRailsifiedString 6 | { 7 | [self assert:@"movies" equals:[[CPString stringWithString:@"Movies"] railsifiedString]]; 8 | [self assert:@"movie_titles" equals:[[CPString stringWithString:@"MovieTitles"] railsifiedString]]; 9 | [self assert:@"movie_titles" equals:[[CPString stringWithString:@"movie_titles"] railsifiedString]]; 10 | [self assert:@"happy_birth_day" equals:[[CPString stringWithString:@"HappyBirthDay"] railsifiedString]]; 11 | } 12 | 13 | - (void)testCPStringCappifiedString 14 | { 15 | [self assert:@"movies" equals:[[CPString stringWithString:@"Movies"] cappifiedString]]; 16 | [self assert:@"movieTitles" equals:[[CPString stringWithString:@"movie_titles"] cappifiedString]]; 17 | [self assert:@"happyBirthDay" equals:[[CPString stringWithString:@"happy_birth_day"] cappifiedString]]; 18 | [self assert:@"happyBirthDay" equals:[[CPString stringWithString:@"happyBirthDay"] cappifiedString]]; 19 | } 20 | 21 | - (void)testCPStringToJSONWithSingleObject 22 | { 23 | var string1 = '{"user":{"email":"test@test.com","password":"secret"}}', 24 | expected1 = {"user":{"email":"test@test.com","password":"secret"}}, 25 | actual1 = [string1 toJSON], 26 | string2 = '{"movie":{"id":42,"title":"Terminator 2"}}', 27 | expected2 = {"movie":{"id":42,"title":"Terminator 2"}}, 28 | actual2 = [string2 toJSON]; 29 | 30 | [self assert:expected1.user.email equals:actual1.user.email]; 31 | [self assert:expected1.user.password equals:actual1.user.password]; 32 | [self assert:expected2.movie.id equals:actual2.movie.id]; 33 | [self assert:expected2.movie.title equals:actual2.movie.title]; 34 | } 35 | 36 | - (void)testCPStringToJSONWithMultipleObjects 37 | { 38 | var string = '[{"user":{"id":1,"email":"one@test.com"}},{"user":{"id":2,"email":"two@test.com"}}]', 39 | expected = [{"user":{"id":1,"email":"one@test.com"}},{"user":{"id":2,"email":"two@test.com"}}], 40 | actual = [string toJSON]; 41 | 42 | [self assert:2 equals:[actual count]]; 43 | [self assert:expected[0].user.email equals:actual[0].user.email]; 44 | [self assert:expected[0].user.id equals:actual[0].user.id]; 45 | [self assert:expected[1].user.email equals:actual[1].user.email]; 46 | [self assert:expected[1].user.id equals:actual[1].user.id]; 47 | } 48 | 49 | - (void)testCPStringParamaterStringFromJSON 50 | { 51 | var params = {"name":"joe","age":27,"sex":"yes please"}, 52 | expected = 'name=joe&age=27&sex=yes%20please'; 53 | [self assert:expected equals:[CPString paramaterStringFromJSON:params]]; 54 | } 55 | 56 | - (void)testCPStringParamaterStringFromJSON 57 | { 58 | var params = [CPDictionary dictionaryWithJSObject:{"name":"joe","age":27,"sex":"yes please"}], 59 | expected = 'name=joe&age=27&sex=yes%20please'; 60 | [self assert:expected equals:[CPString paramaterStringFromCPDictionary:params]]; 61 | } 62 | 63 | 64 | - (void)testCPURLRequestRequestJSONWithURL 65 | { 66 | var request = [CPURLRequest requestJSONWithURL:@"/"]; 67 | [self assert:@"application/json" equals:[request valueForHTTPHeaderField:@"Accept"]]; 68 | [self assert:@"application/json" equals:[request valueForHTTPHeaderField:@"Content-Type"]]; 69 | } 70 | 71 | - (void)testCPDateDateWithDateString 72 | { 73 | var date = [CPDate dateWithDateString:@"2009-12-31"]; 74 | [self assert:CPDate equals:[date class]]; 75 | [self assert:2009 equals:[date year]]; 76 | [self assert:12 equals:[date month]]; 77 | [self assert:31 equals:[date day]]; 78 | } 79 | 80 | - (void)testCPDateDateWithDateTimeString 81 | { 82 | var date = [CPDate dateWithDateTimeString:@"2009-11-30T21:50:00Z"]; 83 | [self assert:CPDate equals:[date class]]; 84 | [self assert:2009 equals:[date year]]; 85 | [self assert:11 equals:[date month]]; 86 | [self assert:30 equals:[date day]]; 87 | } 88 | 89 | - (void)testCPDateDateWithTimezoneDateTimeString 90 | { 91 | var date = [CPDate dateWithDateTimeString:@"2011-02-28T11:47:51+01:00"]; 92 | [self assert:CPDate equals:[date class]]; 93 | [self assert:2011 equals:[date year]]; 94 | [self assert:02 equals:[date month]]; 95 | [self assert:28 equals:[date day]]; 96 | } 97 | 98 | - (void)testCPDateToDateString 99 | { 100 | var expected = @"2001-01-01", 101 | date = [CPDate dateWithDateString:expected]; 102 | [self assert:expected equals:[date toDateString]]; 103 | } 104 | 105 | 106 | @end 107 | -------------------------------------------------------------------------------- /Framework/CappuccinoResource/CRSupport.j: -------------------------------------------------------------------------------- 1 | @import 2 | @import 3 | @import 4 | @import 5 | 6 | @implementation CPDate (CRSupport) 7 | 8 | + (CPDate)dateWithDateString:(CPString)aDate 9 | { 10 | return [[self alloc] initWithString:aDate + " 12:00:00 +0000"]; 11 | } 12 | 13 | + (CPDate)dateWithDateTimeString:(CPString)aDateTime 14 | { 15 | var format = /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})(\+\d{2}:\d{2}|Z)?$/, 16 | d = aDateTime.match(new RegExp(format)); 17 | 18 | if (d[3] === 'Z') 19 | d[3] = '+00:00'; 20 | 21 | var string = d[1] + " " + d[2] + " " + d[3].replace(':', ''); 22 | return [[self alloc] initWithString:string]; 23 | } 24 | 25 | - (int)year 26 | { 27 | return self.getFullYear(); 28 | } 29 | 30 | - (int)month 31 | { 32 | return self.getMonth() + 1; 33 | } 34 | 35 | - (int)day 36 | { 37 | return self.getDate(); 38 | } 39 | 40 | - (CPString)toDateString 41 | { 42 | return [CPString stringWithFormat:@"%04d-%02d-%02d", [self year], [self month], [self day]]; 43 | } 44 | 45 | 46 | @end 47 | 48 | @implementation CPString (CRSupport) 49 | 50 | + (CPString)paramaterStringFromJSON:(JSObject)params 51 | { 52 | paramsArray = [CPArray array]; 53 | 54 | for (var param in params) { 55 | [paramsArray addObject:(escape(param) + "=" + escape(params[param]))]; 56 | } 57 | 58 | return paramsArray.join("&"); 59 | } 60 | 61 | + (CPString)paramaterStringFromCPDictionary:(CPDictionary)params 62 | { 63 | var paramsArray = [CPArray array], 64 | keys = [params allKeys]; 65 | 66 | for (var i = 0; i < [params count]; ++i) { 67 | [paramsArray addObject:(escape(keys[i]) + "=" + escape([params valueForKey:keys[i]]))]; 68 | } 69 | 70 | return paramsArray.join("&"); 71 | } 72 | 73 | /* Rails expects strings to be lowercase and underscored. 74 | * eg - user_session, movie_title, created_at, etc. 75 | * Always use this format when sending data to Rails 76 | */ 77 | - (CPString)railsifiedString 78 | { 79 | var str=self; 80 | var str_path=str.split('::'); 81 | var upCase=new RegExp('([ABCDEFGHIJKLMNOPQRSTUVWXYZ])','g'); 82 | var fb=new RegExp('^_'); 83 | for(var i=0;i 14 | 15 | Optionally, install manually by copying /Framework/CappuccinoResource/*.j into your project and `@import` CRBase.j where needed 16 | 17 | @import "CRBase.j" 18 | 19 | 20 | ## Usage ## 21 | 22 | There is now an open-source demo application demonstrating basic usage. The demo is available [on Heroku](http://capp-resource-example.heroku.com), and the source is available [on GitHub](http://github.com/sant0sk1/CappResourceExample). For more detailed instructions, read on. 23 | 24 | ### In Rails ### 25 | 26 | Make sure your RESTful controllers render json. You can take or leave the `respond_to` block depending on your needs. 27 | 28 | Rails 2 Example: 29 | 30 | class PostsController < ApplicationController 31 | 32 | def index 33 | @posts = Post.all 34 | 35 | respond_to do |format| 36 | format.html 37 | format.json { render :json => @posts } 38 | end 39 | end 40 | 41 | # other actions ... 42 | end 43 | 44 | Rails 3 Example: 45 | 46 | class PostsController < ApplicationController 47 | respond_to :html, :json 48 | 49 | def index 50 | @posts = Post.all 51 | respond_with(@posts) 52 | end 53 | 54 | # other actions ... 55 | end 56 | 57 | ### In Capp ### 58 | 59 | Create a class which inherits from CR: 60 | 61 | @implementation Post : CappuccinoResource 62 | { 63 | CPString title @accessors; 64 | CPString body @accessors; 65 | CPDate publishedOn; 66 | BOOL isViewable; 67 | } 68 | 69 | - (JSObject)attributes 70 | { 71 | return { 72 | "post": { 73 | "title":title, 74 | "body":body, 75 | "published_on":[publishedOn toDateString], 76 | "is_viewable":isViewable 77 | } 78 | }; 79 | } 80 | 81 | The `attributes` instance method MUST be declared in your class for it to save properly. 82 | 83 | CR performs naïve class pluralization (it just adds an "s"). If your class name has a more complex inflection, you can simply override the `resourcePath` class method. For instance, a `Person` class: 84 | 85 | @implementation Person : CappuccinoResource 86 | { 87 | CPString name; 88 | } 89 | 90 | + (CPURL)resourcePath 91 | { 92 | return [CPURL URLWithString:@"/people"]; 93 | } 94 | 95 | - (JSObject)attributes 96 | { 97 | return {"person":{"name":name}}; 98 | } 99 | 100 | @end 101 | 102 | Using your new class should feel familiar to Rails devs. 103 | 104 | ### CRUD ### 105 | 106 | Instantiate a blank Post object 107 | 108 | var post = [Post new]; 109 | 110 | Optionally declare attributes at the same time. JSON feels like Ruby hashes! 111 | 112 | var post = [Post new:{"title":"First Post!","body":"Lorem and stuff"}]; 113 | 114 | Just like in ActiveResource, create = new + save 115 | 116 | var post = [Post create:{"title":"First Post!","body":"Lorem and stuff"}]; 117 | 118 | Get all the posts from Rails 119 | 120 | var posts = [Post all]; 121 | [posts class]; // CPArray 122 | [[posts objectAtIndex:0] class]; // Post 123 | 124 | You can fetch a resource with its identifier... 125 | 126 | var post = [Post find:@"4"]; 127 | 128 | Change its title... 129 | 130 | [post setTitle:@"Shiny New Name"]; 131 | 132 | And save it in your Rails back-end. 133 | 134 | [post save]; 135 | 136 | Deleting is just as easy 137 | 138 | [post destroy]; 139 | 140 | ### More Advanced Finds ### 141 | 142 | You can also run find with JSON paramaters (or a CPDictionary) 143 | 144 | var myPost = [Post findWithParams:{"title":"Oh Noes!"}]; 145 | [myPost class]; // Post 146 | 147 | Or the same thing with a collection 148 | 149 | var posts = [Post allWithParams:{"body":"happy"}]; 150 | [posts class]; // CPArray 151 | [[posts objectAtIndex:0] class]; // Post 152 | 153 | The parameters will get serialized and be available to your Rails controller's `params` hash. It's up to Rails to return the appropriate records. 154 | 155 | ### Custom Identifiers ### 156 | 157 | You don't need to use the default Rails `id` in your URLS. For example, if you'd rather use the `login` attribute as a unique identifier, overwrite your class's `identifierKey` class method like this: 158 | 159 | + (CPString)identifierKey 160 | { 161 | return @"login"; 162 | } 163 | 164 | CR will take care of the rest. 165 | 166 | ## Notifications ## 167 | 168 | There are multiple events you can observe in the life cycle of a CR object. The notification names are comprised of the object's class name followed by the event name. So, for a `Movie` class which inherits from CR, the list of observable events are: 169 | 170 | * MovieResourceWillLoad 171 | * MovieResourceDidLoad 172 | * MovieCollectionWillLoad 173 | * MovieCollectionDidLoad 174 | * MovieResourceWillSave 175 | * MovieResourceWillCreate 176 | * MovieResourceWillUpdate 177 | * MovieResourceDidSave 178 | * MovieResourceDidCreate 179 | * MovieResourceDidUpdate 180 | * MovieResourceDidNotSave 181 | * MovieResourceDidNotCreate 182 | * MovieResourceDidNotUpdate 183 | * MovieResourceWillDestroy 184 | * MovieResourceDidDestroy 185 | 186 | One thing worth pointing out; whenever you try to save a resource, it will post 2 notifications per event. The first is the Will/Did/DidNot Save notification. The second is either Will/Did/DidNot Create or Will/Did/DidNot Update depending on what type of a save it is. 187 | 188 | # Contributing # 189 | 190 | Please do! Like so: 191 | 192 | 1. Fork CR 193 | 2. Pass all tests (see below) 194 | 3. Create a topic branch - `git checkout -b my_branch` 195 | 4. Push to your branch - `git push origin my_branch` 196 | 5. Pass all tests 197 | 6. Create an [Issue](http://github.com/sant0sk1/CappuccinoResource/issues) with a link to your branch 198 | 199 | ## Testing ## 200 | 201 | Please include passing tests with any proposed additions/modifications. To run the test suite: 202 | 203 | 1. Install ojmoq: `sudo tusk install ojmoq` 204 | 2. Run tests with: `jake test` OR `ojtest Tests/*Test.j` OR `autotest` 205 | 206 | # Credit # 207 | 208 | Much of this library was inspired by other open-source projects, the most noteworthy of which are: 209 | 210 | 1. [CPActiveRecord](http://github.com/nciagra/Cappuccino-Extensions/tree/master/CPActiveRecord/) 211 | 2. [ObjectiveResource](http://github.com/yfactorial/objectiveresource) 212 | 213 | I'd like to thank their authors for opening their source code to others. 214 | 215 | # Todo List # 216 | 217 | * Infer -attributes from ivars (maybe with @property?) 218 | * Better error handling 219 | * Validations 220 | * Callbacks 221 | * Nested Models 222 | 223 | # Meta # 224 | 225 | ## Author ## 226 | 227 | [Jerod Santo](http://jerodsanto.net) 228 | 229 | ## Contributors ## 230 | 231 | Just me so far! 232 | 233 | ## License ## 234 | 235 | [MIT](http://www.opensource.org/licenses/mit-license.php) Stylee 236 | -------------------------------------------------------------------------------- /Framework/CappuccinoResource/CRBase.j: -------------------------------------------------------------------------------- 1 | @import 2 | @import "CRSupport.j" 3 | 4 | var defaultIdentifierKey = @"id", 5 | classAttributeNames = [CPDictionary dictionary]; 6 | 7 | @implementation CappuccinoResource : CPObject 8 | { 9 | CPString identifier @accessors; 10 | } 11 | 12 | // override this method to use a custom identifier for lookups 13 | + (CPString)identifierKey 14 | { 15 | return defaultIdentifierKey; 16 | } 17 | 18 | // this provides very, very basic pluralization (adding an 's'). 19 | // override this method for more complex inflections 20 | + (CPURL)resourcePath 21 | { 22 | return [CPURL URLWithString:@"/" + [self railsName] + @"s"]; 23 | } 24 | 25 | + (CPString)railsName 26 | { 27 | return [[self className] railsifiedString]; 28 | } 29 | 30 | - (JSObject)attributes 31 | { 32 | CPLog.warn('This method must be declared in your class to save properly.'); 33 | return {}; 34 | } 35 | 36 | // switch to this if we can get attribute types 37 | // + (CPDictionary)attributes 38 | // { 39 | // var array = class_copyIvarList(self), 40 | // dict = [[CPDictionary alloc] init]; 41 | // 42 | // for (var i = 0; i < array.length; i++) 43 | // [dict setObject:array[i].type forKey:array[i].name]; 44 | // return dict; 45 | // } 46 | 47 | - (CPArray)attributeNames 48 | { 49 | if ([classAttributeNames objectForKey:[self className]]) { 50 | return [classAttributeNames objectForKey:[self className]]; 51 | } 52 | 53 | var attributeNames = [CPArray array], 54 | attributes = class_copyIvarList([self class]); 55 | 56 | for (var i = 0; i < attributes.length; i++) { 57 | [attributeNames addObject:attributes[i].name]; 58 | } 59 | 60 | [classAttributeNames setObject:attributeNames forKey:[self className]]; 61 | 62 | return attributeNames; 63 | } 64 | 65 | - (void)setAttributes:(JSObject)attributes 66 | { 67 | for (var attribute in attributes) { 68 | if (attribute == [[self class] identifierKey]) { 69 | [self setIdentifier:attributes[attribute].toString()]; 70 | } else { 71 | var attributeName = [attribute cappifiedString]; 72 | if ([[self attributeNames] containsObject:attributeName]) { 73 | var value = attributes[attribute]; 74 | /* 75 | * I would much rather retrieve the ivar class than pattern match the 76 | * response from Rails, but objective-j does not support this. 77 | */ 78 | switch (typeof value) { 79 | case "boolean": 80 | if (value) { 81 | [self setValue:YES forKey:attributeName]; 82 | } else { 83 | [self setValue:NO forKey:attributeName]; 84 | } 85 | break; 86 | case "number": 87 | [self setValue:value forKey:attributeName]; 88 | break; 89 | case "string": 90 | if (value.match(/^\d{4}-\d{2}-\d{2}$/)) { 91 | // its a date 92 | [self setValue:[CPDate dateWithDateString:value] forKey:attributeName]; 93 | } else if (value.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\+\d{2}:\d{2}|Z)$/)) { 94 | // its a datetime 95 | [self setValue:[CPDate dateWithDateTimeString:value] forKey:attributeName]; 96 | } else { 97 | // its a string 98 | [self setValue:value forKey:attributeName]; 99 | } 100 | break; 101 | } 102 | } 103 | } 104 | } 105 | } 106 | 107 | + (id)new 108 | { 109 | return [self new:nil]; 110 | } 111 | 112 | + (id)new:(JSObject)attributes 113 | { 114 | var resource = [[self alloc] init]; 115 | 116 | if (!attributes) 117 | attributes = {}; 118 | 119 | [resource setAttributes:attributes]; 120 | return resource; 121 | } 122 | 123 | + (id)create:(JSObject)attributes 124 | { 125 | var resource = [self new:attributes]; 126 | if ([resource save]) { 127 | return resource; 128 | } else { 129 | return nil; 130 | } 131 | } 132 | 133 | - (BOOL)save 134 | { 135 | var request = [self resourceWillSave]; 136 | 137 | if (!request) { 138 | return NO; 139 | } 140 | 141 | var response = [CPURLConnection sendSynchronousRequest:request]; 142 | 143 | if (response[0] >= 400) { 144 | [self resourceDidNotSave:response[1]]; 145 | return NO; 146 | } else { 147 | [self resourceDidSave:response[1]]; 148 | return YES; 149 | } 150 | } 151 | 152 | - (BOOL)destroy 153 | { 154 | var request = [self resourceWillDestroy]; 155 | 156 | if (!request) { 157 | return NO; 158 | } 159 | 160 | var response = [CPURLConnection sendSynchronousRequest:request]; 161 | 162 | if (response[0] == 200) { 163 | [self resourceDidDestroy]; 164 | return YES; 165 | } else { 166 | return NO; 167 | } 168 | } 169 | 170 | + (CPArray)all 171 | { 172 | var request = [self collectionWillLoad]; 173 | 174 | if (!request) { 175 | return NO; 176 | } 177 | 178 | var response = [CPURLConnection sendSynchronousRequest:request]; 179 | 180 | if (response[0] >= 400) { 181 | return nil; 182 | } else { 183 | return [self collectionDidLoad:response[1]]; 184 | } 185 | } 186 | 187 | + (CPArray)allWithParams:(JSObject)params 188 | { 189 | var request = [self collectionWillLoad:params]; 190 | 191 | var response = [CPURLConnection sendSynchronousRequest:request]; 192 | 193 | if (response[0] >= 400) { 194 | return nil; 195 | } else { 196 | return [self collectionDidLoad:response[1]]; 197 | } 198 | } 199 | 200 | + (id)find:(CPString)identifier 201 | { 202 | var request = [self resourceWillLoad:identifier]; 203 | 204 | if (!request) { 205 | return NO; 206 | } 207 | 208 | var response = [CPURLConnection sendSynchronousRequest:request]; 209 | 210 | if (response[0] >= 400) { 211 | return nil; 212 | } else { 213 | return [self resourceDidLoad:response[1]]; 214 | } 215 | } 216 | 217 | + (id)findWithParams:(JSObject)params 218 | { 219 | var collection = [self allWithParams:params]; 220 | 221 | if ([collection count] > 0) { 222 | return [collection objectAtIndex:0]; 223 | } else { 224 | return nil; 225 | } 226 | } 227 | 228 | // All the following methods post notifications using their class name 229 | // You can observe these notifications and take further action if desired 230 | + (CPURLRequest)resourceWillLoad:(CPString)identifier 231 | { 232 | var path = [self resourcePath] + "/" + identifier, 233 | notificationName = [self className] + "ResourceWillLoad"; 234 | 235 | if (!path) { 236 | return nil; 237 | } 238 | 239 | var request = [CPURLRequest requestJSONWithURL:path]; 240 | [request setHTTPMethod:@"GET"]; 241 | 242 | [[CPNotificationCenter defaultCenter] postNotificationName:notificationName object:self]; 243 | return request; 244 | } 245 | 246 | + (id)resourceDidLoad:(CPString)aResponse 247 | { 248 | var response = [aResponse toJSON], 249 | attributes = response[[self railsName]], 250 | notificationName = [self className] + "ResourceDidLoad", 251 | resource = [self new]; 252 | 253 | [resource setAttributes:attributes]; 254 | [[CPNotificationCenter defaultCenter] postNotificationName:notificationName object:resource]; 255 | return resource; 256 | } 257 | 258 | + (CPURLRequest)collectionWillLoad 259 | { 260 | return [self collectionWillLoad:nil]; 261 | } 262 | 263 | // can handle a JSObject or a CPDictionary 264 | + (CPURLRequest)collectionWillLoad:(id)params 265 | { 266 | var path = [self resourcePath], 267 | notificationName = [self className] + "CollectionWillLoad"; 268 | 269 | if (params) { 270 | if (params.isa && [params isKindOfClass:CPDictionary]) { 271 | path += ("?" + [CPString paramaterStringFromCPDictionary:params]); 272 | } else { 273 | path += ("?" + [CPString paramaterStringFromJSON:params]); 274 | } 275 | } 276 | 277 | if (!path) { 278 | return nil; 279 | } 280 | 281 | var request = [CPURLRequest requestJSONWithURL:path]; 282 | [request setHTTPMethod:@"GET"]; 283 | 284 | [[CPNotificationCenter defaultCenter] postNotificationName:notificationName object:self]; 285 | 286 | return request; 287 | } 288 | 289 | + (CPArray)collectionDidLoad:(CPString)aResponse 290 | { 291 | var resourceArray = [CPArray array], 292 | notificationName = [self className] + "CollectionDidLoad"; 293 | 294 | if ([[aResponse stringByTrimmingWhitespace] length] > 0) { 295 | var collection = [aResponse toJSON]; 296 | 297 | for (var i = 0; i < collection.length; i++) { 298 | var resource = collection[i]; 299 | var attributes = resource[[self railsName]]; 300 | [resourceArray addObject:[self new:attributes]]; 301 | } 302 | } 303 | 304 | [[CPNotificationCenter defaultCenter] postNotificationName:notificationName object:resourceArray]; 305 | return resourceArray; 306 | } 307 | 308 | - (CPURLRequest)resourceWillSave 309 | { 310 | var abstractNotificationName = [self className] + "ResourceWillSave"; 311 | 312 | if (identifier) { 313 | var path = [[self class] resourcePath] + "/" + identifier, 314 | notificationName = [self className] + "ResourceWillUpdate"; 315 | } else { 316 | var path = [[self class] resourcePath], 317 | notificationName = [self className] + "ResourceWillCreate"; 318 | } 319 | 320 | if (!path) { 321 | return nil; 322 | } 323 | 324 | var request = [CPURLRequest requestJSONWithURL:path]; 325 | 326 | [request setHTTPMethod:identifier ? @"PUT" : @"POST"]; 327 | [request setHTTPBody:[CPString JSONFromObject:[self attributes]]]; 328 | 329 | [[CPNotificationCenter defaultCenter] postNotificationName:notificationName object:self]; 330 | [[CPNotificationCenter defaultCenter] postNotificationName:abstractNotificationName object:self]; 331 | return request; 332 | } 333 | 334 | - (void)resourceDidSave:(CPString)aResponse 335 | { 336 | if ([aResponse length] > 1) 337 | { 338 | var response = [aResponse toJSON], 339 | attributes = response[[[self class] railsName]]; 340 | } 341 | var abstractNotificationName = [self className] + "ResourceDidSave"; 342 | 343 | if (identifier) { 344 | var notificationName = [self className] + "ResourceDidUpdate"; 345 | } else { 346 | var notificationName = [self className] + "ResourceDidCreate"; 347 | } 348 | 349 | [self setAttributes:attributes]; 350 | [[CPNotificationCenter defaultCenter] postNotificationName:notificationName object:self]; 351 | [[CPNotificationCenter defaultCenter] postNotificationName:abstractNotificationName object:self]; 352 | } 353 | 354 | - (void)resourceDidNotSave:(CPString)aResponse 355 | { 356 | var abstractNotificationName = [self className] + "ResourceDidNotSave"; 357 | 358 | // TODO - do something with errors 359 | if (identifier) { 360 | var notificationName = [self className] + "ResourceDidNotUpdate"; 361 | } else { 362 | var notificationName = [self className] + "ResourceDidNotCreate"; 363 | } 364 | 365 | [[CPNotificationCenter defaultCenter] postNotificationName:notificationName object:self]; 366 | [[CPNotificationCenter defaultCenter] postNotificationName:abstractNotificationName object:self]; 367 | } 368 | 369 | - (CPURLRequest)resourceWillDestroy 370 | { 371 | var path = [[self class] resourcePath] + "/" + identifier, 372 | notificationName = [self className] + "ResourceWillDestroy"; 373 | 374 | if (!path) { 375 | return nil; 376 | } 377 | 378 | var request = [CPURLRequest requestJSONWithURL:path]; 379 | [request setHTTPMethod:@"DELETE"]; 380 | 381 | [[CPNotificationCenter defaultCenter] postNotificationName:notificationName object:self]; 382 | return request; 383 | } 384 | 385 | -(void)resourceDidDestroy 386 | { 387 | var notificationName = [self className] + "ResourceDidDestroy"; 388 | [[CPNotificationCenter defaultCenter] postNotificationName:notificationName object:self]; 389 | } 390 | 391 | @end -------------------------------------------------------------------------------- /common.jake: -------------------------------------------------------------------------------- 1 | var SYSTEM = require("system"); 2 | var FILE = require("file"); 3 | var OS = require("os"); 4 | var UTIL = require("narwhal/util"); 5 | var stream = require("narwhal/term").stream; 6 | 7 | var requiresSudo = false; 8 | 9 | SYSTEM.args.slice(1).forEach(function(arg){ 10 | if (arg === "sudo-install") 11 | requiresSudo = true; 12 | }); 13 | 14 | function ensurePackageUpToDate(packageName, requiredVersion, options) 15 | { 16 | options = options || {}; 17 | 18 | var packageInfo = require("narwhal/packages").catalog[packageName]; 19 | if (!packageInfo) 20 | { 21 | if (options.optional) 22 | return; 23 | 24 | print("You are missing package \"" + packageName + "\", version " + requiredVersion + " or later. Please install using \"tusk install "+packageName+"\" and re-run jake"); 25 | OS.exit(1); 26 | } 27 | 28 | var version = packageInfo.version; 29 | if (typeof version === "string") 30 | version = version.split("."); 31 | 32 | if (typeof requiredVersion === "string") 33 | requiredVersion = requiredVersion.split("."); 34 | 35 | if (version && UTIL.compare(version, requiredVersion) !== -1) 36 | return; 37 | 38 | print("Your copy of " + packageName + " is out of date (" + (version||["0"]).join(".") + " installed, " + requiredVersion.join(".") + " required)."); 39 | 40 | if (!options.noupdate) 41 | { 42 | print("Update? Existing package will be overwritten. yes or no:"); 43 | if (!SYSTEM.env["CAPP_AUTO_UPGRADE"] && system.stdin.readLine() !== "yes\n") 44 | { 45 | print("Jake aborted."); 46 | OS.exit(1); 47 | } 48 | 49 | if (requiresSudo) 50 | { 51 | if (OS.system(["sudo", "tusk", "install", "--force", packageName])) 52 | { 53 | // Attempt a hackish work-around for sudo compiled with the --with-secure-path option 54 | if (OS.system("sudo bash -c 'source " + getShellConfigFile() + "; tusk install --force "+packageName)) 55 | OS.exit(1); //rake abort if ($? != 0) 56 | } 57 | } 58 | else 59 | OS.system(["tusk", "install", "--force", packageName]); 60 | } 61 | 62 | if (options.after) 63 | { 64 | options.after(packageInfo.directory); 65 | } 66 | 67 | if (options.message) 68 | { 69 | print(options.message); 70 | OS.exit(1); 71 | } 72 | } 73 | 74 | // UPDATE THESE TO PICK UP CORRESPONDING CHANGES IN DEPENDENCIES 75 | ensurePackageUpToDate("jake", "0.3"); 76 | ensurePackageUpToDate("browserjs", "0.1.1"); 77 | ensurePackageUpToDate("shrinksafe", "0.2"); 78 | ensurePackageUpToDate("narwhal", "0.3.1", { 79 | noupdate : true, 80 | message : "Update Narwhal by re-running bootstrap.sh, or pulling the latest from git (see: http://github.com/280north/narwhal)." 81 | }); 82 | ensurePackageUpToDate("narwhal-jsc", "0.3", { 83 | optional : true, 84 | after : function(dir) { 85 | if (OS.system("cd " + OS.enquote(dir) + " && make webkit")) { 86 | print("Problem building narwhal-jsc."); 87 | OS.exit(1); 88 | } 89 | } 90 | }); 91 | 92 | var JAKE = require("jake"); 93 | 94 | // Set up development environment variables. 95 | 96 | // record the initial SYSTEM.env so we know which need to be serialized later 97 | var envInitial = Object.freeze(UTIL.copy(SYSTEM.env)); 98 | 99 | SYSTEM.env["BUILD_PATH"] = FILE.absolute( 100 | SYSTEM.env["BUILD_PATH"] || 101 | SYSTEM.env["CAPP_BUILD"] || // Global Cappuccino build directory. 102 | SYSTEM.env["STEAM_BUILD"] || // Maintain backwards compatibility with steam. 103 | FILE.join(FILE.dirname(module.path), "Build") // Just build here. 104 | ); 105 | 106 | if (!SYSTEM.env["CAPP_BUILD"] && SYSTEM.env["STEAM_BUILD"]) 107 | system.stderr.print("STEAM_BUILD environment variable is deprecated; Please use CAPP_BUILD instead."); 108 | 109 | if (!SYSTEM.env["CONFIG"]) 110 | SYSTEM.env["CONFIG"] = "Release"; 111 | 112 | global.ENV = SYSTEM.env; 113 | global.ARGV = SYSTEM.args 114 | global.FILE = FILE; 115 | global.OS = OS; 116 | 117 | global.task = JAKE.task; 118 | global.directory = JAKE.directory; 119 | global.file = JAKE.file; 120 | global.filedir = JAKE.filedir; 121 | global.FileList = JAKE.FileList; 122 | 123 | global.$CONFIGURATION = SYSTEM.env['CONFIG']; 124 | global.$BUILD_DIR = SYSTEM.env['BUILD_PATH']; 125 | global.$BUILD_CONFIGURATION_DIR = FILE.join($BUILD_DIR, $CONFIGURATION); 126 | 127 | global.$BUILD_CJS_OBJECTIVE_J = FILE.join($BUILD_CONFIGURATION_DIR, "CommonJS", "objective-j"); 128 | 129 | global.$BUILD_CJS_CAPPUCCINO = FILE.join($BUILD_CONFIGURATION_DIR, "CommonJS", "cappuccino"); 130 | global.$BUILD_CJS_CAPPUCCINO_BIN = FILE.join($BUILD_CJS_CAPPUCCINO, "bin"); 131 | global.$BUILD_CJS_CAPPUCCINO_LIB = FILE.join($BUILD_CJS_CAPPUCCINO, "lib"); 132 | global.$BUILD_CJS_CAPPUCCINO_FRAMEWORKS = FILE.join($BUILD_CJS_CAPPUCCINO, "Frameworks"); 133 | 134 | global.CLEAN = require("jake/clean").CLEAN; 135 | global.CLOBBER = require("jake/clean").CLOBBER; 136 | global.CLEAN.include(global.$BUILD_DIR); 137 | global.CLOBBER.include(global.$BUILD_DIR); 138 | 139 | global.$HOME_DIR = FILE.absolute(FILE.dirname(module.path)); 140 | global.$LICENSE_FILE = FILE.absolute(FILE.join(FILE.dirname(module.path), 'LICENSE')); 141 | 142 | global.FIXME_fileDependency = function(destinationPath, sourcePath) 143 | { 144 | file(destinationPath, [sourcePath], function(){ 145 | FILE.touch(destinationPath); 146 | }); 147 | }; 148 | 149 | // logic to determine which packages should be loaded but are not. 150 | // used in serializedENV() 151 | function additionalPackages() 152 | { 153 | var builtObjectiveJPackage = FILE.path($BUILD_CONFIGURATION_DIR).join("CommonJS", "objective-j", ""); 154 | var builtCappuccinoPackage = FILE.path($BUILD_CONFIGURATION_DIR).join("CommonJS", "cappuccino", ""); 155 | 156 | var packages = []; 157 | 158 | // load built objective-j if exists, otherwise unbuilt 159 | if (builtObjectiveJPackage.join("package.json").exists()) { 160 | if (!packageInCatalog(builtObjectiveJPackage)) 161 | packages.push(builtObjectiveJPackage); 162 | } 163 | 164 | // load built cappuccino if it exists 165 | if (builtCappuccinoPackage.join("package.json").exists()) { 166 | if (!packageInCatalog(builtCappuccinoPackage)) 167 | packages.push(builtCappuccinoPackage); 168 | } 169 | 170 | return packages; 171 | } 172 | 173 | // checks to see if a path is in the package catalog 174 | function packageInCatalog(path) 175 | { 176 | var catalog = require("narwhal/packages").catalog; 177 | for (var name in catalog) 178 | if (String(catalog[name].directory) === String(path)) 179 | return true; 180 | return false; 181 | } 182 | 183 | serializedENV = function() 184 | { 185 | var envNew = {}; 186 | 187 | // add changed keys to the new ENV 188 | Object.keys(SYSTEM.env).forEach(function(key) { 189 | if (SYSTEM.env[key] !== envInitial[key]) 190 | envNew[key] = SYSTEM.env[key]; 191 | }); 192 | 193 | // pseudo-HACK: add NARWHALOPT with packages we should ensure are loaded 194 | var packages = additionalPackages(); 195 | if (packages.length) { 196 | envNew["NARWHALOPT"] = packages.map(function(p) { return "-p " + OS.enquote(p); }).join(" "); 197 | envNew["PATH"] = packages.map(function(p) { return FILE.join(p, "bin"); }).concat(SYSTEM.env["PATH"]).join(":"); 198 | } 199 | 200 | return Object.keys(envNew).map(function(key) { 201 | return key + "=" + OS.enquote(envNew[key]); 202 | }).join(" "); 203 | } 204 | 205 | function reforkWithPackages() 206 | { 207 | if (additionalPackages().length > 0) { 208 | var cmd = serializedENV() + " " + system.args.map(OS.enquote).join(" "); 209 | //print("REFORKING: " + cmd); 210 | OS.exit(OS.system(cmd)); 211 | } 212 | } 213 | 214 | reforkWithPackages(); 215 | 216 | function handleSetupEnvironmentError(e) { 217 | if (String(e).indexOf("require error")==-1) { 218 | print("setupEnvironment warning: " + e); 219 | //throw e; 220 | } 221 | } 222 | 223 | function setupEnvironment() 224 | { 225 | try { 226 | require("objective-j").OBJJ_INCLUDE_PATHS.push(FILE.join($BUILD_CONFIGURATION_DIR, "CommonJS", "cappuccino", "Frameworks")); 227 | } catch (e) { 228 | handleSetupEnvironmentError(e); 229 | } 230 | } 231 | 232 | setupEnvironment(); 233 | 234 | global.rm_rf = function(/*String*/ aFilename) 235 | { 236 | try { FILE.rmtree(aFilename); } 237 | catch (anException) { } 238 | } 239 | 240 | global.cp_r = function(/*String*/ from, /*String*/ to) 241 | { 242 | if (FILE.exists(to)) 243 | rm_rf(to); 244 | 245 | if (FILE.isDirectory(from)) 246 | FILE.copyTree(from, to); 247 | else{try{ 248 | FILE.copy(from, to);}catch(e) { print(e + FILE.exists(from) + " " + FILE.exists(FILE.dirname(to))); }} 249 | } 250 | 251 | global.cp = function(/*String*/ from, /*String*/ to) 252 | { 253 | FILE.copy(from, to); 254 | // FILE.chmod(to, FILE.mod(from)); 255 | } 256 | 257 | global.mv = function(/*String*/ from, /*String*/ to) 258 | { 259 | FILE.move(from, to); 260 | } 261 | 262 | global.subjake = function(/*Array*/ directories, /*String*/ aTaskName) 263 | { 264 | if (!Array.isArray(directories)) 265 | directories = [directories]; 266 | 267 | directories.forEach(function(/*String*/ aDirectory) 268 | { 269 | if (FILE.isDirectory(aDirectory) && FILE.isFile(FILE.join(aDirectory, "Jakefile"))) 270 | { 271 | var cmd = "cd " + OS.enquote(aDirectory) + " && " + serializedENV() + " " + OS.enquote(SYSTEM.args[0]) + " " + OS.enquote(aTaskName); 272 | var returnCode = OS.system(cmd); 273 | if (returnCode) 274 | OS.exit(returnCode); 275 | } 276 | else 277 | print("warning: subjake missing: " + aDirectory + " (this is not necessarily an error, " + aDirectory + " may be optional)"); 278 | }); 279 | } 280 | 281 | global.executableExists = function(/*String*/ executableName) 282 | { 283 | var paths = SYSTEM.env["PATH"].split(':'); 284 | for (var i = 0; i < paths.length; i++) { 285 | var path = FILE.join(paths[i], executableName); 286 | if (FILE.exists(path)) 287 | return path; 288 | } 289 | return null; 290 | } 291 | 292 | $OBJJ_TEMPLATE_EXECUTABLE = FILE.join($HOME_DIR, "Objective-J", "CommonJS", "objj-executable"); 293 | 294 | global.make_objj_executable = function(aPath) 295 | { 296 | cp($OBJJ_TEMPLATE_EXECUTABLE, aPath); 297 | FILE.chmod(aPath, 0755); 298 | } 299 | 300 | global.symlink_executable = function(source) 301 | { 302 | relative = FILE.relative($ENVIRONMENT_NARWHAL_BIN_DIR, source); 303 | destination = FILE.join($ENVIRONMENT_NARWHAL_BIN_DIR, FILE.basename(source)); 304 | FILE.symlink(relative, destination); 305 | } 306 | 307 | global.getCappuccinoVersion = function() { 308 | var versionFile = FILE.path(module.path).dirname().join("version.json"); 309 | return JSON.parse(versionFile.read({ charset : "UTF-8" })).version; 310 | } 311 | 312 | global.setPackageMetadata = function(packagePath) { 313 | var pkg = JSON.parse(FILE.read(packagePath, { charset : "UTF-8" })); 314 | 315 | var p = OS.popen(["git", "rev-parse", "--verify", "HEAD"]); 316 | if (p.wait() === 0) { 317 | var sha = p.stdout.read().split("\n")[0]; 318 | if (sha.length === 40) 319 | pkg["cappuccino-revision"] = sha; 320 | } 321 | 322 | pkg["cappuccino-timestamp"] = new Date().getTime(); 323 | pkg["version"] = getCappuccinoVersion(); 324 | 325 | stream.print(" Version: \0purple(" + pkg["version"] + "\0)"); 326 | stream.print(" Revision: \0purple(" + pkg["cappuccino-revision"] + "\0)"); 327 | stream.print(" Timestamp: \0purple(" + pkg["cappuccino-timestamp"] + "\0)"); 328 | 329 | FILE.write(packagePath, JSON.stringify(pkg, null, 4), { charset : "UTF-8" }); 330 | } 331 | 332 | global.subtasks = function(subprojects, taskNames) 333 | { 334 | taskNames.forEach(function(aTaskName) 335 | { 336 | var subtaskName = aTaskName + "_subprojects"; 337 | 338 | task (aTaskName, [subtaskName]); 339 | 340 | task (subtaskName, function() 341 | { 342 | subjake(subprojects, aTaskName); 343 | }); 344 | }); 345 | } 346 | 347 | function spawnJake(/*String*/ aTaskName) 348 | { 349 | if (OS.system(serializedENV() + " " + SYSTEM.args[0] + " " + aTaskName)) 350 | OS.exit(1);//rake abort if ($? != 0) 351 | } 352 | 353 | // built in tasks 354 | 355 | task ("build"); 356 | task ("default", "build"); 357 | 358 | task ("release", function() 359 | { 360 | SYSTEM.env["CONFIG"] = "Release"; 361 | spawnJake("build"); 362 | }); 363 | 364 | task ("debug", function() 365 | { 366 | SYSTEM.env["CONFIG"] = "Debug"; 367 | spawnJake("build"); 368 | }); 369 | 370 | task ("all", ["debug", "release"]); 371 | 372 | task ("clean-debug", function() 373 | { 374 | SYSTEM.env['CONFIG'] = 'Debug' 375 | spawnJake("clean"); 376 | }); 377 | 378 | task ("cleandebug", ["clean-debug"]); 379 | 380 | task ("clean-release", function() 381 | { 382 | SYSTEM.env["CONFIG"] = "Release"; 383 | spawnJake("clean"); 384 | }); 385 | 386 | task ("cleanrelease", ["clean-release"]); 387 | 388 | task ("clean-all", ["clean-debug", "clean-release"]); 389 | task ("cleanall", ["clean-all"]); 390 | 391 | task ("clobber-debug", function() 392 | { 393 | SYSTEM.env["CONFIG"] = "Debug"; 394 | spawnJake("clobber"); 395 | }); 396 | 397 | task ("clobberdebug", ["clobber-debug"]); 398 | 399 | task ("clobber-release", function() 400 | { 401 | SYSTEM.env["CONFIG"] = "Release"; 402 | spawnJake("clobber"); 403 | }); 404 | 405 | task ("clobberrelease", ['clobber-release']); 406 | 407 | task ("clobber-all", ["clobber-debug", "clobber-release"]); 408 | task ("clobberall", ["clobber-all"]); 409 | -------------------------------------------------------------------------------- /Tests/CRBaseTest.j: -------------------------------------------------------------------------------- 1 | @import "TestHelper.j" 2 | 3 | var userResourceJSON = '{"user":{"id":1,"email":"test@test.com","password":"secret"}}', 4 | userCollectionJSON = '[{"user":{"id":1,"email":"one@test.com"}},' + 5 | '{"user":{"id":2,"email":"two@test.com"}},' + 6 | '{"user":{"id":3,"email":"three@test.com"}}]'; 7 | 8 | 9 | @implementation CRBaseTest : OJTestCase 10 | 11 | - (void)setUp 12 | { 13 | user = [[User alloc] init]; 14 | session = [[UserSession alloc] init]; 15 | // mock network connections 16 | oldCPURLConnection = CPURLConnection; 17 | CPURLConnection = moq(); 18 | // setup an obvserver 19 | observer = [[Observer alloc] init]; 20 | } 21 | 22 | - (void)tearDown 23 | { 24 | // destroy mock 25 | [CPURLConnection verifyThatAllExpectationsHaveBeenMet]; 26 | CPURLConnection = oldCPURLConnection; 27 | } 28 | 29 | - (void)testIdentifierKey 30 | { 31 | [self assert:@"id" equals:[User identifierKey]]; 32 | [self assert:@"token" equals:[UserSession identifierKey]]; 33 | } 34 | 35 | - (void)testResourcePath 36 | { 37 | [self assert:[CPURL URLWithString:@"/users"] equals:[User resourcePath]]; 38 | [self assert:[CPURL URLWithString:@"/user_sessions"] equals:[UserSession resourcePath]]; 39 | } 40 | 41 | - (void)testAttributes 42 | { 43 | var expected = '{"email":"test@test.com","password":"secret","age":27}'; 44 | [user setEmail:@"test@test.com"]; 45 | [user setPassword:@"secret"]; 46 | [user setAge:27]; 47 | [self assert:expected equals:JSON.stringify([user attributes])]; 48 | } 49 | 50 | - (void)testAttributeNames 51 | { 52 | [self assert:["email","password","age","isAlive"] equals:[user attributeNames]]; 53 | [self assert:["userName","startDate"] equals:[session attributeNames]]; 54 | } 55 | 56 | - (void)testSetAttributes 57 | { 58 | var atts1 = {"email":"test@test.com", "password":"secret", "id":12, "age":24, "is_alive":true}, 59 | atts2 = {"token":"8675309", "user_name":"dorky", "ignore":"this","start_date":"2009-12-19"}, 60 | atts3 = {"token":"8675309", "user_name":"dorky", "start_date":"2007-04-01T12:34:31Z"} 61 | atts4 = {"token":"1337", "user_name":"dutchman", "start_date":"2011-08-13T11:42:51+01:00"} 62 | 63 | [user setAttributes:atts1]; 64 | [self assert:@"test@test.com" equals:[user email]]; 65 | [self assert:@"secret" equals:[user password]]; 66 | [self assert:@"12" equals:[user identifier]]; 67 | [self assert:24 equals:[user age]]; 68 | [self assert:true equals:[user isAlive]]; 69 | 70 | [session setAttributes:atts2]; 71 | [self assert:@"dorky" equals:[session userName]]; 72 | [self assert:@"8675309" equals:[session identifier]]; 73 | [self assert:2009 equals:[[session startDate] year]]; 74 | [self assert:12 equals:[[session startDate] month]]; 75 | [self assert:19 equals:[[session startDate] day]]; 76 | 77 | [session setAttributes:atts3]; 78 | [self assert:2007 equals:[[session startDate] year]]; 79 | [self assert:4 equals:[[session startDate] month]]; 80 | [self assert:1 equals:[[session startDate] day]]; 81 | 82 | [session setAttributes:atts4]; 83 | [self assert:2011 equals:[[session startDate] year]]; 84 | [self assert:8 equals:[[session startDate] month]]; 85 | [self assert:13 equals:[[session startDate] day]]; 86 | } 87 | 88 | - (void)testNewSansAttributes 89 | { 90 | tester1 = [User new]; 91 | [self assert:User equals:[tester1 class]]; 92 | [self assert:@"User" equals:[tester1 className]]; 93 | [self assert:nil equals:[tester1 email]]; 94 | [self assert:nil equals:[tester1 password]]; 95 | 96 | tester2 = [UserSession new]; 97 | [self assert:UserSession equals:[tester2 class]]; 98 | [self assert:@"UserSession" equals:[tester2 className]]; 99 | [self assert:nil equals:[tester2 userName]]; 100 | [self assert:nil equals:[tester2 startDate]]; 101 | } 102 | 103 | - (void)testNewWithAttributes 104 | { 105 | tester1 = [User new:{"email":"test@test.com", "password":"secret"}]; 106 | [self assert:User equals:[tester1 class]]; 107 | [self assert:@"User" equals:[tester1 className]]; 108 | [self assert:@"test@test.com" equals:[tester1 email]]; 109 | [self assert:@"secret" equals:[tester1 password]]; 110 | 111 | tester2 = [UserSession new:{"userName":"snoop", "startDate":"2009-04-05"}]; 112 | [self assert:UserSession equals:[tester2 class]]; 113 | [self assert:@"UserSession" equals:[tester2 className]]; 114 | [self assert:@"snoop" equals:[tester2 userName]]; 115 | [self assert:@"2009-04-05" equals:[[tester2 startDate] toDateString]]; 116 | } 117 | 118 | - (void)testResourceWillSaveWithNewResource 119 | { 120 | [observer startObserving:@"UserResourceWillSave"]; 121 | [observer startObserving:@"UserResourceWillCreate"]; 122 | var request = [user resourceWillSave], url = [request URL]; 123 | [self assert:@"POST" equals:[request HTTPMethod]]; 124 | [self assert:@"/users" equals:[url absoluteString]]; 125 | [self assertTrue:[observer didObserve:@"UserResourceWillSave"]]; 126 | [self assertTrue:[observer didObserve:@"UserResourceWillCreate"]]; 127 | [self assertFalse:[observer didObserve:@"UserResourceWillUpdate"]]; 128 | } 129 | 130 | - (void)testResourceWillSaveWithExistingResource 131 | { 132 | [observer startObserving:@"UserResourceWillSave"]; 133 | [observer startObserving:@"UserResourceWillUpdate"]; 134 | [user setIdentifier:@"42"]; 135 | var request = [user resourceWillSave], url = [request URL]; 136 | [self assert:@"PUT" equals:[request HTTPMethod]]; 137 | [self assert:@"/users/42" equals:[url absoluteString]]; 138 | [self assertTrue:[observer didObserve:@"UserResourceWillSave"]]; 139 | [self assertTrue:[observer didObserve:@"UserResourceWillUpdate"]]; 140 | [self assertFalse:[observer didObserve:@"UserResourceWillCreate"]]; 141 | } 142 | 143 | - (void)testSuccessfulSaveWithNewResource 144 | { 145 | [observer startObserving:@"UserResourceDidSave"]; 146 | [observer startObserving:@"UserResourceDidCreate"]; 147 | var response = [201, userResourceJSON]; 148 | [CPURLConnection selector:@selector(sendSynchronousRequest:) returns:response]; 149 | [self assertTrue:[user save]]; 150 | [self assertTrue:[observer didObserve:@"UserResourceDidSave"]]; 151 | [self assertTrue:[observer didObserve:@"UserResourceDidCreate"]]; 152 | } 153 | 154 | - (void)testFailedSaveWithNewResource 155 | { 156 | [observer startObserving:@"UserResourceDidNotSave"]; 157 | [observer startObserving:@"UserResourceDidNotCreate"]; 158 | var response = [422,'["email","already in use"]']; 159 | [CPURLConnection selector:@selector(sendSynchronousRequest:) returns:response]; 160 | [self assertFalse:[user save]]; 161 | [self assertTrue:[observer didObserve:@"UserResourceDidNotSave"]]; 162 | [self assertTrue:[observer didObserve:@"UserResourceDidNotCreate"]]; 163 | } 164 | 165 | - (void)testSuccessfulCreate 166 | { 167 | [observer startObserving:@"UserResourceDidSave"]; 168 | [observer startObserving:@"UserResourceDidCreate"]; 169 | var response = [201,userResourceJSON]; 170 | [CPURLConnection selector:@selector(sendSynchronousRequest:) returns:response]; 171 | var result = [User create:{"email":"test@test.com", "password":"secret"}]; 172 | [self assert:@"1" equals:[result identifier]]; 173 | [self assert:@"test@test.com" equals:[result email]]; 174 | [self assert:@"secret" equals:[result password]]; 175 | [self assertTrue:[observer didObserve:@"UserResourceDidSave"]]; 176 | [self assertTrue:[observer didObserve:@"UserResourceDidCreate"]]; 177 | } 178 | 179 | - (void)testFailedCreate 180 | { 181 | [observer startObserving:@"UserResourceDidNotSave"]; 182 | [observer startObserving:@"UserResourceDidNotCreate"]; 183 | var response = [422,'["email","already in use"]']; 184 | [CPURLConnection selector:@selector(sendSynchronousRequest:) returns:response]; 185 | var result = [User create:{"email":"test@test.com", "password":"secret"}]; 186 | [self assertNull:result]; 187 | [self assertTrue:[observer didObserve:@"UserResourceDidNotSave"]]; 188 | [self assertTrue:[observer didObserve:@"UserResourceDidNotCreate"]]; 189 | } 190 | 191 | - (void)testDestroy 192 | { 193 | [observer startObserving:@"UserResourceDidDestroy"]; 194 | var response = [200,'']; 195 | [CPURLConnection selector:@selector(sendSynchronousRequest:) returns:response]; 196 | [self assertTrue:[user destroy]]; 197 | [self assertTrue:[observer didObserve:@"UserResourceDidDestroy"]]; 198 | } 199 | 200 | - (void)testResourceWillLoad 201 | { 202 | [observer startObserving:@"UserResourceWillLoad"]; 203 | var request = [User resourceWillLoad:@"42"], url = [request URL]; 204 | [self assert:@"GET" equals:[request HTTPMethod]]; 205 | [self assert:@"/users/42" equals:[url absoluteString]]; 206 | [self assertTrue:[observer didObserve:@"UserResourceWillLoad"]]; 207 | } 208 | 209 | - (void)testResourceDidLoad 210 | { 211 | [observer startObserving:@"UserResourceDidLoad"]; 212 | var response = userResourceJSON, 213 | resource = [User resourceDidLoad:response]; 214 | [self assert:@"1" equals:[resource identifier]]; 215 | [self assert:@"test@test.com" equals:[resource email]]; 216 | [self assert:@"secret" equals:[resource password]]; 217 | [self assertTrue:[observer didObserve:@"UserResourceDidLoad"]]; 218 | } 219 | 220 | - (void)testFindingByIdentifierKey 221 | { 222 | var response = [201,userResourceJSON]; 223 | [CPURLConnection selector:@selector(sendSynchronousRequest:) returns:response]; 224 | var result = [User find:@"1"]; 225 | [self assert:@"1" equals:[result identifier]]; 226 | [self assert:@"test@test.com" equals:[result email]]; 227 | [self assert:@"secret" equals:[result password]]; 228 | 229 | result = [User find:1]; 230 | [self assert:@"1" equals:[result identifier]]; 231 | [self assert:@"test@test.com" equals:[result email]]; 232 | [self assert:@"secret" equals:[result password]]; 233 | } 234 | 235 | - (void)testFindingWithParams 236 | { 237 | var response = [201,userCollectionJSON]; 238 | [CPURLConnection selector:@selector(sendSynchronousRequest:) returns:response]; 239 | var result = [User findWithParams:{"email":"test"}]; 240 | [self assert:@"1" equals:[result identifier]]; 241 | [self assert:@"one@test.com" equals:[result email]]; 242 | 243 | var response = [201, ""]; 244 | [CPURLConnection selector:@selector(sendSynchronousRequest:) returns:response]; 245 | var result = [User findWithParams:{"email":"test"}]; 246 | [self assert:nil equals:result]; 247 | } 248 | 249 | - (void)testFindingAll 250 | { 251 | var response = [201,userCollectionJSON]; 252 | [CPURLConnection selector:@selector(sendSynchronousRequest:) returns:response]; 253 | var results = [User all]; 254 | [self assert:_CPJavaScriptArray equals:[results class]]; 255 | [self assert:@"1" equals:[[results objectAtIndex:0] identifier]]; 256 | [self assert:@"2" equals:[[results objectAtIndex:1] identifier]]; 257 | [self assert:@"3" equals:[[results objectAtIndex:2] identifier]]; 258 | [self assert:@"one@test.com" equals:[[results objectAtIndex:0] email]]; 259 | [self assert:@"two@test.com" equals:[[results objectAtIndex:1] email]]; 260 | [self assert:@"three@test.com" equals:[[results objectAtIndex:2] email]]; 261 | } 262 | 263 | - (void)testFindingAllWithParams 264 | { 265 | var response = [201,userCollectionJSON]; 266 | [CPURLConnection selector:@selector(sendSynchronousRequest:) returns:response]; 267 | var results = [User allWithParams:{"email":"test"}]; 268 | [self assert:_CPJavaScriptArray equals:[results class]]; 269 | [self assert:@"1" equals:[[results objectAtIndex:0] identifier]]; 270 | [self assert:@"2" equals:[[results objectAtIndex:1] identifier]]; 271 | [self assert:@"3" equals:[[results objectAtIndex:2] identifier]]; 272 | [self assert:@"one@test.com" equals:[[results objectAtIndex:0] email]]; 273 | [self assert:@"two@test.com" equals:[[results objectAtIndex:1] email]]; 274 | [self assert:@"three@test.com" equals:[[results objectAtIndex:2] email]]; 275 | } 276 | 277 | - (void)testCollectionWillLoad 278 | { 279 | [observer startObserving:@"UserCollectionWillLoad"]; 280 | var request = [User collectionWillLoad], url = [request URL]; 281 | [self assert:@"GET" equals:[request HTTPMethod]]; 282 | [self assert:@"/users" equals:[url absoluteString]]; 283 | [self assertTrue:[observer didObserve:@"UserCollectionWillLoad"]]; 284 | } 285 | 286 | - (void)testCollectionWillLoadWithOneJSObjectParam 287 | { 288 | [observer startObserving:@"UserCollectionWillLoad"]; 289 | var request = [User collectionWillLoad:{"password":"secret"}], url = [request URL]; 290 | [self assert:@"GET" equals:[request HTTPMethod]]; 291 | [self assert:@"/users?password=secret" equals:[url absoluteString]]; 292 | [self assertTrue:[observer didObserve:@"UserCollectionWillLoad"]]; 293 | } 294 | 295 | - (void)testCollectionWillLoadWithMultipleJSObjectParams 296 | { 297 | [observer startObserving:@"UserCollectionWillLoad"]; 298 | var request = [User collectionWillLoad:{"name":"joe blow","password":"secret"}], url = [request URL]; 299 | [self assert:@"GET" equals:[request HTTPMethod]]; 300 | [self assert:@"/users?name=joe%20blow&password=secret" equals:[url absoluteString]]; 301 | [self assertTrue:[observer didObserve:@"UserCollectionWillLoad"]]; 302 | } 303 | 304 | - (void)testCollectionWillLoadWithCPDictionary 305 | { 306 | [observer startObserving:@"UserCollectionWillLoad"]; 307 | var params = [CPDictionary dictionaryWithJSObject:{"name":"joe blow","password":"secret"}], 308 | request = [User collectionWillLoad:params], 309 | url = [request URL]; 310 | [self assert:@"GET" equals:[request HTTPMethod]]; 311 | [self assert:@"/users?name=joe%20blow&password=secret" equals:[url absoluteString]]; 312 | [self assertTrue:[observer didObserve:@"UserCollectionWillLoad"]]; 313 | } 314 | 315 | - (void)testCollectionDidLoad 316 | { 317 | [observer startObserving:@"UserCollectionDidLoad"]; 318 | var response = userCollectionJSON, 319 | collection = [User collectionDidLoad:response]; 320 | [self assert:_CPJavaScriptArray equals:[collection class]]; 321 | [self assert:User equals:[[collection objectAtIndex:0] class]]; 322 | [self assert:@"1" equals:[[collection objectAtIndex:0] identifier]]; 323 | [self assert:@"2" equals:[[collection objectAtIndex:1] identifier]]; 324 | [self assert:@"3" equals:[[collection objectAtIndex:2] identifier]]; 325 | [self assert:@"one@test.com" equals:[[collection objectAtIndex:0] email]]; 326 | [self assert:@"two@test.com" equals:[[collection objectAtIndex:1] email]]; 327 | [self assert:@"three@test.com" equals:[[collection objectAtIndex:2] email]]; 328 | [self assertTrue:[observer didObserve:@"UserCollectionDidLoad"]]; 329 | } 330 | 331 | @end 332 | --------------------------------------------------------------------------------