jsonapi-resources 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e5e235b0488716337c8c547c7a97bdbd1df7c05a
4
+ data.tar.gz: af645d11d41b982eb57a1c7a340be2692a9633a9
5
+ SHA512:
6
+ metadata.gz: 4365d7b76273359e386534a52a9ba7a40608d674c4e643815f564588afa8480f16b61cad7337e78dc13162872ae84e6b7a6fd9ca1a0a4d262f22877633fab008
7
+ data.tar.gz: 8f6bc0f211760c55df35515e61833bb3413ace9c093b9f56b5ec45b875476043110a2acc49b42446efb588ab9b63cb7958bece1e688b26bcb044f89614aad8e6
data/.gitignore ADDED
@@ -0,0 +1,20 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ .ruby-version
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
19
+ coverage
20
+ test/log
data/Gemfile ADDED
@@ -0,0 +1,22 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ platforms :ruby do
6
+ # sqlite3 1.3.9 does not work with rubinius 2.2.5:
7
+ # https://github.com/sparklemotion/sqlite3-ruby/issues/122
8
+ gem 'sqlite3', '1.3.8'
9
+ end
10
+
11
+ platforms :jruby do
12
+ gem 'activerecord-jdbcsqlite3-adapter'
13
+ end
14
+
15
+ version = ENV['RAILS_VERSION'] || '4.0.4'
16
+ rails = case version
17
+ when 'master'
18
+ {:github => 'rails/rails'}
19
+ else
20
+ "~> #{version}"
21
+ end
22
+ gem 'rails', rails
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Larry Gebhardt
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,451 @@
1
+ # JSONAPI::Resources
2
+
3
+ JSONAPI::Resources, or "JR", provides a framework for developing a server that complies with the [JSON API](http://jsonapi.org/) specification.
4
+
5
+ Like JSON API itself, JR's design is focused on the resources served by an API. JR needs little more than a definition of your resources, including their attributes and relationships, to make your server compliant with JSON API.
6
+
7
+ JR is designed to work with Rails, and provides custom routes, controllers, and serializers. JR's resources may be backed by ActiveRecord models or by custom objects.
8
+
9
+ ## Demo App
10
+
11
+ We have a simple demo app, called [Peeps](https://github.com/cerebris/peeps), available to show how JR is used.
12
+
13
+ ## Installation
14
+
15
+ Add JR to your application's `Gemfile`:
16
+
17
+ gem 'jsonapi-resources'
18
+
19
+ And then execute:
20
+
21
+ $ bundle
22
+
23
+ Or install it yourself as:
24
+
25
+ $ gem install jsonapi-resources
26
+
27
+ ## Usage
28
+
29
+ ### Resources
30
+
31
+ Resources define the public interface to your API. A resource defines which attributes are exposed, as well as relationships to other resources.
32
+
33
+ Resource definitions should by convention be placed in a directory under app named resources, `app/resources`. The class name should be the single underscored name of the model that backs the resource with `_resource.rb` appended. For example, a `Contact` model's resource should have a class named `ContactResource` defined in a file named `contact_resource.rb`.
34
+
35
+ #### JSONAPI::Resource
36
+
37
+ Resources must be derived from `JSONAPI::Resource`, or a class that is itself derived from `JSONAPI::Resource`.
38
+
39
+ For example:
40
+
41
+ ```
42
+ require 'jsonapi/resource'
43
+
44
+ class ContactResource < JSONAPI::Resource
45
+ end
46
+ ```
47
+
48
+ #### Attributes
49
+
50
+ Any of a resource's attributes that are accessible must be explicitly declared. Single attributes can be declared using the `attribute` method, and multiple attributes can be declared with the `attributes` method on the resource class.
51
+
52
+ For example:
53
+
54
+ ```
55
+ require 'jsonapi/resource'
56
+
57
+ class ContactResource < JSONAPI::Resource
58
+ attribute :id
59
+ attribute :name_first
60
+ attributes :name_last, :email, :twitter
61
+ end
62
+ ```
63
+
64
+ This resource has 5 attributes: `:id`, `:name_first`, `:name_last`, `:email`, `:twitter`. By default these attributes must exist on the model that is handled by the resource.
65
+
66
+ A resource object wraps a Ruby object, usually an ActiveModel record, which is available as the `@object` variable. This allows a resource's methods to access the underlying object.
67
+
68
+ For example, a computed attribute for `full_name` could be defined as such:
69
+
70
+ ```
71
+ require 'jsonapi/resource'
72
+
73
+ class ContactResource < JSONAPI::Resource
74
+ attributes :id, :name_first, :name_last, :email, :twitter
75
+ attribute :full_name
76
+
77
+ def full_name
78
+ "#{@object.name_first}, #{@object.name_last}"
79
+ end
80
+ end
81
+ ```
82
+
83
+ ##### Fetchable Attributes
84
+
85
+ By default all attributes are assumed to be fetchable. The list of fetchable attributes can be filtered by overriding the `fetchable` method.
86
+
87
+ Here's an example that prevents guest users from seeing the `email` field:
88
+
89
+ ```
90
+ class AuthorResource < JSONAPI::Resource
91
+ attributes :id, :name, :email
92
+ model_name 'Person'
93
+ has_many :posts
94
+
95
+ def fetchable(keys, context)
96
+ if (context.current_user.guest)
97
+ super(keys - [:email])
98
+ else
99
+ super(keys)
100
+ end
101
+ end
102
+ end
103
+ ```
104
+
105
+ Context flows through from the controller and can be used to control the attributes based on the current user (or other value)).
106
+
107
+ ##### Creatable and Updateable Attributes
108
+
109
+ By default all attributes are assumed to be updateble and creatable. To prevent some attributes from being accepted by the `update` or `create` methods, override the `self.updateable` and `self.creatable` methods on a resource.
110
+
111
+ This example prevents `full_name` from being set:
112
+
113
+ ```
114
+ require 'jsonapi/resource'
115
+
116
+ class ContactResource < JSONAPI::Resource
117
+ attributes :id, :name_first, :name_last, :full_name
118
+
119
+ def full_name
120
+ "#{@object.name_first}, #{@object.name_last}"
121
+ end
122
+
123
+ def self.updateable(keys, context = nil)
124
+ super(keys - [:full_name])
125
+ end
126
+
127
+ def self.createable(keys, context = nil)
128
+ super(keys - [:full_name])
129
+ end
130
+ end
131
+ ```
132
+
133
+ The `context` is not used by the `ResourceController`, but may be used if you override the controller methods.
134
+
135
+ #### Key
136
+
137
+ The primary key of the resource defaults to `id`, which can be changed using the `key` method.
138
+
139
+ ```
140
+ class CurrencyResource < JSONAPI::Resource
141
+ key :code
142
+ attributes :code, :name
143
+
144
+ has_many :expense_entries
145
+ end
146
+
147
+ ```
148
+
149
+ #### Model Name
150
+
151
+ The name of the underlying model is inferred from the Resource name. It can be overridden by use of the `model_name` method. For example:
152
+
153
+ ```
154
+ class AuthorResource < JSONAPI::Resource
155
+ attributes :id, :name
156
+ model_name 'Person'
157
+ has_many :posts
158
+ end
159
+ ```
160
+
161
+ #### Associations
162
+
163
+ Related resources need to be specified in the resource. These are declared with the `has_one` and the `has_many` methods.
164
+
165
+ Here's a simple example where a post has a single author and an author can have many posts:
166
+
167
+ ```
168
+ class PostResource < JSONAPI::Resource
169
+ attribute :id, :title, :body
170
+
171
+ has_one :author
172
+ end
173
+ ```
174
+
175
+ And the corresponding author:
176
+
177
+ ```
178
+ class AuthorResource < JSONAPI::Resource
179
+ attribute :id, :name
180
+
181
+ has_many :posts
182
+ end
183
+ ```
184
+
185
+ ##### Options
186
+
187
+ The association methods support the following options:
188
+ * `class_name` - a string specifying the underlying class for the related resource
189
+ * `primary_key` - the primary key to the related resource, if different than `id`
190
+ * `key` - the key in the resource that identifies the related resource, if different than `<resource_name>_id`
191
+ * `treat_as_set` - allows the entire set of related records to be replaced in one operation. Defaults to false if not set.
192
+
193
+ Examples:
194
+
195
+ ```
196
+ class CommentResource < JSONAPI::Resource
197
+ attributes :id, :body
198
+ has_one :post
199
+ has_one :author, class_name: 'Person'
200
+ has_many :tags, treat_as_set: true
201
+ end
202
+ ```
203
+
204
+ ```
205
+ class ExpenseEntryResource < JSONAPI::Resource
206
+ attributes :id, :cost, :transaction_date
207
+
208
+ has_one :currency, class_name: 'Currency', key: 'currency_code'
209
+ has_one :employee
210
+ end
211
+ ```
212
+
213
+ #### Filters
214
+
215
+ Filters for locating objects of the resource type are specified in the resource definition. Single filters can be declared using the `filter` method, and multiple filters can be declared with the `filters` method on the
216
+ resource class.
217
+
218
+ For example:
219
+
220
+ ```
221
+ require 'jsonapi/resource'
222
+
223
+ class ContactResource < JSONAPI::Resource
224
+ attributes :id, :name_first, :name_last, :email, :twitter
225
+
226
+ filter :id
227
+ filters :name_first, :name_last
228
+ end
229
+ ```
230
+
231
+ ##### Finders
232
+
233
+ Basic finding by filters is supported by resources. However if you have more complex requirements for finding you can override the `find` and `find_by_key` methods on the resource.
234
+
235
+ Here's an example that defers the `find` operation to a `current_user` set on the `context`:
236
+
237
+ ```
238
+ class AuthorResource < JSONAPI::Resource
239
+ attributes :id, :name
240
+ model_name 'Person'
241
+ has_many :posts
242
+
243
+ filter :name
244
+
245
+ def self.find(attrs, context = nil)
246
+ authors = context.current_user.find_authors(attrs)
247
+
248
+ return authors.map do |author|
249
+ self.new(author)
250
+ end
251
+ end
252
+ end
253
+ ```
254
+
255
+ ### Controllers
256
+
257
+ JSONAPI::Resources provides a class, `ResourceController`, that can be used as the base class for your controllers. `ResourceController` supports `index`, `show`, `create`, `update`, and `destroy` methods. Just deriving your controller from `ResourceController` will give you a fully functional controller.
258
+
259
+ For example:
260
+
261
+ ```
262
+ class PeopleController < JSONAPI::ResourceController
263
+
264
+ end
265
+ ```
266
+
267
+ Of course you are free to extend this as needed and override action handlers or other methods.
268
+
269
+ The context that's used for serialization and resource configuration is set by the controller's `context` method.
270
+
271
+ For example:
272
+
273
+ ```
274
+ class ApplicationController < JSONAPI::ResourceController
275
+ def context
276
+ {current_user: current_user}
277
+ end
278
+ end
279
+
280
+ # Specific resource controllers derive from ApplicationController
281
+ # and share its context
282
+ class PeopleController < ApplicationController
283
+
284
+ end
285
+ ```
286
+
287
+ #### Error codes
288
+
289
+ Error codes are provided for each error object returned, based on the error. These errors are:
290
+
291
+ ```
292
+ module JSONAPI
293
+ VALIDATION_ERROR = 100
294
+ INVALID_RESOURCE = 101
295
+ FILTER_NOT_ALLOWED = 102
296
+ INVALID_FIELD_VALUE = 103
297
+ INVALID_FIELD = 104
298
+ PARAM_NOT_ALLOWED = 105
299
+ PARAM_MISSING = 106
300
+ INVALID_FILTER_VALUE = 107
301
+ COUNT_MISMATCH = 108
302
+ KEY_ORDER_MISMATCH = 109
303
+ KEY_NOT_INCLUDED_IN_URL = 110
304
+
305
+ RECORD_NOT_FOUND = 404
306
+ LOCKED = 423
307
+ end
308
+ ```
309
+
310
+ These codes can be customized in your app by creating an initializer to override any or all of the codes.
311
+
312
+ ### Serializer
313
+
314
+ The `ResourceSerializer` can be used to serialize a resource into JSON API compliant JSON. `ResourceSerializer` has a `serialize` method that takes a resource instance to serialize. For example:
315
+
316
+ ```
317
+ post = Post.find(1)
318
+ JSONAPI::ResourceSerializer.new.serialize(PostResource.new(post))
319
+ ```
320
+
321
+ This returns results like this:
322
+
323
+ ```
324
+ {
325
+ posts: [{
326
+ id: 1,
327
+ title: 'New post',
328
+ body: 'A body!!!',
329
+ links: {
330
+ section: nil,
331
+ author: 1,
332
+ tags: [1,2,3],
333
+ comments: [1,2]
334
+ }
335
+ }]
336
+ }
337
+ ```
338
+
339
+ #### Serialize method options
340
+
341
+ The serialize method also takes some optional parameters:
342
+
343
+ ##### `include`
344
+
345
+ An array of resources. Nested resources can be specified with dot notation.
346
+
347
+ *Purpose*: determines which objects will be side loaded with the source objects in a linked section
348
+
349
+ *Example*: ```include: ['comments','author','comments.tags','author.posts']```
350
+
351
+ ##### `fields`
352
+
353
+ A hash of resource types and arrays of fields for each resource type.
354
+
355
+ *Purpose*: determines which fields are serialized for a resource type. This encompasses both attributes and association ids in the links section for a resource. Fields are global for a resource type.
356
+
357
+ *Example*: ```fields: { people: [:id, :email, :comments], posts: [:id, :title, :author], comments: [:id, :body, :post]}```
358
+
359
+ ```
360
+ post = Post.find(1)
361
+ JSONAPI::ResourceSerializer.new.serialize(PostResource.new(post),
362
+ include: ['comments','author','comments.tags','author.posts'],
363
+ fields: {
364
+ people: [:id, :email, :comments],
365
+ posts: [:id, :title, :author],
366
+ tags: [:name],
367
+ comments: [:id, :body, :post]})
368
+ ```
369
+
370
+ ##### `context`
371
+
372
+ Context data can be provided to the serializer, which passes it to each resource as it is inspected.
373
+
374
+ #### Routing
375
+
376
+ JR has a couple of helper methods available to assist you with setting up routes.
377
+
378
+ ##### `jsonapi_resources`
379
+
380
+ Like `resources` in ActionDispatch provides a resourceful route provides a mapping between HTTP verbs and URLs and
381
+ controller actions. This will also setup mappings for relationship URLs for a resource's associations. For example
382
+
383
+ ```
384
+ require 'jsonapi/routing_ext'
385
+
386
+ Peeps::Application.routes.draw do
387
+ jsonapi_resources :contacts
388
+ jsonapi_resources :phone_numbers
389
+ end
390
+ ```
391
+
392
+ gives the following routes
393
+
394
+ ```
395
+ Prefix Verb URI Pattern Controller#Action
396
+ contact_links_phone_numbers GET /contacts/:contact_id/links/phone_numbers(.:format) contacts#show_association {:association=>"phone_numbers"}
397
+ POST /contacts/:contact_id/links/phone_numbers(.:format) contacts#create_association {:association=>"phone_numbers"}
398
+ DELETE /contacts/:contact_id/links/phone_numbers/:keys(.:format) contacts#destroy_association {:association=>"phone_numbers"}
399
+ contacts GET /contacts(.:format) contacts#index
400
+ POST /contacts(.:format) contacts#create
401
+ new_contact GET /contacts/new(.:format) contacts#new
402
+ edit_contact GET /contacts/:id/edit(.:format) contacts#edit
403
+ contact GET /contacts/:id(.:format) contacts#show
404
+ PATCH /contacts/:id(.:format) contacts#update
405
+ PUT /contacts/:id(.:format) contacts#update
406
+ DELETE /contacts/:id(.:format) contacts#destroy
407
+ phone_number_links_contact GET /phone_numbers/:phone_number_id/links/contact(.:format) phone_numbers#show_association {:association=>"contact"}
408
+ POST /phone_numbers/:phone_number_id/links/contact(.:format) phone_numbers#create_association {:association=>"contact"}
409
+ DELETE /phone_numbers/:phone_number_id/links/contact(.:format) phone_numbers#destroy_association {:association=>"contact"}
410
+ phone_numbers GET /phone_numbers(.:format) phone_numbers#index
411
+ POST /phone_numbers(.:format) phone_numbers#create
412
+ new_phone_number GET /phone_numbers/new(.:format) phone_numbers#new
413
+ edit_phone_number GET /phone_numbers/:id/edit(.:format) phone_numbers#edit
414
+ phone_number GET /phone_numbers/:id(.:format) phone_numbers#show
415
+ PATCH /phone_numbers/:id(.:format) phone_numbers#update
416
+ PUT /phone_numbers/:id(.:format) phone_numbers#update
417
+ DELETE /phone_numbers/:id(.:format) phone_numbers#destroy
418
+ ```
419
+
420
+ ##### `jsonapi_resource`
421
+
422
+ Like `jsonapi_resources`, but for resources you lookup without an id.
423
+
424
+ ##### `jsonapi_links`
425
+
426
+ You can control the relationship routes by passing a block into `jsonapi_resources` or `jsonapi_resource`. An empty block
427
+ will not create any relationship routes.
428
+
429
+ You can add relationship routes in with `jsonapi_links`, for example:
430
+
431
+ ```
432
+ jsonapi_resources :posts, except: [:destroy] do
433
+ jsonapi_link :author, except: [:destroy]
434
+ jsonapi_links :tags, only: [:show, :create]
435
+ end
436
+
437
+ ```
438
+
439
+ This will create relationship routes for author (show and create, but not destroy) and for tags (again show and create, but not destroy).
440
+
441
+ ## Contributing
442
+
443
+ 1. Fork it ( http://github.com/cerebris/jsonapi-resources/fork )
444
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
445
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
446
+ 4. Push to the branch (`git push origin my-new-feature`)
447
+ 5. Create a new Pull Request
448
+
449
+ ## License
450
+
451
+ Copyright 2014 Cerebris Corporation. MIT License (see LICENSE for details).