low_type 0.2.2 → 0.4.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: f39ec6a2c5d0986b75bd52a8bec8033b38bd5ba094515994fa655e253b47a669
4
- data.tar.gz: f7aa54e8ce075b121a8e8fed025ea5a60e13a151a6891cb6aaedbe63432f8769
3
+ metadata.gz: efc284f18360245938c679ce476f0588ea31560da0b75a1076d080d8d687e07c
4
+ data.tar.gz: 7839a82ee075a6436203aaf991be31267a180e9b35f94a4a444365506f4700d8
5
5
  SHA512:
6
- metadata.gz: 021eec0aae1a09604d45f5911647000f1620a0d1482cd743ee5066ae660b7e425af246a4806b7ef50763bc1dad3587631eeeea5e428abb7cc20308dec043d9f1
7
- data.tar.gz: a238797c74dbe741db04a104ebad2f081d197bd5cea38dde2ae4ee5cb4a6735de183d1722b08229691d063c14e2f8ffd2662957fb19573f840bb19bc01cc4626
6
+ metadata.gz: b2260394d54e93340450d2d4cfccd93e3d462f91c39fec4778205c52699590f4467639ac847cf9faa697b5e468308ee1d917c077670b30a4e9d41ca3413bc7b7
7
+ data.tar.gz: 521ee18436fac4d04caa4a2787e93b65367ccbe0fdd69eee7b7479316e2d7642e0a99628fedc8fc93996c28f2620a91d51407143c4ec1baa02040a0478d5a4e8
data/lib/low_type.rb CHANGED
@@ -6,9 +6,30 @@ require_relative 'type_expression'
6
6
  module LowType
7
7
  # We do as much as possible on class load rather than on instantiation to be thread-safe and efficient.
8
8
  def self.included(klass)
9
+
10
+ # Array[] class method returns a type expression only for the duration of this "included" hook.
11
+ array_class_method = Array.method('[]').unbind
12
+ Array.define_singleton_method('[]') do |expression|
13
+ TypeExpression.new(type: [(expression)])
14
+ end
15
+
16
+ # Hash[] class method returns a type expression only for the duration of this "included" hook.
17
+ hash_class_method = Hash.method('[]').unbind
18
+ Hash.define_singleton_method('[]') do |expression|
19
+ # Support Pry which uses Hash[].
20
+ unless LowType.type?(expression)
21
+ Hash.define_singleton_method('[]', hash_class_method)
22
+ result = Hash[expression]
23
+ Hash.method('[]').unbind
24
+ return result
25
+ end
26
+
27
+ TypeExpression.new(type: expression)
28
+ end
29
+
9
30
  class << klass
10
- def low_params
11
- @low_params ||= {}
31
+ def low_methods
32
+ @low_methods ||= {}
12
33
  end
13
34
 
14
35
  def type(expression)
@@ -24,8 +45,12 @@ module LowType
24
45
 
25
46
  parser = Parser.new(file_path: LowType.file_path(klass:))
26
47
  private_start_line = parser.private_start_line
48
+
27
49
  klass.prepend LowType::Redefiner.redefine_methods(method_nodes: parser.instance_methods, private_start_line:, klass:)
28
50
  klass.singleton_class.prepend LowType::Redefiner.redefine_methods(method_nodes: parser.class_methods, private_start_line:, klass:)
51
+ ensure
52
+ Array.define_singleton_method('[]', array_class_method)
53
+ Hash.define_singleton_method('[]', hash_class_method)
29
54
  end
30
55
 
31
56
  class << self
@@ -34,7 +59,7 @@ module LowType
34
59
  end
35
60
 
36
61
  def type?(expression)
37
- expression.respond_to?(:new) || expression == Integer
62
+ expression.respond_to?(:new) || expression == Integer || (expression.is_a?(Hash) && expression.keys.first.respond_to?(:new) && expression.values.first.respond_to?(:new))
38
63
  end
39
64
 
40
65
  def value?(expression)
@@ -44,6 +69,4 @@ module LowType
44
69
 
45
70
  class ValueExpression; end
46
71
  class Boolean; end # TrueClass or FalseClass
47
- class KeyValue; end # KeyValue[String => Hash]
48
- class MixedTypes; end # MixedTypes[String | Integer]
49
72
  end
data/lib/parser.rb CHANGED
@@ -18,6 +18,15 @@ module LowType
18
18
  @class_methods = method_visitor.class_methods
19
19
  @private_start_line = method_visitor.private_start_line
20
20
  end
21
+
22
+ def self.return_node(method_node:)
23
+ # Only a lambda defined immediately after a method's parameters is considered a return type expression.
24
+ node = method_node.compact_child_nodes.find { |node| node.is_a?(Prism::StatementsNode) }.body.first
25
+
26
+ return node if node.is_a?(Prism::LambdaNode)
27
+
28
+ nil
29
+ end
21
30
  end
22
31
 
23
32
  class MethodVisitor < Prism::Visitor
data/lib/redefiner.rb CHANGED
@@ -1,3 +1,4 @@
1
+ require_relative 'method_proxy'
1
2
  require_relative 'param_proxy'
2
3
  require_relative 'parser'
3
4
  require_relative 'type_expression'
@@ -8,54 +9,56 @@ module LowType
8
9
  def redefine_methods(method_nodes:, private_start_line:, klass:)
9
10
  Module.new do
10
11
  method_nodes.each do |method_node|
11
- args = method_node.parameters.slice
12
-
13
- proxy_method = eval("-> (#{args}) {}")
14
- required_args, required_kwargs = Redefiner.required_args(proxy_method)
15
-
16
- klass.low_params[method_node.name] = Redefiner.type_expressions_from_params(proxy_method, args, required_args, required_kwargs)
17
- next if klass.low_params[method_node.name].empty?
12
+ name = method_node.name
13
+ params = Redefiner.params_with_type_expressions(method_node:)
14
+ return_expression = Redefiner.return_type_expression(method_node:)
15
+
16
+ klass.low_methods[name] = MethodProxy.new(name:, params:, return_expression:)
17
+
18
+ define_method(name) do |*args, **kwargs|
19
+ klass.low_methods[name].params.each do |param_proxy|
20
+ value = param_proxy.position ? args[param_proxy.position] : kwargs[param_proxy.name]
21
+ value = param_proxy.type_expression.default_value if value.nil? && param_proxy.type_expression.default_value != :LOW_TYPE_UNDEFINED
22
+ param_proxy.type_expression.validate!(value:, name: param_proxy.name)
23
+ param_proxy.position ? args[param_proxy.position] = value : kwargs[param_proxy.name] = value
24
+ end
18
25
 
19
- define_method(method_node.name) do |*args, **kwargs|
20
- klass.low_params[method_node.name].each do |param_proxy|
21
- arg = param_proxy.position ? args[param_proxy.position] : kwargs[param_proxy.name]
22
- arg = param_proxy.type_expression.default_value if arg.nil? && param_proxy.type_expression.default_value != :LOW_TYPE_UNDEFINED
23
- param_proxy.type_expression.validate!(arg:, name: param_proxy.name)
24
- param_proxy.position ? args[param_proxy.position] = arg : kwargs[param_proxy.name] = arg
26
+ if return_expression
27
+ return_value = super(*args, **kwargs)
28
+ return_expression.validate!(value: return_value, name:)
29
+ return return_value
25
30
  end
26
31
 
27
32
  super(*args, **kwargs)
28
33
  end
29
34
 
30
35
  if private_start_line && method_node.start_line > private_start_line
31
- private method_node.name
36
+ private name
32
37
  end
33
38
  end
34
39
  end
35
40
  end
36
41
 
37
- def required_args(proxy_method)
38
- required_args = []
39
- required_kwargs = {}
42
+ def return_type_expression(method_node:)
43
+ return_node = Parser.return_node(method_node:)
44
+ return nil if return_node.nil?
40
45
 
41
- proxy_method.parameters.each do |param|
42
- param_type, param_name = param
43
-
44
- case param_type
45
- when :req
46
- required_args << nil
47
- when :keyreq
48
- required_kwargs[param_name] = nil
49
- end
50
- end
46
+ expression = eval(return_node.slice).call
51
47
 
52
- [required_args, required_kwargs]
48
+ return expression if expression.class == TypeExpression
49
+ TypeExpression.new(type: expression)
53
50
  end
54
51
 
55
- def type_expressions_from_params(proxy_method, args, required_args, required_kwargs)
52
+ def params_with_type_expressions(method_node:)
53
+ return [] if method_node.parameters.nil?
54
+
55
+ params = method_node.parameters.slice
56
+ proxy_method = eval("-> (#{params}) {}")
57
+ required_args, required_kwargs = Redefiner.required_args(proxy_method:)
58
+
56
59
  typed_method = eval(
57
60
  <<~RUBY
58
- -> (#{args}) {
61
+ -> (#{params}) {
59
62
  param_proxies = []
60
63
 
61
64
  proxy_method.parameters.each_with_index do |param, position|
@@ -78,6 +81,28 @@ module LowType
78
81
 
79
82
  # Call method with only its required args to evaluate type expressions (which are stored as default values).
80
83
  typed_method.call(*required_args, **required_kwargs)
84
+
85
+ # TODO: Write spec for this.
86
+ rescue ArgumentError => e
87
+ raise ArgumentError, "Incorrect param syntax"
88
+ end
89
+
90
+ def required_args(proxy_method:)
91
+ required_args = []
92
+ required_kwargs = {}
93
+
94
+ proxy_method.parameters.each do |param|
95
+ param_type, param_name = param
96
+
97
+ case param_type
98
+ when :req
99
+ required_args << nil
100
+ when :keyreq
101
+ required_kwargs[param_name] = nil
102
+ end
103
+ end
104
+
105
+ [required_args, required_kwargs]
81
106
  end
82
107
  end
83
108
  end
@@ -1,10 +1,10 @@
1
1
  module LowType
2
2
  class TypeExpression
3
- attr_reader :type, :default_value
3
+ attr_reader :types, :default_value
4
4
 
5
- def initialize(type:)
5
+ def initialize(type:, default_value: :LOW_TYPE_UNDEFINED)
6
6
  @types = [type]
7
- @default_value = :LOW_TYPE_UNDEFINED
7
+ @default_value = default_value
8
8
  end
9
9
 
10
10
  def |(expression)
@@ -24,12 +24,22 @@ module LowType
24
24
  @default_value == :LOW_TYPE_UNDEFINED
25
25
  end
26
26
 
27
- def validate!(arg:, name:)
28
- if arg.nil? && required?
29
- raise ArgumentError, "Missing required argument of type '#{@types.join(', ')}' for '#{name}'"
27
+ def validate!(value:, name:)
28
+ if value.nil?
29
+ return true if @default_value.nil?
30
+ raise ArgumentError, "Missing required argument of type '#{@types.join(', ')}' for '#{name}'" if required?
30
31
  end
31
32
 
32
- raise TypeError, "Invalid type '#{arg.class}' for '#{name}'" unless @types.include?(arg.class)
33
+ @types.each do |type|
34
+ return true if LowType.type?(type) && type == value.class
35
+ # TODO: Shallow validation of enumerables could be made deeper with user config.
36
+ return true if type.class == Array && value.class == Array && type.first == value.first.class
37
+ if type.class == Hash && value.class == Hash && type.keys[0] == value.keys[0].class && type.values[0] == value.values[0].class
38
+ return true
39
+ end
40
+ end
41
+
42
+ raise TypeError, "Invalid type '#{value.class}' for '#{name}'"
33
43
  end
34
44
  end
35
45
  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.2.2'
4
+ VERSION = '0.4.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.2.2
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - maedi