eipiai 0.1.0 → 0.2.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 -0
- data/CHANGELOG.md +10 -0
- data/README.md +1 -1
- data/features/support/app.rb +6 -4
- data/features/validation.feature +48 -0
- data/features/webmachine.feature +10 -14
- data/lib/eipiai/validation/concerns/formatted_errors.rb +38 -0
- data/lib/eipiai/validation/validators/base.rb +94 -0
- data/lib/eipiai/validation/validators/sequel.rb +46 -0
- data/lib/eipiai/validation.rb +4 -0
- data/lib/eipiai/version.rb +1 -1
- data/lib/eipiai/webmachine/ext/request.rb +24 -0
- data/lib/eipiai/webmachine/resources/base.rb +50 -0
- data/lib/eipiai/webmachine/resources/collection.rb +2 -8
- data/lib/eipiai/webmachine/resources/singular.rb +0 -9
- data/lib/eipiai.rb +1 -0
- metadata +8 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 11411b6b54cd8d262763b931fd39c8cc07267b01
|
4
|
+
data.tar.gz: b0fd69f854bee571d6a064c594e44ccfea576b98
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9e819fa5def0aa433c40fb91a170f0b18215e460132dd4c7c75987fcf3e169ac594b3d30e3f58461cc398239c37b060721f8b5201c2f790ac23bda0122e1999d
|
7
|
+
data.tar.gz: 6644c4fb34968fcb2bb987929852d223f1a24e0404e2ada895578de0da41b5a9dff53fa6289d317cb6e75316efbd2ed46ddea81d4c93ff540f09fdfa6dd5b416
|
data/.gitignore
CHANGED
data/CHANGELOG.md
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
# CHANGELOG
|
2
|
+
|
3
|
+
## [v0.1.0](https://github.com/blendle/eipiai/tree/v0.1.0) (2015-12-12)
|
4
|
+
**Merged pull requests:**
|
5
|
+
|
6
|
+
- initial implementation [\#1](https://github.com/blendle/eipiai/pull/1) ([JeanMertz](https://github.com/JeanMertz))
|
7
|
+
|
8
|
+
|
9
|
+
|
10
|
+
\* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)*
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# `/ˌeɪ.piˈaɪ/` – The JSON API Framework
|
1
|
+
# `/ˌeɪ.piˈaɪ/` – The JSON API Framework [](https://app.wercker.com/project/bykey/afba12ab733cdd8a55cb39db0b68c275)
|
2
2
|
|
3
3
|
Opinionated JSON-API stack to get the job done.
|
4
4
|
|
data/features/support/app.rb
CHANGED
@@ -3,16 +3,18 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
3
3
|
|
4
4
|
require 'eipiai'
|
5
5
|
require 'webmachine/adapters/rack'
|
6
|
-
require_relative '../../support/fixtures/
|
6
|
+
require_relative '../../support/fixtures/app'
|
7
7
|
|
8
|
-
include Eipiai::
|
8
|
+
include Eipiai::TestApp
|
9
9
|
|
10
|
-
|
10
|
+
WebmachineApp = Webmachine::Application.new do |app|
|
11
11
|
app.routes do
|
12
12
|
add ['api'], Eipiai::ApiResource
|
13
13
|
|
14
14
|
add ['items'], ItemsResource
|
15
15
|
add ['item', :item_uid], ItemResource
|
16
|
+
add ['users'], UsersResource
|
17
|
+
add ['user', :user_uid], UserResource
|
16
18
|
end
|
17
19
|
|
18
20
|
app.configure do |config|
|
@@ -23,5 +25,5 @@ App = Webmachine::Application.new do |app|
|
|
23
25
|
end
|
24
26
|
|
25
27
|
def app
|
26
|
-
|
28
|
+
WebmachineApp.adapter
|
27
29
|
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
Feature: Object validation
|
2
|
+
|
3
|
+
When accepting external requests to create objects
|
4
|
+
I want to validate the requests
|
5
|
+
So that invalid requests are caught correctly
|
6
|
+
|
7
|
+
Scenario: Invalid user
|
8
|
+
Given the client provides the header "Content-Type: application/json"
|
9
|
+
When the client does a POST request to the "users" resource with the following content:
|
10
|
+
"""json
|
11
|
+
{
|
12
|
+
"first_name": "Bart"
|
13
|
+
}
|
14
|
+
"""
|
15
|
+
Then the status code should be "422" (Unprocessable Entity)
|
16
|
+
And the response should be JSON:
|
17
|
+
"""json
|
18
|
+
{
|
19
|
+
"_errors": [
|
20
|
+
{
|
21
|
+
"id": "MissingUid",
|
22
|
+
"message": "missing uid"
|
23
|
+
}
|
24
|
+
]
|
25
|
+
}
|
26
|
+
"""
|
27
|
+
|
28
|
+
Scenario: Uniqueness constraint
|
29
|
+
Given the user with uid "bartsimpson" exists
|
30
|
+
Given the client provides the header "Content-Type: application/json"
|
31
|
+
When the client does a POST request to the "users" resource with the following content:
|
32
|
+
"""json
|
33
|
+
{
|
34
|
+
"uid": "bartsimpson"
|
35
|
+
}
|
36
|
+
"""
|
37
|
+
Then the status code should be "422" (Unprocessable Entity)
|
38
|
+
And the response should be JSON:
|
39
|
+
"""json
|
40
|
+
{
|
41
|
+
"_errors": [
|
42
|
+
{
|
43
|
+
"id": "ResourceExists",
|
44
|
+
"message": "resource exists"
|
45
|
+
}
|
46
|
+
]
|
47
|
+
}
|
48
|
+
"""
|
data/features/webmachine.feature
CHANGED
@@ -2,7 +2,7 @@ Feature: Webmachine CRUD operations
|
|
2
2
|
|
3
3
|
When a new JSON API service needs to be developed
|
4
4
|
I want to use Eipiai's default CRUD setup
|
5
|
-
So that the basic API operations are quick and
|
5
|
+
So that the basic API operations are quick and easy to implement
|
6
6
|
|
7
7
|
Scenario: List main API endpoint
|
8
8
|
When the client does a GET request to "/api"
|
@@ -23,6 +23,13 @@ Feature: Webmachine CRUD operations
|
|
23
23
|
"item": {
|
24
24
|
"templated": true,
|
25
25
|
"href": "https://example.org/item/{item_uid}"
|
26
|
+
},
|
27
|
+
"users": {
|
28
|
+
"href": "https://example.org/users"
|
29
|
+
},
|
30
|
+
"user": {
|
31
|
+
"templated": true,
|
32
|
+
"href": "https://example.org/user/{user_uid}"
|
26
33
|
}
|
27
34
|
}
|
28
35
|
}
|
@@ -71,7 +78,7 @@ Feature: Webmachine CRUD operations
|
|
71
78
|
|
72
79
|
Scenario: Update resource
|
73
80
|
Given the item with uid "hello" and the price "10" exists
|
74
|
-
|
81
|
+
And the client provides the header "Content-Type: application/json"
|
75
82
|
When the client does a PUT request to the "item" resource with the template variable "item_uid" set to "hello" and the following content:
|
76
83
|
"""json
|
77
84
|
{
|
@@ -112,18 +119,7 @@ Feature: Webmachine CRUD operations
|
|
112
119
|
"href": "https://example.org/item/hello"
|
113
120
|
}
|
114
121
|
]
|
115
|
-
}
|
116
|
-
"b:items": [
|
117
|
-
{
|
118
|
-
"_links": {
|
119
|
-
"self": {
|
120
|
-
"href": "https://example.org/item/hello"
|
121
|
-
}
|
122
|
-
},
|
123
|
-
"uid": "hello",
|
124
|
-
"price": 10
|
125
|
-
}
|
126
|
-
]
|
122
|
+
}
|
127
123
|
}
|
128
124
|
"""
|
129
125
|
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# FormattedErrors
|
2
|
+
#
|
3
|
+
# copy/paste of the https://github.com/blendle/formatted_errors gem, until that
|
4
|
+
# gem is made open source.
|
5
|
+
#
|
6
|
+
module FormattedErrors
|
7
|
+
class << self
|
8
|
+
def parse(*errors)
|
9
|
+
# First check for array, don't switch these lines as the hash is converted to
|
10
|
+
# an array
|
11
|
+
errors = errors.flatten(1) if errors.length == 1 && errors.first.is_a?(Array)
|
12
|
+
errors = errors.first if errors.length == 1 && errors.first.is_a?(Hash)
|
13
|
+
|
14
|
+
errors = errors.map do |key, value|
|
15
|
+
parse_single(key, **value || {})
|
16
|
+
end
|
17
|
+
|
18
|
+
{ _errors: errors }
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def parse_single(error, **properties)
|
24
|
+
{
|
25
|
+
id: capitalize(error.to_s),
|
26
|
+
message: humanize(error.to_s)
|
27
|
+
}.merge(properties)
|
28
|
+
end
|
29
|
+
|
30
|
+
def capitalize(string)
|
31
|
+
string.split('_').map(&:capitalize).join
|
32
|
+
end
|
33
|
+
|
34
|
+
def humanize(string)
|
35
|
+
string.split('_').join(' ').downcase
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module Eipiai
|
2
|
+
# Validator
|
3
|
+
#
|
4
|
+
# default module used for validation and error handling. Any validator should
|
5
|
+
# always inherit from this validator.
|
6
|
+
#
|
7
|
+
module Validator
|
8
|
+
NotImplementedError = Class.new(StandardError)
|
9
|
+
|
10
|
+
# valid?
|
11
|
+
#
|
12
|
+
# runs validators and returns `true` if no validations failed, otherwise
|
13
|
+
# returns `false`.
|
14
|
+
#
|
15
|
+
# @example successful validation
|
16
|
+
# Item.new(uid: 'valid').extend(ItemValidator).valid? # => true
|
17
|
+
#
|
18
|
+
# @example failed validation
|
19
|
+
# Item.new.extend(ItemValidator).valid? # => false
|
20
|
+
#
|
21
|
+
# @return [true, false] status of validators
|
22
|
+
#
|
23
|
+
def valid?
|
24
|
+
validate
|
25
|
+
end
|
26
|
+
|
27
|
+
# invalid?
|
28
|
+
#
|
29
|
+
# @see #valid?
|
30
|
+
# @return [true, false] opposite return value of #valid?
|
31
|
+
#
|
32
|
+
def invalid?
|
33
|
+
!valid?
|
34
|
+
end
|
35
|
+
|
36
|
+
# validate
|
37
|
+
#
|
38
|
+
# This method needs to be implemented in any validators that include this
|
39
|
+
# base validator.
|
40
|
+
#
|
41
|
+
# If the validator does not define this method, a `NotImplementedError`
|
42
|
+
# error is raised.
|
43
|
+
#
|
44
|
+
# @example return NotImplementedError
|
45
|
+
# Item.new.extend(InvalidValidator).validate
|
46
|
+
# # => raise Eipiai::Validator::NotImplementedError, "#validate required"
|
47
|
+
#
|
48
|
+
# @return [void]
|
49
|
+
#
|
50
|
+
def validate
|
51
|
+
fail NotImplementedError, '#validate required'
|
52
|
+
end
|
53
|
+
|
54
|
+
# errors
|
55
|
+
#
|
56
|
+
# Set of errors filled during validation. To set errors, see `#add_error`.
|
57
|
+
#
|
58
|
+
# @example
|
59
|
+
# item = Item.new.extend(ItemValidator)
|
60
|
+
# item.valid?
|
61
|
+
# item.errors.to_a # => [:missing_uid]
|
62
|
+
#
|
63
|
+
# @return [Set] set of errors
|
64
|
+
#
|
65
|
+
def errors
|
66
|
+
@errors ||= Set.new
|
67
|
+
end
|
68
|
+
|
69
|
+
# add_error
|
70
|
+
#
|
71
|
+
# given an object, the object is added to the set of errors.
|
72
|
+
#
|
73
|
+
# The method always returns `false`. This can be used to easily check for
|
74
|
+
# errors and add an error message in one go:
|
75
|
+
#
|
76
|
+
# uid.present? || add_error(:uid_missing)
|
77
|
+
#
|
78
|
+
# The above example will return true if the uid exists, or add the error and
|
79
|
+
# return false to indicate the validation check failed.
|
80
|
+
#
|
81
|
+
# @example
|
82
|
+
# item = Item.new.extend(ItemValidator)
|
83
|
+
# item.add_error(:hello_world) # => false
|
84
|
+
# item.errors.to_a # => [:hello_world]
|
85
|
+
#
|
86
|
+
# @param [Object] error object to add to the errors set
|
87
|
+
# @return [false]
|
88
|
+
#
|
89
|
+
def add_error(error)
|
90
|
+
errors << error
|
91
|
+
false
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'eipiai/validation/validators/base'
|
2
|
+
|
3
|
+
module Eipiai
|
4
|
+
# SequelValidator
|
5
|
+
#
|
6
|
+
# provides default validators specific for Sequel-backed models.
|
7
|
+
#
|
8
|
+
# This validator expects each model to have a `uid` attribute, which
|
9
|
+
# represents the "public" unique identifier of the object.
|
10
|
+
#
|
11
|
+
module SequelValidator
|
12
|
+
include Validator
|
13
|
+
|
14
|
+
# valid_uid?
|
15
|
+
#
|
16
|
+
# validates the presence of the object uid.
|
17
|
+
#
|
18
|
+
# @example valid uid
|
19
|
+
# Item.new(uid: 'valid').extend(ItemSequelValidator).valid_uid? # => true
|
20
|
+
#
|
21
|
+
# @example invalid uid
|
22
|
+
# Item.new.extend(ItemSequelValidator).valid_uid? # => false
|
23
|
+
#
|
24
|
+
# @return [true, false] validation status of resource's uid attribute
|
25
|
+
#
|
26
|
+
def valid_uid?
|
27
|
+
uid.present? || add_error(:missing_uid)
|
28
|
+
end
|
29
|
+
|
30
|
+
# unique?
|
31
|
+
#
|
32
|
+
# returns `true` if an object with the same `uid` already exists.
|
33
|
+
#
|
34
|
+
# @example
|
35
|
+
# item = Item.new(uid: 'hello').extend(ItemSequelValidator)
|
36
|
+
# item.unique? # => true
|
37
|
+
# item.save
|
38
|
+
# item.unique? # => false
|
39
|
+
#
|
40
|
+
# @return [true, false] uniqueness status of object, based on uid attribute
|
41
|
+
#
|
42
|
+
def unique?
|
43
|
+
self.class.first(uid: uid).blank? || add_error(:resource_exists)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/eipiai/version.rb
CHANGED
@@ -7,6 +7,30 @@ module Webmachine
|
|
7
7
|
# @see http://git.io/vWZFi
|
8
8
|
#
|
9
9
|
class Request
|
10
|
+
# json?
|
11
|
+
#
|
12
|
+
# returns `true` if the Content-Type header is of type `application/json`.
|
13
|
+
#
|
14
|
+
# @example Content-Type: application/json
|
15
|
+
# post('/items', '{}', 'Content-Type': 'application/json')
|
16
|
+
# resource.request.json? # => true
|
17
|
+
#
|
18
|
+
# @example Content-Type: application/json; charset=utf-8
|
19
|
+
# post('/items', '{}', 'Content-Type': 'application/json; charset=utf-8')
|
20
|
+
# resource.request.json? # => true
|
21
|
+
#
|
22
|
+
# @example unset Content-Type header
|
23
|
+
# post('/items', '{}')
|
24
|
+
# resource.request.json? # => false
|
25
|
+
#
|
26
|
+
# @return [true, false]
|
27
|
+
#
|
28
|
+
def json?
|
29
|
+
return false unless content_type
|
30
|
+
|
31
|
+
Webmachine::MediaType.parse(content_type).type_matches?('application/json')
|
32
|
+
end
|
33
|
+
|
10
34
|
# URIReplacement
|
11
35
|
#
|
12
36
|
# Swaps `Webmachine::Request#uri` from `URI` to `Addressable::URI`.
|
@@ -29,6 +29,50 @@ module Eipiai
|
|
29
29
|
base.extend(ClassMethods)
|
30
30
|
end
|
31
31
|
|
32
|
+
# malformed_request?
|
33
|
+
#
|
34
|
+
# return `true` if content_type is of type `application/json`, and the JSON
|
35
|
+
# request body cannot be parsed.
|
36
|
+
#
|
37
|
+
# @example valid JSON payload
|
38
|
+
# post('/items', '{ "uid": "hello" }', 'Content-Type': 'application/json')
|
39
|
+
# resource.malformed_request? # => false
|
40
|
+
#
|
41
|
+
# @example invalid JSON payload
|
42
|
+
# post('/items', '{ invalid! }', 'Content-Type': 'application/json')
|
43
|
+
# resource.malformed_request? # => true
|
44
|
+
#
|
45
|
+
# @return [true, false]
|
46
|
+
#
|
47
|
+
def malformed_request?
|
48
|
+
return false unless request.json? && request.body.to_s.present?
|
49
|
+
|
50
|
+
JSON.parse(request.body.to_s) && false
|
51
|
+
rescue JSON::ParserError
|
52
|
+
json_error_body(:invalid_json)
|
53
|
+
end
|
54
|
+
|
55
|
+
# unprocessable_entity?
|
56
|
+
#
|
57
|
+
# return `true` if content_type is of type `application/json`, and the newly
|
58
|
+
# generated object reports back as "invalid".
|
59
|
+
#
|
60
|
+
# @example invalid object
|
61
|
+
# post('/users', '{}', 'Content-Type': 'application/json')
|
62
|
+
# resource.unprocessable_entity? # => true
|
63
|
+
#
|
64
|
+
# @example valid object
|
65
|
+
# post('/users', '{ "uid": "bartsimpson" }', 'Content-Type': 'application/json')
|
66
|
+
# resource.unprocessable_entity? # => false
|
67
|
+
#
|
68
|
+
# @return [true, false]
|
69
|
+
#
|
70
|
+
def unprocessable_entity?
|
71
|
+
return false unless request.json? && new_object.respond_to?(:invalid?)
|
72
|
+
|
73
|
+
new_object.invalid? && json_error_body(*new_object.errors)
|
74
|
+
end
|
75
|
+
|
32
76
|
def content_types_provided
|
33
77
|
[['application/hal+json', :to_json]]
|
34
78
|
end
|
@@ -182,6 +226,12 @@ module Eipiai
|
|
182
226
|
nil
|
183
227
|
end
|
184
228
|
|
229
|
+
def json_error_body(errors)
|
230
|
+
response.headers['Content-Type'] = 'application/json'
|
231
|
+
response.body = FormattedErrors.parse(errors).to_json
|
232
|
+
true
|
233
|
+
end
|
234
|
+
|
185
235
|
# ClassMethods
|
186
236
|
#
|
187
237
|
# These methods will be defined on the class in which this module is
|
@@ -15,12 +15,6 @@ module Eipiai
|
|
15
15
|
%w(GET POST)
|
16
16
|
end
|
17
17
|
|
18
|
-
# TODO(JeanMertz): enable when validators are implemented.
|
19
|
-
#
|
20
|
-
# def unprocessable_entity?
|
21
|
-
# super && json_error_body(*new_object.errors)
|
22
|
-
# end
|
23
|
-
|
24
18
|
def post_is_create?
|
25
19
|
true
|
26
20
|
end
|
@@ -36,8 +30,8 @@ module Eipiai
|
|
36
30
|
private
|
37
31
|
|
38
32
|
def content_type_handler
|
39
|
-
content_type = response.headers[
|
40
|
-
media_type =
|
33
|
+
content_type = response.headers[Webmachine::CONTENT_TYPE]
|
34
|
+
media_type = Webmachine::MediaType.parse(content_type)
|
41
35
|
|
42
36
|
content_types_provided.find { |ct, _| media_type.type_matches?(ct) }.last
|
43
37
|
end
|
@@ -19,15 +19,6 @@ module Eipiai
|
|
19
19
|
%w(GET PUT DELETE)
|
20
20
|
end
|
21
21
|
|
22
|
-
# TODO(JeanMertz): enable when validators are implemented.
|
23
|
-
#
|
24
|
-
# def unprocessable_entity?
|
25
|
-
# return unless super
|
26
|
-
# return if request.put? && new_object.errors.include?(:resource_exists)
|
27
|
-
#
|
28
|
-
# json_error_body(*new_object.errors)
|
29
|
-
# end
|
30
|
-
|
31
22
|
def resource_exists?
|
32
23
|
object.present?
|
33
24
|
end
|
data/lib/eipiai.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: eipiai
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jean Mertz
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-12-
|
11
|
+
date: 2015-12-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -231,6 +231,7 @@ files:
|
|
231
231
|
- ".hubbit.yml"
|
232
232
|
- ".rubocop.yml"
|
233
233
|
- ".wercker.yml"
|
234
|
+
- CHANGELOG.md
|
234
235
|
- Gemfile
|
235
236
|
- README.md
|
236
237
|
- Rakefile
|
@@ -238,10 +239,15 @@ files:
|
|
238
239
|
- features/support/app.rb
|
239
240
|
- features/support/db.rb
|
240
241
|
- features/support/env.rb
|
242
|
+
- features/validation.feature
|
241
243
|
- features/webmachine.feature
|
242
244
|
- lib/eipiai.rb
|
243
245
|
- lib/eipiai/roar.rb
|
244
246
|
- lib/eipiai/roar/representers/api.rb
|
247
|
+
- lib/eipiai/validation.rb
|
248
|
+
- lib/eipiai/validation/concerns/formatted_errors.rb
|
249
|
+
- lib/eipiai/validation/validators/base.rb
|
250
|
+
- lib/eipiai/validation/validators/sequel.rb
|
245
251
|
- lib/eipiai/version.rb
|
246
252
|
- lib/eipiai/webmachine.rb
|
247
253
|
- lib/eipiai/webmachine/ext/decision.rb
|