plumb 0.0.2 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
data/lib/plumb/schema.rb CHANGED
@@ -5,13 +5,13 @@ require 'plumb/json_schema_visitor'
5
5
 
6
6
  module Plumb
7
7
  class Schema
8
- include Steppable
8
+ include Composable
9
9
 
10
10
  def self.wrap(sch = nil, &block)
11
11
  raise ArgumentError, 'expected a block or a schema' if sch.nil? && !block_given?
12
12
 
13
13
  if sch
14
- raise ArgumentError, 'expected a Steppable' unless sch.is_a?(Steppable)
14
+ raise ArgumentError, 'expected a Composable' unless sch.is_a?(Composable)
15
15
 
16
16
  return sch
17
17
  end
@@ -49,8 +49,8 @@ module Plumb
49
49
  self
50
50
  end
51
51
 
52
- def json_schema
53
- JSONSchemaVisitor.call(_hash).to_h
52
+ def to_json_schema
53
+ _hash.to_json_schema(root: true)
54
54
  end
55
55
 
56
56
  def call(result)
@@ -120,7 +120,7 @@ module Plumb
120
120
  block_given? ? ArrayClass.new(element_type: Schema.new(&block)) : type
121
121
  when nil
122
122
  block_given? ? Schema.new(&block) : Types::Any
123
- when Steppable
123
+ when Composable
124
124
  type
125
125
  when Class
126
126
  if type == Array && block_given?
@@ -140,13 +140,14 @@ module Plumb
140
140
  self
141
141
  end
142
142
 
143
- def meta(md = nil)
144
- @_type = @_type.meta(md) if md
145
- self
143
+ def metadata(data = Undefined)
144
+ if data == Undefined
145
+ @_type.metadata
146
+ else
147
+ @_type = @_type.metadata(data)
148
+ end
146
149
  end
147
150
 
148
- def metadata = @_type.metadata
149
-
150
151
  def options(opts)
151
152
  @_type = @_type.options(opts)
152
153
  self
@@ -172,8 +173,8 @@ module Plumb
172
173
  self
173
174
  end
174
175
 
175
- def rule(...)
176
- @_type = @_type.rule(...)
176
+ def policy(...)
177
+ @_type = @_type.policy(...)
177
178
  self
178
179
  end
179
180
 
@@ -1,17 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'plumb/steppable'
3
+ require 'plumb/composable'
4
4
 
5
5
  module Plumb
6
6
  class StaticClass
7
- include Steppable
7
+ include Composable
8
8
 
9
- attr_reader :value
9
+ attr_reader :children
10
10
 
11
11
  def initialize(value = Undefined)
12
12
  raise ArgumentError, 'value must be frozen' unless value.frozen?
13
13
 
14
14
  @value = value
15
+ @children = [value].freeze
15
16
  freeze
16
17
  end
17
18
 
data/lib/plumb/step.rb CHANGED
@@ -1,16 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'plumb/steppable'
3
+ require 'plumb/composable'
4
4
 
5
5
  module Plumb
6
6
  class Step
7
- include Steppable
7
+ include Composable
8
8
 
9
- attr_reader :_metadata
9
+ attr_reader :_metadata, :children
10
10
 
11
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
+ @children = [@callable].freeze
14
15
  @inspect = inspect || @callable.inspect
15
16
  freeze
16
17
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'thread'
4
- require 'plumb/steppable'
4
+ require 'plumb/composable'
5
5
 
6
6
  module Plumb
7
7
  # A stream that validates each element.
@@ -15,18 +15,19 @@ module Plumb
15
15
  # result.value # => ['name', 10]
16
16
  # end
17
17
  class StreamClass
18
- include Steppable
18
+ include Composable
19
19
 
20
- attr_reader :element_type
20
+ attr_reader :children
21
21
 
22
- # @option element_type [Steppable] the type of the elements in the stream
22
+ # @option element_type [Composable] the type of the elements in the stream
23
23
  def initialize(element_type: Types::Any)
24
- @element_type = Steppable.wrap(element_type)
24
+ @element_type = Composable.wrap(element_type)
25
+ @children = [@element_type].freeze
25
26
  freeze
26
27
  end
27
28
 
28
29
  # return a new Stream definition.
29
- # @param element_type [Steppable] the type of the elements in the stream
30
+ # @param element_type [Composable] the type of the elements in the stream
30
31
  def [](element_type)
31
32
  self.class.new(element_type:)
32
33
  end
@@ -39,7 +40,7 @@ module Plumb
39
40
 
40
41
  enum = Enumerator.new do |y|
41
42
  result.value.each do |e|
42
- y << element_type.resolve(e)
43
+ y << @element_type.resolve(e)
43
44
  end
44
45
  end
45
46
 
@@ -1,29 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'plumb/steppable'
3
+ require 'plumb/composable'
4
4
 
5
5
  module Plumb
6
6
  class TaggedHash
7
- include Steppable
7
+ include Composable
8
8
 
9
- attr_reader :key, :types
9
+ attr_reader :key, :children
10
10
 
11
- def initialize(hash_type, key, types)
11
+ def initialize(hash_type, key, children)
12
12
  @hash_type = hash_type
13
13
  @key = Key.wrap(key)
14
- @types = types
14
+ @children = children
15
15
 
16
- raise ArgumentError, 'all types must be HashClass' if @types.size.zero? || @types.any? do |t|
16
+ raise ArgumentError, 'all types must be HashClass' if @children.size.zero? || @children.any? do |t|
17
17
  !t.is_a?(HashClass)
18
18
  end
19
- raise ArgumentError, "all types must define key #{@key}" unless @types.all? { |t| !!t.at_key(@key) }
19
+ raise ArgumentError, "all types must define key #{@key}" unless @children.all? { |t| !!t.at_key(@key) }
20
20
 
21
21
  # types are assumed to have literal values for the index field :key
22
- @index = @types.each.with_object({}) do |t, memo|
22
+ @index = @children.each.with_object({}) do |t, memo|
23
23
  key_type = t.at_key(@key)
24
24
  raise TypeError, "key type at :#{@key} #{key_type} must be a Match type" unless key_type.is_a?(MatchClass)
25
25
 
26
- memo[key_type.matcher] = t
26
+ memo[key_type.children[0]] = t
27
27
  end
28
28
 
29
29
  freeze
@@ -41,6 +41,6 @@ module Plumb
41
41
 
42
42
  private
43
43
 
44
- def _inspect = "TaggedHash[#{@key.inspect}, #{@types.map(&:inspect).join(', ')}]"
44
+ def _inspect = "TaggedHash[#{@key.inspect}, #{@children.map(&:inspect).join(', ')}]"
45
45
  end
46
46
  end
@@ -1,16 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'plumb/steppable'
3
+ require 'plumb/composable'
4
4
 
5
5
  module Plumb
6
6
  class Transform
7
- include Steppable
7
+ include Composable
8
8
 
9
- attr_reader :target_type
9
+ attr_reader :children
10
10
 
11
11
  def initialize(target_type, callable)
12
12
  @target_type = target_type
13
13
  @callable = callable || Plumb::NOOP
14
+ @children = [target_type].freeze
14
15
  freeze
15
16
  end
16
17
 
@@ -1,15 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'plumb/steppable'
3
+ require 'plumb/composable'
4
4
 
5
5
  module Plumb
6
6
  class TupleClass
7
- include Steppable
7
+ include Composable
8
8
 
9
- attr_reader :types
9
+ attr_reader :children
10
10
 
11
- def initialize(*types)
12
- @types = types.map { |t| Steppable.wrap(t) }
11
+ def initialize(*children)
12
+ @children = children.map { |t| Composable.wrap(t) }.freeze
13
13
  freeze
14
14
  end
15
15
 
@@ -21,10 +21,10 @@ module Plumb
21
21
 
22
22
  def call(result)
23
23
  return result.invalid(errors: 'must be an Array') unless result.value.is_a?(::Array)
24
- return result.invalid(errors: 'must have the same size') unless result.value.size == @types.size
24
+ return result.invalid(errors: 'must have the same size') unless result.value.size == @children.size
25
25
 
26
26
  errors = {}
27
- values = @types.map.with_index do |type, idx|
27
+ values = @children.map.with_index do |type, idx|
28
28
  val = result.value[idx]
29
29
  r = type.resolve(val)
30
30
  errors[idx] = ["expected #{type.inspect}, got #{val.inspect}", r.errors].flatten unless r.valid?
@@ -39,7 +39,7 @@ module Plumb
39
39
  private
40
40
 
41
41
  def _inspect
42
- "Tuple[#{@types.map(&:inspect).join(', ')}]"
42
+ "Tuple[#{@children.map(&:inspect).join(', ')}]"
43
43
  end
44
44
  end
45
45
  end
@@ -7,7 +7,7 @@ module Plumb
7
7
  case obj
8
8
  when Module
9
9
  obj.extend TypeRegistry
10
- when Steppable
10
+ when Composable
11
11
  anc = [name, const_name].join('::')
12
12
  obj.freeze.name.set(anc)
13
13
  end
@@ -17,16 +17,19 @@ module Plumb
17
17
  host.extend TypeRegistry
18
18
  constants(false).each do |const_name|
19
19
  const = const_get(const_name)
20
+
20
21
  anc = [host.name, const_name].join('::')
21
22
  case const
22
23
  when Module
24
+ next if const.is_a?(Class)
25
+
23
26
  child_mod = Module.new
24
27
  child_mod.define_singleton_method(:name) do
25
28
  anc
26
29
  end
27
30
  child_mod.send(:include, const)
28
31
  host.const_set(const_name, child_mod)
29
- when Steppable
32
+ when Composable
30
33
  type = const.dup
31
34
  type.freeze.name.set(anc)
32
35
  host.const_set(const_name, type)
data/lib/plumb/types.rb CHANGED
@@ -3,25 +3,127 @@
3
3
  require 'bigdecimal'
4
4
 
5
5
  module Plumb
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) }
6
+ # Define core policies
7
+ # Allowed options for an array type.
8
+ # It validates that each element is in the options array.
9
+ # Usage:
10
+ # type = Types::Array.options(['a', 'b'])
11
+ policy :options, helper: true, for_type: ::Array do |type, opts|
12
+ type.check("must be included in #{opts.inspect}") do |v|
13
+ v.all? { |val| opts.include?(val) }
14
+ end
15
+ end
16
+
17
+ # Generic options policy for all other types.
18
+ # Usage:
19
+ # type = Types::String.options(['a', 'b'])
20
+ policy :options do |type, opts|
21
+ type.check("must be included in #{opts.inspect}") do |v|
22
+ opts.include?(v)
23
+ end
24
+ end
25
+
26
+ # Validate that array elements are NOT in the options array.
27
+ # Usage:
28
+ # type = Types::Array.policy(excluded_from: ['a', 'b'])
29
+ policy :excluded_from, for_type: ::Array do |type, opts|
30
+ type.check("must not be included in #{opts.inspect}") do |v|
31
+ v.none? { |val| opts.include?(val) }
32
+ end
33
+ end
34
+
35
+ # Usage:
36
+ # type = Types::String.policy(excluded_from: ['a', 'b'])
37
+ policy :excluded_from do |type, opts|
38
+ type.check("must not be included in #{opts.inspect}") do |v|
39
+ !opts.include?(v)
40
+ end
41
+ end
42
+
43
+ # Validate #size against a number or any object that responds to #===.
44
+ # This works with any type that repsonds to #size.
45
+ # Usage:
46
+ # type = Types::String.policy(size: 10)
47
+ # type = Types::Integer.policy(size: 1..10)
48
+ # type = Types::Array.policy(size: 1..)
49
+ # type = Types::Any[Set].policy(size: 1..)
50
+ policy :size, for_type: :size do |type, size|
51
+ type.check("must be of size #{size}") { |v| size === v.size }
8
52
  end
9
- Rules.define :included_in, 'must be included in %<value>s' do |result, opts|
10
- opts.include? result.value
53
+
54
+ # Validate that an object is not #empty? nor #nil?
55
+ # Usage:
56
+ # Types::String.present
57
+ # Types::Array.present
58
+ policy :present, helper: true do |type, *_args|
59
+ type.check('must be present') do |v|
60
+ if v.respond_to?(:empty?)
61
+ !v.empty?
62
+ else
63
+ !v.nil?
64
+ end
65
+ end
11
66
  end
12
- Rules.define :excluded_from, 'elements must not be included in %<value>s', expects: ::Array do |result, value|
13
- result.value.all? { |v| !value.include?(v) }
67
+
68
+ # Allow nil values for a type.
69
+ # Usage:
70
+ # nullable_int = Types::Integer.nullable
71
+ # nullable_int.parse(nil) # => nil
72
+ # nullable_int.parse(10) # => 10
73
+ # nullable_int.parse('nope') # => error: not an Integer
74
+ policy :nullable, helper: true do |type, *_args|
75
+ Types::Nil | type
14
76
  end
15
- Rules.define :excluded_from, 'must not be included in %<value>s' do |result, value|
16
- !value.include?(result.value)
77
+
78
+ # Validate that a value responds to a method
79
+ # Usage:
80
+ # type = Types::Any.policy(respond_to: :upcase)
81
+ # type = Types::Any.policy(respond_to: [:upcase, :strip])
82
+ policy :respond_to do |type, method_names|
83
+ type.check("must respond to #{method_names.inspect}") do |value|
84
+ Array(method_names).all? { |m| value.respond_to?(m) }
85
+ end
17
86
  end
18
- Rules.define :respond_to, 'must respond to %<value>s' do |result, value|
19
- Array(value).all? { |m| result.value.respond_to?(m) }
87
+
88
+ # Return a default value if the input value is Undefined (ie key not present in a Hash).
89
+ # Usage:
90
+ # type = Types::String.default('default')
91
+ # type.parse(Undefined) # => 'default'
92
+ # type.parse('yes') # => 'yes'
93
+ #
94
+ # Works with a block too:
95
+ # date = Type::Any[Date].default { Date.today }
96
+ #
97
+ policy :default, helper: true do |type, value = Undefined, &block|
98
+ val_type = if value == Undefined
99
+ Step.new(->(result) { result.valid(block.call) }, 'default proc')
100
+ else
101
+ Types::Static[value]
102
+ end
103
+
104
+ (Types::Undefined >> val_type) | type
20
105
  end
21
- Rules.define :size, 'must be of size %<value>s', expects: :size do |result, value|
22
- value === result.value.size
106
+
107
+ # Split a string into an array. Default separator is /\s*,\s*/
108
+ # Usage:
109
+ # type = Types::String.split
110
+ # type.parse('a,b,c') # => ['a', 'b', 'c']
111
+ #
112
+ # Custom separator:
113
+ # type = Types::String.split(';')
114
+ module SplitPolicy
115
+ DEFAULT_SEPARATOR = /\s*,\s*/
116
+
117
+ def self.call(type, separator = DEFAULT_SEPARATOR)
118
+ type.invoke(:split, separator) >> Types::Array[String]
119
+ end
120
+
121
+ def self.for_type = ::String
122
+ def self.helper = false
23
123
  end
24
124
 
125
+ policy :split, SplitPolicy
126
+
25
127
  module Types
26
128
  extend TypeRegistry
27
129
 
@@ -43,17 +145,11 @@ module Plumb
43
145
  Tuple = TupleClass.new
44
146
  Hash = HashClass.new
45
147
  Interface = InterfaceClass.new
46
- # TODO: type-speficic concept of blank, via Rules
47
- Blank = (
48
- Undefined \
49
- | Nil \
50
- | String.value(BLANK_STRING) \
51
- | Hash.value(BLANK_HASH) \
52
- | Array.value(BLANK_ARRAY)
53
- )
54
-
55
- Present = Blank.invalid(errors: 'must be present')
56
- Split = String.transform(::String) { |v| v.split(/\s*,\s*/) }
148
+
149
+ class Data
150
+ extend Composable
151
+ include Plumb::Attributes
152
+ end
57
153
 
58
154
  module Lax
59
155
  NUMBER_EXPR = /^\d{1,3}(?:,\d{3})*(?:\.\d+)?$/
@@ -1,15 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'plumb/steppable'
3
+ require 'plumb/composable'
4
4
 
5
5
  module Plumb
6
6
  class ValueClass
7
- include Steppable
7
+ include Composable
8
8
 
9
- attr_reader :value
9
+ attr_reader :children
10
10
 
11
11
  def initialize(value = Undefined)
12
12
  @value = value
13
+ @children = [value].freeze
13
14
  freeze
14
15
  end
15
16
 
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.2'
4
+ VERSION = '0.0.4'
5
5
  end
@@ -23,7 +23,12 @@ module Plumb
23
23
  else
24
24
  :"#{(node.is_a?(::Class) ? node : node.class)}_class"
25
25
  end
26
- method_name = "visit_#{method_name}"
26
+
27
+ visit_name(method_name, node, props)
28
+ end
29
+
30
+ def visit_name(method_name, node, props = BLANK_HASH)
31
+ method_name = :"visit_#{method_name}"
27
32
  if respond_to?(method_name)
28
33
  send(method_name, node, props)
29
34
  else
@@ -34,5 +39,11 @@ module Plumb
34
39
  def on_missing_handler(node, _props, method_name)
35
40
  raise "No handler for #{node.inspect} with :#{method_name}"
36
41
  end
42
+
43
+ def visit_children(node, props = BLANK_HASH)
44
+ node.children.reduce(props) do |acc, child|
45
+ visit(child, acc)
46
+ end
47
+ end
37
48
  end
38
49
  end
data/lib/plumb.rb CHANGED
@@ -1,16 +1,71 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'plumb/policies'
4
+
3
5
  module Plumb
6
+ @policies = Policies.new
7
+
8
+ def self.policies
9
+ @policies
10
+ end
11
+
12
+ # Register a policy with the given name and block.
13
+ # Optionally define a method on the Composable method to call the policy.
14
+ # Example:
15
+ # Plumb.policy(:multiply_by, for_type: Integer, helper: true) do |step, factor, &block|
16
+ # step.transform(Integer) { |number| number * factor }
17
+ # end
18
+ #
19
+ # type = Types::Integer.multiply_by(2)
20
+ # type.parse(10) # => 20
21
+ #
22
+ # @param name [Symbol] the name of the policy
23
+ # @param opts [Hash] options for the policy
24
+ # @yield [Step, Object, &block] the step (type), policy argument, and policy block, if any.
25
+ def self.policy(name, opts = {}, &block)
26
+ name = name.to_sym
27
+ if opts.is_a?(Hash) && block_given?
28
+ for_type = opts[:for_type] || Object
29
+ helper = opts[:helper] || false
30
+ elsif opts.respond_to?(:call) && opts.respond_to?(:for_type) && opts.respond_to?(:helper)
31
+ for_type = opts.for_type
32
+ helper = opts.helper
33
+ block = opts.method(:call)
34
+ else
35
+ raise ArgumentError, 'Expected a block or a hash with :for_type and :helper keys'
36
+ end
37
+
38
+ policies.register(for_type, name, block)
39
+
40
+ return self unless helper
41
+
42
+ if Composable.instance_methods.include?(name)
43
+ raise Policies::MethodAlreadyDefinedError, "Method #{name} is already defined on Composable"
44
+ end
45
+
46
+ Composable.define_method(name) do |arg = Undefined, &bl|
47
+ if arg == Undefined
48
+ policy(name, &bl)
49
+ else
50
+ policy(name, arg, &bl)
51
+ end
52
+ end
53
+
54
+ self
55
+ end
56
+
57
+ def self.decorate(type, &block)
58
+ Decorator.call(type, &block)
59
+ end
4
60
  end
5
61
 
6
62
  require 'plumb/result'
7
63
  require 'plumb/type_registry'
8
- require 'plumb/steppable'
64
+ require 'plumb/composable'
9
65
  require 'plumb/any_class'
10
66
  require 'plumb/step'
11
67
  require 'plumb/and'
12
68
  require 'plumb/pipeline'
13
- require 'plumb/rules'
14
69
  require 'plumb/static_class'
15
70
  require 'plumb/value_class'
16
71
  require 'plumb/match_class'
@@ -21,6 +76,8 @@ require 'plumb/array_class'
21
76
  require 'plumb/stream_class'
22
77
  require 'plumb/hash_class'
23
78
  require 'plumb/interface_class'
79
+ require 'plumb/attributes'
24
80
  require 'plumb/types'
25
81
  require 'plumb/json_schema_visitor'
26
82
  require 'plumb/schema'
83
+ require 'plumb/decorator'
metadata CHANGED
@@ -1,17 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: plumb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ismael Celis
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-07-11 00:00:00.000000000 Z
11
+ date: 2024-08-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: concurrent-ruby
14
+ name: bigdecimal
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - ">="
@@ -25,7 +25,7 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
- name: bigdecimal
28
+ name: concurrent-ruby
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
@@ -53,13 +53,18 @@ files:
53
53
  - examples/command_objects.rb
54
54
  - examples/concurrent_downloads.rb
55
55
  - examples/csv_stream.rb
56
+ - examples/env_config.rb
57
+ - examples/event_registry.rb
56
58
  - examples/programmers.csv
57
59
  - examples/weekdays.rb
58
60
  - lib/plumb.rb
59
61
  - lib/plumb/and.rb
60
62
  - lib/plumb/any_class.rb
61
63
  - lib/plumb/array_class.rb
64
+ - lib/plumb/attributes.rb
62
65
  - lib/plumb/build.rb
66
+ - lib/plumb/composable.rb
67
+ - lib/plumb/decorator.rb
63
68
  - lib/plumb/deferred.rb
64
69
  - lib/plumb/hash_class.rb
65
70
  - lib/plumb/hash_map.rb
@@ -72,12 +77,12 @@ files:
72
77
  - lib/plumb/not.rb
73
78
  - lib/plumb/or.rb
74
79
  - lib/plumb/pipeline.rb
80
+ - lib/plumb/policies.rb
81
+ - lib/plumb/policy.rb
75
82
  - lib/plumb/result.rb
76
- - lib/plumb/rules.rb
77
83
  - lib/plumb/schema.rb
78
84
  - lib/plumb/static_class.rb
79
85
  - lib/plumb/step.rb
80
- - lib/plumb/steppable.rb
81
86
  - lib/plumb/stream_class.rb
82
87
  - lib/plumb/tagged_hash.rb
83
88
  - lib/plumb/transform.rb
@@ -87,7 +92,7 @@ files:
87
92
  - lib/plumb/value_class.rb
88
93
  - lib/plumb/version.rb
89
94
  - lib/plumb/visitor_handlers.rb
90
- homepage: ''
95
+ homepage: https://github.com/ismasan/plumb
91
96
  licenses:
92
97
  - MIT
93
98
  metadata: {}