caprese 0.2.3 → 0.3.0

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: 9b539135a9520430f58f763ab6dec137d29b5de5
4
- data.tar.gz: e0159857fd3adc9bfde3a247a59f495176042f47
3
+ metadata.gz: c285f46b32620d9d16fe5b6698098a385f5e163f
4
+ data.tar.gz: 559a81f42af6e01fac7cecec6b8a9b9c8134139b
5
5
  SHA512:
6
- metadata.gz: 91542f3ccf9492a10ace8d7bc258d05573257b71b0258086950d6dd43e3da852dec063c1f637c41e9e6a10b428fcc8aa40892ac8397f1bfe8c53542eb2ecb576
7
- data.tar.gz: 105f5f9d421d3e7e7015f919138fd0be80aefc32d5e3d493e321136a3a41e1d2ecf998e33c2c8cf902012cec20467b8f9df259519eb8ec2452a9e8d20032cbb2
6
+ metadata.gz: 346d727d18ae79c56606c0dc60e5badc09995e0430dff1fff2147a2f85a3cdeeef63a6e1ab0d4f0aea2bcb120ad1470de2d0b04fa60de347e93f11f819b9f53e
7
+ data.tar.gz: f7fe6949fb0241f3a4fec64ad91b3470a55bfffb3bc4131e0ac4690ec53829be8da23d69f11e0699aeb2f96f5e4943c9e2f18f4e5524b044700b6c876f2f3ddf
data/.travis.yml CHANGED
@@ -1,10 +1,11 @@
1
1
  language: ruby
2
2
  cache: bundler
3
3
  rvm:
4
- - 2.1.5
5
4
  - 2.2.2
5
+ - 2.3.1
6
6
  env:
7
- - DB=sqlite
7
+ - "RAILS_VERSION=4.2.7"
8
+ - "RAILS_VERSION=5.0.1"
8
9
  script:
9
10
  - cd spec/dummy
10
11
  - RAILS_ENV=test bundle exec rake db:migrate --trace
data/CHANGELOG.md ADDED
@@ -0,0 +1,8 @@
1
+ ## 0.3.0
2
+
3
+ * Have children controllers inherit callbacks from their parent
4
+ * Have child records without their own serializers inherit their type names from their parent
5
+ * Enforce `Caprese::Record` on all records serialized
6
+ * Allow editing of meta tag document
7
+ * Modify `validates_associated` to propagate nested association errors to the record itself
8
+ * Fail with `422 Unprocessable Entity` if any callbacks add an error to a record being persisted or updated
data/caprese.gemspec CHANGED
@@ -21,8 +21,8 @@ Gem::Specification.new do |spec|
21
21
 
22
22
  spec.required_ruby_version = '>= 2.1'
23
23
 
24
- spec.add_dependency 'active_model_serializers', '~> 0.10.0'
25
- spec.add_dependency 'kaminari'
24
+ spec.add_dependency 'active_model_serializers', '0.10.2'
25
+ spec.add_dependency 'kaminari', '0.17.0'
26
26
  spec.add_dependency 'rails', '>= 4.2.0'
27
27
 
28
28
  spec.add_development_dependency 'bundler'
@@ -3,20 +3,16 @@ module Caprese
3
3
  class JsonApi
4
4
  class ResourceIdentifier
5
5
  def self.type_for(class_name, serializer_type = nil, transform_options = {})
6
- if serializer_type
7
- raw_type = serializer_type
8
- else
9
- inflection =
10
- if ActiveModelSerializers.config.jsonapi_resource_type == :singular
11
- :singularize
12
- else
13
- :pluralize
14
- end
15
-
16
- raw_type = class_name.underscore
17
- raw_type = ActiveSupport::Inflector.public_send(inflection, raw_type)
18
- raw_type
19
- end
6
+ inflection =
7
+ if ActiveModelSerializers.config.jsonapi_resource_type == :singular
8
+ :singularize
9
+ else
10
+ :pluralize
11
+ end
12
+
13
+ raw_type = serializer_type || class_name.underscore
14
+ raw_type = ActiveSupport::Inflector.public_send(inflection, raw_type)
15
+
20
16
  JsonApi.send(:transform_key_casing!, raw_type, transform_options)
21
17
  end
22
18
 
@@ -37,7 +33,7 @@ module Caprese
37
33
  private
38
34
 
39
35
  def type_for(serializer, transform_options)
40
- self.class.type_for(serializer.object.class.name, serializer._type, transform_options)
36
+ self.class.type_for(serializer.object.class.name, serializer.json_key, transform_options)
41
37
  end
42
38
 
43
39
  def id_for(serializer)
@@ -49,10 +49,22 @@ module Caprese
49
49
  #
50
50
  # @param [Symbol,Array<Symbol>] callbacks the name(s) of callbacks to add to list of callbacks
51
51
  define_singleton_method method_name do |*callbacks|
52
- unless (all_callbacks = self.instance_variable_get(instance_variable_name))
53
- all_callbacks = self.instance_variable_set(instance_variable_name, [])
54
- end
52
+ all_callbacks = self.instance_variable_get(instance_variable_name) || []
55
53
  all_callbacks.push *callbacks
54
+ self.instance_variable_set(instance_variable_name, all_callbacks)
55
+ end
56
+ end
57
+ end
58
+
59
+ module ClassMethods
60
+ # Is called when any controller class inherits from a parent controller, copying to the child controller
61
+ # all of the callbacks that have been stored in instance variables on the parent
62
+ #
63
+ # @param [Class] subclass the child class that is to inherit the callbacks
64
+ def inherited(subclass)
65
+ CALLBACKS.each do |method_name|
66
+ instance_variable_name = "@#{method_name}_callbacks"
67
+ subclass.instance_variable_set(instance_variable_name, instance_variable_get(instance_variable_name))
56
68
  end
57
69
  end
58
70
  end
@@ -35,28 +35,19 @@ module Caprese
35
35
  #
36
36
  # @note For this action to succeed, the given controller must define `create_params`
37
37
  # @see #create_params
38
- #
39
- # 1. Check that type of record to be created matches type that the given controller manages
40
- # 2. Build the appropriate attributes/associations for the create action
41
- # 3. Build a record with the attributes
42
- # 4. Execute after_initialize callbacks
43
- # 5. Execute before_create callbacks
44
- # 6. Execute before_save callbacks
45
- # 7. Create the record by saving it (or fail with RecordInvalid and render errors)
46
- # 8. Execute after_create callbacks
47
- # 9. Execute after_save callbacks
48
- # 10. Return the created resource with 204 Created
49
- # @see #rescue_from ActiveRecord::RecordInvalid
50
38
  def create
51
39
  fail_on_type_mismatch(data_params[:type])
52
40
 
53
41
  record = queried_record_scope.build
54
42
  assign_record_attributes(record, permitted_params_for(:create), data_params)
43
+
55
44
  execute_after_initialize_callbacks(record)
56
45
 
57
46
  execute_before_create_callbacks(record)
58
47
  execute_before_save_callbacks(record)
59
48
 
49
+ fail RecordInvalidError.new(record) if record.errors.any?
50
+
60
51
  record.save!
61
52
 
62
53
  execute_after_create_callbacks(record)
@@ -74,22 +65,16 @@ module Caprese
74
65
  #
75
66
  # @note For this action to succeed, the given controller must define `update_params`
76
67
  # @see #update_params
77
- #
78
- # 1. Check that type of record to be updated matches type that the given controller manages
79
- # 2. Execute before_update callbacks
80
- # 3. Execute before_save callbacks
81
- # 4. Update the record (or fail with RecordInvalid and render errors)
82
- # 5. Execute after_update callbacks
83
- # 6. Execute after_save callbacks
84
- # 7. Return the updated resource
85
- # @see #rescue_from ActiveRecord::RecordInvalid
86
68
  def update
87
69
  fail_on_type_mismatch(data_params[:type])
88
70
 
71
+ assign_record_attributes(queried_record, permitted_params_for(:update), data_params)
72
+
89
73
  execute_before_update_callbacks(queried_record)
90
74
  execute_before_save_callbacks(queried_record)
91
75
 
92
- assign_record_attributes(queried_record, permitted_params_for(:update), data_params)
76
+ fail RecordInvalidError.new(queried_record) if queried_record.errors.any?
77
+
93
78
  queried_record.save!
94
79
 
95
80
  execute_after_update_callbacks(queried_record)
@@ -285,21 +270,29 @@ module Caprese
285
270
  # @param [Hash] resource_identifier the resource identifier for the resource
286
271
  # @return [ActiveRecord::Base] the found or built resource for the relationship
287
272
  def record_for_relationship(owner, relationship_name, resource_identifier)
288
- record =
273
+ if resource_identifier[:type]
274
+ # { type: '...', id: '...' }
289
275
  if (id = resource_identifier[Caprese.config.resource_primary_key])
290
276
  get_record!(
291
277
  resource_identifier[:type],
292
278
  Caprese.config.resource_primary_key,
293
279
  id
294
280
  )
281
+
282
+ # { type: '...', attributes: { ... } }
295
283
  elsif contains_constructable_data?(resource_identifier)
296
284
  record_scope(resource_identifier[:type]).build
285
+
286
+ # { type: '...' }
297
287
  else
298
288
  owner.errors.add(relationship_name)
299
289
  nil
300
290
  end
301
-
302
- record
291
+ else
292
+ # { id: '...' } && { attributes: { ... } }
293
+ owner.errors.add(relationship_name)
294
+ nil
295
+ end
303
296
  end
304
297
 
305
298
  # Assigns permitted attributes for a record in a relationship, for a given action
@@ -58,12 +58,14 @@ module Caprese
58
58
  links = { self: request.original_url }
59
59
 
60
60
  if !target.respond_to?(:to_ary) &&
61
- respond_to?(related_url = version_name("#{params[:relationship].singularize}_url"))
61
+ Rails.application.routes.url_helpers
62
+ .respond_to?(related_url = version_name("#{params[:relationship].singularize}_url"))
62
63
 
63
64
  links[:related] =
64
- send(
65
+ Rails.application.routes.url_helpers.send(
65
66
  related_url,
66
- target.read_attribute(self.config.resource_primary_key)
67
+ target.read_attribute(self.config.resource_primary_key),
68
+ host: caprese_default_url_options_host
67
69
  )
68
70
  end
69
71
 
@@ -110,8 +112,15 @@ module Caprese
110
112
  links = { self: request.original_url }
111
113
 
112
114
  # Add related link for this relationship if it exists
113
- if respond_to?(related_path = "relationship_data_#{version_name(unversion(params[:controller]).singularize)}_url")
114
- links[:related] = send(related_path, params[:id], params[:relationship])
115
+ if Rails.application.routes.url_helpers
116
+ .respond_to?(related_path = "relationship_data_#{version_name(unversion(params[:controller]).singularize)}_url")
117
+
118
+ links[:related] = Rails.application.routes.url_helpers.send(
119
+ related_path,
120
+ params[:id],
121
+ params[:relationship],
122
+ host: caprese_default_url_options_host
123
+ )
115
124
  end
116
125
 
117
126
  target = queried_association.reader
@@ -246,5 +255,13 @@ module Caprese
246
255
 
247
256
  @queried_association
248
257
  end
258
+
259
+ # Fetches the host from Caprese.config.default_url_options or fails if it is not set
260
+ # @note default_url_options[:host] is used to render the host in links that are serialized in the response
261
+ def caprese_default_url_options_host
262
+ Caprese.config.default_url_options.fetch(:host) do
263
+ fail 'Caprese requires that config.default_url_options[:host] be set when rendering links.'
264
+ end
265
+ end
249
266
  end
250
267
  end
@@ -9,6 +9,7 @@ module Caprese
9
9
  # instead of requiring that they be explicity stated
10
10
  def render(options = {})
11
11
  options[:adapter] = Caprese::Adapter::JsonApi
12
+ options[:meta] = meta unless meta.empty?
12
13
 
13
14
  if options[:json].respond_to?(:to_ary)
14
15
  if options[:json].first.is_a?(Error)
@@ -27,6 +28,16 @@ module Caprese
27
28
  super
28
29
  end
29
30
 
31
+ # Allows for meta tags to be added in response document
32
+ #
33
+ # @example
34
+ # meta[:redirect_url] = ...
35
+ #
36
+ # @return [Hash] the meta tag object
37
+ def meta
38
+ @caprese_meta ||= {}
39
+ end
40
+
30
41
  private
31
42
 
32
43
  # Finds a versioned serializer for a given resource
@@ -34,10 +45,15 @@ module Caprese
34
45
  # @example
35
46
  # serializer_for(post) => API::V1::PostSerializer
36
47
  #
48
+ # @note The reason this method is a duplicate of Caprese::Serializer.serializer_for is
49
+ # because the latter is only ever called from children of Caprese::Serializer, like those
50
+ # in the API::V1:: scope. If we tried to use that method instead of this one, we end up
51
+ # with Caprese::[record.class.name]Serializer instead of the proper versioned serializer
52
+ #
37
53
  # @param [ActiveRecord::Base] record the record to find a serializer for
38
54
  # @return [Serializer,Nil] the serializer for the given record
39
55
  def serializer_for(record)
40
- version_module("#{record.class.name}Serializer").constantize
56
+ version_module("#{record.class.name}Serializer").constantize if Serializer.valid_for_serialization(record)
41
57
  end
42
58
  end
43
59
  end
@@ -0,0 +1,33 @@
1
+ module Caprese
2
+ module Record
3
+ # Formats nested errors on associations in a more useful way than Rails alone
4
+ #
5
+ # @note BEFORE
6
+ # POST /posts (with invalid resources) =>
7
+ # [
8
+ # { "key"=>"invalid", "field"=>"attachment", "message"=>"Attachment is invalid." }
9
+ # ]
10
+ #
11
+ # @note AFTER
12
+ # POST /posts (with invalid resources) =>
13
+ # [
14
+ # { "key"=>"not_found", "field"=>"attachment.file", "message"=>"Could not find a file at ..."}
15
+ # ]
16
+ class AssociatedValidator < ActiveModel::EachValidator
17
+ def validate_each(record, attribute, value)
18
+ Array(value).reject { |r| r.marked_for_destruction? || r.valid? }.each do |invalid_record|
19
+ invalid_record.errors.to_a.each do |error|
20
+ field_name = error.field ? "#{attribute}.#{error.field}" : attribute
21
+ record.errors.add(field_name, error.code, options.merge(value: invalid_record))
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ module ClassMethods
28
+ def validates_associated(*attr_names)
29
+ validates_with AssociatedValidator, _merge_attributes(attr_names)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -2,6 +2,7 @@ require 'active_model'
2
2
  require 'active_support/concern'
3
3
  require 'active_support/dependencies'
4
4
  require 'caprese/errors'
5
+ require 'caprese/record/associated_validator'
5
6
  require 'caprese/record/errors'
6
7
 
7
8
  module Caprese
@@ -12,8 +12,14 @@ module Caprese
12
12
  # object = Order<@token='asd27h'>
13
13
  # links = { self: '/api/v1/orders/asd27hß' }
14
14
  link :self do
15
- if respond_to?(url = serializer.version_name("#{object.class.name.underscore}_url"))
16
- send(url, object.read_attribute(Caprese.config.resource_primary_key))
15
+ if Rails.application.routes.url_helpers
16
+ .respond_to?(url = serializer.version_name("#{object.class.name.underscore}_url"))
17
+
18
+ Rails.application.routes.url_helpers.send(
19
+ url,
20
+ object.read_attribute(Caprese.config.resource_primary_key),
21
+ host: serializer.send(:caprese_default_url_options_host)
22
+ )
17
23
  end
18
24
  end
19
25
  end
@@ -67,22 +73,24 @@ module Caprese
67
73
  Proc.new do |serializer|
68
74
  link :self do
69
75
  url = "relationship_definition_#{serializer.version_name("#{object.class.name.underscore}_url")}"
70
- if respond_to? url
71
- send(
76
+ if Rails.application.routes.url_helpers.respond_to? url
77
+ Rails.application.routes.url_helpers.send(
72
78
  url,
73
79
  primary_key => object.read_attribute(primary_key),
74
- relationship: reflection_name
80
+ relationship: reflection_name,
81
+ host: serializer.send(:caprese_default_url_options_host)
75
82
  )
76
83
  end
77
84
  end
78
85
 
79
86
  link :related do
80
87
  url = "relationship_data_#{serializer.version_name("#{object.class.name.underscore}_url")}"
81
- if respond_to? url
82
- send(
88
+ if Rails.application.routes.url_helpers.respond_to? url
89
+ Rails.application.routes.url_helpers.send(
83
90
  url,
84
91
  primary_key => object.read_attribute(primary_key),
85
- relationship: reflection_name
92
+ relationship: reflection_name,
93
+ host: serializer.send(:caprese_default_url_options_host)
86
94
  )
87
95
  end
88
96
  end
@@ -91,6 +99,16 @@ module Caprese
91
99
  end
92
100
  end
93
101
  end
102
+
103
+ private
104
+
105
+ # Fetches the host from Caprese.config.default_url_options or fails if it is not set
106
+ # @note default_url_options[:host] is used to render the host in links that are serialized in the response
107
+ def caprese_default_url_options_host
108
+ Caprese.config.default_url_options.fetch(:host) do
109
+ fail 'Caprese requires that config.default_url_options[:host] be set when rendering links.'
110
+ end
111
+ end
94
112
  end
95
113
  end
96
114
  end
@@ -14,7 +14,23 @@ module Caprese
14
14
  # @param [Hash] options options for `super` to use when getting the serializer
15
15
  # @return [Serializer,Nil] the serializer for the given record
16
16
  def serializer_for(record, options = {})
17
- get_serializer_for(record.class) || super
17
+ return ActiveModel::Serializer::CollectionSerializer if record.respond_to?(:to_ary)
18
+
19
+ get_serializer_for(record.class) if valid_for_serialization(record)
20
+ end
21
+
22
+ # Indicates whether or not the record specified can be serialized by Caprese
23
+ #
24
+ # @note The only requirement right now is that the record model has Caprese::Record included
25
+ #
26
+ # @param [Object] record the record to check if is valid for serialization
27
+ # @return [True] this method either returns true, or fails - breaking control flow
28
+ def valid_for_serialization(record)
29
+ if record && !record.class.included_modules.include?(Caprese::Record)
30
+ fail 'All models managed by Caprese must include Caprese::Record'
31
+ end
32
+
33
+ true
18
34
  end
19
35
 
20
36
  private
@@ -9,5 +9,9 @@ module Caprese
9
9
  include Versioning
10
10
  include Links
11
11
  include Lookup
12
+
13
+ def json_key
14
+ unversion(self.class.name).gsub('Serializer', '').underscore
15
+ end
12
16
  end
13
17
  end
@@ -1,3 +1,3 @@
1
1
  module Caprese
2
- VERSION = '0.2.3'
2
+ VERSION = '0.3.0'
3
3
  end
data/lib/caprese.rb CHANGED
@@ -23,7 +23,7 @@ module Caprese
23
23
  config.only_path_links ||= true
24
24
 
25
25
  # If true, relationship data will not be serialized unless it is in `include`
26
- config.optimize_relationships ||= true
26
+ config.optimize_relationships ||= false
27
27
 
28
28
  # Defines the translation scope for model and controller errors
29
29
  config.i18n_scope ||= '' # 'api.v1.errors'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: caprese
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nick Landgrebe
@@ -10,36 +10,36 @@ authors:
10
10
  autorequire:
11
11
  bindir: exe
12
12
  cert_chain: []
13
- date: 2016-10-18 00:00:00.000000000 Z
13
+ date: 2017-02-01 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: active_model_serializers
17
17
  requirement: !ruby/object:Gem::Requirement
18
18
  requirements:
19
- - - "~>"
19
+ - - '='
20
20
  - !ruby/object:Gem::Version
21
- version: 0.10.0
21
+ version: 0.10.2
22
22
  type: :runtime
23
23
  prerelease: false
24
24
  version_requirements: !ruby/object:Gem::Requirement
25
25
  requirements:
26
- - - "~>"
26
+ - - '='
27
27
  - !ruby/object:Gem::Version
28
- version: 0.10.0
28
+ version: 0.10.2
29
29
  - !ruby/object:Gem::Dependency
30
30
  name: kaminari
31
31
  requirement: !ruby/object:Gem::Requirement
32
32
  requirements:
33
- - - ">="
33
+ - - '='
34
34
  - !ruby/object:Gem::Version
35
- version: '0'
35
+ version: 0.17.0
36
36
  type: :runtime
37
37
  prerelease: false
38
38
  version_requirements: !ruby/object:Gem::Requirement
39
39
  requirements:
40
- - - ">="
40
+ - - '='
41
41
  - !ruby/object:Gem::Version
42
- version: '0'
42
+ version: 0.17.0
43
43
  - !ruby/object:Gem::Dependency
44
44
  name: rails
45
45
  requirement: !ruby/object:Gem::Requirement
@@ -204,6 +204,7 @@ files:
204
204
  - ".coveralls.yml"
205
205
  - ".gitignore"
206
206
  - ".travis.yml"
207
+ - CHANGELOG.md
207
208
  - Gemfile
208
209
  - LICENSE.txt
209
210
  - README.md
@@ -233,6 +234,7 @@ files:
233
234
  - lib/caprese/error.rb
234
235
  - lib/caprese/errors.rb
235
236
  - lib/caprese/record.rb
237
+ - lib/caprese/record/associated_validator.rb
236
238
  - lib/caprese/record/errors.rb
237
239
  - lib/caprese/routing/caprese_resources.rb
238
240
  - lib/caprese/serializer.rb