functional-ruby 0.7.7 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +92 -152
- data/doc/memo.txt +192 -0
- data/doc/pattern_matching.txt +485 -0
- data/doc/protocol.txt +221 -0
- data/doc/record.txt +144 -0
- data/doc/thread_safety.txt +8 -0
- data/lib/functional.rb +48 -18
- data/lib/functional/abstract_struct.rb +161 -0
- data/lib/functional/delay.rb +117 -0
- data/lib/functional/either.rb +222 -0
- data/lib/functional/memo.rb +93 -0
- data/lib/functional/method_signature.rb +72 -0
- data/lib/functional/option.rb +209 -0
- data/lib/functional/pattern_matching.rb +117 -100
- data/lib/functional/protocol.rb +157 -0
- data/lib/functional/protocol_info.rb +193 -0
- data/lib/functional/record.rb +155 -0
- data/lib/functional/type_check.rb +112 -0
- data/lib/functional/union.rb +152 -0
- data/lib/functional/version.rb +3 -1
- data/spec/functional/abstract_struct_shared.rb +154 -0
- data/spec/functional/complex_pattern_matching_spec.rb +205 -0
- data/spec/functional/configuration_spec.rb +17 -0
- data/spec/functional/delay_spec.rb +147 -0
- data/spec/functional/either_spec.rb +237 -0
- data/spec/functional/memo_spec.rb +207 -0
- data/spec/functional/option_spec.rb +292 -0
- data/spec/functional/pattern_matching_spec.rb +279 -276
- data/spec/functional/protocol_info_spec.rb +444 -0
- data/spec/functional/protocol_spec.rb +274 -0
- data/spec/functional/record_spec.rb +175 -0
- data/spec/functional/type_check_spec.rb +103 -0
- data/spec/functional/union_spec.rb +110 -0
- data/spec/spec_helper.rb +6 -4
- metadata +55 -45
- data/lib/functional/behavior.rb +0 -138
- data/lib/functional/behaviour.rb +0 -2
- data/lib/functional/catalog.rb +0 -487
- data/lib/functional/collection.rb +0 -403
- data/lib/functional/inflect.rb +0 -127
- data/lib/functional/platform.rb +0 -120
- data/lib/functional/search.rb +0 -132
- data/lib/functional/sort.rb +0 -41
- data/lib/functional/utilities.rb +0 -189
- data/md/behavior.md +0 -188
- data/md/catalog.md +0 -32
- data/md/collection.md +0 -32
- data/md/inflect.md +0 -32
- data/md/pattern_matching.md +0 -512
- data/md/platform.md +0 -32
- data/md/search.md +0 -32
- data/md/sort.md +0 -32
- data/md/utilities.md +0 -55
- data/spec/functional/behavior_spec.rb +0 -528
- data/spec/functional/catalog_spec.rb +0 -1206
- data/spec/functional/collection_spec.rb +0 -752
- data/spec/functional/inflect_spec.rb +0 -85
- data/spec/functional/integration_spec.rb +0 -205
- data/spec/functional/platform_spec.rb +0 -501
- data/spec/functional/search_spec.rb +0 -187
- data/spec/functional/sort_spec.rb +0 -61
- data/spec/functional/utilities_spec.rb +0 -277
@@ -0,0 +1,157 @@
|
|
1
|
+
require_relative 'protocol_info'
|
2
|
+
|
3
|
+
module Functional
|
4
|
+
|
5
|
+
# An exception indicating a problem during protocol processing.
|
6
|
+
ProtocolError = Class.new(StandardError)
|
7
|
+
|
8
|
+
# Specify a new protocol or retrieve the specification of an existing
|
9
|
+
# protocol.
|
10
|
+
#
|
11
|
+
# When called without a block the global protocol registry will be searched
|
12
|
+
# for a protocol with the matching name. If found the corresponding
|
13
|
+
# {Functional::ProtocolInfo} object will be returned. If not found `nil` will
|
14
|
+
# be returned.
|
15
|
+
#
|
16
|
+
# When called with a block, a new protocol with the given name will be
|
17
|
+
# created and the block will be processed to provide the specifiction.
|
18
|
+
# When successful the new {Functional::ProtocolInfo} object will be returned.
|
19
|
+
# An exception will be raised if a protocol with the same name already
|
20
|
+
# exists.
|
21
|
+
#
|
22
|
+
# @example
|
23
|
+
# Functional::SpecifyProtocol(:Queue) do
|
24
|
+
# instance_method :push, 1
|
25
|
+
# instance_method :pop, 0
|
26
|
+
# instance_method :length, 0
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# @param [Symbol] name The global name of the new protocol
|
30
|
+
# @yield The protocol definition
|
31
|
+
# @return [Functional::ProtocolInfo] the newly created or already existing
|
32
|
+
# protocol specification
|
33
|
+
#
|
34
|
+
# @raise [Functional::ProtocolError] when attempting to specify a protocol
|
35
|
+
# that has already been specified.
|
36
|
+
#
|
37
|
+
# @see Functional::Protocol
|
38
|
+
def SpecifyProtocol(name, &block)
|
39
|
+
name = name.to_sym
|
40
|
+
protocol_info = Protocol.class_variable_get(:@@info)[name]
|
41
|
+
|
42
|
+
return protocol_info unless block_given?
|
43
|
+
|
44
|
+
if block_given? && protocol_info
|
45
|
+
raise ProtocolError.new(":#{name} has already been defined")
|
46
|
+
end
|
47
|
+
|
48
|
+
info = ProtocolInfo.new(name, &block)
|
49
|
+
Protocol.class_variable_get(:@@info)[name] = info
|
50
|
+
end
|
51
|
+
module_function :SpecifyProtocol
|
52
|
+
|
53
|
+
# Protocols provide a polymorphism and method-dispatch mechanism that exchews
|
54
|
+
# stong typing and embraces the dynamic duck typing of Ruby. Rather than
|
55
|
+
# interrogate a module, class, or object for its type and ancestry, protocols
|
56
|
+
# allow modules, classes, and methods to be interrogated based on their behavior.
|
57
|
+
# It is a logical extension of the `respond_to?` method, but vastly more powerful.
|
58
|
+
#
|
59
|
+
# @!macro protocol
|
60
|
+
module Protocol
|
61
|
+
|
62
|
+
# The global registry of specified protocols.
|
63
|
+
@@info = {}
|
64
|
+
|
65
|
+
# Does the given module/class/object fully satisfy the given protocol(s)?
|
66
|
+
#
|
67
|
+
# @param [Object] target the method/class/object to interrogate
|
68
|
+
# @param [Symbol] protocols one or more protocols to check against the target
|
69
|
+
# @return [Boolean] true if the target satisfies all given protocols else false
|
70
|
+
#
|
71
|
+
# @raise [ArgumentError] when no protocols given
|
72
|
+
def Satisfy?(target, *protocols)
|
73
|
+
raise ArgumentError.new('no protocols given') if protocols.empty?
|
74
|
+
protocols.all?{|protocol| Protocol.satisfies?(target, protocol.to_sym) }
|
75
|
+
end
|
76
|
+
module_function :Satisfy?
|
77
|
+
|
78
|
+
# Does the given module/class/object fully satisfy the given protocol(s)?
|
79
|
+
# Raises a {Functional::ProtocolError} on failure.
|
80
|
+
#
|
81
|
+
# @param [Object] target the method/class/object to interrogate
|
82
|
+
# @param [Symbol] protocols one or more protocols to check against the target
|
83
|
+
# @return [Symbol] the target
|
84
|
+
#
|
85
|
+
# @raise [Functional::ProtocolError] when one or more protocols are not satisfied
|
86
|
+
# @raise [ArgumentError] when no protocols given
|
87
|
+
def Satisfy!(target, *protocols)
|
88
|
+
Protocol::Satisfy?(target, *protocols) or
|
89
|
+
Protocol.error(target, 'does not', *protocols)
|
90
|
+
target
|
91
|
+
end
|
92
|
+
module_function :Satisfy!
|
93
|
+
|
94
|
+
# Have the given protocols been specified?
|
95
|
+
#
|
96
|
+
# @param [Symbol] protocols the list of protocols to check
|
97
|
+
# @return [Boolean] true if all given protocols have been specified else false
|
98
|
+
#
|
99
|
+
# @raise [ArgumentError] when no protocols are given
|
100
|
+
def Specified?(*protocols)
|
101
|
+
raise ArgumentError.new('no protocols given') if protocols.empty?
|
102
|
+
Protocol.unspecified(*protocols).empty?
|
103
|
+
end
|
104
|
+
module_function :Specified?
|
105
|
+
|
106
|
+
# Have the given protocols been specified?
|
107
|
+
# Raises a {Functional::ProtocolError} on failure.
|
108
|
+
#
|
109
|
+
# @param [Symbol] protocols the list of protocols to check
|
110
|
+
# @return [Boolean] true if all given protocols have been specified
|
111
|
+
#
|
112
|
+
# @raise [Functional::ProtocolError] if one or more of the given protocols have
|
113
|
+
# not been specified
|
114
|
+
# @raise [ArgumentError] when no protocols are given
|
115
|
+
def Specified!(*protocols)
|
116
|
+
raise ArgumentError.new('no protocols given') if protocols.empty?
|
117
|
+
(unspecified = Protocol.unspecified(*protocols)).empty? or
|
118
|
+
raise ProtocolError.new("The following protocols are unspecified: :#{unspecified.join('; :')}.")
|
119
|
+
end
|
120
|
+
module_function :Specified!
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
# Does the target satisfy the given protocol?
|
125
|
+
#
|
126
|
+
# @param [Object] target the module/class/object to check
|
127
|
+
# @param [Symbol] protocol the protocol to check against the target
|
128
|
+
# @return [Boolean] true if the target satisfies the protocol else false
|
129
|
+
def self.satisfies?(target, protocol)
|
130
|
+
info = @@info[protocol]
|
131
|
+
return info && info.satisfies?(target)
|
132
|
+
end
|
133
|
+
|
134
|
+
# Reduces a list of protocols to a list of unspecified protocols.
|
135
|
+
#
|
136
|
+
# @param [Symbol] protocols the list of protocols to check
|
137
|
+
# @return [Array] zero or more unspecified protocols
|
138
|
+
def self.unspecified(*protocols)
|
139
|
+
protocols.drop_while do |protocol|
|
140
|
+
@@info.has_key? protocol.to_sym
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# Raise a {Functional::ProtocolError} formatted with the given data.
|
145
|
+
#
|
146
|
+
# @param [Object] target the object that was being interrogated
|
147
|
+
# @param [String] message the message fragment to inject into the error
|
148
|
+
# @param [Symbol] protocols list of protocols that were being checked against the target
|
149
|
+
#
|
150
|
+
# @raise [Functional::ProtocolError] the formatted exception object
|
151
|
+
def self.error(target, message, *protocols)
|
152
|
+
target = target.class unless target.is_a?(Module)
|
153
|
+
raise ProtocolError,
|
154
|
+
"Value (#{target.class}) '#{target}' #{message} behave as all of: :#{protocols.join('; :')}."
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,193 @@
|
|
1
|
+
module Functional
|
2
|
+
|
3
|
+
# An immutable object describing a single protocol and capable of building
|
4
|
+
# itself from a block. Used by {Functional#SpecifyProtocol}.
|
5
|
+
#
|
6
|
+
# @see Functional::Protocol
|
7
|
+
class ProtocolInfo
|
8
|
+
|
9
|
+
# The symbolic name of the protocol
|
10
|
+
attr_reader :name
|
11
|
+
|
12
|
+
# Process a protocol specification block and build a new object.
|
13
|
+
#
|
14
|
+
# @param [Symbol] name the symbolic name of the protocol
|
15
|
+
# @yield self to the given specification block
|
16
|
+
# @return [Functional::ProtocolInfo] the new info object, frozen
|
17
|
+
#
|
18
|
+
# @raise [ArgumentError] when name is nil or an empty string
|
19
|
+
# @raise [ArgumentError] when no block given
|
20
|
+
def initialize(name, &specification)
|
21
|
+
raise ArgumentError.new('no block given') unless block_given?
|
22
|
+
raise ArgumentError.new('no name given') if name.nil? || name.empty?
|
23
|
+
@name = name.to_sym
|
24
|
+
@info = Info.new({}, {}, [])
|
25
|
+
self.instance_eval(&specification)
|
26
|
+
@info.each_pair{|col, _| col.freeze}
|
27
|
+
@info.freeze
|
28
|
+
self.freeze
|
29
|
+
end
|
30
|
+
|
31
|
+
# The instance methods expected by this protocol.
|
32
|
+
#
|
33
|
+
# @return [Hash] a frozen hash of all instance method names and their
|
34
|
+
# expected arity for this protocol
|
35
|
+
def instance_methods
|
36
|
+
@info.instance_methods
|
37
|
+
end
|
38
|
+
|
39
|
+
# The class methods expected by this protocol.
|
40
|
+
#
|
41
|
+
# @return [Hash] a frozen hash of all class method names and their
|
42
|
+
# expected arity for this protocol
|
43
|
+
def class_methods
|
44
|
+
@info.class_methods
|
45
|
+
end
|
46
|
+
|
47
|
+
# The constants expected by this protocol.
|
48
|
+
#
|
49
|
+
# @return [Array] a frozen list of the constants expected by this protocol
|
50
|
+
def constants
|
51
|
+
@info.constants
|
52
|
+
end
|
53
|
+
|
54
|
+
# Does the given module/class/object satisfy this protocol?
|
55
|
+
#
|
56
|
+
# @return [Boolean] true if the target satisfies this protocol else false
|
57
|
+
def satisfies?(target)
|
58
|
+
satisfies_constants?(target) &&
|
59
|
+
satisfies_instance_methods?(target) &&
|
60
|
+
satisfies_class_methods?(target)
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
# Data structure for encapsulating the protocol info data.
|
66
|
+
Info = Struct.new(:instance_methods, :class_methods, :constants)
|
67
|
+
|
68
|
+
# Does the target satisfy the constants expected by this protocol?
|
69
|
+
#
|
70
|
+
# @param [target] target the module/class/object to interrogate
|
71
|
+
# @return [Boolean] true when satisfied else false
|
72
|
+
def satisfies_constants?(target)
|
73
|
+
clazz = target.is_a?(Module) ? target : target.class
|
74
|
+
@info.constants.all?{|constant| clazz.const_defined?(constant) }
|
75
|
+
end
|
76
|
+
|
77
|
+
# Does the target satisfy the instance methods expected by this protocol?
|
78
|
+
#
|
79
|
+
# @param [target] target the module/class/object to interrogate
|
80
|
+
# @return [Boolean] true when satisfied else false
|
81
|
+
def satisfies_instance_methods?(target)
|
82
|
+
@info.instance_methods.all? do |method, arity|
|
83
|
+
if target.is_a? Module
|
84
|
+
target.method_defined?(method) && check_arity?(target.instance_method(method), arity)
|
85
|
+
else
|
86
|
+
target.respond_to?(method) && check_arity?(target.method(method), arity)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
|
92
|
+
# Does the target satisfy the class methods expected by this protocol?
|
93
|
+
#
|
94
|
+
# @param [target] target the module/class/object to interrogate
|
95
|
+
# @return [Boolean] true when satisfied else false
|
96
|
+
def satisfies_class_methods?(target)
|
97
|
+
clazz = target.is_a?(Module) ? target : target.class
|
98
|
+
@info.class_methods.all? do |method, arity|
|
99
|
+
break false unless clazz.respond_to? method
|
100
|
+
method = clazz.method(method)
|
101
|
+
check_arity?(method, arity)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Does the given method have the expected arity? Returns true
|
106
|
+
# if the arity of the method is `-1` (variable length argument list
|
107
|
+
# with no required arguments), when expected is `nil` (indicating any
|
108
|
+
# arity is acceptable), or the arity of the method exactly matches the
|
109
|
+
# expected arity.
|
110
|
+
#
|
111
|
+
# @param [Method] method the method object to interrogate
|
112
|
+
# @param [Fixnum] expected the expected arity
|
113
|
+
# @return [Boolean] true when an acceptable match else false
|
114
|
+
#
|
115
|
+
# @see http://www.ruby-doc.org/core-2.1.2/Method.html#method-i-arity Method#arity
|
116
|
+
def check_arity?(method, expected)
|
117
|
+
arity = method.arity
|
118
|
+
expected.nil? || arity == -1 || expected == arity
|
119
|
+
end
|
120
|
+
|
121
|
+
#################################################################
|
122
|
+
# DSL methods
|
123
|
+
|
124
|
+
# Specify an instance method.
|
125
|
+
#
|
126
|
+
# @param [Symbol] name the name of the method
|
127
|
+
# @param [Fixnum] arity the required arity
|
128
|
+
def instance_method(name, arity = nil)
|
129
|
+
arity = arity.to_i unless arity.nil?
|
130
|
+
@info.instance_methods[name.to_sym] = arity
|
131
|
+
end
|
132
|
+
|
133
|
+
# Specify a class method.
|
134
|
+
#
|
135
|
+
# @param [Symbol] name the name of the method
|
136
|
+
# @param [Fixnum] arity the required arity
|
137
|
+
def class_method(name, arity = nil)
|
138
|
+
arity = arity.to_i unless arity.nil?
|
139
|
+
@info.class_methods[name.to_sym] = arity
|
140
|
+
end
|
141
|
+
|
142
|
+
# Specify an instance reader attribute.
|
143
|
+
#
|
144
|
+
# @param [Symbol] name the name of the attribute
|
145
|
+
def attr_reader(name)
|
146
|
+
instance_method(name, 0)
|
147
|
+
end
|
148
|
+
|
149
|
+
# Specify an instance writer attribute.
|
150
|
+
#
|
151
|
+
# @param [Symbol] name the name of the attribute
|
152
|
+
def attr_writer(name)
|
153
|
+
instance_method("#{name}=".to_sym, 1)
|
154
|
+
end
|
155
|
+
|
156
|
+
# Specify an instance accessor attribute.
|
157
|
+
#
|
158
|
+
# @param [Symbol] name the name of the attribute
|
159
|
+
def attr_accessor(name)
|
160
|
+
attr_reader(name)
|
161
|
+
attr_writer(name)
|
162
|
+
end
|
163
|
+
|
164
|
+
# Specify a class reader attribute.
|
165
|
+
#
|
166
|
+
# @param [Symbol] name the name of the attribute
|
167
|
+
def class_attr_reader(name)
|
168
|
+
class_method(name, 0)
|
169
|
+
end
|
170
|
+
|
171
|
+
# Specify a class writer attribute.
|
172
|
+
#
|
173
|
+
# @param [Symbol] name the name of the attribute
|
174
|
+
def class_attr_writer(name)
|
175
|
+
class_method("#{name}=".to_sym, 1)
|
176
|
+
end
|
177
|
+
|
178
|
+
# Specify a class accessor attribute.
|
179
|
+
#
|
180
|
+
# @param [Symbol] name the name of the attribute
|
181
|
+
def class_attr_accessor(name)
|
182
|
+
class_attr_reader(name)
|
183
|
+
class_attr_writer(name)
|
184
|
+
end
|
185
|
+
|
186
|
+
# Specify a constant.
|
187
|
+
#
|
188
|
+
# @param [Symbol] name the name of the constant
|
189
|
+
def constant(name)
|
190
|
+
@info.constants << name.to_sym
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
require_relative 'abstract_struct'
|
2
|
+
require_relative 'type_check'
|
3
|
+
|
4
|
+
module Functional
|
5
|
+
|
6
|
+
# An immutable data structure with multiple data fields. A `Record` is a
|
7
|
+
# convenient way to bundle a number of field attributes together,
|
8
|
+
# using accessor methods, without having to write an explicit class.
|
9
|
+
# The `Record` module generates new `AbstractStruct` subclasses that hold a
|
10
|
+
# set of fields with a reader method for each field.
|
11
|
+
#
|
12
|
+
# A `Record` is very similar to a Ruby `Struct` and shares many of its behaviors
|
13
|
+
# and attributes. Unlike a # Ruby `Struct`, a `Record` is immutable: its values
|
14
|
+
# are set at construction and can never be changed. Divergence between the two
|
15
|
+
# classes derive from this core difference.
|
16
|
+
#
|
17
|
+
# @!macro record
|
18
|
+
#
|
19
|
+
# @see Functional::AbstractStruct
|
20
|
+
# @see Functional::Union
|
21
|
+
#
|
22
|
+
# @!macro thread_safe_immutable_object
|
23
|
+
module Record
|
24
|
+
extend self
|
25
|
+
|
26
|
+
# Create a new record class with the given fields.
|
27
|
+
#
|
28
|
+
# @return [Functional::AbstractStruct] the new record subclass
|
29
|
+
# @raise [ArgumentError] no fields specified
|
30
|
+
def new(*fields, &block)
|
31
|
+
raise ArgumentError.new('no fields provided') if fields.empty?
|
32
|
+
build(fields, &block)
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
# @!visibility private
|
38
|
+
#
|
39
|
+
# A set of restrictions governing the creation of a new record.
|
40
|
+
class Restrictions
|
41
|
+
include TypeCheck
|
42
|
+
|
43
|
+
# Create a new restrictions object by processing the given
|
44
|
+
# block. The block should be the DSL for defining a record class.
|
45
|
+
#
|
46
|
+
# @param [Proc] block A DSL definition of a new record.
|
47
|
+
# @yield A DSL definition of a new record.
|
48
|
+
def initialize(&block)
|
49
|
+
@required = []
|
50
|
+
@defaults = {}
|
51
|
+
instance_eval(&block) if block_given?
|
52
|
+
@required.freeze
|
53
|
+
@defaults.freeze
|
54
|
+
self.freeze
|
55
|
+
end
|
56
|
+
|
57
|
+
# DSL method for declaring one or more fields to be mandatory.
|
58
|
+
#
|
59
|
+
# @param [Symbol] fields zero or more mandatory fields
|
60
|
+
def mandatory(*fields)
|
61
|
+
@required.concat(fields.collect{|field| field.to_sym})
|
62
|
+
end
|
63
|
+
|
64
|
+
# DSL method for declaring a default value for a field
|
65
|
+
#
|
66
|
+
# @param [Symbol] field the field to be given a default value
|
67
|
+
# @param [Object] value the default value of the field
|
68
|
+
def default(field, value)
|
69
|
+
@defaults[field] = value
|
70
|
+
end
|
71
|
+
|
72
|
+
# Clone a default value if it is cloneable. Else just return
|
73
|
+
# the value.
|
74
|
+
#
|
75
|
+
# @param [Symbol] field the name of the field from which the
|
76
|
+
# default value is to be cloned.
|
77
|
+
# @return [Object] a clone of the value or the value if uncloneable
|
78
|
+
def clone_default(field)
|
79
|
+
value = @defaults[field]
|
80
|
+
value = value.clone unless uncloneable?(value)
|
81
|
+
rescue TypeError
|
82
|
+
# can't be cloned
|
83
|
+
ensure
|
84
|
+
return value
|
85
|
+
end
|
86
|
+
|
87
|
+
# Check the given data hash to see if it contains non-nil values for
|
88
|
+
# all mandatory fields.
|
89
|
+
#
|
90
|
+
# @param [Hash] data the data hash
|
91
|
+
# @raise [ArgumentError] if any mandatory fields are missing
|
92
|
+
def check_mandatory!(data)
|
93
|
+
if data.any?{|k,v| @required.include?(k) && v.nil? }
|
94
|
+
raise ArgumentError.new('mandatory fields must not be nil')
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
# Is the given object uncloneable?
|
101
|
+
#
|
102
|
+
# @param [Object] object the object to check
|
103
|
+
# @return [Boolean] true if the object cannot be cloned else false
|
104
|
+
def uncloneable?(object)
|
105
|
+
Type? object, NilClass, TrueClass, FalseClass, Fixnum, Bignum, Float
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# Use the given `AbstractStruct` class and build the methods necessary
|
110
|
+
# to support the given data fields.
|
111
|
+
#
|
112
|
+
# @param [Array] fields the list of symbolic names for all data fields
|
113
|
+
# @return [Functional::AbstractStruct] the record class
|
114
|
+
def build(fields, &block)
|
115
|
+
record, fields = AbstractStruct.define_class(self, :record, fields)
|
116
|
+
record.class_variable_set(:@@restrictions, Restrictions.new(&block))
|
117
|
+
define_initializer(record)
|
118
|
+
fields.each do |field|
|
119
|
+
define_reader(record, field)
|
120
|
+
end
|
121
|
+
record
|
122
|
+
end
|
123
|
+
|
124
|
+
# Define an initializer method on the given record class.
|
125
|
+
#
|
126
|
+
# @param [Functional::AbstractStruct] record the new record class
|
127
|
+
# @return [Functional::AbstractStruct] the record class
|
128
|
+
def define_initializer(record)
|
129
|
+
record.send(:define_method, :initialize) do |data = {}|
|
130
|
+
restrictions = self.class.class_variable_get(:@@restrictions)
|
131
|
+
data = fields.reduce({}) do |memo, field|
|
132
|
+
memo[field] = data.fetch(field, restrictions.clone_default(field))
|
133
|
+
memo
|
134
|
+
end
|
135
|
+
restrictions.check_mandatory!(data)
|
136
|
+
set_data_hash(data)
|
137
|
+
set_values_array(data.values)
|
138
|
+
self.freeze
|
139
|
+
end
|
140
|
+
record
|
141
|
+
end
|
142
|
+
|
143
|
+
# Define a reader method on the given record class for the given data field.
|
144
|
+
#
|
145
|
+
# @param [Functional::AbstractStruct] record the new record class
|
146
|
+
# @param [Symbol] field symbolic name of the current data field
|
147
|
+
# @return [Functional::AbstractStruct] the record class
|
148
|
+
def define_reader(record, field)
|
149
|
+
record.send(:define_method, field) do
|
150
|
+
to_h[field]
|
151
|
+
end
|
152
|
+
record
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|