onsi 0.8.0 → 1.0.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
  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