toy_adt 0.1.0 → 0.1.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1f45563ee2b678fd2d1ad8dd632d05fee56b16dbc46b5b4952082be20f48dea4
4
- data.tar.gz: 56ea2b517926e966389b211ccc30d2d9c2113d916996b514146519c2646eef52
3
+ metadata.gz: e3d42d697ccf26c13802f9052755b8a2472ed96ed4fc51b1de0559fd77fc3c6f
4
+ data.tar.gz: 3fff27aff7e04fe74fb86ab350f6f768126f9c39c31f55f4c9146b159c276760
5
5
  SHA512:
6
- metadata.gz: 90beb64a03734f4c5405a15165b939298b33cac5bf0acb1b3422083df919aa9d4ff10325c36d983f4254ce770dfbeae8792a7ea247ad6d92e8a779ed449f3f64
7
- data.tar.gz: c0a93e38be8bdfa02b24599ed4a49cd6aafda170e1488a82426497d2a7d07e0ee1540a1167e7a34bd2bdd3c15a986d47d4ce38d034c143dc129e778a6cf2ab1d
6
+ metadata.gz: 40947f6a8f30381e893adaf8a6675832c53bc9a968b7d53a5c013bd815239ed12615cec0840007d37727caa08be9d8f3d92556c8411b7571d0c5703cf7b45a0a
7
+ data.tar.gz: 2c251dbe86f76e7f837969372c7078c869d9a008747206f8124d177facba235b85d073550ccd38313d9c36a8517c4b3d857d5484d891f7c904792029adba3636
@@ -16,7 +16,7 @@ module ToyAdt
16
16
  ).returns(T::Hash[Symbol, T.untyped])
17
17
  }
18
18
  def deconstruct_keys(keys)
19
- return properties.to_h { |k| [k, send(k)] } if keys.empty? || keys.nil?
19
+ return properties.to_h { |k| [k, send(k)] } if keys.nil? || keys.empty?
20
20
 
21
21
  (properties & keys).to_h { |k| [k, send(k)] }
22
22
  end
@@ -0,0 +1,133 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ require "set"
5
+
6
+ module ToyAdt
7
+ class Matcher
8
+ extend T::Sig
9
+
10
+ POSITIONAL_ARGS = Set[:req, :opt, :rest]
11
+ KEYWORD_ARGS = Set[:keyreq, :key, :keyrest]
12
+ BLOCK_ARGS = Set[:block]
13
+
14
+ def initialize(from:, &fn)
15
+ @from = from
16
+ @fields = from::FIELDS
17
+ @sub_classes = @fields.to_h { [_1, from.const_get(_1.capitalize)] }
18
+
19
+ create_branch_methods
20
+
21
+ instance_eval(&fn) if block_given?
22
+ end
23
+
24
+ def else(&fn)
25
+ @else_fn = fn
26
+ end
27
+
28
+ def call(input)
29
+ # Find the subclass the input matches to
30
+ branch_name, _ = @sub_classes.find do |_, klass|
31
+ matches_type?(klass, input)
32
+ end
33
+
34
+ # If there are none either hit the else or fail hard
35
+ if branch_name.nil?
36
+ return @else_fn.call(input) if @else_fn
37
+ T.absurd(input)
38
+ end
39
+
40
+ # All values to compare against conditions
41
+ values = input.deconstruct_keys(nil)
42
+
43
+ # Try to find the first matching branch function via
44
+ # conditions:
45
+ #
46
+ # value.match do |m|
47
+ # m.some(value: 0..5) {}
48
+ # m.some {}
49
+ # m.else {}
50
+ # end
51
+ #
52
+ # In this case a "Some" type with a value matching will hit
53
+ # first. Granted less specific branches going first means
54
+ # those get hit first, so not recommended.
55
+ _, branch_fn = @branches.find do |branch_condition, _|
56
+ name, conditions = branch_condition
57
+
58
+ # First thing is the key is a tuple of subclass / branch
59
+ # name, the second part is the condition if there are any.
60
+ branch_name == name && conditions.all? { |k, condition|
61
+ matches_type?(condition, values[k])
62
+ }
63
+ end
64
+
65
+ return branch_fn.call(input) if branch_fn
66
+ return @else_fn.call(input) if @else_fn
67
+
68
+ T.absurd(input)
69
+ end
70
+
71
+ alias_method :===, :call
72
+
73
+ def to_proc
74
+ -> input { call(input) }
75
+ end
76
+
77
+ # Making this respond to both `===` and the sorbet type
78
+ # validation variants.
79
+ private def matches_type?(type, value)
80
+ type === value || sorbet_is_a?(type, value)
81
+ end
82
+
83
+ private def sorbet_is_a?(type, value)
84
+ T::Types::Base === type && type.valid?(value)
85
+ end
86
+
87
+ private def create_branch_methods
88
+ @branches = {}
89
+
90
+ @fields.each do |field|
91
+ define_singleton_method(field) do |**conditions, &fn|
92
+ param_type = get_param_type(fn)
93
+ param_names = fn.parameters.map(&:last)
94
+
95
+ # Trying to make it play nicely with pattern matching
96
+ # semantics, hitting both positional and keywords but
97
+ # also no-args.
98
+ @branches[[field, conditions]] = -> input do
99
+ case param_type
100
+ when :keyword
101
+ fn.call(**input.deconstruct_keys(param_names))
102
+ when :positional
103
+ fn.call(*input.deconstruct) # Should probably disable this
104
+ when :none
105
+ fn.call
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+
112
+ private def get_param_type(fn)
113
+ param_types = fn.parameters.map(&:first)
114
+
115
+ has_positional = param_types.any?(POSITIONAL_ARGS)
116
+ has_keywords = param_types.any?(KEYWORD_ARGS)
117
+ has_block = param_types.any?(BLOCK_ARGS)
118
+
119
+ if has_block
120
+ raise ArgumentError, "Cannot use function arguments"
121
+ end
122
+
123
+ if has_positional && has_keywords
124
+ raise ArgumentError, "Cannot have both positional and keyword arguments"
125
+ end
126
+
127
+ return :keyword if has_keywords
128
+ return :positional if has_positional
129
+
130
+ :none
131
+ end
132
+ end
133
+ end
@@ -1,5 +1,6 @@
1
1
  # typed: false
2
2
  require "sorbet-runtime"
3
+ require "set"
3
4
 
4
5
  module ToyAdt
5
6
  module PublicApi
@@ -34,7 +35,12 @@ module ToyAdt
34
35
  extend T::Helpers
35
36
  sealed!
36
37
 
38
+ const_set(:FIELDS, types.keys)
39
+
37
40
  module_eval(&fn) if block_given?
41
+
42
+ def self.match(&fn) = Matcher.new(from: self, &fn)
43
+ def self.match_call(input, &fn) = match(&fn).call(input)
38
44
  end
39
45
 
40
46
  types.each do |type, config|
@@ -42,12 +48,19 @@ module ToyAdt
42
48
  include container_module
43
49
  include DeconstructableSorbetStruct
44
50
 
51
+ const_set(:CONTAINER, container_module)
52
+
45
53
  config.each do |field, type|
46
54
  const field, type
47
55
  end
56
+
57
+ def match(&fn) = self.class::CONTAINER.match(&fn).call(self)
48
58
  end
49
59
 
50
60
  container_module.const_set(type.capitalize, klass)
61
+ container_module.define_method(type) do
62
+ klass
63
+ end
51
64
  end
52
65
 
53
66
  container_module
@@ -1,6 +1,6 @@
1
- # typed: strict
1
+ # typed: false
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module ToyAdt
5
- VERSION = "0.1.0"
5
+ VERSION = "0.1.1"
6
6
  end
data/lib/toy_adt.rb CHANGED
@@ -3,6 +3,7 @@
3
3
 
4
4
  require_relative "toy_adt/version"
5
5
  require_relative "toy_adt/deconstructable_sorbet_struct"
6
+ require_relative "toy_adt/matcher"
6
7
  require_relative "toy_adt/public_api"
7
8
 
8
9
  module ToyAdt
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: toy_adt
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brandon Weaver
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-05-26 00:00:00.000000000 Z
11
+ date: 2022-05-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -84,6 +84,7 @@ files:
84
84
  - Rakefile
85
85
  - lib/toy_adt.rb
86
86
  - lib/toy_adt/deconstructable_sorbet_struct.rb
87
+ - lib/toy_adt/matcher.rb
87
88
  - lib/toy_adt/public_api.rb
88
89
  - lib/toy_adt/version.rb
89
90
  - sorbet/config