pragma-decorator 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7fbddf33eb638f9dc7cc36f989bc22ffea969ee5
4
+ data.tar.gz: 363a2780b6a8316b3330eb7756f1cda1e5a768cf
5
+ SHA512:
6
+ metadata.gz: 34b21171d3f3961379083f634e2178d6b6166d7c75b0d5c50ddb6a5a6ac75390728988beaeb4a31a7ca1a9c45f0436ea32809d84ab13fd015a3866c9323054b6
7
+ data.tar.gz: 1f5a2172ef77a3261676d119deb273bd9b75962db264a3787fa0f3feb600390ac3fb9d280d226e2bc83919075f66eb82ce1ab9ce03f5ef38db044bd195a62aeb
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /spec/examples.txt
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,84 @@
1
+ require: rubocop-rspec
2
+
3
+ AllCops:
4
+ TargetRubyVersion: 2.3
5
+ Include:
6
+ - '**/Gemfile'
7
+ - '**/Rakefile'
8
+ Exclude:
9
+ - 'bin/*'
10
+ - 'db/**/*'
11
+ - 'vendor/bundle/**/*'
12
+ - 'spec/spec_helper.rb'
13
+ - 'spec/rails_helper.rb'
14
+ - 'spec/support/**/*'
15
+ - 'config/**/*'
16
+ - '**/Rakefile'
17
+ - '**/Gemfile'
18
+
19
+ RSpec/DescribeClass:
20
+ Exclude:
21
+ - 'spec/requests/**/*'
22
+
23
+ Style/BlockDelimiters:
24
+ Exclude:
25
+ - 'spec/**/*'
26
+
27
+ Style/AlignParameters:
28
+ EnforcedStyle: with_fixed_indentation
29
+
30
+ Style/ClosingParenthesisIndentation:
31
+ Enabled: false
32
+
33
+ Metrics/LineLength:
34
+ Max: 100
35
+ AllowURI: true
36
+
37
+ Style/FirstParameterIndentation:
38
+ Enabled: false
39
+
40
+ Style/MultilineMethodCallIndentation:
41
+ EnforcedStyle: indented
42
+
43
+ Style/IndentArray:
44
+ EnforcedStyle: consistent
45
+
46
+ Style/IndentHash:
47
+ EnforcedStyle: consistent
48
+
49
+ Style/SignalException:
50
+ EnforcedStyle: semantic
51
+
52
+ Style/BracesAroundHashParameters:
53
+ EnforcedStyle: context_dependent
54
+
55
+ Lint/EndAlignment:
56
+ AlignWith: variable
57
+ AutoCorrect: true
58
+
59
+ Style/AndOr:
60
+ EnforcedStyle: conditionals
61
+
62
+ Style/MultilineBlockChain:
63
+ Enabled: false
64
+
65
+ RSpec/NamedSubject:
66
+ Enabled: false
67
+
68
+ RSpec/ExampleLength:
69
+ Enabled: false
70
+
71
+ Style/MultilineMethodCallBraceLayout:
72
+ Enabled: false
73
+
74
+ Metrics/MethodLength:
75
+ Enabled: false
76
+
77
+ Metrics/AbcSize:
78
+ Enabled: false
79
+
80
+ Metrics/PerceivedComplexity:
81
+ Enabled: false
82
+
83
+ Metrics/CyclomaticComplexity:
84
+ Enabled: false
data/.travis.yml ADDED
@@ -0,0 +1,9 @@
1
+ rvm:
2
+ - 2.3.0
3
+ before_install:
4
+ - gem update --system
5
+ - gem update bundler
6
+ - gem cleanup bundler
7
+ branches:
8
+ only:
9
+ - master
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in pragma-decorator.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Alessandro Desantis
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,252 @@
1
+ # Pragma::Decorator
2
+
3
+ [![Build Status](https://img.shields.io/travis/pragmarb/pragma-decorator.svg?maxAge=3600&style=flat-square)](https://travis-ci.org/pragmarb/pragma-decorator)
4
+ [![Dependency Status](https://img.shields.io/gemnasium/pragmarb/pragma-decorator.svg?maxAge=3600&style=flat-square)](https://gemnasium.com/github.com/pragmarb/pragma-decorator)
5
+ [![Code Climate](https://img.shields.io/codeclimate/github/pragmarb/pragma-decorator.svg?maxAge=3600&style=flat-square)](https://codeclimate.com/github/pragmarb/pragma-decorator)
6
+ [![Coveralls](https://img.shields.io/coveralls/pragmarb/pragma-decorator.svg?maxAge=3600&style=flat-square)](https://coveralls.io/github/pragmarb/pragma-decorator)
7
+
8
+ Decorators are a way to easily convert your API resources to JSON with minimum hassle.
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.
12
+
13
+ ## Installation
14
+
15
+ Add this line to your application's Gemfile:
16
+
17
+ ```ruby
18
+ gem 'pragma-decorator'
19
+ ```
20
+
21
+ And then execute:
22
+
23
+ ```console
24
+ $ bundle
25
+ ```
26
+
27
+ Or install it yourself as:
28
+
29
+ ```console
30
+ $ gem install pragma-decorator
31
+ ```
32
+
33
+ ## Usage
34
+
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 Resource < 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::Resource.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 Resource < 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 Resource < 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
+ ### Associations
125
+
126
+ `Pragma::Decorator::Association` allows you to define associations in your decorator (currently,
127
+ only `belongs_to`/`has_one` associations are supported):
128
+
129
+ ```ruby
130
+ module API
131
+ module V1
132
+ module Invoice
133
+ module Decorator
134
+ class Resource < Pragma::Decorator::Base
135
+ feature Pragma::Decorator::Association
136
+
137
+ belongs_to :customer
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
143
+ ```
144
+
145
+ Rendering an invoice will now create the following representation:
146
+
147
+ ```json
148
+ {
149
+ "customer": {
150
+ "id": 19
151
+ }
152
+ }
153
+ ```
154
+
155
+ Not impressed? Just wait.
156
+
157
+ We also support association expansion through an interface similar to the one provided by the
158
+ [Stripe API](https://stripe.com/docs/api/curl#expanding_objects). You can define which associations
159
+ are expandable in the decorator:
160
+
161
+ ```ruby
162
+ module API
163
+ module V1
164
+ module Invoice
165
+ module Decorator
166
+ class Resource < Pragma::Decorator::Base
167
+ feature Pragma::Decorator::Association
168
+
169
+ belongs_to :customer, expandable: true
170
+ end
171
+ end
172
+ end
173
+ end
174
+ end
175
+ ```
176
+
177
+ You can now pass `expand[]=customer` as a request parameter and have the `customer` property
178
+ expanded into a full object!
179
+
180
+ ```json
181
+ {
182
+ "customer": {
183
+ "id": 19,
184
+ "...": "..."
185
+ }
186
+ }
187
+ ```
188
+
189
+ This also works for nested associations. For instance, if the customer has a `company` association
190
+ marked as expandable, you can pass `expand[]=customer&expand[]=customer.company` to get that
191
+ association expanded too.
192
+
193
+ In order for association expansion to work, you will have to pass the associations to expand to the
194
+ representer as a user option:
195
+
196
+ ```ruby
197
+ decorator = API::V1::Invoice::Decorator::Resource.new(invoice)
198
+ decorator.to_json(user_options: {
199
+ expand: ['customer', 'customer.company', 'customer.company.contact']
200
+ })
201
+ ```
202
+
203
+ Here's a list of options accepted when defining an association:
204
+
205
+ Name | Type | Default | Meaning
206
+ ---- | ---- | ------- | -------
207
+ `expandable` | Boolean | `false` | Whether this association is expandable by consumers. Attempting to expand a non-expandable association will raise a `UnexpandableError`.
208
+ `decorator` | Class | - | If provided, decorates the expanded object with this decorator. Otherwise, simply calls `#to_hash` on the object to get a representable hash.
209
+ `render_nil` | Boolean | `false` | Whether the property should be rendered at all when it is `nil`.
210
+ `exec_context` | Symbol | `:decorated` | Whether to call the getter on the decorator (`:decorator`) or the decorated object (`:decorated`).
211
+
212
+ ### Timestamps
213
+
214
+ [UNIX time](https://en.wikipedia.org/wiki/Unix_time) is your safest bet when rendering/parsing
215
+ timestamps in your API, as it doesn't require a timezone indicator (the timezone is always UTC).
216
+
217
+ You can use the `Timestamp` mixin for converting `Time` instances to UNIX times:
218
+
219
+ ```ruby
220
+ module API
221
+ module V1
222
+ module User
223
+ module Decorator
224
+ class Resource < Pragma::Decorator::Base
225
+ feature Pragma::Decorator::Timestamp
226
+
227
+ timestamp :created_at
228
+ end
229
+ end
230
+ end
231
+ end
232
+ end
233
+ ```
234
+
235
+ This will render a user like this:
236
+
237
+ ```json
238
+ {
239
+ "type": "user",
240
+ "created_at": 1480287994
241
+ }
242
+ ```
243
+
244
+ The `#timestamp` method supports all the options supported by `#property` (except for `:as`).
245
+
246
+ ## Contributing
247
+
248
+ Bug reports and pull requests are welcome on GitHub at https://github.com/pragmarb/pragma-decorator.
249
+
250
+ ## License
251
+
252
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "pragma/decorator"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+ module Pragma
3
+ module Decorator
4
+ module Association
5
+ # Links an association definition to a specific decorator instance, allowing to render it.
6
+ #
7
+ # @author Alessandro Desantis
8
+ class Binding
9
+ # @!attribute [r] reflection
10
+ # @return [Reflection] the association reflection
11
+ #
12
+ # @!attribute [r] decorator
13
+ # @return [Pragma::Decorator::Base] the decorator instance
14
+ attr_reader :reflection, :decorator
15
+
16
+ # Initializes the binding.
17
+ #
18
+ # @param reflection [Reflection] the association reflection
19
+ # @param decorator [Pragma::Decorator::Base] the decorator instance
20
+ def initialize(reflection:, decorator:)
21
+ @reflection = reflection
22
+ @decorator = decorator
23
+ end
24
+
25
+ # Returns the associated object.
26
+ #
27
+ # @return [Object]
28
+ def associated_object
29
+ case reflection.options[:exec_context]
30
+ when :decorated
31
+ decorator.decorated.send(reflection.property)
32
+ when :decorator
33
+ decorator.send(reflection.property)
34
+ end
35
+ end
36
+
37
+ # Returns the unexpanded hash for the associated object (i.e. a hash with only the +id+
38
+ # property).
39
+ #
40
+ # @return [Hash]
41
+ def unexpanded_hash
42
+ return unless associated_object
43
+
44
+ {
45
+ id: associated_object.id
46
+ }
47
+ end
48
+
49
+ # Returns the expanded hash for the associated object.
50
+ #
51
+ # If a decorator was specified for the association, first decorates the associated object,
52
+ # then calls +#to_hash+ to render it as a hash.
53
+ #
54
+ # If no decorator was specified, calls +#as_json+ on the associated object.
55
+ #
56
+ # In any case, passes all nested associations as the +expand+ user option of the method
57
+ # called.
58
+ #
59
+ # @param expand [Array<String>] the associations to expand
60
+ #
61
+ # @return [Hash]
62
+ #
63
+ # @raise [UnexpandableError] if the association is not expandable
64
+ def expanded_hash(expand)
65
+ fail UnexpandableError, reflection unless reflection.expandable?
66
+
67
+ return unless associated_object
68
+
69
+ options = {
70
+ user_options: {
71
+ expand: flatten_expand(expand)
72
+ }
73
+ }
74
+
75
+ if reflection.options[:decorator]
76
+ reflection.options[:decorator].new(associated_object).to_hash(options)
77
+ else
78
+ associated_object.as_json(options)
79
+ end
80
+ end
81
+
82
+ # Renders the unexpanded or expanded associations, depending on the +expand+ user option
83
+ # passed to the decorator.
84
+ #
85
+ # @param expand [Array<String>] the associations to expand
86
+ #
87
+ # @return [Hash|Pragma::Decorator::Base]
88
+ def render(expand)
89
+ return unless associated_object
90
+
91
+ expand ||= []
92
+
93
+ if expand.any? { |value| value.to_s == reflection.property.to_s }
94
+ expanded_hash(expand)
95
+ else
96
+ unexpanded_hash
97
+ end
98
+ end
99
+
100
+ private
101
+
102
+ def flatten_expand(expand)
103
+ expected_beginning = "#{reflection.property}."
104
+
105
+ expand.reject { |value| value.to_s == reflection.property.to_s }.map do |value|
106
+ if value.start_with?(expected_beginning)
107
+ value.sub(expected_beginning, '')
108
+ else
109
+ value
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+ module Pragma
3
+ module Decorator
4
+ module Association
5
+ # Holds the information about an association.
6
+ #
7
+ # @author Alessandro Desantis
8
+ class Reflection
9
+ # @!attribute [r] type
10
+ # @return [Symbol] the type of the association
11
+ #
12
+ # @!attribute [r] property
13
+ # @return [Symbol] the property holding the associated object
14
+ #
15
+ # @!attribute [r] options
16
+ # @return [Hash] additional options for the association
17
+ attr_reader :type, :property, :options
18
+
19
+ # Initializes the association.
20
+ #
21
+ # @param type [Symbol] the type of the association
22
+ # @param property [Symbol] the property holding the associated object
23
+ # @param options [Hash] additional options
24
+ #
25
+ # @option options [Boolean] :expandable (`false`) whether the association is expandable
26
+ # @option options [Class] :decorator the decorator to use for the associated object
27
+ # @option options [Boolean] :render_nil (`true`) whether to render a +nil+ association
28
+ # @option options [Symbol] :exec_context (`decorated`) whether to call the getter on the
29
+ # decorator (+decorator+) or the decorated object (+decorated+)
30
+ def initialize(type, property, **options)
31
+ @type = type
32
+ @property = property
33
+ @options = options
34
+
35
+ normalize_options
36
+ validate_options
37
+ end
38
+
39
+ # Returns whether the association is expandable.
40
+ #
41
+ # @return [Boolean]
42
+ def expandable?
43
+ options[:expandable]
44
+ end
45
+
46
+ private
47
+
48
+ def normalize_options
49
+ @options = {
50
+ expandable: false,
51
+ render_nil: false,
52
+ exec_context: :decorated
53
+ }.merge(options).tap do |opts|
54
+ opts[:exec_context] = opts[:exec_context].to_sym
55
+ end
56
+ end
57
+
58
+ def validate_options
59
+ unless [:decorator, :decorated].include?(options[:exec_context])
60
+ fail(
61
+ ArgumentError,
62
+ "'#{options[:exec_context]}' is not a valid value for :exec_context."
63
+ )
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+ module Pragma
3
+ module Decorator
4
+ module Association
5
+ # This error is raised when expansion of an unexpandable association is attempted.
6
+ #
7
+ # @author Alessandro Desantis
8
+ class UnexpandableError < StandardError
9
+ # Initializes the error.
10
+ #
11
+ # @param reflection [Reflection] the unexpandable association
12
+ def initialize(reflection)
13
+ super "Association '#{reflection.property}' cannot be expanded."
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+ module Pragma
3
+ module Decorator
4
+ # Adds association expansion to decorators.
5
+ #
6
+ # @author Alessandro Desantis
7
+ module Association
8
+ def self.included(klass)
9
+ klass.extend ClassMethods
10
+
11
+ klass.class_eval do
12
+ @associations = {}
13
+
14
+ def self.associations
15
+ @associations
16
+ end
17
+ end
18
+ end
19
+
20
+ # Inizializes the decorator and bindings for all the associations.
21
+ #
22
+ # @see Association::Binding
23
+ def initialize(*)
24
+ super
25
+
26
+ @association_bindings = {}
27
+ self.class.associations.each_pair do |property, reflection|
28
+ @association_bindings[property] = Binding.new(reflection: reflection, decorator: self)
29
+ end
30
+ end
31
+
32
+ module ClassMethods # rubocop:disable Style/Documentation
33
+ # Defines a +belongs_to+ association.
34
+ #
35
+ # See {Association::Reflection#initialize} for the list of available options.
36
+ #
37
+ # @param property [Symbol] the property containing the associated object
38
+ # @param options [Hash] the options of the association
39
+ def belongs_to(property, options = {})
40
+ define_association :belongs_to, property, options
41
+ end
42
+
43
+ # Defines a +has_one+ association.
44
+ #
45
+ # See {Association::Reflection#initialize} for the list of available options.
46
+ #
47
+ # @param property [Symbol] the property containing the associated object
48
+ # @param options [Hash] the options of the association
49
+ def has_one(property, options = {}) # rubocop:disable Style/PredicateName
50
+ define_association :has_one, property, options
51
+ end
52
+
53
+ private
54
+
55
+ def define_association(type, property, options = {})
56
+ create_association_definition(type, property, options)
57
+ create_association_getter(property)
58
+ create_association_property(property)
59
+ end
60
+
61
+ def create_association_definition(type, property, options)
62
+ @associations[property.to_sym] = Reflection.new(type, property, options)
63
+ end
64
+
65
+ def create_association_getter(property)
66
+ class_eval <<~RUBY
67
+ private def _#{property}_association
68
+ @association_bindings[:#{property}].render(user_options[:expand])
69
+ end
70
+ RUBY
71
+ end
72
+
73
+ def create_association_property(property_name)
74
+ options = {
75
+ exec_context: :decorator,
76
+ as: property_name
77
+ }.tap do |opts|
78
+ if @associations[property_name].options.key?(:render_nil)
79
+ opts[:render_nil] = @associations[property_name].options[:render_nil]
80
+ end
81
+ end
82
+
83
+ property("_#{property_name}_association", options)
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+ require 'roar/decorator'
3
+ require 'roar/json'
4
+
5
+ module Pragma
6
+ module Decorator
7
+ # This is the base decorator that all your resource-specific decorators should extend from.
8
+ #
9
+ # It is already configured to render your resources in JSON.
10
+ #
11
+ # @author Alessandro Desantis
12
+ class Base < Roar::Decorator
13
+ feature Roar::JSON
14
+
15
+ # Overrides Representable's default +#to_hash+ to save the last options the method was run
16
+ # with.
17
+ #
18
+ # This allows accessing the options from property getters and is required by {Association}.
19
+ #
20
+ # @param options [Hash]
21
+ #
22
+ # @return [Hash]
23
+ def to_hash(options = {}, *args)
24
+ @last_options = options
25
+ super(options, *args)
26
+ end
27
+
28
+ protected
29
+
30
+ # Returns the options +#to_hash+ was last run with.
31
+ #
32
+ # @return [Hash]
33
+ def options
34
+ @last_options
35
+ end
36
+
37
+ # Returns the user options +#to_hash+ was last run with.
38
+ #
39
+ # @return [Hash]
40
+ #
41
+ # @see #options
42
+ def user_options
43
+ @last_options[:user_options] || {}
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+ module Pragma
3
+ module Decorator
4
+ # Supports rendering timestamps as UNIX times.
5
+ #
6
+ # @author Alessandro Desantis
7
+ module Timestamp
8
+ def self.included(klass)
9
+ klass.extend ClassMethods
10
+ end
11
+
12
+ module ClassMethods # rubocop:disable Style/Documentation
13
+ # Defines a timestamp property which will be rendered as UNIX time.
14
+ #
15
+ # @param name [Symbol] the name of the property
16
+ # @param options [Hash] the options of the property
17
+ def timestamp(name, options = {})
18
+ create_timestamp_getter(name, options)
19
+ create_timestamp_property(name, options)
20
+ end
21
+
22
+ private
23
+
24
+ def create_timestamp_getter(name, options = {})
25
+ define_method "_#{name}_timestamp" do
26
+ if options[:exec_context] && options[:exec_context].to_sym == :decorator
27
+ send(name)
28
+ else
29
+ decorated.send(name)
30
+ end&.to_i
31
+ end
32
+ end
33
+
34
+ def create_timestamp_property(name, options = {})
35
+ property "_#{name}_timestamp", options.merge(
36
+ as: name,
37
+ exec_context: :decorator
38
+ )
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+ module Pragma
3
+ module Decorator
4
+ # Adds a +type+ property containing the machine-readable type of the represented object.
5
+ #
6
+ # @author Alessandro Desantis
7
+ module Type
8
+ TYPE_OVERRIDES = {
9
+ array: 'list'
10
+ }.freeze
11
+
12
+ def self.included(klass)
13
+ klass.class_eval do
14
+ property :type, exec_context: :decorator, render_nil: false
15
+ end
16
+ end
17
+
18
+ # Returns the type of the decorated object (i.e. its underscored class name).
19
+ #
20
+ # @return [String]
21
+ def type
22
+ type = decorated.class.name
23
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
24
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
25
+ .downcase
26
+
27
+ TYPE_OVERRIDES[type.to_sym] || type
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+ module Pragma
3
+ module Decorator
4
+ VERSION = '0.1.0'
5
+ end
6
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+ require 'roar'
3
+
4
+ require 'pragma/decorator/version'
5
+ require 'pragma/decorator/base'
6
+ require 'pragma/decorator/association'
7
+ require 'pragma/decorator/association/reflection'
8
+ require 'pragma/decorator/association/binding'
9
+ require 'pragma/decorator/association/unexpandable_error'
10
+ require 'pragma/decorator/timestamp'
11
+ require 'pragma/decorator/type'
12
+
13
+ module Pragma
14
+ # Represent your API resources in JSON with minimum hassle.
15
+ #
16
+ # @author Alessandro Desantis
17
+ module Decorator
18
+ end
19
+ end
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'pragma/decorator/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'pragma-decorator'
8
+ spec.version = Pragma::Decorator::VERSION
9
+ spec.authors = ['Alessandro Desantis']
10
+ spec.email = ['desa.alessandro@gmail.com']
11
+
12
+ spec.summary = 'Convert your API resources into JSON with minimum hassle.'
13
+ spec.homepage = 'https://github.com/pragmarb/pragma-decorator'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
17
+ f.match(%r{^(test|spec|features)/})
18
+ end
19
+ spec.bindir = 'exe'
20
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
+ spec.require_paths = ['lib']
22
+
23
+ spec.add_dependency 'roar', '~> 1.0'
24
+ spec.add_dependency 'multi_json', '~> 1.12'
25
+
26
+ spec.add_development_dependency 'bundler'
27
+ spec.add_development_dependency 'rake'
28
+ spec.add_development_dependency 'rspec'
29
+ spec.add_development_dependency 'rubocop'
30
+ spec.add_development_dependency 'rubocop-rspec'
31
+ spec.add_development_dependency 'coveralls'
32
+ end
metadata ADDED
@@ -0,0 +1,176 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pragma-decorator
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Alessandro Desantis
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-12-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: roar
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: multi_json
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.12'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.12'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop-rspec
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: coveralls
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ description:
126
+ email:
127
+ - desa.alessandro@gmail.com
128
+ executables: []
129
+ extensions: []
130
+ extra_rdoc_files: []
131
+ files:
132
+ - ".gitignore"
133
+ - ".rspec"
134
+ - ".rubocop.yml"
135
+ - ".travis.yml"
136
+ - Gemfile
137
+ - LICENSE.txt
138
+ - README.md
139
+ - Rakefile
140
+ - bin/console
141
+ - bin/setup
142
+ - lib/pragma/decorator.rb
143
+ - lib/pragma/decorator/association.rb
144
+ - lib/pragma/decorator/association/binding.rb
145
+ - lib/pragma/decorator/association/reflection.rb
146
+ - lib/pragma/decorator/association/unexpandable_error.rb
147
+ - lib/pragma/decorator/base.rb
148
+ - lib/pragma/decorator/timestamp.rb
149
+ - lib/pragma/decorator/type.rb
150
+ - lib/pragma/decorator/version.rb
151
+ - pragma-decorator.gemspec
152
+ homepage: https://github.com/pragmarb/pragma-decorator
153
+ licenses:
154
+ - MIT
155
+ metadata: {}
156
+ post_install_message:
157
+ rdoc_options: []
158
+ require_paths:
159
+ - lib
160
+ required_ruby_version: !ruby/object:Gem::Requirement
161
+ requirements:
162
+ - - ">="
163
+ - !ruby/object:Gem::Version
164
+ version: '0'
165
+ required_rubygems_version: !ruby/object:Gem::Requirement
166
+ requirements:
167
+ - - ">="
168
+ - !ruby/object:Gem::Version
169
+ version: '0'
170
+ requirements: []
171
+ rubyforge_project:
172
+ rubygems_version: 2.5.2
173
+ signing_key:
174
+ specification_version: 4
175
+ summary: Convert your API resources into JSON with minimum hassle.
176
+ test_files: []