caprese 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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