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