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.
- checksums.yaml +7 -0
- data/.gitignore +53 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +32 -0
- data/README.md +228 -0
- data/Rakefile +2 -0
- data/WARRANTY.txt +22 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/isomorphic.gemspec +38 -0
- data/lib/isomorphic.rb +13 -0
- data/lib/isomorphic/errors.rb +20 -0
- data/lib/isomorphic/factory.rb +173 -0
- data/lib/isomorphic/hash_with_indifferent_access.rb +62 -0
- data/lib/isomorphic/inflector.rb +180 -0
- data/lib/isomorphic/lens.rb +433 -0
- data/lib/isomorphic/memoization.rb +135 -0
- data/lib/isomorphic/model.rb +83 -0
- data/lib/isomorphic/node.rb +1211 -0
- data/lib/isomorphic/version.rb +3 -0
- metadata +105 -0
@@ -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
|