low_type 0.8.10 → 0.9.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 +12 -8
- data/lib/adapters/sinatra_adapter.rb +55 -58
- data/lib/basic_types.rb +7 -2
- data/lib/error_types.rb +6 -4
- data/lib/factories/proxy_factory.rb +22 -0
- data/lib/instance_types.rb +58 -0
- data/lib/interfaces/adapter_interface.rb +2 -0
- data/lib/interfaces/error_interface.rb +2 -0
- data/lib/local_types.rb +26 -21
- data/lib/low_type.rb +49 -31
- data/lib/parser.rb +22 -14
- data/lib/proxies/file_proxy.rb +2 -0
- data/lib/proxies/local_proxy.rb +2 -0
- data/lib/proxies/method_proxy.rb +2 -0
- data/lib/proxies/param_proxy.rb +3 -1
- data/lib/proxies/return_proxy.rb +2 -0
- data/lib/redefiner.rb +31 -40
- data/lib/type_expression.rb +25 -19
- data/lib/value_expression.rb +3 -0
- data/lib/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a340469d2c2f4945b043ab3cd4973f14cf12f9d80336b7939bdb85b0da5b739e
|
|
4
|
+
data.tar.gz: 2e51c25e1b944dff67fb85f4582a2f3f38eca299fe58d4cc464c4b8c162a9d8a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8857cb2e301aa2e7f4ddf167fe622d3988d7d28ff223361a50a056f28ea248e4ad81ecc48fc2174f2f8845c7af7e8fad072ac9bf3ed4eea5a0bad37a06ab9b6d
|
|
7
|
+
data.tar.gz: c401324adf4f5aa4b4da5a1c6f19d5b3daa095e7ff28a43a9075f28f4d7a8947a13fcba500bab252eece331b51d65e89fee09efd99e1d61f5e0290faae51dab7
|
|
@@ -1,17 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require_relative 'sinatra_adapter'
|
|
2
4
|
|
|
3
5
|
module LowType
|
|
4
|
-
|
|
5
|
-
class
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
module Adapter
|
|
7
|
+
class Loader
|
|
8
|
+
class << self
|
|
9
|
+
def load(klass:, parser:, file_path:)
|
|
10
|
+
adaptor = nil
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
|
|
12
|
+
ancestors = klass.ancestors.map(&:to_s)
|
|
13
|
+
adaptor = Sinatra.new(klass:, parser:, file_path:) if ancestors.include?('Sinatra::Base')
|
|
11
14
|
|
|
12
|
-
|
|
15
|
+
return if adaptor.nil?
|
|
13
16
|
|
|
14
|
-
|
|
17
|
+
adaptor
|
|
18
|
+
end
|
|
15
19
|
end
|
|
16
20
|
end
|
|
17
21
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'prism'
|
|
2
4
|
|
|
3
5
|
require_relative '../interfaces/adapter_interface'
|
|
@@ -6,77 +8,72 @@ require_relative '../proxies/return_proxy'
|
|
|
6
8
|
require_relative '../error_types'
|
|
7
9
|
|
|
8
10
|
module LowType
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
@file_path = file_path
|
|
18
|
-
end
|
|
11
|
+
module Adapter
|
|
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
|
+
class Sinatra < AdapterInterface
|
|
14
|
+
def initialize(klass:, parser:, file_path:)
|
|
15
|
+
@klass = klass
|
|
16
|
+
@parser = parser
|
|
17
|
+
@file_path = file_path
|
|
18
|
+
end
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
def process # rubocop:disable Metrics/AbcSize
|
|
21
|
+
method_calls = @parser.method_calls(method_names: %i[get post patch put delete options query])
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
# Type check return values.
|
|
24
|
+
method_calls.each do |method_call|
|
|
25
|
+
arguments_node = method_call.compact_child_nodes.first
|
|
26
|
+
next unless arguments_node.is_a?(Prism::ArgumentsNode)
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
pattern = arguments_node.arguments.first.content
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return_proxy = return_proxy(method_node: method_call, pattern:, file:)
|
|
34
|
-
next unless return_proxy
|
|
30
|
+
line = Parser.line_number(node: method_call)
|
|
31
|
+
file = FileProxy.new(path: @file_path, line:, scope: "#{@klass}##{method_call.name}")
|
|
32
|
+
next unless (return_proxy = return_proxy(method_node: method_call, pattern:, file:))
|
|
35
33
|
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
route = "#{method_call.name.upcase} #{pattern}"
|
|
35
|
+
params = [ParamProxy.new(type_expression: nil, name: :route, type: :req, position: 0, file:)]
|
|
36
|
+
@klass.low_methods[route] = MethodProxy.new(name: method_call.name, params:, return_proxy:)
|
|
37
|
+
end
|
|
38
38
|
end
|
|
39
|
-
end
|
|
40
39
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
proxy.type_expression.types.each do |type|
|
|
51
|
-
proxy.type_expression.validate!(value: res, proxy:)
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
res = [res] if (Integer === res) || (String === res)
|
|
56
|
-
if (Array === res) && (Integer === res.first)
|
|
57
|
-
res = res.dup
|
|
58
|
-
status(res.shift)
|
|
59
|
-
body(res.pop)
|
|
60
|
-
headers(*res)
|
|
61
|
-
elsif res.respond_to? :each
|
|
62
|
-
body res
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
nil # avoid double setting the same response tuple twice
|
|
66
|
-
end
|
|
40
|
+
def return_proxy(method_node:, pattern:, file:)
|
|
41
|
+
return_type = Parser.return_type(method_node:)
|
|
42
|
+
return nil if return_type.nil?
|
|
43
|
+
|
|
44
|
+
# Not a security risk because the code comes from a trusted source; the file that did the include. Does the file trust itself?
|
|
45
|
+
expression = eval(return_type.slice).call # rubocop:disable Security/Eval
|
|
46
|
+
expression = TypeExpression.new(type: expression) unless expression.is_a?(TypeExpression)
|
|
47
|
+
|
|
48
|
+
ReturnProxy.new(type_expression: expression, name: "#{method_node.name.upcase} #{pattern}", file:)
|
|
67
49
|
end
|
|
68
50
|
end
|
|
69
51
|
|
|
70
|
-
|
|
52
|
+
module Methods
|
|
53
|
+
def invoke(&block)
|
|
54
|
+
res = catch(:halt, &block)
|
|
55
|
+
|
|
56
|
+
low_validate!(value: res) if res
|
|
71
57
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
58
|
+
res = [res] if res.is_a?(Integer) || res.is_a?(String)
|
|
59
|
+
if res.is_a?(Array) && res.first.is_a?(Integer)
|
|
60
|
+
res = res.dup
|
|
61
|
+
status(res.shift)
|
|
62
|
+
body(res.pop)
|
|
63
|
+
headers(*res)
|
|
64
|
+
elsif res.respond_to? :each
|
|
65
|
+
body res
|
|
66
|
+
end
|
|
75
67
|
|
|
76
|
-
|
|
77
|
-
|
|
68
|
+
nil # avoid double setting the same response tuple twice
|
|
69
|
+
end
|
|
78
70
|
|
|
79
|
-
|
|
71
|
+
def low_validate!(value:)
|
|
72
|
+
route = "#{request.request_method} #{request.path}"
|
|
73
|
+
if (method_proxy = self.class.low_methods[route]) && (proxy = method_proxy.return_proxy)
|
|
74
|
+
proxy.type_expression.validate!(value:, proxy:)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
80
77
|
end
|
|
81
78
|
end
|
|
82
79
|
end
|
data/lib/basic_types.rb
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
class Boolean; end # TrueClass or FalseClass
|
|
1
|
+
# frozen_string_literal: true
|
|
3
2
|
|
|
3
|
+
module LowType
|
|
4
|
+
# TrueClass or FalseClass
|
|
5
|
+
class Boolean; end
|
|
6
|
+
class Tuple < Array; end
|
|
7
|
+
class Status < Integer; end
|
|
8
|
+
class Headers < Hash; end
|
|
4
9
|
class HTML < String; end
|
|
5
10
|
class JSON < String; end
|
|
6
11
|
class XML < String; end
|
data/lib/error_types.rb
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module LowType
|
|
2
|
-
class ArgumentTypeError < TypeError; end
|
|
3
|
-
class LocalTypeError < TypeError; end
|
|
4
|
-
class ReturnTypeError < TypeError; end
|
|
5
|
-
class AllowedTypeError < TypeError; end
|
|
4
|
+
class ArgumentTypeError < TypeError; end
|
|
5
|
+
class LocalTypeError < TypeError; end
|
|
6
|
+
class ReturnTypeError < TypeError; end
|
|
7
|
+
class AllowedTypeError < TypeError; end
|
|
6
8
|
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../proxies/return_proxy'
|
|
4
|
+
require_relative '../parser'
|
|
5
|
+
require_relative '../type_expression'
|
|
6
|
+
|
|
7
|
+
module LowType
|
|
8
|
+
class ProxyFactory
|
|
9
|
+
class << self
|
|
10
|
+
def return_proxy(method_node:, file:)
|
|
11
|
+
return_type = Parser.return_type(method_node:)
|
|
12
|
+
return nil if return_type.nil?
|
|
13
|
+
|
|
14
|
+
# Not a security risk because the code comes from a trusted source; the file that did the include. Does the file trust itself?
|
|
15
|
+
expression = eval(return_type.slice, binding, __FILE__, __LINE__).call # rubocop:disable Security/Eval
|
|
16
|
+
expression = TypeExpression.new(type: expression) unless expression.is_a?(TypeExpression)
|
|
17
|
+
|
|
18
|
+
ReturnProxy.new(type_expression: expression, name: method_node.name, file:)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'proxies/return_proxy'
|
|
4
|
+
require_relative 'type_expression'
|
|
5
|
+
|
|
6
|
+
module LowType
|
|
7
|
+
module InstanceTypes
|
|
8
|
+
def type_reader(named_expressions)
|
|
9
|
+
named_expressions.each do |name, expression|
|
|
10
|
+
last_caller = caller_locations(1, 1).first
|
|
11
|
+
type_expression = type_expression(expression)
|
|
12
|
+
file = FileProxy.new(path: last_caller.path, line: last_caller.lineno, scope: "#{self}##{name}")
|
|
13
|
+
|
|
14
|
+
@low_methods[name] = MethodProxy.new(name:, return_proxy: ReturnProxy.new(type_expression:, name:, file:))
|
|
15
|
+
|
|
16
|
+
define_method(name) do
|
|
17
|
+
method_proxy = self.class.low_methods[name]
|
|
18
|
+
value = instance_variable_get("@#{name}")
|
|
19
|
+
type_expression.validate!(value:, proxy: method_proxy.return_proxy)
|
|
20
|
+
value
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def type_writer(named_expressions)
|
|
26
|
+
named_expressions.each do |name, expression|
|
|
27
|
+
last_caller = caller_locations(1, 1).first
|
|
28
|
+
type_expression = type_expression(expression)
|
|
29
|
+
file = FileProxy.new(path: last_caller.path, line: last_caller.lineno, scope: "#{self}##{name}")
|
|
30
|
+
|
|
31
|
+
@low_methods["#{name}="] = MethodProxy.new(name:, params: [ParamProxy.new(type_expression:, name:, type: :hashreq, file:)])
|
|
32
|
+
|
|
33
|
+
define_method("#{name}=") do |value|
|
|
34
|
+
method_proxy = self.class.low_methods["#{name}="]
|
|
35
|
+
type_expression.validate!(value:, proxy: method_proxy.params.first)
|
|
36
|
+
instance_variable_set("@#{name}", value)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def type_accessor(named_expressions)
|
|
42
|
+
named_expressions.each do |name, expression|
|
|
43
|
+
type_reader({ name => expression })
|
|
44
|
+
type_writer({ name => expression })
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def type_expression(expression)
|
|
51
|
+
if expression.instance_of?(TypeExpression)
|
|
52
|
+
expression
|
|
53
|
+
elsif ::LowType.type?(expression)
|
|
54
|
+
TypeExpression.new(type: expression)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
data/lib/local_types.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require_relative 'proxies/file_proxy'
|
|
2
4
|
require_relative 'proxies/local_proxy'
|
|
3
5
|
require_relative 'type_expression'
|
|
@@ -7,11 +9,9 @@ module LocalTypes
|
|
|
7
9
|
class AssignmentError < StandardError; end
|
|
8
10
|
|
|
9
11
|
def type(type_expression)
|
|
10
|
-
referenced_object = type_expression.default_value
|
|
12
|
+
referenced_object = type_expression.default_value.dup
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
raise AssignmentError, "Single-instance objects like #{referenced_object} are not supported"
|
|
14
|
-
end
|
|
14
|
+
raise AssignmentError, "Single-instance objects like #{referenced_object} are not supported" unless LowType.value?(referenced_object)
|
|
15
15
|
|
|
16
16
|
last_caller = caller_locations(1, 1).first
|
|
17
17
|
file = LowType::FileProxy.new(path: last_caller.path, line: last_caller.lineno, scope: 'local type')
|
|
@@ -20,36 +20,23 @@ module LocalTypes
|
|
|
20
20
|
|
|
21
21
|
type_expression.validate!(value: referenced_object, proxy: local_proxy)
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
local_proxy = self.instance_variable_get('@local_proxy')
|
|
25
|
-
type_expression = local_proxy.type_expression
|
|
26
|
-
type_expression.validate!(value:, proxy: local_proxy)
|
|
27
|
-
|
|
28
|
-
# We can't reassign self in Ruby so we reassign instance variables instead.
|
|
29
|
-
value.instance_variables.each do |variable|
|
|
30
|
-
self.instance_variable_set(variable, value.instance_variable_get(variable))
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
self
|
|
34
|
-
end
|
|
23
|
+
define_with_type(referenced_object:)
|
|
35
24
|
|
|
36
25
|
return referenced_object.value if referenced_object.is_a?(ValueExpression)
|
|
37
26
|
|
|
38
27
|
referenced_object
|
|
39
28
|
end
|
|
40
|
-
|
|
29
|
+
alias low_type type
|
|
41
30
|
|
|
42
31
|
def value(type)
|
|
43
32
|
LowType.value(type:)
|
|
44
33
|
end
|
|
45
|
-
|
|
34
|
+
alias low_value value
|
|
46
35
|
|
|
47
36
|
# Scoped to the class that includes LowTypes module.
|
|
48
37
|
class Array < ::Array
|
|
49
38
|
def self.[](*types)
|
|
50
|
-
if types.all? { |type| LowType.type?(type) }
|
|
51
|
-
return LowType::TypeExpression.new(type: [*types])
|
|
52
|
-
end
|
|
39
|
+
return LowType::TypeExpression.new(type: [*types]) if types.all? { |type| LowType.type?(type) }
|
|
53
40
|
|
|
54
41
|
super
|
|
55
42
|
end
|
|
@@ -59,7 +46,25 @@ module LocalTypes
|
|
|
59
46
|
class Hash < ::Hash
|
|
60
47
|
def self.[](type)
|
|
61
48
|
return LowType::TypeExpression.new(type:) if LowType.type?(type)
|
|
49
|
+
|
|
62
50
|
super
|
|
63
51
|
end
|
|
64
52
|
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def define_with_type(referenced_object:)
|
|
57
|
+
def referenced_object.with_type=(value)
|
|
58
|
+
local_proxy = instance_variable_get('@local_proxy')
|
|
59
|
+
type_expression = local_proxy.type_expression
|
|
60
|
+
type_expression.validate!(value:, proxy: local_proxy)
|
|
61
|
+
|
|
62
|
+
# We can't reassign self in Ruby so we reassign instance variables instead.
|
|
63
|
+
value.instance_variables.each do |variable|
|
|
64
|
+
instance_variable_set(variable, value.instance_variable_get(variable))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
self
|
|
68
|
+
end
|
|
69
|
+
end
|
|
65
70
|
end
|
data/lib/low_type.rb
CHANGED
|
@@ -2,33 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'adapters/adapter_loader'
|
|
4
4
|
require_relative 'basic_types'
|
|
5
|
+
require_relative 'instance_types'
|
|
5
6
|
require_relative 'redefiner'
|
|
6
7
|
require_relative 'type_expression'
|
|
7
8
|
require_relative 'value_expression'
|
|
8
9
|
|
|
10
|
+
# Include this module into your class to define and check types.
|
|
9
11
|
module LowType
|
|
10
12
|
# We do as much as possible on class load rather than on instantiation to be thread-safe and efficient.
|
|
11
|
-
def self.included(klass)
|
|
12
|
-
|
|
13
|
-
# Array[] class method returns a type expression only for the duration of this "included" hook.
|
|
13
|
+
def self.included(klass) # rubocop:disable Metrics/AbcSize
|
|
14
|
+
# Array[] and Hash[] class method returns a type expression only for the duration of this "included" hook.
|
|
14
15
|
array_class_method = Array.method('[]').unbind
|
|
15
|
-
Array.define_singleton_method('[]') do |*types|
|
|
16
|
-
TypeExpression.new(type: [*types])
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
# Hash[] class method returns a type expression only for the duration of this "included" hook.
|
|
20
16
|
hash_class_method = Hash.method('[]').unbind
|
|
21
|
-
|
|
22
|
-
# Support Pry which uses Hash[].
|
|
23
|
-
unless LowType.type?(type)
|
|
24
|
-
Hash.define_singleton_method('[]', hash_class_method)
|
|
25
|
-
result = Hash[type]
|
|
26
|
-
Hash.method('[]').unbind
|
|
27
|
-
return result
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
TypeExpression.new(type:)
|
|
31
|
-
end
|
|
17
|
+
LowType.redefine(hash_class_method:)
|
|
32
18
|
|
|
33
19
|
class << klass
|
|
34
20
|
def low_methods
|
|
@@ -37,15 +23,16 @@ module LowType
|
|
|
37
23
|
end
|
|
38
24
|
|
|
39
25
|
file_path = LowType.file_path(klass:)
|
|
40
|
-
parser = Parser.new(file_path:)
|
|
26
|
+
parser = LowType::Parser.new(file_path:)
|
|
41
27
|
private_start_line = parser.private_start_line
|
|
42
28
|
|
|
43
|
-
klass.
|
|
44
|
-
klass.
|
|
29
|
+
klass.extend InstanceTypes
|
|
30
|
+
klass.prepend LowType::Redefiner.redefine(method_nodes: parser.instance_methods, klass:, private_start_line:, file_path:)
|
|
31
|
+
klass.singleton_class.prepend LowType::Redefiner.redefine(method_nodes: parser.class_methods, klass:, private_start_line:, file_path:)
|
|
45
32
|
|
|
46
|
-
if (adapter =
|
|
33
|
+
if (adapter = Adapter::Loader.load(klass:, parser:, file_path:))
|
|
47
34
|
adapter.process
|
|
48
|
-
klass.prepend
|
|
35
|
+
klass.prepend Adapter::Methods
|
|
49
36
|
end
|
|
50
37
|
ensure
|
|
51
38
|
Array.define_singleton_method('[]', array_class_method)
|
|
@@ -63,12 +50,12 @@ module LowType
|
|
|
63
50
|
def configure
|
|
64
51
|
yield(config)
|
|
65
52
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
53
|
+
return unless config.local_types
|
|
54
|
+
|
|
55
|
+
require_relative 'local_types'
|
|
56
|
+
include LocalTypes
|
|
70
57
|
end
|
|
71
|
-
|
|
58
|
+
|
|
72
59
|
# Internal API.
|
|
73
60
|
|
|
74
61
|
def file_path(klass:)
|
|
@@ -78,9 +65,21 @@ module LowType
|
|
|
78
65
|
caller.find { |callee| callee.end_with?("<class:#{class_name}>'") }.split(':').first
|
|
79
66
|
end
|
|
80
67
|
|
|
81
|
-
# TODO: Unit test
|
|
68
|
+
# TODO: Unit test.
|
|
82
69
|
def type?(type)
|
|
83
|
-
|
|
70
|
+
LowType.basic_type?(type:) || LowType.complex_type?(type:)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def basic_type?(type:)
|
|
74
|
+
type.respond_to?(:new) || type == Integer || type == Symbol
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def complex_type?(type:)
|
|
78
|
+
!basic_type?(type:) && LowType.typed_hash?(type:)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def typed_hash?(type:)
|
|
82
|
+
type.is_a?(::Hash) && LowType.basic_type?(type: type.keys.first) && LowType.basic_type?(type: type.values.first)
|
|
84
83
|
end
|
|
85
84
|
|
|
86
85
|
def value?(expression)
|
|
@@ -90,5 +89,24 @@ module LowType
|
|
|
90
89
|
def value(type:)
|
|
91
90
|
TypeExpression.new(default_value: ValueExpression.new(value: type))
|
|
92
91
|
end
|
|
92
|
+
|
|
93
|
+
# TODO: Unit test.
|
|
94
|
+
def redefine(hash_class_method:)
|
|
95
|
+
Array.define_singleton_method('[]') do |*types|
|
|
96
|
+
TypeExpression.new(type: [*types])
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
Hash.define_singleton_method('[]') do |type|
|
|
100
|
+
# Support Pry which uses Hash[].
|
|
101
|
+
unless LowType.type?(type)
|
|
102
|
+
Hash.define_singleton_method('[]', hash_class_method)
|
|
103
|
+
result = Hash[type]
|
|
104
|
+
Hash.method('[]').unbind
|
|
105
|
+
return result
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
TypeExpression.new(type:)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
93
111
|
end
|
|
94
112
|
end
|
data/lib/parser.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'prism'
|
|
2
4
|
|
|
3
5
|
module LowType
|
|
@@ -25,23 +27,29 @@ module LowType
|
|
|
25
27
|
block_visitor.method_calls
|
|
26
28
|
end
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
class << self
|
|
31
|
+
# Only a lambda defined immediately after a method's parameters/block is considered a return type expression.
|
|
32
|
+
def return_type(method_node:)
|
|
33
|
+
# Method statements.
|
|
34
|
+
statements_node = method_node.compact_child_nodes.find { |node| node.is_a?(Prism::StatementsNode) }
|
|
32
35
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
# Block statements.
|
|
37
|
+
if statements_node.nil?
|
|
38
|
+
block_node = method_node.compact_child_nodes.find { |node| node.is_a?(Prism::BlockNode) }
|
|
39
|
+
statements_node = block_node.compact_child_nodes.find { |node| node.is_a?(Prism::StatementsNode) } if block_node
|
|
40
|
+
end
|
|
38
41
|
|
|
39
|
-
|
|
42
|
+
return nil if statements_node.nil? # Sometimes developers define methods without code inside them.
|
|
40
43
|
|
|
41
|
-
|
|
42
|
-
|
|
44
|
+
node = statements_node.body.first
|
|
45
|
+
return node if node.is_a?(Prism::LambdaNode)
|
|
43
46
|
|
|
44
|
-
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def line_number(node:)
|
|
51
|
+
node.respond_to?(:start_line) ? node.start_line : nil
|
|
52
|
+
end
|
|
45
53
|
end
|
|
46
54
|
end
|
|
47
55
|
|
|
@@ -73,7 +81,7 @@ module LowType
|
|
|
73
81
|
private
|
|
74
82
|
|
|
75
83
|
def class_method?(node)
|
|
76
|
-
return true if node.is_a?(::Prism::DefNode) && node.receiver.
|
|
84
|
+
return true if node.is_a?(::Prism::DefNode) && node.receiver.instance_of?(Prism::SelfNode) # self.method_name
|
|
77
85
|
return true if node.is_a?(::Prism::SingletonClassNode) # class << self
|
|
78
86
|
|
|
79
87
|
if (parent_node = @parent_map[node])
|
data/lib/proxies/file_proxy.rb
CHANGED
data/lib/proxies/local_proxy.rb
CHANGED
data/lib/proxies/method_proxy.rb
CHANGED
data/lib/proxies/param_proxy.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require_relative '../interfaces/error_interface'
|
|
2
4
|
require_relative '../error_types'
|
|
3
5
|
|
|
@@ -5,7 +7,7 @@ module LowType
|
|
|
5
7
|
class ParamProxy < ErrorInterface
|
|
6
8
|
attr_reader :type_expression, :name, :type, :position
|
|
7
9
|
|
|
8
|
-
def initialize(type_expression:, name:, type:, position: nil
|
|
10
|
+
def initialize(type_expression:, name:, type:, file:, position: nil)
|
|
9
11
|
@type_expression = type_expression
|
|
10
12
|
@name = name
|
|
11
13
|
@type = type
|
data/lib/proxies/return_proxy.rb
CHANGED
data/lib/redefiner.rb
CHANGED
|
@@ -1,21 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'factories/proxy_factory'
|
|
1
4
|
require_relative 'proxies/file_proxy'
|
|
2
5
|
require_relative 'proxies/method_proxy'
|
|
3
6
|
require_relative 'proxies/param_proxy'
|
|
4
|
-
require_relative 'proxies/return_proxy'
|
|
5
|
-
require_relative 'parser'
|
|
6
7
|
require_relative 'type_expression'
|
|
7
8
|
|
|
8
9
|
module LowType
|
|
10
|
+
# Redefine methods to have their arguments and return values type checked.
|
|
9
11
|
class Redefiner
|
|
10
12
|
class << self
|
|
11
|
-
def
|
|
13
|
+
def redefine(method_nodes:, klass:, private_start_line:, file_path:) # rubocop:disable Metrics
|
|
12
14
|
Module.new do
|
|
13
15
|
method_nodes.each do |method_node|
|
|
14
16
|
name = method_node.name
|
|
15
|
-
line =
|
|
17
|
+
line = Parser.line_number(node: method_node)
|
|
16
18
|
file = FileProxy.new(path: file_path, line:, scope: "#{klass}##{method_node.name}")
|
|
17
19
|
params = Redefiner.params_with_type_expressions(method_node:, file:)
|
|
18
|
-
return_proxy =
|
|
20
|
+
return_proxy = ProxyFactory.return_proxy(method_node:, file:)
|
|
19
21
|
|
|
20
22
|
klass.low_methods[name] = MethodProxy.new(name:, params:, return_proxy:)
|
|
21
23
|
|
|
@@ -23,7 +25,9 @@ module LowType
|
|
|
23
25
|
klass.low_methods[name].params.each do |param_proxy|
|
|
24
26
|
# Get argument value or default value.
|
|
25
27
|
value = param_proxy.position ? args[param_proxy.position] : kwargs[param_proxy.name]
|
|
26
|
-
|
|
28
|
+
if value.nil? && param_proxy.type_expression.default_value != :LOW_TYPE_UNDEFINED
|
|
29
|
+
value = param_proxy.type_expression.default_value
|
|
30
|
+
end
|
|
27
31
|
# Validate argument type.
|
|
28
32
|
param_proxy.type_expression.validate!(value:, proxy: param_proxy)
|
|
29
33
|
# Handle value(type) special case.
|
|
@@ -41,9 +45,7 @@ module LowType
|
|
|
41
45
|
super(*args, **kwargs)
|
|
42
46
|
end
|
|
43
47
|
|
|
44
|
-
if private_start_line && method_node.start_line > private_start_line
|
|
45
|
-
private name
|
|
46
|
-
end
|
|
48
|
+
private name if private_start_line && method_node.start_line > private_start_line
|
|
47
49
|
end
|
|
48
50
|
end
|
|
49
51
|
end
|
|
@@ -52,50 +54,39 @@ module LowType
|
|
|
52
54
|
return [] if method_node.parameters.nil?
|
|
53
55
|
|
|
54
56
|
params = method_node.parameters.slice
|
|
55
|
-
|
|
57
|
+
# Not a security risk because the code comes from a trusted source; the file that did the include. Does the file trust itself?
|
|
58
|
+
proxy_method = eval("-> (#{params}) {}", binding, __FILE__, __LINE__) # rubocop:disable Security/Eval
|
|
56
59
|
required_args, required_kwargs = required_args(proxy_method:)
|
|
57
60
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
# Not a security risk because the code comes from a trusted source; the file that did the include. Does the file trust itself?
|
|
62
|
+
typed_method = <<~RUBY
|
|
63
|
+
-> (#{params}) {
|
|
64
|
+
param_proxies = []
|
|
62
65
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
proxy_method.parameters.each_with_index do |param, position|
|
|
67
|
+
type, name = param
|
|
68
|
+
position = nil unless [:opt, :req, :rest].include?(type)
|
|
69
|
+
expression = binding.local_variable_get(name)
|
|
66
70
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
elsif ::LowType.type?(expression)
|
|
72
|
-
param_proxies << ParamProxy.new(type_expression: TypeExpression.new(type: expression), name:, type:, position:, file:)
|
|
73
|
-
end
|
|
71
|
+
if expression.is_a?(TypeExpression)
|
|
72
|
+
param_proxies << ParamProxy.new(type_expression: expression, name:, type:, position:, file:)
|
|
73
|
+
elsif ::LowType.type?(expression)
|
|
74
|
+
param_proxies << ParamProxy.new(type_expression: TypeExpression.new(type: expression), name:, type:, position:, file:)
|
|
74
75
|
end
|
|
76
|
+
end
|
|
75
77
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
)
|
|
78
|
+
param_proxies
|
|
79
|
+
}
|
|
80
|
+
RUBY
|
|
80
81
|
|
|
81
|
-
# Called with only required args
|
|
82
|
-
typed_method.call(*required_args, **required_kwargs)
|
|
82
|
+
# Called with only required args (as nil) and optional args omitted, to evaluate type expressions (stored as default values).
|
|
83
|
+
eval(typed_method, binding, __FILE__, __LINE__).call(*required_args, **required_kwargs) # rubocop:disable Security/Eval
|
|
83
84
|
|
|
84
85
|
# TODO: Write spec for this.
|
|
85
86
|
rescue ArgumentError => e
|
|
86
87
|
raise ArgumentError, "Incorrect param syntax: #{e.message}"
|
|
87
88
|
end
|
|
88
89
|
|
|
89
|
-
def return_proxy(method_node:, file:)
|
|
90
|
-
return_type = Parser.return_type(method_node:)
|
|
91
|
-
return nil if return_type.nil?
|
|
92
|
-
|
|
93
|
-
expression = eval(return_type.slice).call
|
|
94
|
-
expression = TypeExpression.new(type: expression) unless expression.is_a?(TypeExpression)
|
|
95
|
-
|
|
96
|
-
ReturnProxy.new(type_expression: expression, name: method_node.name, file:)
|
|
97
|
-
end
|
|
98
|
-
|
|
99
90
|
private
|
|
100
91
|
|
|
101
92
|
def required_args(proxy_method:)
|
data/lib/type_expression.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require_relative 'proxies/param_proxy'
|
|
2
4
|
|
|
3
5
|
module LowType
|
|
@@ -5,8 +7,9 @@ module LowType
|
|
|
5
7
|
file_path = File.expand_path(__FILE__)
|
|
6
8
|
adapter_paths = Dir.chdir(root_path) { Dir.glob('adapters/*') }.map { |path| File.join(root_path, path) }
|
|
7
9
|
|
|
8
|
-
HIDDEN_PATHS = [file_path, *adapter_paths, File.join(root_path, 'redefiner.rb')]
|
|
10
|
+
HIDDEN_PATHS = [file_path, *adapter_paths, File.join(root_path, 'redefiner.rb')].freeze
|
|
9
11
|
|
|
12
|
+
# Represent types and default values as a series of chainable expressions.
|
|
10
13
|
class TypeExpression
|
|
11
14
|
attr_reader :types, :default_value
|
|
12
15
|
|
|
@@ -18,8 +21,8 @@ module LowType
|
|
|
18
21
|
end
|
|
19
22
|
|
|
20
23
|
def |(expression)
|
|
21
|
-
if expression.
|
|
22
|
-
@types
|
|
24
|
+
if expression.instance_of?(::LowType::TypeExpression)
|
|
25
|
+
@types += expression.types
|
|
23
26
|
@default_value = expression.default_value
|
|
24
27
|
elsif ::LowType.value?(expression)
|
|
25
28
|
@default_value = expression
|
|
@@ -34,20 +37,17 @@ module LowType
|
|
|
34
37
|
@default_value == :LOW_TYPE_UNDEFINED
|
|
35
38
|
end
|
|
36
39
|
|
|
37
|
-
def validate!(value:, proxy:)
|
|
40
|
+
def validate!(value:, proxy:) # rubocop:disable Metrics
|
|
38
41
|
if value.nil?
|
|
39
42
|
return true if @default_value.nil?
|
|
40
43
|
raise proxy.error_type, proxy.error_message(value:) if required?
|
|
41
44
|
end
|
|
42
45
|
|
|
43
46
|
@types.each do |type|
|
|
44
|
-
|
|
45
|
-
return true if
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if type.class == ::Hash && value.class == ::Hash && type.keys[0] == value.keys[0].class && type.values[0] == value.values[0].class
|
|
49
|
-
return true
|
|
50
|
-
end
|
|
47
|
+
# Example: HTML is a subclass of String and should pass as a String.
|
|
48
|
+
return true if LowType.basic_type?(type:) && type <= value.class
|
|
49
|
+
return true if type.is_a?(::Array) && value.is_a?(::Array) && array_types_match_values?(types: type, values: value)
|
|
50
|
+
return true if type.is_a?(::Hash) && value.is_a?(::Hash) && hash_types_match_values?(type:, value:)
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
raise proxy.error_type, proxy.error_message(value:)
|
|
@@ -57,7 +57,7 @@ module LowType
|
|
|
57
57
|
|
|
58
58
|
def valid_types
|
|
59
59
|
types = @types.map { |type| type.inspect.to_s }
|
|
60
|
-
types
|
|
60
|
+
types += ['nil'] if @default_value.nil?
|
|
61
61
|
types.join(' | ')
|
|
62
62
|
end
|
|
63
63
|
|
|
@@ -67,7 +67,8 @@ module LowType
|
|
|
67
67
|
# [T, T, T]
|
|
68
68
|
if types.length > 1
|
|
69
69
|
types.each_with_index do |type, index|
|
|
70
|
-
|
|
70
|
+
# Example: HTML is a subclass of String and should pass as a String.
|
|
71
|
+
return false unless type <= values[index].class
|
|
71
72
|
end
|
|
72
73
|
# [T]
|
|
73
74
|
elsif types.length == 1
|
|
@@ -78,6 +79,11 @@ module LowType
|
|
|
78
79
|
true
|
|
79
80
|
end
|
|
80
81
|
|
|
82
|
+
def hash_types_match_values?(type:, value:)
|
|
83
|
+
# TODO: Shallow validation of hash could be made deeper with user config.
|
|
84
|
+
type.keys[0] == value.keys[0].class && type.values[0] == value.values[0].class
|
|
85
|
+
end
|
|
86
|
+
|
|
81
87
|
def backtrace_with_proxy(proxy:, backtrace:)
|
|
82
88
|
# Remove LowType defined method file paths from the backtrace.
|
|
83
89
|
filtered_backtrace = backtrace.reject { |line| HIDDEN_PATHS.find { |file_path| line.include?(file_path) } }
|
|
@@ -92,21 +98,21 @@ module LowType
|
|
|
92
98
|
end
|
|
93
99
|
end
|
|
94
100
|
|
|
101
|
+
# For "Type | [type_expression/type/value]" situations, redirecting to or generating a type expression from types.
|
|
102
|
+
# "|" is not defined on Object class and this is the most compute-efficient way to achieve our goal (world peace).
|
|
103
|
+
# "|" is overridable by any child object. While we could def/undef this method, this approach is actually lighter.
|
|
104
|
+
# "|" bitwise operator on Integer is not defined when the receiver is an Integer class, so we are not in conflict.
|
|
95
105
|
class Object
|
|
96
106
|
class << self
|
|
97
|
-
# For "Type | [type_expression/type/value]" situations, redirecting to or generating a type expression from types.
|
|
98
|
-
# "|" is not defined on Object class and this is the most compute-efficient way to achieve our goal (world peace).
|
|
99
|
-
# "|" is overridable by any child object. While we could def/undef this method, this approach is actually lighter.
|
|
100
|
-
# "|" bitwise operator on Integer is not defined when the receiver is an Integer class, so we are not in conflict.
|
|
101
107
|
def |(expression)
|
|
102
|
-
if expression.
|
|
108
|
+
if expression.instance_of?(::LowType::TypeExpression)
|
|
103
109
|
# We pass our type into their type expression.
|
|
104
110
|
expression | self
|
|
105
111
|
expression
|
|
106
112
|
else
|
|
107
113
|
# We turn our type into a type expression and pass in their [type_expression/type/value].
|
|
108
114
|
type_expression = ::LowType::TypeExpression.new(type: self)
|
|
109
|
-
type_expression | expression
|
|
115
|
+
type_expression | expression
|
|
110
116
|
end
|
|
111
117
|
end
|
|
112
118
|
end
|
data/lib/value_expression.rb
CHANGED
data/lib/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: low_type
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- maedi
|
|
@@ -21,6 +21,8 @@ files:
|
|
|
21
21
|
- lib/adapters/sinatra_adapter.rb
|
|
22
22
|
- lib/basic_types.rb
|
|
23
23
|
- lib/error_types.rb
|
|
24
|
+
- lib/factories/proxy_factory.rb
|
|
25
|
+
- lib/instance_types.rb
|
|
24
26
|
- lib/interfaces/adapter_interface.rb
|
|
25
27
|
- lib/interfaces/error_interface.rb
|
|
26
28
|
- lib/local_types.rb
|