eipiai 0.1.0 → 0.2.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 -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 [![wercker status](https://app.wercker.com/status/afba12ab733cdd8a55cb39db0b68c275/s/master "wercker status")](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
|