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 +4 -4
- data/.gitignore +3 -1
- data/CHANGELOG.md +8 -0
- data/README.md +43 -0
- data/lib/served/attribute/base.rb +0 -3
- data/lib/served/config.rb +6 -3
- data/lib/served/error.rb +13 -0
- data/lib/served/http_client.rb +3 -2
- data/lib/served/resource/attributable.rb +30 -13
- data/lib/served/resource/base.rb +5 -145
- data/lib/served/resource/http_errors.rb +142 -0
- data/lib/served/resource/invalid_attribute_serializer.rb +11 -0
- data/lib/served/resource/requestable.rb +196 -0
- data/lib/served/resource/resource_invalid.rb +9 -0
- data/lib/served/resource/response_invalid.rb +13 -0
- data/lib/served/resource/serializable.rb +60 -50
- data/lib/served/resource/validatable.rb +2 -9
- data/lib/served/serializers/json.rb +32 -0
- data/lib/served/serializers/json_api/error.rb +58 -0
- data/lib/served/serializers/json_api/errors.rb +25 -0
- data/lib/served/serializers/json_api.rb +96 -0
- data/lib/served/serializers/serialization_error.rb +11 -0
- data/lib/served/serializers.rb +3 -0
- data/lib/served/version.rb +1 -1
- data/lib/served.rb +4 -2
- metadata +14 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a4ea90b5253d1415e94853bf1654c93ff97f6b84
|
4
|
+
data.tar.gz: 8aabb4afe45f8233b64f1796212e694ff319c744
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cf4c6bd4b29c2436f049948a41eb58ea1a7b36470b761543826694d2e049e7a7db30755ae74b91d2b7e3f00d5134c72f769396e8e9d99bae2601224d99d27be4
|
7
|
+
data.tar.gz: 5ffe22b04cca966d5f28f7d31159ccebb7ddc372d464f102c19e0bbb67f4c43915be1f857adf0b66cc1655b50973eba3f3ef3abb0bf32a220eb40911424f691c
|
data/.gitignore
CHANGED
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
|
+
```
|
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
|
8
|
-
config.backend
|
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
|
data/lib/served/error.rb
ADDED
data/lib/served/http_client.rb
CHANGED
@@ -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 <
|
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
|
-
|
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
|
-
|
55
|
-
|
56
|
-
|
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
|
-
|
71
|
-
|
72
|
-
|
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
|
-
|
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
|
data/lib/served/resource/base.rb
CHANGED
@@ -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,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,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
|
-
|
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
|
23
|
+
module ClassMethods
|
38
24
|
|
39
|
-
def
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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
|
-
|
31
|
+
raise ResponseInvalid.new(self) unless result
|
32
|
+
result
|
56
33
|
end
|
57
34
|
|
58
|
-
|
59
|
-
|
60
|
-
|
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
|
65
|
-
|
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
|
-
|
73
|
-
presenter.to_json
|
82
|
+
dump
|
74
83
|
end
|
75
84
|
|
76
|
-
|
77
|
-
|
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
|
-
|
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
|
data/lib/served/version.rb
CHANGED
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.
|
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-
|
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
|