low_type 1.0.8 → 1.1.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: 6e3e1dca0d960082a4de505f3422e00c648f660306c22d24db65590db0543f5e
4
- data.tar.gz: a87156e750aee311a9afb2a95166a9d00f99a744c05fc1dac0d9ab7120882760
3
+ metadata.gz: d8dae7f03ed51a1acea72d53deec50af0722e9c57ac8baab72297420bae36f4b
4
+ data.tar.gz: d7de0927fb139ac7d2b934c01f464224bb30fe2fbba6ca8b62a789955561e602
5
5
  SHA512:
6
- metadata.gz: f8e3054ab427c0635aa27f78ae966e3a90c9f78ec660651d6025956ddc83582a1b46f66268e7a335d8ff72ed87bf92a09672a38509067d42d3b79253a97e8d49
7
- data.tar.gz: 4698ae94c57a2de408ba085029f18b1bc100375972f3f253294dfcef71741d959da1db6773544f4bf55133d5a95d3e2eee44f274f167c503e7f462489e247153
6
+ metadata.gz: f4a91869b8c041091cd2dcadfb5ff931883041f7d57319b6cfb60b8ef7c1607924d81b93fc1a936724a3d577e904ed1d76c5bb03c89d6bf872e6446f0490f52e
7
+ data.tar.gz: d42d49968af57b0122e76449d01a5ed118bb4394d28fe42a6a9ec9095f6900ec02940e237e75fe3c1c4417b307c0b85497193a492b721e3c139902a15d92be7b
@@ -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
@@ -10,9 +10,18 @@ module LowType
10
10
  @output_size = LowType.config.output_size
11
11
  end
12
12
 
13
+ def error_type
14
+ raise NotImplementedError
15
+ end
16
+
17
+ def error_message(value:)
18
+ raise NotImplementedError
19
+ end
20
+
13
21
  def output(value:)
14
22
  case @output_mode
15
23
  when :type
24
+ # TODO: Show full type structure in error output instead of just the type of the supertype.
16
25
  value.class
17
26
  when :value
18
27
  value.inspect[0...@output_size]
@@ -21,12 +30,16 @@ module LowType
21
30
  end
22
31
  end
23
32
 
24
- def error_type
25
- raise NotImplementedError
26
- end
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) } }
27
36
 
28
- def error_message(value:)
29
- raise NotImplementedError
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]
30
43
  end
31
44
  end
32
45
  end
@@ -1,27 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../type_expression'
4
+
3
5
  module LowType
4
6
  # TODO: Unit test.
5
7
  class TypeQuery
6
8
  class << self
7
- def type?(type)
8
- basic_type?(type:) || complex_type?(type:)
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))
9
15
  end
10
16
 
11
- def basic_type?(type:)
12
- type.instance_of?(Class)
17
+ def value?(value)
18
+ !basic_type?(expression: value) && !complex_type?(expression: value)
13
19
  end
14
20
 
15
- def complex_type?(type:)
16
- !basic_type?(type:) && typed_hash?(type:)
21
+ def complex_type?(expression:)
22
+ LowType::COMPLEX_TYPES.include?(expression) || typed_array?(expression:) || typed_hash?(expression:)
17
23
  end
18
24
 
19
- def typed_hash?(type:)
20
- type.is_a?(::Hash) && basic_type?(type: type.keys.first) && basic_type?(type: type.values.first)
25
+ private
26
+
27
+ def basic_type?(expression:)
28
+ expression.instance_of?(Class)
21
29
  end
22
30
 
23
- def value?(expression)
24
- !expression.respond_to?(:new) && expression != Integer
31
+ def typed_hash?(expression:)
32
+ expression.is_a?(Hash) && basic_type?(expression: expression.keys.first) && basic_type?(expression: expression.values.first)
25
33
  end
26
34
  end
27
35
  end
data/lib/redefiner.rb CHANGED
@@ -106,7 +106,7 @@ module LowType
106
106
  }
107
107
  RUBY
108
108
 
109
- # 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).
110
110
  eval(typed_method, binding, __FILE__, __LINE__).call(*required_args, **required_kwargs) # rubocop:disable Security/Eval
111
111
 
112
112
  # TODO: Write spec for this.
data/lib/syntax/syntax.rb CHANGED
@@ -5,8 +5,8 @@ require_relative '../queries/type_query'
5
5
  module LowType
6
6
  module Syntax
7
7
  refine Array.singleton_class do
8
- def [](*types)
9
- return TypeExpression.new(type: [*types]) if types.all? { |type| TypeQuery.type?(type) }
8
+ def [](*expression)
9
+ return TypeExpression.new(type: [*expression]) if TypeQuery.type?(expression.first) || TypeQuery.typed_array?(expression:)
10
10
 
11
11
  super
12
12
  end
@@ -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?(expression: 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.8'
4
+ VERSION = '1.1.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: 1.0.8
4
+ version: 1.1.1
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