eipiai 0.6.0 → 0.7.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 +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
|