plumb 0.0.1 → 0.0.3

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.
data/lib/plumb/schema.rb CHANGED
@@ -7,13 +7,13 @@ module Plumb
7
7
  class Schema
8
8
  include Steppable
9
9
 
10
- def self.wrap(sc = nil, &block)
11
- raise ArgumentError, 'expected a block or a schema' if sc.nil? && !block_given?
10
+ def self.wrap(sch = nil, &block)
11
+ raise ArgumentError, 'expected a block or a schema' if sch.nil? && !block_given?
12
12
 
13
- if sc
14
- raise ArgumentError, 'expected a Steppable' unless sc.is_a?(Steppable)
13
+ if sch
14
+ raise ArgumentError, 'expected a Steppable' unless sch.is_a?(Steppable)
15
15
 
16
- return sc
16
+ return sch
17
17
  end
18
18
 
19
19
  new(&block)
@@ -25,9 +25,10 @@ module Plumb
25
25
  @pipeline = Types::Any
26
26
  @before = Types::Any
27
27
  @after = Types::Any
28
- @_schema = {}
29
28
  @_hash = hash
30
- @fields = SymbolAccessHash.new({})
29
+ @fields = @_hash._schema.each.with_object(SymbolAccessHash.new({})) do |(k, v), memo|
30
+ memo[k] = Field.new(k, v)
31
+ end
31
32
 
32
33
  setup(&block) if block_given?
33
34
 
@@ -71,18 +72,17 @@ module Plumb
71
72
 
72
73
  private def finish
73
74
  @pipeline = @before.freeze >> @_hash.freeze >> @after.freeze
74
- @_schema.clear.freeze
75
75
  freeze
76
76
  end
77
77
 
78
- def field(key)
78
+ def field(key, type = nil, &block)
79
79
  key = Key.new(key.to_sym)
80
- @fields[key] = Field.new(key)
80
+ @fields[key] = Field.new(key, type, &block)
81
81
  end
82
82
 
83
- def field?(key)
83
+ def field?(key, type = nil, &block)
84
84
  key = Key.new(key.to_sym, optional: true)
85
- @fields[key] = Field.new(key)
85
+ @fields[key] = Field.new(key, type, &block)
86
86
  end
87
87
 
88
88
  def +(other)
@@ -102,10 +102,6 @@ module Plumb
102
102
 
103
103
  attr_reader :_hash
104
104
 
105
- private
106
-
107
- attr_reader :_schema
108
-
109
105
  class SymbolAccessHash < SimpleDelegator
110
106
  def [](key)
111
107
  __getobj__[Key.wrap(key)]
@@ -117,33 +113,28 @@ module Plumb
117
113
 
118
114
  attr_reader :_type, :key
119
115
 
120
- def initialize(key)
116
+ def initialize(key, type = nil, &block)
121
117
  @key = key.to_sym
122
- @_type = Types::Any
118
+ @_type = case type
119
+ when ArrayClass, Array
120
+ block_given? ? ArrayClass.new(element_type: Schema.new(&block)) : type
121
+ when nil
122
+ block_given? ? Schema.new(&block) : Types::Any
123
+ when Steppable
124
+ type
125
+ when Class
126
+ if type == Array && block_given?
127
+ ArrayClass.new(element_type: Schema.new(&block))
128
+ else
129
+ Types::Any[type]
130
+ end
131
+ else
132
+ raise ArgumentError, "expected a Plumb type, but got #{type.inspect}"
133
+ end
123
134
  end
124
135
 
125
136
  def call(result) = _type.call(result)
126
137
 
127
- def type(steppable)
128
- unless steppable.respond_to?(:call)
129
- raise ArgumentError,
130
- "expected a Plumb type, but got #{steppable.inspect}"
131
- end
132
-
133
- @_type >>= steppable
134
- self
135
- end
136
-
137
- def schema(...)
138
- @_type >>= Schema.wrap(...)
139
- self
140
- end
141
-
142
- def array(...)
143
- @_type >>= Types::Array[Schema.wrap(...)]
144
- self
145
- end
146
-
147
138
  def default(v, &block)
148
139
  @_type = @_type.default(v, &block)
149
140
  self
@@ -157,12 +148,12 @@ module Plumb
157
148
  def metadata = @_type.metadata
158
149
 
159
150
  def options(opts)
160
- @_type = @_type.rule(included_in: opts)
151
+ @_type = @_type.options(opts)
161
152
  self
162
153
  end
163
154
 
164
- def optional
165
- @_type = Types::Nil | @_type
155
+ def nullable
156
+ @_type = @_type.nullable
166
157
  self
167
158
  end
168
159
 
@@ -176,8 +167,13 @@ module Plumb
176
167
  self
177
168
  end
178
169
 
179
- def rule(...)
180
- @_type = @_type.rule(...)
170
+ def match(matcher)
171
+ @_type = @_type.match(matcher)
172
+ self
173
+ end
174
+
175
+ def policy(...)
176
+ @_type = @_type.policy(...)
181
177
  self
182
178
  end
183
179
 
@@ -19,12 +19,12 @@ module Plumb
19
19
  self.class.new(value)
20
20
  end
21
21
 
22
- private def _inspect
23
- %(#{name}[#{@value.inspect}])
24
- end
25
-
26
22
  def call(result)
27
23
  result.valid(@value)
28
24
  end
25
+
26
+ private
27
+
28
+ def _inspect = @value.inspect
29
29
  end
30
30
  end
data/lib/plumb/step.rb CHANGED
@@ -8,14 +8,19 @@ module Plumb
8
8
 
9
9
  attr_reader :_metadata
10
10
 
11
- def initialize(callable = nil, &block)
11
+ def initialize(callable = nil, inspect = nil, &block)
12
12
  @_metadata = callable.respond_to?(:metadata) ? callable.metadata : BLANK_HASH
13
13
  @callable = callable || block
14
+ @inspect = inspect || @callable.inspect
14
15
  freeze
15
16
  end
16
17
 
17
18
  def call(result)
18
19
  @callable.call(result)
19
20
  end
21
+
22
+ private
23
+
24
+ def _inspect = "Step[#{@inspect}]"
20
25
  end
21
26
  end
@@ -10,6 +10,7 @@ module Plumb
10
10
 
11
11
  def to_s = inspect
12
12
  def node_name = :undefined
13
+ def empty? = true
13
14
  end
14
15
 
15
16
  TypeError = Class.new(::TypeError)
@@ -19,6 +20,7 @@ module Plumb
19
20
  BLANK_ARRAY = [].freeze
20
21
  BLANK_HASH = {}.freeze
21
22
  BLANK_RESULT = Result.wrap(Undefined)
23
+ NOOP = ->(result) { result }
22
24
 
23
25
  module Callable
24
26
  def metadata
@@ -46,7 +48,7 @@ module Plumb
46
48
 
47
49
  def self.included(base)
48
50
  nname = base.name.split('::').last
49
- nname.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
51
+ nname.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
50
52
  nname.downcase!
51
53
  nname.gsub!(/_class$/, '')
52
54
  nname = nname.to_sym
@@ -56,10 +58,12 @@ module Plumb
56
58
  def self.wrap(callable)
57
59
  if callable.is_a?(Steppable)
58
60
  callable
61
+ elsif callable.is_a?(::Hash)
62
+ HashClass.new(schema: callable)
59
63
  elsif callable.respond_to?(:call)
60
64
  Step.new(callable)
61
65
  else
62
- StaticClass.new(callable)
66
+ MatchClass.new(callable)
63
67
  end
64
68
  end
65
69
 
@@ -108,11 +112,7 @@ module Plumb
108
112
  end
109
113
 
110
114
  def check(errors = 'did not pass the check', &block)
111
- a_check = lambda { |result|
112
- block.call(result.value) ? result : result.invalid(errors:)
113
- }
114
-
115
- self >> a_check
115
+ self >> MatchClass.new(block, error: errors, label: errors)
116
116
  end
117
117
 
118
118
  def meta(data = {})
@@ -137,22 +137,6 @@ module Plumb
137
137
 
138
138
  def [](val) = match(val)
139
139
 
140
- DefaultProc = proc do |callable|
141
- proc do |result|
142
- result.valid(callable.call)
143
- end
144
- end
145
-
146
- def default(val = Undefined, &block)
147
- val_type = if val == Undefined
148
- DefaultProc.call(block)
149
- else
150
- Types::Static[val]
151
- end
152
-
153
- self | (Types::Undefined >> val_type)
154
- end
155
-
156
140
  class Node
157
141
  include Steppable
158
142
 
@@ -172,33 +156,28 @@ module Plumb
172
156
  Node.new(node_name, self, metadata)
173
157
  end
174
158
 
175
- def nullable
176
- Types::Nil | self
177
- end
178
-
179
- def present
180
- Types::Present >> self
181
- end
182
-
183
- def options(opts = [])
184
- rule(included_in: opts)
185
- end
186
-
187
- def rule(*args)
188
- specs = case args
189
- in [::Symbol => rule_name, value]
190
- { rule_name => value }
191
- in [::Hash => rules]
192
- rules
159
+ # Register a policy for this step.
160
+ # Mode 1.a: #policy(:name, arg) a single policy with an argument
161
+ # Mode 1.b: #policy(:name) a single policy without an argument
162
+ # Mode 2: #policy(p1: value, p2: value) multiple policies with arguments
163
+ # The latter mode will be expanded to multiple #policy calls.
164
+ # @return [Step]
165
+ def policy(*args, &blk)
166
+ case args
167
+ in [::Symbol => name, *rest] # #policy(:name, arg)
168
+ types = Array(metadata[:type]).uniq
169
+
170
+ bargs = [self]
171
+ bargs << rest.first if rest.any?
172
+ block = Plumb.policies.get(types, name)
173
+ pol = block.call(*bargs, &blk)
174
+
175
+ Policy.new(name, rest.first, pol)
176
+ in [::Hash => opts] # #policy(p1: value, p2: value)
177
+ opts.reduce(self) { |step, (name, value)| step.policy(name, value) }
193
178
  else
194
- raise ArgumentError, "expected 1 or 2 arguments, but got #{args.size}"
179
+ raise ArgumentError, "expected a symbol or hash, got #{args.inspect}"
195
180
  end
196
-
197
- self >> Rules.new(specs, metadata[:type])
198
- end
199
-
200
- def is_a(klass)
201
- rule(is_a: klass)
202
181
  end
203
182
 
204
183
  def ===(other)
@@ -210,18 +189,6 @@ module Plumb
210
189
  end
211
190
  end
212
191
 
213
- def coerce(type, coercion = nil, &block)
214
- coercion ||= block
215
- step = lambda { |result|
216
- if type === result.value
217
- result.valid(coercion.call(result.value))
218
- else
219
- result.invalid(errors: "%s can't be coerced" % result.value.inspect)
220
- end
221
- }
222
- self >> step
223
- end
224
-
225
192
  def build(cns, factory_method = :new, &block)
226
193
  self >> Build.new(cns, factory_method:, &block)
227
194
  end
@@ -233,10 +200,30 @@ module Plumb
233
200
  def to_s
234
201
  inspect
235
202
  end
203
+
204
+ # Build a step that will invoke one or more methods on the value.
205
+ # Ex 1: Types::String.invoke(:downcase)
206
+ # Ex 2: Types::Array.invoke(:[], 1)
207
+ # Ex 3 chain of methods: Types::String.invoke([:downcase, :to_sym])
208
+ # @return [Step]
209
+ def invoke(*args, &block)
210
+ case args
211
+ in [::Symbol => method_name, *rest]
212
+ self >> Step.new(
213
+ ->(result) { result.valid(result.value.public_send(method_name, *rest, &block)) },
214
+ [method_name.inspect, rest.inspect].join(' ')
215
+ )
216
+ in [Array => methods] if methods.all? { |m| m.is_a?(Symbol) }
217
+ methods.reduce(self) { |step, method| step.invoke(method) }
218
+ else
219
+ raise ArgumentError, "expected a symbol or array of symbols, got #{args.inspect}"
220
+ end
221
+ end
236
222
  end
237
223
  end
238
224
 
239
225
  require 'plumb/deferred'
240
226
  require 'plumb/transform'
227
+ require 'plumb/policy'
241
228
  require 'plumb/build'
242
229
  require 'plumb/metadata'
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thread'
4
+ require 'plumb/steppable'
5
+
6
+ module Plumb
7
+ # A stream that validates each element.
8
+ # Example:
9
+ # row = Types::Tuple[String, Types::Lax::Integer]
10
+ # csv_stream = Types::Stream[row]
11
+ #
12
+ # stream = csv_stream.parse(CSV.new(File.new('data.csv')).to_enum)
13
+ # stream.each |result|
14
+ # result.valid? # => true
15
+ # result.value # => ['name', 10]
16
+ # end
17
+ class StreamClass
18
+ include Steppable
19
+
20
+ attr_reader :element_type
21
+
22
+ # @option element_type [Steppable] the type of the elements in the stream
23
+ def initialize(element_type: Types::Any)
24
+ @element_type = Steppable.wrap(element_type)
25
+ freeze
26
+ end
27
+
28
+ # return a new Stream definition.
29
+ # @param element_type [Steppable] the type of the elements in the stream
30
+ def [](element_type)
31
+ self.class.new(element_type:)
32
+ end
33
+
34
+ # The [Step] interface
35
+ # @param result [Result::Valid]
36
+ # @return [Result::Valid, Result::Invalid]
37
+ def call(result)
38
+ return result.invalid(errors: 'is not an Enumerable') unless result.value.respond_to?(:each)
39
+
40
+ enum = Enumerator.new do |y|
41
+ result.value.each do |e|
42
+ y << element_type.resolve(e)
43
+ end
44
+ end
45
+
46
+ result.valid(enum)
47
+ end
48
+
49
+ # @return [Step] a step that resolves to an Enumerator that filters out invalid elements
50
+ def filtered
51
+ self >> Step.new(nil, 'filtered') do |result|
52
+ set = result.value.lazy.filter_map { |e| e.value if e.valid? }
53
+ result.valid(set)
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ def _inspect = "Stream[#{@element_type.inspect}]"
60
+ end
61
+ end
@@ -13,15 +13,20 @@ module Plumb
13
13
  @key = Key.wrap(key)
14
14
  @types = types
15
15
 
16
- raise ArgumentError, 'all types must be HashClass' if @types.size == 0 || @types.any? do |t|
16
+ raise ArgumentError, 'all types must be HashClass' if @types.size.zero? || @types.any? do |t|
17
17
  !t.is_a?(HashClass)
18
18
  end
19
19
  raise ArgumentError, "all types must define key #{@key}" unless @types.all? { |t| !!t.at_key(@key) }
20
20
 
21
- # types are assumed to have static values for the index field :key
21
+ # types are assumed to have literal values for the index field :key
22
22
  @index = @types.each.with_object({}) do |t, memo|
23
- memo[t.at_key(@key).resolve.value] = t
23
+ key_type = t.at_key(@key)
24
+ raise TypeError, "key type at :#{@key} #{key_type} must be a Match type" unless key_type.is_a?(MatchClass)
25
+
26
+ memo[key_type.matcher] = t
24
27
  end
28
+
29
+ freeze
25
30
  end
26
31
 
27
32
  def call(result)
@@ -33,5 +38,9 @@ module Plumb
33
38
 
34
39
  child.call(result)
35
40
  end
41
+
42
+ private
43
+
44
+ def _inspect = "TaggedHash[#{@key.inspect}, #{@types.map(&:inspect).join(', ')}]"
36
45
  end
37
46
  end
@@ -10,11 +10,16 @@ module Plumb
10
10
 
11
11
  def initialize(target_type, callable)
12
12
  @target_type = target_type
13
- @callable = callable
13
+ @callable = callable || Plumb::NOOP
14
+ freeze
14
15
  end
15
16
 
16
17
  def call(result)
17
18
  result.valid(@callable.call(result.value))
18
19
  end
20
+
21
+ private
22
+
23
+ def _inspect = "->(#{@target_type.inspect})"
19
24
  end
20
25
  end
@@ -9,7 +9,8 @@ module Plumb
9
9
  attr_reader :types
10
10
 
11
11
  def initialize(*types)
12
- @types = types.map { |t| t.is_a?(Steppable) ? t : Types::Any.value(t) }
12
+ @types = types.map { |t| Steppable.wrap(t) }
13
+ freeze
13
14
  end
14
15
 
15
16
  def of(*types)
@@ -18,10 +19,6 @@ module Plumb
18
19
 
19
20
  alias [] of
20
21
 
21
- private def _inspect
22
- "#{name}[#{@types.map(&:inspect).join(', ')}]"
23
- end
24
-
25
22
  def call(result)
26
23
  return result.invalid(errors: 'must be an Array') unless result.value.is_a?(::Array)
27
24
  return result.invalid(errors: 'must have the same size') unless result.value.size == @types.size
@@ -38,5 +35,11 @@ module Plumb
38
35
 
39
36
  result.invalid(errors:)
40
37
  end
38
+
39
+ private
40
+
41
+ def _inspect
42
+ "Tuple[#{@types.map(&:inspect).join(', ')}]"
43
+ end
41
44
  end
42
45
  end