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 +7 -0
- data/.gitignore +10 -0
- data/.rspec +3 -0
- data/.rubocop.yml +84 -0
- data/.travis.yml +9 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +252 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/pragma/decorator/association/binding.rb +116 -0
- data/lib/pragma/decorator/association/reflection.rb +69 -0
- data/lib/pragma/decorator/association/unexpandable_error.rb +18 -0
- data/lib/pragma/decorator/association.rb +88 -0
- data/lib/pragma/decorator/base.rb +47 -0
- data/lib/pragma/decorator/timestamp.rb +43 -0
- data/lib/pragma/decorator/type.rb +31 -0
- data/lib/pragma/decorator/version.rb +6 -0
- data/lib/pragma/decorator.rb +19 -0
- data/pragma-decorator.gemspec +32 -0
- metadata +176 -0
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
data/.rspec
ADDED
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
data/Gemfile
ADDED
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
|
+
[](https://travis-ci.org/pragmarb/pragma-decorator)
|
4
|
+
[](https://gemnasium.com/github.com/pragmarb/pragma-decorator)
|
5
|
+
[](https://codeclimate.com/github/pragmarb/pragma-decorator)
|
6
|
+
[](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
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,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,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: []
|