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
@@ -7,225 +7,69 @@
|
|
7
7
|
# - create new instance, call to_json
|
8
8
|
# - create new instance, add objects to serialize one by one.
|
9
9
|
# This lets you reuse it without determining known data
|
10
|
-
|
10
|
+
#
|
11
|
+
class Fars::BaseModelSerializer < Fars::BaseObjectSerializer
|
11
12
|
class << self
|
12
|
-
|
13
|
-
|
14
|
-
end
|
15
|
-
|
16
|
-
def all_attributes
|
17
|
-
@attributes
|
18
|
-
end
|
19
|
-
|
20
|
-
# Returns {Object} of class, that corresponds to model,
|
21
|
-
# this class meant to serialize
|
22
|
-
def model
|
23
|
-
@klass ||= get_model
|
24
|
-
end
|
25
|
-
|
26
|
-
# Returns {Symbol} instance hash root key, as an underscored Model name
|
27
|
-
def root_key
|
28
|
-
model.to_s.underscore.to_sym
|
29
|
-
end
|
30
|
-
|
31
|
-
# Returns {String} capitalized API version
|
32
|
-
def api_version
|
33
|
-
namespace_array = name.split('::')
|
34
|
-
namespace_array.size > 1 ? namespace_array[0] : nil
|
35
|
-
end
|
36
|
-
|
37
|
-
# Returns {Array} with names of Model relations. Consists of Symbols.
|
38
|
-
# Filtrated by #all_attributes
|
39
|
-
def model_relations
|
40
|
-
@model_relations ||= get_model_relations
|
41
|
-
end
|
42
|
-
|
43
|
-
# Returns {Array} with names of Model methods. Consists of Symbols
|
13
|
+
##
|
14
|
+
# Returns: {Array} with names of Model methods. Consists of Symbols
|
44
15
|
# Filtrated by #all_attributes
|
16
|
+
#
|
45
17
|
def model_methods
|
46
|
-
@model_methods ||=
|
18
|
+
@model_methods ||= ((model.attribute_names.map(&:to_sym) | model.instance_methods) - model_relations - serializer_methods) & all_attributes
|
47
19
|
end
|
48
20
|
|
49
|
-
|
21
|
+
##
|
22
|
+
# Returns: {Array} with names of Model relations. Consists of Symbols.
|
50
23
|
# Filtrated by #all_attributes
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
def serializer_for_relation(name, _api_version=nil)
|
56
|
-
serializers_cache[name] ||= resolve_serializer(name, _api_version)
|
57
|
-
end
|
58
|
-
|
59
|
-
private
|
60
|
-
|
61
|
-
def serializers_cache
|
62
|
-
@serializers_cache ||= {}
|
63
|
-
end
|
64
|
-
|
65
|
-
def resolve_serializer(relation_name, _api_version=nil)
|
66
|
-
(
|
67
|
-
api_prefix +
|
68
|
-
model.reflect_on_all_associations.
|
69
|
-
find { |assoc| assoc.name == relation_name }.
|
70
|
-
class_name.pluralize +
|
71
|
-
'Serializer'
|
72
|
-
).constantize
|
73
|
-
end
|
74
|
-
|
75
|
-
def get_model
|
76
|
-
(self.to_s.match /#{api_prefix}(\w+)Serializer/)[1].singularize.constantize
|
77
|
-
end
|
78
|
-
|
79
|
-
def get_model_relations
|
80
|
-
(model.reflect_on_all_associations.map { |r| r.name.to_sym } - serializer_methods) & all_attributes
|
81
|
-
end
|
82
|
-
|
83
|
-
def get_model_methods
|
84
|
-
((model.attribute_names.map(&:to_sym) | model.instance_methods) - model_relations - serializer_methods) & all_attributes
|
85
|
-
end
|
86
|
-
|
87
|
-
def get_serializer_methods
|
88
|
-
self.instance_methods & all_attributes
|
89
|
-
end
|
90
|
-
|
91
|
-
# Returns {String} prefix for serializer class name using API version
|
92
|
-
def api_prefix
|
93
|
-
api_version ? api_version + '::' : ''
|
24
|
+
#
|
25
|
+
def model_relations
|
26
|
+
@model_relations ||= (model.reflect_on_all_associations.map { |r| r.name.to_sym } - serializer_methods) & all_attributes
|
94
27
|
end
|
95
28
|
end
|
96
29
|
|
97
|
-
# Initialize new instance
|
98
|
-
#
|
99
|
-
# Params:
|
100
|
-
# - object {Object} to serialize
|
101
|
-
# - opts {Hash} of options:
|
102
|
-
# - fields {Array} of attributes to serialize. Can be {NilClass}.
|
103
|
-
# If so - will use all available.
|
104
|
-
# - scope {Object} context of request. Usually current user
|
105
|
-
# or current ability. Can be passed as a {Proc}. If so -
|
106
|
-
# evaluated only when actually called.
|
107
|
-
# - :add_metadata {Boolean} if to add a node '_metadata'
|
108
|
-
# - :root_key {Symbol} overwrites the default one from serializer's Class
|
109
|
-
def initialize(object, opts={})
|
110
|
-
@object = object
|
111
|
-
@scope = opts[:scope]
|
112
|
-
@fields = opts[:fields]
|
113
|
-
@add_metadata = opts.fetch(:add_metadata, true)
|
114
|
-
@root_key = opts.fetch(:root_key, self.class.root_key)
|
115
|
-
|
116
|
-
@serialized_klass = self.class.model # this goes first
|
117
|
-
@all_attributes = self.class.all_attributes
|
118
|
-
@object_relations = self.class.model_relations
|
119
|
-
@object_attributes = self.class.model_methods
|
120
|
-
@serializer_methods = self.class.serializer_methods
|
121
|
-
end
|
122
|
-
|
123
|
-
def with_object(object)
|
124
|
-
@object = object
|
125
|
-
self
|
126
|
-
end
|
127
|
-
|
128
30
|
def as_json
|
129
31
|
# as we can re-use one class of serializer for
|
130
32
|
# many objects, we need to re-evaluate list
|
131
33
|
# of available_attributes for each of them
|
132
34
|
all_attrs = available_attributes
|
133
35
|
item = {}
|
134
|
-
|
135
|
-
|
136
|
-
item[attr] = object.send(attr)
|
36
|
+
(requested_model_methods & all_attrs).each do |attr|
|
37
|
+
item[attr] = object.public_send(attr)
|
137
38
|
end
|
138
|
-
|
139
39
|
(requested_serializer_methods & all_attrs).each do |meth|
|
140
|
-
item[meth] = self.
|
40
|
+
item[meth] = self.public_send(meth)
|
141
41
|
end
|
142
|
-
|
143
|
-
(requested_object_relations & all_attrs).each do |rel|
|
42
|
+
(requested_model_relations & all_attrs).each do |rel|
|
144
43
|
item[rel] = serialize_relation(rel)
|
145
44
|
end
|
146
|
-
|
147
|
-
hash = {root_key => item}
|
45
|
+
hash = { root_key => item }
|
148
46
|
hash[:_metadata] = meta if add_metadata?
|
149
47
|
hash
|
150
48
|
end
|
151
49
|
|
152
|
-
def to_json
|
153
|
-
MultiJson.dump(as_json)
|
154
|
-
end
|
155
|
-
|
156
|
-
protected
|
157
|
-
|
158
|
-
# TODO: Document
|
159
|
-
def serialize_relation(relation_name, relation=nil)
|
160
|
-
klass = self.class.serializer_for_relation(relation_name)
|
161
|
-
relation ||= object.send(relation_name)
|
162
|
-
klass.new(relation,
|
163
|
-
root_key: false,
|
164
|
-
scope: @scope,
|
165
|
-
add_metadata: add_metadata?).as_json
|
166
|
-
end
|
167
|
-
|
168
50
|
private
|
169
51
|
|
170
|
-
|
171
|
-
attr_reader :object, :fields, :root_key
|
172
|
-
|
173
|
-
# Utility things
|
174
|
-
attr_reader :serialized_klass
|
52
|
+
delegate :model_relations, :model_methods, to: :'self.class'
|
175
53
|
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
# List of attributes requested to be shown.
|
180
|
-
# This is frequently done by :fields HTTP request
|
181
|
-
# parameter
|
182
|
-
def requested_attributes
|
183
|
-
@requested_attributes ||= get_requested_attributes
|
184
|
-
end
|
185
|
-
|
186
|
-
def scope
|
187
|
-
if @scope.is_a? Proc
|
188
|
-
@scope = @scope.call
|
189
|
-
else
|
190
|
-
@scope
|
191
|
-
end
|
192
|
-
end
|
193
|
-
|
194
|
-
def add_metadata?
|
195
|
-
@add_metadata
|
196
|
-
end
|
197
|
-
|
198
|
-
def requested_object_attributes
|
199
|
-
@requested_object_attributes ||= object_attributes & requested_attributes
|
54
|
+
def requested_model_methods
|
55
|
+
@requested_model_methods ||= model_methods & requested_attributes
|
200
56
|
end
|
201
57
|
|
202
|
-
def
|
203
|
-
@
|
58
|
+
def requested_model_relations
|
59
|
+
@requested_model_relations ||= model_relations & requested_attributes
|
204
60
|
end
|
205
61
|
|
206
|
-
|
207
|
-
|
208
|
-
end
|
209
|
-
|
210
|
-
# List of attributes available to be shown
|
211
|
-
# by current security context.
|
62
|
+
##
|
63
|
+
# Serializes object's relation
|
212
64
|
#
|
213
|
-
#
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
when Symbol then [fields.to_sym]
|
223
|
-
when String then [fields]
|
224
|
-
end
|
225
|
-
end
|
226
|
-
|
227
|
-
# Returns {String} - API version is got by instance class
|
228
|
-
def api_version
|
229
|
-
self.class.api_version
|
65
|
+
# Returns: {Hash}
|
66
|
+
#
|
67
|
+
def serialize_relation(relation_name, relation = nil)
|
68
|
+
relation ||= object.public_send(relation_name)
|
69
|
+
::Fars::BaseCollectionSerializer.new(relation,
|
70
|
+
root_key: false,
|
71
|
+
scope: @scope,
|
72
|
+
add_metadata: add_metadata?,
|
73
|
+
api_version: api_version).as_json
|
230
74
|
end
|
231
75
|
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
##
|
2
|
+
# Class: BaseObjectSerializer
|
3
|
+
#
|
4
|
+
class Fars::BaseObjectSerializer
|
5
|
+
class << self
|
6
|
+
def attributes(*attrs)
|
7
|
+
@attributes = attrs.map(&:to_sym)
|
8
|
+
end
|
9
|
+
|
10
|
+
def all_attributes
|
11
|
+
@attributes
|
12
|
+
end
|
13
|
+
|
14
|
+
##
|
15
|
+
# Returns: {Class} of serialized model (can be nil)
|
16
|
+
#
|
17
|
+
def model
|
18
|
+
@model ||= begin
|
19
|
+
@class_name ||= self.to_s.sub(/^V\d+::/, '').sub(/Serializer$/, '')
|
20
|
+
@class_name.constantize if Object.const_defined?(@class_name)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
attr_writer :model
|
24
|
+
|
25
|
+
##
|
26
|
+
# Returns: {Symbol} instance hash root key, as an underscored Model name
|
27
|
+
#
|
28
|
+
def root_key
|
29
|
+
@root_key ||= (model ? model.to_s.underscore.to_sym : nil)
|
30
|
+
end
|
31
|
+
attr_writer :root_key
|
32
|
+
|
33
|
+
##
|
34
|
+
# Returns: {String} capitalized API version (can be nil)
|
35
|
+
#
|
36
|
+
def api_version
|
37
|
+
namespace_array = name.split('::')
|
38
|
+
namespace_array.size > 1 ? namespace_array[0] : nil
|
39
|
+
end
|
40
|
+
|
41
|
+
##
|
42
|
+
# Returns: {Array} with names of this serializer instance methods. Consists of Symbols
|
43
|
+
# Filtrated by #all_attributes
|
44
|
+
#
|
45
|
+
def serializer_methods
|
46
|
+
@serializer_methods ||= self.instance_methods & all_attributes
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
##
|
51
|
+
# Initialize new instance
|
52
|
+
#
|
53
|
+
# Params:
|
54
|
+
# - object {ActiveRecord::Base} or any {Object} to serialize
|
55
|
+
# - opts {Hash} of options:
|
56
|
+
# - fields {Array} of attributes to serialize. Can be {NilClass}.
|
57
|
+
# If so - will use all available.
|
58
|
+
# - scope {Object} context of request. Usually current user
|
59
|
+
# or current ability. Can be passed as a {Proc}. If so -
|
60
|
+
# evaluated only when actually called.
|
61
|
+
# - :add_metadata {Boolean} if to add a node '_metadata'
|
62
|
+
# - :root_key {Symbol} overwrites the default one from serializer's Class
|
63
|
+
#
|
64
|
+
def initialize(object, opts = {})
|
65
|
+
@object = object
|
66
|
+
@scope = opts[:scope]
|
67
|
+
@fields = opts[:fields]
|
68
|
+
@add_metadata = opts.fetch(:add_metadata, self.respond_to?(:meta))
|
69
|
+
@root_key = opts.fetch(:root_key, self.class.root_key)
|
70
|
+
@api_version = opts.fetch(:api_version, self.class.api_version)
|
71
|
+
@params = opts[:params] || {}
|
72
|
+
end
|
73
|
+
|
74
|
+
def with_object(object)
|
75
|
+
@object = object
|
76
|
+
self
|
77
|
+
end
|
78
|
+
|
79
|
+
def as_json
|
80
|
+
item = {}
|
81
|
+
((requested_attributes - requested_serializer_methods) & available_attributes).each do |m|
|
82
|
+
next if m == :id && !object.respond_to?(:id)
|
83
|
+
item[m] = object.public_send(m)
|
84
|
+
end
|
85
|
+
(requested_serializer_methods & available_attributes).each do |m|
|
86
|
+
item[m] = self.public_send(m)
|
87
|
+
end
|
88
|
+
hash = { root_key => item }
|
89
|
+
hash[:_metadata] = meta if add_metadata?
|
90
|
+
hash
|
91
|
+
end
|
92
|
+
|
93
|
+
def to_json
|
94
|
+
MultiJson.dump(as_json)
|
95
|
+
end
|
96
|
+
|
97
|
+
def call(obj)
|
98
|
+
with_object(obj).as_json
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
# Things we get from options
|
104
|
+
attr_reader :object, :fields, :root_key, :api_version, :params
|
105
|
+
|
106
|
+
def scope
|
107
|
+
if @scope.is_a? Proc
|
108
|
+
@scope = @scope.call
|
109
|
+
else
|
110
|
+
@scope
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def add_metadata?
|
115
|
+
@add_metadata
|
116
|
+
end
|
117
|
+
|
118
|
+
delegate :all_attributes, :serializer_methods, to: :'self.class'
|
119
|
+
|
120
|
+
def requested_serializer_methods
|
121
|
+
@requested_serializer_methods ||= serializer_methods & requested_attributes
|
122
|
+
end
|
123
|
+
|
124
|
+
##
|
125
|
+
# List of attributes requested to be shown.
|
126
|
+
# This is frequently done by :fields HTTP request
|
127
|
+
# parameter
|
128
|
+
#
|
129
|
+
def requested_attributes
|
130
|
+
@requested_attributes ||= begin
|
131
|
+
case fields
|
132
|
+
when NilClass then all_attributes
|
133
|
+
when Array then fields.map(&:to_sym) | [:id]
|
134
|
+
when Symbol then [fields]
|
135
|
+
when String then [fields.to_sym]
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
##
|
141
|
+
# List of attributes available to be shown
|
142
|
+
# by current security context.
|
143
|
+
#
|
144
|
+
# Can be re-defined in inherited class
|
145
|
+
#
|
146
|
+
alias_method :available_attributes, :all_attributes
|
147
|
+
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
module Fars::ModelSerializable
|
2
|
+
def serialize(opts = {})
|
3
|
+
api_prefix = opts[:api_version] + '::' if opts[:api_version]
|
4
|
+
serializer_class = (opts[:serializer] || "#{api_prefix}#{self.class.base_class}Serializer").constantize
|
5
|
+
serializer_class.new(self, opts).to_json
|
6
|
+
end
|
7
|
+
end
|
data/lib/fars/version.rb
CHANGED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'ActiveRecord::Base#serialize' do
|
4
|
+
subject { Master.create(id: 1, name: 'Object1', data: '123') }
|
5
|
+
|
6
|
+
specify 'with default options without metadata' do
|
7
|
+
json_data = { master: { id: 1, name: 'Object1', data: '123', slaves: [] } }.to_json
|
8
|
+
subject.serialize(add_metadata: false).should == json_data
|
9
|
+
end
|
10
|
+
|
11
|
+
specify 'with custom item serializer class' do
|
12
|
+
json_data = { father: { id: 1, name: 'Object1', number: 14, slaves: [] } }.to_json
|
13
|
+
subject.serialize(serializer: 'AnotherMasterSerializer').should == json_data
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'ActiveRecord::Relation#serialize' do
|
4
|
+
before(:each) do
|
5
|
+
1.upto(2) { |i| Master.create(id: i, name: "Object#{i}", data: '123') }
|
6
|
+
1.upto(3) { |i| Slave.create(id: i, master_id: (i % 2) + 1, name: "Slave #{i}") }
|
7
|
+
end
|
8
|
+
subject { Master.where('1 = 1').order(:id) }
|
9
|
+
|
10
|
+
specify 'add metadata for collection' do
|
11
|
+
json_data = { masters: [{ master: { name: 'Object1' } }, { master: { name: 'Object2' } }],
|
12
|
+
_metadata: { order: :id } }.to_json
|
13
|
+
|
14
|
+
subject.serialize(fields: :name, add_metadata: false, metadata: { order: :id }).should == json_data
|
15
|
+
end
|
16
|
+
end
|