ruby-serializer 0.0.1 → 1.0.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 +4 -4
- data/Gemfile.lock +1 -1
- data/lib/ruby_serializer/association.rb +24 -0
- data/lib/ruby_serializer/base.rb +31 -3
- data/lib/ruby_serializer/dsl.rb +19 -2
- data/lib/ruby_serializer/field.rb +2 -2
- data/lib/ruby_serializer.rb +5 -4
- data/readme.md +138 -24
- data/test/association_test.rb +288 -0
- data/test/basic_test.rb +18 -13
- data/test/expose_test.rb +22 -17
- data/test/model.rb +66 -0
- data/test/test_case.rb +9 -0
- metadata +4 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 64873bd0b35de8defd662d6bd92bb9b52100937e
|
4
|
+
data.tar.gz: a735bbb0a3e92c063bbef5be1c1b58dac515cfd2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: da5bf86ac16016adec3d8951000c0846f488408e67826afc85395bf273456854319e35289bb083377404c4863c276fa9e650ba716b34cae6e5090206ec7d6fce
|
7
|
+
data.tar.gz: d26a542f36175f9638a29091fe03250ae7272cacc1cff5e5a52f8ba7bf91a052ccf875921fd47f0971443f57f290ad07475e8a482563e86fb31a8c8df29538f7
|
data/Gemfile.lock
CHANGED
@@ -0,0 +1,24 @@
|
|
1
|
+
module RubySerializer
|
2
|
+
class Association < Field
|
3
|
+
|
4
|
+
#----------------------------------------------------------------------------------------------
|
5
|
+
|
6
|
+
def serialize(resource, serializer)
|
7
|
+
includes = serializer.send(:includes)[field]
|
8
|
+
association = resource.send(field)
|
9
|
+
if association
|
10
|
+
RubySerializer.as_json(association, include: includes)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
#----------------------------------------------------------------------------------------------
|
15
|
+
|
16
|
+
def present?(resource, serializer)
|
17
|
+
super &&
|
18
|
+
serializer.send(:include?, field)
|
19
|
+
end
|
20
|
+
|
21
|
+
#----------------------------------------------------------------------------------------------
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
data/lib/ruby_serializer/base.rb
CHANGED
@@ -3,8 +3,7 @@ module RubySerializer
|
|
3
3
|
|
4
4
|
extend RubySerializer::Dsl
|
5
5
|
|
6
|
-
attr_reader :options
|
7
|
-
# them available to any potential :value, :only and :unless lambdas in the derived serializer
|
6
|
+
attr_reader :options
|
8
7
|
|
9
8
|
def initialize(resource)
|
10
9
|
@resource = resource
|
@@ -23,6 +22,11 @@ module RubySerializer
|
|
23
22
|
private
|
24
23
|
|
25
24
|
attr_reader :resource
|
25
|
+
attr_reader :includes
|
26
|
+
|
27
|
+
def include?(association)
|
28
|
+
includes.key?(association.to_sym)
|
29
|
+
end
|
26
30
|
|
27
31
|
def serialize(options = {})
|
28
32
|
if resource.respond_to? :to_ary
|
@@ -40,7 +44,8 @@ module RubySerializer
|
|
40
44
|
end
|
41
45
|
|
42
46
|
def serialize_object(options)
|
43
|
-
@options
|
47
|
+
@options = options
|
48
|
+
@includes = expand_includes(options[:include])
|
44
49
|
json = {}
|
45
50
|
json[:errors] = resource.errors if resource.respond_to?(:valid?) && !resource.valid?
|
46
51
|
self.class.fields.each do |field|
|
@@ -58,5 +63,28 @@ module RubySerializer
|
|
58
63
|
|
59
64
|
#----------------------------------------------------------------------------------------------
|
60
65
|
|
66
|
+
def expand_includes(includes, options = {})
|
67
|
+
|
68
|
+
return includes if includes.is_a?(Hash) # already expanded
|
69
|
+
|
70
|
+
includes = case includes
|
71
|
+
when nil then []
|
72
|
+
when Array then includes
|
73
|
+
when Symbol then [ includes ]
|
74
|
+
when String then includes.split(',')
|
75
|
+
else
|
76
|
+
raise ArgumentError, "unexpected includes #{includes.class}"
|
77
|
+
end
|
78
|
+
|
79
|
+
includes.each_with_object({}) do |i, hash|
|
80
|
+
i.to_s.downcase.strip.split('.').reduce(hash) do |h, part|
|
81
|
+
h[part.to_sym] ||= {}
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
#----------------------------------------------------------------------------------------------
|
88
|
+
|
61
89
|
end
|
62
90
|
end
|
data/lib/ruby_serializer/dsl.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
module RubySerializer
|
2
2
|
module Dsl
|
3
3
|
|
4
|
+
#----------------------------------------------------------------------------------------------
|
5
|
+
|
4
6
|
def serializes(name)
|
5
7
|
define_method(name) do # provide a friendly-name accessor to the underlying resource
|
6
8
|
resource
|
@@ -11,8 +13,17 @@ module RubySerializer
|
|
11
13
|
fields << Field.new(field, namespace, options)
|
12
14
|
end
|
13
15
|
|
14
|
-
def
|
15
|
-
|
16
|
+
def belongs_to(field, options = {})
|
17
|
+
fields << Field.new("#{field}_id", namespace, options)
|
18
|
+
fields << Association.new(field, namespace, options)
|
19
|
+
end
|
20
|
+
|
21
|
+
def has_many(field, options = {})
|
22
|
+
fields << Association.new(field, namespace, options)
|
23
|
+
end
|
24
|
+
|
25
|
+
def has_one(field, options = {})
|
26
|
+
fields << Association.new(field, namespace, options)
|
16
27
|
end
|
17
28
|
|
18
29
|
def namespace(ns = nil, &block)
|
@@ -23,5 +34,11 @@ module RubySerializer
|
|
23
34
|
@namespace.pop
|
24
35
|
end
|
25
36
|
|
37
|
+
def fields
|
38
|
+
@fields ||= []
|
39
|
+
end
|
40
|
+
|
41
|
+
#----------------------------------------------------------------------------------------------
|
42
|
+
|
26
43
|
end
|
27
44
|
end
|
@@ -5,8 +5,8 @@ module RubySerializer
|
|
5
5
|
|
6
6
|
def initialize(field, namespace, options)
|
7
7
|
@field = field.to_sym
|
8
|
-
@as = options[:as] || field
|
9
|
-
@from = options[:from] || field
|
8
|
+
@as = options[:as] || @field
|
9
|
+
@from = options[:from] || @field
|
10
10
|
@value = options[:value]
|
11
11
|
@only = options[:only]
|
12
12
|
@unless = options[:unless]
|
data/lib/ruby_serializer.rb
CHANGED
@@ -2,16 +2,17 @@ module RubySerializer
|
|
2
2
|
|
3
3
|
#------------------------------------------------------------------------------------------------
|
4
4
|
|
5
|
-
VERSION = '0.0
|
5
|
+
VERSION = '1.0.0'
|
6
6
|
SUMMARY = 'Serialize POROs to JSON'
|
7
7
|
DESCRIPTION = 'A general purpose library for serializing plain old ruby objects (POROs) into JSON using a declarative DSL'
|
8
8
|
LIB = File.dirname(__FILE__)
|
9
9
|
|
10
10
|
#------------------------------------------------------------------------------------------------
|
11
11
|
|
12
|
-
autoload :Dsl,
|
13
|
-
autoload :Base,
|
14
|
-
autoload :Field,
|
12
|
+
autoload :Dsl, File.join(LIB, 'ruby_serializer/dsl')
|
13
|
+
autoload :Base, File.join(LIB, 'ruby_serializer/base')
|
14
|
+
autoload :Field, File.join(LIB, 'ruby_serializer/field')
|
15
|
+
autoload :Association, File.join(LIB ,'ruby_serializer/association')
|
15
16
|
|
16
17
|
#------------------------------------------------------------------------------------------------
|
17
18
|
|
data/readme.md
CHANGED
@@ -6,7 +6,7 @@ into JSON using a declarative DSL.
|
|
6
6
|
## Installation
|
7
7
|
|
8
8
|
```bash
|
9
|
-
|
9
|
+
$ gem install ruby-serializer # ... or add to your Gemfile as appropriate
|
10
10
|
```
|
11
11
|
|
12
12
|
## Usage
|
@@ -38,6 +38,21 @@ And serialize using...
|
|
38
38
|
# { size: 'large', color: 'red', shape: 'square' }
|
39
39
|
```
|
40
40
|
|
41
|
+
### Serializer naming convention
|
42
|
+
|
43
|
+
By default, `ruby-serializer` will look for a serializer with the following naming convention
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
"#{resource.class.name}Serializer"
|
47
|
+
```
|
48
|
+
|
49
|
+
But you can always specify the serializer class explicitly:
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
shape = Shape.new(...)
|
53
|
+
json = RubySerializer.as_json shape, with: MyCustomSerializer
|
54
|
+
```
|
55
|
+
|
41
56
|
## Beyond the basics
|
42
57
|
|
43
58
|
Ok, that was very basic, but with RubySerializer you can also:
|
@@ -50,54 +65,153 @@ Ok, that was very basic, but with RubySerializer you can also:
|
|
50
65
|
* `:include` nested `:belongs_to` associated models
|
51
66
|
* `:include` nested `:has_one` associated models
|
52
67
|
* `:include` nested `:has_many` associated models
|
53
|
-
* serialize an array of models
|
54
|
-
* add `ActionController` integration for easy API serialization
|
55
68
|
|
56
69
|
### Exposing attributes with another name
|
57
70
|
|
58
|
-
|
71
|
+
You can expose an attribute with a different name using `:as`
|
72
|
+
|
73
|
+
```ruby
|
74
|
+
class UserSerializer < RubySerializer::Base
|
75
|
+
expose :id
|
76
|
+
expose :name, as: :user_name
|
77
|
+
expose :email, as: :user_email
|
78
|
+
end
|
79
|
+
|
80
|
+
user = User.new(id: 42, name: 'Jake', email: 'jake@codeincomplete.com')
|
81
|
+
json = RubySerializer.as_json user
|
82
|
+
|
83
|
+
# { id: 42, user_name: 'Jake', user_email: 'jake@codeincomplete.com' }
|
84
|
+
```
|
59
85
|
|
60
86
|
### Exposing attributes with custom values
|
61
87
|
|
62
|
-
|
88
|
+
You can expose an attribute with a different value using `:value`
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
class UserSerializer < RubySerializer::Base
|
92
|
+
expose :id
|
93
|
+
expose :static, value: 'static' # expose a static attribute
|
94
|
+
expose :dynamic, value: -> { resource.name.reverse } # expose a dynamic attribute using a lambda
|
95
|
+
expose :method, value: :name # expose a dynamic attribute using a symbol (calls a method on the underlying resource automatically)
|
96
|
+
end
|
97
|
+
|
98
|
+
user = User.new(id: 42, name: 'Jake')
|
99
|
+
json = RubySerializer.as_json user
|
100
|
+
|
101
|
+
# { id: 42, static: 'static', dynamic: 'ekaJ', method: 'Jake' }
|
102
|
+
```
|
63
103
|
|
64
104
|
### Exposing attributes conditionally
|
65
105
|
|
66
|
-
|
106
|
+
You can expose attributes conditionally using `:only` and `:unless`
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
class UserSerializer < RubySerializer::Base
|
110
|
+
expose :id
|
111
|
+
expose :admin, only: :admin? # expose this field only when resource.admin?
|
112
|
+
expose :name, only: -> { resource.name.present? } # expose this field only when resource.name.present?
|
113
|
+
expose :email, unless: -> { resource.email.blank? } # expose this field unless resource.email.blank?
|
114
|
+
end
|
115
|
+
|
116
|
+
user = User.new(id: 1)
|
117
|
+
json = RubySerializer.as_json user
|
118
|
+
|
119
|
+
# { id: 1 }
|
120
|
+
|
121
|
+
user = User.new(id: 2, name: 'Joe')
|
122
|
+
json = RubySerializer.as_json user
|
123
|
+
|
124
|
+
# { id: 2, name: 'Joe' }
|
125
|
+
|
126
|
+
user = User.new(id: 3, name: 'Bob', admin: true)
|
127
|
+
json = RubySerializer.as_json user
|
128
|
+
|
129
|
+
# { id: 3, name: 'Bob', admin: true }
|
130
|
+
|
131
|
+
```
|
67
132
|
|
68
133
|
### Exposing attributes within a namespace
|
69
134
|
|
70
|
-
|
135
|
+
You can nest serialized attributes using a `:namespace`:
|
71
136
|
|
72
|
-
|
137
|
+
```ruby
|
138
|
+
class UserSerializer < RubySerializer::Base
|
139
|
+
expose :id
|
140
|
+
namespace :i18n do
|
141
|
+
expose :locale
|
142
|
+
expose :time_zone
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
user = User.new(id: 42, locale: 'en-us', time_zone: 'UTC')
|
147
|
+
json = RubySerializer.as_json user
|
73
148
|
|
74
|
-
|
149
|
+
# { id: 42, i18n: { locale: 'en-us', time_zone: 'UTC' } }
|
150
|
+
```
|
75
151
|
|
76
152
|
### Exposing associations
|
77
153
|
|
78
|
-
|
154
|
+
If your models have ActiveRecord like associations you can declare them in your serializers and
|
155
|
+
then use the optional `:include` mechanism to choose when to include them as nested resources
|
156
|
+
during serialization:
|
157
|
+
|
158
|
+
```ruby
|
159
|
+
class Publisher
|
160
|
+
attr :id, :name
|
161
|
+
has_many :books
|
162
|
+
end
|
163
|
+
|
164
|
+
class Book
|
165
|
+
attr :isbn, :name
|
166
|
+
belongs_to :publisher
|
167
|
+
end
|
168
|
+
|
169
|
+
class PublisherSerializer
|
170
|
+
expose :id
|
171
|
+
expose :name
|
172
|
+
has_many :books
|
173
|
+
end
|
174
|
+
|
175
|
+
class BookSerializer
|
176
|
+
expose :isbn
|
177
|
+
expose :name
|
178
|
+
belongs_to :publisher
|
179
|
+
end
|
180
|
+
|
181
|
+
publisher = Publisher.new(id: 42, name: 'Addison Wesley')
|
182
|
+
|
183
|
+
RubySerializer.as_json publisher # WITHOUT :include
|
184
|
+
|
185
|
+
# { id: 42, name: 'Addison Wesley' }
|
186
|
+
|
187
|
+
RubySerializer.as_json publisher, include: :books # WITH :include
|
79
188
|
|
80
|
-
|
189
|
+
# { id: 42, name: 'Addison Wesley', books: [
|
190
|
+
# { isbn: '020161622X', name: 'Pragmatic Programmer' },
|
191
|
+
# { isbn: '0201633612', name: 'Design Patterns '},
|
192
|
+
# ...
|
193
|
+
# ] }
|
194
|
+
```
|
81
195
|
|
82
|
-
|
196
|
+
`ruby-serializer` supports `belongs_to`, `has_one`, and `has_many` associations.
|
83
197
|
|
84
|
-
|
198
|
+
You can include multiple associations using an array of includes:
|
85
199
|
|
86
|
-
|
200
|
+
```ruby
|
201
|
+
RubySerializer.as_json publisher, include: [ :books, :address, :websites ]
|
202
|
+
```
|
87
203
|
|
88
|
-
|
204
|
+
You can also include nested associations:
|
89
205
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
* documentation
|
206
|
+
```ruby
|
207
|
+
RubySerializer.as_json publisher, include: { books: [ :authors ] }
|
208
|
+
```
|
209
|
+
|
210
|
+
### Exposing model validation errors
|
96
211
|
|
97
|
-
|
212
|
+
By default, if your underlying model responds to `:valid?` and returns `false` then the
|
213
|
+
serialized response will automatically include a serialized `:errors` attribute.
|
98
214
|
|
99
|
-
* Extensibility with custom Field types
|
100
|
-
|
101
215
|
# License
|
102
216
|
|
103
217
|
See [LICENSE](https://github.com/jakesgordon/ruby-serializer/blob/master/LICENSE) file.
|
@@ -0,0 +1,288 @@
|
|
1
|
+
require_relative 'test_case'
|
2
|
+
|
3
|
+
module RubySerializer
|
4
|
+
class AssociationTest < TestCase
|
5
|
+
|
6
|
+
#----------------------------------------------------------------------------------------------
|
7
|
+
|
8
|
+
PORTLAND = { id: :portland, city: 'Portland', state: 'OR' }
|
9
|
+
SEATTLE = { id: :seattle, city: 'Seattle', state: 'WA' }
|
10
|
+
BOSTON = { id: :boston, city: 'Boston', state: 'MA' }
|
11
|
+
NYC = { id: :nyc, city: 'New York', state: 'NY' }
|
12
|
+
SF = { id: :sf, city: 'San Francisco', state: 'CA' }
|
13
|
+
LA = { id: :la, city: 'Los Angeles', state: 'LA' }
|
14
|
+
CHICAGO = { id: :chicago, city: 'Chicago', state: 'IL' }
|
15
|
+
DALLAS = { id: :dallas, city: 'Dallas', state: 'TX' }
|
16
|
+
REDMOND = { id: :redmond, city: 'Redmond', state: 'WA' }
|
17
|
+
|
18
|
+
MCCONNEL = { id: :mcconnel, name: 'Steve McConnel', address: SEATTLE }
|
19
|
+
HUNT = { id: :hunt, name: 'Andrew Hunt', address: PORTLAND }
|
20
|
+
THOMAS = { id: :thomas, name: 'David Thomas', address: BOSTON }
|
21
|
+
GAMMA = { id: :gamma, name: 'Erich Gamma', address: NYC }
|
22
|
+
HELM = { id: :helm, name: 'Richard Helm', address: SF }
|
23
|
+
JOHNSON = { id: :johnson, name: 'Ralph Johnson', address: CHICAGO }
|
24
|
+
VLISSIDES = { id: :vlissides, name: 'John Vlissides', address: DALLAS }
|
25
|
+
|
26
|
+
PP = { id: :pp, isbn: '020161622X', name: 'Pragmatic Programmer', publisher_id: :aw, authors: [ HUNT, THOMAS ] }
|
27
|
+
DP = { id: :dp, isbn: '0201633612', name: 'Design Patterns', publisher_id: :aw, authors: [ GAMMA, HELM, JOHNSON, VLISSIDES ] }
|
28
|
+
CC = { id: :cc, isbn: '0735619670', name: 'Code Complete', publisher_id: :ms, authors: [ MCCONNEL ] }
|
29
|
+
|
30
|
+
AW = { id: :aw, name: 'Addison-Wesley', address: LA, books: [ PP, DP ] }
|
31
|
+
MS = { id: :ms, name: 'Microsoft Press', address: REDMOND, books: [ CC ] }
|
32
|
+
|
33
|
+
PROGRAMMING = { id: :programming, name: 'Programming', books: [ PP, DP, CC ] }
|
34
|
+
|
35
|
+
#----------------------------------------------------------------------------------------------
|
36
|
+
|
37
|
+
class Publisher < Model
|
38
|
+
attr :id, :name
|
39
|
+
has_one :address
|
40
|
+
has_many :books
|
41
|
+
end
|
42
|
+
|
43
|
+
class Book < Model
|
44
|
+
attr :isbn, :name
|
45
|
+
belongs_to :publisher
|
46
|
+
has_many :authors
|
47
|
+
end
|
48
|
+
|
49
|
+
class Author < Model
|
50
|
+
attr :id, :name
|
51
|
+
has_one :address
|
52
|
+
end
|
53
|
+
|
54
|
+
class Address < Model
|
55
|
+
attr :id, :city, :state
|
56
|
+
end
|
57
|
+
|
58
|
+
class Category < Model
|
59
|
+
attr :id, :name
|
60
|
+
has_many :books
|
61
|
+
end
|
62
|
+
|
63
|
+
#----------------------------------------------------------------------------------------------
|
64
|
+
|
65
|
+
class PublisherSerializer < RubySerializer::Base
|
66
|
+
expose :id
|
67
|
+
expose :name
|
68
|
+
has_one :address
|
69
|
+
has_many :books
|
70
|
+
end
|
71
|
+
|
72
|
+
class BookSerializer < RubySerializer::Base
|
73
|
+
expose :isbn
|
74
|
+
expose :name
|
75
|
+
belongs_to :publisher
|
76
|
+
has_many :authors
|
77
|
+
end
|
78
|
+
|
79
|
+
class AuthorSerializer < RubySerializer::Base
|
80
|
+
expose :id
|
81
|
+
expose :name
|
82
|
+
has_one :address
|
83
|
+
end
|
84
|
+
|
85
|
+
class AddressSerializer < RubySerializer::Base
|
86
|
+
expose :id
|
87
|
+
expose :city
|
88
|
+
expose :state
|
89
|
+
end
|
90
|
+
|
91
|
+
class CategorySerializer < RubySerializer::Base
|
92
|
+
expose :id
|
93
|
+
expose :name
|
94
|
+
has_many :books
|
95
|
+
end
|
96
|
+
|
97
|
+
#----------------------------------------------------------------------------------------------
|
98
|
+
|
99
|
+
def test_belongs_to
|
100
|
+
|
101
|
+
book = books(PP)
|
102
|
+
with = serialize book, include: :publisher
|
103
|
+
without = serialize book
|
104
|
+
|
105
|
+
assert_book PP, with, with: :publisher
|
106
|
+
assert_publisher AW, with[:publisher]
|
107
|
+
assert_book PP, without
|
108
|
+
|
109
|
+
end
|
110
|
+
|
111
|
+
#----------------------------------------------------------------------------------------------
|
112
|
+
|
113
|
+
def test_has_one
|
114
|
+
|
115
|
+
author = authors(HUNT)
|
116
|
+
with = serialize author, include: :address
|
117
|
+
without = serialize author
|
118
|
+
|
119
|
+
assert_author HUNT, with, with: :address
|
120
|
+
assert_address PORTLAND, with[:address]
|
121
|
+
assert_author HUNT, without
|
122
|
+
|
123
|
+
end
|
124
|
+
|
125
|
+
#----------------------------------------------------------------------------------------------
|
126
|
+
|
127
|
+
def test_has_many
|
128
|
+
|
129
|
+
publisher = publishers(AW)
|
130
|
+
with = serialize publisher, include: :books
|
131
|
+
without = serialize publisher
|
132
|
+
|
133
|
+
assert_publisher AW, with, with: :books
|
134
|
+
assert_equal 2, with[:books].length
|
135
|
+
assert_book PP, with[:books][0]
|
136
|
+
assert_book DP, with[:books][1]
|
137
|
+
assert_publisher AW, without
|
138
|
+
|
139
|
+
end
|
140
|
+
|
141
|
+
#----------------------------------------------------------------------------------------------
|
142
|
+
|
143
|
+
def test_multiple_includes
|
144
|
+
variations = [
|
145
|
+
[ :books, :address ], # verify using array of symbols
|
146
|
+
[ 'books', 'address' ], # verify using array of strings
|
147
|
+
"books,address", # verify using a comma separated string
|
148
|
+
" books , address " # verify using a comma separated string (with whitespace)
|
149
|
+
]
|
150
|
+
variations.each do |includes|
|
151
|
+
publisher = publishers(AW)
|
152
|
+
json = serialize publisher, include: includes
|
153
|
+
assert_publisher AW, json, with: [ :address, :books ]
|
154
|
+
assert_address LA, json[:address]
|
155
|
+
assert_equal 2, json[:books].length
|
156
|
+
assert_book PP, json[:books][0]
|
157
|
+
assert_book DP, json[:books][1]
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
#----------------------------------------------------------------------------------------------
|
162
|
+
|
163
|
+
def test_nested_includes
|
164
|
+
publisher = publishers(AW)
|
165
|
+
json = serialize publisher, include: "books.authors.address"
|
166
|
+
assert_publisher AW, json, with: :books
|
167
|
+
assert_equal 2, json[:books].length
|
168
|
+
assert_book PP, json[:books][0], with: :authors
|
169
|
+
assert_equal 2, json[:books][0][:authors].length
|
170
|
+
assert_author HUNT, json[:books][0][:authors][0], with: :address
|
171
|
+
assert_address PORTLAND, json[:books][0][:authors][0][:address]
|
172
|
+
assert_author THOMAS, json[:books][0][:authors][1], with: :address
|
173
|
+
assert_address BOSTON, json[:books][0][:authors][1][:address]
|
174
|
+
assert_book DP, json[:books][1], with: :authors
|
175
|
+
assert_equal 4, json[:books][1][:authors].length
|
176
|
+
assert_author GAMMA, json[:books][1][:authors][0], with: :address
|
177
|
+
assert_address NYC, json[:books][1][:authors][0][:address]
|
178
|
+
assert_author HELM, json[:books][1][:authors][1], with: :address
|
179
|
+
assert_address SF, json[:books][1][:authors][1][:address]
|
180
|
+
assert_author JOHNSON, json[:books][1][:authors][2], with: :address
|
181
|
+
assert_address CHICAGO, json[:books][1][:authors][2][:address]
|
182
|
+
assert_author VLISSIDES, json[:books][1][:authors][3], with: :address
|
183
|
+
assert_address DALLAS, json[:books][1][:authors][3][:address]
|
184
|
+
end
|
185
|
+
|
186
|
+
#----------------------------------------------------------------------------------------------
|
187
|
+
|
188
|
+
def test_multiple_nested_includes
|
189
|
+
category = categories(PROGRAMMING)
|
190
|
+
json = serialize category, include: "books.publisher, books.authors"
|
191
|
+
assert_category PROGRAMMING, json, with: :books
|
192
|
+
assert_equal 3, json[:books].length
|
193
|
+
assert_book PP, json[:books][0], with: [ :publisher, :authors ]
|
194
|
+
assert_publisher AW, json[:books][0][:publisher]
|
195
|
+
assert_equal 2, json[:books][0][:authors].length
|
196
|
+
assert_author HUNT, json[:books][0][:authors][0]
|
197
|
+
assert_author THOMAS, json[:books][0][:authors][1]
|
198
|
+
assert_book DP, json[:books][1], with: [ :publisher, :authors ]
|
199
|
+
assert_publisher AW, json[:books][1][:publisher]
|
200
|
+
assert_equal 4, json[:books][1][:authors].length
|
201
|
+
assert_author GAMMA, json[:books][1][:authors][0]
|
202
|
+
assert_author HELM, json[:books][1][:authors][1]
|
203
|
+
assert_author JOHNSON, json[:books][1][:authors][2]
|
204
|
+
assert_author VLISSIDES, json[:books][1][:authors][3]
|
205
|
+
assert_book CC, json[:books][2], with: [ :publisher, :authors ]
|
206
|
+
assert_publisher MS, json[:books][2][:publisher]
|
207
|
+
assert_equal 1, json[:books][2][:authors].length
|
208
|
+
assert_author MCCONNEL, json[:books][2][:authors][0]
|
209
|
+
end
|
210
|
+
|
211
|
+
#----------------------------------------------------------------------------------------------
|
212
|
+
|
213
|
+
private
|
214
|
+
|
215
|
+
def addresses(fixture)
|
216
|
+
Address.new(fixture)
|
217
|
+
end
|
218
|
+
|
219
|
+
def authors(fixture)
|
220
|
+
author = Author.new(fixture)
|
221
|
+
author.address = addresses(fixture[:address]) if fixture.key?(:address)
|
222
|
+
author
|
223
|
+
end
|
224
|
+
|
225
|
+
def books(fixture)
|
226
|
+
book = Book.new(fixture)
|
227
|
+
book.publisher = publishers(fixture[:publisher_id], false)
|
228
|
+
book.authors = Array(fixture[:authors]).map { |a| authors(a) } if fixture.key?(:authors)
|
229
|
+
book
|
230
|
+
end
|
231
|
+
|
232
|
+
def publishers(fixture, include_books = true)
|
233
|
+
fixture = AW if fixture == :aw
|
234
|
+
fixture = MS if fixture == :ms
|
235
|
+
publisher = Publisher.new(fixture)
|
236
|
+
publisher.address = addresses(fixture[:address]) if fixture.key?(:address)
|
237
|
+
publisher.books = Array(fixture[:books]).map { |b| books(b) } if include_books && fixture[:books]
|
238
|
+
publisher
|
239
|
+
end
|
240
|
+
|
241
|
+
def categories(fixture)
|
242
|
+
category = Category.new(fixture)
|
243
|
+
category.books = Array(fixture[:books]).map { |b| books(b) } if fixture.key?(:books)
|
244
|
+
category
|
245
|
+
end
|
246
|
+
|
247
|
+
#----------------------------------------------------------------------------------------------
|
248
|
+
|
249
|
+
def assert_publisher(expected, actual, options = {})
|
250
|
+
expected_keys = [ :id, :name ] + Array(options[:with])
|
251
|
+
assert_set expected_keys, actual.keys
|
252
|
+
assert_equal expected[:id], actual[:id]
|
253
|
+
assert_equal expected[:name], actual[:name]
|
254
|
+
end
|
255
|
+
|
256
|
+
def assert_book(expected, actual, options = {})
|
257
|
+
expected_keys = [ :isbn, :name, :publisher_id ] + Array(options[:with])
|
258
|
+
assert_set expected_keys, actual.keys
|
259
|
+
assert_equal expected[:isbn], actual[:isbn]
|
260
|
+
assert_equal expected[:name], actual[:name]
|
261
|
+
assert_equal expected[:publisher_id], actual[:publisher_id]
|
262
|
+
end
|
263
|
+
|
264
|
+
def assert_author(expected, actual, options = {})
|
265
|
+
expected_keys = [ :id, :name ] + Array(options[:with])
|
266
|
+
assert_set expected_keys, actual.keys
|
267
|
+
assert_equal expected[:id], actual[:id]
|
268
|
+
assert_equal expected[:name], actual[:name]
|
269
|
+
end
|
270
|
+
|
271
|
+
def assert_address(expected, actual)
|
272
|
+
assert_set [ :id, :city, :state ], actual.keys
|
273
|
+
assert_equal expected[:id], actual[:id]
|
274
|
+
assert_equal expected[:city], actual[:city]
|
275
|
+
assert_equal expected[:state], actual[:state]
|
276
|
+
end
|
277
|
+
|
278
|
+
def assert_category(expected, actual, options = {})
|
279
|
+
expected_keys = [ :id, :name ] + Array(options[:with])
|
280
|
+
assert_equal expected_keys, actual.keys
|
281
|
+
assert_equal expected[:id], actual[:id]
|
282
|
+
assert_equal expected[:name], actual[:name]
|
283
|
+
end
|
284
|
+
|
285
|
+
#----------------------------------------------------------------------------------------------
|
286
|
+
|
287
|
+
end
|
288
|
+
end
|
data/test/basic_test.rb
CHANGED
@@ -6,15 +6,20 @@ module RubySerializer
|
|
6
6
|
#----------------------------------------------------------------------------------------------
|
7
7
|
|
8
8
|
def test_gem_information
|
9
|
-
assert_equal '0.0
|
9
|
+
assert_equal '1.0.0', VERSION
|
10
10
|
assert_equal 'Serialize POROs to JSON', SUMMARY
|
11
11
|
assert_match 'A general purpose library for', DESCRIPTION
|
12
12
|
end
|
13
13
|
|
14
14
|
#----------------------------------------------------------------------------------------------
|
15
15
|
|
16
|
-
Shape
|
17
|
-
|
16
|
+
class Shape < Model
|
17
|
+
attr :color, :shape
|
18
|
+
end
|
19
|
+
|
20
|
+
class Car < Model
|
21
|
+
attr :make, :model
|
22
|
+
end
|
18
23
|
|
19
24
|
class ShapeSerializer < RubySerializer::Base
|
20
25
|
expose :color
|
@@ -30,12 +35,12 @@ module RubySerializer
|
|
30
35
|
|
31
36
|
def test_basic_serialization
|
32
37
|
|
33
|
-
json =
|
38
|
+
json = serialize Shape.new(color: :red, shape: :square)
|
34
39
|
assert_set [ :color, :shape ], json.keys
|
35
40
|
assert_equal :red, json[:color]
|
36
41
|
assert_equal :square, json[:shape]
|
37
42
|
|
38
|
-
json =
|
43
|
+
json = serialize Car.new(make: :honda, model: :civic)
|
39
44
|
assert_set [ :make, :model ], json.keys
|
40
45
|
assert_equal :honda, json[:make]
|
41
46
|
assert_equal :civic, json[:model]
|
@@ -46,12 +51,12 @@ module RubySerializer
|
|
46
51
|
|
47
52
|
def test_array_serialization
|
48
53
|
resources = [
|
49
|
-
Shape.new(:red, :square),
|
50
|
-
Shape.new(:blue, :circle),
|
51
|
-
Car.new(:honda, :civic),
|
52
|
-
Car.new(:jeep, :wrangler)
|
54
|
+
Shape.new(color: :red, shape: :square),
|
55
|
+
Shape.new(color: :blue, shape: :circle),
|
56
|
+
Car.new(make: :honda, model: :civic),
|
57
|
+
Car.new(make: :jeep, model: :wrangler)
|
53
58
|
]
|
54
|
-
json =
|
59
|
+
json = serialize resources
|
55
60
|
assert_equal resources.length, json.length
|
56
61
|
assert_set [ :color, :shape ], json[0].keys
|
57
62
|
assert_equal :red, json[0][:color]
|
@@ -70,7 +75,7 @@ module RubySerializer
|
|
70
75
|
#----------------------------------------------------------------------------------------------
|
71
76
|
|
72
77
|
def test_serialize_with_root
|
73
|
-
json =
|
78
|
+
json = serialize Car.new(make: :honda, model: :civic), root: :car
|
74
79
|
assert_set [ :car ], json.keys
|
75
80
|
assert_equal :honda, json[:car][:make]
|
76
81
|
assert_equal :civic, json[:car][:model]
|
@@ -79,8 +84,8 @@ module RubySerializer
|
|
79
84
|
#----------------------------------------------------------------------------------------------
|
80
85
|
|
81
86
|
def test_serialize_with_errors
|
82
|
-
resource =
|
83
|
-
json =
|
87
|
+
resource = Shape.new(color: :red, shape: :sausages, valid: false, errors: { shape: 'is invalid' })
|
88
|
+
json = serialize resource, with: ShapeSerializer
|
84
89
|
expected = [ :color, :shape, :errors ]
|
85
90
|
assert_set expected, json.keys
|
86
91
|
assert_equal :red, json[:color]
|
data/test/expose_test.rb
CHANGED
@@ -10,7 +10,14 @@ module RubySerializer
|
|
10
10
|
EMAIL = 'Email'
|
11
11
|
SECRET = 'Secret'
|
12
12
|
|
13
|
-
Resource
|
13
|
+
class Resource < Model
|
14
|
+
attr :id
|
15
|
+
attr :name
|
16
|
+
attr :email
|
17
|
+
attr :secret # this attribute will not be exposed in any serializer
|
18
|
+
attr :value # this attribute might be used as a custom :value in some serializers
|
19
|
+
attr :show # this attribute might be used to conditionally show :only or :unless
|
20
|
+
end
|
14
21
|
|
15
22
|
#==============================================================================================
|
16
23
|
# Sample Serializers
|
@@ -45,9 +52,8 @@ module RubySerializer
|
|
45
52
|
class CustomValueSerializer < RubySerializer::Base
|
46
53
|
expose :id
|
47
54
|
expose :static, value: 'static value'
|
48
|
-
expose :method, value: :method_value
|
49
55
|
expose :dynamic, value: -> { "dynamic value (#{resource.name})" }
|
50
|
-
expose :
|
56
|
+
expose :method, value: :value
|
51
57
|
end
|
52
58
|
|
53
59
|
#----------------------------------------------------------------------------------------------
|
@@ -89,7 +95,7 @@ module RubySerializer
|
|
89
95
|
|
90
96
|
def test_expose_attributes_unchanged
|
91
97
|
resource = Resource.new(id: ID, name: NAME, email: EMAIL, secret: SECRET)
|
92
|
-
json =
|
98
|
+
json = serialize resource, with: BasicSerializer
|
93
99
|
expected = [ :id, :name, :email ]
|
94
100
|
assert_set expected, json.keys
|
95
101
|
assert_equal ID, json[:id]
|
@@ -101,7 +107,7 @@ module RubySerializer
|
|
101
107
|
|
102
108
|
def test_expose_renamed_attributes
|
103
109
|
resource = Resource.new(id: ID, name: NAME, email: EMAIL, secret: SECRET)
|
104
|
-
json =
|
110
|
+
json = serialize resource, with: RenamingSerializer
|
105
111
|
expected = [ :id, :user_name, :user_email ]
|
106
112
|
assert_set expected, json.keys
|
107
113
|
assert_equal ID, json[:id]
|
@@ -113,7 +119,7 @@ module RubySerializer
|
|
113
119
|
|
114
120
|
def test_expose_namespaced_attributes
|
115
121
|
resource = Resource.new(id: ID, name: NAME, email: EMAIL, secret: SECRET)
|
116
|
-
json =
|
122
|
+
json = serialize resource, with: NamespaceSerializer
|
117
123
|
expected = [ :id, :user ]
|
118
124
|
assert_set expected, json.keys
|
119
125
|
assert_equal ID, json[:id]
|
@@ -125,23 +131,22 @@ module RubySerializer
|
|
125
131
|
#----------------------------------------------------------------------------------------------
|
126
132
|
|
127
133
|
def test_expose_attributes_with_custom_values
|
128
|
-
resource = Resource.new(id: ID, name: NAME,
|
129
|
-
json =
|
130
|
-
expected = [ :id, :static, :dynamic, :method
|
134
|
+
resource = Resource.new(id: ID, name: NAME, value: 'method value')
|
135
|
+
json = serialize resource, with: CustomValueSerializer
|
136
|
+
expected = [ :id, :static, :dynamic, :method ]
|
131
137
|
assert_set expected, json.keys
|
132
138
|
assert_equal ID, json[:id]
|
133
139
|
assert_equal 'static value', json[:static]
|
134
140
|
assert_equal 'dynamic value (Name)', json[:dynamic]
|
135
141
|
assert_equal 'method value', json[:method]
|
136
|
-
assert_equal nil, json[:empty]
|
137
142
|
end
|
138
143
|
|
139
144
|
#----------------------------------------------------------------------------------------------
|
140
145
|
|
141
146
|
def test_expose_attributes_conditionally
|
142
|
-
show =
|
143
|
-
noshow =
|
144
|
-
unspecified =
|
147
|
+
show = serialize Resource.new(id: ID, show: true), with: ConditionalSerializer
|
148
|
+
noshow = serialize Resource.new(id: ID, show: false), with: ConditionalSerializer
|
149
|
+
unspecified = serialize Resource.new(id: ID), with: ConditionalSerializer
|
145
150
|
assert_set [ :id, :only_true, :unless_false, :only_method, :only_dynamic ], show.keys
|
146
151
|
assert_set [ :id, :only_true, :unless_false, :unless_method, :unless_dynamic ], noshow.keys
|
147
152
|
assert_set [ :id, :only_true, :unless_false, :unless_method, :unless_dynamic ], unspecified.keys
|
@@ -152,7 +157,7 @@ module RubySerializer
|
|
152
157
|
def test_expose_attributes_using_custom_resource_name
|
153
158
|
|
154
159
|
resource = Resource.new(id: ID, value: 42, show: false)
|
155
|
-
json =
|
160
|
+
json = serialize resource, with: CustomResourceNameSerializer
|
156
161
|
expected = [ :id, :value, :show_unless ]
|
157
162
|
assert_set expected, json.keys
|
158
163
|
assert_equal ID, json[:id]
|
@@ -160,7 +165,7 @@ module RubySerializer
|
|
160
165
|
assert_equal "show unless", json[:show_unless]
|
161
166
|
|
162
167
|
resource = Resource.new(id: ID, value: 99, show: true)
|
163
|
-
json =
|
168
|
+
json = serialize resource, with: CustomResourceNameSerializer
|
164
169
|
expected = [ :id, :value, :show_only ]
|
165
170
|
assert_set expected, json.keys
|
166
171
|
assert_equal ID, json[:id]
|
@@ -175,14 +180,14 @@ module RubySerializer
|
|
175
180
|
|
176
181
|
resource = Resource.new(id: ID)
|
177
182
|
|
178
|
-
json =
|
183
|
+
json = serialize resource, value: 42, show: false, with: CustomOptionsSerializer
|
179
184
|
expected = [ :id, :value, :show_unless ]
|
180
185
|
assert_set expected, json.keys
|
181
186
|
assert_equal ID, json[:id]
|
182
187
|
assert_equal "value is 42", json[:value]
|
183
188
|
assert_equal "show unless", json[:show_unless]
|
184
189
|
|
185
|
-
json =
|
190
|
+
json = serialize resource, value: 99, show: true, with: CustomOptionsSerializer
|
186
191
|
expected = [ :id, :value, :show_only ]
|
187
192
|
assert_set expected, json.keys
|
188
193
|
assert_equal ID, json[:id]
|
data/test/model.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
module RubySerializer
|
2
|
+
class Model # simple model with ActiveRecord like associations
|
3
|
+
|
4
|
+
def initialize(attributes = {})
|
5
|
+
@valid = true
|
6
|
+
@errors = []
|
7
|
+
attributes.each do |key, value|
|
8
|
+
instance_variable_set "@#{key}", value
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def valid?
|
13
|
+
@valid
|
14
|
+
end
|
15
|
+
|
16
|
+
def errors
|
17
|
+
@errors
|
18
|
+
end
|
19
|
+
|
20
|
+
#----------------------------------------------------------------------------------------------
|
21
|
+
|
22
|
+
def self.belongs_to(key)
|
23
|
+
idkey = "#{key}_id"
|
24
|
+
instvar = "@#{key}"
|
25
|
+
idinstvar = "@#{idkey}"
|
26
|
+
define_method key do
|
27
|
+
instance_variable_get instvar
|
28
|
+
end
|
29
|
+
define_method idkey do
|
30
|
+
instance_variable_get idinstvar
|
31
|
+
end
|
32
|
+
define_method "#{key}=" do |value|
|
33
|
+
instance_variable_set instvar, value
|
34
|
+
instance_variable_set idinstvar, value ? value.id : nil
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
#----------------------------------------------------------------------------------------------
|
39
|
+
|
40
|
+
def self.has_one(key)
|
41
|
+
instvar = "@#{key}"
|
42
|
+
define_method key do
|
43
|
+
instance_variable_get instvar
|
44
|
+
end
|
45
|
+
define_method "#{key}=" do |value|
|
46
|
+
instance_variable_set instvar, value
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
#----------------------------------------------------------------------------------------------
|
51
|
+
|
52
|
+
def self.has_many(key)
|
53
|
+
instvar = "@#{key}"
|
54
|
+
define_method key do
|
55
|
+
instance_variable_set instvar, [] unless instance_variable_defined?(instvar)
|
56
|
+
instance_variable_get instvar
|
57
|
+
end
|
58
|
+
define_method "#{key}=" do |value|
|
59
|
+
instance_variable_set instvar, Array(value)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
#----------------------------------------------------------------------------------------------
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
data/test/test_case.rb
CHANGED
@@ -2,13 +2,22 @@ require_relative '../lib/ruby_serializer'
|
|
2
2
|
|
3
3
|
require 'minitest/autorun'
|
4
4
|
require 'minitest/pride'
|
5
|
+
require 'ostruct'
|
5
6
|
require 'byebug'
|
6
7
|
|
8
|
+
require_relative 'model'
|
9
|
+
|
7
10
|
module RubySerializer
|
8
11
|
class TestCase < Minitest::Test
|
9
12
|
|
10
13
|
#----------------------------------------------------------------------------------------------
|
11
14
|
|
15
|
+
def serialize(resource, options = {})
|
16
|
+
RubySerializer.as_json resource, options
|
17
|
+
end
|
18
|
+
|
19
|
+
#----------------------------------------------------------------------------------------------
|
20
|
+
|
12
21
|
def assert_set(expected, actual, message = nil)
|
13
22
|
assert_equal Set.new(expected), Set.new(actual), message
|
14
23
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruby-serializer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jake Gordon
|
@@ -81,13 +81,16 @@ files:
|
|
81
81
|
- LICENSE
|
82
82
|
- Rakefile
|
83
83
|
- lib/ruby_serializer.rb
|
84
|
+
- lib/ruby_serializer/association.rb
|
84
85
|
- lib/ruby_serializer/base.rb
|
85
86
|
- lib/ruby_serializer/dsl.rb
|
86
87
|
- lib/ruby_serializer/field.rb
|
87
88
|
- readme.md
|
88
89
|
- ruby-serializer.gemspec
|
90
|
+
- test/association_test.rb
|
89
91
|
- test/basic_test.rb
|
90
92
|
- test/expose_test.rb
|
93
|
+
- test/model.rb
|
91
94
|
- test/test_case.rb
|
92
95
|
homepage: https://github.com/jakesgordon/ruby-serializer
|
93
96
|
licenses:
|