jsonapi-resources 0.0.14 → 0.0.15

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6700d2a30e18dc6391ba4aa8bc0ecdd517e2fdfb
4
- data.tar.gz: ad330dfe0d8fdc5da07d7ee40db08e892293296d
3
+ metadata.gz: 526bbf206d79bc7971772b890f0734ab2af6b426
4
+ data.tar.gz: 015940fbcc6439e1616bbfc538c63e213d5544da
5
5
  SHA512:
6
- metadata.gz: 893a1947cd78da813d77e69c92dafbaac07b50c8dab2ab495ebdf49da50c082f8123c99361cf95793e606dd4916d140401566a4465829bf137aa1f86144589db
7
- data.tar.gz: d00fb87cc0c3a787d28fb68bc1d8072b6dc30143108dc3672770b19704430126511bf0aa73d2b86434546812ff6a4d194d710eb521a6f9d88840d8faeeef59d6
6
+ metadata.gz: f9470953f1434a9ae0fff2de51f730cc0357369f7240b07a0edc7d948440b843e52e20f78e19dcf7e28f3b18a7ba8fd31a5136f53d9919603d47d8b1fe174201
7
+ data.tar.gz: 23b05b382b869d38a26937b9c9c2c8d75811543e456e6c37963d498b9f06d9e644ab5ca8318afcee4d6b27c84e379453037cef50dc28027cebb60a182a811212
@@ -3,3 +3,4 @@ rvm:
3
3
  - 1.9.3
4
4
  - 2.0.0
5
5
  - 2.1
6
+ - 2.2
data/Gemfile CHANGED
@@ -3,9 +3,7 @@ source 'https://rubygems.org'
3
3
  gemspec
4
4
 
5
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'
6
+ gem 'sqlite3', '1.3.10'
9
7
  end
10
8
 
11
9
  platforms :jruby do
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # JSONAPI::Resources [![Build Status](https://secure.travis-ci.org/cerebris/jsonapi-resources.png?branch=master)](http://travis-ci.org/cerebris/jsonapi-resources)
2
2
 
3
- JSONAPI::Resources, or "JR", provides a framework for developing a server that complies with the [JSON API](http://jsonapi.org/) specification.
3
+ `JSONAPI::Resources`, or "JR", provides a framework for developing a server that complies with the [JSON API](http://jsonapi.org/) specification.
4
4
 
5
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
6
 
@@ -63,7 +63,7 @@ end
63
63
 
64
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
65
 
66
- A resource object wraps a Ruby object, usually an ActiveModel record, which is available as the `@model` variable. This allows a resource's methods to access the underlying model.
66
+ A resource object wraps a Ruby object, usually an `ActiveModel` record, which is available as the `@model` variable. This allows a resource's methods to access the underlying model.
67
67
 
68
68
  For example, a computed attribute for `full_name` could be defined as such:
69
69
 
@@ -102,7 +102,7 @@ class AuthorResource < JSONAPI::Resource
102
102
  end
103
103
  ```
104
104
 
105
- Context flows through from the controller and can be used to control the attributes based on the current user (or other value)).
105
+ Context flows through from the controller and can be used to control the attributes based on the current user (or other value).
106
106
 
107
107
  ##### Creatable and Updateable Attributes
108
108
 
@@ -152,9 +152,9 @@ end
152
152
 
153
153
  ##### Attribute Formatting
154
154
 
155
- Attributes can have a Format. By default all attributes use the default formatter. If an attribute has the `format` option set the system will attempt to find a formatter based on this name. In the following example the `last_login_time` will be returned formatted to a certain time zone:
155
+ Attributes can have a `Format`. By default all attributes use the default formatter. If an attribute has the `format` option set the system will attempt to find a formatter based on this name. In the following example the `last_login_time` will be returned formatted to a certain time zone:
156
156
 
157
- ```
157
+ ```ruby
158
158
  class PersonResource < JSONAPI::Resource
159
159
  attributes :id, :name, :email
160
160
  attribute :last_login_time, format: :date_with_timezone
@@ -235,7 +235,7 @@ Examples:
235
235
  class ExpenseEntryResource < JSONAPI::Resource
236
236
  attributes :id, :cost, :transaction_date
237
237
 
238
- has_one :currency, class_name: 'Currency', key: 'currency_code'
238
+ has_one :currency, class_name: 'Currency', foreign_key: 'currency_code'
239
239
  has_one :employee
240
240
  end
241
241
  ```
@@ -260,7 +260,7 @@ end
260
260
 
261
261
  ##### Finders
262
262
 
263
- Basic finding by filters is supported by resources. This is implemented in the `find`, `find_by_key` and `find_by_keys` finder methods. Currently this is implemented for ActiveRecord based resources. The finder methods rely on the `records` method to get an Arel relation. It is therefore possible to override `records` to affect the three find related methods.
263
+ Basic finding by filters is supported by resources. This is implemented in the `find`, `find_by_key` and `find_by_keys` finder methods. Currently this is implemented for `ActiveRecord` based resources. The finder methods rely on the `records` method to get an `Arel` relation. It is therefore possible to override `records` to affect the three find related methods.
264
264
 
265
265
  ###### Customizing base records for finder methods
266
266
 
@@ -281,9 +281,9 @@ end
281
281
 
282
282
  ###### Applying Filters
283
283
 
284
- The `apply_filter` method is called to apply each filter to the Arel relation. You may override this method to gain control over how the filters are applied to the Arel relation.
284
+ The `apply_filter` method is called to apply each filter to the `Arel` relation. You may override this method to gain control over how the filters are applied to the `Arel` relation.
285
285
 
286
- For example to change how a
286
+ This example shows how you can implement different approaches for different filters.
287
287
 
288
288
  ```ruby
289
289
  def apply_filter(records, filter, value)
@@ -332,7 +332,7 @@ end
332
332
 
333
333
  ### Controllers
334
334
 
335
- 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.
335
+ `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.
336
336
 
337
337
  For example:
338
338
 
@@ -362,6 +362,98 @@ class PeopleController < ApplicationController
362
362
  end
363
363
  ```
364
364
 
365
+ #### Namespaces
366
+
367
+ JSONAPI::Resources supports namespacing of controllers and resources. With namespacing you can version your API.
368
+
369
+ If you namespace your controller it will require a namespaced resource.
370
+
371
+ In the following example we have a `resource` that isn't namespaced, and one the has now been namespaced. There are slight differences between the two resources, as might be seen in a new version of an API:
372
+
373
+ ```ruby
374
+ class PostResource < JSONAPI::Resource
375
+ attribute :id
376
+ attribute :title
377
+ attribute :body
378
+ attribute :subject
379
+
380
+ has_one :author, class_name: 'Person'
381
+ has_one :section
382
+ has_many :tags, acts_as_set: true
383
+ has_many :comments, acts_as_set: false
384
+ def subject
385
+ @model.title
386
+ end
387
+
388
+ filters :title, :author, :tags, :comments
389
+ filter :id
390
+ end
391
+
392
+ ...
393
+
394
+ module Api
395
+ module V1
396
+ class PostResource < JSONAPI::Resource
397
+ # V1 replaces the non-namespaced resource
398
+ # V1 no longer supports tags and now calls author 'writer'
399
+ attribute :id
400
+ attribute :title
401
+ attribute :body
402
+ attribute :subject
403
+
404
+ has_one :writer, foreign_key: 'author_id'
405
+ has_one :section
406
+ has_many :comments, acts_as_set: false
407
+
408
+ def subject
409
+ @model.title
410
+ end
411
+
412
+ filters :writer
413
+ end
414
+
415
+ class WriterResource < JSONAPI::Resource
416
+ attributes :id, :name, :email
417
+ model_name 'Person'
418
+ has_many :posts
419
+
420
+ filter :name
421
+ end
422
+ end
423
+ end
424
+ ```
425
+
426
+ The following controllers are used:
427
+
428
+ ```ruby
429
+ class PostsController < JSONAPI::ResourceController
430
+ end
431
+
432
+ module Api
433
+ module V1
434
+ class PostsController < JSONAPI::ResourceController
435
+ end
436
+ end
437
+ end
438
+ ```
439
+
440
+ You will also need to namespace your routes:
441
+
442
+ ```ruby
443
+ Rails.application.routes.draw do
444
+
445
+ jsonapi_resources :posts
446
+
447
+ namespace :api do
448
+ namespace :v1 do
449
+ jsonapi_resources :posts
450
+ end
451
+ end
452
+ end
453
+ ```
454
+
455
+ When a namespaced `resource` is used, any related `resources` must also be in the same namespace.
456
+
365
457
  #### Error codes
366
458
 
367
459
  Error codes are provided for each error object returned, based on the error. These errors are:
@@ -420,7 +512,7 @@ This returns results like this:
420
512
  }
421
513
  ```
422
514
 
423
- #### Serialize_to_hash method options
515
+ #### serialize_to_hash method options
424
516
 
425
517
  The `serialize_to_hash` method also takes some optional parameters:
426
518
 
@@ -442,13 +534,16 @@ A hash of resource types and arrays of fields for each resource type.
442
534
 
443
535
  ```ruby
444
536
  post = Post.find(1)
445
- JSONAPI::ResourceSerializer.new(PostResource).serialize_to_hash(PostResource.new(post),
446
- include: ['comments','author','comments.tags','author.posts'],
447
- fields: {
448
- people: [:id, :email, :comments],
449
- posts: [:id, :title, :author],
450
- tags: [:name],
451
- comments: [:id, :body, :post]})
537
+ JSONAPI::ResourceSerializer.new(PostResource).serialize_to_hash(
538
+ PostResource.new(post),
539
+ include: ['comments','author','comments.tags','author.posts'],
540
+ fields: {
541
+ people: [:id, :email, :comments],
542
+ posts: [:id, :title, :author],
543
+ tags: [:name],
544
+ comments: [:id, :body, :post]
545
+ }
546
+ )
452
547
  ```
453
548
 
454
549
  ##### `context`
@@ -461,7 +556,7 @@ JR has a couple of helper methods available to assist you with setting up routes
461
556
 
462
557
  ##### `jsonapi_resources`
463
558
 
464
- Like `resources` in ActionDispatch, `jsonapi_resources` provides resourceful routes mapping between HTTP verbs and URLs and controller actions. This will also setup mappings for relationship URLs for a resource's associations. For example
559
+ Like `resources` in `ActionDispatch`, `jsonapi_resources` provides resourceful routes mapping between HTTP verbs and URLs and controller actions. This will also setup mappings for relationship URLs for a resource's associations. For example:
465
560
 
466
561
  ```ruby
467
562
  require 'jsonapi/routing_ext'
@@ -586,13 +681,13 @@ end
586
681
 
587
682
  You can also create your own Value Formatter. Value Formatters must be named with the `format` name followed by `ValueFormatter`, i.e. `DateWithTimezoneValueFormatter` and derive from `JSONAPI::ValueFormatter`. It is recommended that you create a directory for your formatters, called `formatters`.
588
683
 
589
- The `format` method is called by the ResourceSerializer as is serializing a resource. The format method takes the `raw_value`, and `context` parameters. `raw_value` is the value as read from the model, and `context` is the context of the current user/request. From this you can base the formatted version of the attribute current context.
684
+ The `format` method is called by the `ResourceSerializer` as is serializing a resource. The format method takes the `raw_value`, and `context` parameters. `raw_value` is the value as read from the model, and `context` is the context of the current user/request. From this you can base the formatted version of the attribute current context.
590
685
 
591
686
  The `unformat` method is called when processing the request. Each incoming attribute (except `links`) are run through the `unformat` method. The `unformat` method takes the `value`, and `context` parameters. `value` is the value as it comes in on the request, and `context` is the context of the current user/request. This allows you process the incoming value to alter its state before it is stored in the model. By default no processing is applied.
592
687
 
593
688
  ###### Use a Different Default Value Formatter
594
689
 
595
- Another way to handle formatting is to set a different default value formatter. This will affect all attributes that do notw have a `format` set. You can do this by overriding the `default_attribute_options` method for a resource (or a base resource for a system wide change).
690
+ Another way to handle formatting is to set a different default value formatter. This will affect all attributes that do not have a `format` set. You can do this by overriding the `default_attribute_options` method for a resource (or a base resource for a system wide change).
596
691
 
597
692
  ```ruby
598
693
  def default_attribute_options
@@ -623,7 +718,7 @@ This way all DateTime values will be formatted to display in the specified timez
623
718
 
624
719
  #### Key Format
625
720
 
626
- JSONAPI is agnostic on the format of the keys used in the responses. By default JR uses underscored keys which match the attribute names used by rails models. This can be changed by specifying a different key formatter.
721
+ JSONAPI is agnostic on the format of the keys used in the responses. By default JR uses underscored keys which match the attribute names used by Rails models. This can be changed by specifying a different key formatter.
627
722
 
628
723
  For example to use camel cased keys with an initial lowercase character (JSON's default) create an initializer and add the following:
629
724
 
@@ -634,7 +729,7 @@ JSONAPI.configure do |config|
634
729
  end
635
730
  ```
636
731
 
637
- This will cause the serializer to use the CamelizedKeyFormatter. Besides UnderscoredKeyFormatter and CamelizedKeyFormatter JR defines the DasherizedKeyFormatter. You can also create your own KeyFormatter, for example:
732
+ This will cause the serializer to use the `CamelizedKeyFormatter`. Besides `UnderscoredKeyFormatter` and `CamelizedKeyFormatter` JR defines the `DasherizedKeyFormatter`. You can also create your own `KeyFormatter`, for example:
638
733
 
639
734
  ```ruby
640
735
  class UpperCamelizedKeyFormatter < JSONAPI::KeyFormatter
@@ -102,6 +102,15 @@ class DefaultValueFormatter < JSONAPI::ValueFormatter
102
102
  end
103
103
  end
104
104
 
105
+ class IdValueFormatter < JSONAPI::ValueFormatter
106
+ class << self
107
+ def format(raw_value, context)
108
+ return if raw_value.nil?
109
+ raw_value.to_s
110
+ end
111
+ end
112
+ end
113
+
105
114
  class UnderscoredRouteFormatter < JSONAPI::RouteFormatter
106
115
  end
107
116
 
@@ -127,4 +136,4 @@ class DasherizedRouteFormatter < JSONAPI::RouteFormatter
127
136
  formatted_route.to_s.underscore.to_sym
128
137
  end
129
138
  end
130
- end
139
+ end
@@ -20,7 +20,7 @@ module JSONAPI
20
20
  end
21
21
 
22
22
  def setup(params)
23
- @resource_klass ||= self.class.resource_for(params[:controller].split('/').last) if params[:controller]
23
+ @resource_klass ||= self.class.resource_for(params[:controller]) if params[:controller]
24
24
 
25
25
  unless params.nil?
26
26
  case params[:action]
@@ -77,7 +77,7 @@ module JSONAPI
77
77
  underscored_type = unformat_key(type)
78
78
  fields[type] = []
79
79
  begin
80
- type_resource = self.class.resource_for(underscored_type)
80
+ type_resource = self.class.resource_for(@resource_klass.module_path + underscored_type.to_s)
81
81
  rescue NameError
82
82
  @errors.concat(JSONAPI::Exceptions::InvalidResource.new(type).errors)
83
83
  end
@@ -109,7 +109,7 @@ module JSONAPI
109
109
  association = resource_klass._association(association_name)
110
110
  if association
111
111
  unless include_parts.last.empty?
112
- check_include(Resource.resource_for(association.class_name), include_parts.last.partition('.'))
112
+ check_include(Resource.resource_for(@resource_klass.module_path + association.class_name.to_s), include_parts.last.partition('.'))
113
113
  end
114
114
  else
115
115
  @errors.concat(JSONAPI::Exceptions::InvalidInclude.new(format_key(resource_klass._type),
@@ -219,13 +219,13 @@ module JSONAPI
219
219
  association = @resource_klass._association(param)
220
220
 
221
221
  if association.is_a?(JSONAPI::Association::HasOne)
222
- checked_has_one_associations[param] = @resource_klass.resource_for(association.type).verify_key(value, @context)
222
+ checked_has_one_associations[param] = @resource_klass.resource_for(@resource_klass.module_path + association.type.to_s).verify_key(value, @context)
223
223
  elsif association.is_a?(JSONAPI::Association::HasMany)
224
224
  keys = []
225
225
  if value.is_a?(Array)
226
- keys = @resource_klass.resource_for(association.type).verify_keys(value, @context)
226
+ keys = @resource_klass.resource_for(@resource_klass.module_path + association.type.to_s).verify_keys(value, @context)
227
227
  else
228
- keys.push(@resource_klass.resource_for(association.type).verify_key(value, @context))
228
+ keys.push(@resource_klass.resource_for(@resource_klass.module_path + association.type.to_s).verify_key(value, @context))
229
229
  end
230
230
  checked_has_many_associations[param] = keys
231
231
  else
@@ -398,6 +398,10 @@ module JSONAPI
398
398
  _allowed_filters.include?(filter)
399
399
  end
400
400
 
401
+ def module_path
402
+ @module_path ||= self.name =~ /::[^:]+\Z/ ? ($`.freeze.gsub('::', '/') + '/').downcase : ''
403
+ end
404
+
401
405
  private
402
406
 
403
407
  def check_reserved_resource_name(type, name)
@@ -441,8 +445,8 @@ module JSONAPI
441
445
 
442
446
  if @_associations[attr].is_a?(JSONAPI::Association::HasOne)
443
447
  define_method attr do
444
- type_name = self.class._associations[attr].type
445
- resource_class = self.class.resource_for(type_name)
448
+ type_name = self.class._associations[attr].type.to_s
449
+ resource_class = self.class.resource_for(self.class.module_path + type_name)
446
450
  if resource_class
447
451
  associated_model = @model.send attr
448
452
  return associated_model ? resource_class.new(associated_model, @context) : nil
@@ -450,8 +454,8 @@ module JSONAPI
450
454
  end unless method_defined?(attr)
451
455
  elsif @_associations[attr].is_a?(JSONAPI::Association::HasMany)
452
456
  define_method attr do
453
- type_name = self.class._associations[attr].type
454
- resource_class = self.class.resource_for(type_name)
457
+ type_name = self.class._associations[attr].type.to_s
458
+ resource_class = self.class.resource_for(self.class.module_path + type_name)
455
459
  resources = []
456
460
  if resource_class
457
461
  associated_models = @model.send attr
@@ -104,7 +104,7 @@ module JSONAPI
104
104
  # :nocov:
105
105
 
106
106
  def resource_klass_name
107
- @resource_klass_name ||= "#{self.class.name.demodulize.sub(/Controller$/, '').singularize}Resource"
107
+ @resource_klass_name ||= "#{self.class.name.sub(/Controller$/, '').singularize}Resource"
108
108
  end
109
109
 
110
110
  def setup_request
@@ -9,7 +9,7 @@ module JSONAPI
9
9
  if RUBY_VERSION >= '2.0'
10
10
  def resource_for(type)
11
11
  resource_name = JSONAPI::Resource._resource_name_from_type(type)
12
- Object.const_get resource_name if resource_name
12
+ Object.const_get(resource_name, false) if resource_name
13
13
  rescue NameError
14
14
  raise NameError, "JSONAPI: Could not find resource '#{type}'. (Class #{resource_name} not found)"
15
15
  end
@@ -127,9 +127,15 @@ module JSONAPI
127
127
  end
128
128
 
129
129
  fields.each_with_object({}) do |name, hash|
130
- hash[format_key(name)] = format_value(source.send(name),
131
- source.class._attribute_options(name)[:format],
132
- source)
130
+ format = source.class._attribute_options(name)[:format]
131
+ if format == :default && name == :id
132
+ format = 'id'
133
+ end
134
+ hash[format_key(name)] = format_value(
135
+ source.send(name),
136
+ format,
137
+ source
138
+ )
133
139
  end
134
140
  end
135
141
 
@@ -148,10 +154,9 @@ module JSONAPI
148
154
  included_associations = source.fetchable_fields & associations.keys
149
155
  associations.each_with_object({}) do |(name, association), hash|
150
156
  if included_associations.include? name
151
- foreign_key = association.foreign_key
152
157
 
153
158
  if field_set.include?(name)
154
- hash[format_key(name)] = source.send(foreign_key)
159
+ hash[format_key(name)] = foreign_key_value(source, association)
155
160
  end
156
161
 
157
162
  ia = requested_associations.is_a?(Hash) ? requested_associations[name] : nil
@@ -198,6 +203,18 @@ module JSONAPI
198
203
  return @linked_objects.key?(type) && @linked_objects[type].key?(id)
199
204
  end
200
205
 
206
+ # Extracts the foreign key value for an association.
207
+ def foreign_key_value(source, association)
208
+ foreign_key = association.foreign_key
209
+ value = source.send(foreign_key)
210
+
211
+ if association.is_a?(JSONAPI::Association::HasMany)
212
+ value.map { |value| IdValueFormatter.format(value, {}) }
213
+ elsif association.is_a?(JSONAPI::Association::HasOne)
214
+ IdValueFormatter.format(value, {})
215
+ end
216
+ end
217
+
201
218
  # Sets that an object should be included in the primary document of the response.
202
219
  def set_primary(type, id)
203
220
  type = format_key(type)
@@ -1,5 +1,5 @@
1
1
  module JSONAPI
2
2
  module Resources
3
- VERSION = "0.0.14"
3
+ VERSION = "0.0.15"
4
4
  end
5
5
  end