caprese 0.2.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.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.coveralls.yml +1 -0
  3. data/.gitignore +12 -0
  4. data/.travis.yml +18 -0
  5. data/Gemfile +3 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +41 -0
  8. data/Rakefile +8 -0
  9. data/bin/console +14 -0
  10. data/bin/setup +8 -0
  11. data/caprese.gemspec +38 -0
  12. data/lib/caprese/adapter/json_api/error.rb +123 -0
  13. data/lib/caprese/adapter/json_api/json_pointer.rb +52 -0
  14. data/lib/caprese/adapter/json_api/jsonapi.rb +49 -0
  15. data/lib/caprese/adapter/json_api/link.rb +88 -0
  16. data/lib/caprese/adapter/json_api/meta.rb +37 -0
  17. data/lib/caprese/adapter/json_api/pagination_links.rb +69 -0
  18. data/lib/caprese/adapter/json_api/relationship.rb +65 -0
  19. data/lib/caprese/adapter/json_api/resource_identifier.rb +49 -0
  20. data/lib/caprese/adapter/json_api.rb +509 -0
  21. data/lib/caprese/concerns/versioning.rb +69 -0
  22. data/lib/caprese/controller/concerns/callbacks.rb +60 -0
  23. data/lib/caprese/controller/concerns/errors.rb +42 -0
  24. data/lib/caprese/controller/concerns/persistence.rb +327 -0
  25. data/lib/caprese/controller/concerns/query.rb +209 -0
  26. data/lib/caprese/controller/concerns/relationships.rb +250 -0
  27. data/lib/caprese/controller/concerns/rendering.rb +43 -0
  28. data/lib/caprese/controller/concerns/typing.rb +39 -0
  29. data/lib/caprese/controller.rb +26 -0
  30. data/lib/caprese/error.rb +121 -0
  31. data/lib/caprese/errors.rb +69 -0
  32. data/lib/caprese/record/errors.rb +82 -0
  33. data/lib/caprese/record.rb +19 -0
  34. data/lib/caprese/routing/caprese_resources.rb +27 -0
  35. data/lib/caprese/serializer/concerns/links.rb +96 -0
  36. data/lib/caprese/serializer/concerns/lookup.rb +37 -0
  37. data/lib/caprese/serializer/error_serializer.rb +13 -0
  38. data/lib/caprese/serializer.rb +13 -0
  39. data/lib/caprese/version.rb +3 -0
  40. data/lib/caprese.rb +35 -0
  41. metadata +273 -0
@@ -0,0 +1,250 @@
1
+ require 'active_support/concern'
2
+ require 'caprese/errors'
3
+
4
+ module Caprese
5
+ module Relationships
6
+ extend ActiveSupport::Concern
7
+
8
+ # Applies further scopes to a collection association
9
+ # @note Can be overridden to customize scoping at a per-relationship level
10
+ #
11
+ # @example
12
+ # def relationship_scope(name, scope)
13
+ # case name
14
+ # when :transactions
15
+ # scope.by_merchant(...)
16
+ # when :orders
17
+ # scope.by_user(...)
18
+ # end
19
+ # end
20
+ #
21
+ # @param [String] name the name of the association
22
+ # @param [Relation] scope the scope corresponding to a collection association
23
+ def relationship_scope(name, scope)
24
+ scope
25
+ end
26
+
27
+ # Retrieves the data for a relationship, not just the definition/resource identifier
28
+ # @note Resource Identifier = { id: '...', type: '....' }
29
+ # @note Resource = Resource Identifier + { attributes: { ... } }
30
+ #
31
+ # @note Adds a links[:self] link to this endpoint itself, to be JSON API compliant
32
+ # @note When returning single resource, adds a related endpoint URL that points to
33
+ # the root resource URL
34
+ #
35
+ # @example Order<token: 'asd27h'> with product
36
+ # links[:self] = 'http://www.example.com/api/v1/orders/asd27h/product'
37
+ # links[:related] = 'http://www.example.com/api/v1/products/h45sql'
38
+ #
39
+ # @example Order<token: 'asd27h'> with transactions
40
+ # links[:self] = 'http://www.example.com/api/v1/orders/asd27h/transactions/7ytr4l'
41
+ # links[:related] = 'http://www.example.com/api/v1/transactions/7ytr4l'
42
+ #
43
+ # GET /api/v1/:controller/:id/:relationship(/:relation_primary_key_value)
44
+ def get_relationship_data
45
+ target =
46
+ if queried_association.reflection.collection?
47
+ scope = relationship_scope(params[:relationship], queried_association.reader)
48
+
49
+ if params[:relation_primary_key_value].present?
50
+ get_record!(scope, self.class.config.resource_primary_key, params[:relation_primary_key_value])
51
+ else
52
+ apply_sorting_pagination_to_scope(scope)
53
+ end
54
+ else
55
+ queried_association.reader
56
+ end
57
+
58
+ links = { self: request.original_url }
59
+
60
+ if !target.respond_to?(:to_ary) &&
61
+ respond_to?(related_url = version_name("#{params[:relationship].singularize}_url"))
62
+
63
+ links[:related] =
64
+ send(
65
+ related_url,
66
+ target.read_attribute(self.config.resource_primary_key)
67
+ )
68
+ end
69
+
70
+ render json: target, links: links
71
+ end
72
+
73
+ # Returns relationship data for a resource
74
+ #
75
+ # 1. Find resource we are updating relationship for
76
+ # 2. Check relationship exists *or respond with error*
77
+ # 3. Add self/related links for relationship
78
+ # 4. Respond with relationship data
79
+ #
80
+ # @example to-one relationship
81
+ # GET /orders/asd27h/relationships/product
82
+ #
83
+ # {
84
+ # "links": {
85
+ # "self": "/orders/asd27h/relationships/product",
86
+ # "related": "orders/asd27h/product"
87
+ # },
88
+ # "data": {
89
+ # "type": "products",
90
+ # "token": "hy7sql"
91
+ # }
92
+ # }
93
+ #
94
+ # @example to-many relationship
95
+ # GET /orders/1/relationships/transactions
96
+ #
97
+ # {
98
+ # "links": {
99
+ # "self": "/orders/asd27h/relationships/transactions",
100
+ # "related": "orders/asd27h/transactions"
101
+ # },
102
+ # "data": [
103
+ # { "type": "transactions", "token": "hy7sql" },
104
+ # { "type": "transactions", "token": "lki26s" },
105
+ # ]
106
+ # }
107
+ #
108
+ # GET /api/v1/:controller/:id/relationships/:relationship
109
+ def get_relationship_definition
110
+ links = { self: request.original_url }
111
+
112
+ # 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
+ end
116
+
117
+ target = queried_association.reader
118
+
119
+ if queried_association.reflection.collection?
120
+ target = relationship_scope(params[:relationship], target)
121
+ end
122
+
123
+ render json: target, links: links, fields: {}
124
+ end
125
+
126
+ # Updates a relationship for a resource
127
+ #
128
+ # 1. Find resource we are updating relationship for
129
+ # 2. Check relationship exists *or respond with error*
130
+ # 3. Find each potential relationship resource corresponding to the resource identifiers passed in
131
+ # 4. Modify relationship based on relationship type (one-to-many, one-to-one) and HTTP verb (PATCH, POST, DELETE)
132
+ # 5. Check if update was successful
133
+ # * If successful, return 204 No Content
134
+ # * If unsuccessful, return 403 Forbidden
135
+ #
136
+ # @example modify to-one relationship
137
+ # PATCH /orders/asd27h/relationships/product
138
+ #
139
+ # {
140
+ # "data": { "type": "products", "token": "hy7sql" }
141
+ # }
142
+ #
143
+ # @example clear to-one relationship
144
+ # PATCH /orders/asd27h/relationships/product
145
+ #
146
+ # {
147
+ # "data": null
148
+ # }
149
+ #
150
+ # @example modify to-many relationship
151
+ # PATCH /orders/asd27h/relationships/transactions
152
+ #
153
+ # {
154
+ # "data": [
155
+ # { "type": "transactions", "token": "hy7sql" },
156
+ # { "type": "transactions", "token": "lki26s" },
157
+ # ]
158
+ # }
159
+ #
160
+ # @example clear to to-many relationship
161
+ # PATCH /orders/asd27h/relationships/transactions
162
+ #
163
+ # {
164
+ # "data": []
165
+ # }
166
+ #
167
+ # @example append to to-many relationship
168
+ # POST /orders/asd27h/relationships/transactions
169
+ #
170
+ # {
171
+ # "data": [
172
+ # { "type": "transactions", "token": "hy7sql" },
173
+ # { "type": "transactions", "token": "lki26s" },
174
+ # ]
175
+ # }
176
+ #
177
+ # @example remove from to-many relationship
178
+ # DELETE /orders/asd27h/relationships/transactions
179
+ #
180
+ # {
181
+ # "data": [
182
+ # { "type": "transactions", "token": "hy7sql" },
183
+ # { "type": "transactions", "token": "lki26s" },
184
+ # ]
185
+ # }
186
+ #
187
+ # PATCH/POST/DELETE /api/v1/:controller/:id/relationships/:relationship
188
+ def update_relationship_definition
189
+ if queried_association &&
190
+ flattened_keys_for(permitted_params_for(:update)).include?(params[:relationship].to_sym)
191
+
192
+ relationship_resources =
193
+ Array.wrap(params[:data]).map do |resource_identifier|
194
+ get_record!(
195
+ resource_identifier[:type],
196
+ column = self.config.resource_primary_key,
197
+ resource_identifier[column]
198
+ )
199
+ end
200
+
201
+ successful =
202
+ case queried_association.reflection.macro
203
+ when :has_many
204
+ if request.patch?
205
+ queried_record.send("#{params[:relationship]}=", relationship_resources)
206
+ elsif request.post?
207
+ queried_record.send(params[:relationship]).push relationship_resources
208
+ elsif request.delete?
209
+ queried_record.send(params[:relationship]).delete relationship_resources
210
+ end
211
+ when :has_one
212
+ if request.patch?
213
+ queried_record.send("#{params[:relationship]}=", relationship_resources[0])
214
+ objects[0].save
215
+ end
216
+ when :belongs_to
217
+ if request.patch?
218
+ queried_record.send("#{params[:relationship]}=", relationship_resources[0])
219
+ queried_record.save
220
+ end
221
+ end
222
+ else
223
+ successful = false
224
+ end
225
+
226
+ if successful
227
+ head :no_content
228
+ else
229
+ fail ActionForbiddenError.new
230
+ end
231
+ end
232
+
233
+ private
234
+
235
+ # Gets the association queried by the relationship call
236
+ #
237
+ # @note Fails with 404 Not Found if association cannot be found
238
+ def queried_association
239
+ unless @queried_association
240
+ begin
241
+ @queried_association = queried_record.association(params[:relationship])
242
+ rescue ActiveRecord::AssociationNotFoundError => e
243
+ fail AssociationNotFoundError.new(params[:relationship])
244
+ end
245
+ end
246
+
247
+ @queried_association
248
+ end
249
+ end
250
+ end
@@ -0,0 +1,43 @@
1
+ require 'active_support/concern'
2
+ require 'caprese/adapter/json_api'
3
+
4
+ module Caprese
5
+ module Rendering
6
+ extend ActiveSupport::Concern
7
+
8
+ # Override render so we can automatically use our adapter and find the appropriate serializer
9
+ # instead of requiring that they be explicity stated
10
+ def render(options = {})
11
+ options[:adapter] = Caprese::Adapter::JsonApi
12
+
13
+ if options[:json].respond_to?(:to_ary)
14
+ if options[:json].first.is_a?(Error)
15
+ options[:each_serializer] ||= Serializer::ErrorSerializer
16
+ elsif options[:json].any?
17
+ options[:each_serializer] ||= serializer_for(options[:json].first)
18
+ end
19
+ else
20
+ if options[:json].is_a?(Error)
21
+ options[:serializer] ||= Serializer::ErrorSerializer
22
+ elsif options[:json].present?
23
+ options[:serializer] ||= serializer_for(options[:json])
24
+ end
25
+ end
26
+
27
+ super
28
+ end
29
+
30
+ private
31
+
32
+ # Finds a versioned serializer for a given resource
33
+ #
34
+ # @example
35
+ # serializer_for(post) => API::V1::PostSerializer
36
+ #
37
+ # @param [ActiveRecord::Base] record the record to find a serializer for
38
+ # @return [Serializer,Nil] the serializer for the given record
39
+ def serializer_for(record)
40
+ version_module("#{record.class.name}Serializer").constantize
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,39 @@
1
+ require 'active_support/concern'
2
+ require 'caprese/errors'
3
+
4
+ # Manages type determination and checking for a given controller
5
+ module Caprese
6
+ module Typing
7
+ extend ActiveSupport::Concern
8
+
9
+ # Gets the class for a record type
10
+ # @note "record type" can be plural, singular, or classified
11
+ # i.e. 'orders', 'order', or 'Order'
12
+ #
13
+ # @example
14
+ # record_class(:orders) # => Order
15
+ #
16
+ # @param [Symbol] type the record type to get the class for
17
+ # @return [Class] the class for a given record type
18
+ def record_class(type)
19
+ type.to_s.classify.constantize
20
+ end
21
+
22
+ # Gets the record class for the current controller
23
+ #
24
+ # @return [Class] the record class for the current controller
25
+ def controller_record_class
26
+ record_class(unversion(params[:controller]))
27
+ end
28
+
29
+ # Checks if a given type mismatches the controller type
30
+ # @note Throws :invalid_type error if true
31
+ #
32
+ # @param [String] type the pluralized type to check ('products','orders',etc.)
33
+ def fail_on_type_mismatch(type)
34
+ unless record_class(type) == controller_record_class
35
+ fail InvalidTypeError.new(type)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,26 @@
1
+ require 'action_controller'
2
+ require 'active_support/configurable'
3
+ require 'caprese/concerns/versioning'
4
+ require 'caprese/controller/concerns/callbacks'
5
+ require 'caprese/controller/concerns/errors'
6
+ require 'caprese/controller/concerns/persistence'
7
+ require 'caprese/controller/concerns/query'
8
+ require 'caprese/controller/concerns/relationships'
9
+ require 'caprese/controller/concerns/rendering'
10
+ require 'caprese/controller/concerns/typing'
11
+
12
+ module Caprese
13
+ # TODO: Convert to ActionController::API with Rails 5
14
+ class Controller < ActionController::Base
15
+ include ActiveSupport::Configurable
16
+ include Callbacks
17
+ include Errors
18
+ include Persistence
19
+ include Query
20
+ include Relationships
21
+ include Rendering
22
+ include Typing
23
+ include Versioning
24
+ extend Versioning
25
+ end
26
+ end
@@ -0,0 +1,121 @@
1
+ # TODO: Remove in favor of Rails 5 error details and dynamically setting i18n_scope
2
+ module Caprese
3
+ class Error < StandardError
4
+ attr_reader :field
5
+ attr_reader :code
6
+
7
+ attr_reader :header
8
+
9
+ # Initializes a new error
10
+ #
11
+ # @param [Symbol] model a symbol representing the model that the error occurred on
12
+ # @param [String] controller the name of the controller the error occurred in
13
+ # @param [String] action the name of the controller action the error occurred in
14
+ # @param [Symbol,String] field a symbol or string representing the field (model attribute or controller param) that the error occurred on
15
+ # if Symbol, a shallow field name. EX: :password
16
+ # if String, a nested field name. EX: 'order_items.amount'
17
+ # @param [Symbol] code the error code
18
+ # @param [Hash] t the interpolation variables to supply to I18n.t when creating the full error message
19
+ def initialize(model: nil, controller: nil, action: nil, field: nil, code: :invalid, t: {})
20
+ @model = model
21
+
22
+ @controller = controller
23
+ @action = action
24
+
25
+ @field = field
26
+ @code = code
27
+
28
+ @t = t
29
+
30
+ @header = { status: :bad_request }
31
+ end
32
+
33
+ # @return [String] The scope to look for I18n translations in
34
+ def i18n_scope
35
+ Caprese.config.i18n_scope
36
+ end
37
+
38
+ # The full error message based on the different attributes we initialized the error with
39
+ #
40
+ # @return [String] the full error message
41
+ def full_message
42
+ if @model
43
+ if field
44
+ if i18n_set? "#{i18n_scope}.models.#{@model}.#{field}.#{code}", t
45
+ I18n.t("#{i18n_scope}.models.#{@model}.#{field}.#{code}", t)
46
+ elsif i18n_set?("#{i18n_scope}.field.#{code}", t)
47
+ I18n.t("#{i18n_scope}.field.#{code}", t)
48
+ else
49
+ I18n.t("#{i18n_scope}.#{code}", t)
50
+ end
51
+ else
52
+ if i18n_set? "#{i18n_scope}.models.#{@model}.#{code}", t
53
+ I18n.t("#{i18n_scope}.models.#{@model}.#{code}", t)
54
+ elsif i18n_set? "#{i18n_scope}.#{code}", t
55
+ I18n.t("#{i18n_scope}.#{code}", t)
56
+ else
57
+ code.to_s
58
+ end
59
+ end
60
+ elsif @controller && @action
61
+ if field && i18n_set?("#{i18n_scope}.controllers.#{@controller}.#{@action}.#{field}.#{code}", t)
62
+ I18n.t("#{i18n_scope}.controllers.#{@controller}.#{@action}.#{field}.#{code}", t)
63
+ elsif i18n_set?("#{i18n_scope}.controllers.#{@controller}.#{@action}.#{code}", t)
64
+ I18n.t("#{i18n_scope}.controllers.#{@controller}.#{@action}.#{code}", t)
65
+ else
66
+ I18n.t("#{i18n_scope}.#{code}", t)
67
+ end
68
+ elsif field && i18n_set?("#{i18n_scope}.field.#{code}", t)
69
+ I18n.t("#{i18n_scope}.field.#{code}", t)
70
+ elsif i18n_set? "#{i18n_scope}.#{code}", t
71
+ I18n.t("#{i18n_scope}.#{code}", t)
72
+ else
73
+ code.to_s
74
+ end
75
+ end
76
+ alias_method :message, :full_message
77
+
78
+ # Allows us to add to the response header when we are failing
79
+ #
80
+ # @note Should be used as such: fail Error.new(...).with_headers(...)
81
+ #
82
+ # @param [Hash] headers the headers to supply in the error response
83
+ # @option [Symbol] status the HTTP status code to return
84
+ # @option [String, Path] location the value for the Location header, useful for redirects
85
+ def with_header(header = {})
86
+ @header = header
87
+ @header[:status] ||= :bad_request
88
+ self
89
+ end
90
+
91
+ # Creates a serializable hash for the error so we can serialize and return it
92
+ #
93
+ # @return [Hash] the serializable hash of the error
94
+ def as_json
95
+ {
96
+ code: code,
97
+ field: field,
98
+ message: full_message
99
+ }
100
+ end
101
+
102
+ private
103
+
104
+ # Adds field and capitalized Field title to the translation params for every error
105
+ #
106
+ # @return [Hash] the full translation params of the error
107
+ def t
108
+ @t.merge(field: @field, field_title: @field.to_s.titleize)
109
+ end
110
+
111
+ # Checks whether or not a translation exists
112
+ #
113
+ # @param [String] key the I18n translation key
114
+ # @param [Hash] params any params to pass into the translations so we only raise NotFound
115
+ # exception we're looking for missing params would cause this to return false improperly
116
+ # @return [Boolean] whether or not the translation exists in I18n
117
+ def i18n_set?(key, params = {})
118
+ I18n.t key, params, :raise => true rescue false
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,69 @@
1
+ require 'caprese/error'
2
+
3
+ module Caprese
4
+ # Thrown when a record was attempted to be persisted and was invalidated
5
+ #
6
+ # @param [ActiveRecord::Base] record the record that is invalid
7
+ class RecordInvalidError < Error
8
+ attr_reader :record
9
+
10
+ def initialize(record)
11
+ super()
12
+ @record = record
13
+ @header = { status: :unprocessable_entity }
14
+ end
15
+
16
+ def as_json
17
+ record.errors.as_json
18
+ end
19
+ end
20
+
21
+ # Thrown when a record could not be found
22
+ #
23
+ # @param [Symbol] field the field that we searched with
24
+ # @param [String] model the name of the model we searched for a record of
25
+ # @param [Value] value the value we searched for a match with
26
+ class RecordNotFoundError < Error
27
+ def initialize(field: :id, model: nil, value: nil)
28
+ super field: field, code: :not_found, t: { model: model, value: value }
29
+ @header = { status: :not_found }
30
+ end
31
+ end
32
+
33
+ # Thrown when an association was not found when calling `record.association()`
34
+ #
35
+ # @param [String] name the name of the association
36
+ class AssociationNotFoundError < Error
37
+ def initialize(name)
38
+ super field: name, code: :association_not_found
39
+ @header = { status: :not_found }
40
+ end
41
+ end
42
+
43
+ # Thrown when an action that is forbidden was attempted
44
+ class ActionForbiddenError < Error
45
+ def initialize
46
+ super code: :forbidden
47
+ @header = { status: :forbidden }
48
+ end
49
+ end
50
+
51
+ # Thrown when an attempt was made to delete a record, but the record could not be deleted
52
+ # because of restrictions
53
+ #
54
+ # @param [String] reason the reason for the restriction
55
+ class DeleteRestrictedError < Error
56
+ def initialize(reason)
57
+ super code: :delete_restricted, t: { reason: reason }
58
+ @header = { status: :forbidden }
59
+ end
60
+ end
61
+
62
+ # Thrown when an attempt was made to create a record of type that is different than the type
63
+ # of the controller that the data was sent to
64
+ class InvalidTypeError < Error
65
+ def initialize(type)
66
+ super code: :invalid_type, t: { type: type }
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,82 @@
1
+ require 'active_model'
2
+
3
+ module Caprese
4
+ module Record
5
+ class Errors < ActiveModel::Errors
6
+ # Adds an error object for a field, with a code, to the messages hash
7
+ #
8
+ # @param [Symbol] attribute the attribute of the model that this error applies to
9
+ # @param [Symbol] code the error code for the attribute
10
+ # @option [Exception,Boolean] strict raise an exception when creating an error
11
+ # if Exception, raises that exception
12
+ # if Boolean `true`, raises ActiveModel::StrictValidationFailed
13
+ # @option [Hash] t interpolation variables to add into the translated full message
14
+ def add(attribute, code = :invalid, options = {})
15
+ options = options.dup
16
+ options[:t] ||= {}
17
+ options[:t][:count] = options[:count]
18
+ options[:t][:value] =
19
+ if attribute != :base && @base.respond_to?(attribute)
20
+ @base.send(:read_attribute_for_validation, attribute)
21
+ else
22
+ nil
23
+ end
24
+
25
+ e = Error.new(
26
+ model: @base.class.name.underscore.downcase,
27
+ field: attribute == :base ? nil : attribute,
28
+ code: code,
29
+ t: options[:t]
30
+ )
31
+
32
+ if (exception = options[:strict])
33
+ exception = ActiveModel::StrictValidationFailed if exception == true
34
+ raise exception, e.full_message
35
+ end
36
+
37
+ self[attribute] << e
38
+ end
39
+
40
+ # @return [Boolean] true if the model has no errors
41
+ def empty?
42
+ all? { |k,v| !v }
43
+ end
44
+
45
+ # Returns the full error messages for each error in the model
46
+ # @note Overriden because original renders full_messages using internal helpers of self instead of error
47
+ # @return [Hash] a hash mapped by attribute, with each value being an array of full messages for that attribute of the model
48
+ def full_messages
49
+ map { |_, e| e.full_message }
50
+ end
51
+
52
+ # @param attribute [Symbol] an attribute in the model
53
+ # @note Overriden because original renders full_messages using internal helpers of self instead of error
54
+ # @return [Array] an array of full error messages for a given attribute of the model
55
+ def full_messages_for(attribute)
56
+ (get(attribute) || []).map { |e| e.full_message }
57
+ end
58
+
59
+ # @note Overriden because traditionally to_a is an alias for `full_messages`, because in Rails standard there is
60
+ # no difference between an error and a full message, an error is that full message. With our API, an error is a
61
+ # model that can render full messages, but it is still a distinct model. `to_a` thus has a different meaning here
62
+ # than in Rails standard.
63
+ # @return [Array] an array of all errors in the model
64
+ def to_a
65
+ values.flatten
66
+ end
67
+
68
+ # We have not and will not ever implement an XML rendering of these errors
69
+ def to_xml(options = {})
70
+ raise NotImplementedError
71
+ end
72
+
73
+ def as_json
74
+ Hash[
75
+ map do |field, errors|
76
+ [field, Array.wrap(errors).map { |e| e.as_json }]
77
+ end
78
+ ]
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,19 @@
1
+ require 'active_model'
2
+ require 'active_support/concern'
3
+ require 'active_support/dependencies'
4
+ require 'caprese/errors'
5
+ require 'caprese/record/errors'
6
+
7
+ module Caprese
8
+ module Record
9
+ extend ActiveSupport::Concern
10
+
11
+ mattr_accessor :caprese_style_errors
12
+ @@caprese_style_errors = true
13
+
14
+ # @return [Errors] a cached instance of the model errors class
15
+ def errors
16
+ @errors ||= (Caprese::Record.caprese_style_errors ? Caprese::Record : ActiveModel)::Errors.new(self)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,27 @@
1
+ require 'action_dispatch/routing/mapper'
2
+
3
+ class ActionDispatch::Routing::Mapper
4
+ def caprese_resources(*resources, &block)
5
+ options = resources.extract_options!
6
+
7
+ resources.each do |r|
8
+ resources r, only: [:index, :show, :create, :update, :destroy] do
9
+ yield if block_given?
10
+
11
+ member do
12
+ get 'relationships/:relationship',
13
+ to: "#{parent_resource.name}#get_relationship_definition",
14
+ as: :relationship_definition
15
+
16
+ match 'relationships/:relationship',
17
+ to: "#{parent_resource.name}#update_relationship_definition",
18
+ via: [:patch, :post, :delete]
19
+
20
+ get ':relationship(/:relation_primary_key_value)',
21
+ to: "#{parent_resource.name}#get_relationship_data",
22
+ as: :relationship_data
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end