File: predicate_inferrer.rb

package info (click to toggle)
ruby-dry-types 1.2.2-2
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, bullseye, forky, sid, trixie
  • size: 504 kB
  • sloc: ruby: 3,059; makefile: 4
file content (197 lines) | stat: -rw-r--r-- 4,272 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# frozen_string_literal: true

require 'dry/core/cache'
require 'dry/types/predicate_registry'

module Dry
  module Types
    # PredicateInferrer returns the list of predicates used by a type.
    #
    # @api public
    class PredicateInferrer
      extend Core::Cache

      TYPE_TO_PREDICATE = {
        DateTime => :date_time?,
        FalseClass => :false?,
        Integer => :int?,
        NilClass => :nil?,
        String => :str?,
        TrueClass => :true?,
        BigDecimal => :decimal?
      }.freeze

      REDUCED_TYPES = {
        [[[:true?], [:false?]]] => %i[bool?]
      }.freeze

      HASH = %i[hash?].freeze

      ARRAY = %i[array?].freeze

      NIL = %i[nil?].freeze

      # Compiler reduces type AST into a list of predicates
      #
      # @api private
      class Compiler
        # @return [PredicateRegistry]
        # @api private
        attr_reader :registry

        # @api private
        def initialize(registry)
          @registry = registry
        end

        # @api private
        def infer_predicate(type)
          [TYPE_TO_PREDICATE.fetch(type) { :"#{type.name.split('::').last.downcase}?" }]
        end

        # @api private
        def visit(node)
          meth, rest = node
          public_send(:"visit_#{meth}", rest)
        end

        # @api private
        def visit_nominal(node)
          type = node[0]
          predicate = infer_predicate(type)

          if registry.key?(predicate[0])
            predicate
          else
            [type?: type]
          end
        end

        # @api private
        def visit_hash(_)
          HASH
        end

        # @api private
        def visit_array(_)
          ARRAY
        end

        # @api private
        def visit_lax(node)
          visit(node)
        end

        # @api private
        def visit_constructor(node)
          other, * = node
          visit(other)
        end

        # @api private
        def visit_enum(node)
          other, * = node
          visit(other)
        end

        # @api private
        def visit_sum(node)
          left_node, right_node, = node
          left = visit(left_node)
          right = visit(right_node)

          if left.eql?(NIL)
            right
          else
            [[left, right]]
          end
        end

        # @api private
        def visit_constrained(node)
          other, rules = node
          predicates = visit(rules)

          if predicates.empty?
            visit(other)
          else
            [*visit(other), *merge_predicates(predicates)]
          end
        end

        # @api private
        def visit_any(_)
          EMPTY_ARRAY
        end

        # @api private
        def visit_and(node)
          left, right = node
          visit(left) + visit(right)
        end

        # @api private
        def visit_predicate(node)
          pred, args = node

          if pred.equal?(:type?)
            EMPTY_ARRAY
          elsif registry.key?(pred)
            *curried, _ = args
            values = curried.map { |_, v| v }

            if values.empty?
              [pred]
            else
              [pred => values[0]]
            end
          else
            EMPTY_ARRAY
          end
        end

        private

        # @api private
        def merge_predicates(nodes)
          preds, merged = nodes.each_with_object([[], {}]) do |predicate, (ps, h)|
            if predicate.is_a?(::Hash)
              h.update(predicate)
            else
              ps << predicate
            end
          end

          merged.empty? ? preds : [*preds, merged]
        end
      end

      # @return [Compiler]
      # @api private
      attr_reader :compiler

      # @api private
      def initialize(registry = PredicateRegistry.new)
        @compiler = Compiler.new(registry)
      end

      # Infer predicate identifier from the provided type
      #
      # @param [Type] type
      # @return [Symbol]
      #
      # @api private
      def [](type)
        self.class.fetch_or_store(type) do
          predicates = compiler.visit(type.to_ast)

          if predicates.is_a?(::Hash)
            predicates
          else
            REDUCED_TYPES[predicates] || predicates
          end
        end
      end
    end
  end
end