onsi 0.8.0 → 1.0.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
  SHA1:
3
- metadata.gz: 96d2e299515f7829ffc2f93f76dfc26cbe8a25de
4
- data.tar.gz: 67855153d40ba6e49e46d0d2e87af9f9ef587987
3
+ metadata.gz: 4603a96ecc939b0121b0ba15b31f09fb54f6078c
4
+ data.tar.gz: 0b12f9fead4d425b6a64555d10214d8ea2f5d7aa
5
5
  SHA512:
6
- metadata.gz: 62ee8543afa30b562aad29d3ed678ab47f930b03761f3cd01c7f6e76cb68aba37f8317df61dfaae4004125aac164813dcf02e74a90788e67ce01780e952d3407
7
- data.tar.gz: a8a4a5be644d66942fc3fb362e9692fec191c1fca43e1b1202d1678326e66da44e2d3616bcf0e4c8d582aac65788e37cc45b626b9017ebb4ff7ecba788fb5396
6
+ metadata.gz: 738ea3ac8f9fc4fc118130c9b9760c66a1ca49d3bc5c6efd0870d7f052f6f578383cb357439e1084aa9cbc8675f172f322c291540b58f9b3239a2a367ae4dba1
7
+ data.tar.gz: fa8db433e03643c00579eb51f0d3d8c0e186ccee4aec830563aa014358d53142b87a03b5ab287e01ea2de310ffd03a8eed5f3b29c5bf8432102937053a664d49
@@ -2,7 +2,7 @@ version: 2
2
2
  jobs:
3
3
  latest:
4
4
  environment:
5
- CC_TEST_REPORTER_ID: 15f1ab72e4d38e92cc8ffe4490022ae9bdc9265a0319c30e673ee3e4e7a5a371
5
+ CC_TEST_REPORTER_ID: 7e6c6740d17509f50ec9c750311962b3b3fcb2d3a7033c2f664dc3b012bd9439
6
6
  docker:
7
7
  - image: circleci/ruby:latest
8
8
  working_directory: ~/repo
@@ -4,4 +4,4 @@ engines:
4
4
  rubocop:
5
5
  enabled: true
6
6
  bundler-audit:
7
- enabled: true
7
+ enabled: false
@@ -0,0 +1,4 @@
1
+ --no-private
2
+ lib/**/*.rb
3
+ README
4
+ CHANGELOG
@@ -0,0 +1,11 @@
1
+ # Version 1.0.0
2
+
3
+ ## Released November 15, 2018
4
+
5
+ ### Changes
6
+
7
+ - The first official release of Onsi.
8
+
9
+ ### Upgrading
10
+
11
+ - There are no breaking changes from 0.8.0 to 1.0.0
data/README.md CHANGED
@@ -4,11 +4,11 @@ Used to generate API responses from a Rails App.
4
4
 
5
5
  ***
6
6
 
7
- [![CircleCI](https://circleci.com/gh/skylarsch/onsi.svg?style=svg)](https://circleci.com/gh/skylarsch/onsi)
7
+ [![CircleCI](https://circleci.com/gh/maddiesch/onsi.svg?style=svg)](https://circleci.com/gh/maddiesch/onsi)
8
8
 
9
- [![Maintainability](https://api.codeclimate.com/v1/badges/c3ee44371f7565f2709c/maintainability)](https://codeclimate.com/github/skylarsch/onsi/maintainability)
9
+ [![Maintainability](https://api.codeclimate.com/v1/badges/8d21ec50b172146416c9/maintainability)](https://codeclimate.com/github/maddiesch/onsi/maintainability)
10
10
 
11
- [![Test Coverage](https://api.codeclimate.com/v1/badges/c3ee44371f7565f2709c/test_coverage)](https://codeclimate.com/github/skylarsch/onsi/test_coverage)
11
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/8d21ec50b172146416c9/test_coverage)](https://codeclimate.com/github/maddiesch/onsi/test_coverage)
12
12
 
13
13
  ### Install
14
14
 
data/Rakefile CHANGED
@@ -1,6 +1,19 @@
1
1
  require 'bundler/gem_tasks'
2
2
  require 'rspec/core/rake_task'
3
+ require 'yard'
3
4
 
4
5
  RSpec::Core::RakeTask.new(:spec)
5
6
 
6
7
  task default: :spec
8
+
9
+ namespace :docs do
10
+ desc 'Generate docs'
11
+ task :generate do
12
+ YARD::CLI::Yardoc.run
13
+ end
14
+
15
+ desc 'Get docs stats'
16
+ task :stats do
17
+ YARD::CLI::Stats.run('--list-undoc')
18
+ end
19
+ end
@@ -7,5 +7,7 @@ require 'onsi/params'
7
7
  require 'onsi/resource'
8
8
  require 'onsi/version'
9
9
 
10
+ ##
11
+ # Create versioned JSON-API objects.
10
12
  module Onsi
11
13
  end
@@ -1,20 +1,53 @@
1
1
  require 'active_support/concern'
2
2
 
3
3
  module Onsi
4
+ ##
5
+ # Helper methods for rendering API responses.
6
+ #
7
+ # @example
8
+ # class PersonController < ActionController::API
9
+ # include Onsi::Controller
10
+ #
11
+ # render_version(:v1)
12
+ #
13
+ # def show
14
+ # person = Person.find(params[:id])
15
+ # render_resource(person)
16
+ # end
17
+ # end
4
18
  module Controller
5
19
  extend ActiveSupport::Concern
6
20
 
21
+ ##
22
+ # Defines class methods available on the class.
7
23
  module ClassMethods
24
+ ##
25
+ # Set a controller wide default render version.
26
+ #
27
+ # @param version [Symbol] The version.
8
28
  def render_version(version = nil)
9
29
  @render_version = version if version
10
30
  @render_version
11
31
  end
12
32
 
33
+ ##
34
+ # Ensures that the render_version is set on a subclass
35
+ #
36
+ # @private
13
37
  def inherited(subclass)
14
38
  subclass.render_version(@render_version)
15
39
  end
16
40
  end
17
41
 
42
+ ##
43
+ # Render the JSON response.
44
+ #
45
+ # @param resource [Onsi::Resource, Enumerable, Onsi::Model]
46
+ #
47
+ # @param opts [Hash] The options hash. If a version is included that will
48
+ # take presidence over the controller default .render_version
49
+ #
50
+ # - The other keys for opts will be passed directly the #render method.
18
51
  def render_resource(resource, opts = {})
19
52
  version = opts.delete(:version) || self.class.render_version || Model::DEFAULT_API_VERSION
20
53
  payload = Resource.render(resource, version)
@@ -3,6 +3,25 @@ require 'active_support/concern'
3
3
  module Onsi
4
4
  ##
5
5
  # Handles default errors without StandardError
6
+ #
7
+ # Error handled by default:
8
+ # - ActiveRecord::RecordNotFound
9
+ # - ActiveRecord::RecordInvalid
10
+ # - ActionController::ParameterMissing
11
+ # - {Onsi::Params::MissingReqiredAttribute}
12
+ # - {Onsi::Params::RelationshipNotFound}
13
+ # - {Onsi::Errors::UnknownVersionError}
14
+ #
15
+ # @example
16
+ # class PeopleController < ApplicationController
17
+ # include Onsi::Controller
18
+ # include Onsi::ErrorResponderBase
19
+ #
20
+ # # ...
21
+ # end
22
+ #
23
+ # @author Maddie Schipper
24
+ # @since 1.0.0
6
25
  module ErrorResponderBase
7
26
  extend ActiveSupport::Concern
8
27
 
@@ -15,16 +34,45 @@ module Onsi
15
34
  rescue_from Onsi::Errors::UnknownVersionError, with: :respond_invalid_version_error_400
16
35
  end
17
36
 
37
+ ##
38
+ # Render an API error response.
39
+ #
40
+ # @param response [Onsi::ErrorResponse] The response object to render
18
41
  def render_error(response)
19
42
  render(response.renderable)
20
43
  end
21
44
 
45
+ ##
46
+ # Can be overriden to report an un-handled exception to your error service
47
+ # of choice.
48
+ #
49
+ # @param exception [StandardError] The error to report.
50
+ #
51
+ # @example
52
+ # class ApplicationController < ActionController::API
53
+ # include Onsi::ErrorResponderBase
54
+ # include Onsi::Controller
55
+ #
56
+ # def notify_unhandled_exception(exception)
57
+ # Bugsnag.notify(exception)
58
+ # end
59
+ #
60
+ # # ...
61
+ # end
62
+ def notify_unhandled_exception(exception)
63
+ Rails.logger.error "Unhandled Exception `#{exception.class.name}: #{exception.message}`"
64
+ end
65
+
66
+ ##
67
+ # @private
22
68
  def render_error_404(_error)
23
69
  response = ErrorResponse.new(404)
24
70
  response.add(404, 'not_found')
25
71
  render_error(response)
26
72
  end
27
73
 
74
+ ##
75
+ # @private
28
76
  def render_error_422(error)
29
77
  response = ErrorResponse.new(422)
30
78
  error.record.errors.details.each do |name, details|
@@ -40,6 +88,8 @@ module Onsi
40
88
  render_error(response)
41
89
  end
42
90
 
91
+ ##
92
+ # @private
43
93
  def respond_param_error_400(error)
44
94
  response = ErrorResponse.new(400)
45
95
  response.add(
@@ -50,6 +100,8 @@ module Onsi
50
100
  render_error(response)
51
101
  end
52
102
 
103
+ ##
104
+ # @private
53
105
  def respond_missing_relationship_error_400(error)
54
106
  response = ErrorResponse.new(400)
55
107
  response.add(
@@ -60,6 +112,8 @@ module Onsi
60
112
  render_error(response)
61
113
  end
62
114
 
115
+ ##
116
+ # @private
63
117
  def respond_invalid_version_error_400(error)
64
118
  notify_unhandled_exception(error)
65
119
  response = ErrorResponse.new(400)
@@ -71,6 +125,8 @@ module Onsi
71
125
  render_error(response)
72
126
  end
73
127
 
128
+ ##
129
+ # @private
74
130
  def respond_missing_attr_error_400(error)
75
131
  response = ErrorResponse.new(400)
76
132
  response.add(
@@ -83,10 +139,6 @@ module Onsi
83
139
  render_error(response)
84
140
  end
85
141
 
86
- def notify_unhandled_exception(exception)
87
- Rails.logger.error "Unhandled Exception `#{exception.class.name}: #{exception.message}`"
88
- end
89
-
90
142
  private
91
143
 
92
144
  def error_metadata(error)
@@ -103,14 +155,48 @@ module Onsi
103
155
  end
104
156
  end
105
157
 
158
+ ##
159
+ # The error response container.
160
+ #
161
+ # @author Maddie Schipper
162
+ # @since 1.0.0
163
+ #
164
+ # @example
165
+ # def handle_error(error)
166
+ # response = Onsi::ErrorResponse.new(400)
167
+ # response.add(400, 'bad_request', title: 'The payload was invalid')
168
+ # render_error(response)
169
+ # end
106
170
  class ErrorResponse
171
+ ##
172
+ # The HTTP status for the response.
173
+ #
174
+ # @return [Integer]
107
175
  attr_reader :status
108
176
 
177
+ ##
178
+ # Create a new ErrorResponse
179
+ #
180
+ # @param status [Integer] The HTTP status for the response.
109
181
  def initialize(status)
110
182
  @status = status
111
183
  @errors = []
112
184
  end
113
185
 
186
+ ##
187
+ # Add a renderable error to the errors
188
+ #
189
+ # @param status [#to_s, nil] The status of the error. Usually the same as
190
+ # the HTTP status passed to #initialize
191
+ #
192
+ # @param code [String] The error code for the error. e.g. `bad_request`
193
+ #
194
+ # @param title [String, nil] The user displayable title for the error.
195
+ #
196
+ # @param details [String, nil] The user displayable details for the error.
197
+ #
198
+ # @param meta [Hash, nil] Any additional metadata to associate
199
+ # with the error.
114
200
  def add(status, code, title: nil, details: nil, meta: nil)
115
201
  @errors << {}.tap do |err|
116
202
  err[:status] = (status || @status).to_s
@@ -121,10 +207,18 @@ module Onsi
121
207
  end
122
208
  end
123
209
 
210
+ ##
211
+ # Create the error objects.
212
+ #
213
+ # @return [Hash] The JSON-API error hash.
124
214
  def as_json
125
215
  { errors: @errors.as_json }
126
216
  end
127
217
 
218
+ ##
219
+ # Returns a hash that can be passed to #render
220
+ #
221
+ # @private
128
222
  def renderable
129
223
  {
130
224
  json: as_json,
@@ -137,6 +231,17 @@ end
137
231
  module Onsi
138
232
  ##
139
233
  # Handles default errors and builds JSON-API responses.
234
+ #
235
+ # @note Also includes Onsi::ErrorResponderBase but will add a StandardError
236
+ # handler.
237
+ #
238
+ # @example
239
+ # class PeopleController < ApplicationController
240
+ # include Onsi::Controller
241
+ # include Onsi::ErrorResponder
242
+ #
243
+ # # ...
244
+ # end
140
245
  module ErrorResponder
141
246
  extend ActiveSupport::Concern
142
247
 
@@ -145,6 +250,10 @@ module Onsi
145
250
  include(Onsi::ErrorResponderBase)
146
251
  end
147
252
 
253
+ ##
254
+ # Render a 500 error.
255
+ #
256
+ # @private
148
257
  def render_error_500(error)
149
258
  notify_unhandled_exception(error)
150
259
  response = ErrorResponse.new(500)
@@ -1,18 +1,39 @@
1
1
  module Onsi
2
+ ##
3
+ # Container module for custom errors
2
4
  module Errors
5
+ ##
6
+ # Base Error for all Onsi custom errors
7
+ #
8
+ # @author Maddie Schipper
9
+ # @since 1.0.0
3
10
  class BaseError < StandardError; end
4
11
 
12
+ ##
13
+ # An unknown version is requested to be rendered
14
+ #
15
+ # @author Maddie Schipper
16
+ # @since 1.0.0
5
17
  class UnknownVersionError < BaseError
6
- attr_reader :klass, :version
18
+ ##
19
+ # The class that does not support the requested version.
20
+ attr_reader :klass
7
21
 
22
+ ##
23
+ # The version requested that isn't supported
24
+ attr_reader :version
25
+
26
+ ##
27
+ # Create a new UnknownVersionError
28
+ #
29
+ # @param klass (see #klass)
30
+ #
31
+ # @param version (see #version)
8
32
  def initialize(klass, version)
33
+ super("Unsupported version #{version} for #{klass.name}")
9
34
  @klass = klass
10
35
  @version = version
11
36
  end
12
-
13
- def message
14
- "Unsupported version #{version} for #{klass.name}"
15
- end
16
37
  end
17
38
  end
18
39
  end
@@ -1,19 +1,64 @@
1
1
  module Onsi
2
+ ##
3
+ # Used to include other objects in a root Resource objects.
4
+ #
5
+ # @example
6
+ # def index
7
+ # @person = Person.find(params[:person_id])
8
+ # @email = @person.emails.find(params[:id])
9
+ # @includes = Onsi::Includes.new(params[:include])
10
+ # @includes.fetch_person { @person }
11
+ # @includes.fetch_messages { @email.messages }
12
+ # render_resource(Onsi::Resource.new(@email, params[:version].to_sym, includes: @includes))
13
+ # end
2
14
  class Includes
15
+ ##
16
+ # The fetch method matcher regex
17
+ #
18
+ # @private
19
+ FETCH_METHOD_REGEXP = Regexp.new('\Afetch_(?:.*)\z').freeze
20
+
21
+ ##
22
+ # The includes
23
+ #
24
+ # @return [Array<Symbol>]
3
25
  attr_reader :included
4
26
 
27
+ ##
28
+ # Create a new Includes object.
29
+ #
30
+ # @param included [String, Enumerable<String, Symbol>, nil] The keys to be
31
+ # included.
32
+ #
33
+ # @return [Onsi::Includes]
5
34
  def initialize(included)
6
35
  @included = parse_included(included)
7
36
  end
8
37
 
38
+ ##
39
+ # @private
9
40
  def method_missing(name, *args, &block)
10
- if name =~ /\Afetch_(.*)/
41
+ if name =~ FETCH_METHOD_REGEXP
11
42
  add_fetch_method(name.to_s.gsub(/\Afetch_/, ''), *args, &block)
12
43
  else
13
44
  super
14
45
  end
15
46
  end
16
47
 
48
+ ##
49
+ # @private
50
+ def respond_to_missing?(name, include_private = false)
51
+ if name =~ FETCH_METHOD_REGEXP
52
+ true
53
+ else
54
+ super
55
+ end
56
+ end
57
+
58
+ ##
59
+ # Load all included resources.
60
+ #
61
+ # @private
17
62
  def load_included
18
63
  @load_included ||= {}.tap do |root|
19
64
  included.each do |name|
@@ -1,16 +1,68 @@
1
1
  require 'active_support/concern'
2
2
 
3
3
  module Onsi
4
+ ##
5
+ # The Model helper for create a renderable helper.
6
+ #
7
+ # @author Maddie Schipper
8
+ # @since 1.0.0
9
+ #
10
+ # @example
11
+ # class Person < ApplicationRecord
12
+ # include Onsi::Model
13
+ #
14
+ # api_render(:v1) do
15
+ # # Passing the name of the attribute only will call that name as a method on
16
+ # # the instance of the method.
17
+ # attribute(:first_name)
18
+ # attribute(:last_name)
19
+ # # You can give attribute a block and it will be called on the object
20
+ # # instance. This lets you rename or compute attributes
21
+ # attribute(:full_name) { "#{first_name} #{last_name}" }
22
+ #
23
+ # # Relationship requires a minimum of 2 parameters. The first is the name
24
+ # # of the relationship in the rendered JSON. The second is the type.
25
+ # # When fetching the value, Onsi will add `_id` and call that method on the
26
+ # # object instance. e.g. `team_id` in this case.
27
+ # relationship(:team, :team)
28
+ #
29
+ # # Relationships can take a block that will be called on the object instance
30
+ # # and the return value will be used as the ID
31
+ # relationship(:primary_email, :email) { emails.where(primary: true).first.id }
32
+ # end
33
+ # end
4
34
  module Model
5
- DEFAULT_API_VERSION = :v1
6
-
7
35
  extend ActiveSupport::Concern
8
36
 
37
+ ##
38
+ # The current default rendered API version.
39
+ DEFAULT_API_VERSION = :v1
40
+
41
+ ##
42
+ # Defines class methods available on the class.
9
43
  module ClassMethods
44
+ ##
45
+ # Add a version to be rendered.
46
+ #
47
+ # @param version [Symbol] The version that will trigger this render block.
48
+ #
49
+ # @param block [Block] The block. Called on an instance
50
+ # of {Onsi::Model::ModelRenderer}
10
51
  def api_render(version, &block)
11
52
  api_renderer(version).instance_exec(&block)
12
53
  end
13
54
 
55
+ ##
56
+ # Fetch the {Onsi::Model::ModelRenderer} for the version.
57
+ #
58
+ # @param version [Symbol] The version to fetch the renderer for.
59
+ #
60
+ # @param for_render [true, false] Specifies if the version should be
61
+ # required to exist. Should only ever be true when attempting to render
62
+ # the resource.
63
+ #
64
+ # @raise [Onsi::Errors::UnknownVersionError] If the version isn't defined
65
+ # and the for_render param is true.
14
66
  def api_renderer(version, for_render: false)
15
67
  @api_renderer ||= {}
16
68
  if for_render
@@ -20,95 +72,174 @@ module Onsi
20
72
  end
21
73
  @api_renderer[version]
22
74
  end
75
+ end
23
76
 
24
- class ModelRenderer
25
- DATE_FORMAT = '%Y-%m-%d'.freeze
26
- DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%SZ'.freeze
27
-
28
- def initialize
29
- @attributes = {}
30
- @relationships = {}
31
- @metadata = {}
32
- end
77
+ ##
78
+ # The class that holds attributes and relationships for a model's version.
79
+ #
80
+ # @note You shouldn't ever have to directly interact with one of
81
+ # these classes.
82
+ #
83
+ # @author Maddie Schipper
84
+ # @since 1.0.0
85
+ class ModelRenderer
86
+ ##
87
+ # The default date format for a rendered Date. (ISO-8601)
88
+ DATE_FORMAT = '%Y-%m-%d'.freeze
89
+
90
+ ##
91
+ # The default date-time format for a rendered Date and Time. (ISO-8601)
92
+ DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%SZ'.freeze
93
+
94
+ ##
95
+ # Create a new ModelRenderer
96
+ #
97
+ # @private
98
+ def initialize
99
+ @attributes = {}
100
+ @relationships = {}
101
+ @metadata = {}
102
+ end
33
103
 
34
- def type(name = nil)
35
- @type = name if name
36
- @type
37
- end
104
+ ##
105
+ # The type name.
106
+ #
107
+ # @param name [String, nil] The resource object type name.
108
+ #
109
+ # @note Not required. If there is no type, the class name will be used
110
+ # when rendering the object. (Name is underscored)
111
+ def type(name = nil)
112
+ @type = name if name
113
+ @type
114
+ end
38
115
 
39
- def attribute(name, &block)
40
- @attributes[name.to_sym] = block || name
41
- end
116
+ ##
117
+ # Add an attribute to the rendered attributes.
118
+ #
119
+ # @param name [String, Symbol, #to_sym] The name of the attribute.
120
+ # If no block is passed the name will be called on
121
+ # the {Onsi::Resource#object}
122
+ #
123
+ # @param block [Block] The block used to fetch a dynamic attribute.
124
+ # It will be executed in the context of the {Onsi::Resource#object}
125
+ #
126
+ # @example
127
+ # api_render(:v1) do
128
+ # attribute(:first_name)
129
+ # attribute(:last_name)
130
+ # attribute(:full_name) { "#{first_name} #{last_name}" }
131
+ #
132
+ # # ...
133
+ #
134
+ # end
135
+ def attribute(name, &block)
136
+ @attributes[name.to_sym] = block || name
137
+ end
42
138
 
43
- def relationship(name, type, &block)
44
- @relationships[name.to_sym] = { type: type, attr: block || name }
45
- end
139
+ ##
140
+ # Add a relationship to the rendered relationships.
141
+ #
142
+ # @param name [Symbol, #to_sym] The relationship name.
143
+ #
144
+ # @param type [String, #to_s] The relationship type.
145
+ #
146
+ # @param block [Block] The block used to fetch a dynamic attribute.
147
+ # It will be executed in the context of the {Onsi::Resource#object}
148
+ #
149
+ # @example
150
+ # api_render(:v1) do
151
+ # relationship(:team, :team)
152
+ #
153
+ # # ...
154
+ #
155
+ # end
156
+ def relationship(name, type, &block)
157
+ @relationships[name.to_sym] = { type: type, attr: block || name }
158
+ end
46
159
 
47
- def meta(name, &block)
48
- @metadata[name.to_sym] = block
49
- end
160
+ ##
161
+ # Add a metadata value to the rendered object's meta.
162
+ #
163
+ # @param name [#to_sym] The name for the meta value.
164
+ #
165
+ # @param block [Block] The block used to fetch the meta value.
166
+ # It will be executed in the context of the {Onsi::Resource#object}
167
+ def meta(name, &block)
168
+ @metadata[name.to_sym] = block
169
+ end
50
170
 
51
- def render_attributes(object)
52
- @attributes.each_with_object({}) do |(key, value), attrs|
53
- val = value.respond_to?(:call) ? object.instance_exec(&value) : object.send(value)
54
- attrs[key.to_s] = format_attribute(val)
55
- end
171
+ ##
172
+ # Render all attributes
173
+ #
174
+ # @private
175
+ def render_attributes(object)
176
+ @attributes.each_with_object({}) do |(key, value), attrs|
177
+ val = value.respond_to?(:call) ? object.instance_exec(&value) : object.send(value)
178
+ attrs[key.to_s] = format_attribute(val)
56
179
  end
180
+ end
57
181
 
58
- def render_relationships(object)
59
- @relationships.each_with_object({}) do |(key, value), rels|
60
- render_relationship_entry(object, key, value, rels)
61
- end
182
+ ##
183
+ # Render all relationships
184
+ #
185
+ # @private
186
+ def render_relationships(object)
187
+ @relationships.each_with_object({}) do |(key, value), rels|
188
+ render_relationship_entry(object, key, value, rels)
62
189
  end
190
+ end
63
191
 
64
- def render_metadata(object)
65
- @metadata.each_with_object({}) do |(key, block), meta|
66
- meta[key.to_s] = object.instance_exec(&block)
67
- end
192
+ ##
193
+ # Render all metadata
194
+ #
195
+ # @private
196
+ def render_metadata(object)
197
+ @metadata.each_with_object({}) do |(key, block), meta|
198
+ meta[key.to_s] = object.instance_exec(&block)
68
199
  end
200
+ end
69
201
 
70
- private
202
+ private
71
203
 
72
- def render_relationship_entry(object, key, value, rels)
73
- attr = value[:attr]
74
- relationship = get_relationship_value(attr, object)
75
- data = format_relationship(relationship, value)
76
- rels[key.to_s] = {
77
- 'data' => data
78
- }
79
- end
204
+ def render_relationship_entry(object, key, value, rels)
205
+ attr = value[:attr]
206
+ relationship = get_relationship_value(attr, object)
207
+ data = format_relationship(relationship, value)
208
+ rels[key.to_s] = {
209
+ 'data' => data
210
+ }
211
+ end
80
212
 
81
- def get_relationship_value(attr, object)
82
- if attr.respond_to?(:call)
83
- object.instance_exec(&attr)
84
- else
85
- object.send("#{attr}_id")
86
- end
213
+ def get_relationship_value(attr, object)
214
+ if attr.respond_to?(:call)
215
+ object.instance_exec(&attr)
216
+ else
217
+ object.send("#{attr}_id")
87
218
  end
219
+ end
88
220
 
89
- def format_relationship(relationship, value)
90
- case relationship
91
- when Array
92
- relationship.map { |v| { 'type' => value[:type].to_s, 'id' => v.to_s } }
93
- else
94
- {
95
- 'type' => value[:type].to_s,
96
- 'id' => relationship.to_s
97
- }
98
- end
221
+ def format_relationship(relationship, value)
222
+ case relationship
223
+ when Array
224
+ relationship.map { |v| { 'type' => value[:type].to_s, 'id' => v.to_s } }
225
+ else
226
+ {
227
+ 'type' => value[:type].to_s,
228
+ 'id' => relationship.to_s
229
+ }
99
230
  end
231
+ end
100
232
 
101
- def format_attribute(value)
102
- case value
103
- when Date
104
- value.strftime(DATE_FORMAT)
105
- when DateTime, Time
106
- value.utc.strftime(DATETIME_FORMAT)
107
- when String
108
- value.presence
109
- else
110
- value
111
- end
233
+ def format_attribute(value)
234
+ case value
235
+ when Date
236
+ value.strftime(DATE_FORMAT)
237
+ when DateTime, Time
238
+ value.utc.strftime(DATETIME_FORMAT)
239
+ when String
240
+ value.presence
241
+ else
242
+ value
112
243
  end
113
244
  end
114
245
  end
@@ -1,36 +1,75 @@
1
+ require_relative 'errors'
2
+
1
3
  module Onsi
2
4
  ##
3
5
  # Used to handle parsing JSON-API formated params
6
+ #
7
+ # @example
8
+ # class PeopleController < ApplicationController
9
+ # include Onsi::Controller
10
+ #
11
+ # def create
12
+ # attributes = Onsi::Param.parse(
13
+ # params,
14
+ # [:first_name, :last_name],
15
+ # [:team]
16
+ # )
17
+ # render_resource Person.create!(attributes.flatten)
18
+ # end
19
+ # end
4
20
  class Params
5
21
  ##
6
- # Raised when using `Params#safe_fetch`
22
+ # Raised when a safe_fetch fails.
7
23
  #
8
- # The ErrorResponder will rescue from this and return an appropriate
9
- # error to the user
10
- class RelationshipNotFound < StandardError
24
+ # @note The ErrorResponder will rescue from this and return an appropriate
25
+ # error to the user
26
+ class RelationshipNotFound < Onsi::Errors::BaseError
27
+ ##
28
+ # The key that the relationship wasn't found for
29
+ #
30
+ # @return [String]
11
31
  attr_reader :key
12
32
 
33
+ ##
34
+ # @private
13
35
  def initialize(message, key)
14
36
  super(message)
15
- @key = key
37
+ @key = key.to_s
16
38
  end
17
39
  end
18
40
 
19
41
  ##
20
42
  # Raised when a required attribute has a nil value. `Params#require`
21
43
  #
22
- # The ErrorResponder will rescue from this and return an appropriate
23
- # error to the user
24
- class MissingReqiredAttribute < StandardError
44
+ # @note The ErrorResponder will rescue from this and return an appropriate
45
+ # error to the user
46
+ class MissingReqiredAttribute < Onsi::Errors::BaseError
47
+ ##
48
+ # The attribute that was missing when required.
49
+ #
50
+ # @return [String]
25
51
  attr_reader :attribute
26
52
 
53
+ ##
54
+ # @private
27
55
  def initialize(message, attr)
28
56
  super(message)
29
- @attribute = attr
57
+ @attribute = attr.to_s
30
58
  end
31
59
  end
32
60
 
33
61
  class << self
62
+ ##
63
+ # Parse a JSON-API formatted params object.
64
+ #
65
+ # @param params [ActionController::Parameters] The parameters to parse.
66
+ #
67
+ # @param attributes [Array<String, Symbol>] The whitelisted attributes.
68
+ #
69
+ # @param relationships [Array<String, Symbol>] The whitelisted relationships.
70
+ # Should be the key for the relationships name.
71
+ #
72
+ # @return [Params] The new params object.
34
73
  def parse(params, attributes = [], relationships = [])
35
74
  data = params.require(:data)
36
75
  data.require(:type)
@@ -39,6 +78,17 @@ module Onsi
39
78
  new(attrs, relas)
40
79
  end
41
80
 
81
+ ##
82
+ # Parse a JSON-API formatted JSON object.
83
+ #
84
+ # @param body [String, #read] The parameters to parse.
85
+ #
86
+ # @param attributes [Array<String, Symbol>] The whitelisted attributes.
87
+ #
88
+ # @param relationships [Array<String, Symbol>] The whitelisted relationships.
89
+ # Should be the key for the relationships name.
90
+ #
91
+ # @return [Onsi::Params] The new params object.
42
92
  def parse_json(body, attributes = [], relationships = [])
43
93
  content = body.respond_to?(:read) ? body.read : body
44
94
  json = JSON.parse(content)
@@ -104,8 +154,28 @@ module Onsi
104
154
  end
105
155
  end
106
156
 
107
- attr_reader :attributes, :relationships
157
+ ##
158
+ # The attributes for the params.
159
+ #
160
+ # @return [ActionController::Parameters]
161
+ attr_reader :attributes
162
+
163
+ ##
164
+ # The relationships for the params.
165
+ #
166
+ # @return [Hash]
167
+ attr_reader :relationships
108
168
 
169
+ ##
170
+ # Create a new Params instance.
171
+ #
172
+ # @param attributes [ActionController::Parameters] The attributes
173
+ #
174
+ # @param relationships [Hash] Flattened relationships hash
175
+ #
176
+ # @note Should not be created directly. Use .parse or .parse_json
177
+ #
178
+ # @private
109
179
  def initialize(attributes, relationships)
110
180
  @attributes = attributes
111
181
  @relationships = relationships
@@ -113,12 +183,20 @@ module Onsi
113
183
 
114
184
  ##
115
185
  # Flatten an merge the attributes & relationships into one hash.
186
+ #
187
+ # @return [Hash] The flattened attributes and relationships
116
188
  def flatten
117
189
  attrs_hash.to_h.merge(relationships.to_h).with_indifferent_access
118
190
  end
119
191
 
120
192
  ##
121
193
  # Fetch a value from the attributes or return the passed default value
194
+ #
195
+ # @param key [String, Symbol] The key to fetch.
196
+ #
197
+ # @param default [Any] The default value if the attribute doesn't exist.
198
+ #
199
+ # @return [Any]
122
200
  def fetch(key, default = nil)
123
201
  attrs_hash[key] || default
124
202
  end
@@ -126,7 +204,11 @@ module Onsi
126
204
  ##
127
205
  # Make an attributes key required.
128
206
  #
129
- # Throws MissingReqiredAttribute if the value is nil
207
+ # @param key [String, Symbol] The key of the attribute to require.
208
+ #
209
+ # @raise [MissingReqiredAttribute] The value you have required isn't present
210
+ #
211
+ # @return [Any] The value for the attribute
130
212
  def require(key)
131
213
  value = attrs_hash[key]
132
214
  if value.nil?
@@ -137,14 +219,19 @@ module Onsi
137
219
  end
138
220
 
139
221
  ##
140
- # Handle finding a relationship's object
222
+ # Handle finding a relationship's object.
223
+ #
224
+ # @param key [String, Symbol] The key for the relationship
141
225
  #
142
- # If an ActiveRecord::RecordNotFound is raised, a RelationshipNotFound error will
143
- # be raised so the ErrorResponder can build an appropriate error message
226
+ # @raise [RelationshipNotFound] Thrown instead of an `ActiveRecord::RecordNotFound`
227
+ # This allows the `Onsi::ErrorResponder` to build an appropriate response.
144
228
  #
145
- # params.safe_fetch(:person) do |id|
146
- # Person.find(id)
147
- # end
229
+ # @example
230
+ # params.safe_fetch(:person) do |id|
231
+ # Person.find(id)
232
+ # end
233
+ #
234
+ # @return [Any]
148
235
  def safe_fetch(key)
149
236
  yield(@relationships[key])
150
237
  rescue ActiveRecord::RecordNotFound
@@ -156,21 +243,36 @@ module Onsi
156
243
  #
157
244
  # Any getter will run the value through the transform block.
158
245
  #
159
- # (The values are memoized)
246
+ # @param key [String, Symbol] The key to transform.
247
+ #
248
+ # @param block [Block] The block to perform the transform.
249
+ #
250
+ # @note The values are memoized
160
251
  #
161
- # `params.transform(:date) { |date| Time.parse(date) }`
252
+ # @example
253
+ # params.transform(:date) { |date| Time.parse(date) }
254
+ #
255
+ # @return [Any]
162
256
  def transform(key, &block)
163
257
  @attrs_hash = nil
164
258
  transforms[key.to_sym] = block
165
259
  end
166
260
 
167
261
  ##
168
- # Set a default value.
262
+ # Set a default value for attributes.
169
263
  #
170
264
  # This value will only be used if the key is missing from the passed attributes
171
265
  #
172
- # Can take any object. If the object responds to call (Lambda) it will be called when
173
- # parsing attributes
266
+ # @param key [String, Symbol] The key to set a default on.
267
+ #
268
+ # @param value [Any, #call] The default value.
269
+ # If the object responds to call (Lambda) it will be called when
270
+ # parsing attributes
271
+ #
272
+ # @example
273
+ # params.default(:missing, -> { :foo })
274
+ # subject.flatten[:missing]
275
+ # # => :foo
174
276
  def default(key, value)
175
277
  @attrs_hash = nil
176
278
  defaults[key.to_sym] = value
@@ -1,18 +1,72 @@
1
+ require_relative 'errors'
2
+
1
3
  module Onsi
4
+ ##
5
+ # The wrapper for generating a object
6
+ #
7
+ # @author Maddie Schipper
8
+ # @since 1.0.0
2
9
  class Resource
3
- attr_reader :object, :version, :includes
10
+ ##
11
+ # Root object type key
12
+ #
13
+ # @private
14
+ TYPE_KEY = 'type'.freeze
15
+
16
+ ##
17
+ # Root object id key
18
+ #
19
+ # @private
20
+ ID_KEY = 'id'.freeze
21
+
22
+ ##
23
+ # Root object attributes key
24
+ #
25
+ # @private
26
+ ATTRIBUTES_KEY = 'attributes'.freeze
4
27
 
5
- TYPE_KEY = 'type'.freeze
6
- ID_KEY = 'id'.freeze
7
- ATTRIBUTES_KEY = 'attributes'.freeze
28
+ ##
29
+ # Root object relationships key
30
+ #
31
+ # @private
8
32
  RELATIONSHIPS_KEY = 'relationships'.freeze
9
- META_KEY = 'meta'.freeze
10
- DATA_KEY = 'data'.freeze
11
- INCLUDED_KEY = 'included'.freeze
12
33
 
13
- class InvalidResourceError < StandardError; end
34
+ ##
35
+ # Root object meta key
36
+ #
37
+ # @private
38
+ META_KEY = 'meta'.freeze
39
+
40
+ ##
41
+ # Root object data key
42
+ #
43
+ # @private
44
+ DATA_KEY = 'data'.freeze
45
+
46
+ ##
47
+ # Root object included key
48
+ #
49
+ # @private
50
+ INCLUDED_KEY = 'included'.freeze
51
+
52
+ ##
53
+ # Raised if the resource or includes are invalid.
54
+ class InvalidResourceError < Onsi::Errors::BaseError; end
14
55
 
15
56
  class << self
57
+ ##
58
+ # Convert an object into a Onsi::Resource
59
+ #
60
+ # @param resource [Onsi::Resource, Enumerable, ActiveRecord::Base] The
61
+ # object to be converted.
62
+ # - If a Onsi::Resource is passed it will be directly returned.
63
+ # - If an Enumerable is passed #map will be called and .as_resource will
64
+ # be recursivly called for each object.
65
+ # - If any other object is passed it will be wrapped in a Onsi::Resource
66
+ #
67
+ # @param version [Symbol] The version of the resource. `:v1`
68
+ #
69
+ # @return [Onsi::Resource, Array<Onsi::Resource>]
16
70
  def as_resource(resource, version)
17
71
  case resource
18
72
  when Onsi::Resource
@@ -24,6 +78,15 @@ module Onsi
24
78
  end
25
79
  end
26
80
 
81
+ ##
82
+ # Render a resource to JSON
83
+ #
84
+ # @param resource (see .as_resource)
85
+ #
86
+ # @param version [Symbol] The version to render as. `:v1`
87
+ #
88
+ # @return [Hash] The rendered resource as a hash ready to be converted
89
+ # to JSON.
27
90
  def render(resource, version)
28
91
  resources = as_resource(resource, version)
29
92
  {}.tap do |root|
@@ -35,6 +98,8 @@ module Onsi
35
98
  end
36
99
  end
37
100
 
101
+ private
102
+
38
103
  def all_included(resources)
39
104
  Array(resources).map(&:flat_includes).flatten.uniq do |res|
40
105
  "#{res[TYPE_KEY]}-#{res[ID_KEY]}"
@@ -42,6 +107,39 @@ module Onsi
42
107
  end
43
108
  end
44
109
 
110
+ ##
111
+ # The backing object.
112
+ #
113
+ # @note MUST include Onsi::Model
114
+ #
115
+ # @return [Any] The object to be rendered by the resource.
116
+ attr_reader :object
117
+
118
+ ##
119
+ # The version to render.
120
+ #
121
+ # @return [Symbol]
122
+ attr_reader :version
123
+
124
+ ##
125
+ # The includes for the object.
126
+ #
127
+ # @return [Array<Onsi::Includes>]
128
+ attr_reader :includes
129
+
130
+ ##
131
+ # Create a new resouce.
132
+ #
133
+ # @param object [Any] The resource backing object.
134
+ #
135
+ # @param version [Symbol] The version to render. Can be nil. If nil is
136
+ # passed the DEFAULT_API_VERSION will be used.
137
+ #
138
+ # @note The object MUST be a single object that includes Onsi::Model
139
+ #
140
+ # @note The includes MUST be an array of Onsi::Include objects.
141
+ #
142
+ # @return [Onsi::Resource] The new resource
45
143
  def initialize(object, version = nil, includes: nil)
46
144
  @object = object
47
145
  @version = version || Model::DEFAULT_API_VERSION
@@ -49,6 +147,10 @@ module Onsi
49
147
  validate!
50
148
  end
51
149
 
150
+ ##
151
+ # Creates a raw JSON object.
152
+ #
153
+ # @return [Hash]
52
154
  def as_json(_opts = {})
53
155
  {}.tap do |root|
54
156
  root[TYPE_KEY] = type
@@ -60,10 +162,18 @@ module Onsi
60
162
  end
61
163
  end
62
164
 
165
+ ##
166
+ # All rendered includes
167
+ #
168
+ # @private
63
169
  def rendered_includes
64
170
  @rendered_includes ||= perform_render_includes
65
171
  end
66
172
 
173
+ ##
174
+ # Flat includes
175
+ #
176
+ # @private
67
177
  def flat_includes
68
178
  rendered_includes.values.map { |root| root[DATA_KEY] }.flatten
69
179
  end
@@ -1,3 +1,5 @@
1
1
  module Onsi
2
- VERSION = '0.8.0'.freeze
2
+ ##
3
+ # The current version of Onsi
4
+ VERSION = '1.0.0'.freeze
3
5
  end
@@ -32,4 +32,5 @@ Gem::Specification.new do |spec|
32
32
  spec.add_development_dependency 'rspec-rails', '~> 3.7.2'
33
33
  spec.add_development_dependency 'simplecov', '~> 0.15'
34
34
  spec.add_development_dependency 'sqlite3', '~> 1.3.10'
35
+ spec.add_development_dependency 'yard', '~> 0.9.16'
35
36
  end
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.8.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Maddie Schipper
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-11-13 00:00:00.000000000 Z
11
+ date: 2018-11-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -142,6 +142,20 @@ dependencies:
142
142
  - - "~>"
143
143
  - !ruby/object:Gem::Version
144
144
  version: 1.3.10
145
+ - !ruby/object:Gem::Dependency
146
+ name: yard
147
+ requirement: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - "~>"
150
+ - !ruby/object:Gem::Version
151
+ version: 0.9.16
152
+ type: :development
153
+ prerelease: false
154
+ version_requirements: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - "~>"
157
+ - !ruby/object:Gem::Version
158
+ version: 0.9.16
145
159
  description: Format JSON API responses and parse inbound requests.
146
160
  email:
147
161
  - me@maddiesch.com
@@ -154,6 +168,8 @@ files:
154
168
  - ".gitignore"
155
169
  - ".rspec"
156
170
  - ".rubocop.yml"
171
+ - ".yardopts"
172
+ - CHANGELOG.md
157
173
  - Gemfile
158
174
  - LICENSE.txt
159
175
  - README.md