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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +609 -57
  3. data/bench/compare_parametric_schema.rb +102 -0
  4. data/bench/compare_parametric_struct.rb +68 -0
  5. data/bench/parametric_schema.rb +229 -0
  6. data/bench/plumb_hash.rb +109 -0
  7. data/examples/concurrent_downloads.rb +3 -3
  8. data/examples/env_config.rb +2 -2
  9. data/examples/event_registry.rb +127 -0
  10. data/examples/weekdays.rb +1 -1
  11. data/lib/plumb/and.rb +4 -3
  12. data/lib/plumb/any_class.rb +4 -4
  13. data/lib/plumb/array_class.rb +8 -5
  14. data/lib/plumb/attributes.rb +268 -0
  15. data/lib/plumb/build.rb +4 -3
  16. data/lib/plumb/composable.rb +381 -0
  17. data/lib/plumb/decorator.rb +57 -0
  18. data/lib/plumb/deferred.rb +1 -1
  19. data/lib/plumb/hash_class.rb +19 -8
  20. data/lib/plumb/hash_map.rb +8 -6
  21. data/lib/plumb/interface_class.rb +6 -2
  22. data/lib/plumb/json_schema_visitor.rb +59 -32
  23. data/lib/plumb/match_class.rb +5 -4
  24. data/lib/plumb/metadata.rb +5 -1
  25. data/lib/plumb/metadata_visitor.rb +13 -42
  26. data/lib/plumb/not.rb +4 -3
  27. data/lib/plumb/or.rb +10 -4
  28. data/lib/plumb/pipeline.rb +27 -7
  29. data/lib/plumb/policy.rb +10 -3
  30. data/lib/plumb/schema.rb +11 -10
  31. data/lib/plumb/static_class.rb +4 -3
  32. data/lib/plumb/step.rb +4 -3
  33. data/lib/plumb/stream_class.rb +8 -7
  34. data/lib/plumb/tagged_hash.rb +11 -11
  35. data/lib/plumb/transform.rb +4 -3
  36. data/lib/plumb/tuple_class.rb +8 -8
  37. data/lib/plumb/type_registry.rb +5 -2
  38. data/lib/plumb/types.rb +30 -1
  39. data/lib/plumb/value_class.rb +4 -3
  40. data/lib/plumb/version.rb +1 -1
  41. data/lib/plumb/visitor_handlers.rb +6 -0
  42. data/lib/plumb.rb +11 -5
  43. metadata +10 -3
  44. data/lib/plumb/steppable.rb +0 -229
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'plumb/steppable'
3
+ require 'plumb/composable'
4
4
 
5
5
  module Plumb
6
6
  class AnyClass
7
- include Steppable
7
+ include Composable
8
8
 
9
- def |(other) = Steppable.wrap(other)
10
- def >>(other) = Steppable.wrap(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(...)
@@ -1,18 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'concurrent'
4
- require 'plumb/steppable'
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 Steppable
10
+ include Composable
11
11
 
12
- attr_reader :element_type
12
+ attr_reader :children
13
13
 
14
14
  def initialize(element_type: Types::Any)
15
- @element_type = Steppable.wrap(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/steppable'
3
+ require 'plumb/composable'
4
4
 
5
5
  module Plumb
6
6
  class Build
7
- include Steppable
7
+ include Composable
8
8
 
9
- attr_reader :type
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