caprese 0.2.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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