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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 240457c28d5920df4c1ece3e02958683cb4ce64880d62c04508b4d61f6f209c4
4
- data.tar.gz: 7cf5164177f088a50d759c1bbb0a5d71f50c8c02196583391d5d5ea3733d2b23
3
+ metadata.gz: ed92bde9b8d7540e7d542621101fedc2cb414aaa07d24c751d57d7b4454b0bcb
4
+ data.tar.gz: 441c0cd6376b4bcc6c086acb0cbec596fed03f0a190a8096e8dac639be4a28d9
5
5
  SHA512:
6
- metadata.gz: ce09111ef7c6b0f5f61fae71fd58c922650aa800f17ac6910805537be0c5f2f9a9b13991ea4c98887baf206fb2ccde9a3ae324e371d01a1d61b7ce9a063eff20
7
- data.tar.gz: fd0da8ddb1f5a11be31a8f78acefd751e1ef020a0c3cbdc3eb2c3b92a53dc60888b7d317906de77fdf75468651a37fcb313b833db2676841b63826e4ea1284e9
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
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- onsi (0.2.1)
4
+ onsi (0.3.0)
5
5
  rails (>= 5.0, < 6.0)
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Onsi
2
2
 
3
- Used to generate API responses from a rails App.
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
@@ -1,3 +1,3 @@
1
1
  module Onsi
2
- VERSION = '0.2.1'.freeze
2
+ VERSION = '0.3.0'.freeze
3
3
  end
data/lib/onsi.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'onsi/controller'
2
+ require 'onsi/error_responder'
2
3
  require 'onsi/errors'
3
4
  require 'onsi/model'
4
5
  require 'onsi/params'
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.2.1
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-14 00:00:00.000000000 Z
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