lowtype 1.5.0 → 1.6.0
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 +4 -4
- data/lib/adapters/adapter_loader.rb +1 -1
- data/lib/adapters/sinatra_adapter.rb +6 -5
- data/lib/cli/cli.rb +2 -2
- data/lib/cli/rbs/type_mapper.rb +13 -0
- data/lib/cli/type_stripper.rb +150 -1
- data/lib/definitions/evaluator.rb +27 -0
- data/lib/definitions/redefiner.rb +4 -1
- data/lib/definitions/type_accessors.rb +56 -33
- data/lib/expressions/type_expression.rb +19 -5
- data/lib/lowtype.rb +8 -7
- data/lib/queries/type_query.rb +10 -3
- data/lib/types/boolean.rb +17 -0
- data/lib/types/complex_types.rb +3 -1
- data/lib/types/enum.rb +47 -0
- data/lib/types/status.rb +11 -0
- data/lib/types/type_definition.rb +19 -0
- data/lib/version.rb +1 -1
- metadata +8 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 82aa9ca5209933447a6c144575a923487ed7a9454b24af79e814e96322657459
|
|
4
|
+
data.tar.gz: 1c589f63849d78d9f8f80acb02288192650fc115f294741d0ef4098d92d132bb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f5dc29f5c922d64de71b94dc7e72bd263bdefe21fe2463f472893edd3ac37bd1ebcc1502038f1b719004b6db86b6c6884d906bca59bba8f6c48ff45e423e9ef7
|
|
7
|
+
data.tar.gz: 51f526a9ee47bae2d5018e9e7f8e3ecc60eddf4ce1f30bb2df439997912713dde8f30f073230d02a6b8ccc63d0a5245dfe0baee43d396c2b225e986f7f2e8c7b
|
|
@@ -11,10 +11,11 @@ module Low
|
|
|
11
11
|
module Adapter
|
|
12
12
|
# We don't use https://sinatrarb.com/extensions.html because we need to type check all Ruby methods (not just Sinatra) at a lower level.
|
|
13
13
|
class SinatraAdapter < AdapterInterface
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
# Route method proxies are captured on class load, so that validating a response doesn't need Lowkey's cache.
|
|
15
|
+
def module(class_proxy:) # rubocop:disable Metrics/AbcSize
|
|
16
|
+
route_proxies = class_proxy.keyed_methods
|
|
17
17
|
|
|
18
|
+
Module.new do
|
|
18
19
|
# Unfortunately overriding invoke() is the best way to validate types for now. Though direct it's also very compute efficient.
|
|
19
20
|
# I originally tried an after filter and it mostly worked but it only had access to Response which isn't the raw return value.
|
|
20
21
|
# I suggest that Sinatra provide a hook that allows us to access the raw return value of a route before it becomes a Response.
|
|
@@ -36,9 +37,9 @@ module Low
|
|
|
36
37
|
nil # avoid double setting the same response tuple twice
|
|
37
38
|
end
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
define_method(:lowtype_validate!) do |value:|
|
|
40
41
|
route = "#{request.request_method} #{request.path}"
|
|
41
|
-
if (method_proxy =
|
|
42
|
+
if (method_proxy = route_proxies[route]) && (proxy = method_proxy.return_proxy)
|
|
42
43
|
proxy.expression.validate!(value:, proxy:)
|
|
43
44
|
end
|
|
44
45
|
end
|
data/lib/cli/cli.rb
CHANGED
|
@@ -22,8 +22,8 @@ module LowType
|
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
line('strip!') do
|
|
25
|
-
s { 'Strips
|
|
26
|
-
e { 'lowtype strip' }
|
|
25
|
+
s { 'Strips types from files in the current directory' }
|
|
26
|
+
e { 'lowtype strip!' }
|
|
27
27
|
x { TypeStripper.export(src: '.', dest: '.') }
|
|
28
28
|
end
|
|
29
29
|
|
data/lib/cli/rbs/type_mapper.rb
CHANGED
|
@@ -23,6 +23,8 @@ module LowType
|
|
|
23
23
|
return COMPLEX_TYPE_MAP[type] if COMPLEX_TYPE_MAP.key?(type)
|
|
24
24
|
|
|
25
25
|
case type
|
|
26
|
+
when ::Low::Types::Enum::Definition then map_enum(type)
|
|
27
|
+
when ::Low::Types::Status::StatusCode then 'Integer'
|
|
26
28
|
when ::Low::TypeExpression then map_expression(type)
|
|
27
29
|
when Array then map_array(type)
|
|
28
30
|
when Hash then map_hash(type)
|
|
@@ -47,6 +49,17 @@ module LowType
|
|
|
47
49
|
"Array[#{map(type.first)}]"
|
|
48
50
|
end
|
|
49
51
|
|
|
52
|
+
def map_enum(type)
|
|
53
|
+
literals = type.values.map do |value|
|
|
54
|
+
case value
|
|
55
|
+
when Symbol, String, Integer, true, false, nil then value.inspect
|
|
56
|
+
else return 'untyped'
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
literals.length > 1 ? "(#{literals.join(' | ')})" : literals.first
|
|
61
|
+
end
|
|
62
|
+
|
|
50
63
|
def map_hash(type)
|
|
51
64
|
return 'Hash[untyped, untyped]' if type.empty?
|
|
52
65
|
|
data/lib/cli/type_stripper.rb
CHANGED
|
@@ -1,9 +1,158 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'lowkey'
|
|
5
|
+
|
|
6
|
+
require_relative '../definitions/type_accessors'
|
|
7
|
+
require_relative '../queries/file_query'
|
|
8
|
+
|
|
3
9
|
module LowType
|
|
10
|
+
# Removes LowType from source files, leaving plain Ruby:
|
|
11
|
+
# method: def greet(name = String | 'World') -> { String } => def greet(name = 'World')
|
|
12
|
+
# variable: greeting = type String | 'Hello' => greeting = 'Hello'
|
|
13
|
+
# accessor: type_accessor name: String => attr_accessor :name
|
|
14
|
+
#
|
|
15
|
+
# Files are loaded into Ruby to evaluate args and decide a type from a default value. Slim syntax may reduce this need.
|
|
16
|
+
# Lowkey then rewrites the method signatures and method calls.
|
|
4
17
|
module TypeStripper
|
|
18
|
+
LOWTYPE_PATTERN = /^\s*include\s+LowType\b/
|
|
19
|
+
REQUIRE_PATTERN = /\A['"][^'"]*(?:lowtype|low_type)['"]\z/
|
|
20
|
+
ACCESSOR_METHODS = { type_reader: 'attr_reader', type_writer: 'attr_writer', type_accessor: 'attr_accessor' }.freeze
|
|
21
|
+
TYPE_METHODS = %i[type low_type].freeze
|
|
22
|
+
|
|
5
23
|
module_function
|
|
6
24
|
|
|
7
|
-
def export(src:, dest:)
|
|
25
|
+
def export(src:, dest:)
|
|
26
|
+
src = File.expand_path(src)
|
|
27
|
+
dest = File.expand_path(dest)
|
|
28
|
+
|
|
29
|
+
# Stripping reads method calls from their Prism nodes, which Lowkey releases once a file loads by default.
|
|
30
|
+
keeping_nodes do
|
|
31
|
+
file_paths(src:, dest:).each do |file_path|
|
|
32
|
+
dest_path = File.file?(src) ? dest : File.join(dest, file_path.delete_prefix(src))
|
|
33
|
+
next copy(file_path:, dest_path:) unless file_path.end_with?('.rb') && File.read(file_path).match?(LOWTYPE_PATTERN)
|
|
34
|
+
|
|
35
|
+
export_file(file_path:, dest_path:)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def keeping_nodes
|
|
41
|
+
release_nodes = ::Lowkey.config.release_nodes
|
|
42
|
+
::Lowkey.config.release_nodes = false
|
|
43
|
+
yield
|
|
44
|
+
ensure
|
|
45
|
+
::Lowkey.config.release_nodes = release_nodes
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def export_file(file_path:, dest_path:)
|
|
49
|
+
stripped = strip(file_path:)
|
|
50
|
+
write(path: dest_path, content: stripped)
|
|
51
|
+
|
|
52
|
+
puts "Stripped #{dest_path}"
|
|
53
|
+
warn "lowtype: Kept LowType in #{dest_path} as some type accessors have default values" if stripped.match?(LOWTYPE_PATTERN)
|
|
54
|
+
rescue ScriptError, StandardError => e
|
|
55
|
+
warn "lowtype: Skipped #{file_path} as it could not be loaded (#{e.class}: #{e.message.lines.first&.strip})"
|
|
56
|
+
copy(file_path:, dest_path:)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Excludes the destination when it's inside the source, so that a previous export isn't exported again.
|
|
60
|
+
def file_paths(src:, dest: nil)
|
|
61
|
+
return [src] if File.file?(src)
|
|
62
|
+
|
|
63
|
+
Dir.glob('**/*', File::FNM_DOTMATCH, base: src).filter_map do |path|
|
|
64
|
+
file_path = File.join(src, path)
|
|
65
|
+
next if path.split('/').intersect?(%w[. .. .git]) || (dest != src && file_path.start_with?("#{dest}/"))
|
|
66
|
+
|
|
67
|
+
file_path if File.file?(file_path)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def strip(file_path:)
|
|
72
|
+
require file_path
|
|
73
|
+
raise ::Low::MissingFileError, "LowType did not load #{file_path}" unless (file_proxy = ::Lowkey[file_path])
|
|
74
|
+
|
|
75
|
+
# Method calls first, as a method call can be on the same line as a signature: "def method(arg = Type) = type(...)".
|
|
76
|
+
rewrite_method_calls(method_calls: file_proxy.definitions.values.flat_map(&:method_calls), file_path:)
|
|
77
|
+
method_proxies(file_proxy:).each { |method_proxy| method_proxy.rewrite_signature if method_proxy.expressions? }
|
|
78
|
+
|
|
79
|
+
file_proxy.lines.join
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Instance and class methods with the same name share a key in "keyed_methods".
|
|
83
|
+
def method_proxies(file_proxy:)
|
|
84
|
+
file_proxy.definitions.values.flat_map { |proxy| [*proxy.keyed_methods.values, *proxy.class_methods.values] }.uniq
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Method calls on the same line are rewritten from last to first so that their offsets stay valid.
|
|
88
|
+
# Type accessors with default values keep LowType included.
|
|
89
|
+
def rewrite_method_calls(method_calls:, file_path:)
|
|
90
|
+
# Filter before sorting, so that only the method calls being rewritten build their source.
|
|
91
|
+
method_calls = method_calls.filter { |method_call| rewritable?(method_call:) }
|
|
92
|
+
|
|
93
|
+
accessors = method_calls.filter { |method_call| accessor?(method_call:) }
|
|
94
|
+
attributes = accessors.filter { |method_call| attribute?(method_call:, file_path:) }
|
|
95
|
+
keep_lowtype = attributes.count < accessors.count
|
|
96
|
+
|
|
97
|
+
method_calls.sort_by { |method_call| -method_call.start_offset }.each do |method_call|
|
|
98
|
+
rewrite_method_call(method_call:, attribute: attributes.include?(method_call), keep_lowtype:)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def rewritable?(method_call:)
|
|
103
|
+
local_type?(method_call:) || accessor?(method_call:) || lowtype?(method_call:)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def rewrite_method_call(method_call:, attribute:, keep_lowtype:)
|
|
107
|
+
if local_type?(method_call:)
|
|
108
|
+
method_call.rewrite(method_call.default)
|
|
109
|
+
elsif attribute
|
|
110
|
+
method_call.rewrite("#{ACCESSOR_METHODS[method_call.name]} #{method_call.keywords.map(&:inspect).join(', ')}")
|
|
111
|
+
elsif !keep_lowtype && lowtype?(method_call:)
|
|
112
|
+
method_call.remove
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# "type String | 'Hello'" becomes "'Hello'".
|
|
117
|
+
def local_type?(method_call:)
|
|
118
|
+
!method_call.receiver? && TYPE_METHODS.include?(method_call.name) && !method_call.default.nil?
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Only symbol keys can become attribute names. A double splat like "**expressions" can't be converted.
|
|
122
|
+
def accessor?(method_call:)
|
|
123
|
+
!method_call.receiver? && ACCESSOR_METHODS.key?(method_call.name)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Type accessors are attributes when they have no default value. Their expressions are evaluated on class load.
|
|
127
|
+
def attribute?(method_call:, file_path:)
|
|
128
|
+
return false unless method_call.arguments.one? && method_call.keywords
|
|
129
|
+
return false unless (expressions = ::Low::TypeAccessors.expressions[[file_path, method_call.start_line]])
|
|
130
|
+
|
|
131
|
+
expressions.values.all? { |expression| expression.required? || expression.default_value.nil? }
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# "include LowType", "using LowType::Syntax" and "require 'lowtype'".
|
|
135
|
+
def lowtype?(method_call:)
|
|
136
|
+
return false if method_call.receiver?
|
|
137
|
+
|
|
138
|
+
case method_call.name
|
|
139
|
+
when :include then method_call.arguments == ['LowType']
|
|
140
|
+
when :using then method_call.arguments == ['LowType::Syntax']
|
|
141
|
+
when :require, :require_relative then method_call.arguments.one? && method_call.arguments.first.match?(REQUIRE_PATTERN)
|
|
142
|
+
else false
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def write(path:, content:)
|
|
147
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
148
|
+
File.write(path, content)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def copy(file_path:, dest_path:)
|
|
152
|
+
return if dest_path == file_path
|
|
153
|
+
|
|
154
|
+
FileUtils.mkdir_p(File.dirname(dest_path))
|
|
155
|
+
FileUtils.cp(file_path, dest_path, preserve: true)
|
|
156
|
+
end
|
|
8
157
|
end
|
|
9
158
|
end
|
|
@@ -66,6 +66,9 @@ module Low
|
|
|
66
66
|
new.class_evaluate(proxy: param_proxy, class_binding:)
|
|
67
67
|
end
|
|
68
68
|
param_proxy.expression = cast_type_expression(expression:, param_proxy:)
|
|
69
|
+
|
|
70
|
+
polyfill_default(param_proxy:)
|
|
71
|
+
validate_default!(param_proxy:)
|
|
69
72
|
end
|
|
70
73
|
rescue NameError => e
|
|
71
74
|
mp = method_proxy
|
|
@@ -99,6 +102,30 @@ module Low
|
|
|
99
102
|
|
|
100
103
|
nil
|
|
101
104
|
end
|
|
105
|
+
|
|
106
|
+
def polyfill_default(param_proxy:)
|
|
107
|
+
expression = param_proxy.expression
|
|
108
|
+
|
|
109
|
+
case expression.class.name
|
|
110
|
+
when 'Low::Dependency'
|
|
111
|
+
# Remap dependency expression to providers hash lookup method.
|
|
112
|
+
param_proxy.default = "Providers[#{expression.provider_key.inspect}]"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Fail early on class load rather than on method call.
|
|
117
|
+
def validate_default!(param_proxy:)
|
|
118
|
+
expression = param_proxy.expression
|
|
119
|
+
return unless expression.is_a?(TypeExpression) && !expression.required?
|
|
120
|
+
|
|
121
|
+
# When an enum's default value isn't an allowed value.
|
|
122
|
+
if expression.types.any? { |type| type.is_a?(Types::Enum::Definition) }
|
|
123
|
+
expression.validate!(value: expression.default_value, proxy: param_proxy)
|
|
124
|
+
end
|
|
125
|
+
rescue ArgumentError
|
|
126
|
+
raise AllowedTypeError, "Invalid default value #{expression.display_default} for parameter '#{param_proxy.name}' " \
|
|
127
|
+
"at #{param_proxy.file_path}:#{param_proxy.start_line}. Valid types: '#{expression.valid_types}'"
|
|
128
|
+
end
|
|
102
129
|
end
|
|
103
130
|
end
|
|
104
131
|
end
|
|
@@ -104,7 +104,10 @@ module Low
|
|
|
104
104
|
method_proxies.values.filter(&:expressions?).each do |method_proxy|
|
|
105
105
|
method_proxy.rewrite_signature
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
# Class methods are evaluated on the singleton class but "def self.method" would define a method on the
|
|
108
|
+
# singleton class's own singleton class. So methods with a receiver are evaluated in the class instead.
|
|
109
|
+
target = klass.singleton_class? && method_proxy.receiver? ? klass.attached_object : klass
|
|
110
|
+
target.class_eval(method_proxy.export, method_proxy.file_path, method_proxy.start_line)
|
|
108
111
|
|
|
109
112
|
if class_proxy.private_start_line && method_proxy.start_line > class_proxy.private_start_line
|
|
110
113
|
klass.send(:private, method_proxy.name)
|
|
@@ -17,53 +17,76 @@ module Low
|
|
|
17
17
|
# type_writer name: String | nil # accepts String or nil
|
|
18
18
|
#
|
|
19
19
|
module TypeAccessors
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
define_method(name) do
|
|
33
|
-
value = instance_variable_get("@#{name}")
|
|
34
|
-
expression.validate!(value:, proxy:)
|
|
35
|
-
value
|
|
36
|
-
end
|
|
20
|
+
class << self
|
|
21
|
+
# Expressions by file path and line, so that tools like TypeStripper can find the expressions of a type accessor.
|
|
22
|
+
def expressions
|
|
23
|
+
@expressions ||= {}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# A default value is returned when the instance variable is nil. Mutable defaults are new copies each time.
|
|
27
|
+
def default_value(expression:)
|
|
28
|
+
return nil if expression.required?
|
|
29
|
+
|
|
30
|
+
value = expression.default_value
|
|
31
|
+
value.is_a?(ValueExpression) ? value : value.dup
|
|
37
32
|
end
|
|
38
33
|
end
|
|
39
34
|
|
|
35
|
+
def type_reader(named_expressions)
|
|
36
|
+
location = caller_locations(1, 1).first
|
|
37
|
+
named_expressions.each { |name, expression| define_type_reader(name:, expression:, location:) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
40
|
def type_writer(named_expressions)
|
|
41
|
+
location = caller_locations(1, 1).first
|
|
42
|
+
named_expressions.each { |name, expression| define_type_writer(name:, expression:, location:) }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def type_accessor(named_expressions)
|
|
46
|
+
location = caller_locations(1, 1).first
|
|
47
|
+
|
|
41
48
|
named_expressions.each do |name, expression|
|
|
42
|
-
|
|
49
|
+
define_type_reader(name:, expression:, location:)
|
|
50
|
+
define_type_writer(name:, expression:, location:)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
43
55
|
|
|
44
|
-
|
|
45
|
-
|
|
56
|
+
def define_type_reader(name:, expression:, location:)
|
|
57
|
+
expression = register(name:, expression:, location:)
|
|
58
|
+
proxy = ::Lowkey::ReturnProxy.new(name:, source: accessor_source(name:, location:), expression:)
|
|
46
59
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
proxy = ::Lowkey::ParamProxy.new(name:, source:, type: :pos_req, position: nil, expression:)
|
|
60
|
+
define_method(name) do
|
|
61
|
+
value = instance_variable_get("@#{name}")
|
|
62
|
+
value = TypeAccessors.default_value(expression:) if value.nil?
|
|
51
63
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
instance_variable_set("@#{name}", value)
|
|
55
|
-
end
|
|
64
|
+
expression.validate!(value:, proxy:)
|
|
65
|
+
value.is_a?(ValueExpression) ? value.value : value
|
|
56
66
|
end
|
|
57
67
|
end
|
|
58
68
|
|
|
59
|
-
def
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
69
|
+
def define_type_writer(name:, expression:, location:)
|
|
70
|
+
expression = register(name:, expression:, location:)
|
|
71
|
+
proxy = ::Lowkey::ParamProxy.new(name:, source: accessor_source(name:, location:), type: :pos_req, position: nil, expression:)
|
|
72
|
+
|
|
73
|
+
define_method("#{name}=") do |value|
|
|
74
|
+
expression.validate!(value:, proxy:)
|
|
75
|
+
instance_variable_set("@#{name}", value)
|
|
63
76
|
end
|
|
64
77
|
end
|
|
65
78
|
|
|
66
|
-
|
|
79
|
+
def register(name:, expression:, location:)
|
|
80
|
+
expression = cast_type_expression(expression)
|
|
81
|
+
expressions = TypeAccessors.expressions[[location.path, location.lineno]] ||= {}
|
|
82
|
+
expressions[name] = expression
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Type accessors are defined on class load (not file load) so Source needs to be simulated.
|
|
86
|
+
def accessor_source(name:, location:)
|
|
87
|
+
line = location.lineno
|
|
88
|
+
::Lowkey::Source.new(file_path: location.path, scope: "#{self}##{name}", lines: [], start_line: line, end_line: line)
|
|
89
|
+
end
|
|
67
90
|
|
|
68
91
|
def cast_type_expression(expression)
|
|
69
92
|
if expression.is_a?(::Expressions::Expression)
|
|
@@ -55,7 +55,7 @@ module Low
|
|
|
55
55
|
if type.is_a?(Array)
|
|
56
56
|
"[#{type.map { |subtype| valid_subtype(subtype:) }.join(', ')}]"
|
|
57
57
|
else
|
|
58
|
-
type
|
|
58
|
+
display_name(type:)
|
|
59
59
|
end
|
|
60
60
|
end
|
|
61
61
|
|
|
@@ -63,6 +63,13 @@ module Low
|
|
|
63
63
|
types.join(' | ')
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
+
def display_default
|
|
67
|
+
display_default = default_value
|
|
68
|
+
display_default = default_value.value if default_value.is_a?(ValueExpression)
|
|
69
|
+
|
|
70
|
+
display_default.inspect.to_s
|
|
71
|
+
end
|
|
72
|
+
|
|
66
73
|
private
|
|
67
74
|
|
|
68
75
|
def union_expression(expression)
|
|
@@ -85,14 +92,20 @@ module Low
|
|
|
85
92
|
|
|
86
93
|
def valid_subtype(subtype:)
|
|
87
94
|
if subtype.is_a?(TypeExpression)
|
|
88
|
-
types = subtype.types
|
|
95
|
+
types = subtype.types.map { |type| display_name(type:) }
|
|
89
96
|
types << 'nil' if subtype.default_value.nil?
|
|
90
97
|
types.join(' | ')
|
|
91
98
|
else
|
|
92
|
-
subtype
|
|
99
|
+
display_name(type: subtype)
|
|
93
100
|
end
|
|
94
101
|
end
|
|
95
102
|
|
|
103
|
+
def display_name(type:)
|
|
104
|
+
return type.display_name if type.respond_to?(:display_name)
|
|
105
|
+
|
|
106
|
+
type.inspect.to_s.delete_prefix('Low::Types::')
|
|
107
|
+
end
|
|
108
|
+
|
|
96
109
|
def array_types_match_values?(types:, values:, proxy:)
|
|
97
110
|
# [X, Y, Z] An arbitrary amount of elements are arbitrary types in an arbitrary order.
|
|
98
111
|
if types.length > 1
|
|
@@ -136,8 +149,9 @@ module Low
|
|
|
136
149
|
end
|
|
137
150
|
|
|
138
151
|
def type_matches_value?(type:, value:, proxy:)
|
|
139
|
-
if
|
|
140
|
-
return type.match?(value:)
|
|
152
|
+
if Low::TypeQuery.complex_type?(expression: type)
|
|
153
|
+
return type.match?(value:)
|
|
154
|
+
elsif type.instance_of?(Class)
|
|
141
155
|
return value.value <= type if value.instance_of?(ValueExpression)
|
|
142
156
|
|
|
143
157
|
return value.is_a?(type)
|
data/lib/lowtype.rb
CHANGED
|
@@ -32,10 +32,11 @@ require_relative 'types/complex_types'
|
|
|
32
32
|
# │ │ │ │ │
|
|
33
33
|
module LowType
|
|
34
34
|
# Defers evaluation and redefinition until after the class body finishes loading via TracePoint :end.
|
|
35
|
-
def self.included(klass)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
def self.included(klass) # rubocop:disable Metrics/AbcSize
|
|
36
|
+
class_proxy = Lowkey[Low::FileQuery.file_path(klass:)][klass.name]
|
|
37
|
+
|
|
38
|
+
# Type accessors evaluate union types in the class body, before the class is evaluated.
|
|
39
|
+
require_relative 'syntax/union_types' if LowType.config.union_type_expressions
|
|
39
40
|
|
|
40
41
|
klass.include Low::ExpressionHelpers
|
|
41
42
|
klass.extend Low::ExpressionHelpers
|
|
@@ -45,7 +46,7 @@ module LowType
|
|
|
45
46
|
# Use TracePoint :end to capture the class binding after the class body finishes loading.
|
|
46
47
|
# At :end time, trace.self is the including class and trace.binding is the class body's binding,
|
|
47
48
|
# stored on class_proxy.class_binding for use by LowType and other consumers.
|
|
48
|
-
|
|
49
|
+
trace_point = TracePoint.new(:end) do |trace|
|
|
49
50
|
next unless trace.self == klass
|
|
50
51
|
|
|
51
52
|
class_proxy.class_binding = trace.binding
|
|
@@ -60,10 +61,10 @@ module LowType
|
|
|
60
61
|
|
|
61
62
|
Low::Adapter::Loader.load(klass:, class_proxy:)
|
|
62
63
|
|
|
63
|
-
|
|
64
|
+
trace_point.disable
|
|
64
65
|
end
|
|
65
66
|
|
|
66
|
-
|
|
67
|
+
trace_point.enable
|
|
67
68
|
end
|
|
68
69
|
|
|
69
70
|
Config = Struct.new(
|
data/lib/queries/type_query.rb
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative '../expressions/type_expression'
|
|
4
|
+
require_relative '../types/type_definition'
|
|
4
5
|
|
|
5
6
|
module Low
|
|
6
7
|
# TODO: Unit test.
|
|
7
8
|
class TypeQuery
|
|
8
9
|
class << self
|
|
9
10
|
def type?(expression)
|
|
10
|
-
basic_type?(expression:) || complex_type?(expression:)
|
|
11
|
+
basic_type?(expression:) || complex_type?(expression:) || typed_array?(expression:) || typed_hash?(expression:)
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
def typed_array?(expression:)
|
|
@@ -15,11 +16,17 @@ module Low
|
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
def value?(value)
|
|
18
|
-
!
|
|
19
|
+
!type?(value)
|
|
19
20
|
end
|
|
20
21
|
|
|
22
|
+
# Complex types and type definitions respond to "match?(value:)".
|
|
21
23
|
def complex_type?(expression:)
|
|
22
|
-
Low::Types::COMPLEX_TYPES.include?(expression) ||
|
|
24
|
+
Low::Types::COMPLEX_TYPES.include?(expression) || typed_definition?(expression:)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# A parameterized type such as "Status[200]" or "Enum[:a, :b]".
|
|
28
|
+
def typed_definition?(expression:)
|
|
29
|
+
expression.is_a?(Low::TypeDefinition)
|
|
23
30
|
end
|
|
24
31
|
|
|
25
32
|
private
|
data/lib/types/complex_types.rb
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative '../factories/type_factory'
|
|
4
|
+
require_relative 'boolean'
|
|
5
|
+
require_relative 'enum'
|
|
4
6
|
require_relative 'status'
|
|
5
7
|
|
|
6
8
|
module Low
|
|
7
9
|
module Types
|
|
8
10
|
COMPLEX_TYPES = [
|
|
9
|
-
Boolean
|
|
11
|
+
Boolean,
|
|
10
12
|
Headers = TypeFactory.complex_type(Hash),
|
|
11
13
|
HTML = TypeFactory.complex_type(String),
|
|
12
14
|
JSON = TypeFactory.complex_type(String),
|
data/lib/types/enum.rb
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'error_types'
|
|
4
|
+
require_relative 'type_definition'
|
|
5
|
+
|
|
6
|
+
module Low
|
|
7
|
+
module Types
|
|
8
|
+
# A set of allowed values like "Enum[:value, 'value', 123]".
|
|
9
|
+
class Enum
|
|
10
|
+
def self.[](*values)
|
|
11
|
+
Definition.new(values:)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class Definition
|
|
15
|
+
include TypeDefinition
|
|
16
|
+
|
|
17
|
+
attr_reader :values
|
|
18
|
+
|
|
19
|
+
def initialize(values:)
|
|
20
|
+
raise AllowedTypeError, 'Enum requires at least one allowed value' if values.empty?
|
|
21
|
+
|
|
22
|
+
@values = values.freeze
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def match?(value:)
|
|
26
|
+
@values.any? { |allowed| allowed == value }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def display_name
|
|
30
|
+
"Enum[#{@values.map(&:inspect).join(', ')}]"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def ==(other)
|
|
34
|
+
other.class == self.class && other.values == @values
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def eql?(other)
|
|
38
|
+
self == other
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def hash
|
|
42
|
+
[self.class, @values].hash
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
data/lib/types/status.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'complex_type'
|
|
4
4
|
require_relative 'error_types'
|
|
5
|
+
require_relative 'type_definition'
|
|
5
6
|
|
|
6
7
|
module Low
|
|
7
8
|
module Types
|
|
@@ -15,6 +16,8 @@ module Low
|
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
class StatusCode
|
|
19
|
+
include TypeDefinition
|
|
20
|
+
|
|
18
21
|
attr_reader :status_code
|
|
19
22
|
|
|
20
23
|
STATUS_CODES = [
|
|
@@ -37,6 +40,14 @@ module Low
|
|
|
37
40
|
@status_code = status_code
|
|
38
41
|
end
|
|
39
42
|
|
|
43
|
+
def match?(value:)
|
|
44
|
+
value == @status_code
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def display_name
|
|
48
|
+
"Status[#{@status_code}]"
|
|
49
|
+
end
|
|
50
|
+
|
|
40
51
|
def ==(other)
|
|
41
52
|
other.class == self.class && other.status_code == @status_code
|
|
42
53
|
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Low
|
|
4
|
+
# A complex type with params, created via the "Type[params]" syntax.
|
|
5
|
+
module TypeDefinition
|
|
6
|
+
# For "Type[params] | [type_expression/type/value]" situations, convert definition into a type expression.
|
|
7
|
+
def |(other)
|
|
8
|
+
::Low::TypeExpression.new(type: self) | other
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def match?(value:)
|
|
12
|
+
raise NotImplementedError
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def display_name
|
|
16
|
+
raise NotImplementedError
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
data/lib/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lowtype
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- maedi
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 2026-10-17 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: dirload
|
|
@@ -43,14 +43,14 @@ dependencies:
|
|
|
43
43
|
requirements:
|
|
44
44
|
- - "~>"
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
|
-
version: '0.
|
|
46
|
+
version: '0.5'
|
|
47
47
|
type: :runtime
|
|
48
48
|
prerelease: false
|
|
49
49
|
version_requirements: !ruby/object:Gem::Requirement
|
|
50
50
|
requirements:
|
|
51
51
|
- - "~>"
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
|
-
version: '0.
|
|
53
|
+
version: '0.5'
|
|
54
54
|
- !ruby/object:Gem::Dependency
|
|
55
55
|
name: trees
|
|
56
56
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -101,10 +101,13 @@ files:
|
|
|
101
101
|
- lib/queries/type_query.rb
|
|
102
102
|
- lib/syntax/syntax.rb
|
|
103
103
|
- lib/syntax/union_types.rb
|
|
104
|
+
- lib/types/boolean.rb
|
|
104
105
|
- lib/types/complex_type.rb
|
|
105
106
|
- lib/types/complex_types.rb
|
|
107
|
+
- lib/types/enum.rb
|
|
106
108
|
- lib/types/error_types.rb
|
|
107
109
|
- lib/types/status.rb
|
|
110
|
+
- lib/types/type_definition.rb
|
|
108
111
|
- lib/version.rb
|
|
109
112
|
homepage: https://github.com/low-rb/lowtype
|
|
110
113
|
licenses: []
|
|
@@ -125,7 +128,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
125
128
|
- !ruby/object:Gem::Version
|
|
126
129
|
version: '0'
|
|
127
130
|
requirements: []
|
|
128
|
-
rubygems_version:
|
|
131
|
+
rubygems_version: 3.6.2
|
|
129
132
|
specification_version: 4
|
|
130
133
|
summary: Elegant types in Ruby
|
|
131
134
|
test_files: []
|