pragma-decorator 1.3.0 → 2.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: 1d247ca8e236d5a993b70a518c5f64fc82e9ed80
4
- data.tar.gz: 145c3dbf948aa6bbc8e2d63b2e49126778d417b7
3
+ metadata.gz: 4cc969bd13e27d434cc079786db43b29ae6b6154
4
+ data.tar.gz: cfefba30c1aab7b6d53db48031df9e991159537e
5
5
  SHA512:
6
- metadata.gz: 55cb0eacc764ba44011b43c0bb9e1203202dc43d7b395379bf016295ba0a268e0c1dcacfe268dd8238d452cd6dbc5d210bec792fb7a1d6dad3d89c9a7ac32ab7
7
- data.tar.gz: 105d6b13dbcfa5e31943739840e73c39a0aca84da28a4d248cc97d760f160458fc313d354e7d236b98c44a59017bd3af2428a0868e164d90de1f7f703dc31199
6
+ metadata.gz: 26574df0b3a1807c18fe1c156feadf6ea37947fcb044f1587d4de0319782af2164cb5a7c591a99bb954d187b0ec6c6e5c499150eba89be2ab04c8b36c00a6763
7
+ data.tar.gz: d502113406896fd913100c0ededa0242ead3fc8fa1d1cdd7545d2bff0ed15fe9d8a227beac6f3177f22189354223c9bada76a62f02f188b4119b3549bab0c51d
data/.rubocop.yml CHANGED
@@ -15,6 +15,7 @@ AllCops:
15
15
  - 'config/**/*'
16
16
  - '**/Rakefile'
17
17
  - '**/Gemfile'
18
+ - 'pragma-decorator.gemspec'
18
19
 
19
20
  RSpec/DescribeClass:
20
21
  Exclude:
@@ -24,26 +25,26 @@ Style/BlockDelimiters:
24
25
  Exclude:
25
26
  - 'spec/**/*'
26
27
 
27
- Style/AlignParameters:
28
+ Layout/AlignParameters:
28
29
  EnforcedStyle: with_fixed_indentation
29
30
 
30
- Style/ClosingParenthesisIndentation:
31
+ Layout/ClosingParenthesisIndentation:
31
32
  Enabled: false
32
33
 
33
34
  Metrics/LineLength:
34
35
  Max: 100
35
36
  AllowURI: true
36
37
 
37
- Style/FirstParameterIndentation:
38
+ Layout/FirstParameterIndentation:
38
39
  Enabled: false
39
40
 
40
- Style/MultilineMethodCallIndentation:
41
+ Layout/MultilineMethodCallIndentation:
41
42
  EnforcedStyle: indented
42
43
 
43
- Style/IndentArray:
44
+ Layout/IndentArray:
44
45
  EnforcedStyle: consistent
45
46
 
46
- Style/IndentHash:
47
+ Layout/IndentHash:
47
48
  EnforcedStyle: consistent
48
49
 
49
50
  Style/SignalException:
@@ -68,7 +69,7 @@ RSpec/NamedSubject:
68
69
  RSpec/ExampleLength:
69
70
  Enabled: false
70
71
 
71
- Style/MultilineMethodCallBraceLayout:
72
+ Layout/MultilineMethodCallBraceLayout:
72
73
  Enabled: false
73
74
 
74
75
  Metrics/MethodLength:
@@ -86,12 +87,8 @@ Metrics/CyclomaticComplexity:
86
87
  Metrics/BlockLength:
87
88
  Enabled: false
88
89
 
89
- Metrics/ClassLength:
90
- Enabled: false
91
-
92
90
  Style/GuardClause:
93
91
  Enabled: false
94
92
 
95
- Naming/FileName:
96
- Exclude:
97
- - 'pragma-decorator.gemspec'
93
+ Capybara/FeatureMethods:
94
+ Enabled: false
data/Gemfile CHANGED
@@ -2,3 +2,5 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in pragma-decorator.gemspec
4
4
  gemspec
5
+
6
+ gem 'pry'
data/README.md CHANGED
@@ -7,8 +7,8 @@
7
7
 
8
8
  Decorators are a way to easily convert your API resources to JSON with minimum hassle.
9
9
 
10
- They are built on top of [ROAR](https://github.com/apotonick/roar) but provide some useful helpers
11
- for rendering collections, including pagination metadata and expanding associations.
10
+ They are built on top of [ROAR](https://github.com/apotonick/roar). We provide some useful helpers
11
+ for rendering collections, expanding associations and much more.
12
12
 
13
13
  ## Installation
14
14
 
@@ -32,7 +32,286 @@ $ gem install pragma-decorator
32
32
 
33
33
  ## Usage
34
34
 
35
- All documentation is in the [doc](https://github.com/pragmarb/pragma-decorator/tree/master/doc) folder.
35
+ Creating a decorator is as simple as inheriting from `Pragma::Decorator::Base`:
36
+
37
+ ```ruby
38
+ module API
39
+ module V1
40
+ module User
41
+ module Decorator
42
+ class Instance < Pragma::Decorator::Base
43
+ property :id
44
+ property :email
45
+ property :full_name
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ ```
52
+
53
+ Just instantiate the decorator by passing it an object to decorate, then call `#to_hash` or
54
+ `#to_json`:
55
+
56
+ ```ruby
57
+ decorator = API::V1::User::Decorator::Instance.new(user)
58
+ decorator.to_json
59
+ ```
60
+
61
+ This will produce the following JSON:
62
+
63
+ ```json
64
+ {
65
+ "id": 1,
66
+ "email": "jdoe@example.com",
67
+ "full_name": "John Doe"
68
+ }
69
+ ```
70
+
71
+ Since Pragma::Decorator is built on top of [ROAR](https://github.com/apotonick/roar) (which, in
72
+ turn, is built on top of [Representable](https://github.com/apotonick/representable)), you should
73
+ consult their documentation for the basic usage of decorators; the rest of this section only covers
74
+ the features provided specifically by Pragma::Decorator.
75
+
76
+ ### Object Types
77
+
78
+ It is recommended that decorators expose the type of the decorated object. You can achieve this
79
+ with the `Type` mixin:
80
+
81
+ ```ruby
82
+ module API
83
+ module V1
84
+ module User
85
+ module Decorator
86
+ class Instance < Pragma::Decorator::Base
87
+ feature Pragma::Decorator::Type
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ ```
94
+
95
+ This would result in the following representation:
96
+
97
+ ```json
98
+ {
99
+ "type": "user",
100
+ "...": "...""
101
+ }
102
+ ```
103
+
104
+ You can also set a custom type name (just make sure to use it consistently!):
105
+
106
+ ```ruby
107
+ module API
108
+ module V1
109
+ module User
110
+ module Decorator
111
+ class Instance < Pragma::Decorator::Base
112
+ def type
113
+ :custom_type
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
120
+ ```
121
+
122
+ Note: `array` is already overridden with the more language-agnostic `list`.
123
+
124
+ ### Timestamps
125
+
126
+ [UNIX time](https://en.wikipedia.org/wiki/Unix_time) is your safest bet when rendering/parsing
127
+ timestamps in your API, as it doesn't require a timezone indicator (the timezone is always UTC).
128
+
129
+ You can use the `Timestamp` mixin for converting `Time` instances to UNIX times:
130
+
131
+ ```ruby
132
+ module API
133
+ module V1
134
+ module User
135
+ module Decorator
136
+ class Instance < Pragma::Decorator::Base
137
+ feature Pragma::Decorator::Timestamp
138
+
139
+ timestamp :created_at
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
145
+ ```
146
+
147
+ This will render a user like this:
148
+
149
+ ```json
150
+ {
151
+ "type": "user",
152
+ "created_at": 1480287994
153
+ }
154
+ ```
155
+
156
+ The `#timestamp` method supports all the options supported by `#property` (except for `:as`).
157
+
158
+ ### Associations
159
+
160
+ `Pragma::Decorator::Association` allows you to define associations in your decorator (currently,
161
+ only `belongs_to`/`has_one` associations are supported):
162
+
163
+ ```ruby
164
+ module API
165
+ module V1
166
+ module Invoice
167
+ module Decorator
168
+ class Instance < Pragma::Decorator::Base
169
+ feature Pragma::Decorator::Association
170
+
171
+ belongs_to :customer, decorator: API::V1::Customer::Decorator
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
177
+ ```
178
+
179
+ Rendering an invoice will now create the following representation:
180
+
181
+ ```json
182
+ {
183
+ "customer": 19
184
+ }
185
+ ```
186
+
187
+ You can pass `expand[]=customer` as a request parameter and have the `customer` property expanded
188
+ into a full object!
189
+
190
+ ```json
191
+ {
192
+ "customer": {
193
+ "id": 19,
194
+ "...": "..."
195
+ }
196
+ }
197
+ ```
198
+
199
+ This also works for nested associations. For instance, if the customer decorator had a `company`
200
+ association, you could pass `expand[]=customer&expand[]=customer.company` to get the company
201
+ expanded too.
202
+
203
+ Note that you will have to pass the associations to expand as a user option when rendering:
204
+
205
+ ```ruby
206
+ decorator = API::V1::Invoice::Decorator::Instance.new(invoice)
207
+ decorator.to_json(user_options: {
208
+ expand: ['customer', 'customer.company', 'customer.company.contact']
209
+ })
210
+ ```
211
+
212
+ Needless to say, this is done automatically for you when you use all components together through
213
+ the [pragma](https://github.com/pragmarb/pragma) gem! :)
214
+
215
+ ### Collection
216
+
217
+ `Pragma::Decorator::Collection` wraps collections in a `data` property so that you can include
218
+ metadata about them:
219
+
220
+ ```ruby
221
+ module API
222
+ module V1
223
+ module Invoice
224
+ module Decorator
225
+ class Collection < Pragma::Decorator::Base
226
+ feature Pragma::Decorator::Collection
227
+ decorate_with Instance # specify the instance decorator
228
+
229
+ property :total_cents, exec_context: :decorator
230
+
231
+ def total_cents
232
+ represented.sum(:total_cents)
233
+ end
234
+ end
235
+ end
236
+ end
237
+ end
238
+ end
239
+ ```
240
+
241
+ You can now do this:
242
+
243
+ ```ruby
244
+ API::V1::Invoice::Decorator::Collection.new(Invoice.all).to_json
245
+ ```
246
+
247
+ Which will produce the following JSON:
248
+
249
+ ```json
250
+ {
251
+ "data": [{
252
+ "id": 1,
253
+ "total_cents": 1500,
254
+ }, {
255
+ "id": 2,
256
+ "total_cents": 3000,
257
+ }],
258
+ "total_cents": 4500
259
+ }
260
+ ```
261
+
262
+ This is very useful, for instance, when you have a paginated collection, but want to include data
263
+ about the entire collection (not just the current page) in the response.
264
+
265
+ ### Pagination
266
+
267
+ Speaking of pagination, you can use `Pragma::Decorator::Pagination` in combination with
268
+ `Collection` to include pagination data in your response:
269
+
270
+ ```ruby
271
+ module API
272
+ module V1
273
+ module Invoice
274
+ module Decorator
275
+ class Collection < Pragma::Decorator::Base
276
+ feature Pragma::Decorator::Collection
277
+ feature Pragma::Decorator::Pagination
278
+
279
+ decorate_with Instance
280
+ end
281
+ end
282
+ end
283
+ end
284
+ end
285
+ ```
286
+
287
+ Now, you can run this code:
288
+
289
+ ```ruby
290
+ API::V1::Invoice::Decorator::Collection.new(Invoice.all).to_json
291
+ ```
292
+
293
+ Which will produce the following JSON:
294
+
295
+ ```json
296
+ {
297
+ "data": [{
298
+ "id": 1,
299
+ "...": "...",
300
+ }, {
301
+ "id": 2,
302
+ "...": "...",
303
+ }],
304
+ "total_entries": 2,
305
+ "per_page": 30,
306
+ "total_pages": 1,
307
+ "previous_page": null,
308
+ "current_page": 1,
309
+ "next_page": null
310
+ }
311
+ ```
312
+
313
+ It works with both [will_paginate](https://github.com/mislav/will_paginate) and
314
+ [Kaminari](https://github.com/kaminari/kaminari)!
36
315
 
37
316
  ## Contributing
38
317
 
@@ -7,10 +7,11 @@ require 'pragma/decorator/base'
7
7
  require 'pragma/decorator/association'
8
8
  require 'pragma/decorator/association/reflection'
9
9
  require 'pragma/decorator/association/binding'
10
- require 'pragma/decorator/association/unexpandable_error'
11
- require 'pragma/decorator/association/inconsistent_type_error'
10
+ require 'pragma/decorator/association/errors'
12
11
  require 'pragma/decorator/timestamp'
13
12
  require 'pragma/decorator/type'
13
+ require 'pragma/decorator/collection'
14
+ require 'pragma/decorator/pagination'
14
15
 
15
16
  module Pragma
16
17
  # Represent your API resources in JSON with minimum hassle.
@@ -2,88 +2,84 @@
2
2
 
3
3
  module Pragma
4
4
  module Decorator
5
- # Adds association expansion to decorators.
6
- #
7
- # @author Alessandro Desantis
8
5
  module Association
9
6
  def self.included(klass)
10
7
  klass.extend ClassMethods
8
+ klass.include InstanceMethods
9
+ end
11
10
 
12
- klass.class_eval do
13
- @associations = {}
14
-
15
- def self.associations
16
- @associations
17
- end
11
+ module ClassMethods # rubocop:disable Style/Documentation
12
+ def associations
13
+ @associations ||= {}
18
14
  end
19
- end
20
15
 
21
- # Inizializes the decorator and bindings for all the associations.
22
- #
23
- # @see Association::Binding
24
- def initialize(*)
25
- super
16
+ def belongs_to(property_name, options = {})
17
+ define_association :belongs_to, property_name, options
18
+ end
26
19
 
27
- @association_bindings = {}
28
- self.class.associations.each_pair do |property, reflection|
29
- @association_bindings[property] = Binding.new(reflection: reflection, decorator: self)
20
+ def has_one(property_name, options = {}) # rubocop:disable Naming/PredicateName
21
+ define_association :has_one, property_name, options
30
22
  end
31
- end
32
23
 
33
- module ClassMethods # rubocop:disable Style/Documentation
34
- # Defines a +belongs_to+ association.
35
- #
36
- # See {Association::Reflection#initialize} for the list of available options.
37
- #
38
- # @param property [Symbol] the property containing the associated object
39
- # @param options [Hash] the options of the association
40
- def belongs_to(property, options = {})
41
- define_association :belongs_to, property, options
24
+ private
25
+
26
+ def define_association(type, property_name, options = {})
27
+ create_association_definition(type, property_name, options)
28
+ create_association_property(type, property_name, options)
42
29
  end
43
30
 
44
- # Defines a +has_one+ association.
45
- #
46
- # See {Association::Reflection#initialize} for the list of available options.
47
- #
48
- # @param property [Symbol] the property containing the associated object
49
- # @param options [Hash] the options of the association
50
- def has_one(property, options = {}) # rubocop:disable Style/PredicateName
51
- define_association :has_one, property, options
31
+ def create_association_definition(type, property_name, options)
32
+ associations[property_name.to_sym] = Reflection.new(type, property_name, options)
52
33
  end
53
34
 
54
- private
35
+ def create_association_property(_type, property_name, options)
36
+ property_options = options.dup.tap { |po| po.delete(:decorator) }.merge(
37
+ exec_context: :decorator,
38
+ as: property_name,
39
+ getter: (lambda do |decorator:, user_options:, **_args|
40
+ Binding.new(
41
+ reflection: decorator.class.associations[property_name],
42
+ decorator: decorator
43
+ ).render(user_options[:expand])
44
+ end)
45
+ )
55
46
 
56
- def define_association(type, property, options = {})
57
- create_association_definition(type, property, options)
58
- create_association_getter(property)
59
- create_association_property(property)
47
+ property("_#{property_name}_association", property_options)
60
48
  end
49
+ end
61
50
 
62
- def create_association_definition(type, property, options)
63
- @associations[property.to_sym] = Reflection.new(type, property, options)
51
+ module InstanceMethods
52
+ def validate_expansion(expand)
53
+ check_parent_associations_are_expanded(expand)
54
+ check_expanded_associations_exist(expand)
64
55
  end
65
56
 
66
- def create_association_getter(property)
67
- code = <<~RUBY
68
- def _#{property}_association
69
- @association_bindings[:#{property}].render(user_options[:expand])
70
- end
71
- RUBY
57
+ private
58
+
59
+ def check_parent_associations_are_expanded(expand)
60
+ expand = normalize_expand(expand)
61
+
62
+ expand.each do |property|
63
+ next unless property.include?('.')
72
64
 
73
- class_eval code, __FILE__, __LINE__
65
+ parent_path = property.split('.')[0..-2].join('.')
66
+ next if expand.include?(parent_path)
67
+
68
+ fail Association::UnexpandedAssociationParent.new(property, parent_path)
69
+ end
74
70
  end
75
71
 
76
- def create_association_property(property_name)
77
- options = {
78
- exec_context: :decorator,
79
- as: property_name
80
- }.tap do |opts|
81
- if @associations[property_name].options.key?(:render_nil)
82
- opts[:render_nil] = @associations[property_name].options[:render_nil]
83
- end
72
+ def check_expanded_associations_exist(expand)
73
+ expand = normalize_expand(expand)
74
+
75
+ expand.each do |property|
76
+ next if self.class.associations.key?(property.to_sym) || property.include?('.')
77
+ fail Association::AssociationNotFound, property
84
78
  end
79
+ end
85
80
 
86
- property("_#{property_name}_association", options)
81
+ def normalize_expand(expand)
82
+ [expand].flatten.map(&:to_s).reject(&:blank?)
87
83
  end
88
84
  end
89
85
  end
@@ -21,8 +21,6 @@ module Pragma
21
21
  def initialize(reflection:, decorator:)
22
22
  @reflection = reflection
23
23
  @decorator = decorator
24
-
25
- check_type_consistency
26
24
  end
27
25
 
28
26
  # Returns the associated object.
@@ -31,52 +29,18 @@ module Pragma
31
29
  def associated_object
32
30
  case reflection.options[:exec_context]
33
31
  when :decorated
34
- model.public_send(reflection.property)
32
+ decorator.decorated.send(reflection.property)
35
33
  when :decorator
36
- decorator.public_send(reflection.property)
34
+ decorator.send(reflection.property)
37
35
  end
38
36
  end
39
37
 
40
- # Returns whether the association belongs to the model.
41
- #
42
- # @return [Boolean]
43
- def model_context?
44
- reflection.options[:exec_context].to_sym == :decorated
45
- end
46
-
47
- # Returns whether the association belongs to the decorator.
48
- #
49
- # @return [Boolean]
50
- def decorator_context?
51
- reflection.options[:exec_context].to_sym == :decorator
52
- end
53
-
54
38
  # Returns the unexpanded value for the associated object (i.e. its +id+ property).
55
39
  #
56
40
  # @return [String]
57
41
  def unexpanded_value
58
- if decorator_context? || model_reflection.nil? || !reflection.options[:optimize]
59
- return associated_object&.public_send(associated_object.class.primary_key)
60
- end
61
-
62
- case reflection.type
63
- when :belongs_to
64
- model.public_send(model_reflection.foreign_key)
65
- when :has_one
66
- if model.association(reflection.property).loaded?
67
- return associated_object&.public_send(associated_object.class.primary_key)
68
- end
69
-
70
- pk = model.public_send(model_reflection.active_record_primary_key)
71
-
72
- model_reflection
73
- .klass
74
- .where(model_reflection.foreign_key => pk)
75
- .pluck(model_reflection.klass.primary_key)
76
- .first
77
- else
78
- associated_object&.public_send(associated_object.class.primary_key)
79
- end
42
+ return unless associated_object
43
+ associated_object.id
80
44
  end
81
45
 
82
46
  # Returns the expanded value for the associated object.
@@ -92,11 +56,7 @@ module Pragma
92
56
  # @param expand [Array<String>] the associations to expand
93
57
  #
94
58
  # @return [Hash]
95
- #
96
- # @raise [UnexpandableError] if the association is not expandable
97
59
  def expanded_value(expand)
98
- fail UnexpandableError, reflection unless reflection.expandable?
99
-
100
60
  return unless associated_object
101
61
 
102
62
  options = {
@@ -153,36 +113,6 @@ module Pragma
153
113
  reflection.options[:decorator]
154
114
  end
155
115
  end
156
-
157
- def model
158
- decorator.decorated
159
- end
160
-
161
- def model_reflection
162
- # rubocop:disable Metrics/LineLength
163
- @model_reflection ||= if Object.const_defined?('ActiveRecord') && model.is_a?(ActiveRecord::Base)
164
- model.class.reflect_on_association(reflection.property)
165
- end
166
- # rubocop:enable Metrics/LineLength
167
- end
168
-
169
- def model_association_type
170
- return unless model_reflection
171
-
172
- if Object.const_defined?('ActiveRecord') && model.is_a?(ActiveRecord::Base)
173
- model_reflection.macro
174
- end
175
- end
176
-
177
- def check_type_consistency
178
- return if !model_association_type || model_association_type == reflection.type
179
-
180
- fail InconsistentTypeError.new(
181
- decorator: decorator,
182
- reflection: reflection,
183
- model_type: model_association_type
184
- )
185
- end
186
116
  end
187
117
  end
188
118
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pragma
4
+ module Decorator
5
+ module Association
6
+ class ExpansionError < StandardError
7
+ end
8
+
9
+ class AssociationNotFound < ExpansionError
10
+ attr_reader :property
11
+
12
+ def initialize(property)
13
+ @property = property
14
+ super "The '#{property}' association is not defined."
15
+ end
16
+ end
17
+
18
+ class UnexpandedAssociationParent < ExpansionError
19
+ attr_reader :child, :parent
20
+
21
+ def initialize(child, parent)
22
+ @child = child
23
+ @parent = parent
24
+
25
+ super "The '#{child}' association is expanded, but its parent '#{parent}' is not."
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -23,7 +23,6 @@ module Pragma
23
23
  # @param property [Symbol] the property holding the associated object
24
24
  # @param options [Hash] additional options
25
25
  #
26
- # @option options [Boolean] :expandable (`false`) whether the association is expandable
27
26
  # @option options [Class|Proc] :decorator the decorator to use for the associated object
28
27
  # or a callable that will return the decorator class (or +nil+ to skip decoration)
29
28
  # @option options [Boolean] :render_nil (`true`) whether to render a +nil+ association
@@ -38,33 +37,31 @@ module Pragma
38
37
  validate_options
39
38
  end
40
39
 
41
- # Returns whether the association is expandable.
42
- #
43
- # @return [Boolean]
44
- def expandable?
45
- options[:expandable]
46
- end
47
-
48
40
  private
49
41
 
50
42
  def normalize_options
51
43
  @options = {
52
- expandable: false,
53
- render_nil: false,
54
- exec_context: :decorated,
55
- optimize: true,
44
+ render_nil: true,
45
+ exec_context: :decorated
56
46
  }.merge(options).tap do |opts|
57
47
  opts[:exec_context] = opts[:exec_context].to_sym
58
48
  end
59
49
  end
60
50
 
61
51
  def validate_options
62
- return if %i[decorator decorated].include?(options[:exec_context])
52
+ unless %i[decorator decorated].include?(options[:exec_context])
53
+ fail(
54
+ ArgumentError,
55
+ "'#{options[:exec_context]}' is not a valid value for :exec_context."
56
+ )
57
+ end
63
58
 
64
- fail(
65
- ArgumentError,
66
- "'#{options[:exec_context]}' is not a valid value for :exec_context."
67
- )
59
+ unless options[:decorator]
60
+ fail(
61
+ ArgumentError,
62
+ 'The :decorator option is required.'
63
+ )
64
+ end
68
65
  end
69
66
  end
70
67
  end
@@ -13,36 +13,7 @@ module Pragma
13
13
  class Base < Roar::Decorator
14
14
  feature Roar::JSON
15
15
 
16
- # Overrides Representable's default +#to_hash+ to save the last options the method was run
17
- # with.
18
- #
19
- # This allows accessing the options from property getters and is required by {Association}.
20
- #
21
- # @param options [Hash]
22
- #
23
- # @return [Hash]
24
- def to_hash(options = {}, *args)
25
- @last_options = options
26
- super(options, *args)
27
- end
28
-
29
- protected
30
-
31
- # Returns the options +#to_hash+ was last run with.
32
- #
33
- # @return [Hash]
34
- def options
35
- @last_options
36
- end
37
-
38
- # Returns the user options +#to_hash+ was last run with.
39
- #
40
- # @return [Hash]
41
- #
42
- # @see #options
43
- def user_options
44
- @last_options[:user_options] || {}
45
- end
16
+ defaults render_nil: true
46
17
  end
47
18
  end
48
19
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pragma
4
+ module Decorator
5
+ module Collection
6
+ def self.included(klass)
7
+ klass.include InstanceMethods
8
+ klass.extend ClassMethods
9
+
10
+ klass.class_eval do
11
+ collection :represented, as: :data, exec_context: :decorator
12
+ end
13
+ end
14
+
15
+ module InstanceMethods
16
+ def type
17
+ 'collection'
18
+ end
19
+ end
20
+
21
+ module ClassMethods
22
+ def decorate_with(decorator)
23
+ collection :represented, as: :data, exec_context: :decorator, decorator: decorator
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pragma
4
+ module Decorator
5
+ module Pagination
6
+ module InstanceMethods
7
+ def current_page
8
+ represented.current_page.to_i
9
+ end
10
+
11
+ def next_page
12
+ represented.next_page
13
+ end
14
+
15
+ def per_page
16
+ per_page_method = if represented.respond_to?(:per_page)
17
+ :per_page
18
+ else
19
+ :limit_value
20
+ end
21
+
22
+ represented.public_send(per_page_method)
23
+ end
24
+
25
+ def previous_page
26
+ previous_page_method = if represented.respond_to?(:previous_page)
27
+ :previous_page
28
+ else
29
+ :prev_page
30
+ end
31
+
32
+ represented.public_send(previous_page_method)
33
+ end
34
+
35
+ def total_entries
36
+ total_entries_method = if represented.respond_to?(:total_entries)
37
+ :total_entries
38
+ else
39
+ :total_count
40
+ end
41
+
42
+ represented.public_send(total_entries_method)
43
+ end
44
+ end
45
+
46
+ def self.included(klass)
47
+ klass.include InstanceMethods
48
+
49
+ klass.class_eval do
50
+ property :total_entries, exec_context: :decorator
51
+ property :per_page, exec_context: :decorator
52
+ property :total_pages
53
+ property :previous_page, exec_context: :decorator
54
+ property :current_page, exec_context: :decorator
55
+ property :next_page, exec_context: :decorator
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -24,7 +24,7 @@ module Pragma
24
24
 
25
25
  def create_timestamp_getter(name, options = {})
26
26
  define_method "_#{name}_timestamp" do
27
- if options[:exec_context] && options[:exec_context].to_sym == :decorator
27
+ if options[:exec_context]&.to_sym == :decorator
28
28
  send(name)
29
29
  else
30
30
  decorated.send(name)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Pragma
4
4
  module Decorator
5
- VERSION = '1.3.0'
5
+ VERSION = '2.0.0'
6
6
  end
7
7
  end
@@ -1,4 +1,3 @@
1
-
2
1
  # frozen_string_literal: true
3
2
 
4
3
  lib = File.expand_path('../lib', __FILE__)
@@ -22,13 +21,13 @@ Gem::Specification.new do |spec|
22
21
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
22
  spec.require_paths = ['lib']
24
23
 
25
- spec.add_dependency 'multi_json', '~> 1.12'
26
24
  spec.add_dependency 'roar', '~> 1.0'
25
+ spec.add_dependency 'multi_json', '~> 1.12'
27
26
 
28
27
  spec.add_development_dependency 'bundler'
29
- spec.add_development_dependency 'coveralls'
30
28
  spec.add_development_dependency 'rake'
31
29
  spec.add_development_dependency 'rspec'
32
30
  spec.add_development_dependency 'rubocop'
33
31
  spec.add_development_dependency 'rubocop-rspec'
32
+ spec.add_development_dependency 'coveralls'
34
33
  end
metadata CHANGED
@@ -1,43 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pragma-decorator
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alessandro Desantis
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-01-17 00:00:00.000000000 Z
11
+ date: 2017-09-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: multi_json
14
+ name: roar
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '1.12'
19
+ version: '1.0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '1.12'
26
+ version: '1.0'
27
27
  - !ruby/object:Gem::Dependency
28
- name: roar
28
+ name: multi_json
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '1.0'
33
+ version: '1.12'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '1.0'
40
+ version: '1.12'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: bundler
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -53,7 +53,7 @@ dependencies:
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
55
  - !ruby/object:Gem::Dependency
56
- name: coveralls
56
+ name: rake
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - ">="
@@ -67,7 +67,7 @@ dependencies:
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
69
  - !ruby/object:Gem::Dependency
70
- name: rake
70
+ name: rspec
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - ">="
@@ -81,7 +81,7 @@ dependencies:
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
83
  - !ruby/object:Gem::Dependency
84
- name: rspec
84
+ name: rubocop
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
87
  - - ">="
@@ -95,7 +95,7 @@ dependencies:
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
97
  - !ruby/object:Gem::Dependency
98
- name: rubocop
98
+ name: rubocop-rspec
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
101
  - - ">="
@@ -109,7 +109,7 @@ dependencies:
109
109
  - !ruby/object:Gem::Version
110
110
  version: '0'
111
111
  - !ruby/object:Gem::Dependency
112
- name: rubocop-rspec
112
+ name: coveralls
113
113
  requirement: !ruby/object:Gem::Requirement
114
114
  requirements:
115
115
  - - ">="
@@ -139,17 +139,14 @@ files:
139
139
  - Rakefile
140
140
  - bin/console
141
141
  - bin/setup
142
- - doc/01-basic-usage.md
143
- - doc/02-object-types.md
144
- - doc/03-associations.md
145
- - doc/04-timestamps.md
146
142
  - lib/pragma/decorator.rb
147
143
  - lib/pragma/decorator/association.rb
148
144
  - lib/pragma/decorator/association/binding.rb
149
- - lib/pragma/decorator/association/inconsistent_type_error.rb
145
+ - lib/pragma/decorator/association/errors.rb
150
146
  - lib/pragma/decorator/association/reflection.rb
151
- - lib/pragma/decorator/association/unexpandable_error.rb
152
147
  - lib/pragma/decorator/base.rb
148
+ - lib/pragma/decorator/collection.rb
149
+ - lib/pragma/decorator/pagination.rb
153
150
  - lib/pragma/decorator/timestamp.rb
154
151
  - lib/pragma/decorator/type.rb
155
152
  - lib/pragma/decorator/version.rb
@@ -1,42 +0,0 @@
1
- # Basic usage
2
-
3
- Creating a decorator is as simple as inheriting from `Pragma::Decorator::Base`:
4
-
5
- ```ruby
6
- module API
7
- module V1
8
- module User
9
- module Decorator
10
- class Resource < Pragma::Decorator::Base
11
- property :id
12
- property :email
13
- property :full_name
14
- end
15
- end
16
- end
17
- end
18
- end
19
- ```
20
-
21
- Just instantiate the decorator by passing it an object to decorate, then call `#to_hash` or
22
- `#to_json`:
23
-
24
- ```ruby
25
- decorator = API::V1::User::Decorator::Resource.new(user)
26
- decorator.to_json
27
- ```
28
-
29
- This will produce the following JSON:
30
-
31
- ```json
32
- {
33
- "id": 1,
34
- "email": "jdoe@example.com",
35
- "full_name": "John Doe"
36
- }
37
- ```
38
-
39
- Since Pragma::Decorator is built on top of [ROAR](https://github.com/apotonick/roar) (which, in
40
- turn, is built on top of [Representable](https://github.com/apotonick/representable)), you should
41
- consult their documentation for the basic usage of decorators; the rest of this section only covers
42
- the features provided specifically by Pragma::Decorator.
@@ -1,47 +0,0 @@
1
- # Object types
2
-
3
- It is recommended that decorators expose the type of the decorated object. You can achieve this
4
- with the `Type` mixin:
5
-
6
- ```ruby
7
- module API
8
- module V1
9
- module User
10
- module Decorator
11
- class Resource < Pragma::Decorator::Base
12
- feature Pragma::Decorator::Type
13
- end
14
- end
15
- end
16
- end
17
- end
18
- ```
19
-
20
- This would result in the following representation:
21
-
22
- ```json
23
- {
24
- "type": "user",
25
- "...": "...""
26
- }
27
- ```
28
-
29
- You can also set a custom type name (just make sure to use it consistently!):
30
-
31
- ```ruby
32
- module API
33
- module V1
34
- module User
35
- module Decorator
36
- class Resource < Pragma::Decorator::Base
37
- def type
38
- :custom_type
39
- end
40
- end
41
- end
42
- end
43
- end
44
- end
45
- ```
46
-
47
- Note: `array` is already overridden with the more language-agnostic `list`.
@@ -1,91 +0,0 @@
1
- # Associations
2
-
3
- `Pragma::Decorator::Association` allows you to define associations in your decorator (currently,
4
- only `belongs_to`/`has_one` associations are supported):
5
-
6
- ```ruby
7
- module API
8
- module V1
9
- module Invoice
10
- module Decorator
11
- class Resource < Pragma::Decorator::Base
12
- feature Pragma::Decorator::Association
13
-
14
- belongs_to :customer
15
- end
16
- end
17
- end
18
- end
19
- end
20
- ```
21
-
22
- Rendering an invoice will now create the following representation:
23
-
24
- ```json
25
- {
26
- "customer": 19
27
- }
28
- ```
29
-
30
- Not impressed? Just wait.
31
-
32
- ## Expanding associations
33
-
34
- We also support association expansion through an interface similar to the one provided by the
35
- [Stripe API](https://stripe.com/docs/api/curl#expanding_objects). You can define which associations
36
- are expandable in the decorator:
37
-
38
- ```ruby
39
- module API
40
- module V1
41
- module Invoice
42
- module Decorator
43
- class Resource < Pragma::Decorator::Base
44
- feature Pragma::Decorator::Association
45
-
46
- belongs_to :customer, expandable: true
47
- end
48
- end
49
- end
50
- end
51
- end
52
- ```
53
-
54
- You can now pass `expand[]=customer` as a request parameter and have the `customer` property
55
- expanded into a full object!
56
-
57
- ```json
58
- {
59
- "customer": {
60
- "id": 19,
61
- "...": "..."
62
- }
63
- }
64
- ```
65
-
66
- ## Nested associations
67
-
68
- This also works for nested associations. For instance, if the customer has a `company` association
69
- marked as expandable, you can pass `expand[]=customer&expand[]=customer.company` to get that
70
- association expanded too.
71
-
72
- In order for association expansion to work, you will have to pass the associations to expand to the
73
- representer as a user option:
74
-
75
- ```ruby
76
- decorator = API::V1::Invoice::Decorator::Resource.new(invoice)
77
- decorator.to_json(user_options: {
78
- expand: ['customer', 'customer.company', 'customer.company.contact']
79
- })
80
- ```
81
-
82
- ## Accepted options
83
-
84
- Here's a list of options accepted when defining an association:
85
-
86
- Name | Type | Default | Meaning
87
- ---- | ---- | ------- | -------
88
- `expandable` | Boolean | `false` | Whether this association is expandable by consumers. Attempting to expand a non-expandable association will raise a `UnexpandableError`.
89
- `decorator` | Class|Proc | - | If provided, decorates the expanded object with this decorator. Otherwise, simply calls `#to_hash` on the object to get a representable hash. If the option is callable, will call it and pass the associated object - a decorator class should be returned, or `nil` to skip decoration.
90
- `render_nil` | Boolean | `false` | Whether the property should be rendered at all when it is `nil`.
91
- `exec_context` | Symbol | `:decorated` | Whether to call the getter on the decorator (`:decorator`) or the decorated object (`:decorated`).
data/doc/04-timestamps.md DELETED
@@ -1,33 +0,0 @@
1
- # Timestamps
2
-
3
- [UNIX time](https://en.wikipedia.org/wiki/Unix_time) is your safest bet when rendering/parsing
4
- timestamps in your API, as it doesn't require a timezone indicator (the timezone is always UTC).
5
-
6
- You can use the `Timestamp` mixin for converting `Time` instances to UNIX times:
7
-
8
- ```ruby
9
- module API
10
- module V1
11
- module User
12
- module Decorator
13
- class Resource < Pragma::Decorator::Base
14
- feature Pragma::Decorator::Timestamp
15
-
16
- timestamp :created_at
17
- end
18
- end
19
- end
20
- end
21
- end
22
- ```
23
-
24
- This will render a user like this:
25
-
26
- ```json
27
- {
28
- "type": "user",
29
- "created_at": 1480287994
30
- }
31
- ```
32
-
33
- The `#timestamp` method supports all the options supported by `#property` (except for `:as`).
@@ -1,27 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Pragma
4
- module Decorator
5
- module Association
6
- # This error is raised when an association's type is different from its type as reported by
7
- # the model's reflection.
8
- #
9
- # @author Alessandro Desantis
10
- class InconsistentTypeError < StandardError
11
- # Initializes the error.
12
- #
13
- # @param decorator [Base] the decorator where the association is defined
14
- # @param reflection [Reflection] the reflection of the inconsistent association
15
- # @param model_type [Symbol|String] the real type of the association
16
- def initialize(decorator:, reflection:, model_type:)
17
- message = <<~MSG.tr("\n", ' ')
18
- #{decorator.class}: Association #{reflection.property} is defined as #{model_type} on
19
- the model, but as #{reflection.type} in the decorator.
20
- MSG
21
-
22
- super message
23
- end
24
- end
25
- end
26
- end
27
- end
@@ -1,19 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Pragma
4
- module Decorator
5
- module Association
6
- # This error is raised when expansion of an unexpandable association is attempted.
7
- #
8
- # @author Alessandro Desantis
9
- class UnexpandableError < StandardError
10
- # Initializes the error.
11
- #
12
- # @param reflection [Reflection] the unexpandable association
13
- def initialize(reflection)
14
- super "Association '#{reflection.property}' cannot be expanded."
15
- end
16
- end
17
- end
18
- end
19
- end