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 +7 -0
- data/lib/adapters/adapter_loader.rb +19 -0
- data/lib/adapters/sinatra_adapter.rb +49 -0
- data/lib/definitions/evaluator.rb +104 -0
- data/lib/definitions/redefiner.rb +103 -0
- data/lib/definitions/type_accessors.rb +76 -0
- data/lib/expressions/expression_helpers.rb +32 -0
- data/lib/expressions/type_expression.rb +159 -0
- data/lib/expressions/value_expression.rb +18 -0
- data/lib/factories/type_factory.rb +17 -0
- data/lib/interfaces/adapter_interface.rb +9 -0
- data/lib/interfaces/error_handling.rb +38 -0
- data/lib/lowtype.rb +84 -0
- data/lib/proxies/local_proxy.rb +29 -0
- data/lib/proxies/param_proxy.rb +20 -0
- data/lib/proxies/return_proxy.rb +20 -0
- data/lib/queries/file_query.rb +17 -0
- data/lib/queries/type_query.rb +36 -0
- data/lib/syntax/syntax.rb +25 -0
- data/lib/syntax/union_types.rb +20 -0
- data/lib/types/complex_type.rb +11 -0
- data/lib/types/complex_types.rb +18 -0
- data/lib/types/error_types.rb +9 -0
- data/lib/types/status.rb +54 -0
- data/lib/version.rb +7 -0
- metadata +96 -0
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,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,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
|
data/lib/types/status.rb
ADDED
|
@@ -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
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: []
|