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