plumb 0.0.1 → 0.0.2

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,6 +167,11 @@ module Plumb
176
167
  self
177
168
  end
178
169
 
170
+ def match(matcher)
171
+ @_type = @_type.match(matcher)
172
+ self
173
+ end
174
+
179
175
  def rule(...)
180
176
  @_type = @_type.rule(...)
181
177
  self
@@ -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
@@ -19,6 +19,7 @@ module Plumb
19
19
  BLANK_ARRAY = [].freeze
20
20
  BLANK_HASH = {}.freeze
21
21
  BLANK_RESULT = Result.wrap(Undefined)
22
+ NOOP = ->(result) { result }
22
23
 
23
24
  module Callable
24
25
  def metadata
@@ -46,7 +47,7 @@ module Plumb
46
47
 
47
48
  def self.included(base)
48
49
  nname = base.name.split('::').last
49
- nname.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
50
+ nname.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
50
51
  nname.downcase!
51
52
  nname.gsub!(/_class$/, '')
52
53
  nname = nname.to_sym
@@ -56,10 +57,12 @@ module Plumb
56
57
  def self.wrap(callable)
57
58
  if callable.is_a?(Steppable)
58
59
  callable
60
+ elsif callable.is_a?(::Hash)
61
+ HashClass.new(schema: callable)
59
62
  elsif callable.respond_to?(:call)
60
63
  Step.new(callable)
61
64
  else
62
- StaticClass.new(callable)
65
+ MatchClass.new(callable)
63
66
  end
64
67
  end
65
68
 
@@ -108,11 +111,7 @@ module Plumb
108
111
  end
109
112
 
110
113
  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
114
+ self >> MatchClass.new(block, error: errors)
116
115
  end
117
116
 
118
117
  def meta(data = {})
@@ -145,10 +144,10 @@ module Plumb
145
144
 
146
145
  def default(val = Undefined, &block)
147
146
  val_type = if val == Undefined
148
- DefaultProc.call(block)
149
- else
150
- Types::Static[val]
151
- end
147
+ DefaultProc.call(block)
148
+ else
149
+ Types::Static[val]
150
+ end
152
151
 
153
152
  self | (Types::Undefined >> val_type)
154
153
  end
@@ -186,21 +185,17 @@ module Plumb
186
185
 
187
186
  def rule(*args)
188
187
  specs = case args
189
- in [::Symbol => rule_name, value]
190
- { rule_name => value }
191
- in [::Hash => rules]
192
- rules
193
- else
194
- raise ArgumentError, "expected 1 or 2 arguments, but got #{args.size}"
195
- end
188
+ in [::Symbol => rule_name, value]
189
+ { rule_name => value }
190
+ in [::Hash => rules]
191
+ rules
192
+ else
193
+ raise ArgumentError, "expected 1 or 2 arguments, but got #{args.size}"
194
+ end
196
195
 
197
196
  self >> Rules.new(specs, metadata[:type])
198
197
  end
199
198
 
200
- def is_a(klass)
201
- rule(is_a: klass)
202
- end
203
-
204
199
  def ===(other)
205
200
  case other
206
201
  when Steppable
@@ -210,18 +205,6 @@ module Plumb
210
205
  end
211
206
  end
212
207
 
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
208
  def build(cns, factory_method = :new, &block)
226
209
  self >> Build.new(cns, factory_method:, &block)
227
210
  end
@@ -233,6 +216,25 @@ module Plumb
233
216
  def to_s
234
217
  inspect
235
218
  end
219
+
220
+ # Build a step that will invoke onr or more methods on the value.
221
+ # Ex 1: Types::String.invoke(:downcase)
222
+ # Ex 2: Types::Array.invoke(:[], 1)
223
+ # Ex 3 chain of methods: Types::String.invoke([:downcase, :to_sym])
224
+ # @return [Step]
225
+ def invoke(*args, &block)
226
+ case args
227
+ in [::Symbol => method_name, *rest]
228
+ self >> Step.new(
229
+ ->(result) { result.valid(result.value.public_send(method_name, *rest, &block)) },
230
+ [method_name.inspect, rest.inspect].join(' ')
231
+ )
232
+ in [Array => methods] if methods.all? { |m| m.is_a?(Symbol) }
233
+ methods.reduce(self) { |step, method| step.invoke(method) }
234
+ else
235
+ raise ArgumentError, "expected a symbol or array of symbols, got #{args.inspect}"
236
+ end
237
+ end
236
238
  end
237
239
  end
238
240
 
@@ -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
data/lib/plumb/types.rb CHANGED
@@ -3,56 +3,10 @@
3
3
  require 'bigdecimal'
4
4
 
5
5
  module Plumb
6
- Rules.define :eq, 'must be equal to %<value>s' do |result, value|
7
- value == result.value
6
+ Rules.define :included_in, 'elements must be included in %<value>s', expects: ::Array do |result, opts|
7
+ result.value.all? { |v| opts.include?(v) }
8
8
  end
9
- Rules.define :not_eq, 'must not be equal to %<value>s' do |result, value|
10
- value != result.value
11
- end
12
- # :gt for numbers and #size (arrays, strings, hashes)
13
- [::String, ::Array, ::Hash].each do |klass|
14
- Rules.define :gt, 'must contain more than %<value>s elements', expects: klass do |result, value|
15
- value < result.value.size
16
- end
17
-
18
- # :lt for numbers and #size (arrays, strings, hashes)
19
- Rules.define :lt, 'must contain fewer than %<value>s elements', expects: klass do |result, value|
20
- value > result.value.size
21
- end
22
-
23
- Rules.define :gte, 'must be size greater or equal to %<value>s', expects: klass do |result, value|
24
- value <= result.value.size
25
- end
26
-
27
- Rules.define :lte, 'must be size less or equal to %<value>s', expects: klass do |result, value|
28
- value >= result.value
29
- end
30
- end
31
- # :gt and :lt for numbers, BigDecimal
32
- [::Numeric].each do |klass|
33
- Rules.define :gt, 'must be greater than %<value>s', expects: klass do |result, value|
34
- value < result.value
35
- end
36
- Rules.define :lt, 'must be greater than %<value>s', expects: klass do |result, value|
37
- value > result.value
38
- end
39
- Rules.define :gte, 'must be greater or equal to %<value>s', expects: klass do |result, value|
40
- value <= result.value
41
- end
42
- # :lte for numbers and #size (arrays, strings, hashes)
43
- Rules.define :lte, 'must be less or equal to %<value>s', expects: klass do |result, value|
44
- value >= result.value
45
- end
46
- end
47
-
48
- Rules.define :match, 'must match %<value>s', metadata_key: :pattern do |result, value|
49
- value === result.value
50
- end
51
- Rules.define :included_in, 'elements must be included in %<value>s', expects: ::Array,
52
- metadata_key: :options do |result, opts|
53
- result.value.all? { |v| opts.include?(v) }
54
- end
55
- Rules.define :included_in, 'must be included in %<value>s', metadata_key: :options do |result, opts|
9
+ Rules.define :included_in, 'must be included in %<value>s' do |result, opts|
56
10
  opts.include? result.value
57
11
  end
58
12
  Rules.define :excluded_from, 'elements must not be included in %<value>s', expects: ::Array do |result, value|
@@ -64,7 +18,7 @@ module Plumb
64
18
  Rules.define :respond_to, 'must respond to %<value>s' do |result, value|
65
19
  Array(value).all? { |m| result.value.respond_to?(m) }
66
20
  end
67
- Rules.define :size, 'must be of size %<value>s', expects: :size, metadata_key: :size do |result, value|
21
+ Rules.define :size, 'must be of size %<value>s', expects: :size do |result, value|
68
22
  value === result.value.size
69
23
  end
70
24
 
@@ -85,6 +39,7 @@ module Plumb
85
39
  False = Any[::FalseClass]
86
40
  Boolean = (True | False).as_node(:boolean)
87
41
  Array = ArrayClass.new
42
+ Stream = StreamClass.new
88
43
  Tuple = TupleClass.new
89
44
  Hash = HashClass.new
90
45
  Interface = InterfaceClass.new
@@ -104,8 +59,8 @@ module Plumb
104
59
  NUMBER_EXPR = /^\d{1,3}(?:,\d{3})*(?:\.\d+)?$/
105
60
 
106
61
  String = Types::String \
107
- | Any.coerce(BigDecimal) { |v| v.to_s('F') } \
108
- | Any.coerce(::Numeric, &:to_s)
62
+ | Types::Decimal.transform(::String) { |v| v.to_s('F') } \
63
+ | Types::Numeric.transform(::String, &:to_s)
109
64
 
110
65
  Symbol = Types::Symbol | Types::String.transform(::Symbol, &:to_sym)
111
66
 
@@ -115,22 +70,26 @@ module Plumb
115
70
  Numeric = Types::Numeric | CoercibleNumberString.transform(::Numeric, &:to_f)
116
71
 
117
72
  Decimal = Types::Decimal | \
118
- (Types::Numeric.transform(::String, &:to_s) | CoercibleNumberString) \
119
- .transform(::BigDecimal) { |v| BigDecimal(v) }
73
+ (Types::Numeric.transform(::String, &:to_s) | CoercibleNumberString) \
74
+ .transform(::BigDecimal) { |v| BigDecimal(v) }
120
75
 
121
76
  Integer = Numeric.transform(::Integer, &:to_i)
122
77
  end
123
78
 
124
79
  module Forms
125
80
  True = Types::True \
126
- | Types::String >> Any.coerce(/^true$/i) { |_| true } \
127
- | Any.coerce('1') { |_| true } \
128
- | Any.coerce(1) { |_| true }
81
+ | (
82
+ Types::String[/^true$/i] \
83
+ | Types::String['1'] \
84
+ | Types::Integer[1]
85
+ ).transform(::TrueClass) { |_| true }
129
86
 
130
87
  False = Types::False \
131
- | Types::String >> Any.coerce(/^false$/i) { |_| false } \
132
- | Any.coerce('0') { |_| false } \
133
- | Any.coerce(0) { |_| false }
88
+ | (
89
+ Types::String[/^false$/i] \
90
+ | Types::String['0'] \
91
+ | Types::Integer[0]
92
+ ).transform(::FalseClass) { |_| false }
134
93
 
135
94
  Boolean = True | False
136
95
 
@@ -10,14 +10,17 @@ module Plumb
10
10
 
11
11
  def initialize(value = Undefined)
12
12
  @value = value
13
+ freeze
13
14
  end
14
15
 
15
- def inspect = @value.inspect
16
-
17
16
  def [](value) = self.class.new(value)
18
17
 
19
18
  def call(result)
20
19
  @value == result.value ? result : result.invalid(errors: "Must be equal to #{@value}")
21
20
  end
21
+
22
+ private
23
+
24
+ def _inspect = @value.inspect
22
25
  end
23
26
  end
data/lib/plumb/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Plumb
4
- VERSION = '0.0.1'
4
+ VERSION = '0.0.2'
5
5
  end
@@ -9,26 +9,30 @@ module Plumb
9
9
  module ClassMethods
10
10
  def on(node_name, &block)
11
11
  name = node_name.is_a?(Symbol) ? node_name : :"#{node_name}_class"
12
- self.define_method("visit_#{name}", &block)
12
+ define_method("visit_#{name}", &block)
13
13
  end
14
14
 
15
- def visit(type, props = BLANK_HASH)
16
- new.visit(type, props)
15
+ def visit(node, props = BLANK_HASH)
16
+ new.visit(node, props)
17
17
  end
18
18
  end
19
19
 
20
- def visit(type, props = BLANK_HASH)
21
- method_name = type.respond_to?(:node_name) ? type.node_name : :"#{(type.is_a?(::Class) ? type : type.class)}_class"
20
+ def visit(node, props = BLANK_HASH)
21
+ method_name = if node.respond_to?(:node_name)
22
+ node.node_name
23
+ else
24
+ :"#{(node.is_a?(::Class) ? node : node.class)}_class"
25
+ end
22
26
  method_name = "visit_#{method_name}"
23
27
  if respond_to?(method_name)
24
- send(method_name, type, props)
28
+ send(method_name, node, props)
25
29
  else
26
- on_missing_handler(type, props, method_name)
30
+ on_missing_handler(node, props, method_name)
27
31
  end
28
32
  end
29
33
 
30
- def on_missing_handler(type, _props, method_name)
31
- raise "No handler for #{type.inspect} with :#{method_name}"
34
+ def on_missing_handler(node, _props, method_name)
35
+ raise "No handler for #{node.inspect} with :#{method_name}"
32
36
  end
33
37
  end
34
38
  end
data/lib/plumb.rb CHANGED
@@ -18,6 +18,7 @@ require 'plumb/not'
18
18
  require 'plumb/or'
19
19
  require 'plumb/tuple_class'
20
20
  require 'plumb/array_class'
21
+ require 'plumb/stream_class'
21
22
  require 'plumb/hash_class'
22
23
  require 'plumb/interface_class'
23
24
  require 'plumb/types'