active_facet 1.2.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,267 @@
1
+ #TODO --jdc rename field set to facet throughut project
2
+
3
+ # Field = Symbol representing a json attribute that corresponds to resource attributes or extensions
4
+
5
+ # Field Set = Nested, Mixed Collection of Fields, Aliases, and Relations (Strings, Symbols, Arrays and Hashes)
6
+ # e.g. [:a, {b: "c"}]
7
+
8
+ # Normalized Field Set = Field Set with all Strings converted to Symbols, Aliases dealiased, and Arrays converted to Hashes
9
+
10
+ # Field Set Alias = Symbol representing a Field Set
11
+
12
+ module ActiveFacet
13
+ class Config
14
+
15
+ #TODO --decouple this class completely from serializer by moving reflection to resource_manager
16
+
17
+ # Boolean: state
18
+ attr_reader :compiled
19
+
20
+ # Serializer::Base
21
+ attr_reader :serializer
22
+
23
+ # Hash: compiled field sets
24
+ attr_reader :normalized_field_sets
25
+
26
+ # Hash: keys are public API attribute names, values are resource attribute names
27
+ attr_reader :transforms_from, :transforms_to
28
+
29
+ # Hash: API attribute names requiring custom serialization
30
+ attr_reader :serializers
31
+
32
+ # Hash: keys are resource attribute names storing nested JSON, values are nested attribute names
33
+ attr_reader :namespaces
34
+
35
+ # Hash: keys are defined extension values
36
+ attr_reader :extensions
37
+
38
+ # Class: Resource Class to serialize
39
+ attr_accessor :resource_class
40
+
41
+ # Store Facet
42
+ # @param field_set_alias [Symbol]
43
+ # @param field_set_alias [Facet]
44
+ def alias_field_set(field_set_alias, field_set)
45
+ self.compiled = false
46
+ field_sets[field_set_alias] = field_set
47
+ end
48
+
49
+ # Returns Field to resource attribute map
50
+ # @param direction [Symbol]
51
+ # @return [Hash]
52
+ def transforms(direction = :from)
53
+ direction == :from ? transforms_from : transforms_to
54
+ end
55
+
56
+ # (Memoized) Normalizes all Field Set Aliases
57
+ # @param serializer [Serializer::Base]
58
+ # @return [Config]
59
+ def compile!(serializer)
60
+ self.serializer = serializer
61
+ self.normalized_field_sets = { all: {} }.with_indifferent_access
62
+
63
+ #aggregate all compiled field_sets into the all collection
64
+ normalized_field_sets[:all][:fields] = field_sets.inject({}) do |result, (field_set_alias, field_set)|
65
+ result = merge_field_sets(result, dealias_field_set!(field_set, field_set_alias)[:fields])
66
+ end
67
+
68
+ #filter all compiled field_sets into a corresponding attributes collection
69
+ normalized_field_sets.each do |field_set_alias, normalized_field_set|
70
+ normalized_field_set[:attributes] = normalized_field_set[:fields].reject { |field_set, nested_field_sets|
71
+ serializer.send :is_association?, field_set
72
+ }
73
+ end
74
+
75
+ self.compiled = true
76
+ self
77
+ end
78
+
79
+ # Merges all ancestor accessors into self
80
+ # @return [Config]
81
+ def merge!(config)
82
+ self.compiled = false
83
+ self.resource_class ||= config.resource_class
84
+ transforms_from.merge! config.transforms_from
85
+ transforms_to.merge! config.transforms_to
86
+ serializers.merge! config.serializers
87
+ namespaces.merge! config.namespaces
88
+ field_sets.merge! config.field_sets
89
+ extensions.merge! config.extensions
90
+
91
+ self
92
+ end
93
+
94
+ # Invokes block on a Field Set with recursive, depth first traversal
95
+ # @param field_set [Field Set] to traverse
96
+ # @param block [Block] to call for each field
97
+ # @return [Hash] injection of block results
98
+ def field_set_itterator(field_set)
99
+ raise ActiveFacet::Errors::ConfigurationError.new(ActiveFacet::Errors::ConfigurationError::COMPILED_ERROR_MSG) unless compiled
100
+ internal_field_set_itterator(dealias_field_set!(default_field_set(field_set))[:fields], Proc.new)
101
+ end
102
+
103
+ protected
104
+
105
+ attr_accessor :field_sets
106
+
107
+ attr_writer :compiled, :serializer, :normalized_field_sets, :transforms_from, :transforms_to,
108
+ :serializers, :namespaces, :extensions
109
+
110
+ private
111
+
112
+ #TODO --jdc change Serializer::Base to convert all Strings to Symbols and remove indifferent_access
113
+ def initialize
114
+ self.compiled = false
115
+ self.transforms_from = {}.with_indifferent_access
116
+ self.transforms_to = {}.with_indifferent_access
117
+ self.serializers = {}.with_indifferent_access
118
+ self.namespaces = {}.with_indifferent_access
119
+ self.field_sets = {}.with_indifferent_access
120
+ self.extensions = {}.with_indifferent_access
121
+ end
122
+
123
+ # (Memoized) Convert all Field Set Aliases to their declarations and Normalize Field Set
124
+ # @param field_set [Symbol] to evaluate
125
+ # @param field_set_alias [String] key to associate the evaluated field set with
126
+ # @return [Normalized Field Set]
127
+ def dealias_field_set!(field_set, field_set_alias = nil)
128
+ field_set_alias ||= field_set.to_s.to_sym
129
+ normalized_field_sets[field_set_alias] ||= begin
130
+ { fields: normalize_field_set(dealias_field_set field_set) }
131
+ end
132
+ end
133
+
134
+ # Converts all Field Set Aliases in a Field Set into their declarations (see Serializer::Base DSL)
135
+ # Recursively evaluates all aliases embedded within declaration
136
+ # - Does not recursively evalute associations
137
+ # @param field_set [Symbol] to evaluate
138
+ # @return [Mixed]
139
+ def dealias_field_set(field_set)
140
+ case field_set
141
+ when :all
142
+ dealias_field_set serializer.exposed_aliases(:all, true, true)
143
+ when :all_attributes
144
+ dealias_field_set serializer.exposed_aliases
145
+ when Symbol, String
146
+ field_set = field_set.to_sym
147
+ aliased_field_set?(field_set) ? dealias_field_set(field_sets[field_set]) : field_set
148
+ when Array
149
+ field_set.map do |s|
150
+ dealias_field_set(s)
151
+ end
152
+ when Hash
153
+ field_set.inject({}) { |result, (k,v)|
154
+ v.blank? ? inject_field_set(result, dealias_field_set(k)) : result[k] = v #todo: symbolize
155
+ result
156
+ }
157
+ end
158
+ end
159
+
160
+ # Converts Field Set into a Normalized Field Set that can be idempotently itterated
161
+ # @param field_set [Symbol] to normalize
162
+ # @return [Normalized Field Set]
163
+ def normalize_field_set(field_set)
164
+ case field_set
165
+ when nil
166
+ {}
167
+ when Symbol, String
168
+ {field_set.to_sym => nil}
169
+ when Array
170
+ field_set.flatten.compact.inject({}) do |result, s|
171
+ result = merge_field_sets(result, s)
172
+ end
173
+ when Hash
174
+ field_set.inject({}) { |result, (k,v)| result[k.to_sym] = v; result }
175
+ end
176
+ end
177
+
178
+ # Adds :basic to a Field Set unless minimal is specified
179
+ # @param field_set [Field Set] field set to be serialized
180
+ # @return [Field Set]
181
+ def default_field_set(field_set)
182
+ minimal = detect_field_set(field_set, :minimal)
183
+ case field_set
184
+ when nil
185
+ :basic
186
+ when Symbol, String
187
+ minimal ? field_set.to_sym : [field_set.to_sym, :basic]
188
+ when Array
189
+ minimal ? field_set : field_set + [:basic]
190
+ when Hash
191
+ field_set[:basic] = nil unless minimal
192
+ field_set
193
+ else
194
+ raise ActiveFacet::Errors::ConfigurationError.new(ActiveFacet::Errors::ConfigurationError::FIELD_SET_ERROR_MSG)
195
+ end
196
+ end
197
+
198
+ # Iterrates the first level of Field Set checking for key
199
+ # @param field_set [Field Set]
200
+ # @return [Boolean]
201
+ def detect_field_set(field_set, key)
202
+ case field_set
203
+ when nil
204
+ false
205
+ when Symbol
206
+ field_set == key
207
+ when String
208
+ field_set.to_sym == key
209
+ when Array
210
+ field_set.detect { |s| detect_field_set(s, key) }
211
+ when Hash
212
+ field_set.detect { |s, n| detect_field_set(s, key) }.try(:[], 0)
213
+ else
214
+ raise ActiveFacet::Errors::ConfigurationError.new(ActiveFacet::Errors::ConfigurationError::FIELD_SET_ERROR_MSG)
215
+ end
216
+ end
217
+
218
+ # Invokes block on Fields in a Field Set with recursive, depth first traversal
219
+ # Skips fields already processed
220
+ # @param field_set [Field Set] to traverse
221
+ # @param block [Block] to call for each field_set
222
+ # @return [Hash] injection of block results
223
+ def internal_field_set_itterator(field_set, block)
224
+ field_set.each do |field, nested_field_set|
225
+ block.call(field, nested_field_set)
226
+ end
227
+ end
228
+
229
+ # Adds a Field into a Normalized Field Set
230
+ # @param field_set [Normalized Field Set]
231
+ # @param key [Field Set]
232
+ # @return [Hash]
233
+ def inject_field_set(field_set, key)
234
+ case key
235
+ when Symbol, String
236
+ field_set[key.to_sym] = {}
237
+ when Hash
238
+ field_set.merge! key
239
+ when Array
240
+ key.each { |k| inject_field_set(field_set, k) }
241
+ end
242
+ end
243
+
244
+ # Tells if the Field is a Field Set Alias
245
+ # @param field_set [Symbol] to evaluate
246
+ # @return [Boolean]
247
+ def aliased_field_set?(field_set)
248
+ return false unless field_sets.key? field_set
249
+ v = field_sets[field_set]
250
+ !v.is_a?(Symbol) || v != field_set
251
+ end
252
+
253
+ # Recursively merges two Field Sets
254
+ # @param a [Symbol] to merge
255
+ # @param b [Symbol] to merge
256
+ # @return [Field Set]
257
+ def merge_field_sets(a, b)
258
+ na = normalize_field_set(a)
259
+ nb = normalize_field_set(b)
260
+ nb.inject(na.dup) do |result, (field_set, nested_field_sets)|
261
+ result[field_set] = merge_field_sets(na[field_set], nested_field_sets)
262
+ result
263
+ end
264
+ end
265
+
266
+ end
267
+ end
@@ -0,0 +1,52 @@
1
+ # A really dump cache interface that caches everything when enabled.
2
+ # Extend and conditionally cache documents.
3
+ module ActiveFacet
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[ActiveFacet.cache_force_key] || options[:force] || ActiveFacet::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), ActiveFacet::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
+ # override and 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
+ ActiveFacet.cache_enabled && facade.opts[ActiveFacet.cache_bypass_key].blank?
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,6 @@
1
+ module ActiveFacet
2
+ module Errors
3
+ class AttributeError < StandardError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,16 @@
1
+ module ActiveFacet
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
+ ACTS_AS_ERROR_MSG = "filters can only be defined on acts_as_active_facet resources"
12
+ DUPLICATE_ACTS_AS_ERROR_MSG = "acts_as_active_facet_options already exists"
13
+
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,6 @@
1
+ module ActiveFacet
2
+ module Errors
3
+ class LookupError < StandardError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,62 @@
1
+ # Maps filters to resource classes without adding methods to the resource classes directly
2
+ module ActiveFacet
3
+ class Filter
4
+
5
+ # filters holds the resource/filter map for each resource
6
+ # registered_filters holds the resource/filter map for each resource and all superclasses
7
+ # global_filters queues a collection of filters that can be defined
8
+ # on any resource classes having called included and called acts_as_active_facet_options
9
+ cattr_accessor :filters, :registered_filters, :global_filters
10
+ self.filters, self.registered_filters, self.global_filters = {}, {}, {}
11
+
12
+ # Queues a filter to be applied to resource classes
13
+ # @param filter_name [Symbol]
14
+ # @param filter_method [Proc] method body code which implements the filter
15
+ # @return [Proc] for chaining
16
+ def self.register_global(filter_name, filter_method)
17
+ global_filters[filter_name.to_sym] = filter_method
18
+ end
19
+
20
+ # Tells that the receiver class implements a filter
21
+ # @param receiver [Class] resource class
22
+ # @param filter_name [Symbol]
23
+ # @param filter_method_name [Symbol] name of method defined on receiver instances which implments the filter
24
+ # @return [Class] for chaining
25
+ def self.register(receiver, filter_name, filter_method_name)
26
+ raise ActiveFacet::Errors::ConfigurationError.new(ActiveFacet::Errors::ConfigurationError::ACTS_AS_ERROR_MSG) unless filterable?(receiver)
27
+ receiver_filters = filters[receiver.name] ||= {}
28
+ receiver_filters[filter_name.to_sym] = filter_method_name.to_sym
29
+ receiver
30
+ end
31
+
32
+ # Register queued filters for given resource class
33
+ # @param receiver [Class] resource class
34
+ # @return [Class] for chaining
35
+ def self.apply_globals_to(receiver)
36
+ raise ActiveFacet::Errors::ConfigurationError.new(ActiveFacet::Errors::ConfigurationError::ACTS_AS_ERROR_MSG) unless filterable?(receiver)
37
+ global_filters.each do |filter_name, filter_method|
38
+ filter_method_name = receiver.acts_as_active_facet_options[:filter_method_name]
39
+ receiver.send(filter_method_name, filter_name, filter_method)
40
+ end
41
+ receiver
42
+ end
43
+
44
+ # Returns the list of filters the resource class implements
45
+ # Memoized
46
+ # @param receiver [Class] resource class
47
+ # @return [Hash] all filters registered on this resource (and superclass)
48
+ def self.registered_filters_for(receiver)
49
+ registered_filters[receiver.name] ||= begin
50
+ receiver_filters = filters[receiver.name] ||= {}
51
+ receiver_filters.reverse_merge!(registered_filters_for(receiver.superclass)) if filterable?(receiver.superclass)
52
+ receiver_filters
53
+ end
54
+ end
55
+
56
+ # Tells if any filters can be registered for the given resource class
57
+ # @return [Boolean]
58
+ def self.filterable?(receiver)
59
+ receiver.ancestors.include? ActiveFacet::ActsAsActiveFacet
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,116 @@
1
+ # Implements varius methods that bind serializers and resources together
2
+ module ActiveFacet
3
+ class Helper
4
+ # Memoizes resource_mapper results
5
+ cattr_accessor :memoized_resource_map
6
+ self.memoized_resource_map = {}
7
+
8
+ # Memoizes serializer_mapper results
9
+ cattr_accessor :memoized_serializers
10
+ self.memoized_serializers = {}
11
+
12
+ # Default resource mapping scheme, can be replaced with a custom implementation
13
+ # @param resource_class [Class]
14
+ # @return [Array] of Strings for use by filters and field configurations
15
+ def self.default_resource_mapper(resource_class)
16
+ map = []
17
+ until(resource_class.superclass == BasicObject) do
18
+ map << resource_class.name.tableize
19
+ resource_class = resource_class.superclass
20
+ end
21
+ map
22
+ end
23
+
24
+ # Holds reference to resource_mapper method (configurable)
25
+ cattr_accessor :resource_mapper
26
+ self.resource_mapper = method(:default_resource_mapper)
27
+
28
+ # Fetches the set of keys the resource_class might appear as for Filters and Fields
29
+ # Memoized
30
+ # @param resource_class [Object]
31
+ # @return [Array] of Strings
32
+ def self.resource_map(resource_class)
33
+ memoized_resource_map[resource_class] ||= begin
34
+ resource_mapper.call(resource_class)
35
+ end
36
+ end
37
+
38
+ # Default serializer mapping scheme, can be overrided with config
39
+ # Memoized
40
+ # @param resource_class [Class]
41
+ # @return [Serializer::Base | Class]
42
+ def self.default_serializer_mapper(resource_class, serializer, type, version, options)
43
+ key = [resource_class.name, serializer.to_s, type.to_s.camelcase, version].join(".")
44
+ return memoized_serializers[key] if memoized_serializers.key?(key)
45
+ memoized_serializers[key] = internal_serializer_mapper(resource_class, serializer, type, version, options)
46
+ end
47
+
48
+ # Holds reference to serializer_mapper method (configurable)
49
+ cattr_accessor :serializer_mapper
50
+ self.serializer_mapper = method(:default_serializer_mapper)
51
+
52
+ # TODO --jdc implement recursive superclass/parentclass lookup
53
+ # Default serializer mapping scheme, can be overrided with config
54
+ # @param resource_class [Class]
55
+ # @return [Serializer::Base | Class]
56
+ def self.internal_serializer_mapper(resource_class, serializer, type, version, options)
57
+ case type
58
+ when :serializer
59
+ [
60
+ 'V' + version.to_i.to_s + '::' + resource_class.name.camelcase + '::' + resource_class.name.camelcase + type.to_s.camelcase,
61
+ 'V' + version.to_i.to_s + '::' + resource_class.name.camelcase + type.to_s.camelcase,
62
+ ].each { |name|
63
+ klass = name.safe_constantize
64
+ return klass.new if klass.present?
65
+ }
66
+ else
67
+ [
68
+ 'V' + version.to_i.to_s + '::' + resource_class.name.camelcase + '::' + serializer + type.to_s.camelcase,
69
+ 'V' + version.to_i.to_s + '::' + serializer + type.to_s.camelcase,
70
+ ].find { |name|
71
+ klass = name.safe_constantize
72
+ return klass if klass.present?
73
+ }
74
+ end
75
+ end
76
+
77
+ # Fetches the serializer registered for a resource_class
78
+ # @param resource_class [Object] to find serializer for
79
+ # @param options [Hash] context
80
+ # @return [Serializer::Base]
81
+ def self.serializer_for(resource_class, options)
82
+ fetch_serializer(resource_class, resource_class.name.demodulize.to_s.camelcase, :serializer, options)
83
+ end
84
+
85
+ # Fetches the attribute serializer registered for the given resource_class
86
+ # @param resource_class [Object] to find attribute serializer class for
87
+ # @param attribute_class_name [String] to find attribute serializer class for
88
+ # @param options [Hash] context
89
+ # @return [Class]
90
+ def self.attribute_serializer_class_for(resource_class, attribute_name, options)
91
+ fetch_serializer(resource_class, attribute_name.to_s.camelcase, :attribute_serializer, options)
92
+ end
93
+
94
+ # Retrieves the first serializer successfully described by the parameters
95
+ # @param resource_class [Class] the class of the resource to serialize
96
+ # @param serializer [String] name of the base_class of the resource to serialize
97
+ # @param type [String] type of serializer to look for (attribute vs. resource)
98
+ # @param options [Hash] context
99
+ # @return [Serializer::Base | Class]
100
+ def self.fetch_serializer(resource_class, serializer, type, options)
101
+ version = extract_version_from_opts(options)
102
+ unless result = serializer_mapper.call(resource_class, serializer, type, version, options)
103
+ error_message = "Unable to locate serializer for:: " + [resource_class.name, serializer, type, version].to_s
104
+ Rails.logger.debug error_message
105
+ raise ActiveFacet::Errors::LookupError.new(error_message) if ActiveFacet.strict_lookups
106
+ end
107
+ result
108
+ end
109
+
110
+ # Safely extracts version from options hash
111
+ # @return [Numeric]
112
+ def self.extract_version_from_opts(options)
113
+ ((options.try(:[], ActiveFacet.opts_key) || {})[ActiveFacet.version_key] || ActiveFacet.default_version).to_f
114
+ end
115
+ end
116
+ end