Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Newer
Older
100644 263 lines (193 sloc) 10.847 kB
14aebfe @croaky pulled in blog posts for a README
croaky authored
1 Jester
2 ======
3
4 Jester is our implementation of REST, in JavaScript. It provides (nearly) identical syntax to ActiveResource for using REST to find, update, and create data, but from the client side.
5
6 It depends on [Prototype](http://prototypejs.org) and [ObjTree](http://www.kawa.net/works/js/xml/objtree-e.html).
7
8 Syntax
9 ------
10
11 All examples below are taken from inside the JavaScript console of Firebug.
12
13 First, declare a model in Jester by calling model on Base:
14
15 >>> Base.model("User")
16 >>> User
17 Object _name=User _singular=user _plural=users
18
19 This creates a global variable called User. It assumes that the URL prefix it uses to base its HTTP requests from is the current domain and port, and assumes "user" and "users" as single and plural forms to make these URLs. There's no "people/person" intelligence here, so make sure to override these defaults if you need to, like so:
20
21 >>> Base.model("Child", "http://www.thoughtbot.com", "child", "children")
22 >>> Child
23 Object _name=Child _singular=child _plural=children
24
25 If you want to capture the model created in a local variable, or simply prefer more traditional JavaScript syntax, you can do:
26
27 >>> var Child = new Base("Child", "http://www.thoughtbot.com", "child", "children")
28 >>> Child
29 Object _name=Child _singular=child _plural=children
30
31 Find will retrieve a particular instance of your model. Attributes are auto-converted to integer or boolean types if that's what they are on the server side. The "GET" line is not a return value, just Firebug's report of activity, but relevant to understanding what's happening.
32
33 >>> eric = User.find(1)
34 GET http://localhost:3000/users/1.xml
35 Object _name=User _singular=user _plural=users
36
37 >>> eric.attributes
38 ["active", "email", "id", "name"]
39
40 >>> eric.id
41 1
42 >>> eric.name
43 "Eric Mill"
44 >>> eric.active
45 true
46
47 Create takes a hash of attribute values. After calling create, the model will fetch its new ID from the return headers.
48
49 >>> floyd = User.create({name: "Floyd Wright", email: "tfwright@thoughtbot.com"})
50 POST http://localhost:3000/users.xml
51 Object _name=User _singular=user _plural=users
52
53 >>> floyd.id
54 9
55
56 >>> User.find(9).name
57 GET http://localhost:3000/users/9.xml
58 "Floyd Wright"
59
60 Updating is as simple as changing one of the properties and calling save.
61
62 >>> eric = User.find(1)
63 GET http://localhost:3000/users/1.xml
64 Object _name=User _singular=user _plural=users
65
66 >>> eric.email
67 "emill@thoughtbot.com"
68 >>> eric.email = "sandybeach@wintermute.com"
69 "sandybeach@wintermute.com"
70
71 >>> eric.save()
72 POST http://localhost:3000/users/1.xml
73 true
74
75 >>> User.find(eric.id).email
76 GET http://localhost:3000/users/1.xml
77 "sandybeach@wintermute.com"
78
79 Sadly, there's one area where Jester's syntax can't match ActiveResource's perfectly. The method "new" has been renamed to build, due to "new" being an illegal method name in JavaScript up to 1.6. Hopefully this can be updated as the browser landscape evolves. Build was chosen because it is similarly used in ActiveRecord to replace "new" on an association array, where "new" cannot be used.
80
81 >>> chad = User.build({email: "cpytel@thoughtbot.com", name: "Chad Pytel"})
82 Object _name=User _singular=user _plural=users
83
84 >>> chad.new_record()
85 true
86 >>> chad.save()
87 POST http://localhost:3000/users.xml
88 true
89
90 >>> chad.id
91 9
92 >>> chad.new_record()
93 false
94
95 Error validations are supported. If a model fails to save, save returns false, and the model's errors property is set with an array of the error messages returned.
96
97 >>> jared = User.build({name: "", email: ""})
98 Object _name=User _singular=user _plural=users
99
100 >>> jared.save()
101 POST http://localhost:3000/users.xml
102 false
103
104 >>> jared.errors
105 ["Name can't be blank", "Email can't be blank"]
106 >>> jared.valid()
107 false
108
109 >>> jared.name = "Jared Carroll"
110 "Jared Carroll"
111 >>> jared.email = "emill@thoughtbot.com"
112 "emill@thoughtbot.com"
113
114 >>> jared.save()
115 POST http://localhost:3000/users.xml
116 false
117
118 >>> jared.errors
119 ["Email has already been taken"]
120 >>> jared.email = "jcarroll@thoughtbot.com"
121 "jcarroll@thoughtbot.com"
122
123 >>> jared.save()
124 POST http://localhost:3000/users.xml
125 true
126
127 Lastly, associations are also supported. If the association data is included in the XML, they'll be loaded into the returned model as Jester models of their own, using the same assumptions on naming and URL prefix described above. They're full models, so you can edit and save them as you would the parent. Has_many relationships come back as simple arrays, has_one relationships as a property. In this example, User has_many :posts, and Post belongs_to :user.
128
129 >>> eric = User.find(1)
130 GET http://localhost:3000/users/1.xml
131 Object _name=User _singular=user _plural=users
132
133 >>> eric.posts
134 [Object _name=Post _singular=post _plural=posts, Object _name=Post _singular=post _plural=posts]
135
136 >>> eric.posts.first().body
137 "Today I passed the bar exam. Tomorrow, I make Nancy my wife."
138 >>> eric.posts.first().body = "Today I *almost* passed the bar exam. The ring waits one more day."
139 "Today I *almost* passed the bar exam. The ring waits one more day."
140
141 >>> eric.posts.first().save()
142 POST http://localhost:3000/posts/1.xml
143 true
144
145 >>> post = Post.find(1)
146 GET http://localhost:3000/posts/1.xml
147 Object _name=Post _singular=post _plural=posts
148
149 >>> post.body
150 "Today I *almost* passed the bar exam. The ring waits one more day."
151 >>> post.user
152 Object _name=User _singular=user _plural=users
153 >>> post.user.name
154 "Eric Mill"
155
156 Using Jester
157 ------------
158
159 Jester depends on two libraries: Prototype, which comes with Rails and most people are familiar with, and ObjTree, a nice DOM parsing engine for JavaScript. Both of these are packaged along with Jester in this repository, so you don't have to hunt for them yourself. Just make sure you're including all three in your test file.
160
161 <script type="text/javascript" src="/javascripts/prototype.js"></script>
162 <script type="text/javascript" src="/javascripts/ObjTree.js"></script>
163 <script type="text/javascript" src="/javascripts/jester.js"></script>
164
165 JavaScript in the browser is limited to requests with in only the same domain as the script is running in, so without iframe hackery, Jester is probably only useful for writing client code in your own apps, to talk to itself. We're investigating whether Jester can use this hackery to make cross-domain requests, but it's not clear if this will be feasible.
166
167 There are also some basic unit tests included inside Jester's repository, which run using JsUnit. To run them yourself, from Jester's repository open the file test/jsunit/testRunner.html in your browser, and choose test/jester_test.html as the test file.
168
169 The Server Side
170 ---------------
171
172 These examples are talking with a Rails application whose controllers were generated with "./script generate scaffold_resource"—in other words, the ideal RESTful controllers. It's very easy to make your controller RESTful. Here's the source for the User controller I'm using. The lines that deal with returning HTML have been removed, and I have added "(:include => :posts)" as an argument to to_xml in two places, so associations are included (it's that easy!).
173
174 An example of the XML produced here, of a User with one Post, at /users/2.xml:
175
176 <user>
177 <active type="boolean">true</active>
178 <email>cpytel@thoughtbot.com</email>
179 <id type="integer">2</id>
180 <name>Chad Pytel</name>
181 <posts>
182 <post>
183 <title>Life as a Jester</title>
184 <body>It's not as hard as Master said it would be. Today I made 200 dollars.</body>
185 <created-at type="datetime">2007-04-01T04:01:56-04:00</created-at>
186 <id type="integer">2</id>
187 <user-id type="integer">2</user-id>
188 </post>
189 </posts>
190 </user>
191
192 JSONic REST
193 -----------
194
195 Using JSON in Jester is easy. Set the "format" option when defining your model, and JSON will be the format used for all requests dealing with that model. Requests are made using ".json" as a URL suffix. Like so:
196
197 >>> Base.model("User", {format: "json"})
198 >>> eric = User.find(1)
199 GET http://localhost:3000/users/1.json
200
201 The controller code for this is simple. I prefer using wants.json, not wants.js, leaving the ".js" extension available for RJS or whatever you want. This works out of the box, with no need to add a mime types. Here's what I did:
202
203 def show
204 @user = User.find(params[:id])
205 respond_to do |wants|
206 wants.xml {render :xml => @user.to_xml(:include => :posts)}
207 wants.json {render :text => @user.to_json}
208 end
209 end
210
211 Going to /users/1.json produces the following JSON:
212
213 {
214 attributes:
215 {
216 id: "1",
217 bio: "",
218 extra_flag: "0",
219 middle_name: "Rogers",
220 active: "1",
221 created_at: "2007-04-25 19:15:10",
222 email: "yes"
223 }
224 }
225
226 Note that there isn't any automatic typecasting going on here. The default XML output from an ActiveRecord::Base object includes attributes describing types, but the JSON output doesn't. So, boolean flags will come back as the strings "1" and "0". At the Jester level, I've made two auto-casting assumptions: the ID will be turned into an integer, and any fields named created_at/created_on/updated_at/updated_on will be turned into a Date.
227
228 >>> eric = User.find(1)
229 GET http://localhost:3000/users/1.json
230 >>> eric.id
231 1
232 >>> eric.middle_name
233 "Rogers"
234 >>> eric.active
235 "1"
236 >>> eric.created_at
237 Wed Apr 25 2007 15:15:10 GMT-0400 (Eastern Daylight Time)
238
239 As a companion feature, Jester supports passing JSON code through the X-JSON header, passing through the second "json" parameter to any callback you provide to an asynchronous Jester request. I'll just show you.
240
241 >>> var type;
242 >>> User.find(1, {}, function(eric, json) {type = json.active.type})
243 GET http://localhost:3000/users/1.json
244 XMLHttpRequest
245 >>> type
246 "boolean"
247
248 And on the controller side, inside the show action, I have this line:
249
250 headers["X-JSON"] = "{active: {type: 'boolean'}}"
251
252 This allows you to pass extra JSON information along with any data returned from the server. You don't have to have the model's format set to "json" for this to operate, either—you can pass JSON information alongside an XML response in the same way.
253
254 Credits
255 -------
256
257 Thank you to all [the contributors](https://github.com/thoughtbot/jester/contributors).
258
259 License
260 -------
261
262 Jester is Copyright © 2007-2011 thoughtbot. It is free software, and may be redistributed under the terms specified in the MIT-LICENSE file.
Something went wrong with that request. Please try again.