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.
@@ -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