isomorphic 0.1.0

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,135 @@
1
+ require "active_support/concern"
2
+ require "active_support/core_ext/object/try"
3
+ require "active_support/hash_with_indifferent_access"
4
+
5
+ require "isomorphic/hash_with_indifferent_access"
6
+
7
+ module Isomorphic
8
+ # Included when the base class can memo-cache the results of getters and setters of Isomorphic lenses.
9
+ module Memoization
10
+ extend ::ActiveSupport::Concern
11
+
12
+ class_methods do
13
+ # @!scope class
14
+
15
+ # Defines finder methods and instance variables for the Active Record associations given by name.
16
+ #
17
+ # @param inflector [Isomorphic::Inflector::AbstractInflector] the Isomorphic inflector
18
+ # @param terms [Array<Object>] the inflectable terms
19
+ # @param association_names [Array<#to_s>] the association names
20
+ # @param options [Hash<Symbol, Object>] the options
21
+ # @option options [Array<#to_s>] :xmlattrs ([]) the XML attribute names
22
+ # @return [void]
23
+ # @raise [Isomorphic::InflectorError] if an inflectable term is invalid
24
+ def memo_isomorphism_for(inflector, terms, *association_names, **options)
25
+ method_name = inflector.isomorphism(terms)
26
+
27
+ options[:xmlattrs].try(:each) do |xmlattr_name|
28
+ association_names.each do |association_name|
29
+ memo_isomorphism_method_names_by_association_name_for(inflector.base)[association_name] ||= []
30
+ memo_isomorphism_method_names_by_association_name_for(inflector.base)[association_name] << method_name
31
+
32
+ # @!scope instance
33
+
34
+ # @!attribute [r] xmlattr_#{xmlattr_name}_for_#{method_name}_by_#{association_name}
35
+ # @return [ActiveSupport::HashWithIndifferentAccess] the cache of XML attributes
36
+ attribute_name = :"@xmlattr_#{xmlattr_name}_for_#{method_name}_by_#{association_name}"
37
+
38
+ # @!attribute [r] #{method_name}_by_xmlattr_#{xmlattr_name}
39
+ # @return [ActiveSupport::HashWithIndifferentAccess] the cache of instances
40
+ isomorphism_attribute_name = :"@#{method_name}_by_xmlattr_#{xmlattr_name}"
41
+
42
+ # @!method find_or_create_#{method_name}_for_#{association_name}!(record, *args, &block)
43
+ # Find or create the instance for the given Active Record record.
44
+ #
45
+ # @param record [ActiveRecord::Base] the record
46
+ # @param args [Array<Object>] the arguments for the isomorphism
47
+ # @yieldparam [Object] the instance
48
+ # @yieldreturn [void]
49
+ # @return [Object] the instance
50
+ define_method(:"find_or_create_#{method_name}_for_#{association_name}!") do |record, *args, &block|
51
+ instance = (instance_variable_get(attribute_name) || instance_variable_set(attribute_name, ::ActiveSupport::HashWithIndifferentAccess.new)).try { |hash| hash[record] }.try { |xmlattr_for_record|
52
+ (instance_variable_get(isomorphism_attribute_name) || instance_variable_set(isomorphism_attribute_name, ::ActiveSupport::HashWithIndifferentAccess.new)).try { |hash| hash[xmlattr_for_record] }
53
+ }
54
+
55
+ if instance.nil?
56
+ instance = record.send(:"to_#{method_name}", *args)
57
+
58
+ unless instance.nil?
59
+ if instance.respond_to?(:"xmlattr_#{xmlattr_name}")
60
+ xmlattr_for_record = instance.send(:"xmlattr_#{xmlattr_name}")
61
+
62
+ (instance_variable_get(attribute_name) || instance_variable_set(attribute_name, ::ActiveSupport::HashWithIndifferentAccess.new)).try { |hash|
63
+ hash[record] = xmlattr_for_record
64
+ }
65
+
66
+ (instance_variable_get(isomorphism_attribute_name) || instance_variable_set(isomorphism_attribute_name, ::ActiveSupport::HashWithIndifferentAccess.new)).try { |hash|
67
+ hash[xmlattr_for_record] = instance
68
+ }
69
+ end
70
+
71
+ unless block.nil?
72
+ case block.arity
73
+ when 1 then block.call(instance)
74
+ else instance.instance_eval(&block)
75
+ end
76
+ end
77
+ end
78
+ end
79
+
80
+ instance
81
+ end
82
+ end
83
+ end
84
+
85
+ return
86
+ end
87
+
88
+ # Returns the memo-cahe.
89
+ #
90
+ # @param base [Module] the base module
91
+ # @return [Hash<Module, ActiveSupport::HashWithIndifferentAccess>] the memo-cache
92
+ def memo_isomorphism_method_names_by_association_name_for(base)
93
+ unless class_variable_defined?(:"@@memo_isomorphism_method_names_by_association_name_for")
94
+ class_variable_set(:"@@memo_isomorphism_method_names_by_association_name_for", {})
95
+ end
96
+
97
+ class_variable_get(:"@@memo_isomorphism_method_names_by_association_name_for")[base] ||= ::ActiveSupport::HashWithIndifferentAccess.new
98
+ end
99
+ end
100
+
101
+ # @!scope instance
102
+
103
+ # Find all memoized instances for the given Active Record record by XML attribute name.
104
+ #
105
+ # @param inflector [Isomorphic::Inflector::AbstractInflector] the Isomorphic inflector
106
+ # @param record [ActiveRecord::Base] the Active Record record
107
+ # @param options [Hash<Symbol, Object>] the options
108
+ # @option options [Array<#to_s>] :xmlattrs ([]) the XML attribute names
109
+ # @return [ActiveSupport::HashWithIndifferentAccess] the memoized instances by XML attribute name
110
+ # @raise [Isomorphic::InflectorError] if an inflectable term is invalid
111
+ def find_all_with_memo_isomorphism_for(inflector, record, **options)
112
+ association_name = record.class.name.underscore.gsub("/", "_").to_sym
113
+
114
+ options[:xmlattrs].try(:inject, ::ActiveSupport::HashWithIndifferentAccess.new) { |xmlattr_acc, xmlattr_name|
115
+ xmlattr_acc[xmlattr_name] = self.class.memo_isomorphism_method_names_by_association_name_for(inflector.base)[association_name].try(:inject, inflector.convert_hash({})) { |acc, method_name|
116
+ attribute_name = :"@xmlattr_#{xmlattr_name}_for_#{method_name}_by_#{association_name}"
117
+
118
+ isomorphism_attribute_name = :"@#{method_name}_by_xmlattr_#{xmlattr_name}"
119
+
120
+ acc[method_name] ||= instance_variable_get(attribute_name).try { |xmlattr_for_method_name_by_association_name|
121
+ xmlattr_for_method_name_by_association_name[record].try { |xmlattr_for_method_name|
122
+ instance_variable_get(isomorphism_attribute_name).try { |method_name_by_xmlattr|
123
+ method_name_by_xmlattr[xmlattr_for_method_name]
124
+ }
125
+ }
126
+ }
127
+
128
+ acc
129
+ }
130
+
131
+ xmlattr_acc
132
+ }
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,83 @@
1
+ require "active_support/concern"
2
+ require "active_support/hash_with_indifferent_access"
3
+
4
+ require "isomorphic/node"
5
+
6
+ module Isomorphic
7
+ # Included when the base class can define isomorphisms.
8
+ module Model
9
+ extend ::ActiveSupport::Concern
10
+
11
+ class_methods do
12
+ # @!scope class
13
+
14
+ # Define an isomorphism for the given class and optional alias name.
15
+ #
16
+ # @param factory [Isomorphic::Factory::AbstractFactory] the Isomorphic factory
17
+ # @param inflector [Isomorphic::Inflector::AbstractInflector] the Isomorphic inflector
18
+ # @param isomorphism_class [Class] the class for the isomorphism
19
+ # @param method_suffix [#to_s] the optional alias name for the isomorphism
20
+ # @param options [Hash<Symbol, Object>] the options
21
+ # @option options [Boolean] :allow_blank (false) +true+ if the root node should always return a non-+nil+ target
22
+ # @option options [Hash<Symbol, Object>] :attributes ({}) default attributes for the target
23
+ # @option options [Boolean] :collection (false) +true+ if the target is a collection
24
+ # @yieldparam node [Isomorphic::Node::Root] the root node
25
+ # @yieldreturn [void]
26
+ # @return [void]
27
+ # @raise [Isomorphic::InflectorError] if an inflectable term is invalid
28
+ def isomorphism_for(factory, inflector, isomorphism_class, method_suffix = nil, **options, &block)
29
+ isomorphism_method_name = inflector.isomorphism([[isomorphism_class, method_suffix]])
30
+
31
+ # @!scope instance
32
+
33
+ # @!method has_#{isomorphism_method_name}?
34
+ # Does the base class have an Isomorphic node for this isomorphism?
35
+ #
36
+ # @return [Boolean] +true+
37
+ define_singleton_method(:"has_#{isomorphism_method_name}?") do
38
+ true
39
+ end
40
+
41
+ klass = options[:collection] ? Isomorphic::Node::RootCollection : Isomorphic::Node::RootMember
42
+
43
+ # @!method build_isomorphic_node_for_#{isomorphism_method_name}(*args)
44
+ # Builds an Isomorphic node for this isomorphism.
45
+ #
46
+ # @param args [Array<Object>] the arguments for the isomorphism
47
+ # @return [Isomorphic::Node::AbstractNode] the Isomorphic node
48
+ define_singleton_method(:"build_isomorphic_node_for_#{isomorphism_method_name}") do |*args|
49
+ klass.new(factory, inflector, self, isomorphism_class, *args, **options, &block)
50
+ end
51
+
52
+ # @!method from_#{isomorphism_method_name}(isomorphism_instance, record = nil, xmlattr_acc = ::ActiveSupport::HashWithIndifferentAccess.new, *args)
53
+ # Converts the given instance to an Active Record record.
54
+ #
55
+ # @param isomorphism_instance [Object] the instance
56
+ # @param record [ActiveRecord::Base] the record (optional; when not provided, a new instance of the Active Record class is constructed)
57
+ # @param xmlattr_acc [ActiveSupport::HashWithIndifferentAccess] the accumulator for XML attributes
58
+ # @param args [Array<Object>] the arguments for the isomorphism
59
+ # @return [ActiveRecord::Base] the record
60
+ define_singleton_method(:"from_#{isomorphism_method_name}") do |isomorphism_instance, record = nil, xmlattr_acc = ::ActiveSupport::HashWithIndifferentAccess.new, *args|
61
+ node = send(:"build_isomorphic_node_for_#{isomorphism_method_name}", *args)
62
+
63
+ node.from_isomorphism(isomorphism_instance, record, xmlattr_acc)
64
+ end
65
+
66
+ # @!method to_#{isomorphism_method_name}(isomorphism_instance = nil, *args)
67
+ # Converts the given Active Record record to an instance.
68
+ #
69
+ # @param isomorphism_instance [Object] the instance (optional; when not provided, a new instance is constructed)
70
+ # @return [Object] the instance
71
+ define_method(:"to_#{isomorphism_method_name}") do |isomorphism_instance = nil, *args|
72
+ node = self.class.send(:"build_isomorphic_node_for_#{isomorphism_method_name}", *args)
73
+
74
+ node.to_isomorphism(self, isomorphism_instance)
75
+ end
76
+
77
+ return
78
+ end
79
+ end
80
+
81
+ # @!scope instance
82
+ end
83
+ end
@@ -0,0 +1,1211 @@
1
+ require "active_support/concern"
2
+ require "active_support/core_ext/array/extract_options"
3
+ require "active_support/core_ext/object/try"
4
+ require "active_support/hash_with_indifferent_access"
5
+
6
+ require "isomorphic/errors"
7
+
8
+ require "isomorphic/lens"
9
+
10
+ module Isomorphic
11
+ # Generic base class for Isomorphic node errors.
12
+ #
13
+ # @abstract
14
+ class NodeError < Isomorphic::IsomorphicError
15
+ end
16
+
17
+ # Raised when the +:default+ option is not given in the constructor for a {Isomorphic::Node::Guard} node.
18
+ class DefaultOptionNotFound < Isomorphic::NodeError
19
+ end
20
+
21
+ # Raised when the +:default+ option is not included in the array of values for a {Isomorphic::Node::Guard} node.
22
+ class DefaultOptionNotIncluded < Isomorphic::NodeError
23
+ end
24
+
25
+ # Raised when an Isomorphic node cannot find a class.
26
+ class InvalidNodeClass < Isomorphic::NodeError
27
+ # @!attribute [r] klass
28
+ # @return [Class] the class
29
+ attr_reader :klass
30
+
31
+ # Default constructor.
32
+ #
33
+ # @param message [#to_s] the message
34
+ # @param klass [Clsas] the class
35
+ def initialize(message = nil, klass)
36
+ super(message)
37
+
38
+ @klass = klass
39
+ end
40
+ end
41
+
42
+ # Raised when an object is not an instance of a class.
43
+ class InvalidNodeObject < Isomorphic::NodeError
44
+ # @!attribute [r] klass
45
+ # @return [Class] the class
46
+ # @!attribute [r] object
47
+ # @return [Object] the object
48
+ attr_reader :klass, :object
49
+
50
+ # Default constructor.
51
+ #
52
+ # @param message [#to_s] the message
53
+ # @param klass [Clsas] the class
54
+ # @param object [Object] the object
55
+ def initialize(message = nil, klass, object)
56
+ super(message)
57
+
58
+ @klass, @object = klass, object
59
+ end
60
+ end
61
+
62
+ module Node
63
+ module Internal
64
+ # Included when the base class needs to perform deep equality testing.
65
+ module DeepEquals
66
+ extend ::ActiveSupport::Concern
67
+
68
+ # @!scope instance
69
+
70
+ # Are all attributes deep equal for the given object?
71
+ #
72
+ # @param base [Module] the base module
73
+ # @param object [Object] the object
74
+ # @param attributes [ActiveSupport::HashWithIndifferentAccess] the attributes
75
+ # @return [Boolean] +true+ if all attribues are deep equal; otherwise, +false+
76
+ def all_attributes_eql?(base, object, attributes = ::ActiveSupport::HashWithIndifferentAccess.new)
77
+ attributes.try(:each_pair) do |pair|
78
+ attribute_name, expected_value = *pair
79
+
80
+ return false unless deep_eql?(base, expected_value, object.send(attribute_name))
81
+ end
82
+
83
+ true
84
+ end
85
+
86
+ private
87
+
88
+ def deep_eql?(base, value, other_value)
89
+ if value.class.parents[-2] == base
90
+ if value.is_a?(::Array)
91
+ return false unless other_value.is_a?(::Array) && (value.all? { |instance_for_value|
92
+ other_value.any? { |instance_for_other_value|
93
+ deep_eql?(base, instance_for_value, instance_for_other_value)
94
+ }
95
+ })
96
+ elsif value.is_a?(::String)
97
+ return false unless (value.to_s == other_value.to_s)
98
+ else
99
+ attributes = value.class.instance_methods(false).reject { |method_name|
100
+ method_name.to_s.ends_with?("=")
101
+ }.inject(::ActiveSupport::HashWithIndifferentAccess.new) { |acc, method_name|
102
+ value.send(method_name).try { |expected_value|
103
+ acc[method_name] = expected_value
104
+ }
105
+
106
+ acc
107
+ }
108
+
109
+ return false unless (value.class == other_value.class) && all_attributes_eql?(base, other_value, attributes)
110
+ end
111
+ else
112
+ return false unless (value == other_value)
113
+ end
114
+
115
+ true
116
+ end
117
+ end
118
+
119
+ # Included when the base class is a collection.
120
+ module InstanceMethodsForCollection
121
+ extend ::ActiveSupport::Concern
122
+
123
+ included do
124
+ include Isomorphic::Lens::Internal::InstanceMethodsForLens
125
+ end
126
+
127
+ # @!scope instance
128
+
129
+ # Build an Isomorphic node for an Active Record association.
130
+ #
131
+ # @param options [Hash<Symbol, Object>] the options
132
+ # @option options [Boolean] :allow_blank (false) +true+ if the new node should always return a non-+nil+ target
133
+ # @option options [Hash<Symbol, Object>] :attributes ({}) default attributes for the target
134
+ # @yieldparam node [Isomorphic::Node::Association] the Isomorphic node (member)
135
+ # @yieldreturn [void]
136
+ # @return [self]
137
+ def association(**options, &block)
138
+ node = Isomorphic::Node::Association.new(self, **options, &block)
139
+ @__child_nodes << node
140
+ node
141
+ end
142
+
143
+ # Build an Isomorphic node for an Active Record association (as an attribute) using an Isomorphic lens.
144
+ #
145
+ # @param lens_or_association [Isomorphic::Lens::AbstractLens, #to_s] the Isomorphic lens or the name of the Active Record association
146
+ # @param options [Hash<Symbol, Object>] the options
147
+ # @option options [Boolean] :allow_blank (false) +true+ if the new node should always return a non-+nil+ target
148
+ # @option options [Hash<Symbol, Object>] :attributes ({}) default attributes for the target
149
+ # @yieldparam node [Isomorphic::Node::Association] the Isomorphic node (member)
150
+ # @yieldreturn [void]
151
+ # @return [self]
152
+ def association_for(lens_or_association, **options, &block)
153
+ lens = lens_or_association.is_a?(Isomorphic::Lens::AbstractLens) ? lens_or_association : reflect_on_attribute(lens_or_association)
154
+
155
+ association(**options.merge({
156
+ get: ::Proc.new { |record| lens.get(record) },
157
+ set: ::Proc.new { |record, value, xmlattr_acc| lens.set(record, value, xmlattr_acc) },
158
+ }), &block)
159
+ end
160
+
161
+ # Build an Isomorphic node for zero or more members of a collection.
162
+ #
163
+ # @param isomorphism_class [Class] the class
164
+ # @param options [Hash<Symbol, Object>] the options
165
+ # @option options [Boolean] :allow_blank (false) +true+ if the new node should always return a non-+nil+ target
166
+ # @option options [Hash<Symbol, Object>] :attributes ({}) default attributes for the target
167
+ # @yieldparam node [Isomorphic::Node::Element] the Isomorphic node (member)
168
+ # @yieldreturn [void]
169
+ # @return [self]
170
+ def element(isomorphism_class, **options, &block)
171
+ node = Isomorphic::Node::Element.new(self, isomorphism_class, **options, &block)
172
+ @__child_nodes << node
173
+ node
174
+ end
175
+
176
+ # Build an Isomorphic node for a "guard" statement for an Active Record association using an Isomorphic lens.
177
+ #
178
+ # @param lens_or_attribute_name [Isomorphic::Lens::AbstractLens, #to_s] the Isomorphic lens or the name of the Active Record association
179
+ # @param values [Array<Object>] the included values
180
+ # @param options [Hash<Symbol, Object>] the options
181
+ # @option options [Boolean] :allow_blank (false) +true+ if the new node should always return a non-+nil+ target
182
+ # @option options [Hash<Symbol, Object>] :attributes ({}) default attributes for the target
183
+ # @option options [Object] :default (nil) the default value, where more than one included value is given
184
+ # @yieldparam node [Isomorphic::Node::GuardCollection] the Isomorphic node (collection)
185
+ # @yieldreturn [void]
186
+ # @return [self]
187
+ def guard_association_for(lens_or_attribute_name, *values, **options, &block)
188
+ lens = lens_or_attribute_name.is_a?(Isomorphic::Lens::AbstractLens) ? lens_or_attribute_name : reflect_on_attribute(lens_or_attribute_name)
189
+
190
+ node = Isomorphic::Node::GuardCollection.new(self, lens, *values, **options, &block)
191
+ @__child_nodes << node
192
+ node
193
+ end
194
+
195
+ # Build an Isomorphic node for a value of a "namespace" statement that is within scope.
196
+ #
197
+ # @param namespace [#to_s] the namespace name
198
+ # @param terms [Array<Object>] the inflectable terms
199
+ # @param options [Hash<Symbol, Object>] the options
200
+ # @option options [Boolean] :allow_blank (false) +true+ if the new node should always return a non-+nil+ target
201
+ # @option options [Hash<Symbol, Object>] :attributes ({}) default attributes for the target
202
+ # @yieldparam node [Isomorphic::Node::NamespaceAssociation] the Isomorphic node (member)
203
+ # @yieldreturn [void]
204
+ # @return [self]
205
+ # @raise [Isomorphic::InflectorError] if an inflectable term is invalid
206
+ def namespace_association_for(namespace, terms, **options, &block)
207
+ key = inflector.isomorphism(terms)
208
+
209
+ node = Isomorphic::Node::NamespaceAssociation.new(self, namespace, key, **options, &block)
210
+ @__child_nodes << node
211
+ node
212
+ end
213
+ end
214
+
215
+ # Included when the base class is a member.
216
+ module InstanceMethodsForMember
217
+ extend ::ActiveSupport::Concern
218
+
219
+ included do
220
+ include Isomorphic::Lens::Internal::InstanceMethodsForLens
221
+ end
222
+
223
+ # @!scope instance
224
+
225
+ # Build an Isomorphic node for an attribute.
226
+ #
227
+ # @param method_name [#to_s] the method name for the target
228
+ # @param isomorphism_xmlattr_class [Class] the class for the target
229
+ # @param options [Hash<Symbol, Object>] the options
230
+ # @option options [Boolean] :allow_blank (false) +true+ if the new node should always return a non-+nil+ target
231
+ # @option options [Hash<Symbol, Object>] :attributes ({}) default attributes for the target
232
+ # @yieldparam node [Isomorphic::Node::Attribute] the Isomorphic node (member)
233
+ # @yieldreturn [void]
234
+ # @return [self]
235
+ def attribute(method_name, isomorphism_xmlattr_class = nil, **options, &block)
236
+ node = Isomorphic::Node::Attribute.new(self, method_name, isomorphism_xmlattr_class, **options, &block)
237
+ @__child_nodes << node
238
+ node
239
+ end
240
+
241
+ # Build an Isomorphic node for an attribute using the given lens.
242
+ #
243
+ # @param lens_or_attribute_name [Isomorphic::Lens::AbstractLens, #to_s] the Isomorphic lens or the name of the attribute
244
+ # @param method_name [#to_s] the method name for the target
245
+ # @param isomorphism_xmlattr_class [Class] the class for the target
246
+ # @param options [Hash<Symbol, Object>] the options
247
+ # @option options [Boolean] :allow_blank (false) +true+ if the new node should always return a non-+nil+ target
248
+ # @option options [Hash<Symbol, Object>] :attributes ({}) default attributes for the target
249
+ # @yieldparam node [Isomorphic::Node::Attribute] the Isomorphic node (member)
250
+ # @yieldreturn [void]
251
+ # @return [self]
252
+ def attribute_for(lens_or_attribute_name, method_name, isomorphism_xmlattr_class = nil, **options, &block)
253
+ lens = lens_or_attribute_name.is_a?(Isomorphic::Lens::AbstractLens) ? lens_or_attribute_name : reflect_on_attribute(lens_or_attribute_name)
254
+
255
+ attribute(method_name, isomorphism_xmlattr_class, **options.merge({
256
+ get: ::Proc.new { |record| lens.get(record) },
257
+ set: ::Proc.new { |record, value, xmlattr_acc| lens.set(record, value, xmlattr_acc) },
258
+ }), &block)
259
+ end
260
+
261
+ # Build an Isomorphic node for a collection.
262
+ #
263
+ # @param method_name [#to_s] the method name for the target
264
+ # @param isomorphism_class [Class] the class for the target
265
+ # @param options [Hash<Symbol, Object>] the options
266
+ # @option options [Boolean] :allow_blank (false) +true+ if the new node should always return a non-+nil+ target
267
+ # @option options [Hash<Symbol, Object>] :attributes ({}) default attributes for the target
268
+ # @yieldparam node [Isomorphic::Node::Attribute] the Isomorphic node (collection)
269
+ # @yieldreturn [void]
270
+ # @return [self]
271
+ def collection(method_name, isomorphism_class, **options, &block)
272
+ node = Isomorphic::Node::Collection.new(self, method_name, isomorphism_class, **options, &block)
273
+ @__child_nodes << node
274
+ node
275
+ end
276
+
277
+ # Build an Isomorphic node for a +Proc+ that is called in the reverse direction only.
278
+ #
279
+ # @param options [Hash<Symbol, Object>] the options
280
+ # @yieldparam instance [Object] the instance
281
+ # @yieldparam record [ActiveRecord::Base] the record
282
+ # @yieldparam xmlattr_acc [ActiveSupport::HashWithIndifferentAccess] the accumulator for XML attributes
283
+ # @yieldparam scope [Isomorphic::Node::Internal::Scope] the scope
284
+ # @yieldreturn [ActiveRecord::Record] the record
285
+ # @return [self]
286
+ def from(**options, &block)
287
+ node = Isomorphic::Node::ProcFrom.new(self, **options, &block)
288
+ @__child_nodes << node
289
+ node
290
+ end
291
+
292
+ # Build an Isomorphic node for a "guard" statement for an attribute using an Isomorphic lens.
293
+ #
294
+ # @param lens_or_attribute_name [Isomorphic::Lens::AbstractLens, #to_s] the Isomorphic lens or the name of the attribute
295
+ # @param values [Array<Object>] the included values
296
+ # @param options [Hash<Symbol, Object>] the options
297
+ # @option options [Boolean] :allow_blank (false) +true+ if the new node should always return a non-+nil+ target
298
+ # @option options [Hash<Symbol, Object>] :attributes ({}) default attributes for the target
299
+ # @option options [Object] :default (nil) the default value, where more than one included value is given
300
+ # @yieldparam node [Isomorphic::Node::GuardCollection] the Isomorphic node (collection)
301
+ # @yieldreturn [void]
302
+ # @return [self]
303
+ def guard_attribute_for(lens_or_attribute_name, *values, **options, &block)
304
+ lens = lens_or_attribute_name.is_a?(Isomorphic::Lens::AbstractLens) ? lens_or_attribute_name : reflect_on_attribute(lens_or_attribute_name)
305
+
306
+ node = Isomorphic::Node::GuardMember.new(self, lens, *values, **options, &block)
307
+ @__child_nodes << node
308
+ node
309
+ end
310
+
311
+ # Build an Isomorphic node for a member.
312
+ #
313
+ # @param method_name [#to_s] the method name for the target
314
+ # @param isomorphism_class [Class] the class for the target
315
+ # @param options [Hash<Symbol, Object>] the options
316
+ # @option options [Boolean] :allow_blank (false) +true+ if the new node should always return a non-+nil+ target
317
+ # @option options [Hash<Symbol, Object>] :attributes ({}) default attributes for the target
318
+ # @yieldparam node [Isomorphic::Node::Attribute] the Isomorphic node (collection)
319
+ # @yieldreturn [void]
320
+ # @return [self]
321
+ def member(method_name, isomorphism_class, **options, &block)
322
+ node = Isomorphic::Node::Member.new(self, method_name, isomorphism_class, **options, &block)
323
+ @__child_nodes << node
324
+ node
325
+ end
326
+
327
+ # Build an Isomorphic node for a "namespace" statement using an Isomorphic lens.
328
+ #
329
+ # @param namespace [#to_s] the namespace name
330
+ # @param lens_or_attribute_name [Isomorphic::Lens::AbstractLens, #to_s] the Isomorphic lens or the name of the attribute
331
+ # @param options [Hash<Symbol, Object>] the options
332
+ # @option options [Boolean] :allow_blank (false) +true+ if the new node should always return a non-+nil+ target
333
+ # @option options [Hash<Symbol, Object>] :attributes ({}) default attributes for the target
334
+ # @yieldparam node [Isomorphic::Node::NamespaceAssociation] the Isomorphic node (member)
335
+ # @yieldreturn [void]
336
+ # @return [self]
337
+ def namespace(namespace, lens_or_attribute_name, **options, &block)
338
+ lens = lens_or_attribute_name.is_a?(Isomorphic::Lens::AbstractLens) ? lens_or_attribute_name : reflect_on_attribute(lens_or_attribute_name)
339
+
340
+ node = Isomorphic::Node::Namespace.new(self, namespace, lens, **options, &block)
341
+ @__child_nodes << node
342
+ node
343
+ end
344
+
345
+ # Build an Isomorphic node for a value of a "namespace" statement that is within scope.
346
+ #
347
+ # @param namespace [#to_s] the namespace name
348
+ # @param terms [Array<Object>] the inflectable terms
349
+ # @param method_name [#to_s] the method name for the target
350
+ # @param isomorphism_xmlattr_class [Class] the class for the target
351
+ # @param options [Hash<Symbol, Object>] the options
352
+ # @option options [Boolean] :allow_blank (false) +true+ if the new node should always return a non-+nil+ target
353
+ # @option options [Hash<Symbol, Object>] :attributes ({}) default attributes for the target
354
+ # @yieldparam node [Isomorphic::Node::NamespaceAssociation] the Isomorphic node (member)
355
+ # @yieldreturn [void]
356
+ # @return [self]
357
+ # @raise [Isomorphic::InflectorError] if an inflectable term is invalid
358
+ def namespace_attribute_for(namespace, terms, method_name, isomorphism_xmlattr_class = nil, **options, &block)
359
+ key = inflector.isomorphism(terms)
360
+
361
+ node = Isomorphic::Node::NamespaceAttribute.new(self, namespace, key, method_name, isomorphism_xmlattr_class, **options, &block)
362
+ @__child_nodes << node
363
+ node
364
+ end
365
+
366
+ # Build an Isomorphic node for a +Proc+ that is called in the forward direction only.
367
+ #
368
+ # @param options [Hash<Symbol, Object>] the options
369
+ # @yieldparam record [ActiveRecord::Base] the record
370
+ # @yieldparam instance [Object] the instance
371
+ # @yieldparam scope [Isomorphic::Node::Internal::Scope] the scope
372
+ # @yieldreturn [Object] the instance
373
+ # @return [self]
374
+ def to(**options, &block)
375
+ node = Isomorphic::Node::ProcTo.new(self, **options, &block)
376
+ @__child_nodes << node
377
+ node
378
+ end
379
+ end
380
+
381
+ # Implements a tree of hashes, where retrieval is from the bottom-up.
382
+ class Scope
383
+ # @!attribute [r] parent
384
+ # @return [Isomorphic::Node::Internal::Scope] the parent scope
385
+ attr_reader :parent
386
+
387
+ # Default constructor.
388
+ #
389
+ # @param parent [Isomorphic::Node::Internal::Scope] the parent scope
390
+ # @param inflector [Isomorphic::Inflector::AbstractInflector] the Isomorphic inflector
391
+ def initialize(parent, inflector = nil)
392
+ super()
393
+
394
+ @parent, @inflector = parent, inflector
395
+
396
+ @value_by_namespace_and_key = ActiveSupport::HashWithIndifferentAccess.new
397
+ end
398
+
399
+ # @!attribute [r] inflector
400
+ # @return [Isomorphic::Inflector::AbstractInflector] the inflector
401
+ %i(inflector).each do |method_name|
402
+ define_method(method_name) do
403
+ if parent.is_a?(Isomorphic::Node::Internal::Scope)
404
+ parent.send(method_name)
405
+ else
406
+ instance_variable_get(:"@#{method_name}")
407
+ end
408
+ end
409
+ end
410
+
411
+ # Build a new child scope for the given namespace.
412
+ #
413
+ # @param namespace [#to_s] the namespace name
414
+ # @return [Isomorphic::Node::Internal::Scope] the new child scope
415
+ def child(namespace)
416
+ scope = self.class.new(self)
417
+
418
+ scope.instance_variable_get(:@value_by_namespace_and_key).send(:[]=, namespace, inflector.convert_hash({}))
419
+
420
+ scope
421
+ end
422
+
423
+ # Get the scoped value of the given key in the context of the given namespace.
424
+ #
425
+ # @param namespace [#to_s] the namespace name
426
+ # @param key [#to_s] the key
427
+ # @return [Object, nil] the value or +nil+ if the namespace or key are not defined
428
+ def get(namespace, key)
429
+ if @value_by_namespace_and_key.key?(namespace) && @value_by_namespace_and_key[namespace].key?(key)
430
+ @value_by_namespace_and_key[namespace][key]
431
+ elsif @parent.is_a?(self.class)
432
+ @parent.get(namespace, key)
433
+ else
434
+ nil
435
+ end
436
+ end
437
+
438
+ # The keys for the given namespace.
439
+ #
440
+ # @param namespace [#to_s] the namespace name
441
+ # @return [Array<String>] the keys or +nil+ if the namespace is not defined
442
+ def keys(namespace)
443
+ if @value_by_namespace_and_key.key?(namespace)
444
+ @value_by_namespace_and_key[namespace].keys
445
+ else
446
+ nil
447
+ end
448
+ end
449
+
450
+ # The namespaces.
451
+ #
452
+ # @return [Array<String>] the namespace names
453
+ def namespaces
454
+ @value_by_namespace_and_key.keys
455
+ end
456
+
457
+ # Set the scoped value of the given key in the context of the given namespace.
458
+ #
459
+ # @param namespace [#to_s] the namespace name
460
+ # @param key [#to_s] the key
461
+ # @param value [Object] the value
462
+ # @return [Object, nil] the value or +nil+ if the namespace is not defined
463
+ def set(namespace, key, value)
464
+ if @value_by_namespace_and_key.key?(namespace)
465
+ @value_by_namespace_and_key[namespace][key] = value
466
+ elsif @parent.is_a?(self.class)
467
+ @parent.set(namespace, key, value)
468
+ else
469
+ nil
470
+ end
471
+ end
472
+
473
+ # The values for the given namespace.
474
+ #
475
+ # @param namespace [#to_s] the namespace name
476
+ # @return [Array<Object>, nil] the values or +nil+ if the namespace is not defined
477
+ def values(namespace)
478
+ keys(namespace).try(:inject, inflector.convert_hash({})) { |acc, key|
479
+ acc[key] = get(namespace, key)
480
+ acc
481
+ }
482
+ end
483
+ end
484
+ end
485
+
486
+ # Generic base class for Isomorphic nodes.
487
+ #
488
+ # @abstract Subclass and override {#from_isomorphism} and {#to_isomorphism} to implement a custom class.
489
+ class AbstractNode
490
+ include Isomorphic::Node::Internal::DeepEquals
491
+
492
+ # @!attribute [r] parent
493
+ # @return [Isomorphic::Node::AbstractNode] the parent node
494
+ attr_reader :parent
495
+
496
+ # @!attribute [r] options
497
+ # @return [Hash<Symbol, Object>] the options
498
+ attr_reader :options
499
+
500
+ # Default constructor.
501
+ #
502
+ # @param parent [Isomorphic::Node::AbstractNode] the parent node
503
+ # @param args [Array<Object>] the arguments
504
+ # @yieldparam [self]
505
+ # @yieldreturn [void]
506
+ def initialize(parent, *args, &block)
507
+ super()
508
+
509
+ @__cache_for_to_isomorphism_for = {}
510
+ @__child_nodes = []
511
+
512
+ @parent = parent
513
+
514
+ @options = args.extract_options!
515
+
516
+ unless block.nil?
517
+ self.instance_exec(*args, &block)
518
+ end
519
+ end
520
+
521
+ # @!attribute [r] factory
522
+ # @return [Isomorphic::Factory::AbstractFactory] the Isomorphic factory
523
+ # @!attribute [r] inflector
524
+ # @return [Isomorphic::Inflector::AbstractInflector] the Isomorphic inflector
525
+ %i(factory inflector).each do |method_name|
526
+ define_method(method_name) do
527
+ if parent.is_a?(Isomorphic::Node::AbstractNode)
528
+ parent.send(method_name)
529
+ else
530
+ instance_variable_get(:"@#{method_name}")
531
+ end
532
+ end
533
+ end
534
+
535
+ # Converts the given instance to an Active Record record.
536
+ #
537
+ # @param scope [Isomorphic::Node::Internal::Scope] the scope
538
+ # @param isomorphism_instance [Object] the instance
539
+ # @param record [ActiveRecord::Base] the record (optional; when not provided, a new instance of the Active Record class is constructed)
540
+ # @param xmlattr_acc [ActiveSupport::HashWithIndifferentAccess] the accumulator for XML attributes
541
+ # @return [ActiveRecord::Base] the record
542
+ def from_isomorphism(scope, isomorphism_instance, record = nil, xmlattr_acc = ::ActiveSupport::HashWithIndifferentAccess.new)
543
+ raise ::NotImplementedError
544
+ end
545
+
546
+ # Converts the given Active Record record to an instance.
547
+ #
548
+ # @param scope [Isomorphic::Node::Internal::Scope] the scope
549
+ # @param record [ActiveRecord::Base] the record
550
+ # @param isomorphism_instance [Object] the instance (optional; when not provided, a new instance is constructed)
551
+ # @return [Object] the instance
552
+ def to_isomorphism(scope, record, isomorphism_instance = nil)
553
+ raise ::NotImplementedError
554
+ end
555
+
556
+ # Returns the cached instance for the given Active Record record.
557
+ #
558
+ # @param record [ActiveRecord::Base] the record
559
+ # @param args [Array<#to_s>] the method names
560
+ # @return [Object] the cached instance
561
+ def to_isomorphism_for(record, *args)
562
+ args.inject(@__cache_for_to_isomorphism_for[record]) { |acc, arg|
563
+ acc.try(:[], arg)
564
+ }
565
+ end
566
+ end
567
+
568
+ class Association < Isomorphic::Node::AbstractNode
569
+ include Isomorphic::Node::Internal::InstanceMethodsForMember
570
+
571
+ def from_isomorphism(scope, instance, record = nil, xmlattr_acc = ::ActiveSupport::HashWithIndifferentAccess.new)
572
+ is_present = false
573
+
574
+ instance.each do |instance_for_node|
575
+ _set(scope, record, instance_for_node, xmlattr_acc).try { |record_for_node|
576
+ is_present ||= true
577
+
578
+ @__child_nodes.inject(false) { |acc, child_node| child_node.from_isomorphism(scope, instance_for_node, record_for_node, xmlattr_acc).nil? ? acc : true }
579
+ }
580
+ end
581
+
582
+ if is_present
583
+ record
584
+ else
585
+ nil
586
+ end
587
+ end
588
+
589
+ def to_isomorphism(scope, record, instance = nil)
590
+ instance_for_node_or_array = _get(scope, record).try { |instance_for_node_or_hash|
591
+ @__cache_for_to_isomorphism_for[record] ||= instance_for_node_or_hash
592
+
593
+ if instance_for_node_or_hash.is_a?(::Hash)
594
+ instance_for_node_or_hash.values
595
+ else
596
+ instance_for_node_or_hash
597
+ end
598
+ }
599
+
600
+ unless instance_for_node_or_array.nil?
601
+ array = (instance_for_node_or_array.instance_of?(::Array) ? instance_for_node_or_array : [instance_for_node_or_array]).reject(&:nil?)
602
+
603
+ if array.any? || options[:allow_blank]
604
+ array.each do |instance_for_node|
605
+ @__child_nodes.inject(false) { |acc, child_node| child_node.to_isomorphism(scope, record, instance_for_node).nil? ? acc : true }
606
+
607
+ instance.send(:<<, instance_for_node)
608
+ end
609
+
610
+ instance_for_node_or_array
611
+ else
612
+ nil
613
+ end
614
+ else
615
+ nil
616
+ end
617
+ end
618
+
619
+ protected
620
+
621
+ def _get(scope, record)
622
+ options[:get].try { |block|
623
+ case block.arity
624
+ when 1 then block.call(record)
625
+ else record.instance_eval(&block)
626
+ end
627
+ }
628
+ end
629
+
630
+ def _set(scope, record, instance_for_node, xmlattr_acc = ::ActiveSupport::HashWithIndifferentAccess.new)
631
+ options[:set].try { |block|
632
+ case block.arity
633
+ when 3 then block.call(record, instance_for_node, xmlattr_acc)
634
+ else record.instance_exec(instance_for_node, xmlattr_acc, &block)
635
+ end
636
+ }
637
+ end
638
+ end
639
+
640
+ class Attribute < AbstractNode
641
+ include Isomorphic::Node::Internal::InstanceMethodsForMember
642
+
643
+ attr_reader :method_name
644
+
645
+ attr_reader :xmlattr_class
646
+
647
+ def initialize(parent, method_name, xmlattr_class = nil, **options, &block)
648
+ super(parent, **options, &block)
649
+
650
+ unless xmlattr_class.nil? || (xmlattr_class.is_a?(::Class) && (xmlattr_class.parents[-2] == parent.inflector.base))
651
+ raise Isomorphic::InvalidNodeClass.new(nil, xmlattr_class)
652
+ end
653
+
654
+ @method_name = method_name
655
+
656
+ @xmlattr_class = xmlattr_class
657
+ end
658
+
659
+ def from_isomorphism(scope, instance, record = nil, xmlattr_acc = ::ActiveSupport::HashWithIndifferentAccess.new)
660
+ is_present = false
661
+
662
+ instance_for_node = instance.send(method_name)
663
+
664
+ is_valid = (xmlattr_class.nil? || instance_for_node.instance_of?(xmlattr_class)) && all_attributes_eql?(inflector.base, instance_for_node, options[:attributes].try(:each_pair).try(:inject, ::ActiveSupport::HashWithIndifferentAccess.new) { |acc, pair|
665
+ xmlattr_name, value = *pair
666
+
667
+ acc[:"xmlattr_#{xmlattr_name}"] = value
668
+ acc
669
+ })
670
+
671
+ if is_valid && (!instance_for_node.nil? || options[:allow_nil])
672
+ _set(scope, record, instance_for_node, xmlattr_acc).try { |record_for_node|
673
+ is_present ||= true
674
+
675
+ @__child_nodes.inject(false) { |acc, child_node| child_node.from_isomorphism(scope, instance_for_node, record_for_node, xmlattr_acc).nil? ? acc : true }
676
+ }
677
+ end
678
+
679
+ if is_present
680
+ record
681
+ else
682
+ nil
683
+ end
684
+ end
685
+
686
+ def to_isomorphism(scope, record, instance = nil)
687
+ instance_for_node = _get(scope, record).try { |retval_or_hash|
688
+ @__cache_for_to_isomorphism_for[record] ||= retval_or_hash
689
+
690
+ if retval_or_hash.is_a?(::Hash)
691
+ retval_or_hash.values
692
+ else
693
+ retval_or_hash
694
+ end
695
+ }.try { |retval|
696
+ if retval.class.parents[-2] == inflector.base
697
+ @__child_nodes.inject(false) { |acc, child_node| child_node.to_isomorphism(scope, record, retval).nil? ? acc : true }
698
+
699
+ retval
700
+ else
701
+ retval.try(:to_s).try { |s|
702
+ if xmlattr_class.nil?
703
+ s
704
+ else
705
+ instance_for_node = xmlattr_class.new(s)
706
+
707
+ options[:attributes].try(:each_pair).try(:inject, {}) { |acc, pair|
708
+ xmlattr_name, value = *pair
709
+
710
+ acc[:"xmlattr_#{xmlattr_name}"] = value
711
+ acc
712
+ }.try { |attributes|
713
+ factory.update_attributes(instance_for_node, attributes)
714
+ }
715
+
716
+ @__child_nodes.inject(false) { |acc, child_node| child_node.to_isomorphism(scope, record, instance_for_node).nil? ? acc : true }
717
+
718
+ instance_for_node
719
+ end
720
+ }
721
+ end
722
+ }
723
+
724
+ if !instance_for_node.nil? || options[:allow_nil]
725
+ instance.send(:"#{method_name}=", instance_for_node)
726
+ else
727
+ nil
728
+ end
729
+ end
730
+
731
+ protected
732
+
733
+ def _get(scope, record)
734
+ options[:get].try { |block|
735
+ case block.arity
736
+ when 1 then block.call(record)
737
+ else record.instance_eval(&block)
738
+ end
739
+ }
740
+ end
741
+
742
+ def _set(scope, record, instance_for_node, xmlattr_acc = ::ActiveSupport::HashWithIndifferentAccess.new)
743
+ options[:set].try { |block|
744
+ case block.arity
745
+ when 3 then block.call(record, instance_for_node, xmlattr_acc)
746
+ else record.instance_exec(instance_for_node, xmlattr_acc, &block)
747
+ end
748
+ }
749
+ end
750
+ end
751
+
752
+ class Collection < Isomorphic::Node::AbstractNode
753
+ include Isomorphic::Node::Internal::InstanceMethodsForCollection
754
+
755
+ attr_reader :method_name
756
+
757
+ attr_reader :klass
758
+
759
+ def initialize(parent, method_name, klass, **options, &block)
760
+ super(parent, **options, &block)
761
+
762
+ unless klass.is_a?(::Class) && (klass.parents[-2] == parent.inflector.base)
763
+ raise Isomorphic::InvalidNodeClass.new(nil, klass)
764
+ end
765
+
766
+ @method_name = method_name
767
+ @klass = klass
768
+ end
769
+
770
+ def from_isomorphism(scope, instance, record = nil, xmlattr_acc = ::ActiveSupport::HashWithIndifferentAccess.new)
771
+ is_present = false
772
+
773
+ instance.send(method_name).try { |instance_for_node|
774
+ is_valid = instance_for_node.instance_of?(klass) && all_attributes_eql?(inflector.base, instance_for_node, options[:attributes])
775
+
776
+ if is_valid
777
+ is_present ||= @__child_nodes.inject(false) { |acc, child_node| child_node.from_isomorphism(scope, instance_for_node, record, xmlattr_acc).nil? ? acc : true }
778
+ end
779
+ }
780
+
781
+ if is_present
782
+ record
783
+ else
784
+ nil
785
+ end
786
+ end
787
+
788
+ def to_isomorphism(scope, record, instance = nil)
789
+ is_present = false
790
+
791
+ instance_for_node = instance.send(method_name) || factory.for(klass)
792
+
793
+ options[:attributes].try { |attributes|
794
+ factory.update_attributes(instance_for_node, attributes)
795
+ }
796
+
797
+ is_present ||= @__child_nodes.inject(false) { |acc, child_node| child_node.to_isomorphism(scope, record, instance_for_node).nil? ? acc : true }
798
+
799
+ if is_present || options[:allow_blank]
800
+ instance.send(:"#{method_name}=", instance_for_node)
801
+
802
+ instance_for_node
803
+ else
804
+ nil
805
+ end
806
+ end
807
+ end
808
+
809
+ class Element < Isomorphic::Node::AbstractNode
810
+ include Isomorphic::Node::Internal::InstanceMethodsForMember
811
+
812
+ attr_reader :klass
813
+
814
+ def initialize(parent, klass, **options, &block)
815
+ super(parent, **options, &block)
816
+
817
+ unless klass.is_a?(::Class) && (klass.parents[-2] == parent.inflector.base)
818
+ raise Isomorphic::InvalidNodeClass.new(nil, klass)
819
+ end
820
+
821
+ @klass = klass
822
+ end
823
+
824
+ def from_isomorphism(scope, instance, record = nil, xmlattr_acc = ::ActiveSupport::HashWithIndifferentAccess.new)
825
+ is_present = false
826
+
827
+ instance.try(:each) { |instance_for_node|
828
+ is_valid = instance_for_node.instance_of?(klass) && all_attributes_eql?(inflector.base, instance_for_node, options[:attributes])
829
+
830
+ if is_valid
831
+ is_present ||= @__child_nodes.inject(options[:allow_blank]) { |acc, child_node| child_node.from_isomorphism(scope, instance_for_node, record, xmlattr_acc).nil? ? acc : true }
832
+ end
833
+ }
834
+
835
+ if is_present
836
+ record
837
+ else
838
+ nil
839
+ end
840
+ end
841
+
842
+ def to_isomorphism(scope, record, instance = nil)
843
+ is_present = false
844
+
845
+ instance_for_node = factory.for(klass)
846
+
847
+ options[:attributes].try { |attributes|
848
+ factory.update_attributes(instance_for_node, attributes)
849
+ }
850
+
851
+ is_present ||= @__child_nodes.inject(false) { |acc, child_node| child_node.to_isomorphism(scope, record, instance_for_node).nil? ? acc : true }
852
+
853
+ if is_present || options[:allow_blank]
854
+ instance.send(:<<, instance_for_node)
855
+
856
+ instance_for_node
857
+ else
858
+ nil
859
+ end
860
+ end
861
+ end
862
+
863
+ class Guard < Isomorphic::Node::AbstractNode
864
+ attr_reader :lens
865
+
866
+ attr_reader :values
867
+
868
+ def initialize(parent, lens, *values, **options, &block)
869
+ super(parent, **options, &block)
870
+
871
+ unless (lens.is_a?(Isomorphic::Lens::Association) && lens.last_lens.is_a?(Isomorphic::Lens::Attribute)) || lens.is_a?(Isomorphic::Lens::Attribute)
872
+ raise Isomorphic::LensInvalid.new(nil, lens)
873
+ end
874
+
875
+ if (values.size > 1) && !options.key?(:default)
876
+ raise Isomorphic::DefaultOptionNotFound.new(nil)
877
+ elsif options.key?(:default) && !values.include?(options[:default])
878
+ raise Isomorphic::DefaultOptionNotIncluded.new(nil)
879
+ end
880
+
881
+ @lens = lens
882
+ @values = values
883
+ end
884
+
885
+ def from_isomorphism(scope, instance, record = nil, xmlattr_acc = ::ActiveSupport::HashWithIndifferentAccess.new)
886
+ is_present = @__child_nodes.inject(false) { |acc, child_node| child_node.from_isomorphism(scope, instance, record, xmlattr_acc).nil? ? acc : true }
887
+
888
+ if is_present
889
+ value = \
890
+ case values.size
891
+ when 1 then values[0]
892
+ else options[:default]
893
+ end
894
+
895
+ lens.set(record, value, xmlattr_acc)
896
+
897
+ record
898
+ else
899
+ nil
900
+ end
901
+ end
902
+
903
+ def to_isomorphism(scope, record, instance = nil)
904
+ lens.get(record).try { |value_or_hash|
905
+ is_valid = \
906
+ if value_or_hash.is_a?(::Hash)
907
+ value_or_hash.values.all? { |value| values.include?(value) }
908
+ else
909
+ values.include?(value_or_hash)
910
+ end
911
+
912
+ if is_valid
913
+ is_present = @__child_nodes.inject(false) { |acc, child_node| child_node.to_isomorphism(scope, record, instance).nil? ? acc : true }
914
+
915
+ if is_present || options[:allow_blank]
916
+ instance
917
+ else
918
+ nil
919
+ end
920
+ else
921
+ nil
922
+ end
923
+ }
924
+ end
925
+ end
926
+
927
+ class GuardCollection < Isomorphic::Node::Guard
928
+ include Isomorphic::Node::Internal::InstanceMethodsForCollection
929
+ end
930
+
931
+ class GuardMember < Isomorphic::Node::Guard
932
+ include Isomorphic::Node::Internal::InstanceMethodsForMember
933
+ end
934
+
935
+ class Member < Isomorphic::Node::AbstractNode
936
+ include Isomorphic::Node::Internal::InstanceMethodsForMember
937
+
938
+ attr_reader :method_name
939
+
940
+ attr_reader :klass
941
+
942
+ def initialize(parent, method_name, klass, **options, &block)
943
+ super(parent, **options, &block)
944
+
945
+ unless klass.is_a?(::Class) && (klass.parents[-2] == parent.inflector.base)
946
+ raise Isomorphic::InvalidNodeClass.new(nil, klass)
947
+ end
948
+
949
+ @method_name = method_name
950
+ @klass = klass
951
+ end
952
+
953
+ def from_isomorphism(scope, instance, record = nil, xmlattr_acc = ::ActiveSupport::HashWithIndifferentAccess.new)
954
+ is_present = false
955
+
956
+ instance.send(method_name).try { |instance_for_node|
957
+ is_valid = instance_for_node.instance_of?(klass) && all_attributes_eql?(inflector.base, instance_for_node, options[:attributes])
958
+
959
+ if is_valid
960
+ is_present ||= @__child_nodes.inject(options[:allow_blank]) { |acc, child_node| child_node.from_isomorphism(scope, instance_for_node, record, xmlattr_acc).nil? ? acc : true }
961
+ end
962
+ }
963
+
964
+ if is_present
965
+ record
966
+ else
967
+ nil
968
+ end
969
+ end
970
+
971
+ def to_isomorphism(scope, record, instance = nil)
972
+ is_present = false
973
+
974
+ instance_for_node = instance.send(method_name) || factory.for(klass)
975
+
976
+ options[:attributes].try { |attributes|
977
+ factory.update_attributes(instance_for_node, attributes)
978
+ }
979
+
980
+ is_present ||= @__child_nodes.inject(false) { |acc, child_node| child_node.to_isomorphism(scope, record, instance_for_node).nil? ? acc : true }
981
+
982
+ if is_present || options[:allow_blank]
983
+ instance.send(:"#{method_name}=", instance_for_node)
984
+
985
+ instance_for_node
986
+ else
987
+ nil
988
+ end
989
+ end
990
+ end
991
+
992
+ class Namespace < Isomorphic::Node::AbstractNode
993
+ include Isomorphic::Node::Internal::InstanceMethodsForMember
994
+
995
+ attr_reader :namespace_name
996
+
997
+ attr_reader :lens
998
+
999
+ def initialize(parent, namespace_name, lens, **options, &block)
1000
+ super(parent, **options, &block)
1001
+
1002
+ unless (lens.is_a?(Isomorphic::Lens::Association) && lens.last_lens.is_a?(Isomorphic::Lens::Isomorphism)) || lens.is_a?(Isomorphic::Lens::Isomorphism)
1003
+ raise Isomorphic::LensInvalid.new(nil, lens)
1004
+ end
1005
+
1006
+ @namespace_name = namespace_name
1007
+
1008
+ @lens = lens
1009
+ end
1010
+
1011
+ def from_isomorphism(scope, instance, record = nil, xmlattr_acc = ::ActiveSupport::HashWithIndifferentAccess.new)
1012
+ new_scope = scope.child(namespace_name)
1013
+
1014
+ is_present = @__child_nodes.inject(false) { |acc, child_node| child_node.from_isomorphism(new_scope, instance, record, xmlattr_acc).nil? ? acc : true }
1015
+
1016
+ lens.set(record, new_scope.values(namespace_name), xmlattr_acc)
1017
+
1018
+ if is_present
1019
+ record
1020
+ else
1021
+ nil
1022
+ end
1023
+ end
1024
+
1025
+ def to_isomorphism(scope, record, instance = nil)
1026
+ new_scope = scope.child(namespace_name)
1027
+
1028
+ lens.get(record).try { |hash|
1029
+ hash.each do |key, value|
1030
+ new_scope.set(namespace_name, key, value)
1031
+ end
1032
+ }
1033
+
1034
+ is_present = @__child_nodes.inject(false) { |acc, child_node| child_node.to_isomorphism(new_scope, record, instance).nil? ? acc : true }
1035
+
1036
+ if is_present || options[:allow_blank]
1037
+ instance
1038
+ else
1039
+ nil
1040
+ end
1041
+ end
1042
+ end
1043
+
1044
+ class NamespaceAssociation < Isomorphic::Node::Association
1045
+ attr_reader :namespace_name, :key
1046
+
1047
+ def initialize(parent, namespace_name, key, **options, &block)
1048
+ super(parent, **options, &block)
1049
+
1050
+ @namespace_name = namespace_name
1051
+ @key = key
1052
+ end
1053
+
1054
+ protected
1055
+
1056
+ def _get(scope, record)
1057
+ scope.get(namespace_name, key)
1058
+ end
1059
+
1060
+ def _set(scope, record, instance_for_node, xmlattr_acc = ::ActiveSupport::HashWithIndifferentAccess.new)
1061
+ scope.set(namespace_name, key, instance_for_node)
1062
+ end
1063
+ end
1064
+
1065
+ class NamespaceAttribute < Isomorphic::Node::Attribute
1066
+ attr_reader :namespace_name, :key
1067
+
1068
+ def initialize(parent, namespace_name, key, method_name, xmlattr_class = nil, **options, &block)
1069
+ super(parent, method_name, xmlattr_class, **options, &block)
1070
+
1071
+ @namespace_name = namespace_name
1072
+ @key = key
1073
+ end
1074
+
1075
+ protected
1076
+
1077
+ def _get(scope, record)
1078
+ scope.get(namespace_name, key)
1079
+ end
1080
+
1081
+ def _set(scope, record, instance_for_node, xmlattr_acc = ::ActiveSupport::HashWithIndifferentAccess.new)
1082
+ scope.set(namespace_name, key, instance_for_node)
1083
+ end
1084
+ end
1085
+
1086
+ class ProcFrom < Isomorphic::Node::AbstractNode
1087
+ attr_reader :block
1088
+
1089
+ def initialize(parent, **options, &block)
1090
+ # super(parent, **options)
1091
+
1092
+ @block = block
1093
+ end
1094
+
1095
+ def from_isomorphism(scope, instance, record = nil, xmlattr_acc = ::ActiveSupport::HashWithIndifferentAccess.new)
1096
+ if block.nil?
1097
+ record
1098
+ else
1099
+ case block.arity
1100
+ when 1 then block.call(instance)
1101
+ when 2 then block.call(instance, record)
1102
+ when 3 then block.call(instance, record, xmlattr_acc)
1103
+ when 4 then block.call(instance, record, xmlattr_acc, scope)
1104
+ else instance.instance_exec(record, xmlattr_acc, scope, &block)
1105
+ end
1106
+ end
1107
+ end
1108
+
1109
+ def to_isomorphism(scope, record, instance = nil)
1110
+ instance
1111
+ end
1112
+ end
1113
+
1114
+ class ProcTo < Isomorphic::Node::AbstractNode
1115
+ attr_reader :block
1116
+
1117
+ def initialize(parent, **options, &block)
1118
+ # super(parent, **options)
1119
+
1120
+ @block = block
1121
+ end
1122
+
1123
+ def from_isomorphism(scope, instance, record = nil, xmlattr_acc = ::ActiveSupport::HashWithIndifferentAccess.new)
1124
+ record
1125
+ end
1126
+
1127
+ def to_isomorphism(scope, record, instance = nil)
1128
+ if block.nil?
1129
+ instance
1130
+ else
1131
+ case block.arity
1132
+ when 1 then block.call(record)
1133
+ when 2 then block.call(record, instance)
1134
+ when 3 then block.call(record, instance, scope)
1135
+ else record.instance_exec(instance, scope, &block)
1136
+ end
1137
+ end
1138
+ end
1139
+ end
1140
+
1141
+ class Root < Isomorphic::Node::AbstractNode
1142
+ attr_reader :record_class
1143
+
1144
+ attr_reader :klass
1145
+
1146
+ attr_reader :args
1147
+
1148
+ def initialize(factory, inflector, record_class, klass, *args, &block)
1149
+ @factory = factory
1150
+ @inflector = inflector
1151
+
1152
+ @record_class = record_class
1153
+ @klass = klass
1154
+
1155
+ super(nil, *args, &block)
1156
+
1157
+ unless klass.is_a?(::Class) && (klass.parents[-2] == inflector.base)
1158
+ raise Isomorphic::InvalidNodeClass.new(nil, klass)
1159
+ end
1160
+ end
1161
+
1162
+ def from_isomorphism(instance, record = nil, xmlattr_acc = ::ActiveSupport::HashWithIndifferentAccess.new)
1163
+ unless instance.instance_of?(klass)
1164
+ raise Isomorphic::InvalidNodeObject.new(nil, klass, instance)
1165
+ end
1166
+
1167
+ scope = Isomorphic::Node::Internal::Scope.new(nil, inflector)
1168
+
1169
+ is_valid = all_attributes_eql?(inflector.base, instance, options[:attributes])
1170
+
1171
+ if is_valid
1172
+ record_for_node = record || record_class.new
1173
+
1174
+ @__child_nodes.each do |child_node|
1175
+ child_node.from_isomorphism(scope, instance, record_for_node, xmlattr_acc)
1176
+ end
1177
+
1178
+ record_for_node
1179
+ else
1180
+ record
1181
+ end
1182
+ end
1183
+
1184
+ def to_isomorphism(record, instance = nil)
1185
+ scope = Isomorphic::Node::Internal::Scope.new(nil, inflector)
1186
+
1187
+ instance_for_node = instance || factory.for(klass)
1188
+
1189
+ options[:attributes].try { |attributes|
1190
+ factory.update_attributes(instance_for_node, attributes)
1191
+ }
1192
+
1193
+ is_present = @__child_nodes.inject(false) { |acc, child_node| child_node.to_isomorphism(scope, record, instance_for_node).nil? ? acc : true }
1194
+
1195
+ if is_present || options[:allow_blank]
1196
+ instance_for_node
1197
+ else
1198
+ nil
1199
+ end
1200
+ end
1201
+ end
1202
+
1203
+ class RootCollection < Isomorphic::Node::Root
1204
+ include Isomorphic::Node::Internal::InstanceMethodsForCollection
1205
+ end
1206
+
1207
+ class RootMember < Isomorphic::Node::Root
1208
+ include Isomorphic::Node::Internal::InstanceMethodsForMember
1209
+ end
1210
+ end
1211
+ end