pragma-decorator 1.3.0 → 2.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: 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