low_type 1.1.9 → 1.1.10

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: 372ec6574de86edfe13eba87a457e290934635e88b092a711798df69d04aaa30
4
- data.tar.gz: 3afaccd3fed8508ca0064cc9552b80bd05cf7b09ecfdd1906a93a043034ec978
3
+ metadata.gz: 54e1e267e4ef98dc2364738a169dee20cb6364d9cb597c8f7c46084309b2296f
4
+ data.tar.gz: 1c1b44d5b7ba03b955d039bf62a7898897d2d04230dc89ff58df055de719d37d
5
5
  SHA512:
6
- metadata.gz: d59f9cf98881584fb19d3ca79748d67fdc71c5a9b55ff4ff3c4324c5658cedc88567b4037876e1d8ed5a087aafb76deff459f9493f2194ccbf002ff855a1f88b
7
- data.tar.gz: be0a567f9212cb628c7efe852f71901ea4495cb9eb75681a259e68e7ef2fa545cfdfa8df0b01e4774872158db607f57fd950f274a4dcad32910c87f743de6208
6
+ metadata.gz: 39b512c4ff8c250659bdce0bb1be51c5f3e46e7a8ac30fa607deabdf9c933b7459bb78542211aafebf483dff3ecd8600ebb589dd4b91fb0d410c4dd9206e74fb
7
+ data.tar.gz: 0d4840867f0454b51b82f64807b691c4a4e4ac1380f5de113862ca8964f12fd18a62e0accaa51fad1105b94be403659ed5a80f486b4fc3ad4a1cddaf72abbf59
@@ -7,14 +7,11 @@ module Low
7
7
  class Loader
8
8
  class << self
9
9
  def load(klass:, class_proxy:)
10
- adaptor = nil
11
-
12
10
  ancestors = klass.ancestors.map(&:to_s)
13
- adaptor = Sinatra.new(klass:, class_proxy:) if ancestors.include?('Sinatra::Base')
14
11
 
15
- return if adaptor.nil?
12
+ return unless ancestors.include?('Sinatra::Base')
16
13
 
17
- adaptor
14
+ klass.prepend SinatraAdapter.new.module(file_path: class_proxy.file_path)
18
15
  end
19
16
  end
20
17
  end
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'prism'
4
+ require 'lowkey'
4
5
 
5
- require_relative '../factories/proxy_factory'
6
6
  require_relative '../interfaces/adapter_interface'
7
7
  require_relative '../proxies/return_proxy'
8
8
  require_relative '../types/error_types'
@@ -10,66 +10,38 @@ require_relative '../types/error_types'
10
10
  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
- class Sinatra < AdapterInterface
14
- def initialize(klass:, class_proxy:)
15
- @klass = klass
16
- @class_proxy = class_proxy
17
- @file_path = class_proxy.file_path
18
- end
19
-
20
- def process # rubocop:disable Metrics/AbcSize
21
- method_calls = @class_proxy.method_calls(%i[get post patch put delete options query])
22
-
23
- # Type check return values.
24
- method_calls.each do |method_node|
25
- arguments_node = method_node.compact_child_nodes.first
26
- next unless arguments_node.is_a?(Prism::ArgumentsNode)
27
-
28
- pattern = arguments_node.arguments.first.content
29
- name = "#{method_node.name.upcase} #{pattern}"
30
- scope = name
31
- start_line = method_node.start_line
32
-
33
- next unless (return_proxy = ProxyFactory.return_proxy(method_node:, name:, file_path:, scope: pattern))
34
-
35
- route = "#{method_node.name.upcase} #{pattern}"
36
- name = method_node.name
37
- param_proxies = [ParamProxy.new(expression: nil, name: :route, type: :req, file_path:, start_line:, scope:, position: 0)]
38
- @klass.low_methods[route] = MethodProxy.new(file_path:, start_line:, scope:, name:, param_proxies:, return_proxy:)
39
- end
40
- end
41
-
42
- private
43
-
44
- attr_reader :file_path
45
- end
46
-
47
- module Methods
48
- # Unfortunately overriding invoke() is the best way to validate types for now. Though direct it's also very compute efficient.
49
- # I originally tried an after filter and it mostly worked but it only had access to Response which isn't the raw return value.
50
- # I suggest that Sinatra provide a hook that allows us to access the raw return value of a route before it becomes a Response.
51
- def invoke(&block)
52
- res = catch(:halt, &block)
53
-
54
- low_validate!(value: res) if res
55
-
56
- res = [res] if res.is_a?(Integer) || res.is_a?(String)
57
- if res.is_a?(::Array) && res.first.is_a?(Integer)
58
- res = res.dup
59
- status(res.shift)
60
- body(res.pop)
61
- headers(*res)
62
- elsif res.respond_to? :each
63
- body res
64
- end
65
-
66
- nil # avoid double setting the same response tuple twice
67
- end
68
-
69
- def low_validate!(value:)
70
- route = "#{request.request_method} #{request.path}"
71
- if (method_proxy = self.class.low_methods[route]) && (proxy = method_proxy.return_proxy)
72
- proxy.type_expression.validate!(value:, proxy:)
13
+ class SinatraAdapter < AdapterInterface
14
+ def module(file_path:) # rubocop:disable Metrics/AbcSize
15
+ Module.new do
16
+ @@file_path = file_path # rubocop:disable Style/ClassVars
17
+
18
+ # Unfortunately overriding invoke() is the best way to validate types for now. Though direct it's also very compute efficient.
19
+ # 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
+ # I suggest that Sinatra provide a hook that allows us to access the raw return value of a route before it becomes a Response.
21
+ def invoke(&block)
22
+ res = catch(:halt, &block)
23
+
24
+ lowtype_validate!(value: res) if res
25
+
26
+ res = [res] if res.is_a?(Integer) || res.is_a?(String)
27
+ if res.is_a?(::Array) && res.first.is_a?(Integer)
28
+ res = res.dup
29
+ status(res.shift)
30
+ body(res.pop)
31
+ headers(*res)
32
+ elsif res.respond_to? :each
33
+ body res
34
+ end
35
+
36
+ nil # avoid double setting the same response tuple twice
37
+ end
38
+
39
+ def lowtype_validate!(value:)
40
+ route = "#{request.request_method} #{request.path}"
41
+ if (method_proxy = Lowkey[@@file_path][self.class.name][route]) && (proxy = method_proxy.return_proxy)
42
+ proxy.expression.validate!(value:, proxy:)
43
+ end
44
+ end
73
45
  end
74
46
  end
75
47
  end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'expressions'
4
+ require 'lowkey'
5
+
6
+ require_relative '../expressions/expression_helpers'
7
+ require_relative '../expressions/type_expression'
8
+ require_relative '../syntax/syntax'
9
+ require_relative '../types/complex_types'
10
+ require_relative '../types/status'
11
+
12
+ module Low
13
+ # Evaluate code stored in strings into constants and values.
14
+ # ┌────────┐ ┌─────────┐ ┌─────────────┐ ┌─────────┐ ┌─────────┐
15
+ # │ Lowkey │ │ Proxies │ │ Expressions │ │ LowType │ │ Methods │
16
+ # └────┬───┘ └────┬────┘ └──────┬──────┘ └────┬────┘ └────┬────┘
17
+ # │ │ │ │ │
18
+ # │ Parses AST │ │ │ │
19
+ # ├─────────────►│ │ │ │
20
+ # │ │ │ │ │
21
+ # │ │ Stores │ │ │
22
+ # │ ├────────────────►│ │ │
23
+ # │ │ │ │ │
24
+ # │ │ │ Evaluates <-- YOU ARE HERE. |
25
+ # │ │ │◄────────────────┤ │
26
+ # │ │ │ │ │
27
+ # │ │ │ │ Redefines │
28
+ # │ │ │ ├──────────────►│
29
+ # │ │ │ │ │
30
+ # │ │ │ Validates │ │
31
+ # │ │ │◄┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┤
32
+ # │ │ │ │ │
33
+ class Evaluator
34
+ include ExpressionHelpers
35
+ include Types
36
+ using LowType::Syntax
37
+
38
+ def instance_evaluate(proxy:)
39
+ # Not a security risk because the code comes from a trusted source; the file that included lowtype.
40
+ eval(proxy.value, binding, proxy.file_path, proxy.start_line) # rubocop:disable Security/Eval
41
+ end
42
+
43
+ class << self
44
+ def evaluate(method_proxies:)
45
+ require_relative '../syntax/union_types' if LowType.config.union_type_expressions
46
+
47
+ method_proxies.each_value do |method_proxy|
48
+ evaluate_param_proxy_expressions(method_proxy:)
49
+ evaluate_return_proxy_expression(return_proxy: method_proxy.return_proxy) if method_proxy.return_proxy
50
+ end
51
+ end
52
+
53
+ def evaluate_param_proxy_expressions(method_proxy:)
54
+ begin # rubocop:disable Style/RedundantBegin
55
+ method_proxy.tagged_params(:value).each do |param_proxy|
56
+ # TODO: Evaluate in the binding of the class that included LowType if not a type managed by LowType.
57
+ expression = new.instance_evaluate(proxy: param_proxy)
58
+ param_proxy.expression = cast_type_expression(expression:, method_proxy:)
59
+ end
60
+ rescue NameError
61
+ mp = method_proxy
62
+ raise NameError, "Unknown type '#{mp.value}' for #{mp.scope} at #{mp.file_path}:#{mp.start_line}"
63
+ end
64
+ end
65
+
66
+ def evaluate_return_proxy_expression(return_proxy:)
67
+ begin
68
+ expression = new.instance_evaluate(proxy: return_proxy)
69
+ rescue NameError
70
+ rp = return_proxy
71
+ raise NameError, "Unknown return type '#{rp.value}' for #{rp.scope} at #{rp.file_path}:#{rp.start_line}"
72
+ end
73
+
74
+ expression = TypeExpression.new(type: expression) unless expression.is_a?(TypeExpression)
75
+
76
+ return_proxy.expression = expression
77
+ end
78
+
79
+ private
80
+
81
+ def cast_type_expression(expression:, method_proxy:)
82
+ if expression.is_a?(::Expressions::Expression)
83
+ return expression
84
+ elsif expression.instance_of?(Class) && expression.name == 'Low::Dependency'
85
+ return expression.new(provider_key: method_proxy.name)
86
+ elsif TypeQuery.type?(expression)
87
+ return TypeExpression.new(type: expression)
88
+ end
89
+
90
+ nil
91
+ end
92
+ end
93
+ end
94
+ end
@@ -1,35 +1,46 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../expressions/value_expression'
4
- require_relative '../factories/proxy_factory'
5
- require_relative '../proxies/method_proxy'
6
- require_relative '../queries/type_query'
7
- require_relative 'repository'
4
+ require_relative '../definitions/evaluator'
8
5
 
9
6
  module Low
10
7
  # Redefine methods to have their arguments and return values type checked.
8
+ # ┌────────┐ ┌─────────┐ ┌─────────────┐ ┌─────────┐ ┌─────────┐
9
+ # │ Lowkey │ │ Proxies │ │ Expressions │ │ LowType │ │ Methods │
10
+ # └────┬───┘ └────┬────┘ └──────┬──────┘ └────┬────┘ └────┬────┘
11
+ # │ │ │ │ │
12
+ # │ Parses AST │ │ │ │
13
+ # ├─────────────►│ │ │ │
14
+ # │ │ │ │ │
15
+ # │ │ Stores │ │ │
16
+ # │ ├────────────────►│ │ │
17
+ # │ │ │ │ │
18
+ # │ │ │ Evaluates │ │
19
+ # │ │ │◄────────────────┤ │
20
+ # │ │ │ │ │
21
+ # │ │ │ │ Redefines <-- YOU ARE HERE.
22
+ # │ │ │ ├──────────────►│
23
+ # │ │ │ │ │
24
+ # │ │ │ Validates │ │
25
+ # │ │ │◄┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┤
26
+ # │ │ │ │ │
11
27
  class Redefiner
12
28
  class << self
13
- def redefine(method_nodes:, class_proxy:, klass:)
14
- method_proxies = build_methods(method_nodes:, klass:, file_path: class_proxy.file_path)
15
-
29
+ # TODO: Pass in "klass" and use it to class_eval/eval methods in the binding of the class that included LowType.
30
+ def redefine(method_proxies:, class_proxy:)
16
31
  if LowType.config.type_checking
17
- typed_methods(method_proxies:, class_proxy:, klass:)
32
+ typed_methods(method_proxies:, class_proxy:)
18
33
  else
19
- untyped_methods(method_proxies:, class_proxy:, klass:)
34
+ untyped_methods(method_proxies:, class_proxy:)
20
35
  end
21
36
  end
22
37
 
23
- def redefinable?(method_proxy:, class_proxy:, klass:)
24
- method_has_types?(method_proxy:, klass:) && method_within_class_bounds?(method_proxy:, class_proxy:, klass:)
25
- end
26
-
27
38
  def untyped_args(args:, kwargs:, method_proxy:) # rubocop:disable Metrics/AbcSize
28
- method_proxy.param_proxies.each do |param_proxy|
39
+ method_proxy.params_with_expressions.each do |param_proxy|
29
40
  value = param_proxy.position ? args[param_proxy.position] : kwargs[param_proxy.name]
30
41
 
31
42
  next unless value.nil?
32
- raise param_proxy.error_type, param_proxy.error_message(value:) if param_proxy.required?
43
+ raise param_proxy.error_type, param_proxy.error_message(value:) if param_proxy.expression.required?
33
44
 
34
45
  value = param_proxy.expression.default_value # Default value can still be `nil`.
35
46
  value = value.value if value.is_a?(ValueExpression)
@@ -41,96 +52,51 @@ module Low
41
52
 
42
53
  private
43
54
 
44
- def build_methods(method_nodes:, klass:, file_path:)
45
- method_nodes.each do |name, method_node|
46
- begin # rubocop:disable Style/RedundantBegin
47
- name = method_node.name
48
- scope = name
49
-
50
- param_proxies = ProxyFactory.param_proxies(method_node:, file_path:, scope:)
51
- return_proxy = ProxyFactory.return_proxy(method_node:, name:, file_path:, scope:)
52
- method_proxy = MethodProxy.new(file_path:, start_line: method_node.start_line, scope:, name:, param_proxies:, return_proxy:)
53
-
54
- Repository.save(method: method_proxy, klass:)
55
- # When we can't parse the method's params or return type then skip it.
56
- rescue SyntaxError
57
- next
58
- end
59
- end
60
-
61
- Repository.all(klass:)
62
- end
63
-
64
- def typed_methods(method_proxies:, class_proxy:, klass:) # rubocop:disable Metrics
55
+ def typed_methods(method_proxies:, class_proxy:) # rubocop:disable Metrics
65
56
  Module.new do
66
- method_proxies.each do |name, method_proxy|
67
- next unless Low::Redefiner.redefinable?(method_proxy:, class_proxy:, klass:)
68
-
69
- # You are now in the binding of the includer class (`name` is also available here).
70
- define_method(name) do |*args, **kwargs|
71
- # Inlined version of Repository.load() for performance increase.
72
- method_proxy = instance_of?(Class) ? low_methods[name] : self.class.low_methods[name] || Object.low_methods[name]
73
-
74
- method_proxy.param_proxies.each do |param_proxy|
75
- value = param_proxy.position ? args[param_proxy.position] : kwargs[param_proxy.name]
76
- value = param_proxy.expression.default_value if value.nil? && !param_proxy.required?
57
+ method_proxies.values.filter(&:expressions?).each do |method_proxy|
58
+ define_method(method_proxy.name) do |*args, **kwargs|
59
+ method_proxy.params_with_expressions.each do |param_proxy|
60
+ positional = %i[pos_req pos_opt].include?(param_proxy.type)
77
61
 
62
+ value = positional ? args[param_proxy.position] : kwargs[param_proxy.name]
63
+ value = param_proxy.expression.default_value if value.nil? && !param_proxy.expression.required?
78
64
  param_proxy.expression.validate!(value:, proxy: param_proxy)
79
65
  value = value.value if value.is_a?(ValueExpression)
80
- param_proxy.position ? args[param_proxy.position] = value : kwargs[param_proxy.name] = value
66
+
67
+ positional ? args[param_proxy.position] = value : kwargs[param_proxy.name] = value
81
68
  end
82
69
 
83
70
  if (return_proxy = method_proxy.return_proxy)
84
71
  return_value = super(*args, **kwargs)
85
- return_proxy.type_expression.validate!(value: return_value, proxy: return_proxy)
72
+ return_proxy.expression.validate!(value: return_value, proxy: return_proxy)
86
73
  return return_value
87
74
  end
88
75
 
89
76
  super(*args, **kwargs)
90
77
  end
91
78
 
92
- private name if class_proxy.private_start_line && method_proxy.start_line > class_proxy.private_start_line
79
+ private method_proxy.name if class_proxy.private_start_line && method_proxy.start_line > class_proxy.private_start_line
93
80
  end
94
81
  end
95
82
  end
96
83
 
97
- def untyped_methods(method_proxies:, class_proxy:, klass:)
84
+ def untyped_methods(method_proxies:, class_proxy:)
98
85
  Module.new do
99
- method_proxies.each do |name, method_proxy|
100
- next unless Low::Redefiner.redefinable?(method_proxy:, class_proxy:, klass:)
101
-
102
- # You are now in the binding of the includer class (`name` is also available here).
103
- define_method(name) do |*args, **kwargs|
86
+ method_proxies.values.filter(&:expressions?).each do |method_proxy|
87
+ # You are now in the binding of the includer class.
88
+ define_method(method_proxy.name) do |*args, **kwargs|
104
89
  # NOTE: Type checking is currently disabled. See 'config.type_checking'.
105
- method_proxy = instance_of?(Class) ? low_methods[name] : self.class.low_methods[name] || Object.low_methods[name]
90
+ method_proxy = Lowkey[class_proxy.file_path][class_proxy.namespace][__method__]
91
+
106
92
  args, kwargs = Low::Redefiner.untyped_args(args:, kwargs:, method_proxy:)
107
93
  super(*args, **kwargs)
108
94
  end
109
95
 
110
- private name if class_proxy.private_start_line && method_proxy.start_line > class_proxy.private_start_line
96
+ private method_proxy.name if class_proxy.private_start_line && method_proxy.start_line > class_proxy.private_start_line
111
97
  end
112
98
  end
113
99
  end
114
-
115
- def method_has_types?(method_proxy:, klass:)
116
- if method_proxy.param_proxies == [] && method_proxy.return_proxy.nil?
117
- Low::Repository.delete(name: method_proxy.name, klass:)
118
- return false
119
- end
120
-
121
- true
122
- end
123
-
124
- def method_within_class_bounds?(method_proxy:, class_proxy:, klass:)
125
- within_bounds = method_proxy.start_line > class_proxy.start_line && method_proxy.start_line <= class_proxy.end_line
126
-
127
- unless within_bounds
128
- Low::Repository.delete(name: method_proxy.name, klass:)
129
- return false
130
- end
131
-
132
- true
133
- end
134
100
  end
135
101
  end
136
102
  end
@@ -9,37 +9,35 @@ module Low
9
9
  def type_reader(named_expressions)
10
10
  named_expressions.each do |name, exp|
11
11
  last_caller = caller_locations(1, 1).first
12
+
12
13
  file_path = last_caller.path
13
14
  start_line = last_caller.lineno
14
15
  scope = "#{self}##{name}"
15
16
 
16
- type_expression = type_expression(exp)
17
- return_proxy = ReturnProxy.new(type_expression:, name:, file_path:, start_line:, scope:)
18
-
19
- @low_methods[name] = MethodProxy.new(file_path:, start_line:, scope:, name:, return_proxy:)
17
+ expression = cast_type_expression(exp)
18
+ proxy = ::Lowkey::ReturnProxy.new(file_path:, start_line:, scope:, name:, expression:)
20
19
 
21
20
  define_method(name) do
22
- method_proxy = self.class.low_methods[name]
23
21
  value = instance_variable_get("@#{name}")
24
- type_expression.validate!(value:, proxy: method_proxy.return_proxy)
22
+ expression.validate!(value:, proxy:)
25
23
  value
26
24
  end
27
25
  end
28
26
  end
29
27
 
30
- def type_writer(named_expressions) # rubocop:disable Metrics/AbcSize
28
+ def type_writer(named_expressions)
31
29
  named_expressions.each do |name, expression|
32
30
  last_caller = caller_locations(1, 1).first
31
+
33
32
  file_path = last_caller.path
34
33
  start_line = last_caller.lineno
35
34
  scope = "#{self}##{name}"
36
35
 
37
- param_proxies = [ParamProxy.new(expression: type_expression(expression), name:, type: :hashreq, file_path:, start_line:, scope:)]
38
- @low_methods["#{name}="] = MethodProxy.new(file_path:, start_line:, scope:, name:, param_proxies:)
36
+ expression = cast_type_expression(expression)
37
+ proxy = ::Lowkey::ParamProxy.new(file_path:, start_line:, scope:, name:, type: :key_req, expression:)
39
38
 
40
39
  define_method("#{name}=") do |value|
41
- method_proxy = self.class.low_methods["#{name}="]
42
- method_proxy.param_proxies.first.expression.validate!(value:, proxy: method_proxy.param_proxies.first)
40
+ expression.validate!(value:, proxy:)
43
41
  instance_variable_set("@#{name}", value)
44
42
  end
45
43
  end
@@ -54,10 +52,10 @@ module Low
54
52
 
55
53
  private
56
54
 
57
- def type_expression(expression)
55
+ def cast_type_expression(expression)
58
56
  if expression.is_a?(::Expressions::Expression)
59
57
  expression
60
- elsif ::Low::TypeQuery.type?(expression)
58
+ elsif TypeQuery.type?(expression)
61
59
  TypeExpression.new(type: expression)
62
60
  end
63
61
  end
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../factories/expression_factory'
3
+ require_relative 'type_expression'
4
+ require_relative 'value_expression'
4
5
  require_relative '../proxies/local_proxy'
5
- require_relative '../types/error_types'
6
6
 
7
7
  module Low
8
- module Expressions
8
+ module ExpressionHelpers
9
9
  def type(type_expression)
10
10
  value = type_expression.default_value
11
11
 
@@ -20,12 +20,12 @@ module Low
20
20
 
21
21
  value
22
22
  rescue NoMethodError
23
- raise ConfigError, "Invalid type expression, likely because you didn't add 'using LowType::Syntax'"
23
+ raise ConfigError, "Invalid type expression. Did you add 'using LowType::Syntax'?"
24
24
  end
25
25
  alias low_type type
26
26
 
27
27
  def value(type)
28
- ExpressionFactory.type_expression_with_value(type:)
28
+ TypeExpression.new(default_value: ValueExpression.new(value: type))
29
29
  end
30
30
  alias low_value value
31
31
  end
@@ -22,7 +22,7 @@ module Low
22
22
  @types << type unless type.nil?
23
23
  @default_value = default_value
24
24
  # TODO: Override per type expression with a config expression.
25
- @deep_type_check = LowType.config.deep_type_check
25
+ @deep_type_check = nil
26
26
  end
27
27
 
28
28
  def required?
@@ -76,7 +76,7 @@ module Low
76
76
 
77
77
  # Override Expressions as LowType supports complex types which are implemented as values.
78
78
  def value?(expression)
79
- ::Low::TypeQuery.value?(expression) || expression.nil?
79
+ TypeQuery.value?(expression) || expression.nil?
80
80
  end
81
81
 
82
82
  def valid_subtype(subtype:)
@@ -141,7 +141,9 @@ module Low
141
141
  end
142
142
 
143
143
  def deep_type_check?
144
- @deep_type_check || LowType.config.deep_type_check || false
144
+ return @deep_type_check unless @deep_type_check.nil?
145
+
146
+ LowType.config.deep_type_check
145
147
  end
146
148
  end
147
149
  end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # A value expression converts a type to a value in the eyes of LowType.
3
+ # A value expression presents a type as a value:
4
+ # 1. It is an instance
5
+ # 2. It has a class method
4
6
  class ValueExpression
5
7
  attr_reader :value
6
8
 
@@ -2,8 +2,8 @@
2
2
 
3
3
  module Low
4
4
  class AdapterInterface
5
- def process
6
- raise NotImplementedError
5
+ def module
6
+ nil
7
7
  end
8
8
  end
9
9
  end
@@ -2,18 +2,7 @@
2
2
 
3
3
  module Low
4
4
  # Used by proxies to output errors.
5
- class ErrorInterface
6
- attr_reader :file_path, :start_line, :scope
7
-
8
- def initialize(file_path:, start_line:, scope:)
9
- @file_path = file_path
10
- @start_line = start_line
11
- @scope = scope
12
-
13
- @output_mode = LowType.config.output_mode
14
- @output_size = LowType.config.output_size
15
- end
16
-
5
+ module ErrorHandling
17
6
  def error_type
18
7
  raise NotImplementedError
19
8
  end
@@ -23,12 +12,12 @@ module Low
23
12
  end
24
13
 
25
14
  def output(value:)
26
- case @output_mode
15
+ case LowType.config.output_mode
27
16
  when :type
28
17
  # TODO: Show full type structure in error output instead of just the type of the supertype.
29
18
  value.class
30
19
  when :value
31
- value.inspect[0...@output_size]
20
+ value.inspect[0...LowType.config.output_size]
32
21
  else
33
22
  'REDACTED'
34
23
  end
data/lib/low_type.rb CHANGED
@@ -5,38 +5,49 @@ require 'lowkey'
5
5
  require_relative 'adapters/adapter_loader'
6
6
  require_relative 'definitions/redefiner'
7
7
  require_relative 'definitions/type_accessors'
8
- require_relative 'expressions/expressions'
8
+ require_relative 'expressions/expression_helpers'
9
9
  require_relative 'queries/file_query'
10
10
  require_relative 'syntax/syntax'
11
11
  require_relative 'types/complex_types'
12
12
 
13
+ # Architecture:
14
+ # ┌────────┐ ┌─────────┐ ┌─────────────┐ ┌─────────┐ ┌─────────┐
15
+ # │ Lowkey │ │ Proxies │ │ Expressions │ │ LowType │ │ Methods │
16
+ # └────┬───┘ └────┬────┘ └──────┬──────┘ └────┬────┘ └────┬────┘
17
+ # │ │ │ │ │
18
+ # │ Parses AST │ │ │ │
19
+ # ├─────────────►│ │ │ │
20
+ # │ │ │ │ │
21
+ # │ │ Stores │ │ │
22
+ # │ ├────────────────►│ │ │
23
+ # │ │ │ │ │
24
+ # │ │ │ Evaluates │ │
25
+ # │ │ │◄────────────────┤ │
26
+ # │ │ │ │ │
27
+ # │ │ │ │ Redefines │
28
+ # │ │ │ ├──────────────►│
29
+ # │ │ │ │ │
30
+ # │ │ │ Validates │ │
31
+ # │ │ │◄┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┤
32
+ # │ │ │ │ │
13
33
  module LowType
14
- # We do as much as possible on class load rather than on instantiation to be thread-safe and efficient.
15
- def self.included(klass) # rubocop:disable Metrics/AbcSize
16
- require_relative 'syntax/union_types' if LowType.config.union_type_expressions
17
-
18
- class << klass
19
- def low_methods
20
- @low_methods ||= {}
21
- end
22
- end
23
-
34
+ # We do as much as possible on class load rather than on object instantiation to be thread-safe and efficient.
35
+ def self.included(klass)
24
36
  file_path = Low::FileQuery.file_path(klass:)
25
- return unless File.exist?(file_path)
26
-
27
37
  file_proxy = Lowkey.load(file_path:)
28
- class_proxy = file_proxy.definitions[klass.name]
38
+ class_proxy = file_proxy[klass.name]
39
+
40
+ Low::Evaluator.evaluate(method_proxies: class_proxy.keyed_methods)
29
41
 
42
+ klass.include Low::ExpressionHelpers
43
+ klass.extend Low::ExpressionHelpers
30
44
  klass.extend Low::TypeAccessors
31
- klass.include Low::Types
32
- klass.include Low::Expressions
33
- klass.prepend Low::Redefiner.redefine(method_nodes: class_proxy.instance_methods, class_proxy:, klass:)
34
- klass.singleton_class.prepend Low::Redefiner.redefine(method_nodes: class_proxy.class_methods, class_proxy:, klass:)
35
-
36
- if (adapter = Low::Adapter::Loader.load(klass:, class_proxy:))
37
- adapter.process
38
- klass.prepend Low::Adapter::Methods
39
- end
45
+ klass.extend Low::Types
46
+
47
+ klass.prepend Low::Redefiner.redefine(method_proxies: class_proxy.instance_methods, class_proxy:)
48
+ klass.singleton_class.prepend Low::Redefiner.redefine(method_proxies: class_proxy.class_methods, class_proxy:)
49
+
50
+ Low::Adapter::Loader.load(klass:, class_proxy:)
40
51
  end
41
52
 
42
53
  class << self
@@ -49,7 +60,7 @@ module LowType
49
60
  :deep_type_check,
50
61
  :union_type_expressions
51
62
  )
52
- @config ||= config.new(true, :error, :type, 100, false, true)
63
+ @config ||= config.new(true, :error, :type, 100, true, true)
53
64
  end
54
65
 
55
66
  def configure
@@ -1,14 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../interfaces/error_interface'
3
+ require_relative '../interfaces/error_handling'
4
4
  require_relative '../types/error_types'
5
5
 
6
6
  module Low
7
- class LocalProxy < ErrorInterface
8
- attr_reader :type_expression, :name
7
+ class LocalProxy
8
+ include ErrorHandling
9
+
10
+ attr_reader :type_expression, :name, :file_path, :start_line, :scope
9
11
 
10
12
  def initialize(type_expression:, name:, file_path:, start_line:, scope:)
11
- super(file_path:, start_line:, scope:)
13
+ @file_path = file_path
14
+ @start_line = start_line
15
+ @scope = scope
12
16
 
13
17
  @type_expression = type_expression
14
18
  @name = name
@@ -1,28 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../interfaces/error_interface'
4
- require_relative '../types/error_types'
5
-
6
- module Low
7
- class ParamProxy < ErrorInterface
8
- attr_reader :expression, :name, :type, :position
9
-
10
- # TODO: Refactor file path, start line and scope into "meta scope" model.
11
- def initialize(expression:, name:, type:, file_path:, start_line:, scope:, position: nil) # rubocop:disable Metrics/ParameterLists
12
- super(file_path:, start_line:, scope:)
3
+ require 'lowkey'
13
4
 
14
- @expression = expression
15
- @name = name
16
- @type = type
17
- @position = position
18
- end
5
+ require_relative '../interfaces/error_handling'
6
+ require_relative '../types/error_types'
19
7
 
20
- def required?
21
- @expression.required?
22
- end
8
+ module ::Lowkey
9
+ class ParamProxy
10
+ include ::Low::ErrorHandling
23
11
 
24
12
  def error_type
25
- ArgumentTypeError
13
+ ::Low::ArgumentTypeError
26
14
  end
27
15
 
28
16
  def error_message(value:)
@@ -1,25 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../interfaces/error_interface'
4
- require_relative '../types/error_types'
5
-
6
- module Low
7
- class ReturnProxy < ErrorInterface
8
- attr_reader :type_expression, :name
3
+ require 'lowkey'
9
4
 
10
- def initialize(type_expression:, name:, file_path:, start_line:, scope:)
11
- super(file_path:, start_line:, scope:)
5
+ require_relative '../interfaces/error_handling'
6
+ require_relative '../types/error_types'
12
7
 
13
- @type_expression = type_expression
14
- @name = name
15
- end
8
+ module ::Lowkey
9
+ class ReturnProxy
10
+ include ::Low::ErrorHandling
16
11
 
17
12
  def error_type
18
- ReturnTypeError
13
+ ::Low::ReturnTypeError
19
14
  end
20
15
 
21
16
  def error_message(value:)
22
- "Invalid return type '#{output(value:)}' for method '#{@name}'. Valid types: '#{@type_expression.valid_types}'"
17
+ "Invalid return type '#{output(value:)}' for method '#{@name}'. Valid types: '#{@expression.valid_types}'"
23
18
  end
24
19
  end
25
20
  end
@@ -1,11 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Low
4
+ class MissingFileError < StandardError; end
5
+
4
6
  class FileQuery
5
7
  class << self
6
8
  def file_path(klass:)
7
9
  includer_line = line_from_class(klass:) || line_from_include || ''
8
- includer_line.split(':').first || ''
10
+ file_path = includer_line.split(':').first || ''
11
+
12
+ return file_path if File.exist?(file_path)
13
+
14
+ raise MissingFileError, "No file found at path '#{file_path}'"
9
15
  end
10
16
 
11
17
  private
data/lib/version.rb CHANGED
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Low
4
4
  module Type
5
- VERSION = '1.1.9'
5
+ VERSION = '1.1.10'
6
6
  end
7
7
  end
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: 1.1.9
4
+ version: 1.1.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - maedi
@@ -29,14 +29,14 @@ dependencies:
29
29
  requirements:
30
30
  - - "~>"
31
31
  - !ruby/object:Gem::Version
32
- version: '0.2'
32
+ version: '0.3'
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: '0.2'
39
+ version: '0.3'
40
40
  description: An elegant and simple way to define types in Ruby, only when you need
41
41
  them.
42
42
  email:
@@ -47,20 +47,17 @@ extra_rdoc_files: []
47
47
  files:
48
48
  - lib/adapters/adapter_loader.rb
49
49
  - lib/adapters/sinatra_adapter.rb
50
+ - lib/definitions/evaluator.rb
50
51
  - lib/definitions/redefiner.rb
51
- - lib/definitions/repository.rb
52
52
  - lib/definitions/type_accessors.rb
53
- - lib/expressions/expressions.rb
53
+ - lib/expressions/expression_helpers.rb
54
54
  - lib/expressions/type_expression.rb
55
55
  - lib/expressions/value_expression.rb
56
- - lib/factories/expression_factory.rb
57
- - lib/factories/proxy_factory.rb
58
56
  - lib/factories/type_factory.rb
59
57
  - lib/interfaces/adapter_interface.rb
60
- - lib/interfaces/error_interface.rb
58
+ - lib/interfaces/error_handling.rb
61
59
  - lib/low_type.rb
62
60
  - lib/proxies/local_proxy.rb
63
- - lib/proxies/method_proxy.rb
64
61
  - lib/proxies/param_proxy.rb
65
62
  - lib/proxies/return_proxy.rb
66
63
  - lib/queries/file_query.rb
@@ -1,32 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Low
4
- class Repository
5
- class << self
6
- def all(klass:)
7
- klass.low_methods
8
- end
9
-
10
- def save(method:, klass:)
11
- klass.low_methods[method.name] = method
12
- end
13
-
14
- def delete(name:, klass:)
15
- klass.low_methods.delete(name)
16
- end
17
-
18
- # Redefiner inlines this method in define_method() for better performance. TODO: Test this assumption.
19
- def load(name:, object:)
20
- singleton(object:).low_methods[name]
21
- end
22
-
23
- # TODO: export() to RBS
24
-
25
- private
26
-
27
- def singleton(object:)
28
- object.instance_of?(Class) ? object : object.class || Object
29
- end
30
- end
31
- end
32
- end
@@ -1,14 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative '../expressions/type_expression'
4
- require_relative '../expressions/value_expression'
5
-
6
- module Low
7
- class ExpressionFactory
8
- class << self
9
- def type_expression_with_value(type:)
10
- TypeExpression.new(default_value: ValueExpression.new(value: type))
11
- end
12
- end
13
- end
14
- end
@@ -1,114 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'expressions'
4
- require 'lowkey'
5
-
6
- require_relative '../expressions/expressions'
7
- require_relative '../expressions/type_expression'
8
- require_relative '../proxies/param_proxy'
9
- require_relative '../proxies/return_proxy'
10
- require_relative '../syntax/syntax'
11
- require_relative '../types/complex_types'
12
- require_relative '../types/status'
13
-
14
- module Low
15
- class ProxyFactory
16
- using ::LowType::Syntax
17
-
18
- class << self
19
- include Low::Expressions
20
- include Low::Types
21
-
22
- # The evals below aren't a security risk because the code comes from a trusted source; the file itself that did the include.
23
- def param_proxies(method_node:, file_path:, scope:)
24
- return [] if method_node.parameters.nil?
25
-
26
- params_without_block = method_node.parameters.slice.delete_suffix(', &block')
27
-
28
- ruby_method = eval("-> (#{params_without_block}) {}", binding, __FILE__, __LINE__) # rubocop:disable Security/Eval
29
-
30
- # Local variable names are prefixed with __lt or __rb where needed to avoid being overridden by method parameters.
31
- typed_method = <<~RUBY
32
- -> (#{params_without_block}, __rb_method:, __lt_file:) {
33
- param_proxies_for_expressions(ruby_method: __rb_method, file_path: __lt_file, start_line: method_node.start_line, scope:, method_binding: binding)
34
- }
35
- RUBY
36
-
37
- required_args, required_kwargs = required_args(ruby_method:)
38
-
39
- # Called with only required args (as nil) and optional args omitted, to evaluate expressions stored as default values.
40
- eval(typed_method, binding, __FILE__, __LINE__) # rubocop:disable Security/Eval
41
- .call(*required_args, **required_kwargs, __rb_method: ruby_method, __lt_file: file_path)
42
-
43
- # TODO: Unit test this.
44
- rescue ArgumentError => e
45
- raise ArgumentError, "Incorrect param syntax: #{e.message}"
46
- end
47
-
48
- def return_proxy(method_node:, name:, file_path:, scope:)
49
- return_type = Lowkey::ClassProxy.return_type(method_node:)
50
- return nil if return_type.nil?
51
-
52
- start_line = method_node.start_line
53
-
54
- begin
55
- # Not a security risk because the code comes from a trusted source; the file that did the include. Does the file trust itself?
56
- expression = eval(return_type.slice, binding, __FILE__, __LINE__).call # rubocop:disable Security/Eval
57
- rescue NameError
58
- raise NameError, "Unknown return type '#{return_type.slice}' for #{scope} at #{file_path}:#{start_line}"
59
- end
60
-
61
- expression = TypeExpression.new(type: expression) unless expression.is_a?(TypeExpression)
62
-
63
- ReturnProxy.new(type_expression: expression, name:, file_path:, start_line:, scope:)
64
- end
65
-
66
- private
67
-
68
- def required_args(ruby_method:)
69
- required_args = []
70
- required_kwargs = {}
71
-
72
- ruby_method.parameters.each do |param|
73
- param_type, param_name = param
74
-
75
- case param_type
76
- when :req
77
- required_args << nil
78
- when :keyreq
79
- required_kwargs[param_name] = nil
80
- end
81
- end
82
-
83
- [required_args, required_kwargs]
84
- end
85
-
86
- def param_proxies_for_expressions(ruby_method:, file_path:, start_line:, scope:, method_binding:)
87
- param_proxies = []
88
-
89
- ruby_method.parameters.each_with_index do |param, position|
90
- type, name = param
91
-
92
- # We don't support splatted *positional and **keyword arguments as by definition they are untyped.
93
- next if type == :rest
94
-
95
- position = nil unless %i[opt req].include?(type)
96
- local_variable = method_binding.local_variable_get(name)
97
-
98
- expression = nil
99
- if local_variable.is_a?(::Expressions::Expression)
100
- expression = local_variable
101
- elsif local_variable.instance_of?(Class) && local_variable < ::Expressions::Expression
102
- expression = local_variable.new(provider_key: name)
103
- elsif ::Low::TypeQuery.type?(local_variable)
104
- expression = TypeExpression.new(type: local_variable)
105
- end
106
-
107
- param_proxies << ParamProxy.new(expression:, name:, type:, file_path:, start_line:, scope:, position:) if expression
108
- end
109
-
110
- param_proxies
111
- end
112
- end
113
- end
114
- end
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Low
4
- class MethodProxy
5
- attr_reader :file_path, :start_line, :scope, :name, :param_proxies, :return_proxy
6
-
7
- # TODO: Refactor file path, start line and scope into "meta scope" model.
8
- def initialize(file_path:, start_line:, scope:, name:, param_proxies: [], return_proxy: nil) # rubocop:disable Metrics/ParameterLists
9
- @file_path = file_path
10
- @start_line = start_line
11
- @scope = scope
12
-
13
- @name = name
14
- @param_proxies = param_proxies
15
- @return_proxy = return_proxy
16
- end
17
- end
18
- end