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.
@@ -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