onsi 0.2.1 → 0.3.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/.rubocop.yml +8 -0
- data/Gemfile.lock +1 -1
- data/README.md +116 -2
- data/lib/onsi/error_responder.rb +139 -0
- data/lib/onsi/params.rb +17 -0
- data/lib/onsi/version.rb +1 -1
- data/lib/onsi.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ed92bde9b8d7540e7d542621101fedc2cb414aaa07d24c751d57d7b4454b0bcb
|
4
|
+
data.tar.gz: 441c0cd6376b4bcc6c086acb0cbec596fed03f0a190a8096e8dac639be4a28d9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b1b31fa4f17aad3767d5f4f2b46bf92e52ff6bd9ec7343ab4e742796d41f6791f5944b07378ec3dfd16d11b2b1caca07608fea9ad3dcb911b1a89318d8be15cd
|
7
|
+
data.tar.gz: 4e0e591bc4fea1df9aeedc73b6f558a12e7a41b42f65a780eff24784bad73c709731af923e1fd97d0346295ff911b66584cd11e74584071f6f0bffbbef176aef
|
data/.rubocop.yml
CHANGED
@@ -19,6 +19,11 @@ Metrics/ParameterLists:
|
|
19
19
|
Metrics/CyclomaticComplexity:
|
20
20
|
Max: 10
|
21
21
|
|
22
|
+
Metrics/BlockLength:
|
23
|
+
Enabled: true
|
24
|
+
Exclude:
|
25
|
+
- spec/**/*
|
26
|
+
|
22
27
|
DotPosition:
|
23
28
|
EnforcedStyle: leading
|
24
29
|
|
@@ -51,3 +56,6 @@ Style/RaiseArgs:
|
|
51
56
|
|
52
57
|
Style/FrozenStringLiteralComment:
|
53
58
|
Enabled: false
|
59
|
+
|
60
|
+
Style/IfUnlessModifier:
|
61
|
+
Enabled: false
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
# Onsi
|
2
2
|
|
3
|
-
Used to generate API responses from a
|
3
|
+
Used to generate API responses from a Rails App.
|
4
4
|
|
5
|
-
|
5
|
+
***
|
6
6
|
|
7
7
|
[](https://circleci.com/gh/skylarsch/onsi)
|
8
8
|
|
@@ -10,3 +10,117 @@ Used to generate API responses from a rails App.
|
|
10
10
|
|
11
11
|
[](https://codeclimate.com/github/skylarsch/onsi/test_coverage)
|
12
12
|
|
13
|
+
### Install
|
14
|
+
|
15
|
+
1. Add `gem 'onsi'` to your Gemfile
|
16
|
+
|
17
|
+
2. `bundle install`
|
18
|
+
|
19
|
+
## Getting Setup
|
20
|
+
|
21
|
+
### Controllers
|
22
|
+
|
23
|
+
`Onsi::Controller` handles rendering resources for you.
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
class PeopleController < ApplicationController
|
27
|
+
include Onsi::Controller
|
28
|
+
|
29
|
+
# Optional. By default Onsi will render `:v1`
|
30
|
+
render_version :v2
|
31
|
+
|
32
|
+
def show
|
33
|
+
@person = Person.find(params[:id])
|
34
|
+
# You can pass an Object, Enumerable, or Onsi::Resource
|
35
|
+
# Whatever you pass, the object or each element in the collection *MUST*
|
36
|
+
# include `Onsi::Model`
|
37
|
+
render_resource @person
|
38
|
+
end
|
39
|
+
end
|
40
|
+
```
|
41
|
+
|
42
|
+
### Models
|
43
|
+
|
44
|
+
Used to define your API resources. Calling the class method `api_render` will
|
45
|
+
allow you to begin setting up a version of your API. You're able to define as
|
46
|
+
many API versions as you would like. The default rendered if nothing is
|
47
|
+
specified in the `render_resource` method is `:v1`.
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
class Person < ApplicationRecord
|
51
|
+
include Onsi::Model
|
52
|
+
|
53
|
+
api_render(:v1) do
|
54
|
+
# Passing the name of the attribute only will call that name as a method on
|
55
|
+
# the instance of the method.
|
56
|
+
attribute(:first_name)
|
57
|
+
attribute(:last_name)
|
58
|
+
# You can give attribute a block and it will be called on the object
|
59
|
+
# instance. This lets you rename or compute attributes
|
60
|
+
attribute(:full_name) { "#{first_name} #{last_name}" }
|
61
|
+
|
62
|
+
# Relationship requires a minimum of 2 parameters. The first is the name
|
63
|
+
# of the relationship in the rendered JSON. The second is the type.
|
64
|
+
# When fetching the value, Onsi will add `_id` and call that method on the
|
65
|
+
# object instance. e.g. `team_id` in this case.
|
66
|
+
relationship(:team, :team)
|
67
|
+
|
68
|
+
# Relationships can take a block that will be called on the object instance
|
69
|
+
# and the return value will be used as the ID
|
70
|
+
relationship(:primary_email, :email) { emails.where(primary: true).first.id }
|
71
|
+
end
|
72
|
+
end
|
73
|
+
```
|
74
|
+
|
75
|
+
### Params
|
76
|
+
|
77
|
+
`Onsi::Params` can be used to flatten params that come into the API.
|
78
|
+
|
79
|
+
Calling `.parse` will give you an instance of `Onsi::Params` with whitelisted
|
80
|
+
attributes & relationships. The first argument is the params from the controller.
|
81
|
+
The second is an array of attributes and the third is an array of relationships.
|
82
|
+
|
83
|
+
Calling `#flatten` will merge the attributes & relationships.
|
84
|
+
|
85
|
+
```json
|
86
|
+
{
|
87
|
+
"data": {
|
88
|
+
"type": "person",
|
89
|
+
"attributes": {
|
90
|
+
"first_name": "Skylar",
|
91
|
+
"last_name": "Schipper",
|
92
|
+
"bad_value": "'); DROP TABLE `people`; --"
|
93
|
+
},
|
94
|
+
"relationships": {
|
95
|
+
"team": {
|
96
|
+
"data": {
|
97
|
+
"type": "team",
|
98
|
+
"id": "1"
|
99
|
+
}
|
100
|
+
},
|
101
|
+
"unknown": {
|
102
|
+
"data": [
|
103
|
+
{ "type": "foo", "id": "1" }
|
104
|
+
]
|
105
|
+
}
|
106
|
+
}
|
107
|
+
}
|
108
|
+
}
|
109
|
+
```
|
110
|
+
|
111
|
+
Flattened gives you:
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
{ "first_name" => "Skylar", "last_name" => "Schipper", "team_id" => "1" }
|
115
|
+
```
|
116
|
+
|
117
|
+
```ruby
|
118
|
+
class PeopleController < ApplicationController
|
119
|
+
include Onsi::Controller
|
120
|
+
|
121
|
+
def create
|
122
|
+
attributes = Onsi::Param.parse(params, [:first_name, :last_name], [:team])
|
123
|
+
render_resource Person.create!(attributes.flatten)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
```
|
@@ -0,0 +1,139 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
|
3
|
+
module Onsi
|
4
|
+
module ErrorResponder
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
rescue_from StandardError, with: :render_error_500
|
9
|
+
rescue_from ActiveRecord::RecordNotFound, with: :render_error_404
|
10
|
+
rescue_from ActiveRecord::RecordInvalid, with: :render_error_422
|
11
|
+
rescue_from ActionController::ParameterMissing, with: :respond_param_error_400
|
12
|
+
rescue_from Onsi::Params::MissingReqiredAttribute, with: :respond_missing_attr_error_400
|
13
|
+
rescue_from Onsi::Params::RelationshipNotFound, with: :respond_missing_relationship_error_400
|
14
|
+
rescue_from Onsi::Errors::UnknownVersionError, with: :respond_invalid_version_error_400
|
15
|
+
end
|
16
|
+
|
17
|
+
def render_error(response)
|
18
|
+
render(response.renderable)
|
19
|
+
end
|
20
|
+
|
21
|
+
def render_error_500(error)
|
22
|
+
notify_unhandled_exception(error)
|
23
|
+
response = ErrorResponse.new(500)
|
24
|
+
response.add(500, 'internal_server_error', meta: error_metadata(error))
|
25
|
+
render_error(response)
|
26
|
+
end
|
27
|
+
|
28
|
+
def render_error_404(_error)
|
29
|
+
response = ErrorResponse.new(404)
|
30
|
+
response.add(404, 'not_found')
|
31
|
+
render_error(response)
|
32
|
+
end
|
33
|
+
|
34
|
+
def render_error_422(error)
|
35
|
+
response = ErrorResponse.new(422)
|
36
|
+
error.record.errors.details.each do |name, details|
|
37
|
+
details.each do |info|
|
38
|
+
response.add(
|
39
|
+
422,
|
40
|
+
'validation_error',
|
41
|
+
title: "Validation Error: #{info[:error]}",
|
42
|
+
meta: info.merge(param: name)
|
43
|
+
)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
render_error(response)
|
47
|
+
end
|
48
|
+
|
49
|
+
def respond_param_error_400(error)
|
50
|
+
response = ErrorResponse.new(400)
|
51
|
+
response.add(
|
52
|
+
400,
|
53
|
+
'missing_parameter',
|
54
|
+
meta: { param: error.param }
|
55
|
+
)
|
56
|
+
render_error(response)
|
57
|
+
end
|
58
|
+
|
59
|
+
def respond_missing_relationship_error_400(error)
|
60
|
+
response = ErrorResponse.new(400)
|
61
|
+
response.add(
|
62
|
+
400,
|
63
|
+
'missing_relationship',
|
64
|
+
meta: { param: error.key }
|
65
|
+
)
|
66
|
+
render_error(response)
|
67
|
+
end
|
68
|
+
|
69
|
+
def respond_invalid_version_error_400(error)
|
70
|
+
notify_unhandled_exception(error)
|
71
|
+
response = ErrorResponse.new(400)
|
72
|
+
response.add(
|
73
|
+
400,
|
74
|
+
'invalid_version',
|
75
|
+
details: "API version #{error.version} unsupported for #{error.klass.name.underscore}"
|
76
|
+
)
|
77
|
+
render_error(response)
|
78
|
+
end
|
79
|
+
|
80
|
+
def respond_missing_attr_error_400(error)
|
81
|
+
response = ErrorResponse.new(400)
|
82
|
+
response.add(
|
83
|
+
400,
|
84
|
+
'missing_attribute',
|
85
|
+
meta: {
|
86
|
+
attribute: error.attribute
|
87
|
+
}
|
88
|
+
)
|
89
|
+
render_error(response)
|
90
|
+
end
|
91
|
+
|
92
|
+
def notify_unhandled_exception(exception)
|
93
|
+
Rails.logger.error "Unhandled Exception `#{exception.class.name}: #{exception.message}`"
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def error_metadata(error)
|
99
|
+
return nil unless Rails.configuration.consider_all_requests_local
|
100
|
+
{
|
101
|
+
exception: {
|
102
|
+
class: error.class.name,
|
103
|
+
message: error.message,
|
104
|
+
backtrace: error.backtrace
|
105
|
+
}
|
106
|
+
}
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
class ErrorResponse
|
111
|
+
attr_reader :status
|
112
|
+
|
113
|
+
def initialize(status)
|
114
|
+
@status = status
|
115
|
+
@errors = []
|
116
|
+
end
|
117
|
+
|
118
|
+
def add(status, code, title: nil, details: nil, meta: nil)
|
119
|
+
@errors << {}.tap do |err|
|
120
|
+
err[:status] = (status || @status).to_s
|
121
|
+
err[:code] = code
|
122
|
+
err[:title] = title if title.present?
|
123
|
+
err[:detail] = details if details.present?
|
124
|
+
err[:meta] = Hash(meta) if meta.present?
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def as_json
|
129
|
+
{ errors: @errors.as_json }
|
130
|
+
end
|
131
|
+
|
132
|
+
def renderable
|
133
|
+
{
|
134
|
+
json: as_json,
|
135
|
+
status: status
|
136
|
+
}
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
data/lib/onsi/params.rb
CHANGED
@@ -9,6 +9,15 @@ module Onsi
|
|
9
9
|
end
|
10
10
|
end
|
11
11
|
|
12
|
+
class MissingReqiredAttribute < StandardError
|
13
|
+
attr_reader :attribute
|
14
|
+
|
15
|
+
def initialize(message, attr)
|
16
|
+
super(message)
|
17
|
+
@attribute = attr
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
12
21
|
class << self
|
13
22
|
def parse(params, attributes = [], relationships = [])
|
14
23
|
data = params.require(:data)
|
@@ -63,6 +72,14 @@ module Onsi
|
|
63
72
|
attributes.to_h.merge(relationships.to_h).with_indifferent_access
|
64
73
|
end
|
65
74
|
|
75
|
+
def require(key)
|
76
|
+
value = attributes.to_h.with_indifferent_access[key]
|
77
|
+
if value.nil?
|
78
|
+
raise MissingReqiredAttribute.new("Missing attribute #{key}", key)
|
79
|
+
end
|
80
|
+
value
|
81
|
+
end
|
82
|
+
|
66
83
|
def safe_fetch(key)
|
67
84
|
yield(@relationships[key])
|
68
85
|
rescue ActiveRecord::RecordNotFound
|
data/lib/onsi/version.rb
CHANGED
data/lib/onsi.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: onsi
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Skylar Schipper
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-05-
|
11
|
+
date: 2018-05-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -164,6 +164,7 @@ files:
|
|
164
164
|
- bin/setup
|
165
165
|
- lib/onsi.rb
|
166
166
|
- lib/onsi/controller.rb
|
167
|
+
- lib/onsi/error_responder.rb
|
167
168
|
- lib/onsi/errors.rb
|
168
169
|
- lib/onsi/model.rb
|
169
170
|
- lib/onsi/params.rb
|