low_type 0.9.0 → 1.0.1

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: a340469d2c2f4945b043ab3cd4973f14cf12f9d80336b7939bdb85b0da5b739e
4
- data.tar.gz: 2e51c25e1b944dff67fb85f4582a2f3f38eca299fe58d4cc464c4b8c162a9d8a
3
+ metadata.gz: 3541ef39f820d9554a2493481686ad0692f66d36a31920d2c4304a62cb55bbb9
4
+ data.tar.gz: 19bebceb57fff771317130dae93e5caf86962028db3e81321b03855ccf3c42cb
5
5
  SHA512:
6
- metadata.gz: 8857cb2e301aa2e7f4ddf167fe622d3988d7d28ff223361a50a056f28ea248e4ad81ecc48fc2174f2f8845c7af7e8fad072ac9bf3ed4eea5a0bad37a06ab9b6d
7
- data.tar.gz: c401324adf4f5aa4b4da5a1c6f19d5b3daa095e7ff28a43a9075f28f4d7a8947a13fcba500bab252eece331b51d65e89fee09efd99e1d61f5e0290faae51dab7
6
+ metadata.gz: 61dace65facc045407b0300157a9e66e6269b14a3414ed943ed3bb5bd817ab313d4c10fd184cad0c2f6803c50a03c6c7d5929253ce68ba89b909a49f55d79fcb
7
+ data.tar.gz: 207f390f476e60af068bffc5bed65c0f76f20bc4997e1d76905496779e63aeaf9022405fe95576d4d8f14ad35107ea6c07f8a98fb4ddfb447d50eff35a1a6921
@@ -5,7 +5,7 @@ require 'prism'
5
5
  require_relative '../interfaces/adapter_interface'
6
6
  require_relative '../proxies/file_proxy'
7
7
  require_relative '../proxies/return_proxy'
8
- require_relative '../error_types'
8
+ require_relative '../types/error_types'
9
9
 
10
10
  module LowType
11
11
  module Adapter
@@ -27,7 +27,7 @@ module LowType
27
27
 
28
28
  pattern = arguments_node.arguments.first.content
29
29
 
30
- line = Parser.line_number(node: method_call)
30
+ line = FileParser.line_number(node: method_call)
31
31
  file = FileProxy.new(path: @file_path, line:, scope: "#{@klass}##{method_call.name}")
32
32
  next unless (return_proxy = return_proxy(method_node: method_call, pattern:, file:))
33
33
 
@@ -38,7 +38,7 @@ module LowType
38
38
  end
39
39
 
40
40
  def return_proxy(method_node:, pattern:, file:)
41
- return_type = Parser.return_type(method_node:)
41
+ return_type = FileParser.return_type(method_node:)
42
42
  return nil if return_type.nil?
43
43
 
44
44
  # Not a security risk because the code comes from a trusted source; the file that did the include. Does the file trust itself?
@@ -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)
@@ -1,14 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../proxies/return_proxy'
4
- require_relative '../parser'
4
+ require_relative '../file_parser'
5
5
  require_relative '../type_expression'
6
6
 
7
7
  module LowType
8
8
  class ProxyFactory
9
9
  class << self
10
10
  def return_proxy(method_node:, file:)
11
- return_type = Parser.return_type(method_node:)
11
+ return_type = FileParser.return_type(method_node:)
12
12
  return nil if return_type.nil?
13
13
 
14
14
  # Not a security risk because the code comes from a trusted source; the file that did the include. Does the file trust itself?
@@ -3,7 +3,7 @@
3
3
  require 'prism'
4
4
 
5
5
  module LowType
6
- class Parser
6
+ class FileParser
7
7
  attr_reader :parent_map, :instance_methods, :class_methods, :private_start_line
8
8
 
9
9
  def initialize(file_path:)
data/lib/local_types.rb CHANGED
@@ -2,69 +2,32 @@
2
2
 
3
3
  require_relative 'proxies/file_proxy'
4
4
  require_relative 'proxies/local_proxy'
5
+ require_relative 'types/error_types'
5
6
  require_relative 'type_expression'
6
7
  require_relative 'value_expression'
7
8
 
8
- module LocalTypes
9
- class AssignmentError < StandardError; end
9
+ module LowType
10
+ module LocalTypes
11
+ def type(type_expression)
12
+ value = type_expression.default_value
10
13
 
11
- def type(type_expression)
12
- referenced_object = type_expression.default_value.dup
14
+ last_caller = caller_locations(1, 1).first
15
+ file = FileProxy.new(path: last_caller.path, line: last_caller.lineno, scope: 'local type')
16
+ proxy = LocalProxy.new(type_expression:, name: self, file:)
13
17
 
14
- raise AssignmentError, "Single-instance objects like #{referenced_object} are not supported" unless LowType.value?(referenced_object)
18
+ type_expression.validate!(value:, proxy:)
15
19
 
16
- last_caller = caller_locations(1, 1).first
17
- 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)
20
+ return value.value if value.is_a?(ValueExpression)
20
21
 
21
- type_expression.validate!(value: referenced_object, proxy: local_proxy)
22
-
23
- define_with_type(referenced_object:)
24
-
25
- return referenced_object.value if referenced_object.is_a?(ValueExpression)
26
-
27
- referenced_object
28
- end
29
- alias low_type type
30
-
31
- def value(type)
32
- LowType.value(type:)
33
- end
34
- 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
22
+ value
23
+ rescue NoMethodError
24
+ raise ConfigError, "Invalid type expression, likely because you didn't add 'using LowType::Syntax'"
42
25
  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
26
+ alias low_type type
66
27
 
67
- self
28
+ def value(type)
29
+ LowType.value(type:)
68
30
  end
31
+ alias low_value value
69
32
  end
70
33
  end
data/lib/low_type.rb CHANGED
@@ -1,21 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'adapters/adapter_loader'
4
- require_relative 'basic_types'
4
+ require_relative 'types/complex_types'
5
5
  require_relative 'instance_types'
6
+ require_relative 'local_types'
6
7
  require_relative 'redefiner'
8
+ require_relative 'syntax'
7
9
  require_relative 'type_expression'
8
10
  require_relative 'value_expression'
9
11
 
10
- # Include this module into your class to define and check types.
11
12
  module LowType
12
13
  # We do as much as possible on class load rather than on instantiation to be thread-safe and efficient.
13
- def self.included(klass) # rubocop:disable Metrics/AbcSize
14
- # Array[] and Hash[] class method returns a type expression only for the duration of this "included" hook.
15
- array_class_method = Array.method('[]').unbind
16
- hash_class_method = Hash.method('[]').unbind
17
- LowType.redefine(hash_class_method:)
18
-
14
+ def self.included(klass)
19
15
  class << klass
20
16
  def low_methods
21
17
  @low_methods ||= {}
@@ -23,10 +19,11 @@ module LowType
23
19
  end
24
20
 
25
21
  file_path = LowType.file_path(klass:)
26
- parser = LowType::Parser.new(file_path:)
22
+ parser = FileParser.new(file_path:)
27
23
  private_start_line = parser.private_start_line
28
24
 
29
25
  klass.extend InstanceTypes
26
+ klass.include LocalTypes
30
27
  klass.prepend LowType::Redefiner.redefine(method_nodes: parser.instance_methods, klass:, private_start_line:, file_path:)
31
28
  klass.singleton_class.prepend LowType::Redefiner.redefine(method_nodes: parser.class_methods, klass:, private_start_line:, file_path:)
32
29
 
@@ -34,26 +31,18 @@ module LowType
34
31
  adapter.process
35
32
  klass.prepend Adapter::Methods
36
33
  end
37
- ensure
38
- Array.define_singleton_method('[]', array_class_method)
39
- Hash.define_singleton_method('[]', hash_class_method)
40
34
  end
41
35
 
42
36
  class << self
43
37
  # Public API.
44
38
 
45
39
  def config
46
- config = Struct.new(:local_types, :deep_type_check)
47
- @config ||= config.new(false, false)
40
+ config = Struct.new(:deep_type_check, :severity_level)
41
+ @config ||= config.new(false, :error)
48
42
  end
49
43
 
50
44
  def configure
51
45
  yield(config)
52
-
53
- return unless config.local_types
54
-
55
- require_relative 'local_types'
56
- include LocalTypes
57
46
  end
58
47
 
59
48
  # Internal API.
@@ -89,24 +78,5 @@ module LowType
89
78
  def value(type:)
90
79
  TypeExpression.new(default_value: ValueExpression.new(value: type))
91
80
  end
92
-
93
- # TODO: Unit test.
94
- def redefine(hash_class_method:)
95
- Array.define_singleton_method('[]') do |*types|
96
- TypeExpression.new(type: [*types])
97
- end
98
-
99
- Hash.define_singleton_method('[]') do |type|
100
- # Support Pry which uses Hash[].
101
- unless LowType.type?(type)
102
- Hash.define_singleton_method('[]', hash_class_method)
103
- result = Hash[type]
104
- Hash.method('[]').unbind
105
- return result
106
- end
107
-
108
- TypeExpression.new(type:)
109
- end
110
- end
111
81
  end
112
82
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../interfaces/error_interface'
4
- require_relative '../error_types'
4
+ require_relative '../types/error_types'
5
5
 
6
6
  module LowType
7
7
  class LocalProxy < ErrorInterface
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../interfaces/error_interface'
4
- require_relative '../error_types'
4
+ require_relative '../types/error_types'
5
5
 
6
6
  module LowType
7
7
  class ParamProxy < ErrorInterface
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../interfaces/error_interface'
4
- require_relative '../error_types'
4
+ require_relative '../types/error_types'
5
5
 
6
6
  module LowType
7
7
  class ReturnProxy < ErrorInterface
data/lib/redefiner.rb CHANGED
@@ -4,41 +4,57 @@ require_relative 'factories/proxy_factory'
4
4
  require_relative 'proxies/file_proxy'
5
5
  require_relative 'proxies/method_proxy'
6
6
  require_relative 'proxies/param_proxy'
7
+ require_relative 'syntax'
7
8
  require_relative 'type_expression'
8
9
 
9
10
  module LowType
10
11
  # Redefine methods to have their arguments and return values type checked.
11
12
  class Redefiner
13
+ using Syntax
14
+
12
15
  class << self
13
- def redefine(method_nodes:, klass:, private_start_line:, file_path:) # rubocop:disable Metrics
16
+ def redefine(method_nodes:, klass:, private_start_line:, file_path:)
17
+ create_proxies(method_nodes:, klass:, file_path:)
18
+ define_methods(method_nodes:, private_start_line:)
19
+ end
20
+
21
+ private
22
+
23
+ def create_proxies(method_nodes:, klass:, file_path:)
24
+ method_nodes.each do |method_node|
25
+ name = method_node.name
26
+ line = FileParser.line_number(node: method_node)
27
+ file = FileProxy.new(path: file_path, line:, scope: "#{klass}##{name}")
28
+ params = param_proxies(method_node:, file:)
29
+ return_proxy = ProxyFactory.return_proxy(method_node:, file:)
30
+
31
+ klass.low_methods[name] = MethodProxy.new(name:, params:, return_proxy:)
32
+ end
33
+ end
34
+
35
+ def define_methods(method_nodes:, private_start_line:) # rubocop:disable Metrics
14
36
  Module.new do
15
37
  method_nodes.each do |method_node|
16
38
  name = method_node.name
17
- line = Parser.line_number(node: method_node)
18
- file = FileProxy.new(path: file_path, line:, scope: "#{klass}##{method_node.name}")
19
- params = Redefiner.params_with_type_expressions(method_node:, file:)
20
- return_proxy = ProxyFactory.return_proxy(method_node:, file:)
21
-
22
- klass.low_methods[name] = MethodProxy.new(name:, params:, return_proxy:)
23
39
 
24
40
  define_method(name) do |*args, **kwargs|
25
- klass.low_methods[name].params.each do |param_proxy|
41
+ method_proxy = instance_of?(Class) ? low_methods[name] : self.class.low_methods[name]
42
+
43
+ method_proxy.params.each do |param_proxy|
26
44
  # Get argument value or default value.
27
45
  value = param_proxy.position ? args[param_proxy.position] : kwargs[param_proxy.name]
28
46
  if value.nil? && param_proxy.type_expression.default_value != :LOW_TYPE_UNDEFINED
29
47
  value = param_proxy.type_expression.default_value
30
48
  end
31
- # Validate argument type.
49
+
32
50
  param_proxy.type_expression.validate!(value:, proxy: param_proxy)
33
- # Handle value(type) special case.
34
51
  value = value.value if value.is_a?(ValueExpression)
35
- # Redefine argument value.
36
52
  param_proxy.position ? args[param_proxy.position] = value : kwargs[param_proxy.name] = value
37
53
  end
38
54
 
39
- if return_proxy
55
+ if (return_proxy = method_proxy.return_proxy)
40
56
  return_value = super(*args, **kwargs)
41
- return_proxy.type_expression.validate!(value: return_value, proxy: klass.low_methods[name].return_proxy)
57
+ return_proxy.type_expression.validate!(value: return_value, proxy: return_proxy)
42
58
  return return_value
43
59
  end
44
60
 
@@ -50,12 +66,11 @@ module LowType
50
66
  end
51
67
  end
52
68
 
53
- def params_with_type_expressions(method_node:, file:)
69
+ def param_proxies(method_node:, file:)
54
70
  return [] if method_node.parameters.nil?
55
71
 
56
72
  params = method_node.parameters.slice
57
- # Not a security risk because the code comes from a trusted source; the file that did the include. Does the file trust itself?
58
- proxy_method = eval("-> (#{params}) {}", binding, __FILE__, __LINE__) # rubocop:disable Security/Eval
73
+ proxy_method = proxy_method(method_node:)
59
74
  required_args, required_kwargs = required_args(proxy_method:)
60
75
 
61
76
  # Not a security risk because the code comes from a trusted source; the file that did the include. Does the file trust itself?
@@ -87,7 +102,11 @@ module LowType
87
102
  raise ArgumentError, "Incorrect param syntax: #{e.message}"
88
103
  end
89
104
 
90
- private
105
+ def proxy_method(method_node:)
106
+ params = method_node.parameters.slice
107
+ # Not a security risk because the code comes from a trusted source; the file that did the include. Does the file trust itself?
108
+ eval("-> (#{params}) {}", binding, __FILE__, __LINE__) # rubocop:disable Security/Eval
109
+ end
91
110
 
92
111
  def required_args(proxy_method:)
93
112
  required_args = []
data/lib/syntax.rb ADDED
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LowType
4
+ module Syntax
5
+ refine Array.singleton_class do
6
+ def [](*types)
7
+ return LowType::TypeExpression.new(type: [*types]) if types.all? { |type| LowType.type?(type) }
8
+
9
+ super
10
+ end
11
+ end
12
+
13
+ refine Hash.singleton_class do
14
+ def [](type)
15
+ return LowType::TypeExpression.new(type:) if LowType.type?(type)
16
+
17
+ super
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ # Refine doesn't support inheritence.
24
+ class Object
25
+ # For "Type | [type_expression/type/value]" situations, redirecting to or generating a type expression from types.
26
+ # "|" is not defined on Object class and this is the most compute-efficient way to achieve our goal (world peace).
27
+ # "|" is overridable by any child object. While we could def/undef this method, this approach is actually lighter.
28
+ # "|" bitwise operator on Integer is not defined when the receiver is an Integer class, so we are not in conflict.
29
+ class << self
30
+ def |(expression)
31
+ if expression.instance_of?(::LowType::TypeExpression)
32
+ # We pass our type into their type expression.
33
+ expression | self
34
+ expression
35
+ else
36
+ # We turn our type into a type expression and pass in their [type_expression/type/value].
37
+ type_expression = ::LowType::TypeExpression.new(type: self)
38
+ type_expression | expression
39
+ end
40
+ end
41
+ end
42
+ 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::' namespace 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
@@ -5,4 +5,5 @@ module LowType
5
5
  class LocalTypeError < TypeError; end
6
6
  class ReturnTypeError < TypeError; end
7
7
  class AllowedTypeError < TypeError; end
8
+ class ConfigError < TypeError; end
8
9
  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.9.0'
4
+ VERSION = '1.0.1'
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.9.0
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - maedi
@@ -19,22 +19,23 @@ extra_rdoc_files: []
19
19
  files:
20
20
  - lib/adapters/adapter_loader.rb
21
21
  - lib/adapters/sinatra_adapter.rb
22
- - lib/basic_types.rb
23
- - lib/error_types.rb
24
22
  - lib/factories/proxy_factory.rb
23
+ - lib/file_parser.rb
25
24
  - lib/instance_types.rb
26
25
  - lib/interfaces/adapter_interface.rb
27
26
  - lib/interfaces/error_interface.rb
28
27
  - lib/local_types.rb
29
28
  - lib/low_type.rb
30
- - lib/parser.rb
31
29
  - lib/proxies/file_proxy.rb
32
30
  - lib/proxies/local_proxy.rb
33
31
  - lib/proxies/method_proxy.rb
34
32
  - lib/proxies/param_proxy.rb
35
33
  - lib/proxies/return_proxy.rb
36
34
  - lib/redefiner.rb
35
+ - lib/syntax.rb
37
36
  - lib/type_expression.rb
37
+ - lib/types/complex_types.rb
38
+ - lib/types/error_types.rb
38
39
  - lib/value_expression.rb
39
40
  - lib/version.rb
40
41
  homepage: https://codeberg.org/low_ruby/low_type
File without changes