plumb 0.0.1 → 0.0.3

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