pragma-decorator 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []