fars 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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=
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- fars (0.0.1)
4
+ fars (0.0.2)
5
5
  activerecord (>= 3.2)
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -1,4 +1,260 @@
1
- fars
2
- ====
1
+ # Fast ActiveRecord Serializer (fars).
3
2
 
4
- Fast ActiveRecord Serializer
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
@@ -1,4 +1,4 @@
1
- # -*- encoding: utf-8 -*-
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', '>= 3.2'
18
+ gem.add_dependency 'activerecord', '>= 3.2'
19
19
 
20
- gem.add_development_dependency 'rspec', '>= 2.11'
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
@@ -1,5 +1,24 @@
1
- module Fars
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 collections
4
+ # It is used to represent collection
5
5
  #
6
6
  class Fars::BaseCollectionSerializer
7
- class << self
8
- # Returns {String} capitalized API version
9
- def api_version
10
- namespace_array = name.split('::')
11
- namespace_array.size > 1 ? namespace_array[0] : nil
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
- def initialize(objects, opts={})
16
- @objects = objects
17
- @scope = opts[:scope]
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
- the_class = item_serializer_class.new(
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
- objects.each do |object|
36
- items << the_class.with_object(object).as_json
67
+ objects.each do |object|
68
+ items << item_serializer.call(object)
69
+ end
37
70
  end
38
71
 
39
- root_key ? {root_key => items} : items
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, :scope, :fields, :add_metadata, :root_key, :item_serializer_class
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
- def get_root_key
56
- (self.to_s.match /#{api_prefix}(\w+)Serializer/)[1].underscore.to_sym
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
- def get_instance_root_key
60
- # If root_key is false, get a real one
61
- (root_key || get_root_key).to_s.singularize.to_sym
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
- def get_item_serializer_class
65
- (self.class.to_s.gsub('Serializer', '').singularize + 'Serializer').constantize
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
- # Returns {String} prefix for serializer class name using API version
69
- def api_prefix
70
- api_version ? api_version + '::' : ''
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