low_type 1.0.7 → 1.1.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: 6ef30b8ad7d079a95270a664aacb4b28255cad9dcd743451e1f78b1391549222
4
- data.tar.gz: 4e4329460d20826d604b99d1d7e57cee581cb7c3d8e6ffe2621e7ed383967b57
3
+ metadata.gz: a175f78281d51fe16720ca8231b1790506d2eea0d96660c52ff78f1717689836
4
+ data.tar.gz: f8615b0754c2a5e30e99e37922311d7d9436a37025f3a1a76334d4b14da21e7d
5
5
  SHA512:
6
- metadata.gz: c08f01a89a82f293c0320034eceff0831b87a63ec52c45f0bd01ebd23ef067af2099bd8f554c45579ef285284c9afa34713556962a8a2b167d99bf5bc65ce73b
7
- data.tar.gz: d90226573c175294ecc61258b1ed013f9b2351b4fdea32672bed0cf0786dbbea64341021710d21c0826feb152257fbaf4fc11d24cf180b9b3ac739523afef74a
6
+ metadata.gz: 04d69b82160152e0c04cc114e196c68fc5122dc79fa3b622ebafa21106a01ab8d44d7a483df497f2cc51c8280c2038bbf114b680505a2c9e35b73b9cbef2cd04
7
+ data.tar.gz: f2baaeeeaaec312a7306c12094e3b4cd661a935e3b6072b8824df5451fbd88538ffcacc48ba6beedc06e75ec2d685f015227289c529f051cadd246de127aa033
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LowType
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
@@ -4,6 +4,12 @@ module LowType
4
4
  class ErrorInterface
5
5
  attr_reader :file
6
6
 
7
+ def initialize
8
+ @file = nil
9
+ @output_mode = LowType.config.output_mode
10
+ @output_size = LowType.config.output_size
11
+ end
12
+
7
13
  def error_type
8
14
  raise NotImplementedError
9
15
  end
@@ -11,5 +17,29 @@ module LowType
11
17
  def error_message(value:)
12
18
  raise NotImplementedError
13
19
  end
20
+
21
+ def output(value:)
22
+ case @output_mode
23
+ when :type
24
+ # TODO: Show full type structure in error output instead of just the type of the supertype.
25
+ value.class
26
+ when :value
27
+ value.inspect[0...@output_size]
28
+ else
29
+ 'REDACTED'
30
+ end
31
+ end
32
+
33
+ def backtrace(backtrace:, hidden_paths:)
34
+ # Remove LowType defined method file paths from the backtrace.
35
+ filtered_backtrace = backtrace.reject { |line| hidden_paths.find { |file_path| line.include?(file_path) } }
36
+
37
+ # Add the proxied file to the backtrace.
38
+ proxy_file_backtrace = "#{file.path}:#{file.line}:in '#{file.scope}'"
39
+ from_prefix = filtered_backtrace.first.match(/\s+from /)
40
+ proxy_file_backtrace = "#{from_prefix}#{proxy_file_backtrace}" if from_prefix
41
+
42
+ [proxy_file_backtrace, *filtered_backtrace]
43
+ end
14
44
  end
15
45
  end
data/lib/low_type.rb CHANGED
@@ -37,8 +37,8 @@ module LowType
37
37
 
38
38
  class << self
39
39
  def config
40
- config = Struct.new(:deep_type_check, :severity_level, :union_type_expressions)
41
- @config ||= config.new(false, :error, true)
40
+ config = Struct.new(:error_mode, :output_mode, :output_size, :deep_type_check, :union_type_expressions)
41
+ @config ||= config.new(:error, :type, 100, false, true)
42
42
  end
43
43
 
44
44
  def configure
@@ -8,6 +8,8 @@ module LowType
8
8
  attr_reader :type_expression, :name
9
9
 
10
10
  def initialize(type_expression:, name:, file:)
11
+ super()
12
+
11
13
  @type_expression = type_expression
12
14
  @name = name
13
15
  @file = file
@@ -18,7 +20,7 @@ module LowType
18
20
  end
19
21
 
20
22
  def error_message(value:)
21
- "Invalid variable type #{value.class} in '#{@name.class}' on line #{@file.line}. Valid types: '#{@type_expression.valid_types}'"
23
+ "Invalid variable type #{output(value:)} in '#{@name.class}' on line #{@file.line}. Valid types: '#{@type_expression.valid_types}'"
22
24
  end
23
25
  end
24
26
  end
@@ -8,6 +8,8 @@ module LowType
8
8
  attr_reader :type_expression, :name, :type, :position
9
9
 
10
10
  def initialize(type_expression:, name:, type:, file:, position: nil)
11
+ super()
12
+
11
13
  @type_expression = type_expression
12
14
  @name = name
13
15
  @type = type
@@ -20,7 +22,7 @@ module LowType
20
22
  end
21
23
 
22
24
  def error_message(value:)
23
- "Invalid argument type '#{value.class}' for parameter '#{@name}'. Valid types: '#{@type_expression.valid_types}'"
25
+ "Invalid argument type '#{output(value:)}' for parameter '#{@name}'. Valid types: '#{@type_expression.valid_types}'"
24
26
  end
25
27
  end
26
28
  end
@@ -8,6 +8,8 @@ module LowType
8
8
  attr_reader :type_expression, :name
9
9
 
10
10
  def initialize(type_expression:, name:, file:)
11
+ super()
12
+
11
13
  @type_expression = type_expression
12
14
  @name = name
13
15
  @file = file
@@ -18,7 +20,7 @@ module LowType
18
20
  end
19
21
 
20
22
  def error_message(value:)
21
- "Invalid return type '#{value.class}' for method '#{@name}'. Valid types: '#{@type_expression.valid_types}'"
23
+ "Invalid return type '#{output(value:)}' for method '#{@name}'. Valid types: '#{@type_expression.valid_types}'"
22
24
  end
23
25
  end
24
26
  end
@@ -62,7 +62,7 @@ module LowType
62
62
 
63
63
  @instance_methods = []
64
64
  @class_methods = []
65
- @line_numbers = { class_start: 0, class_end: root_node.respond_to?(:end_line) ? root_node.end_line : nil}
65
+ @line_numbers = { class_start: 0, class_end: root_node.respond_to?(:end_line) ? root_node.end_line : nil }
66
66
  end
67
67
 
68
68
  def visit_def_node(node)
@@ -1,19 +1,21 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module LowType
2
4
  class FileQuery
3
5
  class << self
4
6
  def file_path(klass:)
5
- includer_line = line_with_class(klass:) || line_with_include || ''
7
+ includer_line = line_from_class(klass:) || line_from_include || ''
6
8
  includer_line.split(':').first || ''
7
9
  end
8
10
 
9
11
  private
10
12
 
11
- def line_with_class(klass:)
13
+ def line_from_class(klass:)
12
14
  class_name = klass.to_s.split(':').last # Also remove the module namespaces from the class.
13
15
  caller.find { |callee| callee.end_with?("<class:#{class_name}>'") }
14
16
  end
15
17
 
16
- def line_with_include
18
+ def line_from_include
17
19
  caller.find { |callee| callee.end_with?("include'") }
18
20
  end
19
21
  end
@@ -1,3 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../type_expression'
4
+
1
5
  module LowType
2
6
  # TODO: Unit test.
3
7
  class TypeQuery
@@ -6,20 +10,26 @@ module LowType
6
10
  basic_type?(type:) || complex_type?(type:)
7
11
  end
8
12
 
9
- def basic_type?(type:)
10
- type.class == Class
13
+ def value?(value)
14
+ !basic_type?(type: value) && !complex_type?(type: value)
11
15
  end
12
16
 
13
17
  def complex_type?(type:)
14
- !basic_type?(type:) && typed_hash?(type:)
18
+ LowType::COMPLEX_TYPES.include?(type) || typed_array?(type:) || typed_hash?(type:)
15
19
  end
16
20
 
17
- def typed_hash?(type:)
18
- type.is_a?(::Hash) && basic_type?(type: type.keys.first) && basic_type?(type: type.values.first)
21
+ private
22
+
23
+ def basic_type?(type:)
24
+ type.instance_of?(Class)
25
+ end
26
+
27
+ def typed_array?(type:)
28
+ type.is_a?(Array) && (basic_type?(type: type.first) || type.first.is_a?(TypeExpression))
19
29
  end
20
30
 
21
- def value?(expression)
22
- !expression.respond_to?(:new) && expression != Integer
31
+ def typed_hash?(type:)
32
+ type.is_a?(Hash) && basic_type?(type: type.keys.first) && basic_type?(type: type.values.first)
23
33
  end
24
34
  end
25
35
  end
data/lib/redefiner.rb CHANGED
@@ -5,8 +5,8 @@ require_relative 'factories/proxy_factory'
5
5
  require_relative 'proxies/file_proxy'
6
6
  require_relative 'proxies/method_proxy'
7
7
  require_relative 'proxies/param_proxy'
8
- require_relative 'syntax/syntax'
9
8
  require_relative 'queries/type_query'
9
+ require_relative 'syntax/syntax'
10
10
  require_relative 'type_expression'
11
11
  require_relative 'value_expression'
12
12
 
@@ -45,9 +45,7 @@ module LowType
45
45
  method_start = method_node.respond_to?(:start_line) ? method_node.start_line : nil
46
46
  method_end = method_node.respond_to?(:end_line) ? method_node.end_line : nil
47
47
 
48
- if method_start && method_end && class_end
49
- next unless method_start > class_start && method_end <= class_end
50
- end
48
+ next if method_start && method_end && class_end && !(method_start > class_start && method_end <= class_end)
51
49
 
52
50
  name = method_node.name
53
51
 
@@ -108,7 +106,7 @@ module LowType
108
106
  }
109
107
  RUBY
110
108
 
111
- # Called with only required args (as nil) and optional args omitted, to evaluate type expressions (stored as default values).
109
+ # Called with only required args (as nil) and optional args omitted, to evaluate type expressions (from default values).
112
110
  eval(typed_method, binding, __FILE__, __LINE__).call(*required_args, **required_kwargs) # rubocop:disable Security/Eval
113
111
 
114
112
  # TODO: Write spec for this.
data/lib/syntax/syntax.rb CHANGED
@@ -6,14 +6,16 @@ module LowType
6
6
  module Syntax
7
7
  refine Array.singleton_class do
8
8
  def [](*types)
9
- return LowType::TypeExpression.new(type: [*types]) if types.all? { |type| LowType::TypeQuery.type?(type) }
9
+ return TypeExpression.new(type: [*types]) if types.all? { |type| TypeQuery.type?(type) }
10
+
10
11
  super
11
12
  end
12
13
  end
13
14
 
14
15
  refine Hash.singleton_class do
15
16
  def [](type)
16
- return LowType::TypeExpression.new(type:) if LowType::TypeQuery.type?(type)
17
+ return TypeExpression.new(type:) if TypeQuery.type?(type)
18
+
17
19
  super
18
20
  end
19
21
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  ###
2
4
  # Type expressions from union types.
3
5
  #
@@ -5,10 +5,10 @@ require_relative 'queries/type_query'
5
5
 
6
6
  module LowType
7
7
  root_path = File.expand_path(__dir__)
8
- file_path = File.expand_path(__FILE__)
9
8
  adapter_paths = Dir.chdir(root_path) { Dir.glob('adapters/*') }.map { |path| File.join(root_path, path) }
9
+ module_paths = %w[instance_types local_types redefiner].map { |path| File.join(root_path, "#{path}.rb") }
10
10
 
11
- HIDDEN_PATHS = [file_path, *adapter_paths, File.join(root_path, 'redefiner.rb')].freeze
11
+ HIDDEN_PATHS = [File.expand_path(__FILE__), *adapter_paths, *module_paths].freeze
12
12
 
13
13
  # Represent types and default values as a series of chainable expressions.
14
14
  class TypeExpression
@@ -19,6 +19,8 @@ module LowType
19
19
  @types = []
20
20
  @types << type unless type.nil?
21
21
  @default_value = default_value
22
+ # TODO: Override per type expression with a config expression.
23
+ @deep_type_check = LowType.config.deep_type_check
22
24
  end
23
25
 
24
26
  def |(expression)
@@ -45,64 +47,94 @@ module LowType
45
47
  end
46
48
 
47
49
  @types.each do |type|
48
- # Example: HTML is a subclass of String and should pass as a String.
49
- return true if LowType::TypeQuery.basic_type?(type:) && type <= value.class
50
- return true if type.is_a?(::Array) && value.is_a?(::Array) && array_types_match_values?(types: type, values: value)
51
- return true if type.is_a?(::Hash) && value.is_a?(::Hash) && hash_types_match_values?(type:, value:)
50
+ return true if type_matches_value?(type:, value:, proxy:)
51
+ return true if type.is_a?(Array) && value.is_a?(Array) && array_types_match_values?(types: type, values: value, proxy:)
52
+ return true if type.is_a?(Hash) && value.is_a?(Hash) && hash_types_match_values?(types: type, values: value)
52
53
  end
53
54
 
54
55
  raise proxy.error_type, proxy.error_message(value:)
55
56
  rescue proxy.error_type => e
56
- raise proxy.error_type, e.message, backtrace_with_proxy(backtrace: e.backtrace, proxy:)
57
+ raise proxy.error_type, e.message, proxy.backtrace(backtrace: e.backtrace, hidden_paths: HIDDEN_PATHS)
57
58
  end
58
59
 
59
60
  def valid_types
60
61
  types = @types.map do |type|
61
- # Remove 'LowType::' namespace in subtypes.
62
- if type.is_a?(::Array)
63
- "[#{type.map { |subtype| subtype.to_s.delete_prefix('LowType::') }.join(', ')}]"
62
+ if type.is_a?(Array)
63
+ "[#{type.map { |subtype| valid_subtype(subtype:) }.join(', ')}]"
64
64
  else
65
65
  type.inspect.to_s.delete_prefix('LowType::')
66
66
  end
67
67
  end
68
68
 
69
- types += ['nil'] if @default_value.nil?
69
+ types << 'nil' if @default_value.nil?
70
70
  types.join(' | ')
71
71
  end
72
72
 
73
73
  private
74
74
 
75
- def array_types_match_values?(types:, values:)
76
- # [T, T, T]
75
+ def valid_subtype(subtype:)
76
+ if subtype.is_a?(TypeExpression)
77
+ types = subtype.types
78
+ types << 'nil' if subtype.default_value.nil?
79
+ types.join(' | ')
80
+ else
81
+ subtype.to_s.delete_prefix('LowType::')
82
+ end
83
+ end
84
+
85
+ def array_types_match_values?(types:, values:, proxy:)
86
+ # [X, Y, Z] An arbitrary amount of elements are arbitrary types in an arbitrary order.
77
87
  if types.length > 1
78
- types.each_with_index do |type, index|
79
- # Example: HTML is a subclass of String and should pass as a String.
80
- return false unless type <= values[index].class
81
- end
82
- # [T]
88
+ return multiple_types_match_values?(types:, values:, proxy:)
89
+ # [T] All elements are the same type.
83
90
  elsif types.length == 1
84
- return false unless types.first == values.first.class
91
+ return single_type_matches_values?(type: types.first, values:, proxy:)
92
+ end
93
+
94
+ # [] Misconfigured empty Array[] type.
95
+ true
96
+ end
97
+
98
+ def multiple_types_match_values?(types:, values:, proxy:)
99
+ types.each_with_index do |type, index|
100
+ return false unless type_matches_value?(type:, value: values[index], proxy:)
85
101
  end
86
- # TODO: Deep type check (all elements for [T]).
87
102
 
88
103
  true
89
104
  end
90
105
 
91
- def hash_types_match_values?(type:, value:)
106
+ def single_type_matches_values?(type:, values:, proxy:)
107
+ # [V, ...] Type check all elements.
108
+ if deep_type_check?
109
+ return false if values.any? { |value| !type_matches_value?(type:, value:, proxy:) }
110
+ # [V] Type check the first element.
111
+ else
112
+ return false unless type_matches_value?(type:, value: values.first, proxy:)
113
+ end
114
+
115
+ true
116
+ end
117
+
118
+ def hash_types_match_values?(types:, values:)
92
119
  # TODO: Shallow validation of hash could be made deeper with user config.
93
- type.keys[0] == value.keys[0].class && type.values[0] == value.values[0].class
120
+ types.keys[0] == values.keys[0].class && types.values[0] == values.values[0].class
94
121
  end
95
122
 
96
- def backtrace_with_proxy(proxy:, backtrace:)
97
- # Remove LowType defined method file paths from the backtrace.
98
- filtered_backtrace = backtrace.reject { |line| HIDDEN_PATHS.find { |file_path| line.include?(file_path) } }
123
+ def type_matches_value?(type:, value:, proxy:)
124
+ if type.instance_of?(Class)
125
+ return type.match?(value:) if LowType::TypeQuery.complex_type?(type:)
126
+
127
+ return type == value.class
128
+ elsif type.instance_of?(::LowType::TypeExpression)
129
+ type.validate!(value:, proxy:)
130
+ return true
131
+ end
99
132
 
100
- # Add the proxied file to the backtrace.
101
- proxy_file_backtrace = "#{proxy.file.path}:#{proxy.file.line}:in '#{proxy.file.scope}'"
102
- from_prefix = filtered_backtrace.first.match(/\s+from /)
103
- proxy_file_backtrace = "#{from_prefix}#{proxy_file_backtrace}" if from_prefix
133
+ false
134
+ end
104
135
 
105
- [proxy_file_backtrace, *filtered_backtrace]
136
+ def deep_type_check?
137
+ @deep_type_check || LowType.config.deep_type_check || false
106
138
  end
107
139
  end
108
140
  end
@@ -1,12 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../factories/type_factory'
4
+
3
5
  module LowType
4
- # TrueClass or FalseClass
5
- class Boolean; end
6
- class Tuple < Array; end
7
- class Status < Integer; end
8
- class Headers < Hash; end
9
- class HTML < String; end
10
- class JSON < String; end
11
- class XML < String; end
6
+ COMPLEX_TYPES = [
7
+ Boolean = TypeFactory.complex_type(Object),
8
+ Headers = TypeFactory.complex_type(Hash),
9
+ HTML = TypeFactory.complex_type(String),
10
+ JSON = TypeFactory.complex_type(String),
11
+ Status = TypeFactory.complex_type(Integer),
12
+ Tuple = TypeFactory.complex_type(Array),
13
+ XML = TypeFactory.complex_type(String)
14
+ ].freeze
12
15
  end
data/lib/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LowType
4
- VERSION = '1.0.7'
4
+ VERSION = '1.1.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: 1.0.7
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - maedi
@@ -21,6 +21,7 @@ files:
21
21
  - lib/adapters/sinatra_adapter.rb
22
22
  - lib/factories/expression_factory.rb
23
23
  - lib/factories/proxy_factory.rb
24
+ - lib/factories/type_factory.rb
24
25
  - lib/instance_types.rb
25
26
  - lib/interfaces/adapter_interface.rb
26
27
  - lib/interfaces/error_interface.rb