tiny_serializer 2.0.2
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 +18 -0
- data/.idea/vcs.xml +6 -0
- data/.rspec +1 -0
- data/.rubocop.yml +23 -0
- data/.ruby-version +1 -0
- data/.travis.yml +10 -0
- data/CHANGELOG.md +73 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +48 -0
- data/LICENSE.txt +21 -0
- data/README.md +239 -0
- data/Rakefile +15 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/tiny_serializer/dsl.rb +162 -0
- data/lib/tiny_serializer/rails_extensions.rb +18 -0
- data/lib/tiny_serializer/version.rb +5 -0
- data/lib/tiny_serializer.rb +213 -0
- data/tiny_serializer.gemspec +31 -0
- metadata +119 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: b3f2930aab86eaf43af0326633d4698fdd4f1e51
|
4
|
+
data.tar.gz: 297b7d9fb72d0075853448ec056e7ea596c2e4fd
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6c3ab366f4a4e5bcabc111d78ad10eae27ce23c23f54d2f009963b09005adae9dfcbb534755352ba7750923db7e099cdf297f6f1e8c9177df98ffc9cc4ae86bf
|
7
|
+
data.tar.gz: 959b2a9e76f0303615dab63a52cb52a76b3861362832530139e196e583b91bdc9c1110bd90b1fc9e76c99c18da0fb6dc182296b3059d865dc7d6c44cfcfebbda
|
data/.gitignore
ADDED
data/.idea/vcs.xml
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--require spec_helper
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
---
|
2
|
+
AllCops:
|
3
|
+
TargetRubyVersion: 2.4
|
4
|
+
|
5
|
+
Style/StringLiterals:
|
6
|
+
EnforcedStyle: double_quotes
|
7
|
+
Exclude:
|
8
|
+
- "**/*_spec.rb"
|
9
|
+
|
10
|
+
# We might want to make this work on an old Ruby sometime.
|
11
|
+
Style/SafeNavigation:
|
12
|
+
Enabled: false
|
13
|
+
|
14
|
+
Metrics/BlockLength:
|
15
|
+
Exclude:
|
16
|
+
- "**/*_spec.rb"
|
17
|
+
|
18
|
+
Layout/EmptyLineAfterMagicComment:
|
19
|
+
Enabled: false
|
20
|
+
|
21
|
+
Layout/EmptyLines:
|
22
|
+
Exclude:
|
23
|
+
- "**/*_spec.rb"
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.4.5
|
data/.travis.yml
ADDED
data/CHANGELOG.md
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
# SimpleSerializer Change Log
|
2
|
+
|
3
|
+
## 2.0.2
|
4
|
+
|
5
|
+
* Fix major incorrect require statement.
|
6
|
+
|
7
|
+
## 2.0.1
|
8
|
+
|
9
|
+
* Bump version so I can publish under the correct rubygems account.
|
10
|
+
|
11
|
+
## 2.0.0
|
12
|
+
|
13
|
+
* Rename to "tiny_serializer" and push to rubygems.org.
|
14
|
+
* Update .travis.yml ruby versions to 2.4.5 and 2.5.3
|
15
|
+
* Minor rubocop config tweaks and add frozen\_string_literal to specs.
|
16
|
+
|
17
|
+
## 1.0.0
|
18
|
+
|
19
|
+
* Remove netflix/fast_jsonapi NOOP compatibily methods.
|
20
|
+
* Rubocop style improvements.
|
21
|
+
|
22
|
+
## 0.5.3
|
23
|
+
|
24
|
+
* Raise ArgumentError if serialize_each called without a collection.
|
25
|
+
(Helps find unexpected values earlier in testing.)
|
26
|
+
|
27
|
+
## 0.5.2
|
28
|
+
|
29
|
+
* Fix place I forgot to create a new serializer instance.
|
30
|
+
|
31
|
+
## 0.5.1
|
32
|
+
|
33
|
+
* Fix `key` arguments not being passed around correctly.
|
34
|
+
* Minor code clarity improvements.
|
35
|
+
|
36
|
+
## 0.5.0
|
37
|
+
|
38
|
+
* Pass options to has_many definition block.
|
39
|
+
* Use a new serializer instance to serialize each item in a collection.
|
40
|
+
|
41
|
+
## 0.4.0
|
42
|
+
|
43
|
+
* Implement options parameter (from AMS).
|
44
|
+
* Raises ArgumentError if a specified serializer is not actually a
|
45
|
+
SimpleSerializer subclass.
|
46
|
+
* Add note to README.md about attribute inheritance.
|
47
|
+
|
48
|
+
## 0.3.1
|
49
|
+
|
50
|
+
* Fix bug where collections would be checked for existence before serialization,
|
51
|
+
causing unnecessary database queries.
|
52
|
+
|
53
|
+
## 0.3.0
|
54
|
+
|
55
|
+
* Fix blocks not being passed on to the #sub_record method.
|
56
|
+
* Major documentation improvements.
|
57
|
+
* Code cleanup and simplification.
|
58
|
+
* Travis CI config improvements.
|
59
|
+
* Allow #as_json and #to_json to take arguments.
|
60
|
+
|
61
|
+
## 0.2.1
|
62
|
+
|
63
|
+
* Remove lack of inheritance as a downside!
|
64
|
+
|
65
|
+
## 0.2.0
|
66
|
+
|
67
|
+
* Implement serialized attribute inheritance.
|
68
|
+
* README improvements.
|
69
|
+
* Add this CHANGELOG.
|
70
|
+
|
71
|
+
## 0.1.0
|
72
|
+
|
73
|
+
* Initial post to Reddit.
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
tiny_serializer (2.0.2)
|
5
|
+
activesupport (>= 4.2)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
activesupport (5.2.1)
|
11
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
12
|
+
i18n (>= 0.7, < 2)
|
13
|
+
minitest (~> 5.1)
|
14
|
+
tzinfo (~> 1.1)
|
15
|
+
concurrent-ruby (1.0.5)
|
16
|
+
diff-lcs (1.3)
|
17
|
+
i18n (1.1.1)
|
18
|
+
concurrent-ruby (~> 1.0)
|
19
|
+
minitest (5.11.3)
|
20
|
+
rake (10.5.0)
|
21
|
+
rspec (3.7.0)
|
22
|
+
rspec-core (~> 3.7.0)
|
23
|
+
rspec-expectations (~> 3.7.0)
|
24
|
+
rspec-mocks (~> 3.7.0)
|
25
|
+
rspec-core (3.7.1)
|
26
|
+
rspec-support (~> 3.7.0)
|
27
|
+
rspec-expectations (3.7.0)
|
28
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
29
|
+
rspec-support (~> 3.7.0)
|
30
|
+
rspec-mocks (3.7.0)
|
31
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
32
|
+
rspec-support (~> 3.7.0)
|
33
|
+
rspec-support (3.7.1)
|
34
|
+
thread_safe (0.3.6)
|
35
|
+
tzinfo (1.2.5)
|
36
|
+
thread_safe (~> 0.1)
|
37
|
+
|
38
|
+
PLATFORMS
|
39
|
+
ruby
|
40
|
+
|
41
|
+
DEPENDENCIES
|
42
|
+
bundler (~> 1.0)
|
43
|
+
rake (~> 10.0)
|
44
|
+
rspec (~> 3.5)
|
45
|
+
tiny_serializer!
|
46
|
+
|
47
|
+
BUNDLED WITH
|
48
|
+
1.17.1
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2018 William Makley
|
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,239 @@
|
|
1
|
+
# TinySerializer
|
2
|
+
|
3
|
+
Simple DSL for converting objects to Hashes, which is mostly API-compatible with
|
4
|
+
[ActiveModel::Serializers](https://github.com/rails-api/active_model_serializers).
|
5
|
+
Not quite a drop-in replacement, but facilitates migrating away from it.
|
6
|
+
The resulting Hashes can be rendered as JSON by Rails using the ActiveSupport::JSON
|
7
|
+
module.
|
8
|
+
|
9
|
+
Unlike AMS, it does **not** integrate with Rails for automatic usage with the `render` function,
|
10
|
+
which has proven to be one of the biggest sources of confusion and complexity for me.
|
11
|
+
`render json: @object` will continue to work exactly as it does in a base Rails setup.
|
12
|
+
|
13
|
+
Use if you have heavily invested in [active_model_serializers](https://github.com/rails-api/active_model_serializers), but have started experiencing the same frustrations I had with it and can't transition to [jsonapi-rb](http://jsonapi-rb.org/) or [fast_jsonapi](https://github.com/Netflix/fast_jsonapi).
|
14
|
+
|
15
|
+

|
16
|
+
|
17
|
+
**Benefits:**
|
18
|
+
|
19
|
+
* Extremely simple and deterministic behavior; no adapter classes and other complications. Just turns Objects into Hashes.
|
20
|
+
* Easy to understand; uses `#as_json` to serialize attributes.
|
21
|
+
* Simple and explicit invocation.
|
22
|
+
* Does not leak memory in development.
|
23
|
+
* ~200 lines of code, give or take.
|
24
|
+
* Seems pretty darn fast, at least as fast as `public_send`, `Hash#[]=`, and `Hash#to_json` can be.
|
25
|
+
|
26
|
+
**Downsides:**
|
27
|
+
|
28
|
+
* Uses `#as_json` to serialize attributes (maybe unintended consequences, especially with complex objects).
|
29
|
+
* Requires ActiveSupport.
|
30
|
+
|
31
|
+
## Usage
|
32
|
+
|
33
|
+
### Serializer definition:
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
MyObject = Struct.new(:id, :first_name, :last_name, :date) do
|
37
|
+
def parent; nil; end
|
38
|
+
def sub_record; nil; end
|
39
|
+
def related_items; []; end
|
40
|
+
end
|
41
|
+
|
42
|
+
class MyObjectSerializer < TinySerializer
|
43
|
+
attributes :id,
|
44
|
+
:first_name,
|
45
|
+
:last_name,
|
46
|
+
:date
|
47
|
+
|
48
|
+
belongs_to :parent, serializer: ParentSerializer
|
49
|
+
has_one :sub_record # serializer will be inferred to be SubRecordSerializer
|
50
|
+
has_many :related_items, serializer: RelatedItemSerializer
|
51
|
+
|
52
|
+
attribute :full_name do |object|
|
53
|
+
"#{object.first_name} #{object.last_name}"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
```
|
57
|
+
|
58
|
+
**Notes on blocks:**
|
59
|
+
|
60
|
+
The `object` parameter for blocks is optional, as blocks are executed
|
61
|
+
in the context of the serializer instance. It just makes it easier
|
62
|
+
to use the [fast_jsonapi](https://github.com/Netflix/fast_jsonapi) gem later if you want.
|
63
|
+
|
64
|
+
### Usage:
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
object = MyObject.new(...)
|
68
|
+
# Several ways to invoke the serializer are available:
|
69
|
+
MyObjectSerializer.new(object).serialize
|
70
|
+
MyObjectSerializer.serialize(object)
|
71
|
+
```
|
72
|
+
|
73
|
+
Produces:
|
74
|
+
|
75
|
+
```json
|
76
|
+
{
|
77
|
+
"id": 1,
|
78
|
+
"first_name": "Fred",
|
79
|
+
"last_name": "Flintstone",
|
80
|
+
"date": "2000-01-01",
|
81
|
+
"full_name": "Fred Flintstone",
|
82
|
+
"parent": null,
|
83
|
+
"sub_record": null,
|
84
|
+
"related_items": []
|
85
|
+
}
|
86
|
+
```
|
87
|
+
|
88
|
+
### Usage in Rails:
|
89
|
+
|
90
|
+
In Rails, calling `.serialize` is optional because **TinySerializer** implements all the 'magic' methods (`as_json`, `to_json`, and `serializable_hash`), although I'm not the biggest fan (it loses some explicitness):
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
@model = MyObject.new(1, "Fred", "Flintstone", Date.new(2000, 1, 1))
|
94
|
+
render json: MyObjectSerializer.new(@model)
|
95
|
+
```
|
96
|
+
|
97
|
+
### Under the Hood
|
98
|
+
|
99
|
+
Takes every attribute you define, and uses it to call `#public_send` on the object to serialize,
|
100
|
+
then uses `#as_json` to serialize the resulting value. If you define the attribute with a block, it will call the block to get the value instead.
|
101
|
+
|
102
|
+
### Usage with Collections:
|
103
|
+
|
104
|
+
Collections are handled automatically:
|
105
|
+
|
106
|
+
```ruby
|
107
|
+
class MyObjectSerializer < TinySerializer
|
108
|
+
attributes :id, :first_name
|
109
|
+
end
|
110
|
+
|
111
|
+
items = [
|
112
|
+
MyObject.new(1, "Fred"),
|
113
|
+
MyObject.new(2, "Wilma")
|
114
|
+
]
|
115
|
+
MyObjectSerializer.new(items).serialize
|
116
|
+
```
|
117
|
+
|
118
|
+
Produces:
|
119
|
+
|
120
|
+
```json
|
121
|
+
[
|
122
|
+
{"id": 1, "name": "Fred"},
|
123
|
+
{"id": 2, "name": "Wilma"}
|
124
|
+
]
|
125
|
+
```
|
126
|
+
|
127
|
+
### Defining Associations and Serializing Sub-Objects
|
128
|
+
|
129
|
+
The DSL methods `belongs_to`, `has_one`, and `has_many` are all synonyms for "use this serializer to serialize this property". They are basically syntax sugar for:
|
130
|
+
|
131
|
+
```ruby
|
132
|
+
class MySerializer < TinySerializer
|
133
|
+
attribute :parent do |object|
|
134
|
+
ParentSerializer.new(object.parent).serialize
|
135
|
+
end
|
136
|
+
end
|
137
|
+
```
|
138
|
+
|
139
|
+
These methods all optionally take a block, which you can use to customize the
|
140
|
+
object or collection. For example, to avoid loading every single item in a
|
141
|
+
`has_many` relation:
|
142
|
+
|
143
|
+
```ruby
|
144
|
+
class MySerializer < TinySerializer
|
145
|
+
has_many :items, serializer: ItemSerializer do |object|
|
146
|
+
object.items.limit(100)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
```
|
150
|
+
|
151
|
+
### Links and URL's
|
152
|
+
|
153
|
+
When used with Rails, serializer instances have the `#url_helpers`
|
154
|
+
method available.
|
155
|
+
|
156
|
+
|
157
|
+
```ruby
|
158
|
+
class ItemSerializer < TinySerializer
|
159
|
+
attribute :link do
|
160
|
+
url_helpers.item_path(object)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
```
|
164
|
+
|
165
|
+
### Notes on ID's.
|
166
|
+
|
167
|
+
If you don't want ID's to be serialized as numeric types,
|
168
|
+
you can have them automatically detected and coerced to Strings.
|
169
|
+
|
170
|
+
```ruby
|
171
|
+
class MyObjectSerializer < TinySerializer
|
172
|
+
self.coerce_ids_to_string = true
|
173
|
+
end
|
174
|
+
```
|
175
|
+
|
176
|
+
To enable globally, but opt out for a specific attribute:
|
177
|
+
|
178
|
+
```ruby
|
179
|
+
class MyObjectSerializer < TinySerializer
|
180
|
+
self.coerce_ids_to_string = true
|
181
|
+
|
182
|
+
attribute :id, is_id: false
|
183
|
+
end
|
184
|
+
|
185
|
+
### Notes on Options
|
186
|
+
|
187
|
+
If you want to pass in options to the serializer you can access via the second argument of the block
|
188
|
+
|
189
|
+
|
190
|
+
class MyObjectSerializer < TinySerializer
|
191
|
+
attribute :id do |object, options|
|
192
|
+
options[:prefix] + object.id
|
193
|
+
end
|
194
|
+
end
|
195
|
+
```
|
196
|
+
MyObjectSerializer.new(object, { prefix: "prefix_" }).serialize
|
197
|
+
|
198
|
+
Produces:
|
199
|
+
|
200
|
+
{
|
201
|
+
id: "prefix_id"
|
202
|
+
}
|
203
|
+
|
204
|
+
### Attribute Inheritance
|
205
|
+
|
206
|
+
Attributes are inherited from parent classes, but can be extended.
|
207
|
+
This example works as you would expect from [active_model_serializers](https://github.com/rails-api/active_model_serializers)
|
208
|
+
(the `name` attribute will only appear if you use `MyObjectSerializer::WithName.serialize(my_object)`):
|
209
|
+
|
210
|
+
```ruby
|
211
|
+
class MyObjectSerializer < TinySerializer
|
212
|
+
attribute :id
|
213
|
+
|
214
|
+
class WithName < MyObjectSerializer
|
215
|
+
attribute :name
|
216
|
+
end
|
217
|
+
end
|
218
|
+
```
|
219
|
+
|
220
|
+
|
221
|
+
## Installation
|
222
|
+
|
223
|
+
Add this line to your application's Gemfile:
|
224
|
+
|
225
|
+
```ruby
|
226
|
+
gem 'tiny_serializer', '~> 2.0.0'
|
227
|
+
```
|
228
|
+
|
229
|
+
And then execute:
|
230
|
+
|
231
|
+
$ bundle
|
232
|
+
|
233
|
+
## Contributing
|
234
|
+
|
235
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/wmakley/tiny_serializer.
|
236
|
+
|
237
|
+
## License
|
238
|
+
|
239
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require "rspec/core/rake_task"
|
3
|
+
|
4
|
+
RSpec::Core::RakeTask.new(:spec)
|
5
|
+
|
6
|
+
task :default => :spec
|
7
|
+
|
8
|
+
begin
|
9
|
+
require "rdoc/task"
|
10
|
+
RDoc::Task.new do |rdoc|
|
11
|
+
rdoc.main = "README.md"
|
12
|
+
rdoc.rdoc_files.include("README.md", "lib/**/*.rb")
|
13
|
+
end
|
14
|
+
rescue LoadError
|
15
|
+
end
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "tiny_serializer"
|
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(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,162 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/core_ext/string/inflections"
|
4
|
+
|
5
|
+
class TinySerializer
|
6
|
+
# The TinySerializer class macros.
|
7
|
+
module DSL
|
8
|
+
# Get all the sub-record attribute definitions (created by #belongs_to,
|
9
|
+
# #has_one, etc.)
|
10
|
+
def sub_records
|
11
|
+
@sub_records ||=
|
12
|
+
if superclass.respond_to?(:sub_records)
|
13
|
+
superclass.sub_records.dup
|
14
|
+
else
|
15
|
+
[]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# Get only the names of the sub-record attribute definitions (created by
|
20
|
+
# #belongs_to, #has_one, etc.)
|
21
|
+
def sub_record_names
|
22
|
+
sub_records.map(&:first)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Get all the collection definitions (created by #has_many).
|
26
|
+
def collections
|
27
|
+
@collections ||=
|
28
|
+
if superclass.respond_to?(:collections)
|
29
|
+
superclass.collections.dup
|
30
|
+
else
|
31
|
+
[]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Get only the names of the collections definitions (created by #has_many).
|
36
|
+
def collection_names
|
37
|
+
collections.map(&:first)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Definite a new attribute to serialize. The value to serialize is retrieved
|
41
|
+
# in one of two ways:
|
42
|
+
#
|
43
|
+
# 1. *Default:* Calls #public_send(name) on TinySerializer#object.
|
44
|
+
# 2. *Block:* The return value of the block is used.
|
45
|
+
#
|
46
|
+
# name::
|
47
|
+
# The name of the attribute
|
48
|
+
# key::
|
49
|
+
# Optional. Defaults to *name*. The Hash key to assign the attribute's
|
50
|
+
# value to.
|
51
|
+
# is_id::
|
52
|
+
# Optional. Whether the attribute is a database ID. Guessed from its name
|
53
|
+
# by default.
|
54
|
+
#
|
55
|
+
def attribute(name, key: name, is_id: _is_id?(name), &block)
|
56
|
+
_initialize_attributes
|
57
|
+
name = name.to_sym
|
58
|
+
attribute = [name, key, is_id, block]
|
59
|
+
@attributes << attribute
|
60
|
+
return attribute
|
61
|
+
end
|
62
|
+
|
63
|
+
# Define multiple attributes at once, using the defaults.
|
64
|
+
def attributes(*names)
|
65
|
+
if names && !names.empty?
|
66
|
+
names.each do |name|
|
67
|
+
attribute(name)
|
68
|
+
end
|
69
|
+
else
|
70
|
+
_initialize_attributes
|
71
|
+
end
|
72
|
+
return @attributes
|
73
|
+
end
|
74
|
+
|
75
|
+
# Get the names of all attributes defined using #attribute.
|
76
|
+
def attribute_names
|
77
|
+
attributes.map(&:first)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Alias of #sub_record
|
81
|
+
def has_one(association_name, key: association_name, serializer: nil, &block)
|
82
|
+
sub_record(association_name, key: key, serializer: serializer, &block)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Alias of #sub_record
|
86
|
+
def belongs_to(association_name, key: association_name, serializer: nil, &block)
|
87
|
+
sub_record(association_name, key: key, serializer: serializer, &block)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Define a serializer to use for a sub-object of TinySerializer#object.
|
91
|
+
# <b>If given a block:</b> Will use the block to retrieve the
|
92
|
+
# object, instead of public_send(name).
|
93
|
+
#
|
94
|
+
# name::
|
95
|
+
# The method name of the sub-object.
|
96
|
+
# serializer::
|
97
|
+
# Optional. The serializer class to use. Inferred from name if blank.
|
98
|
+
# Must be a subclass of TinySerializer.
|
99
|
+
# key::
|
100
|
+
# Optional. Defaults to *name*. The Hash key to assign the sub-record's
|
101
|
+
# JSON to.
|
102
|
+
#
|
103
|
+
def sub_record(name, key: name, serializer: nil, &block)
|
104
|
+
if serializer.nil?
|
105
|
+
serializer = "#{name.to_s.camelize}Serializer".constantize
|
106
|
+
elsif !_is_serializer?(serializer)
|
107
|
+
raise ArgumentError, "#{serializer} does not appear to be a TinySerializer"
|
108
|
+
end
|
109
|
+
|
110
|
+
sub_records << [name, key, serializer, block]
|
111
|
+
end
|
112
|
+
|
113
|
+
# Alias of #collection
|
114
|
+
def has_many(name, key: name, serializer: nil, &block)
|
115
|
+
collection(name, key: key, serializer: serializer, &block)
|
116
|
+
end
|
117
|
+
|
118
|
+
# Define a serializer to use to serialize a collection of objects
|
119
|
+
# as an Array. Will call #public_send(name) on TinySerializer#object
|
120
|
+
# to get the items in the collection, or use the return value of
|
121
|
+
# the block.
|
122
|
+
#
|
123
|
+
# name::
|
124
|
+
# The name of the collection.
|
125
|
+
# key::
|
126
|
+
# Optional. Defaults to *name*. The Hash key to assign the serialized
|
127
|
+
# Array to.
|
128
|
+
# serializer::
|
129
|
+
# Optional, inferred from *collection_name*. The serializer class to use
|
130
|
+
# to serialize each item in the collection. Must be a subclass of
|
131
|
+
# TinySerializer.
|
132
|
+
#
|
133
|
+
def collection(name, key: name, serializer: nil, &block)
|
134
|
+
if serializer.nil?
|
135
|
+
serializer = "#{name.to_s.singularize.camelize}Serializer".constantize
|
136
|
+
elsif !_is_serializer?(serializer)
|
137
|
+
raise ArgumentError, "#{serializer} does not appear to be a TinySerializer"
|
138
|
+
end
|
139
|
+
collections << [name, key, serializer, block]
|
140
|
+
end
|
141
|
+
|
142
|
+
# Private method to initialize inherited attributes.
|
143
|
+
def _initialize_attributes # :nodoc:
|
144
|
+
@attributes ||=
|
145
|
+
if superclass.respond_to?(:attributes)
|
146
|
+
superclass.attributes.dup
|
147
|
+
else
|
148
|
+
[]
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# Private method to check if an attribute name is an ID.
|
153
|
+
def _is_id?(name) # :nodoc:
|
154
|
+
name == :id || name.to_s.end_with?("_id")
|
155
|
+
end
|
156
|
+
|
157
|
+
# Private method to check if a class is a TinySerializer subclass
|
158
|
+
def _is_serializer?(klass)
|
159
|
+
klass <= TinySerializer
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class TinySerializer
|
4
|
+
# Small convenience improvements to TinySerializer
|
5
|
+
# that are automatically included as serializer instance methods
|
6
|
+
# when used in a Rails app.
|
7
|
+
module RailsExtensions
|
8
|
+
# Alias of +Rails.application.routes.url_helpers+.
|
9
|
+
def url_helpers
|
10
|
+
Rails.application.routes.url_helpers
|
11
|
+
end
|
12
|
+
|
13
|
+
# Return +Rails.logger+ if TinySerializer#logger is not set.
|
14
|
+
def logger
|
15
|
+
super || Rails.logger
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,213 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "tiny_serializer/version"
|
4
|
+
require "tiny_serializer/dsl"
|
5
|
+
require "active_support/json"
|
6
|
+
require "active_support/core_ext/class/attribute"
|
7
|
+
|
8
|
+
# Simple DSL for converting objects to Hashes, which is mostly API-compatible
|
9
|
+
# with ActiveModel::Serializer. The Hashes can be rendered as JSON by Rails.
|
10
|
+
#
|
11
|
+
# I have also added some fast_jsonapi[https://github.com/Netflix/fast_jsonapi]
|
12
|
+
# API parameters that do nothing, to ease later transition to that library.
|
13
|
+
#
|
14
|
+
# == Usage
|
15
|
+
#
|
16
|
+
# # my_object.rb
|
17
|
+
# MyObject = Struct.new(:id, :first_name, :last_name, :date) do
|
18
|
+
# def parent; nil; end
|
19
|
+
# def sub_record; nil; end
|
20
|
+
# def related_items; []; end
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# # my_object_serializer.rb
|
24
|
+
# class MyObjectSerializer < TinySerializer
|
25
|
+
# attributes :id,
|
26
|
+
# :first_name,
|
27
|
+
# :last_name,
|
28
|
+
# :date
|
29
|
+
#
|
30
|
+
# belongs_to :parent, serializer: ParentSerializer
|
31
|
+
# has_one :sub_record # serializer will be inferred to be SubRecordSerializer
|
32
|
+
# has_many :related_items, serializer: RelatedItemSerializer
|
33
|
+
#
|
34
|
+
# attribute :full_name do
|
35
|
+
# "#{object.first_name} #{object.last_name}"
|
36
|
+
# end
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# # my_objects_controller.rb
|
40
|
+
# def show
|
41
|
+
# @my_object = MyObject.new(1, "Fred", "Flintstone", Date.new(2000, 1, 1))
|
42
|
+
# render json: MyObjectSerializer.new(@my_object).serialize
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# == RailsExtensions
|
46
|
+
#
|
47
|
+
# The RailsExtensions module is automatically prepended when TinySerializer
|
48
|
+
# is used in a Rails app. It defines some small convenience instance methods.
|
49
|
+
#
|
50
|
+
class TinySerializer
|
51
|
+
extend DSL
|
52
|
+
|
53
|
+
if defined?(Rails)
|
54
|
+
require "tiny_serializer/rails_extensions"
|
55
|
+
prepend RailsExtensions
|
56
|
+
end
|
57
|
+
|
58
|
+
# Whether to automatically convert "\*_id" properties to String.
|
59
|
+
# *default:* _false_
|
60
|
+
class_attribute :coerce_ids_to_string, default: false
|
61
|
+
|
62
|
+
# The object to serialize as a Hash
|
63
|
+
attr_accessor :object
|
64
|
+
|
65
|
+
# Hash of data to be passed to blocks
|
66
|
+
attr_accessor :options
|
67
|
+
|
68
|
+
# Optional logger object. Defaults to Rails.logger in a Rails app.
|
69
|
+
attr_accessor :logger
|
70
|
+
|
71
|
+
# Create a new serializer instance.
|
72
|
+
#
|
73
|
+
# object::
|
74
|
+
# The object to serialize. Can be a single object or a collection of
|
75
|
+
# objects.
|
76
|
+
def initialize(object, options = {})
|
77
|
+
@object = object
|
78
|
+
@options = options
|
79
|
+
end
|
80
|
+
|
81
|
+
# Serialize #object as a Hash with Symbol keys.
|
82
|
+
# Returns nil if #object is nil.
|
83
|
+
def serializable_hash(_ = nil)
|
84
|
+
return serialize_single_object_to_hash unless collection?
|
85
|
+
|
86
|
+
json = []
|
87
|
+
@object.each do |object|
|
88
|
+
json << self.class.new(object, @options).serializable_hash
|
89
|
+
end
|
90
|
+
json
|
91
|
+
end
|
92
|
+
|
93
|
+
alias to_hash serializable_hash
|
94
|
+
alias serialize serializable_hash
|
95
|
+
|
96
|
+
# Serialize #object as a Hash, then call #as_json on the Hash,
|
97
|
+
# which will convert keys to Strings.
|
98
|
+
#
|
99
|
+
# <b>There shouldn't be a need to call this, but we implement it to fully
|
100
|
+
# support ActiveSupport's magic JSON protocols.</b>
|
101
|
+
def as_json(_ = nil)
|
102
|
+
serializable_hash.as_json
|
103
|
+
end
|
104
|
+
|
105
|
+
# Serialize #object to a JSON String.
|
106
|
+
#
|
107
|
+
# Calls #serializable_hash, then call #to_json on the resulting Hash,
|
108
|
+
# converting it to a String using the automatic facilities for doing so
|
109
|
+
# from ActiveSupport.
|
110
|
+
def to_json(_ = nil)
|
111
|
+
serializable_hash.to_json
|
112
|
+
end
|
113
|
+
|
114
|
+
# Return true if #object is a collection.
|
115
|
+
def collection?
|
116
|
+
self.class.is_collection?(@object)
|
117
|
+
end
|
118
|
+
|
119
|
+
# = Class Methods
|
120
|
+
class << self
|
121
|
+
# The same as:
|
122
|
+
#
|
123
|
+
# TinySerializer.new(object).serializable_hash
|
124
|
+
def serialize(object)
|
125
|
+
new(object).serializable_hash
|
126
|
+
end
|
127
|
+
|
128
|
+
# Same as #serialize, but raises ArgumentError if `collection`
|
129
|
+
# doesn't appear to be a collection.
|
130
|
+
def serialize_each(collection)
|
131
|
+
raise ArgumentError, "collection does not appear to be a collection" unless is_collection?(collection)
|
132
|
+
new(collection).serializable_hash
|
133
|
+
end
|
134
|
+
|
135
|
+
# Check if #object is a collection.
|
136
|
+
def is_collection?(object)
|
137
|
+
object.respond_to?(:each) && !object.respond_to?(:each_pair)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
private
|
142
|
+
|
143
|
+
# Private serialization implementation for a single object.
|
144
|
+
# @object must be a single object.
|
145
|
+
def serialize_single_object_to_hash
|
146
|
+
return nil if @object.nil?
|
147
|
+
|
148
|
+
hash = {}
|
149
|
+
serialize_attributes(hash)
|
150
|
+
serialize_sub_records(hash)
|
151
|
+
serialize_collections(hash)
|
152
|
+
hash
|
153
|
+
end
|
154
|
+
|
155
|
+
def serialize_attributes(hash)
|
156
|
+
self.class.attributes.each do |name, key, is_id, block|
|
157
|
+
raw_value = get_attribute(name, block)
|
158
|
+
hash[key] = serialize_attribute(raw_value, is_id)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def get_attribute(name, block)
|
163
|
+
if block
|
164
|
+
instance_exec(@object, @options, &block)
|
165
|
+
else
|
166
|
+
@object.public_send(name)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
# Internal algorithm to convert any object to a valid JSON string, scalar,
|
171
|
+
# object, array, etc. All objects are passed through this function after they
|
172
|
+
# are retrieved from #object. Currently just calls #as_json.
|
173
|
+
def serialize_attribute(raw_value, is_id = false)
|
174
|
+
if is_id && coerce_ids_to_string?
|
175
|
+
serialize_id(raw_value)
|
176
|
+
else
|
177
|
+
raw_value.as_json
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def serialize_id(id)
|
182
|
+
id && id.to_s
|
183
|
+
end
|
184
|
+
|
185
|
+
def serialize_sub_records(hash)
|
186
|
+
self.class.sub_records.each do |name, key, serializer, block|
|
187
|
+
record = get_attribute(name, block)
|
188
|
+
hash[key] = serializer.new(record, @options).serializable_hash
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def serialize_collections(hash)
|
193
|
+
self.class.collections.each do |collection_name, key, serializer, block|
|
194
|
+
collection = get_collection(collection_name, block)
|
195
|
+
|
196
|
+
json_array = []
|
197
|
+
collection.each do |object|
|
198
|
+
json_array << serializer.new(object, @options).serializable_hash
|
199
|
+
end
|
200
|
+
|
201
|
+
hash[key] = json_array
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
# Get the collection from #object or block, or [] if nil.
|
206
|
+
def get_collection(name, block)
|
207
|
+
if block
|
208
|
+
instance_exec(@object, @options, &block)
|
209
|
+
else
|
210
|
+
@object.public_send(name)
|
211
|
+
end || []
|
212
|
+
end
|
213
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "tiny_serializer/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "tiny_serializer"
|
8
|
+
spec.version = TinySerializer::VERSION
|
9
|
+
spec.authors = ["William Makley"]
|
10
|
+
spec.email = ["william@pioneerstreet.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Tiny Ruby JSON Serialization DSL}
|
13
|
+
# spec.description = %q{TODO: Write a longer description or delete this line.}
|
14
|
+
spec.homepage = "https://github.com/wmakley/simple_serializer.git"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
# Specify which files should be added to the gem when it is released.
|
18
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
19
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
20
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
21
|
+
end
|
22
|
+
spec.bindir = "exe"
|
23
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
24
|
+
spec.require_paths = ["lib"]
|
25
|
+
|
26
|
+
spec.add_dependency "activesupport", ">= 4.2"
|
27
|
+
|
28
|
+
spec.add_development_dependency "bundler", "~> 1.0"
|
29
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
30
|
+
spec.add_development_dependency "rspec", "~> 3.5"
|
31
|
+
end
|
metadata
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tiny_serializer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 2.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- William Makley
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-11-16 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.2'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4.2'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.5'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.5'
|
69
|
+
description:
|
70
|
+
email:
|
71
|
+
- william@pioneerstreet.com
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- ".gitignore"
|
77
|
+
- ".idea/vcs.xml"
|
78
|
+
- ".rspec"
|
79
|
+
- ".rubocop.yml"
|
80
|
+
- ".ruby-version"
|
81
|
+
- ".travis.yml"
|
82
|
+
- CHANGELOG.md
|
83
|
+
- Gemfile
|
84
|
+
- Gemfile.lock
|
85
|
+
- LICENSE.txt
|
86
|
+
- README.md
|
87
|
+
- Rakefile
|
88
|
+
- bin/console
|
89
|
+
- bin/setup
|
90
|
+
- lib/tiny_serializer.rb
|
91
|
+
- lib/tiny_serializer/dsl.rb
|
92
|
+
- lib/tiny_serializer/rails_extensions.rb
|
93
|
+
- lib/tiny_serializer/version.rb
|
94
|
+
- tiny_serializer.gemspec
|
95
|
+
homepage: https://github.com/wmakley/simple_serializer.git
|
96
|
+
licenses:
|
97
|
+
- MIT
|
98
|
+
metadata: {}
|
99
|
+
post_install_message:
|
100
|
+
rdoc_options: []
|
101
|
+
require_paths:
|
102
|
+
- lib
|
103
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
104
|
+
requirements:
|
105
|
+
- - ">="
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
version: '0'
|
108
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
109
|
+
requirements:
|
110
|
+
- - ">="
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: '0'
|
113
|
+
requirements: []
|
114
|
+
rubyforge_project:
|
115
|
+
rubygems_version: 2.6.14.3
|
116
|
+
signing_key:
|
117
|
+
specification_version: 4
|
118
|
+
summary: Tiny Ruby JSON Serialization DSL
|
119
|
+
test_files: []
|