plumb 0.0.3 → 0.0.4
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 +391 -52
- data/examples/concurrent_downloads.rb +3 -3
- data/examples/env_config.rb +2 -2
- data/examples/event_registry.rb +120 -0
- 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 +262 -0
- data/lib/plumb/build.rb +4 -3
- data/lib/plumb/{steppable.rb → composable.rb} +61 -28
- 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 +50 -32
- data/lib/plumb/match_class.rb +4 -3
- 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 +6 -5
- 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 +10 -10
- 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 +6 -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 +6 -3
@@ -15,8 +15,8 @@ module Types
|
|
15
15
|
# Turn a string into an URI
|
16
16
|
URL = String[/^https?:/].build(::URI, :parse)
|
17
17
|
|
18
|
-
# a Struct to
|
19
|
-
Image = Data.define(:url, :io)
|
18
|
+
# a Struct to hold image data
|
19
|
+
Image = ::Data.define(:url, :io)
|
20
20
|
|
21
21
|
# A (naive) step to download files from the internet
|
22
22
|
# and return an Image struct.
|
@@ -38,7 +38,7 @@ module Types
|
|
38
38
|
# Wrap the #reader and #wruter methods into Plumb steps
|
39
39
|
# A step only needs #call(Result) => Result to work in a pipeline,
|
40
40
|
# but wrapping it in Plumb::Step provides the #>> and #| methods for composability,
|
41
|
-
# as well as all the other helper methods provided by the
|
41
|
+
# as well as all the other helper methods provided by the Composable module.
|
42
42
|
def read = Plumb::Step.new(method(:reader))
|
43
43
|
def write = Plumb::Step.new(method(:writer))
|
44
44
|
|
data/examples/env_config.rb
CHANGED
@@ -32,10 +32,10 @@ module Types
|
|
32
32
|
end
|
33
33
|
|
34
34
|
# A dummy S3 client
|
35
|
-
S3Client = Data.define(:bucket, :region)
|
35
|
+
S3Client = ::Data.define(:bucket, :region)
|
36
36
|
|
37
37
|
# A dummy SFTP client
|
38
|
-
SFTPClient = Data.define(:host, :username, :password)
|
38
|
+
SFTPClient = ::Data.define(:host, :username, :password)
|
39
39
|
|
40
40
|
# Map these fields to an S3 client
|
41
41
|
S3Config = Types::Hash[
|
@@ -0,0 +1,120 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'plumb'
|
4
|
+
require 'time'
|
5
|
+
require 'uri'
|
6
|
+
require 'securerandom'
|
7
|
+
require 'debug'
|
8
|
+
|
9
|
+
# Bring Plumb into our own namespace
|
10
|
+
# and define some basic types
|
11
|
+
module Types
|
12
|
+
include Plumb::Types
|
13
|
+
|
14
|
+
# Turn an ISO8601 sring into a Time object
|
15
|
+
ISOTime = String.build(::Time, :parse)
|
16
|
+
|
17
|
+
# A type that can be a Time object or an ISO8601 string >> Time
|
18
|
+
Time = Any[::Time] | ISOTime
|
19
|
+
|
20
|
+
# A UUID string
|
21
|
+
UUID = String[/\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i]
|
22
|
+
|
23
|
+
# A UUID string, or generate a new one
|
24
|
+
AutoUUID = UUID.default { SecureRandom.uuid }
|
25
|
+
|
26
|
+
Email = String[URI::MailTo::EMAIL_REGEXP]
|
27
|
+
end
|
28
|
+
|
29
|
+
# A superclass and registry to define event types
|
30
|
+
# for example for an event-driven or event-sourced system.
|
31
|
+
# All events have an "envelope" set of attributes,
|
32
|
+
# including unique ID, stream_id, type, timestamp, causation ID,
|
33
|
+
# event subclasses have a type string (ex. 'users.name.updated') and an optional payload
|
34
|
+
# This class provides a `.define` method to create new event types with a type and optional payload struct,
|
35
|
+
# a `.from` method to instantiate the correct subclass from a hash, ex. when deserializing from JSON or a web request.
|
36
|
+
# and a `#follow` method to produce new events based on a previous event's envelope, where the #causation_id and #correlation_id
|
37
|
+
# are set to the parent event
|
38
|
+
# @example
|
39
|
+
#
|
40
|
+
# # Define event struct with type and payload
|
41
|
+
# UserCreated = Event.define('users.created') do
|
42
|
+
# attribute :name, Types::String
|
43
|
+
# attribute :email, Types::Email
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
# # Instantiate a full event with .new
|
47
|
+
# user_created = UserCreated.new(stream_id: 'user-1', payload: { name: 'Joe', email: '...' })
|
48
|
+
#
|
49
|
+
# # Use the `.from(Hash) => Event` factory to lookup event class by `type` and produce the right instance
|
50
|
+
# user_created = Event.from(type: 'users.created', stream_id: 'user-1', payload: { name: 'Joe', email: '...' })
|
51
|
+
#
|
52
|
+
# # Use #follow(payload Hash) => Event to produce events following a command or parent event
|
53
|
+
# create_user = CreateUser.new(...)
|
54
|
+
# user_created = create_user.follow(UserCreated, name: 'Joe', email: '...')
|
55
|
+
# user_created.causation_id == create_user.id
|
56
|
+
# user_created.correlation_id == create_user.correlation_id
|
57
|
+
# user_created.stream_id == create_user.stream_id
|
58
|
+
#
|
59
|
+
# ## JSON Schemas
|
60
|
+
# Plumb data structs support `.to_json_schema`, to you can document all events in the registry with something like
|
61
|
+
#
|
62
|
+
# Event.registry.values.map(&:to_json_schema)
|
63
|
+
#
|
64
|
+
class Event < Types::Data
|
65
|
+
attribute :id, Types::AutoUUID
|
66
|
+
attribute :stream_id, Types::String.present
|
67
|
+
attribute :type, Types::String
|
68
|
+
attribute(:created_at, Types::Time.default { ::Time.now })
|
69
|
+
attribute? :causation_id, Types::UUID
|
70
|
+
attribute? :correlation_id, Types::UUID
|
71
|
+
attribute :payload, Types::Static[nil]
|
72
|
+
|
73
|
+
def self.registry
|
74
|
+
@registry ||= {}
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.define(type_str, &payload_block)
|
78
|
+
type_str.freeze unless type_str.frozen?
|
79
|
+
registry[type_str] = Class.new(self) do
|
80
|
+
attribute :type, Types::Static[type_str]
|
81
|
+
attribute :payload, &payload_block if block_given?
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.from(attrs)
|
86
|
+
klass = registry[attrs[:type]]
|
87
|
+
raise ArgumentError, "Unknown event type: #{attrs[:type]}" unless klass
|
88
|
+
|
89
|
+
klass.new(attrs)
|
90
|
+
end
|
91
|
+
|
92
|
+
def follow(event_class, payload_attrs = nil)
|
93
|
+
attrs = { stream_id:, causation_id: id, correlation_id: }
|
94
|
+
attrs[:payload] = payload_attrs if payload_attrs
|
95
|
+
event_class.new(attrs)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Example command and events for a simple event-sourced system
|
100
|
+
#
|
101
|
+
# ## Commands
|
102
|
+
# CreateUser = Event.define('users.create') do
|
103
|
+
# attribute :name, Types::String.present
|
104
|
+
# attribute :email, Types::Email
|
105
|
+
# end
|
106
|
+
#
|
107
|
+
# UpdateUserName = Event.define('users.update_name') do
|
108
|
+
# attribute :name, Types::String.present
|
109
|
+
# end
|
110
|
+
#
|
111
|
+
# ## Events
|
112
|
+
# UserCreated = Event.define('users.created') do
|
113
|
+
# attribute :name, Types::String
|
114
|
+
# attribute :email, Types::Email
|
115
|
+
# end
|
116
|
+
#
|
117
|
+
# UserNameUpdated = Event.define('users.name_updated') do
|
118
|
+
# attribute :name, Types::String
|
119
|
+
# end
|
120
|
+
# debugger
|
data/lib/plumb/and.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 And
|
7
|
-
include
|
7
|
+
include Composable
|
8
8
|
|
9
|
-
attr_reader :
|
9
|
+
attr_reader :children
|
10
10
|
|
11
11
|
def initialize(left, right)
|
12
12
|
@left = left
|
13
13
|
@right = right
|
14
|
+
@children = [left, right].freeze
|
14
15
|
freeze
|
15
16
|
end
|
16
17
|
|
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,262 @@
|
|
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
|
+
end
|
114
|
+
|
115
|
+
attr_reader :errors, :attributes
|
116
|
+
|
117
|
+
def initialize(attrs = {})
|
118
|
+
assign_attributes(attrs)
|
119
|
+
end
|
120
|
+
|
121
|
+
def ==(other)
|
122
|
+
other.is_a?(self.class) && other.attributes == attributes
|
123
|
+
end
|
124
|
+
|
125
|
+
# @return [Boolean]
|
126
|
+
def valid? = !errors || errors.none?
|
127
|
+
|
128
|
+
# @param attrs [Hash]
|
129
|
+
# @return [Plumb::Attributes]
|
130
|
+
def with(attrs = BLANK_HASH)
|
131
|
+
self.class.new(attributes.merge(attrs))
|
132
|
+
end
|
133
|
+
|
134
|
+
def inspect
|
135
|
+
%(#<#{self.class}:#{object_id} [#{valid? ? 'valid' : 'invalid'}] #{attributes.map do |k, v|
|
136
|
+
[k, v.inspect].join(':')
|
137
|
+
end.join(' ')}>)
|
138
|
+
end
|
139
|
+
|
140
|
+
# @return [Hash]
|
141
|
+
def to_h
|
142
|
+
attributes.transform_values do |value|
|
143
|
+
case value
|
144
|
+
when ::Array
|
145
|
+
value.map { |v| v.respond_to?(:to_h) ? v.to_h : v }
|
146
|
+
else
|
147
|
+
value.respond_to?(:to_h) ? value.to_h : value
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def deconstruct(...) = to_h.values.deconstruct(...)
|
153
|
+
def deconstruct_keys(...) = to_h.deconstruct_keys(...)
|
154
|
+
|
155
|
+
private
|
156
|
+
|
157
|
+
def assign_attributes(attrs = BLANK_HASH)
|
158
|
+
raise ArgumentError, 'Must be a Hash of attributes' unless attrs.is_a?(::Hash)
|
159
|
+
|
160
|
+
@errors = BLANK_HASH
|
161
|
+
result = self.class._schema.resolve(attrs)
|
162
|
+
@attributes = result.value
|
163
|
+
@errors = result.errors unless result.valid?
|
164
|
+
end
|
165
|
+
|
166
|
+
module ClassMethods
|
167
|
+
def _schema
|
168
|
+
@_schema ||= HashClass.new
|
169
|
+
end
|
170
|
+
|
171
|
+
def inherited(subclass)
|
172
|
+
_schema._schema.each do |key, type|
|
173
|
+
subclass.attribute(key, type)
|
174
|
+
end
|
175
|
+
super
|
176
|
+
end
|
177
|
+
|
178
|
+
# The Plumb::Step interface
|
179
|
+
# @param result [Plumb::Result::Valid]
|
180
|
+
# @return [Plumb::Result::Valid, Plumb::Result::Invalid]
|
181
|
+
def call(result)
|
182
|
+
return result if result.value.is_a?(self)
|
183
|
+
return result.invalid(errors: ['Must be a Hash of attributes']) unless result.value.is_a?(Hash)
|
184
|
+
|
185
|
+
instance = new(result.value)
|
186
|
+
instance.valid? ? result.valid(instance) : result.invalid(instance, errors: instance.errors)
|
187
|
+
end
|
188
|
+
|
189
|
+
# Person = Data[:name => String, :age => Integer, title?: String]
|
190
|
+
def [](type_specs)
|
191
|
+
klass = Class.new(self)
|
192
|
+
type_specs.each do |key, type|
|
193
|
+
klass.attribute(key, type)
|
194
|
+
end
|
195
|
+
klass
|
196
|
+
end
|
197
|
+
|
198
|
+
# node name for visitors
|
199
|
+
def node_name = :data
|
200
|
+
|
201
|
+
# attribute(:friend) { attribute(:name, String) }
|
202
|
+
# attribute(:friend, MyStruct) { attribute(:name, String) }
|
203
|
+
# attribute(:name, String)
|
204
|
+
# attribute(:friends, Types::Array) { attribute(:name, String) }
|
205
|
+
# attribute(:friends, Types::Array) # same as Types::Array[Types::Any]
|
206
|
+
# attribute(:friends, Types::Array[Person])
|
207
|
+
#
|
208
|
+
def attribute(name, type = Types::Any, &block)
|
209
|
+
key = Key.wrap(name)
|
210
|
+
name = key.to_sym
|
211
|
+
type = Composable.wrap(type)
|
212
|
+
if block_given? # :foo, Array[Data] or :foo, Struct
|
213
|
+
type = Types::Data if type == Types::Any
|
214
|
+
type = Plumb.decorate(type) do |node|
|
215
|
+
if node.is_a?(Plumb::ArrayClass)
|
216
|
+
child = node.children.first
|
217
|
+
child = Types::Data if child == Types::Any
|
218
|
+
Types::Array[build_nested(name, child, &block)]
|
219
|
+
elsif node.is_a?(Plumb::Step)
|
220
|
+
build_nested(name, node, &block)
|
221
|
+
elsif node.is_a?(Class) && node <= Plumb::Attributes
|
222
|
+
build_nested(name, node, &block)
|
223
|
+
else
|
224
|
+
node
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
@_schema = _schema + { key => type }
|
230
|
+
define_method(name) { @attributes[name] }
|
231
|
+
end
|
232
|
+
|
233
|
+
def attribute?(name, *args, &block)
|
234
|
+
attribute(Key.new(name, optional: true), *args, &block)
|
235
|
+
end
|
236
|
+
|
237
|
+
def build_nested(name, node, &block)
|
238
|
+
if node.is_a?(Class) && node <= Plumb::Attributes
|
239
|
+
sub = Class.new(node)
|
240
|
+
sub.instance_exec(&block)
|
241
|
+
__set_nested_class__(name, sub)
|
242
|
+
return Composable.wrap(sub)
|
243
|
+
end
|
244
|
+
|
245
|
+
return node unless node.is_a?(Plumb::Step)
|
246
|
+
|
247
|
+
child = node.children.first
|
248
|
+
return node unless child <= Plumb::Attributes
|
249
|
+
|
250
|
+
sub = Class.new(child)
|
251
|
+
sub.instance_exec(&block)
|
252
|
+
__set_nested_class__(name, sub)
|
253
|
+
Composable.wrap(sub)
|
254
|
+
end
|
255
|
+
|
256
|
+
def __set_nested_class__(name, klass)
|
257
|
+
name = name.to_s.split('_').map(&:capitalize).join.sub(/s$/, '')
|
258
|
+
const_set(name, klass) unless const_defined?(name)
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
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
|
|
@@ -23,10 +23,6 @@ module Plumb
|
|
23
23
|
NOOP = ->(result) { result }
|
24
24
|
|
25
25
|
module Callable
|
26
|
-
def metadata
|
27
|
-
MetadataVisitor.call(self)
|
28
|
-
end
|
29
|
-
|
30
26
|
def resolve(value = Undefined)
|
31
27
|
call(Result.wrap(value))
|
32
28
|
end
|
@@ -43,9 +39,15 @@ module Plumb
|
|
43
39
|
end
|
44
40
|
end
|
45
41
|
|
46
|
-
module
|
47
|
-
|
42
|
+
# This module gets included by Composable,
|
43
|
+
# but only when Composable is `included` in classes, not `extended`.
|
44
|
+
# The rule of this module is to assign a name to constants that point to Composable instances.
|
45
|
+
module Naming
|
46
|
+
attr_reader :name
|
48
47
|
|
48
|
+
# When including this module,
|
49
|
+
# define a #node_name method on the Composable instance
|
50
|
+
# #node_name is used by Visitors to determine the type of node.
|
49
51
|
def self.included(base)
|
50
52
|
nname = base.name.split('::').last
|
51
53
|
nname.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
|
@@ -55,20 +57,6 @@ module Plumb
|
|
55
57
|
base.define_method(:node_name) { nname }
|
56
58
|
end
|
57
59
|
|
58
|
-
def self.wrap(callable)
|
59
|
-
if callable.is_a?(Steppable)
|
60
|
-
callable
|
61
|
-
elsif callable.is_a?(::Hash)
|
62
|
-
HashClass.new(schema: callable)
|
63
|
-
elsif callable.respond_to?(:call)
|
64
|
-
Step.new(callable)
|
65
|
-
else
|
66
|
-
MatchClass.new(callable)
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
attr_reader :name
|
71
|
-
|
72
60
|
class Name
|
73
61
|
def initialize(name)
|
74
62
|
@name = name
|
@@ -94,17 +82,42 @@ module Plumb
|
|
94
82
|
def inspect = name.to_s
|
95
83
|
|
96
84
|
def node_name = self.class.name.split('::').last.to_sym
|
85
|
+
end
|
86
|
+
|
87
|
+
# Composable mixes in composition methods to classes.
|
88
|
+
# such as #>>, #|, #not, and others.
|
89
|
+
# Any Composable class can participate in Plumb compositions.
|
90
|
+
module Composable
|
91
|
+
include Callable
|
92
|
+
|
93
|
+
# This only runs when including Composable,
|
94
|
+
# not extending classes with it.
|
95
|
+
def self.included(base)
|
96
|
+
base.send(:include, Naming)
|
97
|
+
end
|
98
|
+
|
99
|
+
def self.wrap(callable)
|
100
|
+
if callable.is_a?(Composable)
|
101
|
+
callable
|
102
|
+
elsif callable.is_a?(::Hash)
|
103
|
+
HashClass.new(schema: callable)
|
104
|
+
elsif callable.respond_to?(:call)
|
105
|
+
Step.new(callable)
|
106
|
+
else
|
107
|
+
MatchClass.new(callable)
|
108
|
+
end
|
109
|
+
end
|
97
110
|
|
98
111
|
def defer(definition = nil, &block)
|
99
112
|
Deferred.new(definition || block)
|
100
113
|
end
|
101
114
|
|
102
115
|
def >>(other)
|
103
|
-
And.new(self,
|
116
|
+
And.new(self, Composable.wrap(other))
|
104
117
|
end
|
105
118
|
|
106
119
|
def |(other)
|
107
|
-
Or.new(self,
|
120
|
+
Or.new(self, Composable.wrap(other))
|
108
121
|
end
|
109
122
|
|
110
123
|
def transform(target_type, callable = nil, &block)
|
@@ -115,8 +128,12 @@ module Plumb
|
|
115
128
|
self >> MatchClass.new(block, error: errors, label: errors)
|
116
129
|
end
|
117
130
|
|
118
|
-
def
|
119
|
-
|
131
|
+
def metadata(data = Undefined)
|
132
|
+
if data == Undefined
|
133
|
+
MetadataVisitor.call(self)
|
134
|
+
else
|
135
|
+
self >> Metadata.new(data)
|
136
|
+
end
|
120
137
|
end
|
121
138
|
|
122
139
|
def not(other = self)
|
@@ -138,7 +155,7 @@ module Plumb
|
|
138
155
|
def [](val) = match(val)
|
139
156
|
|
140
157
|
class Node
|
141
|
-
include
|
158
|
+
include Composable
|
142
159
|
|
143
160
|
attr_reader :node_name, :type, :attributes
|
144
161
|
|
@@ -168,11 +185,15 @@ module Plumb
|
|
168
185
|
types = Array(metadata[:type]).uniq
|
169
186
|
|
170
187
|
bargs = [self]
|
171
|
-
|
188
|
+
arg = Undefined
|
189
|
+
if rest.any?
|
190
|
+
bargs << rest.first
|
191
|
+
arg = rest.first
|
192
|
+
end
|
172
193
|
block = Plumb.policies.get(types, name)
|
173
194
|
pol = block.call(*bargs, &blk)
|
174
195
|
|
175
|
-
Policy.new(name,
|
196
|
+
Policy.new(name, arg, pol)
|
176
197
|
in [::Hash => opts] # #policy(p1: value, p2: value)
|
177
198
|
opts.reduce(self) { |step, (name, value)| step.policy(name, value) }
|
178
199
|
else
|
@@ -182,13 +203,19 @@ module Plumb
|
|
182
203
|
|
183
204
|
def ===(other)
|
184
205
|
case other
|
185
|
-
when
|
206
|
+
when Composable
|
186
207
|
other == self
|
187
208
|
else
|
188
209
|
resolve(other).valid?
|
189
210
|
end
|
190
211
|
end
|
191
212
|
|
213
|
+
def ==(other)
|
214
|
+
other.is_a?(self.class) && other.children == children
|
215
|
+
end
|
216
|
+
|
217
|
+
def children = BLANK_ARRAY
|
218
|
+
|
192
219
|
def build(cns, factory_method = :new, &block)
|
193
220
|
self >> Build.new(cns, factory_method:, &block)
|
194
221
|
end
|
@@ -201,6 +228,12 @@ module Plumb
|
|
201
228
|
inspect
|
202
229
|
end
|
203
230
|
|
231
|
+
# @option root [Boolean] whether to include JSON Schema $schema property
|
232
|
+
# @return [Hash]
|
233
|
+
def to_json_schema(root: false)
|
234
|
+
JSONSchemaVisitor.call(self, root:)
|
235
|
+
end
|
236
|
+
|
204
237
|
# Build a step that will invoke one or more methods on the value.
|
205
238
|
# Ex 1: Types::String.invoke(:downcase)
|
206
239
|
# Ex 2: Types::Array.invoke(:[], 1)
|