lowtype 1.3.2

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c777de40504eea68870df0ee7936de827b1e5a771aca97e40d1ef2b41d3e38ed
4
+ data.tar.gz: cab3b2a605413173922eee3680a18951f19ec28065e0eb320d5d649b99300c34
5
+ SHA512:
6
+ metadata.gz: 7c4391bb32203a889a6a88ebdbe2895fd19bf8f195caa2a6d890475a5312fbb4b3e184bd5ea7a6d0031d95246411b8c56cf8323f62774aeda7d78b958f6bd2f0
7
+ data.tar.gz: d958d6ccd1f5d6c8c328aef5d7a12b2a46dc75253f82ed0f26d31da29666aa81f6e49e06515be901f6bd1ee5c4781e048131ef5d72505fd753da9135a52dee05
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'sinatra_adapter'
4
+
5
+ module Low
6
+ module Adapter
7
+ class Loader
8
+ class << self
9
+ def load(klass:, class_proxy:)
10
+ ancestors = klass.ancestors.map(&:to_s)
11
+
12
+ return unless ancestors.include?('Sinatra::Base')
13
+
14
+ klass.prepend SinatraAdapter.new.module(file_path: class_proxy.file_path)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'prism'
4
+ require 'lowkey'
5
+
6
+ require_relative '../interfaces/adapter_interface'
7
+ require_relative '../proxies/return_proxy'
8
+ require_relative '../types/error_types'
9
+
10
+ module Low
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 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
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,104 @@
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 low_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
+ def class_evaluate(proxy:, class_binding:)
44
+ # Not a security risk because the code comes from a trusted source; the file that included lowtype.
45
+ eval(proxy.value, class_binding, proxy.file_path, proxy.start_line) # rubocop:disable Security/Eval
46
+ end
47
+
48
+ class << self
49
+ def evaluate(method_proxies:, class_binding: nil)
50
+ require_relative '../syntax/union_types' if LowType.config.union_type_expressions
51
+
52
+ method_proxies.each_value do |method_proxy|
53
+ evaluate_param_proxy_expressions(method_proxy:, class_binding:)
54
+ evaluate_return_proxy_expression(return_proxy: method_proxy.return_proxy) if method_proxy.return_proxy
55
+ end
56
+ end
57
+
58
+ def evaluate_param_proxy_expressions(method_proxy:, class_binding: nil)
59
+ begin # rubocop:disable Style/RedundantBegin
60
+ method_proxy.tagged_params(:value).each do |param_proxy|
61
+ expression = begin
62
+ new.low_evaluate(proxy: param_proxy)
63
+ rescue NameError
64
+ raise unless class_binding
65
+
66
+ new.class_evaluate(proxy: param_proxy, class_binding:)
67
+ end
68
+ param_proxy.expression = cast_type_expression(expression:, param_proxy:)
69
+ end
70
+ rescue NameError => e
71
+ mp = method_proxy
72
+ raise NameError, "Unknown type '#{e.name}' for #{mp.scope} at #{mp.file_path}:#{mp.start_line}"
73
+ end
74
+ end
75
+
76
+ def evaluate_return_proxy_expression(return_proxy:)
77
+ begin
78
+ expression = new.low_evaluate(proxy: return_proxy)
79
+ rescue NameError
80
+ rp = return_proxy
81
+ raise NameError, "Unknown return type '#{rp.value}' for #{rp.scope} at #{rp.file_path}:#{rp.start_line}"
82
+ end
83
+
84
+ expression = TypeExpression.new(type: expression) unless expression.is_a?(TypeExpression)
85
+
86
+ return_proxy.expression = expression
87
+ end
88
+
89
+ private
90
+
91
+ def cast_type_expression(expression:, param_proxy:)
92
+ if expression.is_a?(::Expressions::Expression)
93
+ return expression
94
+ elsif expression.instance_of?(Class) && expression.name == 'Low::Dependency'
95
+ return expression.new(provider_key: param_proxy.name)
96
+ elsif TypeQuery.type?(expression)
97
+ return TypeExpression.new(type: expression)
98
+ end
99
+
100
+ nil
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../expressions/value_expression'
4
+ require_relative '../definitions/evaluator'
5
+
6
+ module Low
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
+ # │ │ │ │ │
27
+ class Redefiner
28
+ class << self
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:)
31
+ if LowType.config.type_checking
32
+ typed_methods(method_proxies:, class_proxy:)
33
+ else
34
+ untyped_methods(method_proxies:, class_proxy:)
35
+ end
36
+ end
37
+
38
+ def untyped_args(args:, kwargs:, method_proxy:) # rubocop:disable Metrics/AbcSize
39
+ method_proxy.params_with_expressions.each do |param_proxy|
40
+ value = param_proxy.position ? args[param_proxy.position] : kwargs[param_proxy.name]
41
+
42
+ next unless value.nil?
43
+ raise param_proxy.error_type, param_proxy.error_message(value:) if param_proxy.expression.required?
44
+
45
+ value = param_proxy.expression.default_value # Default value can still be `nil`.
46
+ value = value.value if value.is_a?(ValueExpression)
47
+ param_proxy.position ? args[param_proxy.position] = value : kwargs[param_proxy.name] = value
48
+ end
49
+
50
+ [args, kwargs]
51
+ end
52
+
53
+ private
54
+
55
+ def typed_methods(method_proxies:, class_proxy:) # rubocop:disable Metrics
56
+ Module.new do
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)
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?
64
+
65
+ param_proxy.expression.validate!(value:, proxy: param_proxy)
66
+ value = value.value if value.is_a?(ValueExpression)
67
+
68
+ positional ? args[param_proxy.position] = value : kwargs[param_proxy.name] = value
69
+ end
70
+
71
+ if (return_proxy = method_proxy.return_proxy)
72
+ return_value = super(*args, **kwargs)
73
+ return_proxy.expression.validate!(value: return_value, proxy: return_proxy)
74
+ return return_value
75
+ end
76
+
77
+ super(*args, **kwargs)
78
+ end
79
+
80
+ private method_proxy.name if class_proxy.private_start_line && method_proxy.start_line > class_proxy.private_start_line
81
+ end
82
+ end
83
+ end
84
+
85
+ def untyped_methods(method_proxies:, class_proxy:)
86
+ Module.new do
87
+ method_proxies.values.filter(&:expressions?).each do |method_proxy|
88
+ # You are now in the binding of the includer class.
89
+ define_method(method_proxy.name) do |*args, **kwargs|
90
+ # NOTE: Type checking is currently disabled. See 'config.type_checking'.
91
+ method_proxy = Lowkey[class_proxy.file_path][class_proxy.namespace][__method__]
92
+
93
+ args, kwargs = Low::Redefiner.untyped_args(args:, kwargs:, method_proxy:)
94
+ super(*args, **kwargs)
95
+ end
96
+
97
+ private method_proxy.name if class_proxy.private_start_line && method_proxy.start_line > class_proxy.private_start_line
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../expressions/type_expression'
4
+ require_relative '../proxies/return_proxy'
5
+ require_relative '../queries/type_query'
6
+
7
+ module Low
8
+ # Usage:
9
+ #
10
+ # type_accessor name: String # => @name getter and @name=() setter
11
+ # type_accessor name: String | 'Cher'
12
+ #
13
+ # type_reader name: String
14
+ # type_reader name: String | 'Cher' # defaults to 'Cher' if nil
15
+ #
16
+ # type_writer name: String
17
+ # type_writer name: String | nil # accepts String or nil
18
+ #
19
+ module TypeAccessors
20
+ def type_reader(named_expressions)
21
+ named_expressions.each do |name, exp|
22
+ last_caller = caller_locations(1, 1).first
23
+
24
+ file_path = last_caller.path
25
+ scope = "#{self}##{name}"
26
+
27
+ expression = cast_type_expression(exp)
28
+ # Source usually defined on file load with access to lines in a source file, but type accessors are defined on class load.
29
+ source = ::Lowkey::Source.new(file_path:, scope:, lines: [], start_line: last_caller.lineno, end_line: last_caller.lineno)
30
+ proxy = ::Lowkey::ReturnProxy.new(name:, source:, expression:)
31
+
32
+ define_method(name) do
33
+ value = instance_variable_get("@#{name}")
34
+ expression.validate!(value:, proxy:)
35
+ value
36
+ end
37
+ end
38
+ end
39
+
40
+ def type_writer(named_expressions)
41
+ named_expressions.each do |name, expression|
42
+ last_caller = caller_locations(1, 1).first
43
+
44
+ file_path = last_caller.path
45
+ scope = "#{self}##{name}"
46
+
47
+ expression = cast_type_expression(expression)
48
+ # Source usually defined on file load with access to lines in a source file, but type accessors are defined on class load.
49
+ source = ::Lowkey::Source.new(file_path:, scope:, lines: [], start_line: last_caller.lineno, end_line: last_caller.lineno)
50
+ proxy = ::Lowkey::ParamProxy.new(name:, source:, type: :pos_req, position: nil, expression:)
51
+
52
+ define_method("#{name}=") do |value|
53
+ expression.validate!(value:, proxy:)
54
+ instance_variable_set("@#{name}", value)
55
+ end
56
+ end
57
+ end
58
+
59
+ def type_accessor(named_expressions)
60
+ named_expressions.each do |name, expression|
61
+ type_reader({ name => expression })
62
+ type_writer({ name => expression })
63
+ end
64
+ end
65
+
66
+ private
67
+
68
+ def cast_type_expression(expression)
69
+ if expression.is_a?(::Expressions::Expression)
70
+ expression
71
+ elsif TypeQuery.type?(expression)
72
+ TypeExpression.new(type: expression)
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'type_expression'
4
+ require_relative 'value_expression'
5
+ require_relative '../proxies/local_proxy'
6
+
7
+ module Low
8
+ module ExpressionHelpers
9
+ def type(type_expression)
10
+ value = type_expression.default_value
11
+
12
+ last_caller = caller_locations(1, 1).first
13
+ file_path = last_caller.path
14
+ start_line = last_caller.lineno
15
+ proxy = LocalProxy.new(type_expression:, name: self, file_path:, start_line:, scope: 'local type')
16
+
17
+ type_expression.validate!(value:, proxy:)
18
+
19
+ return value.value if value.is_a?(ValueExpression)
20
+
21
+ value
22
+ rescue NoMethodError
23
+ raise ConfigError, "Invalid type expression. Did you add 'using LowType::Syntax'?"
24
+ end
25
+ alias low_type type
26
+
27
+ def value(type)
28
+ TypeExpression.new(default_value: ValueExpression.new(value: type))
29
+ end
30
+ alias low_value value
31
+ end
32
+ end
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'expressions'
4
+
5
+ require_relative 'value_expression'
6
+ require_relative '../proxies/param_proxy'
7
+ require_relative '../queries/type_query'
8
+
9
+ module Low
10
+ root_path = File.expand_path(__dir__)
11
+ adapter_paths = Dir.chdir(root_path) { Dir.glob('adapters/*') }.map { |path| File.join(root_path, path) }
12
+ module_paths = %w[expressions/expressions instance_types redefiner].map { |path| File.join(root_path, "#{path}.rb") }
13
+
14
+ HIDDEN_PATHS = [File.expand_path(__FILE__), *adapter_paths, *module_paths].freeze
15
+
16
+ # Represent types and default values as a series of chainable expressions.
17
+ class TypeExpression < ::Expressions::Expression
18
+ attr_reader :types, :default_value
19
+
20
+ # @param type - A literal type or an instance representation of a typed structure.
21
+ def initialize(type: nil, default_value: :LOW_TYPE_UNDEFINED)
22
+ @types = []
23
+ @types << type unless type.nil?
24
+ @default_value = default_value
25
+ # TODO: Override per type expression with a config expression.
26
+ @deep_type_check = nil
27
+ end
28
+
29
+ def required?
30
+ @default_value == :LOW_TYPE_UNDEFINED
31
+ end
32
+
33
+ def validate!(value:, proxy:) # rubocop:disable Metrics
34
+ if value.nil?
35
+ return true if @default_value.nil?
36
+ raise proxy.error_type, proxy.error_message(value:) if required?
37
+ end
38
+
39
+ @types.each do |type|
40
+ return true if type_matches_value?(type:, value:, proxy:)
41
+ return true if type.is_a?(Array) && value.is_a?(Array) && array_types_match_values?(types: type, values: value, proxy:)
42
+ return true if type.is_a?(Hash) && value.is_a?(Hash) && hash_types_match_values?(types: type, values: value)
43
+ end
44
+
45
+ raise proxy.error_type, proxy.error_message(value:)
46
+ rescue proxy.error_type => e
47
+ raise proxy.error_type, e.message, proxy.backtrace(backtrace: e.backtrace, hidden_paths: HIDDEN_PATHS)
48
+ end
49
+
50
+ def valid_types
51
+ types = @types.map do |type|
52
+ if type.is_a?(Array)
53
+ "[#{type.map { |subtype| valid_subtype(subtype:) }.join(', ')}]"
54
+ else
55
+ type.inspect.to_s.delete_prefix('Low::Types::')
56
+ end
57
+ end
58
+
59
+ types << 'nil' if @default_value.nil?
60
+ types.join(' | ')
61
+ end
62
+
63
+ private
64
+
65
+ def union_expression(expression)
66
+ @types += expression.types
67
+ @default_value = expression.default_value
68
+ end
69
+
70
+ def union_type(type)
71
+ @types << type
72
+ end
73
+
74
+ def union_value(value)
75
+ @default_value = value
76
+ end
77
+
78
+ # Override Expressions as LowType supports complex types which are implemented as values.
79
+ def value?(expression)
80
+ TypeQuery.value?(expression) || expression.nil?
81
+ end
82
+
83
+ def valid_subtype(subtype:)
84
+ if subtype.is_a?(TypeExpression)
85
+ types = subtype.types
86
+ types << 'nil' if subtype.default_value.nil?
87
+ types.join(' | ')
88
+ else
89
+ subtype.to_s.delete_prefix('Low::Types::')
90
+ end
91
+ end
92
+
93
+ def array_types_match_values?(types:, values:, proxy:)
94
+ # [X, Y, Z] An arbitrary amount of elements are arbitrary types in an arbitrary order.
95
+ if types.length > 1
96
+ return multiple_types_match_values?(types:, values:, proxy:)
97
+ # [T] All elements are the same type.
98
+ elsif types.length == 1
99
+ return single_type_matches_values?(type: types.first, values:, proxy:)
100
+ end
101
+
102
+ # [] Misconfigured empty Array[] type.
103
+ true
104
+ end
105
+
106
+ def multiple_types_match_values?(types:, values:, proxy:)
107
+ types.each_with_index do |type, index|
108
+ return false unless type_matches_value?(type:, value: values[index], proxy:)
109
+ end
110
+
111
+ true
112
+ end
113
+
114
+ def single_type_matches_values?(type:, values:, proxy:)
115
+ # [V, ...] Type check all elements.
116
+ if deep_type_check?
117
+ return false if values.any? { |value| !type_matches_value?(type:, value:, proxy:) }
118
+ # [V] Type check the first element.
119
+ else
120
+ return false unless type_matches_value?(type:, value: values.first, proxy:)
121
+ end
122
+
123
+ true
124
+ end
125
+
126
+ def hash_types_match_values?(types:, values:)
127
+ return true if values.empty? && empty_hash_default_value?
128
+ return values.empty? if types.empty?
129
+ return false if values.empty?
130
+
131
+ # TODO: Shallow validation of hash could be made deeper with user config.
132
+ types.keys[0] == values.keys[0].class && types.values[0] == values.values[0].class
133
+ end
134
+
135
+ def type_matches_value?(type:, value:, proxy:)
136
+ if type.instance_of?(Class)
137
+ return type.match?(value:) if Low::TypeQuery.complex_type?(expression: type)
138
+ return value.value <= type if value.instance_of?(ValueExpression)
139
+
140
+ return value.is_a?(type)
141
+ elsif type.instance_of?(Low::TypeExpression)
142
+ type.validate!(value:, proxy:)
143
+ return true
144
+ end
145
+
146
+ false
147
+ end
148
+
149
+ def deep_type_check?
150
+ return @deep_type_check unless @deep_type_check.nil?
151
+
152
+ LowType.config.deep_type_check
153
+ end
154
+
155
+ def empty_hash_default_value?
156
+ @default_value.is_a?(Hash) && @default_value.empty?
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Low
4
+ # A value expression presents a type as a value:
5
+ # 1. It is an instance
6
+ # 2. It mimics the class method
7
+ class ValueExpression
8
+ attr_reader :value
9
+
10
+ def initialize(value:)
11
+ @value = value
12
+ end
13
+
14
+ def class
15
+ @value
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Low
4
+ class TypeFactory
5
+ class << self
6
+ def complex_type(parent_type)
7
+ Class.new(parent_type) do
8
+ def self.match?(value:)
9
+ return true if value.instance_of?(self.class) || value.instance_of?(superclass)
10
+
11
+ false
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Low
4
+ class AdapterInterface
5
+ def module
6
+ nil
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Low
4
+ # Used by proxies to output errors.
5
+ module ErrorHandling
6
+ def error_type
7
+ raise NotImplementedError
8
+ end
9
+
10
+ def error_message(value:)
11
+ raise NotImplementedError
12
+ end
13
+
14
+ def output(value:)
15
+ case LowType.config.output_mode
16
+ when :type
17
+ # TODO: Show full type structure in error output instead of just the type of the supertype.
18
+ value.class
19
+ when :value
20
+ value.inspect[0...LowType.config.output_size]
21
+ else
22
+ 'REDACTED'
23
+ end
24
+ end
25
+
26
+ def backtrace(backtrace:, hidden_paths:)
27
+ # Remove LowType defined method file paths from the backtrace.
28
+ filtered_backtrace = backtrace.reject { |line| hidden_paths.find { |file_path| line.include?(file_path) } }
29
+
30
+ # Add the proxied entity to the backtrace.
31
+ proxy_file_backtrace = "#{file_path}:#{start_line}:in '#{scope}'"
32
+ from_prefix = filtered_backtrace.first.match(/\s+from /)
33
+ proxy_file_backtrace = "#{from_prefix}#{proxy_file_backtrace}" if from_prefix
34
+
35
+ [proxy_file_backtrace, *filtered_backtrace]
36
+ end
37
+ end
38
+ end
data/lib/lowtype.rb ADDED
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lowkey'
4
+
5
+ require_relative 'adapters/adapter_loader'
6
+ require_relative 'definitions/redefiner'
7
+ require_relative 'definitions/type_accessors'
8
+ require_relative 'expressions/expression_helpers'
9
+ require_relative 'queries/file_query'
10
+ require_relative 'syntax/syntax'
11
+ require_relative 'types/complex_types'
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
+ # │ │ │ │ │
33
+ module LowType
34
+ # Defers evaluation and redefinition until after the class body finishes loading via TracePoint :end.
35
+ def self.included(klass)
36
+ file_path = Low::FileQuery.file_path(klass:)
37
+ file_proxy = Lowkey.load(file_path)
38
+ class_proxy = file_proxy[klass.name]
39
+
40
+ klass.include Low::ExpressionHelpers
41
+ klass.extend Low::ExpressionHelpers
42
+ klass.extend Low::TypeAccessors
43
+ klass.extend Low::Types
44
+
45
+ # Use TracePoint :end to capture the class binding after the class body finishes loading.
46
+ # At :end time, trace.self is the including class and trace.binding is the class body's binding,
47
+ # stored on class_proxy.class_binding for use by LowType and other consumers.
48
+ tp = TracePoint.new(:end) do |trace|
49
+ next unless trace.self == klass
50
+
51
+ class_proxy.class_binding = trace.binding
52
+
53
+ Low::Evaluator.evaluate(method_proxies: class_proxy.keyed_methods, class_binding: class_proxy.class_binding)
54
+
55
+ klass.prepend Low::Redefiner.redefine(method_proxies: class_proxy.instance_methods, class_proxy:)
56
+ klass.singleton_class.prepend Low::Redefiner.redefine(method_proxies: class_proxy.class_methods, class_proxy:)
57
+
58
+ Low::Adapter::Loader.load(klass:, class_proxy:)
59
+
60
+ tp.disable
61
+ end
62
+
63
+ tp.enable
64
+ end
65
+
66
+ Config = Struct.new(
67
+ :type_checking,
68
+ :error_mode,
69
+ :output_mode,
70
+ :output_size,
71
+ :deep_type_check,
72
+ :union_type_expressions
73
+ )
74
+
75
+ class << self
76
+ def config
77
+ @config ||= Config.new(true, :error, :type, 100, true, true)
78
+ end
79
+
80
+ def configure
81
+ yield(config)
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../interfaces/error_handling'
4
+ require_relative '../types/error_types'
5
+
6
+ module Low
7
+ class LocalProxy
8
+ include ErrorHandling
9
+
10
+ attr_reader :type_expression, :name, :file_path, :start_line, :scope
11
+
12
+ def initialize(type_expression:, name:, file_path:, start_line:, scope:)
13
+ @file_path = file_path
14
+ @start_line = start_line
15
+ @scope = scope
16
+
17
+ @type_expression = type_expression
18
+ @name = name
19
+ end
20
+
21
+ def error_type
22
+ LocalTypeError
23
+ end
24
+
25
+ def error_message(value:)
26
+ "Invalid variable type #{output(value:)} in '#{name.class}:#{@start_line}'. Valid types: '#{type_expression.valid_types}'"
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lowkey'
4
+
5
+ require_relative '../interfaces/error_handling'
6
+ require_relative '../types/error_types'
7
+
8
+ module ::Lowkey
9
+ class ParamProxy
10
+ include ::Low::ErrorHandling
11
+
12
+ def error_type
13
+ ::Low::ArgumentTypeError
14
+ end
15
+
16
+ def error_message(value:)
17
+ "Invalid argument type '#{output(value:)}' for parameter '#{@name}'. Valid types: '#{@expression.valid_types}'"
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lowkey'
4
+
5
+ require_relative '../interfaces/error_handling'
6
+ require_relative '../types/error_types'
7
+
8
+ module ::Lowkey
9
+ class ReturnProxy
10
+ include ::Low::ErrorHandling
11
+
12
+ def error_type
13
+ ::Low::ReturnTypeError
14
+ end
15
+
16
+ def error_message(value:)
17
+ "Invalid return type '#{output(value:)}' for method '#{@name}'. Valid types: '#{@expression.valid_types}'"
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Low
4
+ class MissingFileError < StandardError; end
5
+
6
+ class FileQuery
7
+ class << self
8
+ def file_path(klass:)
9
+ file_path = Object.const_source_location(klass.name).first
10
+
11
+ return file_path if File.exist?(file_path)
12
+
13
+ raise MissingFileError, "No file found at path '#{file_path}'"
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../expressions/type_expression'
4
+
5
+ module Low
6
+ # TODO: Unit test.
7
+ class TypeQuery
8
+ class << self
9
+ def type?(expression)
10
+ basic_type?(expression:) || complex_type?(expression:)
11
+ end
12
+
13
+ def typed_array?(expression:)
14
+ expression.is_a?(Array) && (basic_type?(expression: expression.first) || expression.first.is_a?(TypeExpression))
15
+ end
16
+
17
+ def value?(value)
18
+ !basic_type?(expression: value) && !complex_type?(expression: value)
19
+ end
20
+
21
+ def complex_type?(expression:)
22
+ Low::Types::COMPLEX_TYPES.include?(expression) || typed_array?(expression:) || typed_hash?(expression:)
23
+ end
24
+
25
+ private
26
+
27
+ def basic_type?(expression:)
28
+ expression.instance_of?(Class)
29
+ end
30
+
31
+ def typed_hash?(expression:)
32
+ expression.is_a?(Hash) && basic_type?(expression: expression.keys.first) && basic_type?(expression: expression.values.first)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../queries/type_query'
4
+
5
+ module LowType
6
+ module Syntax
7
+ refine Array.singleton_class do
8
+ def [](*expression)
9
+ if Low::TypeQuery.type?(expression.first) || Low::TypeQuery.typed_array?(expression:)
10
+ return Low::TypeExpression.new(type: [*expression])
11
+ end
12
+
13
+ super
14
+ end
15
+ end
16
+
17
+ refine Hash.singleton_class do
18
+ def [](type)
19
+ return Low::TypeExpression.new(type:) if Low::TypeQuery.type?(type)
20
+
21
+ super
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ ###
4
+ # Type expressions from union types.
5
+ #
6
+ # The "|" pipe syntax requires a monkey-patch but can be disabled if you don't need union types with default values.
7
+ # This is the only monkey-patch in the entire library and is a relatively harmless one.
8
+ # @see LowType.config.union_type_expressions
9
+ ###
10
+ class Object
11
+ # For "Type | [type_expression/type/value]" situations, convert type into a type expression to continue the chain.
12
+ # "|" is not defined on Object class and this is the most compute-efficient way to achieve our goal (world peace).
13
+ # "|" is overridable by any child object. While we could def/undef this method, this approach is actually lighter.
14
+ # "|" bitwise operator on Integer is not defined when the receiver is an Integer class, so we are not in conflict.
15
+ class << self
16
+ def |(expression)
17
+ ::Low::TypeExpression.new(type: self) | expression
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Low
4
+ module ComplexType
5
+ def match?(value:)
6
+ return true if value.instance_of?(self.class) || value.instance_of?(superclass)
7
+
8
+ false
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../factories/type_factory'
4
+ require_relative 'status'
5
+
6
+ module Low
7
+ module Types
8
+ COMPLEX_TYPES = [
9
+ Boolean = TypeFactory.complex_type(Object),
10
+ Headers = TypeFactory.complex_type(Hash),
11
+ HTML = TypeFactory.complex_type(String),
12
+ JSON = TypeFactory.complex_type(String),
13
+ Status,
14
+ Tuple = TypeFactory.complex_type(Array),
15
+ XML = TypeFactory.complex_type(String)
16
+ ].freeze
17
+ end
18
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Low
4
+ class ArgumentTypeError < TypeError; end
5
+ class LocalTypeError < TypeError; end
6
+ class ReturnTypeError < TypeError; end
7
+ class AllowedTypeError < TypeError; end
8
+ class ConfigError < TypeError; end
9
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'complex_type'
4
+ require_relative 'error_types'
5
+
6
+ module Low
7
+ module Types
8
+ # Status is an Integer for basic type checking...
9
+ class Status < Integer
10
+ extend ComplexType
11
+
12
+ # ...but becomes an instance of StatusCode when called with "Status[:code]" for advanced type checking (status + code).
13
+ def self.[](status_code)
14
+ @status_code = StatusCode.new(status_code)
15
+ end
16
+
17
+ class StatusCode
18
+ attr_reader :status_code
19
+
20
+ STATUS_CODES = [
21
+ # Info.
22
+ 100, 101, 102, 103,
23
+ # Success.
24
+ 200, 201, 202, 203, 204, 205, 206, 207, 208, 226,
25
+ # Redirect.
26
+ 300, 301, 302, 303, 304, 305, 306, 307, 308,
27
+ # Client Error.
28
+ 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414,
29
+ 415, 416, 417, 418, 421, 422, 423, 424, 425, 426, 428, 429, 431, 451,
30
+ # Server Error.
31
+ 500, 501, 502, 503, 504, 505, 506, 507, 508, 510, 511
32
+ ].freeze
33
+
34
+ def initialize(status_code)
35
+ raise AllowedTypeError unless STATUS_CODES.include?(status_code)
36
+
37
+ @status_code = status_code
38
+ end
39
+
40
+ def ==(other)
41
+ other.class == self.class && other.status_code == @status_code
42
+ end
43
+
44
+ def eql?(other)
45
+ self == other
46
+ end
47
+
48
+ def hash
49
+ [self.class, @status_code].hash
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
data/lib/version.rb ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Low
4
+ module Type
5
+ VERSION = '1.3.2'
6
+ end
7
+ end
metadata ADDED
@@ -0,0 +1,96 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lowtype
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.3.2
5
+ platform: ruby
6
+ authors:
7
+ - maedi
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: expressions
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.1'
26
+ - !ruby/object:Gem::Dependency
27
+ name: lowkey
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '0.4'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.4'
40
+ description: "LowType introduces the concept of \"type expressions\" in method arguments.
41
+ \nWhen an argument's default value resolves to a type instead of a value then it's
42
+ treated as a type expression. \nNow you can have types in Ruby in the simplest syntax
43
+ possible\n"
44
+ email:
45
+ - maediprichard@gmail.com
46
+ executables: []
47
+ extensions: []
48
+ extra_rdoc_files: []
49
+ files:
50
+ - lib/adapters/adapter_loader.rb
51
+ - lib/adapters/sinatra_adapter.rb
52
+ - lib/definitions/evaluator.rb
53
+ - lib/definitions/redefiner.rb
54
+ - lib/definitions/type_accessors.rb
55
+ - lib/expressions/expression_helpers.rb
56
+ - lib/expressions/type_expression.rb
57
+ - lib/expressions/value_expression.rb
58
+ - lib/factories/type_factory.rb
59
+ - lib/interfaces/adapter_interface.rb
60
+ - lib/interfaces/error_handling.rb
61
+ - lib/lowtype.rb
62
+ - lib/proxies/local_proxy.rb
63
+ - lib/proxies/param_proxy.rb
64
+ - lib/proxies/return_proxy.rb
65
+ - lib/queries/file_query.rb
66
+ - lib/queries/type_query.rb
67
+ - lib/syntax/syntax.rb
68
+ - lib/syntax/union_types.rb
69
+ - lib/types/complex_type.rb
70
+ - lib/types/complex_types.rb
71
+ - lib/types/error_types.rb
72
+ - lib/types/status.rb
73
+ - lib/version.rb
74
+ homepage: https://github.com/low-rb/lowtype
75
+ licenses: []
76
+ metadata:
77
+ homepage_uri: https://github.com/low-rb/lowtype
78
+ source_code_uri: https://github.com/low-rb/lowtype/src/branch/main
79
+ rdoc_options: []
80
+ require_paths:
81
+ - lib
82
+ required_ruby_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: 3.3.0
87
+ required_rubygems_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: '0'
92
+ requirements: []
93
+ rubygems_version: 4.0.6
94
+ specification_version: 4
95
+ summary: Elegant types in Ruby
96
+ test_files: []