eipiai 0.6.0 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -4
- data/.rubocop.yml +3 -0
- data/.ruby-version +1 -0
- data/.wercker.yml +8 -1
- data/CHANGELOG.md +7 -0
- data/Gemfile +1 -0
- data/Rakefile +1 -0
- data/eipiai.gemspec +1 -0
- data/features/resources/api.feature +82 -0
- data/features/resources/collection.feature +60 -0
- data/features/resources/collection/get.feature +189 -0
- data/features/resources/collection/post.feature +230 -0
- data/features/resources/health.feature +27 -0
- data/features/resources/singular.feature +86 -0
- data/features/resources/singular/delete.feature +69 -0
- data/features/resources/singular/get.feature +146 -0
- data/features/resources/singular/post.feature +235 -0
- data/features/resources/singular/put.feature +164 -0
- data/features/step_definitions/steps.rb +10 -0
- data/features/step_definitions/webmachine_steps.rb +58 -0
- data/features/support/app.rb +14 -26
- data/features/support/env.rb +1 -0
- data/features/validation.feature +1 -0
- data/features/webmachine.feature +1 -56
- data/lib/eipiai.rb +1 -0
- data/lib/eipiai/configuration.rb +1 -0
- data/lib/eipiai/models.rb +1 -0
- data/lib/eipiai/models/collection.rb +1 -0
- data/lib/eipiai/models/representable.rb +6 -5
- data/lib/eipiai/roar.rb +1 -0
- data/lib/eipiai/roar/ext/hal.rb +3 -0
- data/lib/eipiai/roar/representers/api.rb +17 -6
- data/lib/eipiai/roar/representers/base.rb +1 -0
- data/lib/eipiai/validation.rb +1 -0
- data/lib/eipiai/validation/concerns/formatted_errors.rb +1 -0
- data/lib/eipiai/validation/validators/base.rb +2 -1
- data/lib/eipiai/validation/validators/sequel.rb +1 -0
- data/lib/eipiai/version.rb +2 -1
- data/lib/eipiai/webmachine.rb +2 -0
- data/lib/eipiai/webmachine/ext/decision.rb +1 -0
- data/lib/eipiai/webmachine/ext/request.rb +2 -1
- data/lib/eipiai/webmachine/resources/api.rb +7 -3
- data/lib/eipiai/webmachine/resources/base.rb +96 -19
- data/lib/eipiai/webmachine/resources/collection.rb +4 -18
- data/lib/eipiai/webmachine/resources/concerns/objectifiable.rb +1 -0
- data/lib/eipiai/webmachine/resources/concerns/representable.rb +69 -0
- data/lib/eipiai/webmachine/resources/health.rb +16 -10
- data/lib/eipiai/webmachine/resources/singular.rb +45 -10
- metadata +16 -4
- data/features/support/db.rb +0 -8
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'eipiai/roar/representers/base'
|
2
3
|
|
3
4
|
module Eipiai
|
@@ -12,23 +13,33 @@ module Eipiai
|
|
12
13
|
|
13
14
|
def self.populate_from_app(app)
|
14
15
|
app.routes.each do |route|
|
15
|
-
|
16
|
+
resource = route.resource.new(nil, nil)
|
17
|
+
next unless resource.respond_to?(:resource_relation) && resource.top_level_relation?
|
16
18
|
|
17
|
-
|
18
|
-
|
19
|
-
|
19
|
+
options = {
|
20
|
+
rel: resource.resource_relation,
|
21
|
+
templated: templated?(route)
|
22
|
+
}
|
20
23
|
|
21
|
-
add_route(route, options)
|
24
|
+
add_route(route, resource.query_keys, options.compact)
|
22
25
|
end
|
23
26
|
end
|
24
27
|
|
25
|
-
def self.add_route(route, options)
|
28
|
+
def self.add_route(route, query_keys, options)
|
26
29
|
link(options) do |context|
|
27
30
|
request = context[:request]
|
28
31
|
path = '/' + route.path_spec.map { |e| e.is_a?(Symbol) ? "{#{e}}" : e }.join('/')
|
32
|
+
path += "{?#{query_keys.join(',')}}" if query_keys.any?
|
29
33
|
|
30
34
|
request.present? ? Addressable::URI.parse(request.uri).merge(path: path).to_s : path
|
31
35
|
end
|
32
36
|
end
|
37
|
+
|
38
|
+
def self.templated?(route)
|
39
|
+
query_keys = route.resource.new(nil, nil).query_keys
|
40
|
+
templated = query_keys.any? || route.path_spec.map(&:class).include?(Symbol)
|
41
|
+
|
42
|
+
templated || nil
|
43
|
+
end
|
33
44
|
end
|
34
45
|
end
|
data/lib/eipiai/validation.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module Eipiai
|
2
3
|
# Validator
|
3
4
|
#
|
@@ -48,7 +49,7 @@ module Eipiai
|
|
48
49
|
# @return [void]
|
49
50
|
#
|
50
51
|
def validate
|
51
|
-
|
52
|
+
raise NotImplementedError, '#validate required'
|
52
53
|
end
|
53
54
|
|
54
55
|
# errors
|
data/lib/eipiai/version.rb
CHANGED
data/lib/eipiai/webmachine.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'webmachine'
|
2
3
|
|
3
4
|
require 'eipiai/webmachine/ext/decision'
|
4
5
|
require 'eipiai/webmachine/ext/request'
|
5
6
|
|
6
7
|
require 'eipiai/webmachine/resources/concerns/objectifiable'
|
8
|
+
require 'eipiai/webmachine/resources/concerns/representable'
|
7
9
|
|
8
10
|
require 'eipiai/webmachine/resources/api'
|
9
11
|
require 'eipiai/webmachine/resources/base'
|
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'addressable/uri'
|
2
3
|
require 'webmachine'
|
3
4
|
|
@@ -38,7 +39,7 @@ module Webmachine
|
|
38
39
|
module URIReplacement
|
39
40
|
# build_uri
|
40
41
|
#
|
41
|
-
# Given an uri object, and headers, calls
|
42
|
+
# Given an uri object, and headers, calls `Webmachine::Request#build_uri`
|
42
43
|
# and parses the result using Addressable (if available), or simply
|
43
44
|
# returns the result as-is.
|
44
45
|
#
|
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'eipiai/webmachine/resources/base'
|
2
3
|
|
3
4
|
require 'active_support/core_ext/string/inflections'
|
@@ -15,13 +16,16 @@ module Eipiai
|
|
15
16
|
|
16
17
|
# ApiResource
|
17
18
|
#
|
18
|
-
# The
|
19
|
-
#
|
20
|
-
# endpoint.
|
19
|
+
# The API resource handles the /api endpoint, which is the main entrypoint for
|
20
|
+
# all HyperMedia based API requests.
|
21
21
|
#
|
22
22
|
class ApiResource < Webmachine::Resource
|
23
23
|
include Resource
|
24
24
|
|
25
|
+
def top_level_relation?
|
26
|
+
true
|
27
|
+
end
|
28
|
+
|
25
29
|
def allowed_methods
|
26
30
|
%w(GET)
|
27
31
|
end
|
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'eipiai/webmachine/resources/singular'
|
2
3
|
require 'eipiai/webmachine/resources/collection'
|
3
4
|
|
@@ -13,6 +14,7 @@ module Eipiai
|
|
13
14
|
#
|
14
15
|
module Resource
|
15
16
|
include Objectifiable
|
17
|
+
include Representable
|
16
18
|
|
17
19
|
# Includes the correct resource into the class, depending on its name.
|
18
20
|
#
|
@@ -27,8 +29,57 @@ module Eipiai
|
|
27
29
|
else
|
28
30
|
base.send(:include, CollectionResource)
|
29
31
|
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# resource_relation
|
35
|
+
#
|
36
|
+
# The name to be used when linking to this resource as a relation.
|
37
|
+
#
|
38
|
+
# The string returned from this method will become the name of the link in
|
39
|
+
# the HAL+JSON representation of this resource.
|
40
|
+
#
|
41
|
+
# @example
|
42
|
+
# resource.resource_relation # => 'items'
|
43
|
+
#
|
44
|
+
# @return [String] resource relation name
|
45
|
+
#
|
46
|
+
def resource_relation
|
47
|
+
resource_name.demodulize.underscore
|
48
|
+
end
|
49
|
+
|
50
|
+
# top_level_relation?
|
51
|
+
#
|
52
|
+
# return `true` if the link relation to this resource should be added to the
|
53
|
+
# top-level API entrypoint.
|
54
|
+
#
|
55
|
+
# It is good practice to keep most link relations out of the top-level
|
56
|
+
# entrypoint, instead opting for nesting the resource within its parent
|
57
|
+
# resource links relations.
|
58
|
+
#
|
59
|
+
# @return [true, false]
|
60
|
+
#
|
61
|
+
def top_level_relation?
|
62
|
+
false
|
63
|
+
end
|
64
|
+
|
65
|
+
# query_keys
|
66
|
+
#
|
67
|
+
# Returns an array of optional query component keys this resource accepts.
|
68
|
+
# The keys are added to the link relation as templated variables.
|
69
|
+
#
|
70
|
+
# Defaults to an empty array, not exposing any optional query keys.
|
71
|
+
#
|
72
|
+
# @return [Array<String>] array of query keys
|
73
|
+
#
|
74
|
+
def query_keys
|
75
|
+
[]
|
76
|
+
end
|
30
77
|
|
31
|
-
|
78
|
+
def service_available?
|
79
|
+
return true if Eipiai::HealthCheck.new.healthy?
|
80
|
+
|
81
|
+
response.headers['Retry-After'] = '60'
|
82
|
+
false
|
32
83
|
end
|
33
84
|
|
34
85
|
# malformed_request?
|
@@ -76,13 +127,17 @@ module Eipiai
|
|
76
127
|
end
|
77
128
|
|
78
129
|
def content_types_provided
|
79
|
-
[['application/hal+json', :to_json]]
|
130
|
+
[['application/hal+json', :to_hal_json], ['application/json', :to_json]]
|
80
131
|
end
|
81
132
|
|
82
133
|
def content_types_accepted
|
83
134
|
[['application/json', :from_json]]
|
84
135
|
end
|
85
136
|
|
137
|
+
def post_is_create?
|
138
|
+
true
|
139
|
+
end
|
140
|
+
|
86
141
|
# params
|
87
142
|
#
|
88
143
|
# Given a string in JSON format, returns the hash representation of that
|
@@ -107,12 +162,12 @@ module Eipiai
|
|
107
162
|
{}
|
108
163
|
end
|
109
164
|
|
110
|
-
#
|
165
|
+
# to_h
|
111
166
|
#
|
112
|
-
# Given an object, calls `#
|
167
|
+
# Given an object, calls `#to_h` on that object,
|
113
168
|
#
|
114
|
-
# If the object's `
|
115
|
-
#
|
169
|
+
# If the object's `to_h` implementation accepts any arguments, the hash
|
170
|
+
# `{ request: request }` is sent as its first argument.
|
116
171
|
#
|
117
172
|
# In practice, this method is used without any parameters, causing the
|
118
173
|
# method to call `represented`, which represents a Roar representer. This in
|
@@ -122,17 +177,23 @@ module Eipiai
|
|
122
177
|
# @example
|
123
178
|
# item = Item.new(uid: 'hello')
|
124
179
|
# get('/item/hello')
|
125
|
-
# resource.
|
180
|
+
# resource.to_h(item)['uid'] # => 'hello'
|
126
181
|
#
|
127
|
-
# @param [Object] obj to call #
|
182
|
+
# @param [Object] obj to call #to_h on
|
128
183
|
# @return [Hash] hash representation of the object
|
129
184
|
#
|
130
|
-
def
|
131
|
-
obj.method(:
|
185
|
+
def to_h(obj = object)
|
186
|
+
obj.method(:to_h).arity.zero? ? obj.to_h : obj.to_h(request: request)
|
187
|
+
rescue
|
188
|
+
obj
|
132
189
|
end
|
133
190
|
|
134
191
|
def to_json
|
135
|
-
|
192
|
+
to_h.to_json
|
193
|
+
end
|
194
|
+
|
195
|
+
def to_hal_json
|
196
|
+
to_h.to_json
|
136
197
|
end
|
137
198
|
|
138
199
|
# new_object
|
@@ -154,6 +215,14 @@ module Eipiai
|
|
154
215
|
@new_object ||= object_class.new.from_hash(params)
|
155
216
|
end
|
156
217
|
|
218
|
+
def base_uri
|
219
|
+
request.base_uri
|
220
|
+
end
|
221
|
+
|
222
|
+
def create_uri
|
223
|
+
Addressable::URI.join(base_uri, create_path)
|
224
|
+
end
|
225
|
+
|
157
226
|
private
|
158
227
|
|
159
228
|
def json_error_body(errors)
|
@@ -162,14 +231,22 @@ module Eipiai
|
|
162
231
|
true
|
163
232
|
end
|
164
233
|
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
234
|
+
def content_type_handler
|
235
|
+
content_type = request.headers['accept']
|
236
|
+
media_type = Webmachine::MediaType.parse(content_type)
|
237
|
+
|
238
|
+
content_types_provided.find { |ct, _| media_type.type_matches?(ct) }.last
|
239
|
+
rescue ArgumentError
|
240
|
+
nil
|
241
|
+
end
|
242
|
+
|
243
|
+
def handle_post_or_put_with_optional_content(created: true)
|
244
|
+
if content_type_handler
|
245
|
+
response.body = send(content_type_handler)
|
246
|
+
created ? 201 : 200
|
247
|
+
else
|
248
|
+
response.headers['Location'] ||= create_uri.to_s
|
249
|
+
created ? 201 : 204
|
173
250
|
end
|
174
251
|
end
|
175
252
|
end
|
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'eipiai/webmachine/resources/base'
|
2
3
|
|
3
4
|
module Eipiai
|
@@ -15,33 +16,18 @@ module Eipiai
|
|
15
16
|
%w(GET POST)
|
16
17
|
end
|
17
18
|
|
18
|
-
def post_is_create?
|
19
|
-
true
|
20
|
-
end
|
21
|
-
|
22
19
|
def create_path
|
23
20
|
new_object.path
|
24
21
|
end
|
25
22
|
|
26
23
|
def from_json
|
27
|
-
new_object.save
|
24
|
+
new_object.save
|
25
|
+
handle_post_or_put_with_optional_content
|
28
26
|
end
|
29
27
|
|
30
28
|
private
|
31
29
|
|
32
|
-
def
|
33
|
-
content_type = response.headers[Webmachine::CONTENT_TYPE]
|
34
|
-
media_type = Webmachine::MediaType.parse(content_type)
|
35
|
-
|
36
|
-
content_types_provided.find { |ct, _| media_type.type_matches?(ct) }.last
|
37
|
-
end
|
38
|
-
|
39
|
-
def handle_post_response
|
40
|
-
response.body = send(content_type_handler)
|
41
|
-
true
|
42
|
-
end
|
43
|
-
|
44
|
-
def to_hash
|
30
|
+
def to_h
|
45
31
|
super(request.get? ? object : new_object)
|
46
32
|
end
|
47
33
|
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Eipiai
|
3
|
+
module Resource
|
4
|
+
# Representable
|
5
|
+
#
|
6
|
+
# When included in a resource, provides basic methods to get to the
|
7
|
+
# "representer" that can be used to represent the object that the resource
|
8
|
+
# provides access to.
|
9
|
+
#
|
10
|
+
module Representable
|
11
|
+
private
|
12
|
+
|
13
|
+
# resource_name
|
14
|
+
#
|
15
|
+
# Take the resource class name, and strip `Resource` part from the name.
|
16
|
+
#
|
17
|
+
# @example
|
18
|
+
# resource.send :resource_name # => 'Eipiai::TestApp::Items'
|
19
|
+
#
|
20
|
+
# @return [String] resource name
|
21
|
+
#
|
22
|
+
def resource_name
|
23
|
+
self.class.name.chomp('Resource')
|
24
|
+
end
|
25
|
+
|
26
|
+
# singular_representer_class
|
27
|
+
#
|
28
|
+
# Constant, representing the representer belonging to the defined
|
29
|
+
# `object`.
|
30
|
+
#
|
31
|
+
# If the resource is called `ItemResource`, the representer will be
|
32
|
+
# `ItemRepresenter`.
|
33
|
+
#
|
34
|
+
# Returns `nil` if constant does not exist.
|
35
|
+
#
|
36
|
+
# @example
|
37
|
+
# resource.send :singular_representer_class # => ItemRepresenter
|
38
|
+
#
|
39
|
+
# @return [Class, nil] representer class, or nil if not found
|
40
|
+
#
|
41
|
+
def singular_representer_class
|
42
|
+
"#{resource_name.singularize}Representer".constantize
|
43
|
+
rescue NameError
|
44
|
+
nil
|
45
|
+
end
|
46
|
+
|
47
|
+
# collection_representer_class
|
48
|
+
#
|
49
|
+
# Constant, representing the representer belonging to the defined
|
50
|
+
# collection of `object`s.
|
51
|
+
#
|
52
|
+
# If the resource is called `ItemResource`, the representer will be
|
53
|
+
# `ItemsRepresenter`.
|
54
|
+
#
|
55
|
+
# Returns `nil` if constant does not exist.
|
56
|
+
#
|
57
|
+
# @example
|
58
|
+
# resource.send :collection_representer_class # => ItemsRepresenter
|
59
|
+
#
|
60
|
+
# @return [Class, nil] representer class, or nil if not found
|
61
|
+
#
|
62
|
+
def collection_representer_class
|
63
|
+
"#{resource_name.pluralize}Representer".constantize
|
64
|
+
rescue NameError
|
65
|
+
nil
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|