low_type 0.8.11 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2f0c69a8ca233198b383a056a7c78819a2dae7944b73d681846924ef8bfe9355
4
- data.tar.gz: fb04f1df5257523e0a1cb266500ff9d106c30723ee8aa46e66929ffa80198340
3
+ metadata.gz: 5872bc6f373425ddfc4342d0421dd09e4e3bd6a5e7e0b4d3ba1d1c19cd077a5c
4
+ data.tar.gz: 9b5543ba7bded1c2a043966376956316624408a101f2b30ec9a6b16d07976973
5
5
  SHA512:
6
- metadata.gz: a8eaaeed7102693d813d82fde2da76124b050eba9afa042bfaea2e94f40923629596c4fe613fce66f8cfba3342b86499bd257e14094c40ae8e399962dec2fe74
7
- data.tar.gz: '0519fec1df0f3e1b80403e376aa41cdc2e25a6d1dbd87d85c3c76cc185340c3c937255c84136f3d467bbd63d01ce8bfbc78e0c253e817324338a7a29e12dddae'
6
+ metadata.gz: c3a31c05871cc0d4b5fde6fca3c03c94fbc7e2ccc2696eb7eab401f8817e0b0a1abbd0bf4aa91d72454a61c60265b22ba9997148161f7d3e796d2e9c790e9c81
7
+ data.tar.gz: 3629f1ac45bc9cbfce9979f85e933bb3b12b731747b714fb8eba6b7399cf139ac6098eb37667b97b624e95eda4e02c104ce9d7cd2b6013ea3fbb639e1e4a0125
@@ -50,13 +50,16 @@ module LowType
50
50
  end
51
51
 
52
52
  module Methods
53
+ # Unfortunately overriding invoke() is the best way to validate types for now. Though direct it's also very compute efficient.
54
+ # I originally tried an after filter and it mostly worked but it only had access to Response which isn't the raw return value.
55
+ # I suggest that Sinatra provide a hook that allows us to access the raw return value of a route before it becomes a Response.
53
56
  def invoke(&block)
54
57
  res = catch(:halt, &block)
55
58
 
56
59
  low_validate!(value: res) if res
57
60
 
58
61
  res = [res] if res.is_a?(Integer) || res.is_a?(String)
59
- if res.is_a?(Array) && res.first.is_a?(Integer)
62
+ if res.is_a?(::Array) && res.first.is_a?(Integer)
60
63
  res = res.dup
61
64
  status(res.shift)
62
65
  body(res.pop)
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../proxies/return_proxy'
4
+ require_relative '../parser'
5
+ require_relative '../type_expression'
6
+
7
+ module LowType
8
+ class ProxyFactory
9
+ class << self
10
+ def return_proxy(method_node:, file:)
11
+ return_type = Parser.return_type(method_node:)
12
+ return nil if return_type.nil?
13
+
14
+ # Not a security risk because the code comes from a trusted source; the file that did the include. Does the file trust itself?
15
+ expression = eval(return_type.slice, binding, __FILE__, __LINE__).call # rubocop:disable Security/Eval
16
+ expression = TypeExpression.new(type: expression) unless expression.is_a?(TypeExpression)
17
+
18
+ ReturnProxy.new(type_expression: expression, name: method_node.name, file:)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'proxies/return_proxy'
4
+ require_relative 'type_expression'
5
+
6
+ module LowType
7
+ module InstanceTypes
8
+ def type_reader(named_expressions)
9
+ named_expressions.each do |name, expression|
10
+ last_caller = caller_locations(1, 1).first
11
+ type_expression = type_expression(expression)
12
+ file = FileProxy.new(path: last_caller.path, line: last_caller.lineno, scope: "#{self}##{name}")
13
+
14
+ @low_methods[name] = MethodProxy.new(name:, return_proxy: ReturnProxy.new(type_expression:, name:, file:))
15
+
16
+ define_method(name) do
17
+ method_proxy = self.class.low_methods[name]
18
+ value = instance_variable_get("@#{name}")
19
+ type_expression.validate!(value:, proxy: method_proxy.return_proxy)
20
+ value
21
+ end
22
+ end
23
+ end
24
+
25
+ def type_writer(named_expressions)
26
+ named_expressions.each do |name, expression|
27
+ last_caller = caller_locations(1, 1).first
28
+ type_expression = type_expression(expression)
29
+ file = FileProxy.new(path: last_caller.path, line: last_caller.lineno, scope: "#{self}##{name}")
30
+
31
+ @low_methods["#{name}="] = MethodProxy.new(name:, params: [ParamProxy.new(type_expression:, name:, type: :hashreq, file:)])
32
+
33
+ define_method("#{name}=") do |value|
34
+ method_proxy = self.class.low_methods["#{name}="]
35
+ type_expression.validate!(value:, proxy: method_proxy.params.first)
36
+ instance_variable_set("@#{name}", value)
37
+ end
38
+ end
39
+ end
40
+
41
+ def type_accessor(named_expressions)
42
+ named_expressions.each do |name, expression|
43
+ type_reader({ name => expression })
44
+ type_writer({ name => expression })
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def type_expression(expression)
51
+ if expression.instance_of?(TypeExpression)
52
+ expression
53
+ elsif ::LowType.type?(expression)
54
+ TypeExpression.new(type: expression)
55
+ end
56
+ end
57
+ end
58
+ end
data/lib/local_types.rb CHANGED
@@ -6,25 +6,18 @@ require_relative 'type_expression'
6
6
  require_relative 'value_expression'
7
7
 
8
8
  module LocalTypes
9
- class AssignmentError < StandardError; end
10
-
11
9
  def type(type_expression)
12
- referenced_object = type_expression.default_value.dup
13
-
14
- raise AssignmentError, "Single-instance objects like #{referenced_object} are not supported" unless LowType.value?(referenced_object)
10
+ value = type_expression.default_value
15
11
 
16
12
  last_caller = caller_locations(1, 1).first
17
13
  file = LowType::FileProxy.new(path: last_caller.path, line: last_caller.lineno, scope: 'local type')
18
- local_proxy = LowType::LocalProxy.new(type_expression:, name: self, file:)
19
- referenced_object.instance_variable_set('@local_proxy', local_proxy)
14
+ proxy = LowType::LocalProxy.new(type_expression:, name: self, file:)
20
15
 
21
- type_expression.validate!(value: referenced_object, proxy: local_proxy)
16
+ type_expression.validate!(value:, proxy:)
22
17
 
23
- define_with_type(referenced_object:)
18
+ return value.value if value.is_a?(ValueExpression)
24
19
 
25
- return referenced_object.value if referenced_object.is_a?(ValueExpression)
26
-
27
- referenced_object
20
+ value
28
21
  end
29
22
  alias low_type type
30
23
 
@@ -32,39 +25,4 @@ module LocalTypes
32
25
  LowType.value(type:)
33
26
  end
34
27
  alias low_value value
35
-
36
- # Scoped to the class that includes LowTypes module.
37
- class Array < ::Array
38
- def self.[](*types)
39
- return LowType::TypeExpression.new(type: [*types]) if types.all? { |type| LowType.type?(type) }
40
-
41
- super
42
- end
43
- end
44
-
45
- # Scoped to the class that includes LowTypes module.
46
- class Hash < ::Hash
47
- def self.[](type)
48
- return LowType::TypeExpression.new(type:) if LowType.type?(type)
49
-
50
- super
51
- end
52
- end
53
-
54
- private
55
-
56
- def define_with_type(referenced_object:)
57
- def referenced_object.with_type=(value)
58
- local_proxy = instance_variable_get('@local_proxy')
59
- type_expression = local_proxy.type_expression
60
- type_expression.validate!(value:, proxy: local_proxy)
61
-
62
- # We can't reassign self in Ruby so we reassign instance variables instead.
63
- value.instance_variables.each do |variable|
64
- instance_variable_set(variable, value.instance_variable_get(variable))
65
- end
66
-
67
- self
68
- end
69
- end
70
28
  end
data/lib/low_type.rb CHANGED
@@ -2,7 +2,10 @@
2
2
 
3
3
  require_relative 'adapters/adapter_loader'
4
4
  require_relative 'basic_types'
5
+ require_relative 'instance_types'
6
+ require_relative 'local_types'
5
7
  require_relative 'redefiner'
8
+ require_relative 'subclasses'
6
9
  require_relative 'type_expression'
7
10
  require_relative 'value_expression'
8
11
 
@@ -10,11 +13,6 @@ require_relative 'value_expression'
10
13
  module LowType
11
14
  # We do as much as possible on class load rather than on instantiation to be thread-safe and efficient.
12
15
  def self.included(klass) # rubocop:disable Metrics/AbcSize
13
- # Array[] and Hash[] class method returns a type expression only for the duration of this "included" hook.
14
- array_class_method = Array.method('[]').unbind
15
- hash_class_method = Hash.method('[]').unbind
16
- LowType.redefine(hash_class_method:)
17
-
18
16
  class << klass
19
17
  def low_methods
20
18
  @low_methods ||= {}
@@ -25,6 +23,8 @@ module LowType
25
23
  parser = LowType::Parser.new(file_path:)
26
24
  private_start_line = parser.private_start_line
27
25
 
26
+ klass.extend InstanceTypes
27
+ klass.include LocalTypes
28
28
  klass.prepend LowType::Redefiner.redefine(method_nodes: parser.instance_methods, klass:, private_start_line:, file_path:)
29
29
  klass.singleton_class.prepend LowType::Redefiner.redefine(method_nodes: parser.class_methods, klass:, private_start_line:, file_path:)
30
30
 
@@ -32,26 +32,18 @@ module LowType
32
32
  adapter.process
33
33
  klass.prepend Adapter::Methods
34
34
  end
35
- ensure
36
- Array.define_singleton_method('[]', array_class_method)
37
- Hash.define_singleton_method('[]', hash_class_method)
38
35
  end
39
36
 
40
37
  class << self
41
38
  # Public API.
42
39
 
43
40
  def config
44
- config = Struct.new(:local_types, :deep_type_check)
45
- @config ||= config.new(false, false)
41
+ config = Struct.new(:deep_type_check, :severity_level)
42
+ @config ||= config.new(false, :error)
46
43
  end
47
44
 
48
45
  def configure
49
46
  yield(config)
50
-
51
- return unless config.local_types
52
-
53
- require_relative 'local_types'
54
- include LocalTypes
55
47
  end
56
48
 
57
49
  # Internal API.
@@ -87,24 +79,5 @@ module LowType
87
79
  def value(type:)
88
80
  TypeExpression.new(default_value: ValueExpression.new(value: type))
89
81
  end
90
-
91
- # TODO: Unit test.
92
- def redefine(hash_class_method:)
93
- Array.define_singleton_method('[]') do |*types|
94
- TypeExpression.new(type: [*types])
95
- end
96
-
97
- Hash.define_singleton_method('[]') do |type|
98
- # Support Pry which uses Hash[].
99
- unless LowType.type?(type)
100
- Hash.define_singleton_method('[]', hash_class_method)
101
- result = Hash[type]
102
- Hash.method('[]').unbind
103
- return result
104
- end
105
-
106
- TypeExpression.new(type:)
107
- end
108
- end
109
82
  end
110
83
  end
data/lib/redefiner.rb CHANGED
@@ -1,10 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'factories/proxy_factory'
3
4
  require_relative 'proxies/file_proxy'
4
5
  require_relative 'proxies/method_proxy'
5
6
  require_relative 'proxies/param_proxy'
6
- require_relative 'proxies/return_proxy'
7
- require_relative 'parser'
8
7
  require_relative 'type_expression'
9
8
 
10
9
  module LowType
@@ -18,7 +17,7 @@ module LowType
18
17
  line = Parser.line_number(node: method_node)
19
18
  file = FileProxy.new(path: file_path, line:, scope: "#{klass}##{method_node.name}")
20
19
  params = Redefiner.params_with_type_expressions(method_node:, file:)
21
- return_proxy = Redefiner.return_proxy(method_node:, file:)
20
+ return_proxy = ProxyFactory.return_proxy(method_node:, file:)
22
21
 
23
22
  klass.low_methods[name] = MethodProxy.new(name:, params:, return_proxy:)
24
23
 
@@ -51,7 +50,7 @@ module LowType
51
50
  end
52
51
  end
53
52
 
54
- def params_with_type_expressions(method_node:, file:) # rubocop:disable Metrics/MethodLength
53
+ def params_with_type_expressions(method_node:, file:)
55
54
  return [] if method_node.parameters.nil?
56
55
 
57
56
  params = method_node.parameters.slice
@@ -88,17 +87,6 @@ module LowType
88
87
  raise ArgumentError, "Incorrect param syntax: #{e.message}"
89
88
  end
90
89
 
91
- def return_proxy(method_node:, file:)
92
- return_type = Parser.return_type(method_node:)
93
- return nil if return_type.nil?
94
-
95
- # Not a security risk because the code comes from a trusted source; the file that did the include. Does the file trust itself?
96
- expression = eval(return_type.slice).call # rubocop:disable Security/Eval
97
- expression = TypeExpression.new(type: expression) unless expression.is_a?(TypeExpression)
98
-
99
- ReturnProxy.new(type_expression: expression, name: method_node.name, file:)
100
- end
101
-
102
90
  private
103
91
 
104
92
  def required_args(proxy_method:)
data/lib/subclasses.rb ADDED
@@ -0,0 +1,58 @@
1
+ module LowType
2
+ # Scoped to the class that includes LowType module.
3
+ class Array < ::Array
4
+ def self.[](*types)
5
+ return LowType::TypeExpression.new(type: [*types]) if types.all? { |type| LowType.type?(type) }
6
+ super
7
+ end
8
+
9
+ def self.===(other)
10
+ return true if other == ::Array
11
+ super
12
+ end
13
+
14
+ def ==(other)
15
+ return true if other.class == ::Array
16
+ super
17
+ end
18
+ end
19
+
20
+ # Scoped to the class that includes LowType module.
21
+ class Hash < ::Hash
22
+ def self.[](type)
23
+ return LowType::TypeExpression.new(type:) if LowType.type?(type)
24
+ super
25
+ end
26
+
27
+ def self.===(other)
28
+ return true if other == ::Hash
29
+ super
30
+ end
31
+
32
+ def ==(other)
33
+ return true if other.class == ::Hash
34
+ super
35
+ end
36
+ end
37
+ end
38
+
39
+ # Scoped to the top-level for method type expressions to work. Could we bind/unbind? Yes. Should we? Probably not.
40
+ class Object
41
+ # For "Type | [type_expression/type/value]" situations, redirecting to or generating a type expression from types.
42
+ # "|" is not defined on Object class and this is the most compute-efficient way to achieve our goal (world peace).
43
+ # "|" is overridable by any child object. While we could def/undef this method, this approach is actually lighter.
44
+ # "|" bitwise operator on Integer is not defined when the receiver is an Integer class, so we are not in conflict.
45
+ class << self
46
+ def |(expression)
47
+ if expression.instance_of?(::LowType::TypeExpression)
48
+ # We pass our type into their type expression.
49
+ expression | self
50
+ expression
51
+ else
52
+ # We turn our type into a type expression and pass in their [type_expression/type/value].
53
+ type_expression = ::LowType::TypeExpression.new(type: self)
54
+ type_expression | expression
55
+ end
56
+ end
57
+ end
58
+ end
@@ -56,7 +56,15 @@ module LowType
56
56
  end
57
57
 
58
58
  def valid_types
59
- types = @types.map { |type| type.inspect.to_s }
59
+ types = @types.map do |type|
60
+ # Remove 'LowType::' namespaces in subtypes.
61
+ if type.is_a?(::Array)
62
+ '[' + type.map { |subtype| subtype.to_s.delete_prefix('LowType::') }.join(', ') + ']'
63
+ else
64
+ type.inspect.to_s.delete_prefix('LowType::')
65
+ end
66
+ end
67
+
60
68
  types += ['nil'] if @default_value.nil?
61
69
  types.join(' | ')
62
70
  end
@@ -97,23 +105,3 @@ module LowType
97
105
  end
98
106
  end
99
107
  end
100
-
101
- # For "Type | [type_expression/type/value]" situations, redirecting to or generating a type expression from types.
102
- # "|" is not defined on Object class and this is the most compute-efficient way to achieve our goal (world peace).
103
- # "|" is overridable by any child object. While we could def/undef this method, this approach is actually lighter.
104
- # "|" bitwise operator on Integer is not defined when the receiver is an Integer class, so we are not in conflict.
105
- class Object
106
- class << self
107
- def |(expression)
108
- if expression.instance_of?(::LowType::TypeExpression)
109
- # We pass our type into their type expression.
110
- expression | self
111
- expression
112
- else
113
- # We turn our type into a type expression and pass in their [type_expression/type/value].
114
- type_expression = ::LowType::TypeExpression.new(type: self)
115
- type_expression | expression
116
- end
117
- end
118
- end
119
- end
data/lib/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LowType
4
- VERSION = '0.8.11'
4
+ VERSION = '1.0.0'
5
5
  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: 0.8.11
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - maedi
@@ -21,6 +21,8 @@ files:
21
21
  - lib/adapters/sinatra_adapter.rb
22
22
  - lib/basic_types.rb
23
23
  - lib/error_types.rb
24
+ - lib/factories/proxy_factory.rb
25
+ - lib/instance_types.rb
24
26
  - lib/interfaces/adapter_interface.rb
25
27
  - lib/interfaces/error_interface.rb
26
28
  - lib/local_types.rb
@@ -32,6 +34,7 @@ files:
32
34
  - lib/proxies/param_proxy.rb
33
35
  - lib/proxies/return_proxy.rb
34
36
  - lib/redefiner.rb
37
+ - lib/subclasses.rb
35
38
  - lib/type_expression.rb
36
39
  - lib/value_expression.rb
37
40
  - lib/version.rb