plumb 0.0.1 → 0.0.2

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,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'