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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -4
  3. data/.rubocop.yml +3 -0
  4. data/.ruby-version +1 -0
  5. data/.wercker.yml +8 -1
  6. data/CHANGELOG.md +7 -0
  7. data/Gemfile +1 -0
  8. data/Rakefile +1 -0
  9. data/eipiai.gemspec +1 -0
  10. data/features/resources/api.feature +82 -0
  11. data/features/resources/collection.feature +60 -0
  12. data/features/resources/collection/get.feature +189 -0
  13. data/features/resources/collection/post.feature +230 -0
  14. data/features/resources/health.feature +27 -0
  15. data/features/resources/singular.feature +86 -0
  16. data/features/resources/singular/delete.feature +69 -0
  17. data/features/resources/singular/get.feature +146 -0
  18. data/features/resources/singular/post.feature +235 -0
  19. data/features/resources/singular/put.feature +164 -0
  20. data/features/step_definitions/steps.rb +10 -0
  21. data/features/step_definitions/webmachine_steps.rb +58 -0
  22. data/features/support/app.rb +14 -26
  23. data/features/support/env.rb +1 -0
  24. data/features/validation.feature +1 -0
  25. data/features/webmachine.feature +1 -56
  26. data/lib/eipiai.rb +1 -0
  27. data/lib/eipiai/configuration.rb +1 -0
  28. data/lib/eipiai/models.rb +1 -0
  29. data/lib/eipiai/models/collection.rb +1 -0
  30. data/lib/eipiai/models/representable.rb +6 -5
  31. data/lib/eipiai/roar.rb +1 -0
  32. data/lib/eipiai/roar/ext/hal.rb +3 -0
  33. data/lib/eipiai/roar/representers/api.rb +17 -6
  34. data/lib/eipiai/roar/representers/base.rb +1 -0
  35. data/lib/eipiai/validation.rb +1 -0
  36. data/lib/eipiai/validation/concerns/formatted_errors.rb +1 -0
  37. data/lib/eipiai/validation/validators/base.rb +2 -1
  38. data/lib/eipiai/validation/validators/sequel.rb +1 -0
  39. data/lib/eipiai/version.rb +2 -1
  40. data/lib/eipiai/webmachine.rb +2 -0
  41. data/lib/eipiai/webmachine/ext/decision.rb +1 -0
  42. data/lib/eipiai/webmachine/ext/request.rb +2 -1
  43. data/lib/eipiai/webmachine/resources/api.rb +7 -3
  44. data/lib/eipiai/webmachine/resources/base.rb +96 -19
  45. data/lib/eipiai/webmachine/resources/collection.rb +4 -18
  46. data/lib/eipiai/webmachine/resources/concerns/objectifiable.rb +1 -0
  47. data/lib/eipiai/webmachine/resources/concerns/representable.rb +69 -0
  48. data/lib/eipiai/webmachine/resources/health.rb +16 -10
  49. data/lib/eipiai/webmachine/resources/singular.rb +45 -10
  50. metadata +16 -4
  51. 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
@@ -1,38 +1,26 @@
1
- lib = File.expand_path('../lib', __dir__)
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
- add ['items'], ItemsResource
16
- add ['item', :item_uid], ItemResource
17
- add ['users'], UsersResource
18
- add ['user', :user_uid], UserResource
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
- WebmachineApp.adapter
17
+ Eipiai::Test::App.adapter
30
18
  end
31
19
 
32
20
  Before do
33
- Eipiai.configuration = Eipiai::Configuration.new
34
- Eipiai.configure do |config|
35
- config.base_uri = 'https://example.org'
36
- config.curie_uris = { b: 'https://api.example.org/rel/{rel}' }
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
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'cucumber/blendle_steps'
2
3
 
3
4
  Before do
@@ -1,3 +1,4 @@
1
+ @wip
1
2
  Feature: Object validation
2
3
 
3
4
  When accepting external requests to create objects
@@ -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
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'eipiai/configuration'
2
3
 
3
4
  require 'eipiai/models'
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  # Eipiai
2
3
  #
3
4
  # The main module for the library.
data/lib/eipiai/models.rb CHANGED
@@ -1,2 +1,3 @@
1
+ # frozen_string_literal: true
1
2
  require 'eipiai/models/collection'
2
3
  require 'eipiai/models/representable'
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Eipiai
2
3
  # Collection
3
4
  #
@@ -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
- # to_hash
25
+ # to_h
25
26
  #
26
- # call `#to_hash` on the representer belonging to the object.
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.to_hash # => { '_links' => { 'self' => { 'href' => '/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 to_hash(options = {})
36
- represented.to_hash(options)
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
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'roar'
2
3
 
3
4
  require 'eipiai/roar/ext/hal'
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'roar/json'
2
3
  require 'set'
3
4
 
@@ -50,6 +51,8 @@ module Roar
50
51
  hash['_links']['curies'] = curies.to_a if curies.any?
51
52
  end
52
53
  end
54
+
55
+ alias to_h to_hash
53
56
  end
54
57
 
55
58
  prepend ToHashExt