isomorphic 0.1.0

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