functional-ruby 0.7.7 → 1.0.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 +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
|