active_facets 1.2.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 +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
|