plumb 0.0.3 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
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