plumb 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -15,6 +15,8 @@ module Plumb
15
15
  end
16
16
 
17
17
  def call(result)
18
+ return result.invalid(errors: 'must be a Hash') unless result.value.is_a?(::Hash)
19
+
18
20
  failed = result.value.lazy.filter_map do |key, value|
19
21
  key_r = @key_type.resolve(key)
20
22
  value_r = @value_type.resolve(value)
@@ -31,5 +33,37 @@ module Plumb
31
33
  rescue StopIteration
32
34
  result
33
35
  end
36
+
37
+ def filtered
38
+ FilteredHashMap.new(key_type, value_type)
39
+ end
40
+
41
+ private def _inspect = "HashMap[#{@key_type.inspect}, #{@value_type.inspect}]"
42
+
43
+ class FilteredHashMap
44
+ include Steppable
45
+
46
+ attr_reader :key_type, :value_type
47
+
48
+ def initialize(key_type, value_type)
49
+ @key_type = key_type
50
+ @value_type = value_type
51
+ freeze
52
+ end
53
+
54
+ def call(result)
55
+ result.invalid(errors: 'must be a Hash') unless result.value.is_a?(::Hash)
56
+
57
+ hash = result.value.each.with_object({}) do |(key, value), memo|
58
+ key_r = @key_type.resolve(key)
59
+ value_r = @value_type.resolve(value)
60
+ memo[key_r.value] = value_r.value if key_r.valid? && value_r.valid?
61
+ end
62
+
63
+ result.valid(hash)
64
+ end
65
+
66
+ private def _inspect = "HashMap[#{@key_type.inspect}, #{@value_type.inspect}].filtered"
67
+ end
34
68
  end
35
69
  end
@@ -16,10 +16,10 @@ module Plumb
16
16
  def of(*args)
17
17
  case args
18
18
  in Array => symbols if symbols.all? { |s| s.is_a?(::Symbol) }
19
- self.class.new(symbols)
20
- else
21
- raise ::ArgumentError, "unexpected value to Types::Interface#of #{args.inspect}"
22
- end
19
+ self.class.new(symbols)
20
+ else
21
+ raise ::ArgumentError, "unexpected value to Types::Interface#of #{args.inspect}"
22
+ end
23
23
  end
24
24
 
25
25
  alias [] of
@@ -31,5 +31,7 @@ module Plumb
31
31
 
32
32
  result
33
33
  end
34
+
35
+ private def _inspect = "Interface[#{method_names.join(', ')}]"
34
36
  end
35
37
  end
@@ -19,39 +19,43 @@ module Plumb
19
19
  MINIMUM = 'minimum'
20
20
  MAXIMUM = 'maximum'
21
21
 
22
- def self.call(type)
23
- {
24
- '$schema' => 'https://json-schema.org/draft-08/schema#',
25
- }.merge(new.visit(type))
22
+ def self.call(node)
23
+ {
24
+ '$schema' => 'https://json-schema.org/draft-08/schema#'
25
+ }.merge(new.visit(node))
26
26
  end
27
27
 
28
28
  private def stringify_keys(hash) = hash.transform_keys(&:to_s)
29
29
 
30
- on(:any) do |type, props|
30
+ on(:any) do |_node, props|
31
31
  props
32
32
  end
33
33
 
34
- on(:pipeline) do |type, props|
35
- visit(type.type, props)
34
+ on(:pipeline) do |node, props|
35
+ visit(node.type, props)
36
+ end
37
+
38
+ on(:step) do |node, props|
39
+ props.merge(stringify_keys(node._metadata))
36
40
  end
37
41
 
38
- on(:step) do |type, props|
39
- props.merge(stringify_keys(type._metadata))
42
+ on(:interface) do |_node, props|
43
+ props
40
44
  end
41
45
 
42
- on(:hash) do |type, props|
46
+ on(:hash) do |node, props|
43
47
  props.merge(
44
48
  TYPE => 'object',
45
- PROPERTIES => type._schema.each_with_object({}) do |(key, value), hash|
49
+ PROPERTIES => node._schema.each_with_object({}) do |(key, value), hash|
46
50
  hash[key.to_s] = visit(value)
47
51
  end,
48
- REQUIRED => type._schema.select { |key, value| !key.optional? }.keys.map(&:to_s)
52
+ REQUIRED => node._schema.reject { |key, _value| key.optional? }.keys.map(&:to_s)
49
53
  )
50
54
  end
51
55
 
52
- on(:and) do |type, props|
53
- left = visit(type.left)
54
- right = visit(type.right)
56
+ on(:and) do |node, props|
57
+ left = visit(node.left)
58
+ right = visit(node.right)
55
59
  type = right[TYPE] || left[TYPE]
56
60
  props = props.merge(left).merge(right)
57
61
  props = props.merge(TYPE => type) if type
@@ -59,148 +63,187 @@ module Plumb
59
63
  end
60
64
 
61
65
  # A "default" value is usually an "or" of expected_value | (undefined >> static_value)
62
- on(:or) do |type, props|
63
- left = visit(type.left)
64
- right = visit(type.right)
66
+ on(:or) do |node, props|
67
+ left = visit(node.left)
68
+ right = visit(node.right)
65
69
  any_of = [left, right].uniq
66
70
  if any_of.size == 1
67
71
  props.merge(left)
68
72
  elsif any_of.size == 2 && (defidx = any_of.index { |p| p.key?(DEFAULT) })
69
- val = any_of[defidx == 0 ? 1 : 0]
73
+ val = any_of[defidx.zero? ? 1 : 0]
70
74
  props.merge(val).merge(DEFAULT => any_of[defidx][DEFAULT])
71
75
  else
72
76
  props.merge(ANY_OF => any_of)
73
77
  end
74
78
  end
75
79
 
76
- on(:value) do |type, props|
77
- props = case type.value
78
- when ::String, ::Symbol, ::Numeric
79
- props.merge(CONST => type.value)
80
- else
81
- props
82
- end
80
+ on(:not) do |node, props|
81
+ props.merge('not' => visit(node.step))
82
+ end
83
83
 
84
- visit(type.value, props)
84
+ on(:value) do |node, props|
85
+ props = case node.value
86
+ when ::String, ::Symbol, ::Numeric
87
+ props.merge(CONST => node.value)
88
+ else
89
+ props
90
+ end
91
+
92
+ visit(node.value, props)
85
93
  end
86
94
 
87
- on(:transform) do |type, props|
88
- visit(type.target_type, props)
95
+ on(:transform) do |node, props|
96
+ visit(node.target_type, props)
89
97
  end
90
98
 
91
- on(:undefined) do |type, props|
99
+ on(:undefined) do |_node, props|
92
100
  props
93
101
  end
94
102
 
95
- on(:static) do |type, props|
96
- props = case type.value
97
- when ::String, ::Symbol, ::Numeric
98
- props.merge(CONST => type.value, DEFAULT => type.value)
99
- else
100
- props
101
- end
102
-
103
- visit(type.value, props)
103
+ on(:static) do |node, props|
104
+ # Set const AND default
105
+ # to emulate static values
106
+ props = case node.value
107
+ when ::String, ::Symbol, ::Numeric
108
+ props.merge(CONST => node.value, DEFAULT => node.value)
109
+ else
110
+ props
111
+ end
112
+
113
+ visit(node.value, props)
104
114
  end
105
115
 
106
- on(:rules) do |type, props|
107
- type.rules.reduce(props) do |acc, rule|
116
+ on(:rules) do |node, props|
117
+ node.rules.reduce(props) do |acc, rule|
108
118
  acc.merge(visit(rule))
109
119
  end
110
120
  end
111
121
 
112
- on(:rule_included_in) do |type, props|
113
- props.merge(ENUM => type.arg_value)
122
+ on(:rule_included_in) do |node, props|
123
+ props.merge(ENUM => node.arg_value)
114
124
  end
115
125
 
116
- on(:match) do |type, props|
117
- visit(type.matcher, props)
126
+ on(:match) do |node, props|
127
+ # Set const if primitive
128
+ props = case node.matcher
129
+ when ::String, ::Symbol, ::Numeric
130
+ props.merge(CONST => node.matcher)
131
+ else
132
+ props
133
+ end
134
+
135
+ visit(node.matcher, props)
118
136
  end
119
137
 
120
- on(:boolean) do |type, props|
138
+ on(:boolean) do |_node, props|
121
139
  props.merge(TYPE => 'boolean')
122
140
  end
123
141
 
124
- on(::String) do |type, props|
142
+ on(::String) do |_node, props|
125
143
  props.merge(TYPE => 'string')
126
144
  end
127
145
 
128
- on(::Integer) do |type, props|
146
+ on(::Integer) do |_node, props|
129
147
  props.merge(TYPE => 'integer')
130
148
  end
131
149
 
132
- on(::Numeric) do |type, props|
150
+ on(::Numeric) do |_node, props|
133
151
  props.merge(TYPE => 'number')
134
152
  end
135
153
 
136
- on(::BigDecimal) do |type, props|
154
+ on(::BigDecimal) do |_node, props|
137
155
  props.merge(TYPE => 'number')
138
156
  end
139
157
 
140
- on(::Float) do |type, props|
158
+ on(::Float) do |_node, props|
141
159
  props.merge(TYPE => 'number')
142
160
  end
143
161
 
144
- on(::TrueClass) do |type, props|
162
+ on(::TrueClass) do |_node, props|
145
163
  props.merge(TYPE => 'boolean')
146
164
  end
147
165
 
148
- on(::NilClass) do |type, props|
166
+ on(::NilClass) do |_node, props|
149
167
  props.merge(TYPE => 'null')
150
168
  end
151
169
 
152
- on(::FalseClass) do |type, props|
170
+ on(::FalseClass) do |_node, props|
153
171
  props.merge(TYPE => 'boolean')
154
172
  end
155
173
 
156
- on(::Regexp) do |type, props|
157
- props.merge(PATTERN => type.source)
174
+ on(::Regexp) do |node, props|
175
+ props.merge(PATTERN => node.source, TYPE => props[TYPE] || 'string')
158
176
  end
159
177
 
160
- on(::Range) do |type, props|
161
- opts = {}
162
- opts[MINIMUM] = type.min if type.begin
163
- opts[MAXIMUM] = type.max if type.end
178
+ on(::Range) do |node, props|
179
+ element = node.begin || node.end
180
+ opts = visit(element.class)
181
+ if element.is_a?(::Numeric)
182
+ opts[MINIMUM] = node.min if node.begin
183
+ opts[MAXIMUM] = node.max if node.end
184
+ end
164
185
  props.merge(opts)
165
186
  end
166
187
 
167
- on(:metadata) do |type, props|
168
- # TODO: here we should filter out the metadata that is not relevant for JSON Schema
169
- props.merge(stringify_keys(type.metadata))
188
+ on(::Hash) do |_node, props|
189
+ props.merge(TYPE => 'object')
170
190
  end
171
191
 
172
- on(:hash_map) do |type, props|
192
+ on(::Array) do |_node, props|
193
+ props.merge(TYPE => 'array')
194
+ end
195
+
196
+ on(:metadata) do |node, props|
197
+ #  TODO: here we should filter out the metadata that is not relevant for JSON Schema
198
+ props.merge(stringify_keys(node.metadata))
199
+ end
200
+
201
+ on(:hash_map) do |node, _props|
173
202
  {
174
203
  TYPE => 'object',
175
204
  'patternProperties' => {
176
- '.*' => visit(type.value_type)
205
+ '.*' => visit(node.value_type)
177
206
  }
178
207
  }
179
208
  end
180
209
 
181
- on(:build) do |type, props|
182
- visit(type.type, props)
210
+ on(:filtered_hash_map) do |node, _props|
211
+ {
212
+ TYPE => 'object',
213
+ 'patternProperties' => {
214
+ '.*' => visit(node.value_type)
215
+ }
216
+ }
217
+ end
218
+
219
+ on(:build) do |node, props|
220
+ visit(node.type, props)
221
+ end
222
+
223
+ on(:array) do |node, _props|
224
+ items = visit(node.element_type)
225
+ { TYPE => 'array', ITEMS => items }
183
226
  end
184
227
 
185
- on(:array) do |type, props|
186
- items = visit(type.element_type)
228
+ on(:stream) do |node, _props|
229
+ items = visit(node.element_type)
187
230
  { TYPE => 'array', ITEMS => items }
188
231
  end
189
232
 
190
- on(:tuple) do |type, props|
191
- items = type.types.map { |t| visit(t) }
233
+ on(:tuple) do |node, _props|
234
+ items = node.types.map { |t| visit(t) }
192
235
  { TYPE => 'array', 'prefixItems' => items }
193
236
  end
194
237
 
195
- on(:tagged_hash) do |type, props|
238
+ on(:tagged_hash) do |node, _props|
196
239
  required = Set.new
197
240
  result = {
198
241
  TYPE => 'object',
199
242
  PROPERTIES => {}
200
243
  }
201
244
 
202
- key = type.key.to_s
203
- children = type.types.map { |c| visit(c) }
245
+ key = node.key.to_s
246
+ children = node.types.map { |c| visit(c) }
204
247
  key_enum = children.map { |c| c[PROPERTIES][key][CONST] }
205
248
  key_type = children.map { |c| c[PROPERTIES][key][TYPE] }
206
249
  required << key
@@ -13,17 +13,20 @@ module Plumb
13
13
 
14
14
  @matcher = matcher
15
15
  @error = error.nil? ? build_error(matcher) : (error % matcher)
16
- end
17
-
18
- def inspect
19
- %(#{name}[#{@matcher.inspect}])
16
+ freeze
20
17
  end
21
18
 
22
19
  def call(result)
23
20
  @matcher === result.value ? result : result.invalid(errors: @error)
24
21
  end
25
22
 
26
- private def build_error(matcher)
23
+ private
24
+
25
+ def _inspect
26
+ @matcher.inspect
27
+ end
28
+
29
+ def build_error(matcher)
27
30
  case matcher
28
31
  when Class # A class primitive, ex. String, Integer, etc.
29
32
  "Must be a #{matcher}"
@@ -8,8 +8,11 @@ module Plumb
8
8
 
9
9
  def initialize(metadata)
10
10
  @metadata = metadata
11
+ freeze
11
12
  end
12
13
 
13
14
  def call(result) = result
15
+
16
+ private def _inspect = "Metadata[#{@metadata.inspect}]"
14
17
  end
15
18
  end
@@ -6,60 +6,61 @@ module Plumb
6
6
  class MetadataVisitor
7
7
  include VisitorHandlers
8
8
 
9
- def self.call(type)
10
- new.visit(type)
9
+ def self.call(node)
10
+ new.visit(node)
11
11
  end
12
12
 
13
- def on_missing_handler(type, props, method_name)
14
- return props.merge(type: type) if type.class == Class
13
+ def on_missing_handler(node, props, method_name)
14
+ return props.merge(type: node) if node.instance_of?(Class)
15
15
 
16
- puts "Missing handler for #{type.inspect} with props #{props.inspect} and method_name :#{method_name}"
16
+ puts "Missing handler for #{node.inspect} with props #{node.inspect} and method_name :#{method_name}"
17
17
  props
18
18
  end
19
19
 
20
- on(:undefined) do |type, props|
20
+ on(:undefined) do |_node, props|
21
21
  props
22
22
  end
23
23
 
24
- on(:any) do |type, props|
24
+ on(:any) do |_node, props|
25
25
  props
26
26
  end
27
27
 
28
- on(:pipeline) do |type, props|
29
- visit(type.type, props)
28
+ on(:pipeline) do |node, props|
29
+ visit(node.type, props)
30
30
  end
31
31
 
32
- on(:step) do |type, props|
33
- props.merge(type._metadata)
32
+ on(:step) do |node, props|
33
+ props.merge(node._metadata)
34
34
  end
35
35
 
36
- on(::Regexp) do |type, props|
37
- props.merge(pattern: type)
36
+ on(::Regexp) do |node, props|
37
+ props.merge(pattern: node, type: props[:type] || String)
38
38
  end
39
39
 
40
- on(::Range) do |type, props|
41
- props.merge(match: type)
40
+ on(::Range) do |node, props|
41
+ type = props[:type] || (node.begin || node.end).class
42
+ props.merge(match: node, type:)
42
43
  end
43
44
 
44
- on(:match) do |type, props|
45
- visit(type.matcher, props)
45
+ on(:match) do |node, props|
46
+ visit(node.matcher, props)
46
47
  end
47
48
 
48
- on(:hash) do |type, props|
49
+ on(:hash) do |_node, props|
49
50
  props.merge(type: Hash)
50
51
  end
51
52
 
52
- on(:and) do |type, props|
53
- left = visit(type.left)
54
- right = visit(type.right)
53
+ on(:and) do |node, props|
54
+ left = visit(node.left)
55
+ right = visit(node.right)
55
56
  type = right[:type] || left[:type]
56
57
  props = props.merge(left).merge(right)
57
58
  props = props.merge(type:) if type
58
59
  props
59
60
  end
60
61
 
61
- on(:or) do |type, props|
62
- child_metas = [visit(type.left), visit(type.right)]
62
+ on(:or) do |node, props|
63
+ child_metas = [visit(node.left), visit(node.right)]
63
64
  types = child_metas.map { |child| child[:type] }.flatten.compact
64
65
  types = types.first if types.size == 1
65
66
  child_metas.reduce(props) do |acc, child|
@@ -67,49 +68,53 @@ module Plumb
67
68
  end.merge(type: types)
68
69
  end
69
70
 
70
- on(:value) do |type, props|
71
- visit(type.value, props)
71
+ on(:value) do |node, props|
72
+ visit(node.value, props)
72
73
  end
73
74
 
74
- on(:transform) do |type, props|
75
- props.merge(type: type.target_type)
75
+ on(:transform) do |node, props|
76
+ props.merge(type: node.target_type)
76
77
  end
77
78
 
78
- on(:static) do |type, props|
79
- props.merge(static: type.value)
79
+ on(:static) do |node, props|
80
+ props.merge(static: node.value)
80
81
  end
81
82
 
82
- on(:rules) do |type, props|
83
- type.rules.reduce(props) do |acc, rule|
83
+ on(:rules) do |node, props|
84
+ node.rules.reduce(props) do |acc, rule|
84
85
  acc.merge(rule.name => rule.arg_value)
85
86
  end
86
87
  end
87
88
 
88
- on(:boolean) do |type, props|
89
+ on(:boolean) do |_node, props|
89
90
  props.merge(type: 'boolean')
90
91
  end
91
92
 
92
- on(:metadata) do |type, props|
93
- props.merge(type.metadata)
93
+ on(:metadata) do |node, props|
94
+ props.merge(node.metadata)
94
95
  end
95
96
 
96
- on(:hash_map) do |type, props|
97
+ on(:hash_map) do |_node, props|
97
98
  props.merge(type: Hash)
98
99
  end
99
100
 
100
- on(:build) do |type, props|
101
- visit(type.type, props)
101
+ on(:build) do |node, props|
102
+ visit(node.type, props)
102
103
  end
103
104
 
104
- on(:array) do |type, props|
105
+ on(:array) do |_node, props|
105
106
  props.merge(type: Array)
106
107
  end
107
108
 
108
- on(:tuple) do |type, props|
109
+ on(:stream) do |_node, props|
110
+ props.merge(type: Enumerator)
111
+ end
112
+
113
+ on(:tuple) do |_node, props|
109
114
  props.merge(type: Array)
110
115
  end
111
116
 
112
- on(:tagged_hash) do |type, props|
117
+ on(:tagged_hash) do |_node, props|
113
118
  props.merge(type: Hash)
114
119
  end
115
120
  end
data/lib/plumb/rules.rb CHANGED
@@ -8,12 +8,12 @@ module Plumb
8
8
  UndefinedRuleError = Class.new(KeyError)
9
9
 
10
10
  class Registry
11
- RuleDef = Data.define(:name, :error_tpl, :callable, :metadata_key, :expects) do
11
+ RuleDef = Data.define(:name, :error_tpl, :callable, :expects) do
12
12
  def supports?(type)
13
13
  types = [type].flatten # may be an array of types for OR logic
14
14
  case expects
15
15
  when Symbol
16
- types.all? { |type| type.public_instance_methods.include?(expects) }
16
+ types.all? { |type| type && type.public_instance_methods.include?(expects) }
17
17
  when Class then types.all? { |type| type <= expects }
18
18
  when Object then true
19
19
  else raise "Unexpected expects: #{expects}"
@@ -29,7 +29,6 @@ module Plumb
29
29
 
30
30
  def node_name = :"rule_#{rule_def.name}"
31
31
  def name = rule_def.name
32
- def metadata_key = rule_def.metadata_key
33
32
 
34
33
  def error_for(result)
35
34
  return nil if rule_def.callable.call(result, arg_value)
@@ -42,10 +41,10 @@ module Plumb
42
41
  @definitions = Hash.new { |h, k| h[k] = Set.new }
43
42
  end
44
43
 
45
- def define(name, error_tpl, callable = nil, metadata_key: name, expects: Object, &block)
44
+ def define(name, error_tpl, callable = nil, expects: Object, &block)
46
45
  name = name.to_sym
47
46
  callable ||= block
48
- @definitions[name] << RuleDef.new(name:, error_tpl:, callable:, metadata_key:, expects:)
47
+ @definitions[name] << RuleDef.new(name:, error_tpl:, callable:, expects:)
49
48
  end
50
49
 
51
50
  # Ex. size: 3, match: /foo/
@@ -54,10 +53,10 @@ module Plumb
54
53
  rule_defs = @definitions.fetch(name.to_sym) { raise UndefinedRuleError, "no rule defined with :#{name}" }
55
54
  rule_def = rule_defs.find { |rd| rd.supports?(for_type) }
56
55
  unless rule_def
57
- raise UnsupportedRuleError, "No :#{name} rule for type #{for_type}" unless for_type.is_a?(Array)
56
+ raise UnsupportedRuleError, "No :#{name} rule for type #{for_type.inspect}" unless for_type.is_a?(Array)
58
57
 
59
58
  raise UnsupportedRuleError,
60
- "Can't apply :#{name} rule for types #{for_type}. All types must support the same rule implementation"
59
+ "Can't apply :#{name} rule for types #{for_type}. All types must support the same rule implementation"
61
60
 
62
61
  end
63
62