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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 877a72bd72e8b2b172a6f1d5e534c938b12dc675
4
- data.tar.gz: 3b11eae2304fa9a61a2ccb45e78eb0c2a1be17ff
3
+ metadata.gz: 11411b6b54cd8d262763b931fd39c8cc07267b01
4
+ data.tar.gz: b0fd69f854bee571d6a064c594e44ccfea576b98
5
5
  SHA512:
6
- metadata.gz: ea7009b9c493cb4e757c5ee6f3a19a0db3ead47df7c5dcd555acf73e01565e97469529a596f031924331821029241d3ccbab328092b0caade6cb7d5c7b896f75
7
- data.tar.gz: 5a1a3e46d6ed34468c386d1388315a9d2d2235563bbfa1c9b0e00906d97958b7cfa87ef5aecffaec29f8647723bac4e234d8046a637737d0baab520319d37975
6
+ metadata.gz: 9e819fa5def0aa433c40fb91a170f0b18215e460132dd4c7c75987fcf3e169ac594b3d30e3f58461cc398239c37b060721f8b5201c2f790ac23bda0122e1999d
7
+ data.tar.gz: 6644c4fb34968fcb2bb987929852d223f1a24e0404e2ada895578de0da41b5a9dff53fa6289d317cb6e75316efbd2ed46ddea81d4c93ff540f09fdfa6dd5b416
data/.gitignore CHANGED
@@ -3,6 +3,7 @@ _cache
3
3
  _projects
4
4
  _steps
5
5
  .bundle
6
+ .DS_Store
6
7
  .yardoc
7
8
  *.gem
8
9
  doc/
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
 
@@ -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/webmachine/app'
6
+ require_relative '../../support/fixtures/app'
7
7
 
8
- include Eipiai::Webmachine::TestApp
8
+ include Eipiai::TestApp
9
9
 
10
- App = Webmachine::Application.new do |app|
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
- App.adapter
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
+ """
@@ -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 easyi to implement
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
- Given the client provides the header "Content-Type: application/json"
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
@@ -0,0 +1,4 @@
1
+ require 'eipiai/validation/concerns/formatted_errors'
2
+
3
+ require 'eipiai/validation/validators/base'
4
+ require 'eipiai/validation/validators/sequel'
@@ -3,5 +3,5 @@
3
3
  # The current version of the Eipiai library.
4
4
  #
5
5
  module Eipiai
6
- VERSION = '0.1.0'
6
+ VERSION = '0.2.0'
7
7
  end
@@ -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[::Webmachine::CONTENT_TYPE]
40
- media_type = ::Webmachine::MediaType.parse(content_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
@@ -1,2 +1,3 @@
1
1
  require 'eipiai/roar'
2
+ require 'eipiai/validation'
2
3
  require 'eipiai/webmachine'
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.1.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-12 00:00:00.000000000 Z
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