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