plumb 0.0.3 → 0.0.5
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 +609 -57
- data/bench/compare_parametric_schema.rb +102 -0
- data/bench/compare_parametric_struct.rb +68 -0
- data/bench/parametric_schema.rb +229 -0
- data/bench/plumb_hash.rb +109 -0
- data/examples/concurrent_downloads.rb +3 -3
- data/examples/env_config.rb +2 -2
- data/examples/event_registry.rb +127 -0
- data/examples/weekdays.rb +1 -1
- data/lib/plumb/and.rb +4 -3
- data/lib/plumb/any_class.rb +4 -4
- data/lib/plumb/array_class.rb +8 -5
- data/lib/plumb/attributes.rb +268 -0
- data/lib/plumb/build.rb +4 -3
- data/lib/plumb/composable.rb +381 -0
- data/lib/plumb/decorator.rb +57 -0
- data/lib/plumb/deferred.rb +1 -1
- data/lib/plumb/hash_class.rb +19 -8
- data/lib/plumb/hash_map.rb +8 -6
- data/lib/plumb/interface_class.rb +6 -2
- data/lib/plumb/json_schema_visitor.rb +59 -32
- data/lib/plumb/match_class.rb +5 -4
- data/lib/plumb/metadata.rb +5 -1
- data/lib/plumb/metadata_visitor.rb +13 -42
- data/lib/plumb/not.rb +4 -3
- data/lib/plumb/or.rb +10 -4
- data/lib/plumb/pipeline.rb +27 -7
- data/lib/plumb/policy.rb +10 -3
- data/lib/plumb/schema.rb +11 -10
- data/lib/plumb/static_class.rb +4 -3
- data/lib/plumb/step.rb +4 -3
- data/lib/plumb/stream_class.rb +8 -7
- data/lib/plumb/tagged_hash.rb +11 -11
- data/lib/plumb/transform.rb +4 -3
- data/lib/plumb/tuple_class.rb +8 -8
- data/lib/plumb/type_registry.rb +5 -2
- data/lib/plumb/types.rb +30 -1
- data/lib/plumb/value_class.rb +4 -3
- data/lib/plumb/version.rb +1 -1
- data/lib/plumb/visitor_handlers.rb +6 -0
- data/lib/plumb.rb +11 -5
- metadata +10 -3
- data/lib/plumb/steppable.rb +0 -229
data/lib/plumb/any_class.rb
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'plumb/
|
3
|
+
require 'plumb/composable'
|
4
4
|
|
5
5
|
module Plumb
|
6
6
|
class AnyClass
|
7
|
-
include
|
7
|
+
include Composable
|
8
8
|
|
9
|
-
def |(other) =
|
10
|
-
def >>(other) =
|
9
|
+
def |(other) = Composable.wrap(other)
|
10
|
+
def >>(other) = Composable.wrap(other)
|
11
11
|
|
12
12
|
# Any.default(value) must trigger default when value is Undefined
|
13
13
|
def default(...)
|
data/lib/plumb/array_class.rb
CHANGED
@@ -1,18 +1,19 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'concurrent'
|
4
|
-
require 'plumb/
|
4
|
+
require 'plumb/composable'
|
5
5
|
require 'plumb/result'
|
6
6
|
require 'plumb/stream_class'
|
7
7
|
|
8
8
|
module Plumb
|
9
9
|
class ArrayClass
|
10
|
-
include
|
10
|
+
include Composable
|
11
11
|
|
12
|
-
attr_reader :
|
12
|
+
attr_reader :children
|
13
13
|
|
14
14
|
def initialize(element_type: Types::Any)
|
15
|
-
@element_type =
|
15
|
+
@element_type = Composable.wrap(element_type)
|
16
|
+
@children = [@element_type].freeze
|
16
17
|
|
17
18
|
freeze
|
18
19
|
end
|
@@ -47,11 +48,13 @@ module Plumb
|
|
47
48
|
values, errors = map_array_elements(result.value)
|
48
49
|
return result.valid(values) unless errors.any?
|
49
50
|
|
50
|
-
result.invalid(errors:)
|
51
|
+
result.invalid(values, errors:)
|
51
52
|
end
|
52
53
|
|
53
54
|
private
|
54
55
|
|
56
|
+
attr_reader :element_type
|
57
|
+
|
55
58
|
def _inspect
|
56
59
|
%(Array[#{element_type}])
|
57
60
|
end
|
@@ -0,0 +1,268 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Plumb
|
4
|
+
module Attributes
|
5
|
+
# A module that provides a simple way to define a struct-like class with
|
6
|
+
# attributes that are type-checked on initialization.
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# class Person
|
10
|
+
# include Plumb::Attributes
|
11
|
+
#
|
12
|
+
# attribute :name, Types::String
|
13
|
+
# attribute :age, Types::Integer[18..]
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# person = Person.new(name: 'Jane', age: 20)
|
17
|
+
# person.valid? # => true
|
18
|
+
# person.errors # => {}
|
19
|
+
# person.name # => 'Jane'
|
20
|
+
#
|
21
|
+
# It supports nested attributes:
|
22
|
+
#
|
23
|
+
# @example
|
24
|
+
# class Person
|
25
|
+
# include Plumb::Attributes
|
26
|
+
#
|
27
|
+
# attribute :friend do
|
28
|
+
# attribute :name, String
|
29
|
+
# end
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# person = Person.new(friend: { name: 'John' })
|
33
|
+
#
|
34
|
+
# Or arrays of nested attributes:
|
35
|
+
#
|
36
|
+
# @example
|
37
|
+
# class Person
|
38
|
+
# include Plumb::Attributes
|
39
|
+
#
|
40
|
+
# attribute :friends, Types::Array do
|
41
|
+
# atrribute :name, String
|
42
|
+
# end
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# person = Person.new(friends: [{ name: 'John' }])
|
46
|
+
#
|
47
|
+
# Or use struct classes defined separately:
|
48
|
+
#
|
49
|
+
# @example
|
50
|
+
# class Company
|
51
|
+
# include Plumb::Attributes
|
52
|
+
# attribute :name, String
|
53
|
+
# end
|
54
|
+
#
|
55
|
+
# class Person
|
56
|
+
# include Plumb::Attributes
|
57
|
+
#
|
58
|
+
# # Single nested struct
|
59
|
+
# attribute :company, Company
|
60
|
+
#
|
61
|
+
# # Array of nested structs
|
62
|
+
# attribute :companies, Types::Array[Company]
|
63
|
+
# end
|
64
|
+
#
|
65
|
+
# Arrays and other types support composition and helpers. Ex. `#default`.
|
66
|
+
#
|
67
|
+
# attribute :companies, Types::Array[Company].default([].freeze)
|
68
|
+
#
|
69
|
+
# Passing a named struct class AND a block will subclass the struct and extend it with new attributes:
|
70
|
+
#
|
71
|
+
# attribute :company, Company do
|
72
|
+
# attribute :address, String
|
73
|
+
# end
|
74
|
+
#
|
75
|
+
# The same works with arrays:
|
76
|
+
#
|
77
|
+
# attribute :companies, Types::Array[Company] do
|
78
|
+
# attribute :address, String
|
79
|
+
# end
|
80
|
+
#
|
81
|
+
# Note that this does NOT work with union'd or piped structs.
|
82
|
+
#
|
83
|
+
# attribute :company, Company | Person do
|
84
|
+
#
|
85
|
+
# ## Optional Attributes
|
86
|
+
# Using `attribute?` allows for optional attributes. If the attribute is not present, it will be set to `Undefined`.
|
87
|
+
#
|
88
|
+
# attribute? :company, Company
|
89
|
+
#
|
90
|
+
# ## Struct Inheritance
|
91
|
+
# Structs can inherit from other structs. This is useful for defining a base struct with common attributes.
|
92
|
+
#
|
93
|
+
# class BasePerson
|
94
|
+
# include Plumb::Attributes
|
95
|
+
#
|
96
|
+
# attribute :name, String
|
97
|
+
# end
|
98
|
+
#
|
99
|
+
# class Person < BasePerson
|
100
|
+
# attribute :age, Integer
|
101
|
+
# end
|
102
|
+
#
|
103
|
+
# ## [] Syntax
|
104
|
+
#
|
105
|
+
# The `[]` syntax can be used to define a struct in a single line.
|
106
|
+
# Like Plumb::Types::Hash, suffixing a key with `?` makes it optional.
|
107
|
+
#
|
108
|
+
# Person = Data[name: String, age?: Integer]
|
109
|
+
# person = Person.new(name: 'Jane')
|
110
|
+
#
|
111
|
+
def self.included(base)
|
112
|
+
base.send(:extend, ClassMethods)
|
113
|
+
base.define_singleton_method(:__plumb_struct_class__) { base }
|
114
|
+
end
|
115
|
+
|
116
|
+
attr_reader :errors, :attributes
|
117
|
+
|
118
|
+
def initialize(attrs = {})
|
119
|
+
assign_attributes(attrs)
|
120
|
+
end
|
121
|
+
|
122
|
+
def ==(other)
|
123
|
+
other.is_a?(self.class) && other.attributes == attributes
|
124
|
+
end
|
125
|
+
|
126
|
+
# @return [Boolean]
|
127
|
+
def valid? = !errors || errors.none?
|
128
|
+
|
129
|
+
# @param attrs [Hash]
|
130
|
+
# @return [Plumb::Attributes]
|
131
|
+
def with(attrs = BLANK_HASH)
|
132
|
+
self.class.new(attributes.merge(attrs))
|
133
|
+
end
|
134
|
+
|
135
|
+
def inspect
|
136
|
+
%(#<#{self.class}:#{object_id} [#{valid? ? 'valid' : 'invalid'}] #{attributes.map do |k, v|
|
137
|
+
[k, v.inspect].join(':')
|
138
|
+
end.join(' ')}>)
|
139
|
+
end
|
140
|
+
|
141
|
+
# @return [Hash]
|
142
|
+
def to_h
|
143
|
+
attributes.transform_values do |value|
|
144
|
+
case value
|
145
|
+
when ::Array
|
146
|
+
value.map { |v| v.respond_to?(:to_h) ? v.to_h : v }
|
147
|
+
else
|
148
|
+
value.respond_to?(:to_h) ? value.to_h : value
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def deconstruct(...) = to_h.values.deconstruct(...)
|
154
|
+
def deconstruct_keys(...) = to_h.deconstruct_keys(...)
|
155
|
+
|
156
|
+
private
|
157
|
+
|
158
|
+
def assign_attributes(attrs = BLANK_HASH)
|
159
|
+
raise ArgumentError, 'Must be a Hash of attributes' unless attrs.respond_to?(:to_h)
|
160
|
+
|
161
|
+
@errors = BLANK_HASH
|
162
|
+
result = self.class._schema.resolve(attrs.to_h)
|
163
|
+
@attributes = result.value
|
164
|
+
@errors = result.errors unless result.valid?
|
165
|
+
end
|
166
|
+
|
167
|
+
module ClassMethods
|
168
|
+
def _schema
|
169
|
+
@_schema ||= HashClass.new
|
170
|
+
end
|
171
|
+
|
172
|
+
def inherited(subclass)
|
173
|
+
_schema._schema.each do |key, type|
|
174
|
+
subclass.attribute(key, type)
|
175
|
+
end
|
176
|
+
super
|
177
|
+
end
|
178
|
+
|
179
|
+
# The Plumb::Step interface
|
180
|
+
# @param result [Plumb::Result::Valid]
|
181
|
+
# @return [Plumb::Result::Valid, Plumb::Result::Invalid]
|
182
|
+
def call(result)
|
183
|
+
return result if result.value.is_a?(self)
|
184
|
+
return result.invalid(errors: ['Must be a Hash of attributes']) unless result.value.respond_to?(:to_h)
|
185
|
+
|
186
|
+
instance = new(result.value.to_h)
|
187
|
+
instance.valid? ? result.valid(instance) : result.invalid(instance, errors: instance.errors.to_h)
|
188
|
+
end
|
189
|
+
|
190
|
+
# Person = Data[:name => String, :age => Integer, title?: String]
|
191
|
+
def [](type_specs)
|
192
|
+
type_specs = type_specs._schema if type_specs.is_a?(Plumb::HashClass)
|
193
|
+
klass = Class.new(self)
|
194
|
+
type_specs.each do |key, type|
|
195
|
+
klass.attribute(key, type)
|
196
|
+
end
|
197
|
+
klass
|
198
|
+
end
|
199
|
+
|
200
|
+
# node name for visitors
|
201
|
+
def node_name = :data
|
202
|
+
|
203
|
+
# attribute(:friend) { attribute(:name, String) }
|
204
|
+
# attribute(:friend, MyStruct) { attribute(:name, String) }
|
205
|
+
# attribute(:name, String)
|
206
|
+
# attribute(:friends, Types::Array) { attribute(:name, String) }
|
207
|
+
# attribute(:friends, Types::Array) # same as Types::Array[Types::Any]
|
208
|
+
# attribute(:friends, Types::Array[Person])
|
209
|
+
#
|
210
|
+
def attribute(name, type = Types::Any, &block)
|
211
|
+
key = Key.wrap(name)
|
212
|
+
name = key.to_sym
|
213
|
+
type = Composable.wrap(type)
|
214
|
+
if block_given? # :foo, Array[Data] or :foo, Struct
|
215
|
+
type = __plumb_struct_class__ if type == Types::Any
|
216
|
+
type = Plumb.decorate(type) do |node|
|
217
|
+
if node.is_a?(Plumb::ArrayClass)
|
218
|
+
child = node.children.first
|
219
|
+
child = __plumb_struct_class__ if child == Types::Any
|
220
|
+
Types::Array[build_nested(name, child, &block)]
|
221
|
+
elsif node.is_a?(Plumb::Step)
|
222
|
+
build_nested(name, node, &block)
|
223
|
+
elsif node.is_a?(Class) && node <= Plumb::Attributes
|
224
|
+
build_nested(name, node, &block)
|
225
|
+
else
|
226
|
+
node
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
@_schema = _schema + { key => type }
|
232
|
+
__plumb_define_attribute_method__(name)
|
233
|
+
end
|
234
|
+
|
235
|
+
def __plumb_define_attribute_method__(name)
|
236
|
+
define_method(name) { @attributes[name] }
|
237
|
+
end
|
238
|
+
|
239
|
+
def attribute?(name, *args, &block)
|
240
|
+
attribute(Key.new(name, optional: true), *args, &block)
|
241
|
+
end
|
242
|
+
|
243
|
+
def build_nested(name, node, &block)
|
244
|
+
if node.is_a?(Class) && node <= Plumb::Attributes
|
245
|
+
sub = Class.new(node)
|
246
|
+
sub.instance_exec(&block)
|
247
|
+
__set_nested_class__(name, sub)
|
248
|
+
return Composable.wrap(sub)
|
249
|
+
end
|
250
|
+
|
251
|
+
return node unless node.is_a?(Plumb::Step)
|
252
|
+
|
253
|
+
child = node.children.first
|
254
|
+
return node unless child <= Plumb::Attributes
|
255
|
+
|
256
|
+
sub = Class.new(child)
|
257
|
+
sub.instance_exec(&block)
|
258
|
+
__set_nested_class__(name, sub)
|
259
|
+
Composable.wrap(sub)
|
260
|
+
end
|
261
|
+
|
262
|
+
def __set_nested_class__(name, klass)
|
263
|
+
name = name.to_s.split('_').map(&:capitalize).join.sub(/s$/, '')
|
264
|
+
const_set(name, klass) unless const_defined?(name)
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|
data/lib/plumb/build.rb
CHANGED
@@ -1,16 +1,17 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'plumb/
|
3
|
+
require 'plumb/composable'
|
4
4
|
|
5
5
|
module Plumb
|
6
6
|
class Build
|
7
|
-
include
|
7
|
+
include Composable
|
8
8
|
|
9
|
-
attr_reader :
|
9
|
+
attr_reader :children
|
10
10
|
|
11
11
|
def initialize(type, factory_method: :new, &block)
|
12
12
|
@type = type
|
13
13
|
@block = block || ->(value) { type.send(factory_method, value) }
|
14
|
+
@children = [type].freeze
|
14
15
|
freeze
|
15
16
|
end
|
16
17
|
|