active_facet 1.2.8

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,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