low_type 0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 849b5a1d0d8d4ec70dda7366f9e72a991aa71ce72631495f66c78e3bb03b8de3
4
+ data.tar.gz: 4e5f71133ec37642bee7ef092103926d9b673e6ac3d2f0c621bee69e25b4953c
5
+ SHA512:
6
+ metadata.gz: dc9e6b031328b7c398b04cb15ebeb9c347c3b4ff6c49a6e64570dbfdfb2b95b8facfde45bbb07f63c285631280629c2d5b8674e7709505ebac8342fd81a539ed
7
+ data.tar.gz: cde6c6557d6b47c1e35b6b6216fb939e85f778439483d5c493a21d64cade1d3f6b4bca5c689d0def331a5fb2f0da7f6ee7c497fd974ece7224af72dbb616970c
data/lib/low_type.rb ADDED
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'type_expression'
4
+
5
+ module LowType
6
+ class InvalidTypeError < StandardError; end;
7
+ class RequiredValueError < StandardError; end;
8
+
9
+ # We do as much as possible on class load rather than on instantiation to be thread-safe and efficient.
10
+ def self.included(base)
11
+ class << base
12
+ def low_params
13
+ @low_params ||= {}
14
+ end
15
+
16
+ def type(expression)
17
+ # TODO: Runtime type expression for the supplied variable.
18
+ end
19
+ alias_method :low_type, :type
20
+
21
+ def value(expression)
22
+ # TODO: Cancel out a type expression.
23
+ end
24
+ alias_method :low_value, :value
25
+ end
26
+
27
+ base.prepend LowType.redefine_methods(file_path: LowType.file_path(klass: base), klass: base)
28
+ end
29
+
30
+ def self.redefine_methods(file_path:, klass:)
31
+ Module.new do
32
+ # TODO: Use an AST.
33
+ File.readlines(file_path).each do |file_line|
34
+ method_line = file_line.strip
35
+ next unless method_line.start_with?('def ') && method_line.include?('(')
36
+
37
+ method_name, args = method_line.delete_prefix('def ').split(/[()]/)
38
+ method_name = method_name.to_sym
39
+
40
+ proxy_method = eval("-> (#{args}) {}")
41
+ required_args, required_kwargs = LowType.required_args(proxy_method)
42
+
43
+ klass.low_params[method_name] = LowType.type_expressions_from_params(proxy_method, args, required_args, required_kwargs)
44
+
45
+ define_method(method_name) do |*args, **kwargs|
46
+ klass.low_params[method_name].each do |param_proxy|
47
+ arg = param_proxy.position ? args[param_proxy.position] : kwargs[param_proxy.name]
48
+ arg = param_proxy.type_expression.default_value if arg.nil? && param_proxy.type_expression.default_value != :LOW_TYPE_UNDEFINED
49
+ param_proxy.type_expression.validate!(arg:, name: param_proxy.name)
50
+ param_proxy.position ? args[param_proxy.position] = arg : kwargs[param_proxy.name] = arg
51
+ end
52
+
53
+ super(*args, **kwargs)
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ class << self
60
+ def methods(klass)
61
+ klass.public_instance_methods(false) + klass.protected_instance_methods(false) + klass.private_instance_methods(false)
62
+ end
63
+
64
+ def file_path(klass:)
65
+ caller.find { |callee| callee.end_with?("<class:#{klass}>'") }.split(':').first
66
+ end
67
+
68
+ def type?(expression)
69
+ expression.respond_to?(:new) || expression == Integer
70
+ end
71
+
72
+ def value?(expression)
73
+ !expression.respond_to?(:new) && expression != Integer
74
+ end
75
+
76
+ def required_args(proxy_method)
77
+ required_args = []
78
+ required_kwargs = {}
79
+
80
+ proxy_method.parameters.each do |param|
81
+ param_type, param_name = param
82
+
83
+ case param_type
84
+ when :req
85
+ required_args << nil
86
+ when :keyreq
87
+ required_kwargs[param_name] = nil
88
+ end
89
+ end
90
+
91
+ [required_args, required_kwargs]
92
+ end
93
+
94
+ def type_expressions_from_params(proxy_method, args, required_args, required_kwargs)
95
+ typed_method = eval(
96
+ <<~RUBY
97
+ -> (#{args}) {
98
+ param_proxies = []
99
+
100
+ proxy_method.parameters.each_with_index do |param, position|
101
+ type, name = param
102
+ position = nil unless [:opt, :req, :rest].include?(type)
103
+
104
+ expression = binding.local_variable_get(name)
105
+
106
+ if expression.class == TypeExpression
107
+ param_proxies << ParamProxy.new(type_expression: expression, name:, type:, position:)
108
+ elsif ::LowType.type?(expression)
109
+ param_proxies << ParamProxy.new(type_expression: TypeExpression.new(type: expression), name:, type:, position:)
110
+ end
111
+ end
112
+
113
+ param_proxies
114
+ }
115
+ RUBY
116
+ )
117
+
118
+ # Call method with only its required args to evaluate type expressions (which are stored as default values).
119
+ typed_method.call(*required_args, **required_kwargs)
120
+ end
121
+ end
122
+
123
+ class ParamProxy
124
+ attr_reader :type_expression, :name, :type, :position
125
+
126
+ def initialize(type_expression:, name:, type:, position: nil)
127
+ @type_expression = type_expression
128
+ @name = name
129
+ @type = type
130
+ @position = position
131
+ end
132
+ end
133
+
134
+ class ValueExpression; end
135
+
136
+ class Boolean; end
137
+ class KeyValue; end
138
+ end
@@ -0,0 +1,51 @@
1
+ module LowType
2
+ class TypeExpression
3
+ attr_reader :type, :default_value
4
+
5
+ def initialize(type:)
6
+ @types = [type]
7
+ @default_value = :LOW_TYPE_UNDEFINED
8
+ end
9
+
10
+ def |(expression)
11
+ if expression.class == ::LowType::TypeExpression
12
+ @types = @types + expression.types
13
+ @default_value = expression.default_value
14
+ elsif ::LowType.value?(expression)
15
+ @default_value = expression
16
+ else
17
+ @types << expression
18
+ end
19
+
20
+ self
21
+ end
22
+
23
+ def required?
24
+ @default_value == :LOW_TYPE_UNDEFINED
25
+ end
26
+
27
+ def validate!(arg:, name:)
28
+ if arg.nil? && required?
29
+ raise ::LowType::RequiredValueError, "Missing value of required type [#{@types.join(',')}] for '#{name}'"
30
+ end
31
+
32
+ raise ::LowType::InvalidTypeError, "Invalid type '#{arg.class}' for '#{name}'" unless @types.include?(arg.class)
33
+ end
34
+ end
35
+ end
36
+
37
+ class Object
38
+ class << self
39
+ # "|" is not defined on Object class and this is the most compute-efficient way to achieve our goal (world peace).
40
+ # "|" bitwise operator on Integer is not defined when the receiver is an Integer class, so we are not in conflict.
41
+ def |(expression)
42
+ if expression.class == ::LowType::TypeExpression
43
+ expression | self
44
+ expression
45
+ else
46
+ type_expression = ::LowType::TypeExpression.new(type: self)
47
+ type_expression | expression
48
+ end
49
+ end
50
+ end
51
+ end
data/lib/version.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LowType
4
+ VERSION = '0.1.0'
5
+ end
metadata ADDED
@@ -0,0 +1,48 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: low_type
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - maedi
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2025-10-10 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: An elegant and simple way to define types in Ruby, only when you need
14
+ them.
15
+ email:
16
+ - maediprichard@gmail.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - lib/low_type.rb
22
+ - lib/type_expression.rb
23
+ - lib/version.rb
24
+ homepage: https://codeberg.org/low_ruby/low_type
25
+ licenses: []
26
+ metadata:
27
+ homepage_uri: https://codeberg.org/low_ruby/low_type
28
+ source_code_uri: https://codeberg.org/low_ruby/low_type/src/branch/main
29
+ post_install_message:
30
+ rdoc_options: []
31
+ require_paths:
32
+ - lib
33
+ required_ruby_version: !ruby/object:Gem::Requirement
34
+ requirements:
35
+ - - ">="
36
+ - !ruby/object:Gem::Version
37
+ version: 3.2.0
38
+ required_rubygems_version: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '0'
43
+ requirements: []
44
+ rubygems_version: 3.5.9
45
+ signing_key:
46
+ specification_version: 4
47
+ summary: An elegant way to define types in Ruby
48
+ test_files: []