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