@@ -54,15 +54,123 @@ def from_hash(messages, save_cache = false)
5454
5555 # Grabs errors from a json response.
5656 def from_json ( json , save_cache = false )
57- decoded = Formats [ :json ] . decode ( json , false ) || { } rescue { }
58- errors = decoded [ "errors" ] || { }
59- from_hash errors , save_cache
57+ from_body json , save_cache , format : Formats [ :json ]
6058 end
6159
6260 # Grabs errors from an XML response.
6361 def from_xml ( xml , save_cache = false )
64- array = Array . wrap ( Formats [ :xml ] . decode ( xml , false ) [ "errors" ] [ "error" ] ) rescue [ ]
65- from_array array , save_cache
62+ from_body xml , save_cache , format : Formats [ :xml ]
63+ end
64+
65+ ##
66+ # :method: from_body
67+ #
68+ # :call-seq:
69+ # from_body(body, save_cache = false)
70+ #
71+ # Grabs errors from a response body.
72+ def from_body ( body , save_cache = false , format : @base . class . format )
73+ decoded = format . decode ( body , false ) || { } rescue { }
74+ errors = @base . class . errors_parser . new ( decoded ) . tap do |parser |
75+ parser . format = format
76+ end . messages
77+
78+ if errors . is_a? ( Array )
79+ from_array errors , save_cache
80+ else
81+ from_hash errors , save_cache
82+ end
83+ end
84+ end
85+
86+ # ActiveResource::ErrorsParser is a wrapper to handle parsing responses in
87+ # response to invalid requests that do not directly map to Active Model error
88+ # conventions.
89+ #
90+ # You can define a custom class that inherits from ActiveResource::ErrorsParser
91+ # in order to to set the elements instance.
92+ #
93+ # The initialize method will receive the ActiveResource::Formats parsed result
94+ # and should set @messages.
95+ #
96+ # ==== Example
97+ #
98+ # Consider a POST /posts.json request that results in a 422 Unprocessable
99+ # Content response with the following +application/json+ body:
100+ #
101+ # {
102+ # "error": true,
103+ # "messages": ["Something went wrong", "Title can't be blank"]
104+ # }
105+ #
106+ # A Post class can be setup to handle it with:
107+ #
108+ # class Post < ActiveResource::Base
109+ # self.errors_parser = PostErrorsParser
110+ # end
111+ #
112+ # A custom ActiveResource::ErrorsParser instance's +messages+ method should
113+ # return a mapping of attribute names (or +"base"+) to arrays of error message
114+ # strings:
115+ #
116+ # class PostErrorsParser < ActiveResource::ErrorsParser
117+ # def initialize(parsed)
118+ # @messages = Hash.new { |hash, attr_name| hash[attr_name] = [] }
119+ #
120+ # parsed["messages"].each do |message|
121+ # if message.starts_with?("Title")
122+ # @messages["title"] << message
123+ # else
124+ # @messages["base"] << message
125+ # end
126+ # end
127+ # end
128+ # end
129+ #
130+ # When the POST /posts.json request is submitted by calling +save+, the errors
131+ # are parsed from the body and assigned to the Post instance's +errors+
132+ # object:
133+ #
134+ # post = Post.new(title: "")
135+ # post.save # => false
136+ # post.valid? # => false
137+ # post.errors.messages_for(:base) # => ["Something went wrong"]
138+ # post.errors.messages_for(:title) # => ["Title can't be blank"]
139+ #
140+ # If the custom ActiveResource::ErrorsParser instance's +messages+ method
141+ # returns an array of error message strings, Active Resource will try to infer
142+ # the attribute name based on the contents of the error message string. If an
143+ # error starts with a known attribute name, Active Resource will add the
144+ # message to that attribute's error messages. If a known attribute name cannot
145+ # be inferred, the error messages will be added to the +:base+ errors:
146+ #
147+ # class PostErrorsParser < ActiveResource::ErrorsParser
148+ # def initialize(parsed)
149+ # @messages = parsed["messages"]
150+ # end
151+ # end
152+ #
153+ # post = Post.new(title: "")
154+ # post.save # => false
155+ # post.valid? # => false
156+ # post.errors.messages_for(:base) # => ["Something went wrong"]
157+ # post.errors.messages_for(:title) # => ["Title can't be blank"]
158+ class ErrorsParser
159+ attr_accessor :messages
160+ attr_accessor :format
161+
162+ def initialize ( parsed )
163+ @messages = parsed
164+ end
165+ end
166+
167+ class ActiveModelErrorsParser < ErrorsParser # :nodoc:
168+ def messages
169+ if format . is_a? ( Formats [ :xml ] )
170+ Array . wrap ( super [ "errors" ] [ "error" ] ) rescue [ ]
171+ else
172+ super [ "errors" ] || { } rescue { }
173+ end
66174 end
67175 end
68176
@@ -138,6 +246,7 @@ module Validations
138246 alias_method :save_without_validation , :save
139247 alias_method :save , :save_with_validation
140248 class_attribute :_remote_errors , instance_accessor : false
249+ class_attribute :_errors_parser , instance_accessor : false
141250 end
142251
143252 class_methods do
@@ -153,6 +262,16 @@ def remote_errors=(errors)
153262 def remote_errors
154263 _remote_errors . presence || ResourceInvalid
155264 end
265+
266+ # Sets the parser to use when a response with errors is returned.
267+ def errors_parser = ( parser_class )
268+ parser_class = parser_class . constantize if parser_class . is_a? ( String )
269+ self . _errors_parser = parser_class
270+ end
271+
272+ def errors_parser
273+ _errors_parser || ActiveResource ::ActiveModelErrorsParser
274+ end
156275 end
157276
158277 # Validate a resource and save (POST) it to the remote web service.
@@ -183,12 +302,7 @@ def save_with_validation(options = {})
183302 # Loads the set of remote errors into the object's Errors based on the
184303 # content-type of the error-block received.
185304 def load_remote_errors ( remote_errors , save_cache = false ) # :nodoc:
186- case self . class . format
187- when ActiveResource ::Formats [ :xml ]
188- errors . from_xml ( remote_errors . response . body , save_cache )
189- when ActiveResource ::Formats [ :json ]
190- errors . from_json ( remote_errors . response . body , save_cache )
191- end
305+ errors . from_body ( remote_errors . response . body , save_cache )
192306 end
193307
194308 # Checks for errors on an object (i.e., is resource.errors empty?).
0 commit comments