active_facets 1.2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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