ruby-serializer 0.0.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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:
|