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
@@ -0,0 +1,164 @@
|
|
1
|
+
Feature: Working with singular resources (GET, POST, PUT, DELETE)
|
2
|
+
|
3
|
+
When a single resource exists
|
4
|
+
I want to use Webmachine to provide API access to that resource
|
5
|
+
So that the resource can easily be worked with
|
6
|
+
|
7
|
+
Scenario: Replace an existing record returns "204 No Content"
|
8
|
+
Given the following top-level resource at "/item/:item_uid":
|
9
|
+
"""ruby
|
10
|
+
class ItemResource < Webmachine::Resource
|
11
|
+
include Eipiai::Resource
|
12
|
+
|
13
|
+
def object
|
14
|
+
OpenStruct.new(hello: 'world')
|
15
|
+
end
|
16
|
+
|
17
|
+
def new_object
|
18
|
+
OpenStruct.new(params)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
"""
|
22
|
+
|
23
|
+
When the client provides the header "Content-Type: application/json"
|
24
|
+
And the client does a PUT request to the "item" resource with the template variable "item_uid" set to "hello" and the following content:
|
25
|
+
"""json
|
26
|
+
{
|
27
|
+
"hello": "galaxy"
|
28
|
+
}
|
29
|
+
"""
|
30
|
+
Then the status code should be "204" (No Content)
|
31
|
+
And the response should contain the header "Location" with value "https://example.org/item/hello"
|
32
|
+
|
33
|
+
Scenario: Creating a new record returns "201 Created"
|
34
|
+
Given the following top-level resource at "/item/:item_uid":
|
35
|
+
"""ruby
|
36
|
+
class ItemResource < Webmachine::Resource
|
37
|
+
include Eipiai::Resource
|
38
|
+
|
39
|
+
def new_object
|
40
|
+
OpenStruct.new(params)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
"""
|
44
|
+
|
45
|
+
When the client provides the header "Content-Type: application/json"
|
46
|
+
And the client does a PUT request to the "item" resource with the template variable "item_uid" set to "hello" and the following content:
|
47
|
+
"""json
|
48
|
+
{
|
49
|
+
"hello": "galaxy"
|
50
|
+
}
|
51
|
+
"""
|
52
|
+
Then the status code should be "201" (Created)
|
53
|
+
And the response should contain the header "Location" with value "https://example.org/item/hello"
|
54
|
+
|
55
|
+
Scenario: Replace an existing record while providing Accept header "application/json" returns "200 OK"
|
56
|
+
Given the following top-level resource at "/item/:item_uid":
|
57
|
+
"""ruby
|
58
|
+
class ItemResource < Webmachine::Resource
|
59
|
+
include Eipiai::Resource
|
60
|
+
|
61
|
+
def to_hal_json
|
62
|
+
to_h.merge(_links: { self: { href: create_uri } }).to_json
|
63
|
+
end
|
64
|
+
|
65
|
+
def object
|
66
|
+
@object ||= OpenStruct.new(hello: 'world', foo: 'bar')
|
67
|
+
end
|
68
|
+
|
69
|
+
def new_object
|
70
|
+
@object = OpenStruct.new(params)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
"""
|
74
|
+
|
75
|
+
When the client provides the header "Accept: application/hal+json"
|
76
|
+
And the client provides the header "Content-Type: application/json"
|
77
|
+
And the client does a PUT request to the "item" resource with the template variable "item_uid" set to "hello" and the following content:
|
78
|
+
"""json
|
79
|
+
{
|
80
|
+
"hello": "galaxy",
|
81
|
+
"foo": "bar"
|
82
|
+
}
|
83
|
+
"""
|
84
|
+
Then the status code should be "200" (OK)
|
85
|
+
And the response should be HAL/JSON:
|
86
|
+
"""json
|
87
|
+
{
|
88
|
+
"_links": {
|
89
|
+
"self": {
|
90
|
+
"href": "https://example.org/item/hello"
|
91
|
+
}
|
92
|
+
},
|
93
|
+
"hello": "galaxy",
|
94
|
+
"foo": "bar"
|
95
|
+
}
|
96
|
+
"""
|
97
|
+
|
98
|
+
Scenario: Create new object while providing Accept "application/json" returns "200 OK"
|
99
|
+
Given the following top-level resource at "/item/:item_uid":
|
100
|
+
"""ruby
|
101
|
+
class ItemResource < Webmachine::Resource
|
102
|
+
include Eipiai::Resource
|
103
|
+
|
104
|
+
def object
|
105
|
+
@object
|
106
|
+
end
|
107
|
+
|
108
|
+
def new_object
|
109
|
+
@object = OpenStruct.new(params)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
"""
|
113
|
+
|
114
|
+
When the client provides the header "Accept: application/json"
|
115
|
+
And the client provides the header "Content-Type: application/json"
|
116
|
+
And the client does a PUT request to the "item" resource with the template variable "item_uid" set to "hello" and the following content:
|
117
|
+
"""json
|
118
|
+
{
|
119
|
+
"hello": "universe",
|
120
|
+
"foo": "bar"
|
121
|
+
}
|
122
|
+
"""
|
123
|
+
Then the status code should be "200" (OK)
|
124
|
+
And the response should be JSON:
|
125
|
+
"""json
|
126
|
+
{
|
127
|
+
"hello": "universe",
|
128
|
+
"foo": "bar"
|
129
|
+
}
|
130
|
+
"""
|
131
|
+
|
132
|
+
Scenario: Replacing existing object while providing Accept "application/json" returns "200 OK"
|
133
|
+
Given the following top-level resource at "/item/:item_uid":
|
134
|
+
"""ruby
|
135
|
+
class ItemResource < Webmachine::Resource
|
136
|
+
include Eipiai::Resource
|
137
|
+
|
138
|
+
def object
|
139
|
+
@object ||= OpenStruct.new(hello: 'world')
|
140
|
+
end
|
141
|
+
|
142
|
+
def new_object
|
143
|
+
@object = OpenStruct.new(params)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
"""
|
147
|
+
|
148
|
+
When the client provides the header "Accept: application/json"
|
149
|
+
And the client provides the header "Content-Type: application/json"
|
150
|
+
And the client does a PUT request to the "item" resource with the template variable "item_uid" set to "hello" and the following content:
|
151
|
+
"""json
|
152
|
+
{
|
153
|
+
"hello": "universe",
|
154
|
+
"foo": "bar"
|
155
|
+
}
|
156
|
+
"""
|
157
|
+
Then the status code should be "200" (OK)
|
158
|
+
And the response should be JSON:
|
159
|
+
"""json
|
160
|
+
{
|
161
|
+
"hello": "universe",
|
162
|
+
"foo": "bar"
|
163
|
+
}
|
164
|
+
"""
|
@@ -1,3 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
Given(/^the configuration "([^"]*)" is set to "([^"]*)"$/) do |config, value|
|
2
3
|
Eipiai.configuration.send("#{config}=", value)
|
3
4
|
end
|
5
|
+
|
6
|
+
Then(/^the response should be XML:$/) do |xml|
|
7
|
+
dump last_response.body
|
8
|
+
|
9
|
+
xml = Liquid::Template.parse(xml).render
|
10
|
+
|
11
|
+
assert_equal last_response.headers['Content-Type'], 'text/xml'
|
12
|
+
expect(last_response.body.strip).to eq(xml)
|
13
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
Given(/^the following( top-level)? resource at "([^"]*)":$/) do |top, path, code|
|
3
|
+
if top
|
4
|
+
code = code.lines.map do |line|
|
5
|
+
next line unless line.include?('Webmachine::Resource')
|
6
|
+
|
7
|
+
line + " def top_level_relation?\n true\n end\n\n"
|
8
|
+
end.join
|
9
|
+
end
|
10
|
+
|
11
|
+
add_resource(path, code)
|
12
|
+
end
|
13
|
+
|
14
|
+
def add_resource(path, code)
|
15
|
+
resource = code[/class (\S+Resource)/, 1]
|
16
|
+
|
17
|
+
if Eipiai::Test.const_defined?(resource)
|
18
|
+
Eipiai::Test.class_exec { remove_const(resource) }
|
19
|
+
end
|
20
|
+
|
21
|
+
Eipiai::Test.class_eval(code)
|
22
|
+
add_route(path, resource)
|
23
|
+
end
|
24
|
+
|
25
|
+
def add_route(path, resource)
|
26
|
+
path = path.split('/').drop(1).map! { |r| r[0] == ':' ? r[1..-1].to_sym : r }
|
27
|
+
|
28
|
+
Eipiai::Test::App.tap do |app|
|
29
|
+
app.add_route path, Eipiai::Test.const_get(resource)
|
30
|
+
Eipiai::ApiRepresenter.populate_from_app(app)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
Before do
|
35
|
+
# Add the `/api` and `/health` routes
|
36
|
+
#
|
37
|
+
Eipiai::Test::App.add_route ['api'], Eipiai::ApiResource
|
38
|
+
Eipiai::Test::App.add_route ['health'], Eipiai::HealthResource
|
39
|
+
end
|
40
|
+
|
41
|
+
After do
|
42
|
+
# Remove any existing resource objects
|
43
|
+
#
|
44
|
+
Eipiai::Test.class_exec do
|
45
|
+
constants.each do |constant|
|
46
|
+
remove_const(constant) if constant.to_s.end_with?('Resource')
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Remove any existing routes
|
51
|
+
#
|
52
|
+
Eipiai::Test::App.dispatcher.instance_variable_set(:@routes, [])
|
53
|
+
|
54
|
+
# Remove links from ApiRepresenter
|
55
|
+
#
|
56
|
+
Eipiai.class_exec { remove_const(:ApiRepresenter) }
|
57
|
+
load 'eipiai/roar/representers/api.rb'
|
58
|
+
end
|
data/features/support/app.rb
CHANGED
@@ -1,38 +1,26 @@
|
|
1
|
-
|
2
|
-
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
-
|
1
|
+
# frozen_string_literal: true
|
4
2
|
require 'eipiai'
|
3
|
+
require 'webmachine'
|
5
4
|
require 'webmachine/adapters/rack'
|
6
|
-
require_relative '../../support/fixtures/app'
|
7
|
-
|
8
|
-
include Eipiai::TestApp
|
9
|
-
|
10
|
-
WebmachineApp = Webmachine::Application.new do |app|
|
11
|
-
app.routes do
|
12
|
-
add ['api'], Eipiai::ApiResource
|
13
|
-
add ['health'], Eipiai::HealthResource
|
14
5
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
6
|
+
module Eipiai
|
7
|
+
module Test
|
8
|
+
App = Webmachine::Application.new do |app|
|
9
|
+
app.configure do |config|
|
10
|
+
config.adapter = :Rack
|
11
|
+
end
|
12
|
+
end
|
19
13
|
end
|
20
|
-
|
21
|
-
app.configure do |config|
|
22
|
-
config.adapter = :Rack
|
23
|
-
end
|
24
|
-
|
25
|
-
Eipiai::ApiRepresenter.populate_from_app(app)
|
26
14
|
end
|
27
15
|
|
28
16
|
def app
|
29
|
-
|
17
|
+
Eipiai::Test::App.adapter
|
30
18
|
end
|
31
19
|
|
32
20
|
Before do
|
33
|
-
Eipiai.configuration
|
34
|
-
|
35
|
-
|
36
|
-
|
21
|
+
Eipiai.configuration.tap do |c|
|
22
|
+
c.healthy = true
|
23
|
+
c.base_uri = 'https://example.org'
|
24
|
+
c.curie_uris = { b: 'https://api.example.org/rel/{rel}' }
|
37
25
|
end
|
38
26
|
end
|
data/features/support/env.rb
CHANGED
data/features/validation.feature
CHANGED
data/features/webmachine.feature
CHANGED
@@ -1,65 +1,10 @@
|
|
1
|
+
@wip
|
1
2
|
Feature: Webmachine CRUD operations
|
2
3
|
|
3
4
|
When a new JSON API service needs to be developed
|
4
5
|
I want to use Eipiai's default CRUD setup
|
5
6
|
So that the basic API operations are quick and easy to implement
|
6
7
|
|
7
|
-
Scenario: List main API endpoint
|
8
|
-
When the client does a GET request to "/api"
|
9
|
-
Then the status code should be "200" (OK)
|
10
|
-
And the response should be HAL/JSON:
|
11
|
-
"""json
|
12
|
-
{
|
13
|
-
"_links": {
|
14
|
-
"self": {
|
15
|
-
"href": "https://example.org/api"
|
16
|
-
},
|
17
|
-
"api": {
|
18
|
-
"href": "https://example.org/api"
|
19
|
-
},
|
20
|
-
"health": {
|
21
|
-
"href": "https://example.org/health"
|
22
|
-
},
|
23
|
-
"items": {
|
24
|
-
"href": "https://example.org/items"
|
25
|
-
},
|
26
|
-
"item": {
|
27
|
-
"templated": true,
|
28
|
-
"href": "https://example.org/item/{item_uid}"
|
29
|
-
},
|
30
|
-
"users": {
|
31
|
-
"href": "https://example.org/users"
|
32
|
-
},
|
33
|
-
"user": {
|
34
|
-
"templated": true,
|
35
|
-
"href": "https://example.org/user/{user_uid}"
|
36
|
-
}
|
37
|
-
}
|
38
|
-
}
|
39
|
-
"""
|
40
|
-
|
41
|
-
Scenario: Get service health
|
42
|
-
When the client does a GET request to "/health"
|
43
|
-
Then the status code should be "200" (OK)
|
44
|
-
And the response should be JSON:
|
45
|
-
"""json
|
46
|
-
{
|
47
|
-
"healthy": true
|
48
|
-
}
|
49
|
-
"""
|
50
|
-
|
51
|
-
Scenario: Get unhealthy service health
|
52
|
-
Given the configuration "healthy" is set to "false"
|
53
|
-
When the client does a GET request to "/health"
|
54
|
-
Then the status code should be "503" (Service Unavailable)
|
55
|
-
And the response contains the "Retry-After" header with value "60"
|
56
|
-
And the response should be JSON:
|
57
|
-
"""json
|
58
|
-
{
|
59
|
-
"healthy": false
|
60
|
-
}
|
61
|
-
"""
|
62
|
-
|
63
8
|
Scenario: Create resource
|
64
9
|
Given the client provides the header "Content-Type: application/json"
|
65
10
|
When the client does a POST request to the "items" resource with the following content:
|
data/lib/eipiai.rb
CHANGED
data/lib/eipiai/configuration.rb
CHANGED
data/lib/eipiai/models.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module Eipiai
|
2
3
|
# Representable
|
3
4
|
#
|
@@ -21,19 +22,19 @@ module Eipiai
|
|
21
22
|
"#{self.class.name}Representer".constantize.new(self)
|
22
23
|
end
|
23
24
|
|
24
|
-
#
|
25
|
+
# to_h
|
25
26
|
#
|
26
|
-
# call `#
|
27
|
+
# call `#to_h` on the representer belonging to the object.
|
27
28
|
#
|
28
29
|
# @example
|
29
30
|
# item = Item.new.extend(Eipiai::Representable)
|
30
|
-
# item.
|
31
|
+
# item.to_h # => { '_links' => { 'self' => { 'href' => '/item' } } }
|
31
32
|
#
|
32
33
|
# @param [Hash] options to be used inside the representer
|
33
34
|
# @return [Hash] hash representation of the object
|
34
35
|
#
|
35
|
-
def
|
36
|
-
represented.
|
36
|
+
def to_h(options = {})
|
37
|
+
represented.to_h(options)
|
37
38
|
end
|
38
39
|
|
39
40
|
# from_hash
|
data/lib/eipiai/roar.rb
CHANGED
data/lib/eipiai/roar/ext/hal.rb
CHANGED