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.
@@ -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,6 @@
1
+ module ActiveFacets
2
+ module Errors
3
+ class AttributeError < StandardError
4
+ end
5
+ end
6
+ 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,6 @@
1
+ module ActiveFacets
2
+ module Errors
3
+ class LookupError < StandardError
4
+ end
5
+ end
6
+ 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