onsi 0.2.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![CircleCI](https://circleci.com/gh/skylarsch/onsi.svg?style=svg)](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
|
[![Test Coverage](https://api.codeclimate.com/v1/badges/c3ee44371f7565f2709c/test_coverage)](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
|