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
@@ -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