active_facets 1.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE.txt +21 -0
- data/README.md +206 -0
- data/Rakefile +29 -0
- data/lib/active_facets.rb +93 -0
- data/lib/active_facets/acts_as_active_facet.rb +131 -0
- data/lib/active_facets/config.rb +263 -0
- data/lib/active_facets/document_cache.rb +52 -0
- data/lib/active_facets/errors/attribute_error.rb +6 -0
- data/lib/active_facets/errors/configuration_error.rb +14 -0
- data/lib/active_facets/errors/lookup_error.rb +6 -0
- data/lib/active_facets/resource_manager.rb +124 -0
- data/lib/active_facets/serializer/base.rb +291 -0
- data/lib/active_facets/serializer/facade.rb +223 -0
- data/lib/active_facets/version.rb +3 -0
- data/lib/rails/generators/active_facets/install/install_generator.rb +21 -0
- data/lib/rails/generators/active_facets/install/templates/active_facets.yml +17 -0
- data/lib/rails/generators/active_facets/install/templates/initializer.rb +82 -0
- data/lib/tasks/active_facets_tasks.rake +4 -0
- metadata +317 -0
@@ -0,0 +1,263 @@
|
|
1
|
+
# Field = Symbol representing a json attribute that corresponds to resource attributes or extensions
|
2
|
+
|
3
|
+
# Field Set = Nested, Mixed Collection of Fields, Aliases, and Relations (Strings, Symbols, Arrays and Hashes)
|
4
|
+
# e.g. [:a, {b: "c"}]
|
5
|
+
|
6
|
+
# Normalized Field Set = Field Set with all Strings converted to Symbols, Aliases dealiased, and Arrays converted to Hashes
|
7
|
+
|
8
|
+
# Field Set Alias = Symbol representing a Field Set
|
9
|
+
|
10
|
+
module ActiveFacets
|
11
|
+
class Config
|
12
|
+
|
13
|
+
#TODO --decouple this class completely from serializer by moving reflection to resource_manager
|
14
|
+
|
15
|
+
# Boolean: state
|
16
|
+
attr_reader :compiled
|
17
|
+
|
18
|
+
# Serializer::Base
|
19
|
+
attr_reader :serializer
|
20
|
+
|
21
|
+
# Hash: compiled field sets
|
22
|
+
attr_reader :normalized_field_sets
|
23
|
+
|
24
|
+
# Hash: keys are public API attribute names, values are resource attribute names
|
25
|
+
attr_reader :transforms_from, :transforms_to
|
26
|
+
|
27
|
+
# Hash: API attribute names requiring custom serialization
|
28
|
+
attr_reader :serializers
|
29
|
+
|
30
|
+
# Hash: keys are resource attribute names storing nested JSON, values are nested attribute names
|
31
|
+
attr_reader :namespaces
|
32
|
+
|
33
|
+
# Hash: keys are defined extension values
|
34
|
+
attr_reader :extensions
|
35
|
+
|
36
|
+
# Class: Resource Class to serialize
|
37
|
+
attr_accessor :resource_class
|
38
|
+
|
39
|
+
def alias_field_set(field_set_alias, field_set)
|
40
|
+
self.compiled = false
|
41
|
+
field_sets[field_set_alias] = field_set
|
42
|
+
end
|
43
|
+
|
44
|
+
# Returns Field to resource attribute map
|
45
|
+
# @param direction [Symbol]
|
46
|
+
# @return [Hash]
|
47
|
+
def transforms(direction = :from)
|
48
|
+
direction == :from ? transforms_from : transforms_to
|
49
|
+
end
|
50
|
+
|
51
|
+
# (Memoized) Normalizes all Field Set Aliases
|
52
|
+
# @param serializer [Serializer::Base]
|
53
|
+
# @return [Config]
|
54
|
+
def compile!(serializer)
|
55
|
+
self.serializer = serializer
|
56
|
+
self.normalized_field_sets = { all: {} }.with_indifferent_access
|
57
|
+
|
58
|
+
#aggregate all compiled field_sets into the all collection
|
59
|
+
normalized_field_sets[:all][:fields] = field_sets.inject({}) do |result, (field_set_alias, field_set)|
|
60
|
+
result = merge_field_sets(result, dealias_field_set!(field_set, field_set_alias)[:fields])
|
61
|
+
end
|
62
|
+
|
63
|
+
#filter all compiled field_sets into a corresponding attributes collection
|
64
|
+
normalized_field_sets.each do |field_set_alias, normalized_field_set|
|
65
|
+
normalized_field_set[:attributes] = normalized_field_set[:fields].reject { |field_set, nested_field_sets|
|
66
|
+
serializer.send :is_association?, field_set
|
67
|
+
}
|
68
|
+
end
|
69
|
+
|
70
|
+
self.compiled = true
|
71
|
+
self
|
72
|
+
end
|
73
|
+
|
74
|
+
# Merges all ancestor accessors into self
|
75
|
+
# @return [Config]
|
76
|
+
def merge!(config)
|
77
|
+
self.compiled = false
|
78
|
+
self.resource_class ||= config.resource_class
|
79
|
+
transforms_from.merge! config.transforms_from
|
80
|
+
transforms_to.merge! config.transforms_to
|
81
|
+
serializers.merge! config.serializers
|
82
|
+
namespaces.merge! config.namespaces
|
83
|
+
field_sets.merge! config.field_sets
|
84
|
+
extensions.merge! config.extensions
|
85
|
+
|
86
|
+
self
|
87
|
+
end
|
88
|
+
|
89
|
+
# Invokes block on a Field Set with recursive, depth first traversal
|
90
|
+
# @param field_set [Field Set] to traverse
|
91
|
+
# @param block [Block] to call for each field
|
92
|
+
# @return [Hash] injection of block results
|
93
|
+
def field_set_itterator(field_set)
|
94
|
+
raise ActiveFacets::Errors::ConfigurationError.new(ActiveFacets::Errors::ConfigurationError::COMPILED_ERROR_MSG) unless compiled
|
95
|
+
internal_field_set_itterator(dealias_field_set!(default_field_set(field_set))[:fields], Proc.new)
|
96
|
+
end
|
97
|
+
|
98
|
+
protected
|
99
|
+
|
100
|
+
attr_accessor :field_sets
|
101
|
+
|
102
|
+
attr_writer :compiled, :serializer, :normalized_field_sets, :transforms_from, :transforms_to,
|
103
|
+
:serializers, :namespaces, :extensions
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
#TODO --jdc change Serializer::Base to convert all Strings to Symbols and remove indifferent_access
|
108
|
+
def initialize
|
109
|
+
self.compiled = false
|
110
|
+
self.transforms_from = {}.with_indifferent_access
|
111
|
+
self.transforms_to = {}.with_indifferent_access
|
112
|
+
self.serializers = {}.with_indifferent_access
|
113
|
+
self.namespaces = {}.with_indifferent_access
|
114
|
+
self.field_sets = {}.with_indifferent_access
|
115
|
+
self.extensions = {}.with_indifferent_access
|
116
|
+
end
|
117
|
+
|
118
|
+
# (Memoized) Convert all Field Set Aliases to their declarations and Normalize Field Set
|
119
|
+
# @param field_set [Symbol] to evaluate
|
120
|
+
# @param field_set_alias [String] key to associate the evaluated field set with
|
121
|
+
# @return [Normalized Field Set]
|
122
|
+
def dealias_field_set!(field_set, field_set_alias = nil)
|
123
|
+
field_set_alias ||= field_set.to_s.to_sym
|
124
|
+
normalized_field_sets[field_set_alias] ||= begin
|
125
|
+
{ fields: normalize_field_set(dealias_field_set field_set) }
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# Converts all Field Set Aliases in a Field Set into their declarations (see Serializer::Base DSL)
|
130
|
+
# Recursively evaluates all aliases embedded within declaration
|
131
|
+
# - Does not recursively evalute associations
|
132
|
+
# @param field_set [Symbol] to evaluate
|
133
|
+
# @return [Mixed]
|
134
|
+
def dealias_field_set(field_set)
|
135
|
+
case field_set
|
136
|
+
when :all
|
137
|
+
dealias_field_set serializer.exposed_aliases(:all, true, true)
|
138
|
+
when :all_attributes
|
139
|
+
dealias_field_set serializer.exposed_aliases
|
140
|
+
when Symbol, String
|
141
|
+
field_set = field_set.to_sym
|
142
|
+
aliased_field_set?(field_set) ? dealias_field_set(field_sets[field_set]) : field_set
|
143
|
+
when Array
|
144
|
+
field_set.map do |s|
|
145
|
+
dealias_field_set(s)
|
146
|
+
end
|
147
|
+
when Hash
|
148
|
+
field_set.inject({}) { |result, (k,v)|
|
149
|
+
v.blank? ? inject_field_set(result, dealias_field_set(k)) : result[k] = v #todo: symbolize
|
150
|
+
result
|
151
|
+
}
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# Converts Field Set into a Normalized Field Set that can be idempotently itterated
|
156
|
+
# @param field_set [Symbol] to normalize
|
157
|
+
# @return [Normalized Field Set]
|
158
|
+
def normalize_field_set(field_set)
|
159
|
+
case field_set
|
160
|
+
when nil
|
161
|
+
{}
|
162
|
+
when Symbol, String
|
163
|
+
{field_set.to_sym => nil}
|
164
|
+
when Array
|
165
|
+
field_set.flatten.compact.inject({}) do |result, s|
|
166
|
+
result = merge_field_sets(result, s)
|
167
|
+
end
|
168
|
+
when Hash
|
169
|
+
field_set.inject({}) { |result, (k,v)| result[k.to_sym] = v; result }
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
# Adds :basic to a Field Set unless minimal is specified
|
174
|
+
# @param field_set [Field Set] field set to be serialized
|
175
|
+
# @return [Field Set]
|
176
|
+
def default_field_set(field_set)
|
177
|
+
minimal = detect_field_set(field_set, :minimal)
|
178
|
+
case field_set
|
179
|
+
when nil
|
180
|
+
:basic
|
181
|
+
when Symbol, String
|
182
|
+
minimal ? field_set.to_sym : [field_set.to_sym, :basic]
|
183
|
+
when Array
|
184
|
+
minimal ? field_set : field_set + [:basic]
|
185
|
+
when Hash
|
186
|
+
field_set[:basic] = nil unless minimal
|
187
|
+
field_set
|
188
|
+
else
|
189
|
+
raise ActiveFacets::Errors::ConfigurationError.new(ActiveFacets::Errors::ConfigurationError::FIELD_SET_ERROR_MSG)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
# Iterrates the first level of Field Set checking for key
|
194
|
+
# @param field_set [Field Set]
|
195
|
+
# @return [Boolean]
|
196
|
+
def detect_field_set(field_set, key)
|
197
|
+
case field_set
|
198
|
+
when nil
|
199
|
+
false
|
200
|
+
when Symbol
|
201
|
+
field_set == key
|
202
|
+
when String
|
203
|
+
field_set.to_sym == key
|
204
|
+
when Array
|
205
|
+
field_set.detect { |s| detect_field_set(s, key) }
|
206
|
+
when Hash
|
207
|
+
field_set.detect { |s, n| detect_field_set(s, key) }.try(:[], 0)
|
208
|
+
else
|
209
|
+
raise ActiveFacets::Errors::ConfigurationError.new(ActiveFacets::Errors::ConfigurationError::FIELD_SET_ERROR_MSG)
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
# Invokes block on Fields in a Field Set with recursive, depth first traversal
|
214
|
+
# Skips fields already processed
|
215
|
+
# @param field_set [Field Set] to traverse
|
216
|
+
# @param block [Block] to call for each field_set
|
217
|
+
# @return [Hash] injection of block results
|
218
|
+
def internal_field_set_itterator(field_set, block)
|
219
|
+
field_set.each do |field, nested_field_set|
|
220
|
+
block.call(field, nested_field_set)
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
# Adds a Field into a Normalized Field Set
|
225
|
+
# @param field_set [Normalized Field Set]
|
226
|
+
# @param key [Field Set]
|
227
|
+
# @return [Hash]
|
228
|
+
def inject_field_set(field_set, key)
|
229
|
+
case key
|
230
|
+
when Symbol, String
|
231
|
+
field_set[key.to_sym] = {}
|
232
|
+
when Hash
|
233
|
+
field_set.merge! key
|
234
|
+
when Array
|
235
|
+
key.each { |k| inject_field_set(field_set, k) }
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
# Tells if the Field is a Field Set Alias
|
240
|
+
# @param field_set [Symbol] to evaluate
|
241
|
+
# @return [Boolean]
|
242
|
+
def aliased_field_set?(field_set)
|
243
|
+
return false unless field_sets.key? field_set
|
244
|
+
v = field_sets[field_set]
|
245
|
+
!v.is_a?(Symbol) || v != field_set
|
246
|
+
end
|
247
|
+
|
248
|
+
# Recursively merges two Field Sets
|
249
|
+
# @param a [Symbol] to merge
|
250
|
+
# @param b [Symbol] to merge
|
251
|
+
# @return [Field Set]
|
252
|
+
def merge_field_sets(a, b)
|
253
|
+
na = normalize_field_set(a)
|
254
|
+
nb = normalize_field_set(b)
|
255
|
+
binding.pry unless nb
|
256
|
+
nb.inject(na.dup) do |result, (field_set, nested_field_sets)|
|
257
|
+
result[field_set] = merge_field_sets(na[field_set], nested_field_sets)
|
258
|
+
result
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
end
|
263
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# This is a really dump cache interface that caches everything
|
2
|
+
# Extend with custom class that conditionally caches and stitches independent documents
|
3
|
+
module ActiveFacets
|
4
|
+
class DocumentCache
|
5
|
+
CACHE_PREFIX = 'af_doc_cache'
|
6
|
+
|
7
|
+
# Fetches a JSON document representing the facade
|
8
|
+
# @param facade [Object] to cache
|
9
|
+
# @param options [Hash] for Rails.cache.fetch
|
10
|
+
# @param &block [Proc] for cache miss
|
11
|
+
# @return [Object]
|
12
|
+
def self.fetch(facade, options = {})
|
13
|
+
return yield unless cacheable?(facade)
|
14
|
+
|
15
|
+
force = facade.opts[ActiveFacets.cache_force_key] || options[:force] || ActiveFacets::default_cache_options[:force]
|
16
|
+
cache_key = digest_key(facade)
|
17
|
+
if force || !(result = Rails.cache.fetch(cache_key))
|
18
|
+
result = yield
|
19
|
+
Rails.cache.write(cache_key, ::Oj.dump(result), ActiveFacets::default_cache_options.merge(options).merge(force: force))
|
20
|
+
result
|
21
|
+
else
|
22
|
+
::Oj.load(result)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Fetches a JSON document representing the association specified for the resource in the facade
|
27
|
+
# @param facade [Object] to cache
|
28
|
+
# @param options [Hash] for Rails.cache.fetch
|
29
|
+
# @param &block [Proc] for cache miss
|
30
|
+
# @return [Object]
|
31
|
+
def self.fetch_association(facade, association, options = {})
|
32
|
+
#TODO --jdc implement
|
33
|
+
yield
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
# Salts and hashes facade cache_key
|
39
|
+
# @param facade [Facade] to generate key for
|
40
|
+
# @return [String]
|
41
|
+
def self.digest_key(facade)
|
42
|
+
Digest::MD5.hexdigest(CACHE_PREFIX + facade.cache_key.to_s)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Tells if the resource to be serialized can be cached
|
46
|
+
# @param facade [Facade] to inspect
|
47
|
+
# @return [Boolean]
|
48
|
+
def self.cacheable?(facade)
|
49
|
+
ActiveFacets.cache_enabled
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module ActiveFacets
|
2
|
+
module Errors
|
3
|
+
class ConfigurationError < StandardError
|
4
|
+
|
5
|
+
RESOURCE_ERROR_MSG = 'unable to identify resource class'
|
6
|
+
STACK_ERROR_MSG = "self referencing attribute declaration"
|
7
|
+
ALL_ATTRIBUTES_ERROR_MSG = "publish name (:all_attributes) reserved"
|
8
|
+
ALL_FIELDS_ERROR_MSG = "publish name (:all) reserved"
|
9
|
+
COMPILED_ERROR_MSG = "field set configuration not compiled"
|
10
|
+
FIELD_SET_ERROR_MSG = "invalid field set"
|
11
|
+
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
#TODO --jdc rebuild this class to either not use an explicit singleton pattern or use a factory pattern
|
2
|
+
module ActiveFacets
|
3
|
+
class ResourceManager
|
4
|
+
|
5
|
+
cattr_accessor :resource_mapper, :serializer_mapper
|
6
|
+
|
7
|
+
# Default resource mapping scheme, can be overrided with config
|
8
|
+
def self.default_resource_mapper(resource_class)
|
9
|
+
[].tap do |map|
|
10
|
+
until(resource_class.superclass == BasicObject) do
|
11
|
+
map << resource_class.name.tableize
|
12
|
+
resource_class = resource_class.superclass
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
self.resource_mapper = method(:default_resource_mapper)
|
17
|
+
|
18
|
+
# TODO --jdc implement recursive superclass/parentclass lookup
|
19
|
+
# Default serializer mapping scheme, can be overrided with config
|
20
|
+
def self.default_serializer_mapper(resource_class, serializer, type, version, options)
|
21
|
+
case type
|
22
|
+
when :serializer
|
23
|
+
[
|
24
|
+
'V' + version.to_i.to_s + '::' + resource_class.name.camelcase + '::' + resource_class.name.camelcase + type.to_s.camelcase,
|
25
|
+
'V' + version.to_i.to_s + '::' + resource_class.name.camelcase + type.to_s.camelcase,
|
26
|
+
].each { |name|
|
27
|
+
klass = name.safe_constantize
|
28
|
+
return klass.new if klass.present?
|
29
|
+
}
|
30
|
+
else
|
31
|
+
[
|
32
|
+
'V' + version.to_i.to_s + '::' + resource_class.name.camelcase + '::' + serializer + type.to_s.camelcase,
|
33
|
+
'V' + version.to_i.to_s + '::' + serializer + type.to_s.camelcase,
|
34
|
+
].find { |name|
|
35
|
+
klass = name.safe_constantize
|
36
|
+
return klass if klass.present?
|
37
|
+
}
|
38
|
+
end
|
39
|
+
end
|
40
|
+
self.serializer_mapper = method(:default_serializer_mapper)
|
41
|
+
|
42
|
+
# Singleton
|
43
|
+
# @return [ResourceManager]
|
44
|
+
def self.instance
|
45
|
+
@instance ||= new
|
46
|
+
end
|
47
|
+
|
48
|
+
# (Memoized) Associate a serializer with a resource_class
|
49
|
+
# @param resource_class [Object]
|
50
|
+
# @param serializer [Serializer::Base]
|
51
|
+
# @param namespace [String] (TODO --jdc currently unused)
|
52
|
+
# @return [Array]
|
53
|
+
def register(resource_class, serializer, namespace = nil)
|
54
|
+
registry[resource_class] = [serializer, namespace]
|
55
|
+
end
|
56
|
+
|
57
|
+
# Fetches the serializer registered for the resource_class
|
58
|
+
# @param resource_class [Object] to find serializer for
|
59
|
+
# @param options [Hash] context
|
60
|
+
# @return [Serializer::Base]
|
61
|
+
def serializer_for(resource_class, options)
|
62
|
+
fetch_serializer(resource_class, resource_class.name.demodulize.to_s.camelcase, :serializer, options)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Fetches the attribute serializer registered for the given resource_class
|
66
|
+
# @param resource_class [Object] to find attribute serializer class for
|
67
|
+
# @param attribute_class_name [String] to find attribute serializer class for
|
68
|
+
# @param options [Hash] context
|
69
|
+
# @return [AttributeSerializer::Base]
|
70
|
+
def attribute_serializer_class_for(resource_class, attribute_name, options)
|
71
|
+
fetch_serializer(resource_class, attribute_name.to_s.camelcase, :attribute_serializer, options)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Fetches the resource class registered for the serializer
|
75
|
+
# @param serializer [Serializer::Base] to find resource class for
|
76
|
+
# @return [Object]
|
77
|
+
def resource_class_for(serializer)
|
78
|
+
registry.each_pair do |resource_class, entry|
|
79
|
+
return resource_class if serializer == entry[0]
|
80
|
+
end
|
81
|
+
nil
|
82
|
+
end
|
83
|
+
|
84
|
+
# Fetches the set of filter and field override indexes for resource_class
|
85
|
+
# @param resource_class [Object]
|
86
|
+
# @return [Array] of string indexes
|
87
|
+
def resource_map(resource_class)
|
88
|
+
memoized_resource_map[resource_class] ||= begin
|
89
|
+
self.class.resource_mapper.call(resource_class)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def extract_version_from_opts(options)
|
94
|
+
((options.try(:[], ActiveFacets.opts_key) || {})[ActiveFacets.version_key] || ActiveFacets.default_version).to_f
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
attr_accessor :registry, :memoized_serializers, :memoized_resource_map
|
100
|
+
|
101
|
+
# @return [ResourceManager]
|
102
|
+
def initialize
|
103
|
+
self.registry = {}
|
104
|
+
self.memoized_serializers = {}
|
105
|
+
self.memoized_resource_map = {}
|
106
|
+
end
|
107
|
+
|
108
|
+
# Retrieves serializer class from memory or lookup
|
109
|
+
# @param resource_class [Class] the class of the resource to serialize
|
110
|
+
# @param serializer [String] name of the base_class of the resource to serialize
|
111
|
+
# @param type [String] type of serializer to look for (attribute vs. basic, etc.)
|
112
|
+
# @param options [Hash] context
|
113
|
+
# @return [Class] the first Class successfully described
|
114
|
+
def fetch_serializer(resource_class, serializer, type, options)
|
115
|
+
version = extract_version_from_opts(options)
|
116
|
+
unless result = self.class.serializer_mapper.call(resource_class, serializer, type, version, options)
|
117
|
+
error_message = "Unable to locate serializer for:: " + [resource_class.name, serializer, type, version].to_s
|
118
|
+
Rails.logger.debug error_message
|
119
|
+
raise ActiveFacets::Errors::LookupError.new(error_message) if ActiveFacets.strict_lookups
|
120
|
+
end
|
121
|
+
result
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|