served 0.2.2 → 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: ac32676f4fe8e2811b3e62f5f83012e4fe2b7034
4
- data.tar.gz: 51b940bbb819c672d1144b77273d78eff11c13b1
3
+ metadata.gz: a4ea90b5253d1415e94853bf1654c93ff97f6b84
4
+ data.tar.gz: 8aabb4afe45f8233b64f1796212e694ff319c744
5
5
  SHA512:
6
- metadata.gz: 9aaf0a3112b8ef71ac02b7a37129409f3b47a19e671a886f416f62f549b9c58c4e67c40fc82ceb74459cc6ca25518c610f969a8457b15640d9067e75db687c4f
7
- data.tar.gz: 4bc38e706ef1eab00eecd7b88a9c33c83b973cc085e9e46bf1a189c8e5115ece33e869e17ccc04e75fd1b60b7fa9a359230279e7b39bbee8e01c09c592a967f2
6
+ metadata.gz: cf4c6bd4b29c2436f049948a41eb58ea1a7b36470b761543826694d2e049e7a7db30755ae74b91d2b7e3f00d5134c72f769396e8e9d99bae2601224d99d27be4
7
+ data.tar.gz: 5ffe22b04cca966d5f28f7d31159ccebb7ddc372d464f102c19e0bbb67f4c43915be1f857adf0b66cc1655b50973eba3f3ef3abb0bf32a220eb40911424f691c
data/.gitignore CHANGED
@@ -8,4 +8,6 @@
8
8
  /spec/reports/
9
9
  /tmp/
10
10
  *.iml
11
- .idea/**
11
+ .idea/**.rvmrc
12
+ .rvmrc
13
+ .idea/
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0
4
+ * Added Serializers feature, ability to define different methods of serializing resource response
5
+ * New error handling, will report different errors based on response from resource
6
+ * added `handler` response configuration, allows different handlers to be set up for specific
7
+ response codes.
8
+ * added `Json` handler as default
9
+ * added `JsonApi` handler
10
+
3
11
  ## 0.2.2
4
12
  * Resource::Base#destroy added, (backend functionality existed, but was never added to #resource)
5
13
 
data/README.md CHANGED
@@ -24,6 +24,7 @@ Served.configure do |config|
24
24
  config.timeout = 100
25
25
 
26
26
  config.backend = :patron
27
+ config.serializer = Served::Serializers::Json
27
28
  end
28
29
  ```
29
30
 
@@ -96,6 +97,35 @@ class SomeService::SomeResource < Served::Resource::Base
96
97
  end
97
98
  ```
98
99
 
100
+ ## JsonAPI
101
+
102
+ Served does support JSON API responses and comes with a dedicated serializer. Nested resources are supported as well.
103
+ By default `Served` is raising exceptions on error.
104
+
105
+ ```ruby
106
+ class JsonApiResource < Served::Resource::Base
107
+ serializer Served::Serializers::JsonApi
108
+
109
+ def self.raise_on_exceptions
110
+ false
111
+ end
112
+ end
113
+
114
+ class PeopleResource < JsonApiResource
115
+ attribute :name
116
+ resource_name 'friends'
117
+ end
118
+
119
+ class ServiceResource < JsonApiResource
120
+ attribute :id
121
+ attribute :first_name, presence: true
122
+ attribute :friends, serialize: PeopleResource, default: []
123
+
124
+ resource_name 'service_resource'
125
+ end
126
+ ```
127
+
128
+
99
129
  ## Validations
100
130
  `Served::Resource::Base` includes `AciveModel::Validations` and supports all validation methods with the exception of
101
131
  `Uniquness`. As a shotcut validations can be passed to the `#attribute` method much in the same way as it can be passed
@@ -138,3 +168,16 @@ Served follows resourceful standards. When a resource is initially saved a **POS
138
168
  to the service. When the resource already exists, a **PUT** request will be sent. Served determines if
139
169
  a resource is new or not based on the presence of an id.
140
170
 
171
+ ### Service Errors
172
+
173
+ If the service returns an error, by default an error is thrown. If you want it to behave more like AR where you get
174
+ validation errors and a `save` returns `true` or `false`, you can configure that.
175
+
176
+
177
+ ```ruby
178
+ class JsonApiResource < Served::Resource::Base
179
+ def self.raise_on_exceptions
180
+ false
181
+ end
182
+ end
183
+ ```
@@ -5,9 +5,6 @@ module Served
5
5
  include Resource::Serializable
6
6
  include Resource::Validatable
7
7
 
8
- def initialize(*args)
9
- # placeholder
10
- end
11
8
  end
12
9
  end
13
10
  end
data/lib/served/config.rb CHANGED
@@ -1,12 +1,15 @@
1
+ require_relative 'serializers/json'
1
2
  module Served
2
3
  include ActiveSupport::Configurable
3
4
  config_accessor :timeout
4
5
  config_accessor :backend
6
+ config_accessor :serializer
5
7
 
6
8
  configure do |config|
7
- config.timeout = 30
8
- config.backend = :http
9
- config.hosts = {}
9
+ config.timeout = 30
10
+ config.backend = :http
11
+ config.hosts = {}
12
+ config.serializer = Served::Serializers::Json
10
13
  end
11
14
 
12
15
  end
@@ -0,0 +1,13 @@
1
+ module Served
2
+
3
+ # TODO: remove in 1.0, this preserves backwards compatibility with 0.2
4
+ module Resource
5
+ class Base
6
+ class ServiceError < StandardError
7
+ end
8
+ end
9
+ end
10
+
11
+ class Error < Served::Resource::Base::ServiceError
12
+ end
13
+ end
@@ -1,4 +1,5 @@
1
1
  require 'addressable/template'
2
+ require_relative 'error'
2
3
  module Served
3
4
  # Provides an interface between the HTTP client and the Resource.
4
5
  class HTTPClient
@@ -10,10 +11,10 @@ module Served
10
11
  delegate :get, :put, :delete, :post, to: :@backend
11
12
  delegate :headers, :timeout, :host, to: :@resource
12
13
 
13
- class ConnectionFailed < StandardError
14
+ class ConnectionFailed < Served::Error
14
15
 
15
16
  def initialize(resource)
16
- super "Resource #{resource.name} could not be reached on #{resource.host}"
17
+ super "Resource '#{resource.name}' could not be reached on '#{resource.host}'"
17
18
  end
18
19
 
19
20
  end
@@ -4,7 +4,7 @@ module Served
4
4
  extend ActiveSupport::Concern
5
5
 
6
6
  included do
7
- prepend Prepend
7
+ include Serializable
8
8
  singleton_class.prepend ClassMethods::Prepend
9
9
  end
10
10
 
@@ -51,13 +51,9 @@ module Served
51
51
 
52
52
  end
53
53
 
54
- module Prepend
55
-
56
- def initialize(options={})
57
- reload_with_attributes(options.symbolize_keys)
58
- super options
59
- end
60
-
54
+ def initialize(hash={})
55
+ reload_with_attributes(normalize_keys(hash) )
56
+ self
61
57
  end
62
58
 
63
59
  # @return [Array] the keys for all the defined attributes
@@ -67,11 +63,22 @@ module Served
67
63
 
68
64
  private
69
65
 
70
- def reload_with_attributes(attributes)
71
- attributes.each do |name, value|
72
- set_attribute(name.to_sym, value)
66
+ # Reloads the instance with the new attributes
67
+ # If result is an Errors object it will create validation errors on the instance
68
+ # @return [Boolean]
69
+ def reload_with_attributes(result)
70
+ if result.is_a?(Served::Error)
71
+ serializer.parse_errors(result, self)
72
+ set_attribute_defaults
73
+ false
74
+ else
75
+ attributes = self.class.from_hash(result)
76
+ attributes.each do |name, value|
77
+ set_attribute(name.to_sym, value)
78
+ end
79
+ set_attribute_defaults
80
+ true
73
81
  end
74
- set_attribute_defaults
75
82
  end
76
83
 
77
84
  def set_attribute_defaults
@@ -85,7 +92,17 @@ module Served
85
92
  instance_variable_set("@#{name}", value)
86
93
  end
87
94
 
88
- end
95
+ def normalize_keys(params)
96
+ case params
97
+ when Hash
98
+ Hash[params.map { |k, v| [k.to_s.tr('-', '_'), normalize_keys(v)] }]
99
+ when Array
100
+ params.map { |v| normalize_keys(v) }
101
+ else
102
+ params
103
+ end
104
+ end
89
105
 
106
+ end
90
107
  end
91
108
  end
@@ -1,7 +1,11 @@
1
1
  require_relative 'attributable'
2
2
  require_relative 'serializable'
3
3
  require_relative 'validatable'
4
+ require_relative 'requestable'
4
5
  require_relative 'configurable'
6
+ require_relative 'resource_invalid'
7
+
8
+ require_relative 'http_errors'
5
9
 
6
10
  module Served
7
11
  module Resource
@@ -18,157 +22,13 @@ module Served
18
22
  # Served Resource, it will validate the nested resource as well as the top level.
19
23
  class Base
20
24
  include Configurable
25
+ include Requestable
21
26
  include Attributable
22
27
  include Validatable
23
28
  include Serializable
24
29
 
25
30
  attribute :id
26
31
 
27
- # Default headers for every request
28
- HEADERS = {'Content-type' => 'application/json', 'Accept' => 'application/json'}
29
-
30
-
31
- # raised when the connection receives a response from a service that does not constitute a 200
32
- class ServiceError < StandardError
33
- attr_reader :response
34
-
35
- def initialize(resource, response)
36
- @response = response
37
- begin
38
- error = JSON.parse(response.body)
39
- rescue JSON::ParserError
40
- super "Service #{resource.class.name} experienced an error and sent back an invalid error response"
41
- return
42
- end
43
- if error['error']
44
- super "Service #{resource.class.name} responded with an error: #{error['error']} -> #{error['exception']}"
45
- set_backtrace(error['traces']['Full Trace'].collect {|e| e['trace']})
46
- end
47
- end
48
- end
49
-
50
- class_configurable :resource_name do
51
- name.split('::').last.tableize
52
- end
53
-
54
- class_configurable :host do
55
- Served.config[:hosts][parent.name.underscore.split('/')[-1]] || Served.config[:hosts][:default]
56
- end
57
-
58
- class_configurable :timeout do
59
- Served.config.timeout
60
- end
61
-
62
- class_configurable :_headers do
63
- HEADERS
64
- end
65
-
66
- class_configurable :template do
67
- '{/resource*}{/id}.json{?query*}'
68
- end
69
-
70
- class << self
71
-
72
- # Defines the default headers that should be used for the request.
73
- #
74
- # @param headers [Hash] the headers to send with each requesat
75
- # @return headers [Hash] the default headers for the class
76
- def headers(h={})
77
- headers ||= _headers
78
- _headers(headers.merge!(h)) unless h.empty?
79
- _headers
80
- end
81
-
82
- # Looks up a resource on the service by id. For example `SomeResource.find(5)` would call `/some_resources/5`
83
- #
84
- # @param id [Integer] the id of the resource
85
- # @return [Resource::Base] the resource object.
86
- def find(id)
87
- instance = new(id: id)
88
- instance.reload
89
- end
90
-
91
- # @return [Served::HTTPClient] the HTTPClient using the configured backend
92
- def client
93
- @client ||= Served::HTTPClient.new(self)
94
- end
95
-
96
- end
97
-
98
- def initialize(options={})
99
- # placeholder
100
- end
101
-
102
- # Saves the record to the service. Will call POST if the record does not have an id, otherwise will call PUT
103
- # to update the record
104
- #
105
- # @return [Boolean] returns true or false depending on save success
106
- def save
107
- if id
108
- reload_with_attributes(put[resource_name.singularize])
109
- else
110
- reload_with_attributes(post[resource_name.singularize])
111
- end
112
- true
113
- end
114
-
115
- # Reloads the resource using attributes from the service
116
- #
117
- # @return [self] self
118
- def reload
119
- reload_with_attributes(get)
120
- self
121
- end
122
-
123
- # Destroys the record on the service. Acts on status code
124
- # If code is a 204 (no content) it will simply return true
125
- # otherwise it will parse the response and reloads the instance
126
- #
127
- # @return [Boolean|self] Returns true or instance
128
- def destroy(params = {})
129
- result = delete(params)
130
- return result if result.is_a?(TrueClass)
131
-
132
- reload_with_attributes(result)
133
- self
134
- end
135
-
136
- private
137
-
138
- def get(params={})
139
- handle_response(client.get(resource_name, id, params))
140
- end
141
-
142
- def put(params={})
143
- body = to_json
144
- handle_response(client.put(resource_name, id, body, params))
145
- end
146
-
147
- def post(params={})
148
- body = to_json
149
- handle_response(client.post(resource_name, body, params))
150
- end
151
-
152
- def delete(params={})
153
- response = client.delete(resource_name, id, params)
154
- return true if response.code == 204
155
-
156
- handle_response(response)
157
- end
158
-
159
- def handle_response(response)
160
- raise ServiceError.new(self, response) unless (200..299).include?(response.code)
161
- JSON.parse(response.body)
162
- end
163
-
164
- def client
165
- self.class.client
166
- end
167
-
168
- def presenter
169
- {resource_name.singularize.to_sym => attributes}
170
- end
171
-
172
32
  end
173
33
  end
174
34
  end
@@ -0,0 +1,142 @@
1
+ module Served
2
+ module Resource
3
+ class HttpError < Served::Error
4
+ attr_reader :message
5
+ attr_reader :server_backtrace
6
+ attr_reader :errors
7
+ attr_reader :response
8
+
9
+ # Defined in individual error classes
10
+ def self.code
11
+ raise NotImplementedError
12
+ end
13
+
14
+ def initialize(resource, response)
15
+ if resource.serializer.respond_to? :exception
16
+ serialized = resource.serializer.exception(response.body).symbolize_keys!
17
+
18
+ @error = serialized[:error]
19
+ @message = serialized[:exception]
20
+ @server_backtrace = serialized[:backtrace]
21
+ @code = serialized[:code] || serialized[:code] = self.class.code
22
+ @response = OpenStruct.new(serialized) # TODO: remove in served 1.0, used for backwards compat
23
+
24
+ super("An error '#{code} #{message}' occurred while making this request")
25
+ end
26
+ super "An error occurred '#{self.class.code}'"
27
+ end
28
+
29
+ def status
30
+ self.class.status
31
+ end
32
+
33
+ def code
34
+ @code
35
+ end
36
+
37
+ end
38
+
39
+ # 301 MovedPermanently
40
+ class MovedPermanently < HttpError
41
+
42
+ def self.code
43
+ 301
44
+ end
45
+
46
+ end
47
+
48
+ # 400 BadRequest
49
+ class BadRequest < HttpError
50
+
51
+ def self.code
52
+ 400
53
+ end
54
+
55
+ end
56
+
57
+ # 401 Unauthorized
58
+ class Unauthorized < HttpError
59
+
60
+ def self.code
61
+ 401
62
+ end
63
+
64
+ end
65
+
66
+ # 403 Forbidden
67
+ class Forbidden < HttpError
68
+
69
+ def self.code
70
+ 403
71
+ end
72
+
73
+ end
74
+
75
+ # 404 NotFound
76
+ class NotFound < HttpError
77
+
78
+ def self.code
79
+ 404
80
+ end
81
+
82
+ end
83
+
84
+ # 405 MethodNotAllowed
85
+ class MethodNotAllowed < HttpError
86
+
87
+ def self.code
88
+ 405
89
+ end
90
+
91
+ end
92
+
93
+ # 406 NotAcceptable
94
+ class NotAcceptable < HttpError
95
+
96
+ def self.code
97
+ 406
98
+ end
99
+
100
+ end
101
+
102
+ # 408 RequestTimeout
103
+ class RequestTimeout < HttpError
104
+
105
+ def self.code
106
+ 408
107
+ end
108
+
109
+ end
110
+
111
+ # 422 UnprocessableEntity
112
+ class UnprocessableEntity < HttpError
113
+
114
+ def self.code
115
+ 422
116
+ end
117
+
118
+ end
119
+
120
+ # 500 InternalServerError
121
+ class InternalServerError < HttpError
122
+
123
+ def self.code
124
+ 500
125
+ end
126
+
127
+ end
128
+
129
+ # 503 BadGateway
130
+ class BadGateway < HttpError
131
+
132
+ def self.code
133
+ 503
134
+ end
135
+
136
+ end
137
+
138
+ end
139
+ end
140
+
141
+
142
+
@@ -0,0 +1,11 @@
1
+ module Served
2
+ module Resource
3
+ class InvalidAttributeSerializer < Served::Error
4
+
5
+ def initialize(s)
6
+ super "'#{s}' attribute serializer does not exist"
7
+ end
8
+
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,196 @@
1
+ module Served
2
+ module Resource
3
+ module Requestable
4
+ extend ActiveSupport::Concern
5
+
6
+ # raised when a handler is defined if the method doesn't exist or if a proc isn't supplied
7
+ class HandlerRequired < StandardError
8
+
9
+ def initialize
10
+ super 'a handler is required, it must be a proc or a valid method'
11
+ end
12
+
13
+ end
14
+
15
+ HEADERS = { 'Content-type' => 'application/json', 'Accept' => 'application/json' }
16
+
17
+ included do
18
+ include Configurable
19
+
20
+ class_configurable :resource_name do
21
+ name.split('::').last.tableize
22
+ end
23
+
24
+ class_configurable :host do
25
+ Served.config[:hosts][parent.name.underscore.split('/')[-1]] || Served.config[:hosts][:default]
26
+ end
27
+
28
+ class_configurable :timeout do
29
+ Served.config.timeout
30
+ end
31
+
32
+ class_configurable :_headers do
33
+ HEADERS
34
+ end
35
+
36
+ class_configurable :handlers, default: {}
37
+
38
+ class_configurable :template do
39
+ '{/resource*}{/id}.json{?query*}'
40
+ end
41
+
42
+ class_configurable :raise_on_exceptions do
43
+ true
44
+ end
45
+
46
+ handle((200..201), :load)
47
+ handle([204, 202]) { attributes }
48
+
49
+ # 400 level errors
50
+ handle(301) { Resource::MovedPermanently }
51
+ handle(400) { Resource::BadRequest }
52
+ handle(401) { Resource::Unauthorized }
53
+ handle(403) { Resource::Forbidden }
54
+ handle(404) { Resource::NotFound }
55
+ handle(405) { Resource::MethodNotAllowed }
56
+ handle(406) { Resource::NotAcceptable }
57
+ handle(408) { Resource::RequestTimeout }
58
+ handle(422) { Resource::UnprocessableEntity }
59
+
60
+ # 500 level errors
61
+ handle(500) { Resource::InternalServerError }
62
+ handle(503) { Resource::BadGateway }
63
+
64
+
65
+ end
66
+
67
+ module ClassMethods
68
+
69
+ def handle_response(response)
70
+ if raise_on_exceptions
71
+ handler = handlers[response.code]
72
+ if handler.is_a? Proc
73
+ result = handler.call(response)
74
+ else
75
+ result = send(handler, response)
76
+ end
77
+ if result.is_a?(HttpError)
78
+ raise result.new(self, response)
79
+ result = Served::Serializers::JsonApi::Errors.new(response)
80
+ end
81
+ result
82
+ else
83
+ serializer.load(self, response)
84
+ end
85
+ end
86
+
87
+ # Sets individual handlers for response codes, accepts a proc or a symbol representing a method
88
+ #
89
+ # @param code [Integer|Range] the response code(s) the handler is to be assigned to
90
+ # @param symbol_or_proc [Symbol|Proc] a symbol representing the method to call, or a proc to be called when
91
+ # the specific response code has been called. The method or proc should return a hash of attributes, if an
92
+ # error class is returned it will be raised
93
+ # @yieldreturn [Hash] a hash of attributes, if an error class is returned it will be raised
94
+ def handle(code_or_range, symbol_or_proc=nil, &block)
95
+ raise HandlerRequired unless symbol_or_proc || block_given?
96
+ if code_or_range.is_a?(Range) || code_or_range.is_a?(Array)
97
+ code_or_range.each { |c|
98
+ handlers[c] = symbol_or_proc || block
99
+ }
100
+ else
101
+ handlers[code_or_range] = symbol_or_proc || block
102
+ end
103
+ end
104
+
105
+ # Defines the default headers that should be used for the request.
106
+ #
107
+ # @param headers [Hash] the headers to send with each requesat
108
+ # @return headers [Hash] the default headers for the class
109
+ def headers(h={})
110
+ headers ||= _headers
111
+ _headers(headers.merge!(h)) unless h.empty?
112
+ _headers
113
+ end
114
+
115
+ # Looks up a resource on the service by id. For example `SomeResource.find(5)` would call `/some_resources/5`
116
+ #
117
+ # @param id [Integer] the id of the resource
118
+ # @return [Resource::Base] the resource object.
119
+ def find(id, params = {})
120
+ instance = new(id: id)
121
+ instance.reload(params)
122
+ end
123
+
124
+ def all(params = {})
125
+ get(nil, params).map { |resource| new(resource) }
126
+ end
127
+
128
+ # @return [Served::HTTPClient] the HTTPClient using the configured backend
129
+ def client
130
+ @client ||= Served::HTTPClient.new(self)
131
+ end
132
+
133
+ def get(id, params = {})
134
+ handle_response(client.get(resource_name, id, params))
135
+ end
136
+ end
137
+
138
+ # Saves the record to the service. Will call POST if the record does not have an id, otherwise will call PUT
139
+ # to update the record
140
+ #
141
+ # @return [Boolean] returns true or false depending on save success
142
+ def save
143
+ id ? reload_with_attributes(put) : reload_with_attributes(post)
144
+ end
145
+
146
+ # Reloads the resource using attributes from the service
147
+ #
148
+ # @return [self] self
149
+ def reload(params = {})
150
+ reload_with_attributes(get(params))
151
+ self
152
+ end
153
+
154
+ # Destroys the record on the service. Acts on status code
155
+ # If code is a 204 (no content) it will simply return true
156
+ # otherwise it will parse the response and reloads the instance
157
+ #
158
+ # @return [Boolean|self] Returns true or instance
159
+ def destroy(params = {})
160
+ result = delete(params)
161
+ return result if result.is_a?(TrueClass)
162
+
163
+ reload_with_attributes(result)
164
+ end
165
+
166
+ private
167
+
168
+ def get(params = {})
169
+ self.class.get(id, params)
170
+ end
171
+
172
+ def put(params={})
173
+ handle_response(client.put(resource_name, id, dump, params))
174
+ end
175
+
176
+ def post(params={})
177
+ handle_response(client.post(resource_name, dump, params))
178
+ end
179
+
180
+ def delete(params={})
181
+ response = client.delete(resource_name, id, params)
182
+ return true if response.code == 204
183
+
184
+ handle_response(response)
185
+ end
186
+
187
+ def client
188
+ self.class.client
189
+ end
190
+
191
+ def handle_response(response)
192
+ self.class.handle_response(response)
193
+ end
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,9 @@
1
+ module Served
2
+ module Resource
3
+ class ResourceInvalid < ::Served::Error
4
+ def initialize(resource)
5
+ super "[#{resource.errors.full_messages.join(', ')}]"
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ module Served
2
+ module Resource
3
+ class ResponseInvalid < Served::Error
4
+
5
+ def initialize(resource, orignal_error=false)
6
+ return super "The resource '#{resource.name}' failed to serialize the response with message: " +
7
+ "'#{orignal_error.message}'" if orignal_error
8
+ super "The resource '#{resource.name}' returned a response, but the result of serialization was `nil`"
9
+ end
10
+
11
+ end
12
+ end
13
+ end
@@ -1,3 +1,6 @@
1
+ require_relative 'response_invalid'
2
+ require_relative 'invalid_attribute_serializer'
3
+
1
4
  module Served
2
5
  module Resource
3
6
  module Serializable
@@ -9,76 +12,83 @@ module Served
9
12
  end
10
13
  end
11
14
 
12
- # Specialized class serializers
13
- SERIALIZERS = {
14
- Fixnum => {call: :to_i},
15
- String => {call: :to_s},
16
- Symbol => {call: :to_sym, converter: -> (value) {
17
- if value.is_a? Array
18
- value = value.map { |a| a.to_sym }
19
- return value
20
- end
21
- }},
22
- Float => {call: :to_f},
23
- Boolean => {converter: -> (value) {
24
- return false unless value == "true"
25
- true
26
- }}
27
- }
28
-
29
15
  included do
16
+ include Configurable
30
17
  include Attributable
31
- prepend Prepend
32
- end
33
18
 
34
- class InvalidPresenter < StandardError
19
+ class_configurable :serializer, default: Served.config.serializer
20
+ class_configurable :use_root_node, default: Served.config.use_root_node
35
21
  end
36
22
 
37
- module Prepend
23
+ module ClassMethods
38
24
 
39
- def set_attribute(name, value)
40
- return unless self.class.attributes[name]
41
- if serializer = self.class.attributes[name][:serialize]
42
- if serializer.is_a? Proc
43
- value = serializer.call(value)
44
- elsif s = SERIALIZERS[serializer]
45
- called = false
46
- if s[:call] && value.respond_to?(s[:call])
47
- value = value.send(s[:call])
48
- called = true
49
- end
50
- value = s[:converter].call(value) if s[:converter] && !called
51
- else
52
- value = serializer.new(value)
53
- end
25
+ def load(string)
26
+ begin
27
+ result = serializer.load(self, string)
28
+ rescue => e
29
+ raise ResponseInvalid.new(self, e)
54
30
  end
55
- super
31
+ raise ResponseInvalid.new(self) unless result
32
+ result
56
33
  end
57
34
 
58
- end
59
-
60
- module ClassMethods
35
+ def from_hash(hash)
36
+ hash.each do |name, value|
37
+ hash[name] = serialize_attribute(name, value)
38
+ end
39
+ hash.symbolize_keys
40
+ end
61
41
 
62
42
  private
63
43
 
64
- def serializer_for_attribute(attr, serializer)
65
- attributes[attr][:serializer] = serializer
44
+ def attribute_serializer_for(type)
45
+ # case statement wont work here because of how it does class matching
46
+ return ->(v) { return v } unless type # nil
47
+ return ->(v) { return v.try(:to_i) } if type == Integer || type == Fixnum
48
+ return ->(v) { return v.try(:to_s) } if type == String
49
+ return ->(v) { return v.try(:to_sym) } if type == Symbol
50
+ return ->(v) { return v.try(:to_f) } if type == Float
51
+ if type == Boolean
52
+ return lambda do |v|
53
+ return false unless v == "true"
54
+ true
55
+ end
56
+ end
57
+ return ->(v) { type.new(v) } if type.ancestors.include?(Served::Resource::Base) ||
58
+ type.ancestors.include?(Served::Attribute::Base)
59
+ raise InvalidAttributeSerializer, type
60
+ end
61
+
62
+ def serialize_attribute(attr, value)
63
+ return false unless attributes[attr.to_sym]
64
+ serializer = attribute_serializer_for(attributes[attr.to_sym][:serialize])
65
+ if value.is_a? Array
66
+ return value unless attributes[attr.to_sym][:serialize]
67
+ value.collect do |v|
68
+ if v.is_a? attributes[attr.to_sym][:serialize]
69
+ v
70
+ else
71
+ serializer.call(v)
72
+ end
73
+ end
74
+ else
75
+ serializer.call(value)
76
+ end
66
77
  end
67
78
 
68
79
  end
69
80
 
70
- # renders the model as json
71
81
  def to_json(*args)
72
- raise InvalidPresenter, 'Presenter must respond to #to_json' unless presenter.respond_to? :to_json
73
- presenter.to_json
82
+ dump
74
83
  end
75
84
 
76
- # override this to return a presenter to be used for serialization, otherwise all attributes will be
77
- # serialized
78
- def presenter
79
- attributes
85
+ def dump
86
+ self.class.serializer.dump(self, attributes)
80
87
  end
81
88
 
89
+ def load(string)
90
+ self.class.serializer.load(string)
91
+ end
82
92
  end
83
93
  end
84
- end
94
+ end
@@ -12,16 +12,9 @@ module Served
12
12
  :inclusion,
13
13
  ]
14
14
 
15
- class ResourceInvalid < StandardError
16
-
17
- def initialize(resource)
18
- super "[#{resource.errors.full_messages.join(', ')}]"
19
- end
20
- end
21
-
22
- # Saves a resource and raises an error if the save fails.
15
+ # Saves a resource and raises an error if the save fails.
23
16
  def save!
24
- raise ResourceInvalid.new(self) unless run_validations! && save(false)
17
+ raise ::Served::Resource::ResourceInvalid.new(self) unless run_validations! && save(false)
25
18
  true
26
19
  end
27
20
 
@@ -0,0 +1,32 @@
1
+ require 'json'
2
+ module Served
3
+ module Serializers
4
+
5
+ # The default serializer assumes that the default Rails API response is used for both data and errors.
6
+ module Json
7
+
8
+ def self.load(resource, data)
9
+ parsed = JSON.parse(data)
10
+ # assume we need to return the entire response if it isn't
11
+ # namespaced by keys. TODO: remove after 1.0, this is strictly for backwards compatibility
12
+ resource_name = resource.resource_name
13
+ if resource_name.is_a? Array
14
+ warn '[DEPRECATION] passing an array for resource name will no longer be supported in Served 1.0, ' +
15
+ 'please ensure a single string is returned instead'
16
+ resource_name = resource_name.last # backwards compatibility
17
+ end
18
+ parsed[resource_name.singularize] || parsed
19
+ end
20
+
21
+ def self.dump(resource, attributes)
22
+ a = Hash[attributes.collect { |k,v| v.blank? ? nil : [k,v] }.compact]
23
+ {resource.resource_name.singularize => a}.to_json
24
+ end
25
+
26
+ def self.exception(data)
27
+ JSON.parse(data)
28
+ end
29
+
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/hash/indifferent_access'
4
+ module Served
5
+ module Serializers
6
+ module JsonApi
7
+ # Error object
8
+ class Error < ::Served::Error
9
+ delegate :[], to: :attrs
10
+
11
+ def initialize(attrs = {})
12
+ @attrs = (attrs || {}).with_indifferent_access
13
+ end
14
+
15
+ def id
16
+ attrs[:id]
17
+ end
18
+
19
+ def status
20
+ attrs[:status]
21
+ end
22
+
23
+ def code
24
+ attrs[:code]
25
+ end
26
+
27
+ def title
28
+ attrs[:title]
29
+ end
30
+
31
+ def detail
32
+ attrs[:detail]
33
+ end
34
+
35
+ def source_parameter
36
+ source.fetch(:parameter) do
37
+ source[:pointer] ? source[:pointer].split('/').last : nil
38
+ end
39
+ end
40
+
41
+ def source_pointer
42
+ source.fetch(:pointer) do
43
+ source[:parameter] ? "/data/attributes/#{source[:parameter]}" : nil
44
+ end
45
+ end
46
+
47
+ def source
48
+ res = attrs.fetch(:source, {})
49
+ res ? res : {}
50
+ end
51
+
52
+ protected
53
+
54
+ attr_reader :attrs
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Served
4
+ module Serializers
5
+ module JsonApi
6
+ # Wraps all error objects
7
+ class Errors < Served::Error
8
+ include Enumerable
9
+ attr_reader :errors
10
+
11
+ def initialize(response)
12
+ errors_hash = JSON.parse(response.body)
13
+ @errors = errors_hash['errors'].map { |error| Error.new(error) }
14
+ rescue JSON::ParserError
15
+ @errors = [Error.new(status: response.code, title: 'Parsing Error',
16
+ detail: 'Service responded with an unparsable body')]
17
+ end
18
+
19
+ def each(&block)
20
+ errors.each(&block)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Served
4
+ module Serializers
5
+ # JSON API serializing
6
+ module JsonApi
7
+ def self.load(_resource, response)
8
+ if (200..299).cover?(response.code)
9
+ data = JSON.parse(response.body)['data']
10
+ included = JSON.parse(response.body)['included']
11
+ if data.is_a?(Array)
12
+ data.map { |d| normalize_and_restructure(d, included) }
13
+ else
14
+ normalize_and_restructure(data, included)
15
+ end
16
+ else
17
+ Served::Serializers::JsonApi::Errors.new(response)
18
+ end
19
+ end
20
+
21
+ def self.parse_errors(result, resource)
22
+ result.each do |error|
23
+ if error.source_parameter && resource.attributes.keys.include?(error.source_parameter.to_sym)
24
+ resource.errors.add(error.source_parameter.to_sym, error_message(error))
25
+ else
26
+ resource.errors.add(:base, error_message(error))
27
+ end
28
+ end
29
+ resource
30
+ end
31
+
32
+ # Fetches the error message from either detail or title
33
+ # if both are nil a custom message is returned
34
+ def self.error_message(error)
35
+ error.detail || error.title || 'Error, but no error message found'
36
+ end
37
+
38
+ def self.normalize_and_restructure(data, included)
39
+ data = normalize_keys(data)
40
+ attributes = restructure_json(data)
41
+ merge_relationships(attributes, data, included) if data['relationships']
42
+ attributes
43
+ end
44
+
45
+ def self.dump(_resource, attributes)
46
+ attributes.to_json
47
+ end
48
+
49
+ def self.normalize_keys(params)
50
+ case params
51
+ when Hash
52
+ Hash[params.map { |k, v| [k.to_s.tr('-', '_'), normalize_keys(v)] }]
53
+ when Array
54
+ params.map { |v| normalize_keys(v) }
55
+ else
56
+ params
57
+ end
58
+ end
59
+
60
+ def self.serialize_individual_error(error)
61
+ {
62
+ json_api: error[:title],
63
+ exception: error[:detail],
64
+ backtrace: error[:source],
65
+ code: error[:code]
66
+ }
67
+ end
68
+
69
+ def self.merge_relationships(restructured, data, included)
70
+ data['relationships'].keys.each do |relationship|
71
+ rel = data['relationships'][relationship]
72
+ next unless rel && rel['data']
73
+ rel_data = rel['data']
74
+
75
+ relationship_attributes = if rel_data.is_a?(Array)
76
+ rel_data.inject([]) { |ary, r| ary << restructure_relationship(r, included) }
77
+ else
78
+ restructure_relationship(rel_data, included)
79
+ end
80
+ restructured.merge!(relationship => relationship_attributes)
81
+ end
82
+ restructured
83
+ end
84
+
85
+ # Restructure JSON API structure into parseable hash
86
+ def self.restructure_json(data)
87
+ data['attributes'].merge('id' => data['id'])
88
+ end
89
+
90
+ def self.restructure_relationship(resource, included)
91
+ relationship = included.find {|r| resource['id'] == r['id'] && resource['type'] == r['type']}
92
+ relationship['attributes'].merge('id' => resource['id']) if relationship
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,11 @@
1
+ module Served
2
+ module Serializers
3
+ class SerializationError < Served::Error
4
+
5
+ def initialize(original_exception)
6
+ super("Failed to serialize object with error: '#{original_exception.message}'")
7
+ end
8
+
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,3 @@
1
+ require_relative 'serializers/serialization_error'
2
+ require_relative 'serializers/json'
3
+ require_relative 'serializers/json_api'
@@ -1,3 +1,3 @@
1
1
  module Served
2
- VERSION = '0.2.2'
2
+ VERSION = '0.3.0'
3
3
  end
data/lib/served.rb CHANGED
@@ -3,6 +3,7 @@ require 'active_support/core_ext/string'
3
3
  require 'active_support/core_ext/module'
4
4
  require 'active_model'
5
5
 
6
+ require 'served/error'
6
7
  require 'served/engine'
7
8
  require 'served/version'
8
9
  require 'served/config'
@@ -10,6 +11,7 @@ require 'served/backends'
10
11
  require 'served/http_client'
11
12
  require 'served/resource'
12
13
  require 'served/attribute'
14
+ require 'served/serializers'
13
15
 
14
-
15
-
16
+ require 'served/serializers/json_api/error'
17
+ require 'served/serializers/json_api/errors'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: served
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jarod Reid
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-04-10 00:00:00.000000000 Z
11
+ date: 2017-06-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -169,13 +169,25 @@ files:
169
169
  - lib/served/backends/patron.rb
170
170
  - lib/served/config.rb
171
171
  - lib/served/engine.rb
172
+ - lib/served/error.rb
172
173
  - lib/served/http_client.rb
173
174
  - lib/served/resource.rb
174
175
  - lib/served/resource/attributable.rb
175
176
  - lib/served/resource/base.rb
176
177
  - lib/served/resource/configurable.rb
178
+ - lib/served/resource/http_errors.rb
179
+ - lib/served/resource/invalid_attribute_serializer.rb
180
+ - lib/served/resource/requestable.rb
181
+ - lib/served/resource/resource_invalid.rb
182
+ - lib/served/resource/response_invalid.rb
177
183
  - lib/served/resource/serializable.rb
178
184
  - lib/served/resource/validatable.rb
185
+ - lib/served/serializers.rb
186
+ - lib/served/serializers/json.rb
187
+ - lib/served/serializers/json_api.rb
188
+ - lib/served/serializers/json_api/error.rb
189
+ - lib/served/serializers/json_api/errors.rb
190
+ - lib/served/serializers/serialization_error.rb
179
191
  - lib/served/version.rb
180
192
  - served.gemspec
181
193
  homepage: http://github.com/fugufish/served