low_type 0.4.1 → 0.6.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: 4ec90c752620d94c2f38e287ce6927684bb77b17caf65c9551765a37b031d2ab
4
- data.tar.gz: 6d6106390f7286f5d1812c4de79fe1a72f021db0d0b0f62c1dce5bf5a4cfc02c
3
+ metadata.gz: 84f89979651e711cb013a30df1a39f9becc619153f3fde1ac2f1810c6c0be9dc
4
+ data.tar.gz: ea670426a49377352c7b5503b897362c0aa1fbfef1b418d9908ba237386ae37c
5
5
  SHA512:
6
- metadata.gz: 791da36eca530ac38f459cbdb25253089be16e1d2c544f478c3c0fc80a7249f5c942706ec93cc4ef5340779d746f5c3be559d435f8bbdb25f2d092c6d9128681
7
- data.tar.gz: a92a680924fd3219cc5b88e6097aaadb12fb0f0f26d175591bbcd27c5d045286dd5c392d3218f5ed1600b639a79be5be643d6c0784212a7c72e6dde945946625
6
+ metadata.gz: 5aeaefb6861e7500b60ecbca69192cbc0893b310e853730d876719e83adb7943cbc8cb3b8636f12a4e838da8bd4ef4aa503790a226230cc9ecd4473a1ba3faca
7
+ data.tar.gz: f4272f09bf0ee57dfd85df4effe1568eba8ff1e7e5136742a8f90c1735df2d32969af269c5269390da81a2c317155b1730f7a73278378ee0ad5d8402cf64f831
data/lib/low_type.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative 'redefiner'
4
4
  require_relative 'type_expression'
5
+ require_relative 'value_expression'
5
6
 
6
7
  module LowType
7
8
  # We do as much as possible on class load rather than on instantiation to be thread-safe and efficient.
@@ -31,16 +32,6 @@ module LowType
31
32
  def low_methods
32
33
  @low_methods ||= {}
33
34
  end
34
-
35
- def type(expression)
36
- # TODO: Runtime type expression for the supplied variable.
37
- end
38
- alias_method :low_type, :type
39
-
40
- def value(expression)
41
- # TODO: Cancel out a type expression.
42
- end
43
- alias_method :low_value, :value
44
35
  end
45
36
 
46
37
  parser = Parser.new(file_path: LowType.file_path(klass:))
@@ -54,19 +45,40 @@ module LowType
54
45
  end
55
46
 
56
47
  class << self
48
+ # Public API.
49
+
50
+ def config
51
+ config = Struct.new(:type_assignment, :deep_type_check)
52
+ @config ||= config.new(false, false)
53
+ end
54
+
55
+ def configure
56
+ yield(config)
57
+
58
+ if config.type_assignment
59
+ require_relative 'type_assignment'
60
+ include TypeAssignment
61
+ end
62
+ end
63
+
64
+ # Internal API.
65
+
57
66
  def file_path(klass:)
58
67
  caller.find { |callee| callee.end_with?("<class:#{klass}>'") }.split(':').first
59
68
  end
60
69
 
61
- def type?(expression)
62
- expression.respond_to?(:new) || expression == Integer || (expression.is_a?(Hash) && expression.keys.first.respond_to?(:new) && expression.values.first.respond_to?(:new))
70
+ def type?(type)
71
+ type.respond_to?(:new) || type == Integer || (type.is_a?(::Hash) && type.keys.first.respond_to?(:new) && type.values.first.respond_to?(:new))
63
72
  end
64
73
 
65
74
  def value?(expression)
66
75
  !expression.respond_to?(:new) && expression != Integer
67
76
  end
77
+
78
+ def value(type:)
79
+ TypeExpression.new(default_value: ValueExpression.new(value: type))
80
+ end
68
81
  end
69
82
 
70
- class ValueExpression; end
71
83
  class Boolean; end # TrueClass or FalseClass
72
84
  end
data/lib/parser.rb CHANGED
@@ -21,8 +21,10 @@ module LowType
21
21
 
22
22
  def self.return_node(method_node:)
23
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
24
+ statements_node = method_node.compact_child_nodes.find { |node| node.is_a?(Prism::StatementsNode) }
25
+ return nil if statements_node.nil? # Sometimes developers define methods without code inside them.
25
26
 
27
+ node = statements_node.body.first
26
28
  return node if node.is_a?(Prism::LambdaNode)
27
29
 
28
30
  nil
data/lib/redefiner.rb CHANGED
@@ -4,6 +4,8 @@ require_relative 'parser'
4
4
  require_relative 'type_expression'
5
5
 
6
6
  module LowType
7
+ class ReturnError < StandardError; end
8
+
7
9
  class Redefiner
8
10
  class << self
9
11
  def redefine_methods(method_nodes:, private_start_line:, klass:)
@@ -17,15 +19,20 @@ module LowType
17
19
 
18
20
  define_method(name) do |*args, **kwargs|
19
21
  klass.low_methods[name].params.each do |param_proxy|
22
+ # Get argument value or default value.
20
23
  value = param_proxy.position ? args[param_proxy.position] : kwargs[param_proxy.name]
21
24
  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)
25
+ # Validate argument type.
26
+ param_proxy.type_expression.validate!(value:, name: param_proxy.name, error_type: ArgumentError, error_keyword: 'required')
27
+ # Handle value(type) special case.
28
+ value = value.value if value.is_a?(ValueExpression)
29
+ # Redefine argument value.
23
30
  param_proxy.position ? args[param_proxy.position] = value : kwargs[param_proxy.name] = value
24
31
  end
25
32
 
26
33
  if return_expression
27
34
  return_value = super(*args, **kwargs)
28
- return_expression.validate!(value: return_value, name:)
35
+ return_expression.validate!(value: return_value, name:, error_type: ReturnError, error_keyword: 'return')
29
36
  return return_value
30
37
  end
31
38
 
@@ -39,22 +46,12 @@ module LowType
39
46
  end
40
47
  end
41
48
 
42
- def return_type_expression(method_node:)
43
- return_node = Parser.return_node(method_node:)
44
- return nil if return_node.nil?
45
-
46
- expression = eval(return_node.slice).call
47
-
48
- return expression if expression.class == TypeExpression
49
- TypeExpression.new(type: expression)
50
- end
51
-
52
49
  def params_with_type_expressions(method_node:)
53
50
  return [] if method_node.parameters.nil?
54
51
 
55
52
  params = method_node.parameters.slice
56
53
  proxy_method = eval("-> (#{params}) {}")
57
- required_args, required_kwargs = Redefiner.required_args(proxy_method:)
54
+ required_args, required_kwargs = required_args(proxy_method:)
58
55
 
59
56
  typed_method = eval(
60
57
  <<~RUBY
@@ -84,9 +81,21 @@ module LowType
84
81
 
85
82
  # TODO: Write spec for this.
86
83
  rescue ArgumentError => e
87
- raise ArgumentError, "Incorrect param syntax"
84
+ raise ArgumentError, "Incorrect param syntax: #{e.message}"
85
+ end
86
+
87
+ def return_type_expression(method_node:)
88
+ return_node = Parser.return_node(method_node:)
89
+ return nil if return_node.nil?
90
+
91
+ expression = eval(return_node.slice).call
92
+
93
+ return expression if expression.class == TypeExpression
94
+ TypeExpression.new(type: expression)
88
95
  end
89
96
 
97
+ private
98
+
90
99
  def required_args(proxy_method:)
91
100
  required_args = []
92
101
  required_kwargs = {}
@@ -104,6 +113,11 @@ module LowType
104
113
 
105
114
  [required_args, required_kwargs]
106
115
  end
116
+
117
+ # Value expressions are eval()'d in the context of this module class (the instance doesn't exist yet) so alias API.
118
+ def value(type)
119
+ LowType.value(type:)
120
+ end
107
121
  end
108
122
  end
109
123
  end
@@ -0,0 +1,54 @@
1
+ require_relative 'type_expression'
2
+ require_relative 'value_expression'
3
+
4
+ module TypeAssignment
5
+ class AssignmentError < StandardError; end
6
+
7
+ def type(type_expression)
8
+ object = type_expression.default_value
9
+
10
+ if !LowType.value?(object)
11
+ raise AssignmentError, "Single-instance objects like #{object} are not supported"
12
+ end
13
+
14
+ object.instance_variable_set('@type_expression', type_expression)
15
+
16
+ def object.with_type=(value)
17
+ type_expression = self.instance_variable_get('@type_expression')
18
+ type_expression.validate!(value:, name: self, error_type: TypeError, error_keyword: 'object')
19
+
20
+ # We can't re-assign self in Ruby so we re-assign instance variables instead.
21
+ value.instance_variables.each do |variable|
22
+ self.instance_variable_set(variable, value.instance_variable_get(variable))
23
+ end
24
+
25
+ self
26
+ end
27
+
28
+ return object.value if object.is_a?(ValueExpression)
29
+
30
+ object
31
+ end
32
+ alias_method :low_type, :type
33
+
34
+ def value(type)
35
+ LowType.value(type:)
36
+ end
37
+ alias_method :low_value, :value
38
+ end
39
+
40
+ module LowType
41
+ class Array < ::Array
42
+ def self.[](type)
43
+ return TypeExpression.new(type: [type]) if LowType.type?(type)
44
+ super
45
+ end
46
+ end
47
+
48
+ class Hash < ::Hash
49
+ def self.[](type)
50
+ return TypeExpression.new(type:) if LowType.type?(type)
51
+ super
52
+ end
53
+ end
54
+ end
@@ -2,8 +2,9 @@ module LowType
2
2
  class TypeExpression
3
3
  attr_reader :types, :default_value
4
4
 
5
- def initialize(type:, default_value: :LOW_TYPE_UNDEFINED)
6
- @types = [type]
5
+ def initialize(type: nil, default_value: :LOW_TYPE_UNDEFINED)
6
+ @types = []
7
+ @types << type unless type.nil?
7
8
  @default_value = default_value
8
9
  end
9
10
 
@@ -24,35 +25,39 @@ module LowType
24
25
  @default_value == :LOW_TYPE_UNDEFINED
25
26
  end
26
27
 
27
- def validate!(value:, name:)
28
+ def validate!(value:, name:, error_type:, error_keyword:)
28
29
  if value.nil?
29
30
  return true if @default_value.nil?
30
- raise ArgumentError, "Missing required argument of type '#{@types.join(', ')}' for '#{name}'" if required?
31
+ raise error_type, "Missing #{error_keyword} value of type '#{@types.join(', ')}' for '#{name}'" if required?
31
32
  end
32
33
 
33
34
  @types.each do |type|
34
35
  return true if LowType.type?(type) && type == value.class
35
36
  # 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
37
+ return true if type.class == ::Array && value.class == ::Array && type.first == value.first.class
38
+ if type.class == ::Hash && value.class == ::Hash && type.keys[0] == value.keys[0].class && type.values[0] == value.values[0].class
38
39
  return true
39
40
  end
40
41
  end
41
42
 
42
- raise TypeError, "Invalid type '#{value.class}' for '#{name}'"
43
+ raise TypeError, "Invalid type '#{value.class}' for '#{name}'. Valid types: [#{@types.join(', ')}]"
43
44
  end
44
45
  end
45
46
  end
46
47
 
47
48
  class Object
48
49
  class << self
50
+ # For "Type | [type_expression/type/value]" situations, redirecting to or generating a type expression from types.
49
51
  # "|" is not defined on Object class and this is the most compute-efficient way to achieve our goal (world peace).
52
+ # "|" is overridable by any child object. While we could def/undef this method, this approach is actually lighter.
50
53
  # "|" bitwise operator on Integer is not defined when the receiver is an Integer class, so we are not in conflict.
51
54
  def |(expression)
52
55
  if expression.class == ::LowType::TypeExpression
56
+ # We pass our type into their type expression.
53
57
  expression | self
54
58
  expression
55
59
  else
60
+ # We turn our type into a type expression and pass in their [type_expression/type/value].
56
61
  type_expression = ::LowType::TypeExpression.new(type: self)
57
62
  type_expression | expression
58
63
  end
@@ -0,0 +1,11 @@
1
+ class ValueExpression
2
+ attr_reader :value
3
+
4
+ def initialize(value:)
5
+ @value = value
6
+ end
7
+
8
+ def class
9
+ @value
10
+ end
11
+ 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.4.1'
4
+ VERSION = '0.6.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.4.1
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - maedi
@@ -22,7 +22,9 @@ files:
22
22
  - lib/param_proxy.rb
23
23
  - lib/parser.rb
24
24
  - lib/redefiner.rb
25
+ - lib/type_assignment.rb
25
26
  - lib/type_expression.rb
27
+ - lib/value_expression.rb
26
28
  - lib/version.rb
27
29
  homepage: https://codeberg.org/low_ruby/low_type
28
30
  licenses: []
@@ -45,5 +47,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
45
47
  requirements: []
46
48
  rubygems_version: 3.7.2
47
49
  specification_version: 4
48
- summary: An elegant way to define types in Ruby
50
+ summary: Elegant types in Ruby
49
51
  test_files: []