isomorphic 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,173 @@
1
+ require "isomorphic/errors"
2
+
3
+ module Isomorphic
4
+ # Generic base class for Isomorphic factory errors.
5
+ #
6
+ # @abstract
7
+ class FactoryError < Isomorphic::IsomorphicError
8
+ end
9
+
10
+ # Raised when an Isomorphic factory cannot find a class.
11
+ class InvalidFactoryClass < Isomorphic::FactoryError
12
+ # @!attribute [r] klass
13
+ # @return [Class] the class
14
+ # @!attribute [r] const_name
15
+ # @return [#to_sym] the constant name
16
+ attr_reader :klass, :const_name
17
+
18
+ # Default constructor.
19
+ #
20
+ # @param message [#to_s] the message
21
+ # @param base [Module] the base module
22
+ # @param klass [Class] the class
23
+ # @param const_name [#to_sym] the constant name
24
+ def initialize(message = nil, base, klass, const_name)
25
+ super(message, base)
26
+
27
+ @klass, @const_name = klass, const_name
28
+ end
29
+ end
30
+
31
+ module Factory
32
+ # Generic base class for Isomorphic factories.
33
+ #
34
+ # @abstract Subclass and override {#const_get} and {#xmlattrs} to implement a custom class.
35
+ class AbstractFactory
36
+ # @!attribute [r] base
37
+ # @return [Module] the base module
38
+ attr_reader :base
39
+
40
+ # Default constructor
41
+ #
42
+ # @param base [Module] the base module
43
+ def initialize(base)
44
+ super()
45
+
46
+ @base = base
47
+ end
48
+
49
+ # Build a new instance of the given class.
50
+ #
51
+ # @param klass [Class] the class
52
+ # @param options [Hash<#to_sym, Object>] the options
53
+ # @option options [Hash<#to_sym, Object>] :attributes ({}) the attributes for the new instance
54
+ # @option options [Hash<#to_sym, #to_s>] :xmlattrs ({}) the XML attributes for the new instance
55
+ # @yieldparam instance [Object] the new instance
56
+ # @yieldreturn [void]
57
+ # @return [Object] the new instance
58
+ # @raise [Isomorphic::InvalidFactoryClass] if the given class cannot be found
59
+ def for(klass, **options, &block)
60
+ unless klass.is_a?(::Class) && (klass.parents[-2] == base)
61
+ raise Isomorphic::InvalidFactoryClass.new(nil, base, klass, nil)
62
+ end
63
+
64
+ instance = klass.new
65
+
66
+ if options.key?(:attributes)
67
+ update_attributes(instance, options[:attributes])
68
+ end
69
+
70
+ send(:xmlattrs).select { |xmlattr_name|
71
+ instance.respond_to?(:"xmlattr_#{xmlattr_name}=")
72
+ }.each do |xmlattr_name|
73
+ instance.send(:"xmlattr_#{xmlattr_name}=", send(:"xmlattr_#{xmlattr_name}_for", instance))
74
+ end
75
+
76
+ unless block.nil?
77
+ case block.arity
78
+ when 1 then block.call(instance)
79
+ else instance.instance_eval(&block)
80
+ end
81
+ end
82
+
83
+ instance
84
+ end
85
+
86
+ # Build a chain of new instances by reflecting on the instance methods for
87
+ # the given instance and then return the end of the chain.
88
+ #
89
+ # @param instance [Object] the instance
90
+ # @param method_names [Array<#to_sym>] the method names
91
+ # @param options [Hash<#to_sym, Object>] the options
92
+ # @option options [Boolean] :try (false) return +nil+ if any receiver in the chain is blank
93
+ # @option options [Hash<#to_sym, Object>] :attributes ({}) the attributes for the new instance
94
+ # @option options [Hash<#to_sym, #to_s>] :xmlattrs ({}) the XML attributes for the new instance
95
+ # @return [Object, nil] the new instance or +nil+
96
+ # @raise [Isomorphic::InvalidFactoryClass] if any class in the chain cannot be found
97
+ def path(instance, *method_names, **options)
98
+ method_names.inject([instance.class, instance]) { |pair, method_name|
99
+ orig_class, orig_instance = *pair
100
+
101
+ if orig_instance.nil? && options[:try]
102
+ [::NilClass, orig_instance]
103
+ else
104
+ s = method_name.to_s
105
+ const_name = s[0].upcase.concat(s[1..-1])
106
+
107
+ new_class = const_get(base, instance_class, const_name)
108
+
109
+ unless new_class.is_a?(::Class)
110
+ raise Isomorphic::InvalidFactoryClass.new(nil, base, orig_class, const_name)
111
+ end
112
+
113
+ new_instance = orig_instance.send(method_name) || (options[:try] ? nil : orig_instance.send(:"#{method_name}=", send(:for, new_class)))
114
+
115
+ [new_class, new_instance]
116
+ end
117
+ }[1]
118
+ end
119
+
120
+ # Is the chain of instances present?
121
+ #
122
+ # @param instance [Object] the instance
123
+ # @param method_names [Array<#to_sym>] the method names
124
+ # @return [Boolean] +true+ if the chain of instances is present; otherwise, +false+
125
+ # @raise [Isomorphic::InvalidFactoryClass] if any class in the chain cannot be found
126
+ def path?(instance, *method_names)
127
+ !path(instance, *method_names, try: true).nil?
128
+ end
129
+
130
+ # Updates the attributes of the instance from the passed-in hash.
131
+ #
132
+ # @param instance [Object] the instance
133
+ # @param attributes [Hash<#to_sym, Object>] the attributes
134
+ # @return [void]
135
+ #
136
+ # @note Before assignment, attributes from the passed-in hash are duplicated.
137
+ def update_attributes(instance, attributes = {})
138
+ attributes.each do |method_name, value|
139
+ instance.send(:"#{method_name}=", ::Marshal.load(::Marshal.dump(value)))
140
+ end
141
+
142
+ return
143
+ end
144
+
145
+ # Returns the array of XML attribute names that are accepted by this factory.
146
+ #
147
+ # @return [Array<#to_sym>] the XML attribute names
148
+ #
149
+ # @note For each XML attribute name, e.g., +name+, a corresponding instance method +#xmlattr_{name}_for(instance)+ must be defined.
150
+ def xmlattrs
151
+ []
152
+ end
153
+
154
+ protected
155
+
156
+ # Checks for a constant with the given name in the base module.
157
+ #
158
+ # @param base [Module] the base module
159
+ # @param klass [Class] the class
160
+ # @param const_name [#to_sym] the constant name
161
+ # @return [Class, Module, nil] the constant with the given name or +nil+ if not defined
162
+ #
163
+ # @note Subclasses override this instance method to implement custom dereferencing strategies for constants.
164
+ def const_get(base, klass, const_name)
165
+ if klass.const_defined?(const_name)
166
+ klass.const_get(const_name)
167
+ else
168
+ nil
169
+ end
170
+ end
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,62 @@
1
+ require "active_support/hash_with_indifferent_access"
2
+
3
+ module Isomorphic
4
+ # Implements a hash where keys +:foo+, +"foo"+ and +Foo+ are considered to be the same.
5
+ class HashWithIndifferentAccess < ::ActiveSupport::HashWithIndifferentAccess
6
+ # @!attribute [r] inflector
7
+ # @return [Isomorphic::Inflector::AbstractInflector] the inflector
8
+ attr_reader :inflector
9
+
10
+ # Default constructor.
11
+ #
12
+ # @param inflector [Isomorphic::Inflector::AbstractInflector] the inflector
13
+ # @param constructor [Hash, #to_hash] the hash or constructor for a hash
14
+ # @raise [Isomorphic::InflectorError] if a key in the hash or the constructor for the hash is invalid
15
+ def initialize(inflector, constructor = {})
16
+ @inflector = inflector
17
+
18
+ super(constructor)
19
+ end
20
+
21
+ # Same as {Hash#[]} where the key passed as argument can be either a string, a symbol or a class.
22
+ #
23
+ # @param key [Object] the key
24
+ # @return [Object] the value
25
+ # @raise [Isomorphic::InflectorError] if the key is invalid
26
+ def [](key)
27
+ super(convert_key(key))
28
+ end
29
+
30
+ # Same as {Hash#assoc} where the key passed as argument can be either a string, a symbol or a class.
31
+ #
32
+ # @param key [Object] the key
33
+ # @return [Array<Object>, nil] the key-value pair or +nil+ if the key is not present
34
+ # @raise [Isomorphic::InflectorError] if the key is invalid
35
+ def assoc(key)
36
+ super(convert_key(key))
37
+ end
38
+
39
+ if ::Hash.new.respond_to?(:dig)
40
+ # Same as {Hash#dig} where the key passed as argument can be either a string, a symbol or a class.
41
+ #
42
+ # @param args [Array<Object>] the keys
43
+ # @return [Object, nil] the value or nil
44
+ # @raise [Isomorphic::InflectorError] if a key is invalid
45
+ def dig(*args)
46
+ args[0] = convert_key(args[0]) if args.size > 0
47
+ super(*args)
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ # Inflect upon the given key.
54
+ #
55
+ # @param key [Object] the key
56
+ # @return [String] the inflected key
57
+ # @raise [Isomorphic::InflectorError] if the key is invalid
58
+ def convert_key(key)
59
+ inflector.isomorphism(key)
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,180 @@
1
+ require "active_support/inflector"
2
+
3
+ require "isomorphic/errors"
4
+ require "isomorphic/hash_with_indifferent_access"
5
+
6
+ module Isomorphic
7
+ # Generic base class for Isomorphic inflector errors.
8
+ #
9
+ # @abstract
10
+ class InflectorError < Isomorphic::IsomorphicError
11
+ end
12
+
13
+ # Raised when an Isomorphic inflector cannot find a class.
14
+ class InvalidInflectionClass < Isomorphic::InflectorError
15
+ # @!attribute [r] klass
16
+ # @return [Class] the class
17
+ attr_reader :klass
18
+
19
+ # Default constructor.
20
+ #
21
+ # @param message [#to_s] the message
22
+ # @param base [Module] the base module
23
+ # @param klass [Class] the class
24
+ def initialize(message = nil, base, klass)
25
+ super(message, base)
26
+
27
+ @klass = klass
28
+ end
29
+ end
30
+
31
+ # Raised when an Isomorphic inflector cannot find an instance method by name.
32
+ class InvalidInflectionMethodName < Isomorphic::InflectorError
33
+ # @!attribute [r] method_name
34
+ # @return [#to_sym] the method name
35
+ attr_reader :method_name
36
+
37
+ # Default constructor.
38
+ #
39
+ # @param message [#to_s] the message
40
+ # @param base [Module] the base module
41
+ # @param method_name [#to_sym] the method name
42
+ def initialize(message = nil, base, method_name)
43
+ super(message, base)
44
+
45
+ @method_name = method_name
46
+ end
47
+ end
48
+
49
+ # Raised when an Isomorphic inflector cannot find an inflectable term.
50
+ class InvalidInflectionTerm < Isomorphic::InflectorError
51
+ # @!attribute [r] term
52
+ # @return [Object] the inflectable term
53
+ attr_reader :term
54
+
55
+ # Default constructor.
56
+ #
57
+ # @param message [#to_s] the message
58
+ # @param base [Module] the base module
59
+ # @param term [Object] the inflectable term
60
+ def initialize(message = nil, base, term)
61
+ super(message, base)
62
+
63
+ @term = term
64
+ end
65
+ end
66
+
67
+ module Inflector
68
+ # Generic base class for Isomorphic inflectors.
69
+ #
70
+ # @abstract
71
+ class AbstractInflector
72
+ # @!attribute [r] base
73
+ # @return [Module] the base module
74
+ attr_reader :base
75
+
76
+ # Default constructor
77
+ #
78
+ # @param base [Module] the base module
79
+ def initialize(base)
80
+ super()
81
+
82
+ @base = base
83
+ end
84
+
85
+ # Inflect upon the given hash or constructor for a hash.
86
+ #
87
+ # @param constructor [Hash, #to_hash] the hash or constructor for a hash
88
+ # @return [Isomorphic::HashWithIndifferentAccess] the inflected hash
89
+ # @raise [Isomorphic::InflectorError] if a key in the given hash or constructor for a hash is invalid
90
+ def convert_hash(constructor = {})
91
+ Isomorphic::HashWithIndifferentAccess.new(self, constructor)
92
+ end
93
+
94
+ # Inflect upon the given terms.
95
+ #
96
+ # @example Inflect upon a {String}
97
+ # Isomorphic::Inflector.new(Foo).isomorphism("bar") #=> "foo_bar"
98
+ # @example Inflect upon a {Symbol}
99
+ # Isomorphic::Inflector.new(Foo).isomorphism(:bar) #=> "foo_bar"
100
+ # @example Inflect upon a {Class}
101
+ # Isomorphic::Inflector.new(Foo).isomorphism(Foo::Bar) #=> "foo_bar"
102
+ # @example Inflect upon an {Array} of inflectable terms
103
+ # Isomorphic::Inflector.new(Foo).isomorphism(["bar", "fum", "baz"]) #=> "foo_bar_and_foo_fum_and_foo_baz"
104
+ # @example Inflect upon an inflectable term-alias pair
105
+ # Isomorphic::Inflector.new(Foo).isomorphism([["bar", "ex1"]]) #=> "foo_bar_as_ex1"
106
+ # @example Inflect upon an {Array} of inflectable term-alias pairs
107
+ # Isomorphic::Inflector.new(Foo).isomorphism([["bar", "ex1"], ["bar", "ex2"], ["bar", "ex3"]]) #=> "foo_bar_as_ex1_and_foo_bar_as_ex2_and_foo_bar_as_ex3"
108
+ #
109
+ # @param terms [Array<Object>] the inflectable terms
110
+ # @return [String] the inflection
111
+ # @raise [Isomorphic::InflectorError] if an inflectable term is invalid
112
+ def isomorphism(terms)
113
+ isomorphism_for(terms)
114
+ end
115
+
116
+ protected
117
+
118
+ # Converts the name of the given class.
119
+ #
120
+ # @param klass [Class] the class
121
+ # @return [String] the underscored name of the class, where occurrences of +"/"+ are replaced with +"_"+
122
+ def convert_class(klass)
123
+ ::ActiveSupport::Inflector.underscore(klass.name).gsub("/", "_")
124
+ end
125
+
126
+ private
127
+
128
+ def isomorphism_for(terms)
129
+ unless terms.is_a?(::Array)
130
+ terms = [terms]
131
+ end
132
+
133
+ terms.collect { |term|
134
+ case term
135
+ when ::Array then isomorphism_for_array(term)
136
+ when ::Class then isomorphism_for_class(term)
137
+ when ::String, ::Symbol then isomorphism_for_method_name(term)
138
+ else raise Isomorphic::InvalidInflectionTerm.new(nil, base, term)
139
+ end
140
+ }.join("_and_")
141
+ end
142
+
143
+ def isomorphism_for_array(array)
144
+ method_name = \
145
+ case array[0]
146
+ when ::Class then isomorphism_for_class(array[0])
147
+ when ::String, ::Symbol then isomorphism_for_method_name(array[0])
148
+ else raise Isomorphic::InvalidInflectionTerm.new(nil, base, array)
149
+ end
150
+
151
+ method_suffix = \
152
+ case array[1]
153
+ when ::NilClass then ""
154
+ when ::String, ::Symbol then ::Kernel.sprintf("_as_%s", array[1])
155
+ else raise Isomorphic::InvalidInflectionTerm.new(nil, base, array)
156
+ end
157
+
158
+ "#{method_name}#{method_suffix}"
159
+ end
160
+
161
+ def isomorphism_for_class(klass)
162
+ unless klass.is_a?(::Class) && (klass.parents[-2] == base)
163
+ raise Isomorphic::InvalidInflectionClass.new(nil, base, klass)
164
+ end
165
+
166
+ convert_class(klass)
167
+ end
168
+
169
+ def isomorphism_for_method_name(method_name)
170
+ s = method_name.to_s
171
+
172
+ unless s.starts_with?(::Kernel.sprintf("%s_", convert_class(base)))
173
+ raise Isomorphic::InvalidInflectionMethodName.new(nil, base, method_name)
174
+ end
175
+
176
+ s
177
+ end
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,433 @@
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/errors"
6
+
7
+ module Isomorphic
8
+ # Generic base class for Isomorphic lens errors.
9
+ #
10
+ # @abstract
11
+ class LensError < Isomorphic::IsomorphicError
12
+ end
13
+
14
+ # Raised when an Isomorphic lens is invalid.
15
+ class InvalidLens < Isomorphic::LensError
16
+ # @!attribute [r] lens
17
+ # @return [Isomorphic::Lens::AbstractLens] the lens
18
+ attr_reader :lens
19
+
20
+ # Default constructor.
21
+ #
22
+ # @param message [String] the message
23
+ # @param lens [Isomorphic::Lens::AbstractLens] the lens
24
+ def initialize(message = nil, lens)
25
+ super(message)
26
+
27
+ @lens = lens
28
+ end
29
+ end
30
+
31
+ module Lens
32
+ module Internal
33
+ # Included when the base class has a +#factory+ and +#inflector+, and hence, can build Isomorphic lenses.
34
+ module InstanceMethodsForLens
35
+ extend ::ActiveSupport::Concern
36
+
37
+ # Build a lens for the Active Record association with the given name.
38
+ #
39
+ # @param association [#to_s] the association name
40
+ # @return [Isomorphic::Lens::Association] the lens
41
+ def reflect_on_association(association)
42
+ Isomorphic::Lens::Association.new(factory, inflector, association, nil)
43
+ end
44
+
45
+ # Build a lens for the attribute with the given name.
46
+ #
47
+ # @param attribute_name [#to_s] the attribute name
48
+ # @param to [Proc] the optional modifier for after the getter
49
+ # @param from [Proc] the optional modifier for before the setter
50
+ # @return [Isomorphic::Lens::Attribute] the lens
51
+ def reflect_on_attribute(attribute_name, to = nil, from = nil)
52
+ Isomorphic::Lens::Attribute.new(factory, inflector, attribute_name, to, from)
53
+ end
54
+
55
+ # Build a lens for the given inflectable terms and optional arguments.
56
+ #
57
+ # @param terms [Array<Object>] the inflectable terms
58
+ # @param args [Array<Object>] the arguments for the isomorphism
59
+ # @return [Isomorphic::Lens::Isomorphism] the lens
60
+ # @raise [Isomorphic::InflectorError] if an inflectable term is invalid
61
+ def reflect_on_isomorphism(terms, *args)
62
+ Isomorphic::Lens::Isomorphism.new(factory, inflector, terms, *args)
63
+ end
64
+ end
65
+ end
66
+
67
+ # Generic base class for Isomorphic lenses.
68
+ #
69
+ # @abstract Subclass and override {#_get} and {#set} to implement a custom class.
70
+ class AbstractLens
71
+ # @!attribute [r] factory
72
+ # @return [Isomorphic::Factory::AbstractFactory] the Isomorphic factory
73
+ # @!attribute [r] inflector
74
+ # @return [Isomorphic::Inflector::AbstractInflector] the Isomorphic inflector
75
+ attr_reader :factory, :inflector
76
+
77
+ # Default constructor.
78
+ #
79
+ # @param factory [Isomorphic::Factory::AbstractFactory] the Isomorphic factory
80
+ # @param inflector [Isomorphic::Inflector::AbstractInflector] the Isomorphic inflector
81
+ def initialize(factory, inflector)
82
+ super()
83
+
84
+ @factory, @inflector = factory, inflector
85
+ end
86
+
87
+ # Getter.
88
+ #
89
+ # @param record_or_hash [ActiveRecord::Base, Hash<ActiveRecord::Base, ActiveRecord::Base>] the Active Record record or hash of records
90
+ # @return [Object] the value of the getter
91
+ # @raise [Isomorphic::InvalidLens] if the lens is invalid
92
+ def get(record_or_hash)
93
+ if record_or_hash.is_a?(::Hash)
94
+ record_or_hash.each_pair.inject({}) { |acc, pair|
95
+ association_record, record = *pair
96
+
97
+ acc[association_record] = _get(record)
98
+ acc
99
+ }
100
+ else
101
+ _get(record_or_hash)
102
+ end
103
+ end
104
+
105
+ # Setter.
106
+ #
107
+ # @param record [ActiveRecord::Base] the Active Record record
108
+ # @param isomorphism_instance [Object] the instance
109
+ # @param xmlattr_acc [ActiveSupport::HashWithIndifferentAccess] the accumulator of XML attributes by name
110
+ # @return [Object] the value of the setter
111
+ # @raise [Isomorphic::InvalidLens] if the lens is invalid
112
+ def set(record, isomorphism_instance, xmlattr_acc = ::ActiveSupport::HashWithIndifferentAccess.new)
113
+ raise ::NotImplementedError
114
+ end
115
+
116
+ protected
117
+
118
+ def _get(record)
119
+ raise ::NotImplementedError
120
+ end
121
+ end
122
+
123
+ # An Isomorphic lens for an Active Record association.
124
+ class Association < Isomorphic::Lens::AbstractLens
125
+ # @!attribute [r] association
126
+ # @return [#to_s] the association name
127
+ attr_reader :association
128
+
129
+ # @!attribute [r] next_lens
130
+ # @return [Isomorphic::Lens::AbstractLens] the next lens or +nil+ if this is the last lens in the composition
131
+ attr_reader :next_lens
132
+
133
+ # Default constructor.
134
+ #
135
+ # @param factory [Isomorphic::Factory::AbstractFactory] the Isomorphic factory
136
+ # @param inflector [Isomorphic::Inflector::AbstractInflector] the Isomorphic inflector
137
+ # @param association [#to_s] the association name
138
+ # @param next_lens [Isomorphic::Lens::AbstractLens] the next lens or +nil+ if this is the last lens in the composition
139
+ def initialize(factory, inflector, association, next_lens = nil)
140
+ super(factory, inflector)
141
+
142
+ @association, @next_lens = association, next_lens
143
+ end
144
+
145
+ # Returns the last lens in the composition.
146
+ #
147
+ # @return [Isomorphic::Lens::AbstractLens] the last lens or +nil+ if this is the last lens in the composition
148
+ def last_lens
149
+ next_lens.try { |current_lens|
150
+ while current_lens.is_a?(self.class)
151
+ current_lens = current_lens.next_lens
152
+ end
153
+ }.try { |current_lens|
154
+ current_lens.next_lens
155
+ }
156
+ end
157
+
158
+ def set(record, isomorphism_instance, xmlattr_acc = ::ActiveSupport::HashWithIndifferentAccess.new)
159
+ unless next_lens.is_a?(Isomorphic::Lens::AbstractLens)
160
+ raise Isomorphic::InvalidLens.new(nil, self)
161
+ end
162
+
163
+ reflection = record.class.reflect_on_association(association)
164
+
165
+ case reflection.macro
166
+ when :has_many, :has_and_belongs_to_many
167
+ raise Isomorphic::InvalidLens.new("invalid macro: #{reflection.macro}", self)
168
+ when :has_one, :belongs_to
169
+ next_lens.set(record.send(association) || record.send(:"build_#{association}"), isomorphism_instance, xmlattr_acc)
170
+ else
171
+ raise Isomorphic::InvalidLens.new("unknown macro: #{reflection.macro}", self)
172
+ end
173
+ end
174
+
175
+ # Compose this lens with a new lens for the Active Record association with the given name.
176
+ #
177
+ # @param association [#to_s] the association name
178
+ # @return [self]
179
+ def reflect_on_association(association)
180
+ append do
181
+ self.class.new(factory, inflector, association, nil)
182
+ end
183
+ end
184
+
185
+ # Compose this lens with a new lens for the attribute with the given name.
186
+ #
187
+ # @param attribute_name [#to_s] the attribute name
188
+ # @param to [Proc] the optional modifier for after the getter
189
+ # @param from [Proc] the optional modifier for before the setter
190
+ # @return [self]
191
+ def reflect_on_attribute(attribute_name, to = nil, from = nil)
192
+ append do
193
+ Isomorphic::Lens::Attribute.new(factory, inflector, attribute_name, to, from)
194
+ end
195
+ end
196
+
197
+ # Compose this lens with a new lens for the given inflectable terms and optional arguments.
198
+ #
199
+ # @param terms [Array<Object>] the inflectable terms
200
+ # @param args [Array<Object>] the arguments for the isomorphism
201
+ # @return [self]
202
+ # @raise [Isomorphic::InflectorError] if an inflectable term is invalid
203
+ def reflect_on_isomorphism(terms, *args)
204
+ append(with_next_lens: true) do |other|
205
+ Isomorphic::Lens::IsomorphismAssociation.new(factory, inflector, other.association, terms, *args)
206
+ end
207
+ end
208
+
209
+ protected
210
+
211
+ def _get(record)
212
+ unless next_lens.is_a?(Isomorphic::Lens::AbstractLens)
213
+ raise Isomorphic::InvalidLens.new(nil, self)
214
+ end
215
+
216
+ reflection = record.class.reflect_on_association(association)
217
+
218
+ case reflection.macro
219
+ when :has_many, :has_and_belongs_to_many
220
+ raise Isomorphic::InvalidLens.new("invalid macro: #{reflection.macro}", self)
221
+ when :has_one, :belongs_to
222
+ record.send(association).try { |association_record|
223
+ next_lens.get(association_record)
224
+ }
225
+ else
226
+ raise Isomorphic::InvalidLens.new("unknown macro: #{reflection.macro}", self)
227
+ end
228
+ end
229
+
230
+ private
231
+
232
+ def append(**options, &block)
233
+ others = [self]
234
+ current_lens = next_lens
235
+ while current_lens.is_a?(self.class)
236
+ others << current_lens
237
+ current_lens = current_lens.next_lens
238
+ end
239
+
240
+ if options[:with_next_lens]
241
+ if others.length == 1
242
+ block.call(others[0])
243
+ else
244
+ others[0..-2].reverse.inject(block.call(others[-1])) { |acc, other|
245
+ self.class.new(factory, inflector, other.association, acc)
246
+ }
247
+ end
248
+ else
249
+ others.reverse.inject(block.call) { |acc, other|
250
+ self.class.new(factory, inflector, other.association, acc)
251
+ }
252
+ end
253
+ end
254
+ end
255
+
256
+ # An Isomorphic lens for an attribute.
257
+ class Attribute < Isomorphic::Lens::AbstractLens
258
+ # @return [Proc] the identity morphism (returns the argument)
259
+ IDENTITY = ::Proc.new { |x| x }.freeze
260
+
261
+ # @!attribute [r] attribute_name
262
+ # @return [String] the attribute name
263
+ attr_reader :attribute_name
264
+
265
+ # @!attribute [r] to
266
+ # @return [Proc] the modifier for after the getter
267
+ # @!attribute [r] from
268
+ # @return [Proc] the modifier for before the setter
269
+ attr_reader :to, :from
270
+
271
+ # Default constructor.
272
+ #
273
+ # @param factory [Isomorphic::Factory::AbstractFactory] the Isomorphic factory
274
+ # @param inflector [Isomorphic::Inflector::AbstractInflector] the Isomorphic inflector
275
+ # @param attribute_name [#to_s] the attribute name
276
+ # @param to [Proc] the modifier for after the getter
277
+ # @param from [Proc] the modifier for before the setter
278
+ def initialize(factory, inflector, attribute_name, to = nil, from = nil)
279
+ super(factory, inflector)
280
+
281
+ @attribute_name = attribute_name
282
+
283
+ @to = to || IDENTITY
284
+ @from = from || IDENTITY
285
+ end
286
+
287
+ def set(record, isomorphism_instance, xmlattr_acc = ::ActiveSupport::HashWithIndifferentAccess.new)
288
+ args = case from.arity
289
+ when 1 then [isomorphism_instance]
290
+ else [isomorphism_instance, xmlattr_acc]
291
+ end
292
+
293
+ record.instance_exec(*args, &from).try { |value|
294
+ record.send(:"#{attribute_name}=", value)
295
+ }
296
+ end
297
+
298
+ protected
299
+
300
+ def _get(record)
301
+ record.instance_exec(record.send(attribute_name), &to)
302
+ end
303
+ end
304
+
305
+ # An Isomorphic lens for inflectable terms.
306
+ class Isomorphism < Isomorphic::Lens::AbstractLens
307
+ # @!attribute [r] terms
308
+ # @return [Array<Object>] the inflectable terms
309
+ # @!attribute [r] args
310
+ # @return [Array<Object>] the arguments for the isomorphism
311
+ attr_reader :terms, :args
312
+
313
+ # @!attribute [r] method_name
314
+ # @return [#to_sym] the instance method name that was inflected from the given inflectable terms
315
+ attr_reader :method_name
316
+
317
+ # Default constructor.
318
+ #
319
+ # @param factory [Isomorphic::Factory::AbstractFactory] the Isomorphic factory
320
+ # @param inflector [Isomorphic::Inflector::AbstractInflector] the Isomorphic inflector
321
+ # @param terms [Array<Object>] the inflectable terms
322
+ # @param args [Array<Object>] the arguments for the isomorphism
323
+ # @raise [Isomorphic::InflectorError] if an inflectable term is invalid
324
+ def initialize(factory, inflector, terms, *args)
325
+ super(factory, inflector)
326
+
327
+ @terms = terms
328
+ @args = args
329
+
330
+ @method_name = inflector.isomorphism(terms)
331
+ end
332
+
333
+ def set(record, isomorphism_instance, xmlattr_acc = ::ActiveSupport::HashWithIndifferentAccess.new)
334
+ (record.class.try(:"has_#{method_name}?") ? record.class.send(:"from_#{method_name}", isomorphism_instance, record, xmlattr_acc, *args) : record.class.send(:"from_#{method_name}", isomorphism_instance, *args)).try { |association_record|
335
+ factory.xmlattrs.try(:each) do |xmlattr_name|
336
+ isomorphism_instance.try(:"xmlattr_#{xmlattr_name}").try { |xmlattr_value|
337
+ xmlattr_acc[xmlattr_name] ||= {}
338
+ xmlattr_acc[xmlattr_name][xmlattr_value] = association_record
339
+ }
340
+ end
341
+
342
+ association_record
343
+ }
344
+ end
345
+
346
+ protected
347
+
348
+ def _get(record)
349
+ record.class.try(:"has_#{method_name}?") ? record.send(:"to_#{method_name}", nil, *args) : record.send(:"to_#{method_name}", *args)
350
+ end
351
+ end
352
+
353
+ # An Isomorphic lens for inflectable terms whose state is determined by an Active Record association.
354
+ class IsomorphismAssociation < Isomorphic::Lens::Isomorphism
355
+ # @!attribute [r] association
356
+ # @return [#to_s] the association name
357
+ attr_reader :association
358
+
359
+ # Default constructor.
360
+ #
361
+ # @param factory [Isomorphic::Factory::AbstractFactory] the Isomorphic factory
362
+ # @param inflector [Isomorphic::Inflector::AbstractInflector] the Isomorphic inflector
363
+ # @param association [#to_s] the association name
364
+ # @param args [Array<Object>] the arguments for the isomorphism
365
+ def initialize(factory, inflector, association, *args)
366
+ super(factory, inflector, *args)
367
+
368
+ @association = association
369
+ end
370
+
371
+ def set(record, isomorphism_instance, xmlattr_acc = ::ActiveSupport::HashWithIndifferentAccess.new)
372
+ if association.nil?
373
+ super(record, isomorphism_instance)
374
+ else
375
+ reflection = record.class.reflect_on_association(association)
376
+
377
+ (reflection.klass.try(:"has_#{method_name}?") ? reflection.klass.send(:"from_#{method_name}", isomorphism_instance, nil, xmlattr_acc, *args) : reflection.klass.send(:"from_#{method_name}", isomorphism_instance, *args)).try { |association_record|
378
+ case reflection.macro
379
+ when :has_many, :has_and_belongs_to_many
380
+ factory.xmlattrs.try(:each) do |xmlattr_name|
381
+ isomorphism_instance.try(:"xmlattr_#{xmlattr_name}").try { |xmlattr_value|
382
+ xmlattr_acc[xmlattr_name] ||= {}
383
+ xmlattr_acc[xmlattr_name][xmlattr_value] = association_record
384
+ }
385
+ end
386
+
387
+ record.send(association).send(:<<, association_record)
388
+ when :has_one, :belongs_to
389
+ factory.xmlattrs.try(:each) do |xmlattr_name|
390
+ isomorphism_instance.try(:"xmlattr_#{xmlattr_name}").try { |xmlattr_value|
391
+ xmlattr_acc[xmlattr_name] ||= {}
392
+ xmlattr_acc[xmlattr_name][xmlattr_value] = association_record
393
+ }
394
+ end
395
+
396
+ record.send(:"#{association}=", association_record)
397
+ else
398
+ raise Isomorphic::InvalidLens.new("unknown macro: #{reflection.macro}", self)
399
+ end
400
+
401
+ association_record
402
+ }
403
+ end
404
+ end
405
+
406
+ protected
407
+
408
+ def _get(record)
409
+ if association.nil?
410
+ super(record)
411
+ else
412
+ reflection = record.class.reflect_on_association(association)
413
+
414
+ case reflection.macro
415
+ when :has_many, :has_and_belongs_to_many
416
+ record.send(association).try { |collection_proxy|
417
+ collection_proxy.to_a.inject({}) { |acc, association_record|
418
+ acc[association_record] = reflection.klass.try(:"has_#{method_name}?") ? association_record.send(:"to_#{method_name}", nil, *args) : association_record.send(:"to_#{method_name}", *args)
419
+ acc
420
+ }
421
+ }
422
+ when :has_one, :belongs_to
423
+ record.send(association).try { |association_record|
424
+ reflection.klass.try(:"has_#{method_name}?") ? association_record.send(:"to_#{method_name}", nil, *args) : association_record.send(:"to_#{method_name}", *args)
425
+ }
426
+ else
427
+ raise Isomorphic::InvalidLens.new("unknown macro: #{reflection.macro}", self)
428
+ end
429
+ end
430
+ end
431
+ end
432
+ end
433
+ end