fars 0.0.1 → 0.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 +15 -0
- data/Gemfile.lock +1 -1
- data/README.md +259 -3
- data/fars.gemspec +3 -4
- data/lib/fars.rb +22 -3
- data/lib/fars/base_collection_serializer.rb +95 -41
- data/lib/fars/base_model_serializer.rb +32 -188
- data/lib/fars/base_object_serializer.rb +147 -0
- data/lib/fars/model_serializable.rb +7 -0
- data/lib/fars/relation_serializable.rb +5 -0
- data/lib/fars/version.rb +1 -1
- data/spec/active_record/base_spec.rb +15 -0
- data/spec/active_record/relation_spec.rb +16 -0
- data/spec/array_spec.rb +19 -0
- data/spec/fars/base_collection_serializer_spec.rb +45 -0
- data/spec/fars/base_model_serializer_spec.rb +110 -0
- data/spec/fars/base_object_serializer_spec.rb +35 -0
- data/spec/hash_spec.rb +36 -0
- data/spec/spec_helper.rb +6 -0
- data/spec/support/models/master.rb +3 -0
- data/spec/support/models/slave.rb +3 -0
- data/spec/support/serializers/another_master_serialiser.rb +11 -0
- data/spec/support/serializers/book_serializer.rb +12 -0
- data/spec/support/serializers/color_serializer.rb +5 -0
- data/spec/support/serializers/master_serializer.rb +8 -0
- data/spec/support/serializers/slave_serializer.rb +3 -0
- data/spec/support/serializers/stat_serializer.rb +5 -0
- data/spec/support/serializers/v1/master_serializer.rb +15 -0
- data/spec/support/serializers/v1/slave_serializer.rb +10 -0
- data/spec/tasks/db_setup.rake +1 -1
- metadata +42 -23
- data/spec/fars/api_version_spec.rb +0 -77
- data/spec/fars/fars_spec.rb +0 -75
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
NDg4ZDFkOTVmYmQ0YTQ4MTI1N2I1ZjlmOWI2MThmZDM1ZDUwMTU0OA==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
MGIzMDgzZmVhNWM0MzEyMDkzODgzMTZkMjhlNWIyODhmYmRiM2YxMA==
|
7
|
+
SHA512:
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
YzhkZWI4MDdmMzExY2M3OGQ0YjhiZmI2NjQ3MzdlYTA4NDEzYTAzOWNjNGJl
|
10
|
+
NDM3YjczZTQ5MmE1YzE2MDdkZGVkZmExNmEzMDI3OGVjZDM2Yjc0OTQyOTg3
|
11
|
+
YTcxZWFlMTEwZWI4NzY0Zjg2ODU3MzI0OWEzZmE2OTYwMjY1ZTM=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
NzZkM2FlNmFkNWI3ZTg5NTQ0ZmM3M2NiYjk1MjYyYTM5MzZkYjdkNWNjYjkz
|
14
|
+
OGZmMTEyODliZjQ2MGExOWFmMzAyOWIxZDZlYmI4ODliOGQzMDUwZmY3NjU0
|
15
|
+
YTE5MjdiNWMwMzVlMDUyMTU5YThjNTJlM2EzNjlmYmM1YzRkZWM=
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -1,4 +1,260 @@
|
|
1
|
-
fars
|
2
|
-
====
|
1
|
+
# Fast ActiveRecord Serializer (fars).
|
3
2
|
|
4
|
-
|
3
|
+
JSON serialization of ActiveRecord models and colections (relations or array of objects). Also can serialzie any Array or Hash with minimal syntax.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'fars'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install fars
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
### Serialize instance (of Class inherited from ActiveRecord::Bace)
|
22
|
+
|
23
|
+
```rb
|
24
|
+
class Customer < ActiveRecord::Base
|
25
|
+
has_many :orders
|
26
|
+
end
|
27
|
+
```
|
28
|
+
|
29
|
+
Create serializer class named `CustomerSerializer` or `V1::CustomerSerializer`
|
30
|
+
|
31
|
+
```rb
|
32
|
+
class CustomerSerializer < Fars::BaseModelSerializer
|
33
|
+
attributes :id, :name, :data, # attrs
|
34
|
+
:created_at, :updated_at, # methods
|
35
|
+
:orders # relations
|
36
|
+
|
37
|
+
def created_at
|
38
|
+
object.created_at.try(:strftime, "%F %H:%M")
|
39
|
+
end
|
40
|
+
|
41
|
+
def updated_at
|
42
|
+
object.updated_at.try(:strftime, "%F %H:%M")
|
43
|
+
end
|
44
|
+
|
45
|
+
# _metadata (optional)
|
46
|
+
def meta
|
47
|
+
abilities = [:update, :destroy].select { |a| scope.can?(a, object) }
|
48
|
+
{ abilities: abilities }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
```
|
52
|
+
|
53
|
+
Then you can call #serialize method on object
|
54
|
+
|
55
|
+
```rb
|
56
|
+
# Option :scope is optional, can be used for providing metadata.
|
57
|
+
Customer.first.serialize scope: current_user
|
58
|
+
```
|
59
|
+
|
60
|
+
Available options are: `:api_version`, `:serializer`, `:scope`, `:fields`, `:add_metadata`, `:root_key`, `:params`.
|
61
|
+
Description of this options provided in [next section](#serialize-relation).
|
62
|
+
|
63
|
+
```rb
|
64
|
+
customer = Customer.first
|
65
|
+
|
66
|
+
# whould be serizlized with CustomerSerializer class
|
67
|
+
customer.serialize
|
68
|
+
|
69
|
+
# whould be serizlized with V1::CustomerSerializer class
|
70
|
+
customer.serialize api_version: 'V1'
|
71
|
+
|
72
|
+
# whould be serizlized with V1::ExtendedCustomerSerializer class
|
73
|
+
customer.serialize api_version: 'V1', serializer: "ExtendedCustomerSerializer"
|
74
|
+
```
|
75
|
+
|
76
|
+
You can specify model class and item root key in serializer:
|
77
|
+
|
78
|
+
|
79
|
+
```rb
|
80
|
+
class ExtendedCustomerSerializer < Fars::BaseModelSerializer
|
81
|
+
self.model = Customer
|
82
|
+
self.root_key = :client
|
83
|
+
end
|
84
|
+
```
|
85
|
+
|
86
|
+
### Serialize relation
|
87
|
+
|
88
|
+
```rb
|
89
|
+
customers = Customer.where("1 = 1")
|
90
|
+
customers.serialize
|
91
|
+
```
|
92
|
+
|
93
|
+
Available some options
|
94
|
+
|
95
|
+
```rb
|
96
|
+
customers.serialize
|
97
|
+
root_key: :clients, # collection root key, default whoud be :customers, false if omit
|
98
|
+
api_version: "V1", # namesapce of model serializer class
|
99
|
+
fields: [:id, :name, :updated_at], # array of needed fields
|
100
|
+
scope: current_user, # user or ability, can be used in serializer meta method
|
101
|
+
add_metadata: true, # add or not item metadata, default is true if serializer respond_to? :meta
|
102
|
+
serializer: "ExtendedCustomerSerializer", # custom model serializer class
|
103
|
+
class_name: "Client", # item model class (can construct serializer class name from it), useful for array of objects
|
104
|
+
metadata: { limit: 10, offset: 50 }, # collection metadata (:root_key cannot be omitted)
|
105
|
+
params: { format: 'long' } # any parameters, can be accessed in serializes class
|
106
|
+
```
|
107
|
+
|
108
|
+
You can override serializers `#available_attributes` method for providing dynamic attributes
|
109
|
+
depending on internal serializer's logic.
|
110
|
+
|
111
|
+
```rb
|
112
|
+
class CustomerSerializer < Fars::BaseModelSerializer
|
113
|
+
attributes :id, :name, :data, # attrs
|
114
|
+
:created_at, :updated_at, # methods
|
115
|
+
:orders # relations
|
116
|
+
|
117
|
+
def created_at
|
118
|
+
object.created_at.try(:strftime, "%F %H:%M")
|
119
|
+
end
|
120
|
+
|
121
|
+
def updated_at
|
122
|
+
object.updated_at.try(:strftime, "%F %H:%M")
|
123
|
+
end
|
124
|
+
|
125
|
+
def available_attributes
|
126
|
+
attributes = [:id, :name, :created_at, :updated_at, :orders]
|
127
|
+
attributes << :data if scope.can?(:view_data, object)
|
128
|
+
attributes
|
129
|
+
end
|
130
|
+
|
131
|
+
# _metadata (optional)
|
132
|
+
def meta
|
133
|
+
abilities = [:update, :destroy].select { |a| scope.can?(a, object) }
|
134
|
+
{ abilities: abilities }
|
135
|
+
end
|
136
|
+
end
|
137
|
+
```
|
138
|
+
|
139
|
+
### Serialize array of instances
|
140
|
+
|
141
|
+
Array of instances can by serialized same as relation. In this case default collection's root_key will be constructed from first element class name (can't be empty array) or from povided class name (class_name option).
|
142
|
+
|
143
|
+
### Serialize any Array
|
144
|
+
|
145
|
+
Provide root_key (false if omit) and serializer (proc, block or custom class)
|
146
|
+
|
147
|
+
```rb
|
148
|
+
array = %w{green blue grey}
|
149
|
+
|
150
|
+
# with proc
|
151
|
+
array.serialize root_key: :colors,
|
152
|
+
serializer: Proc.new { |c| { color: c }
|
153
|
+
|
154
|
+
# with block
|
155
|
+
array.serialize(root_key: :colors) { |c| { color: c } }
|
156
|
+
|
157
|
+
# with custom class
|
158
|
+
class ColorSerializer < Fars::BaseObjectSerializer
|
159
|
+
def as_json
|
160
|
+
{ color: object }
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
array.serialize(root_key: :colors, serializer: 'ColorSerializer')
|
165
|
+
```
|
166
|
+
This will produce:
|
167
|
+
|
168
|
+
```rb
|
169
|
+
{ colors: [
|
170
|
+
{ color: 'green' },
|
171
|
+
{ color: 'blue' },
|
172
|
+
{ color: 'grey' }
|
173
|
+
] }.to_json
|
174
|
+
```
|
175
|
+
|
176
|
+
### Serialize Hash
|
177
|
+
|
178
|
+
Provide root_key (false if omit) and serializer (proc, block or custom class)
|
179
|
+
|
180
|
+
```rb
|
181
|
+
hash = {
|
182
|
+
'2014-01-01' => { visitors: 23, visits: 114 },
|
183
|
+
'2014-01-02' => { visitors: 27, visits: 217 }
|
184
|
+
}
|
185
|
+
|
186
|
+
# with proc
|
187
|
+
hash.serialize root_key: :stats,
|
188
|
+
serializer: Proc.new { |k, v| { day: k, visitors: v[:visitors] } })
|
189
|
+
|
190
|
+
# with block
|
191
|
+
hash.serialize root_key: :stats do |k, v|
|
192
|
+
{ day: k, visitors: v[:visitors] }
|
193
|
+
end
|
194
|
+
|
195
|
+
# with custom class
|
196
|
+
# object in this case is key-value pair
|
197
|
+
class StatSerializer < Fars::BaseObjectSerializer
|
198
|
+
def as_json
|
199
|
+
{ stat_key: object[0], stat_value: object[1] }
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
hash.serialize root_key: :stats, serializer: 'StatSerializer'
|
204
|
+
```
|
205
|
+
|
206
|
+
This will produce:
|
207
|
+
|
208
|
+
```rb
|
209
|
+
{ stats: [
|
210
|
+
{ day: '2014-01-01', visitors: 23 },
|
211
|
+
{ day: '2014-01-02', visitors: 27 }
|
212
|
+
] }.to_json
|
213
|
+
```
|
214
|
+
|
215
|
+
### Serialize any object with serializer inherited from Fars::BaseObjectSerializer
|
216
|
+
|
217
|
+
```rb
|
218
|
+
Book = Struct.new(:isbn, :title, :author, :price, :count)
|
219
|
+
b1 = Book.new('isbn1', 'title1', 'author1', 10, nil)
|
220
|
+
b2 = Book.new('isbn2', 'title2', 'author2', 20.0, 4)
|
221
|
+
b3 = Book.new('isbn3', 'title3', 'author3', 30.5, 7)
|
222
|
+
book = b1
|
223
|
+
books = [b1, b2, b3]
|
224
|
+
|
225
|
+
class BookSerializer < Fars::BaseObjectSerializer
|
226
|
+
attributes :isbn, :title, :author, # attrs
|
227
|
+
:price, :count # methods
|
228
|
+
|
229
|
+
def price
|
230
|
+
"%.2f" % object.price
|
231
|
+
end
|
232
|
+
|
233
|
+
def count
|
234
|
+
object.count.to_i
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
# serializes any object with appropriate serializer
|
239
|
+
BookSerializer.new(book, fields: [:isbn, :title, :price]).to_json
|
240
|
+
# => { book: { isbn: 'isbn1', title: 'title1', price: '10.00' } }.to_json
|
241
|
+
|
242
|
+
# serialize collection
|
243
|
+
books.serialize(root_key: :books, # can be resolved automatically for non empty array
|
244
|
+
serializer: 'BookSerializer', # can be resolved automatically for non empty array
|
245
|
+
fields: [:isbn, :title, :price]) # all by default
|
246
|
+
|
247
|
+
# => { books: [
|
248
|
+
# { book: { isbn: 'isbn1', title: 'title1', price: '10.00' } },
|
249
|
+
# { book: { isbn: 'isbn2', title: 'title2', price: '20.00' } },
|
250
|
+
# { book: { isbn: 'isbn3', title: 'title3', price: '30.50' } }
|
251
|
+
# ] }.to_json
|
252
|
+
```
|
253
|
+
|
254
|
+
## Contributing
|
255
|
+
|
256
|
+
1. Fork it ( http://github.com/Lightpower/fars/fork )
|
257
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
258
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
259
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
260
|
+
5. Create new Pull Request
|
data/fars.gemspec
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
#
|
1
|
+
# encoding: utf-8
|
2
2
|
require File.expand_path('../lib/fars/version', __FILE__)
|
3
3
|
|
4
4
|
Gem::Specification.new do |gem|
|
@@ -15,12 +15,11 @@ Gem::Specification.new do |gem|
|
|
15
15
|
gem.require_paths = ["lib"]
|
16
16
|
gem.version = Fars::VERSION
|
17
17
|
|
18
|
-
gem.add_dependency 'activerecord',
|
18
|
+
gem.add_dependency 'activerecord', '>= 3.2'
|
19
19
|
|
20
|
-
gem.add_development_dependency 'rspec',
|
20
|
+
gem.add_development_dependency 'rspec', '>= 2.11'
|
21
21
|
gem.add_development_dependency 'rake'
|
22
22
|
gem.add_development_dependency 'shoulda'
|
23
23
|
gem.add_development_dependency 'pg'
|
24
24
|
gem.add_development_dependency 'database_cleaner'
|
25
|
-
|
26
25
|
end
|
data/lib/fars.rb
CHANGED
@@ -1,5 +1,24 @@
|
|
1
|
-
|
2
|
-
require 'fars/base_collection_serializer'
|
3
|
-
require 'fars/base_model_serializer'
|
1
|
+
require 'active_record'
|
4
2
|
|
3
|
+
module Fars; end
|
4
|
+
require 'fars/base_object_serializer'
|
5
|
+
require 'fars/base_model_serializer'
|
6
|
+
require 'fars/base_collection_serializer'
|
7
|
+
require 'fars/model_serializable'
|
8
|
+
require 'fars/relation_serializable'
|
9
|
+
|
10
|
+
class ActiveRecord::Base
|
11
|
+
include Fars::ModelSerializable
|
12
|
+
end
|
13
|
+
|
14
|
+
class ActiveRecord::Relation
|
15
|
+
include Fars::RelationSerializable
|
16
|
+
end
|
17
|
+
|
18
|
+
class Array
|
19
|
+
include Fars::RelationSerializable
|
20
|
+
end
|
21
|
+
|
22
|
+
class Hash
|
23
|
+
include Fars::RelationSerializable
|
5
24
|
end
|
@@ -1,72 +1,126 @@
|
|
1
1
|
##
|
2
2
|
# Class: BaseCollectionSerializer
|
3
3
|
#
|
4
|
-
# It is used to represent
|
4
|
+
# It is used to represent collection
|
5
5
|
#
|
6
6
|
class Fars::BaseCollectionSerializer
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
7
|
+
##
|
8
|
+
# Constructor
|
9
|
+
#
|
10
|
+
# Params:
|
11
|
+
# - objects {ActiveRecord::Relation} or {Array} collection to serialize
|
12
|
+
# - opts {Hash} of options:
|
13
|
+
# - fields {Array} of attributes to serialize. Can be {NilClass}.
|
14
|
+
# If so - will use all available.
|
15
|
+
# - scope {Object} context of request. Usually current user
|
16
|
+
# or current ability. Can be passed as a {Proc}. If so -
|
17
|
+
# evaluated only when actually called.
|
18
|
+
# - :add_metadata {Boolean} if to add a node '_metadata'
|
19
|
+
# - :root_key {Symbol} overwrites the default one from serializer's Class
|
20
|
+
# - :api_version {String} namespace for serializers classes, e.g. "V1"
|
21
|
+
# - :class_name {String} serialized model class name
|
22
|
+
# - :serializer {String} model serializer class name
|
23
|
+
# - :metadata {Hash} optional hash with metadata (root_key should not be false)
|
24
|
+
#
|
25
|
+
def initialize(objects, opts = {}, &block)
|
26
|
+
@objects = objects
|
27
|
+
if !opts.has_key?(:root_key) && !opts[:class_name] && empty_array?
|
28
|
+
raise ArgumentError, 'Specify :root_key or model :class_name for empty array.'
|
29
|
+
end
|
30
|
+
# Cann't use Hash#fetch here, becouse if root_key provided default_root_key method should not be called.
|
31
|
+
@root_key = opts.has_key?(:root_key) ? opts[:root_key] : default_root_key
|
32
|
+
if !@root_key && opts[:metadata]
|
33
|
+
raise ArgumentError, 'Can not omit :root_key if provided :metadata'
|
34
|
+
end
|
35
|
+
# Serialized model class name.
|
36
|
+
@class_name = opts[:class_name]
|
37
|
+
if opts[:serializer]
|
38
|
+
if opts[:serializer].is_a? Proc
|
39
|
+
@item_serializer = opts[:serializer]
|
40
|
+
else
|
41
|
+
@item_serializer_class = opts[:serializer].constantize
|
42
|
+
end
|
43
|
+
elsif block_given?
|
44
|
+
@item_serializer = block
|
45
|
+
end
|
46
|
+
@api_version = opts[:api_version]
|
47
|
+
@params = opts[:params] || {}
|
48
|
+
@metadata = opts[:metadata]
|
49
|
+
# Do not need options if serialize items with proc.
|
50
|
+
unless @item_serializer
|
51
|
+
# Options for model serializer.
|
52
|
+
@options = opts.slice(:scope, :fields, :add_metadata, :api_version, :params)
|
53
|
+
# If root_key is false, do not transfer this option to the model serializer class.
|
54
|
+
@options[:root_key] = item_root_key if @root_key
|
12
55
|
end
|
13
56
|
end
|
14
57
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
@fields = opts[:fields]
|
19
|
-
@add_metadata = opts[:add_metadata]
|
20
|
-
@root_key = opts.fetch(:root_key, get_root_key)
|
21
|
-
@item_serializer_class = get_item_serializer_class
|
22
|
-
end
|
23
|
-
|
58
|
+
##
|
59
|
+
# Returns: Hash
|
60
|
+
#
|
24
61
|
def as_json
|
25
62
|
items = []
|
26
63
|
|
27
|
-
|
28
|
-
nil,
|
29
|
-
scope: @scope,
|
30
|
-
add_metadata: add_metadata,
|
31
|
-
fields: fields,
|
32
|
-
root_key: get_instance_root_key,
|
33
|
-
)
|
64
|
+
unless empty_array?
|
65
|
+
@item_serializer ||= item_serializer_class.new(nil, options)
|
34
66
|
|
35
|
-
|
36
|
-
|
67
|
+
objects.each do |object|
|
68
|
+
items << item_serializer.call(object)
|
69
|
+
end
|
37
70
|
end
|
38
71
|
|
39
|
-
|
72
|
+
return items unless root_key
|
73
|
+
|
74
|
+
hash = { root_key => items }
|
75
|
+
hash[:_metadata] = metadata if metadata
|
76
|
+
hash
|
40
77
|
end
|
41
78
|
|
42
79
|
def to_json
|
43
80
|
MultiJson.dump(as_json)
|
44
81
|
end
|
45
82
|
|
46
|
-
# Returns {String} - API version is got by instance class
|
47
|
-
def api_version
|
48
|
-
self.class.api_version
|
49
|
-
end
|
50
|
-
|
51
83
|
private
|
52
84
|
|
53
|
-
attr_reader :objects, :
|
85
|
+
attr_reader :objects, :options, :root_key, :api_version, :params, :metadata, :item_serializer
|
86
|
+
|
87
|
+
##
|
88
|
+
# Checks if objets is not ActiveRecord::Relation and it's empty.
|
89
|
+
# In this case impossible to obtain model's class name.
|
90
|
+
#
|
91
|
+
def empty_array?
|
92
|
+
objects.is_a?(Array) && objects.empty?
|
93
|
+
end
|
54
94
|
|
55
|
-
|
56
|
-
|
95
|
+
##
|
96
|
+
# Returns: {String} ActiveRecord Model base_class name
|
97
|
+
#
|
98
|
+
def class_name
|
99
|
+
@class_name ||= if objects.is_a?(ActiveRecord::Relation)
|
100
|
+
objects.klass
|
101
|
+
else
|
102
|
+
objects.first.class
|
103
|
+
end.base_class.to_s
|
57
104
|
end
|
58
105
|
|
59
|
-
|
60
|
-
|
61
|
-
|
106
|
+
##
|
107
|
+
# Returns: {Symbol}, requires @class_name
|
108
|
+
#
|
109
|
+
def default_root_key
|
110
|
+
class_name.to_s.underscore.pluralize.to_sym
|
62
111
|
end
|
63
112
|
|
64
|
-
|
65
|
-
|
113
|
+
##
|
114
|
+
# Returns: {Symbol} or nil
|
115
|
+
#
|
116
|
+
def item_root_key
|
117
|
+
root_key.to_s.singularize.to_sym if root_key
|
66
118
|
end
|
67
119
|
|
68
|
-
|
69
|
-
|
70
|
-
|
120
|
+
##
|
121
|
+
# Returns: {Class} of Model Serializer
|
122
|
+
#
|
123
|
+
def item_serializer_class
|
124
|
+
@item_serializer_class ||= "#{api_version + '::' if api_version}#{class_name}Serializer".constantize
|
71
125
|
end
|
72
126
|
end
|