t-ruby 0.0.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.
@@ -0,0 +1,441 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TRuby
4
+ # Represents a type constraint
5
+ class Constraint
6
+ attr_reader :type, :condition, :message
7
+
8
+ def initialize(type:, condition:, message: nil)
9
+ @type = type
10
+ @condition = condition
11
+ @message = message
12
+ end
13
+
14
+ def to_s
15
+ "#{@type} where #{@condition}"
16
+ end
17
+ end
18
+
19
+ # Bounds constraint: T <: BaseType (T is subtype of BaseType)
20
+ class BoundsConstraint < Constraint
21
+ attr_reader :subtype, :supertype
22
+
23
+ def initialize(subtype:, supertype:)
24
+ @subtype = subtype
25
+ @supertype = supertype
26
+ super(type: :bounds, condition: "#{subtype} <: #{supertype}")
27
+ end
28
+
29
+ def satisfied?(type_hierarchy)
30
+ type_hierarchy.subtype_of?(@subtype, @supertype)
31
+ end
32
+ end
33
+
34
+ # Equality constraint: T == SpecificType
35
+ class EqualityConstraint < Constraint
36
+ attr_reader :left_type, :right_type
37
+
38
+ def initialize(left_type:, right_type:)
39
+ @left_type = left_type
40
+ @right_type = right_type
41
+ super(type: :equality, condition: "#{left_type} == #{right_type}")
42
+ end
43
+
44
+ def satisfied?(value_type)
45
+ @left_type == value_type || @right_type == value_type
46
+ end
47
+ end
48
+
49
+ # Numeric range constraint: Integer where min..max
50
+ class NumericRangeConstraint < Constraint
51
+ attr_reader :base_type, :min, :max
52
+
53
+ def initialize(base_type:, min: nil, max: nil)
54
+ @base_type = base_type
55
+ @min = min
56
+ @max = max
57
+ range_str = build_range_string
58
+ super(type: :numeric_range, condition: range_str)
59
+ end
60
+
61
+ def satisfied?(value)
62
+ return false unless value.is_a?(Numeric)
63
+ return false if @min && value < @min
64
+ return false if @max && value > @max
65
+ true
66
+ end
67
+
68
+ def validation_code(var_name)
69
+ conditions = []
70
+ conditions << "#{var_name} >= #{@min}" if @min
71
+ conditions << "#{var_name} <= #{@max}" if @max
72
+ conditions.join(" && ")
73
+ end
74
+
75
+ private
76
+
77
+ def build_range_string
78
+ return "#{@min}..#{@max}" if @min && @max
79
+ return ">= #{@min}" if @min
80
+ return "<= #{@max}" if @max
81
+ ""
82
+ end
83
+ end
84
+
85
+ # Pattern constraint for strings: String where /pattern/
86
+ class PatternConstraint < Constraint
87
+ attr_reader :base_type, :pattern
88
+
89
+ def initialize(base_type:, pattern:)
90
+ @base_type = base_type
91
+ @pattern = pattern.is_a?(Regexp) ? pattern : Regexp.new(pattern)
92
+ super(type: :pattern, condition: "=~ #{@pattern.inspect}")
93
+ end
94
+
95
+ def satisfied?(value)
96
+ return false unless value.is_a?(String)
97
+ @pattern.match?(value)
98
+ end
99
+
100
+ def validation_code(var_name)
101
+ "#{@pattern.inspect}.match?(#{var_name})"
102
+ end
103
+ end
104
+
105
+ # Predicate constraint: Type where predicate_method
106
+ class PredicateConstraint < Constraint
107
+ attr_reader :base_type, :predicate
108
+
109
+ def initialize(base_type:, predicate:)
110
+ @base_type = base_type
111
+ @predicate = predicate
112
+ super(type: :predicate, condition: predicate.to_s)
113
+ end
114
+
115
+ def satisfied?(value)
116
+ if @predicate.is_a?(Proc)
117
+ @predicate.call(value)
118
+ elsif @predicate.is_a?(Symbol)
119
+ value.respond_to?(@predicate) && value.send(@predicate)
120
+ else
121
+ false
122
+ end
123
+ end
124
+
125
+ def validation_code(var_name)
126
+ if @predicate.is_a?(Symbol)
127
+ "#{var_name}.#{@predicate}"
128
+ else
129
+ "true" # Proc constraints require runtime evaluation
130
+ end
131
+ end
132
+ end
133
+
134
+ # Length constraint for strings/arrays: Type where length condition
135
+ class LengthConstraint < Constraint
136
+ attr_reader :base_type, :min_length, :max_length, :exact_length
137
+
138
+ def initialize(base_type:, min_length: nil, max_length: nil, exact_length: nil)
139
+ @base_type = base_type
140
+ @min_length = min_length
141
+ @max_length = max_length
142
+ @exact_length = exact_length
143
+ super(type: :length, condition: build_condition)
144
+ end
145
+
146
+ def satisfied?(value)
147
+ return false unless value.respond_to?(:length)
148
+ len = value.length
149
+ return len == @exact_length if @exact_length
150
+ return false if @min_length && len < @min_length
151
+ return false if @max_length && len > @max_length
152
+ true
153
+ end
154
+
155
+ def validation_code(var_name)
156
+ conditions = []
157
+ if @exact_length
158
+ conditions << "#{var_name}.length == #{@exact_length}"
159
+ else
160
+ conditions << "#{var_name}.length >= #{@min_length}" if @min_length
161
+ conditions << "#{var_name}.length <= #{@max_length}" if @max_length
162
+ end
163
+ conditions.join(" && ")
164
+ end
165
+
166
+ private
167
+
168
+ def build_condition
169
+ return "length == #{@exact_length}" if @exact_length
170
+ parts = []
171
+ parts << "length >= #{@min_length}" if @min_length
172
+ parts << "length <= #{@max_length}" if @max_length
173
+ parts.join(", ")
174
+ end
175
+ end
176
+
177
+ # Main constraint checker class
178
+ class ConstraintChecker
179
+ attr_reader :constraints, :errors
180
+
181
+ def initialize
182
+ @constraints = {}
183
+ @errors = []
184
+ end
185
+
186
+ # Register a constrained type
187
+ def register(name, base_type:, constraints: [])
188
+ @constraints[name] = {
189
+ base_type: base_type,
190
+ constraints: constraints
191
+ }
192
+ end
193
+
194
+ # Parse constraint syntax from source
195
+ def parse_constraint(definition)
196
+ # Match: type Name <: BaseType where condition
197
+ if definition.match?(/^(\w+)\s*<:\s*(\w+)\s*where\s*(.+)$/)
198
+ match = definition.match(/^(\w+)\s*<:\s*(\w+)\s*where\s*(.+)$/)
199
+ name = match[1]
200
+ base_type = match[2]
201
+ condition = match[3].strip
202
+
203
+ constraints = parse_condition(base_type, condition)
204
+ return { name: name, base_type: base_type, constraints: constraints }
205
+ end
206
+
207
+ # Match: type Name <: BaseType (bounds only)
208
+ if definition.match?(/^(\w+)\s*<:\s*(\w+)\s*$/)
209
+ match = definition.match(/^(\w+)\s*<:\s*(\w+)\s*$/)
210
+ name = match[1]
211
+ base_type = match[2]
212
+
213
+ return {
214
+ name: name,
215
+ base_type: base_type,
216
+ constraints: [BoundsConstraint.new(subtype: name, supertype: base_type)]
217
+ }
218
+ end
219
+
220
+ # Match: type Name = BaseType where condition
221
+ if definition.match?(/^(\w+)\s*=\s*(\w+)\s*where\s*(.+)$/)
222
+ match = definition.match(/^(\w+)\s*=\s*(\w+)\s*where\s*(.+)$/)
223
+ name = match[1]
224
+ base_type = match[2]
225
+ condition = match[3].strip
226
+
227
+ constraints = parse_condition(base_type, condition)
228
+ return { name: name, base_type: base_type, constraints: constraints }
229
+ end
230
+
231
+ nil
232
+ end
233
+
234
+ # Validate a value against a constrained type
235
+ def validate(type_name, value)
236
+ @errors = []
237
+
238
+ unless @constraints.key?(type_name)
239
+ @errors << "Unknown constrained type: #{type_name}"
240
+ return false
241
+ end
242
+
243
+ type_info = @constraints[type_name]
244
+
245
+ # Check base type
246
+ unless matches_base_type?(value, type_info[:base_type])
247
+ @errors << "Value #{value.inspect} does not match base type #{type_info[:base_type]}"
248
+ return false
249
+ end
250
+
251
+ # Check all constraints
252
+ type_info[:constraints].each do |constraint|
253
+ unless constraint.satisfied?(value)
254
+ @errors << "Constraint violation: #{constraint.condition}"
255
+ return false
256
+ end
257
+ end
258
+
259
+ true
260
+ end
261
+
262
+ # Generate validation code for runtime checking
263
+ def generate_validation_code(type_name, var_name)
264
+ return nil unless @constraints.key?(type_name)
265
+
266
+ type_info = @constraints[type_name]
267
+ conditions = []
268
+
269
+ # Base type check
270
+ case type_info[:base_type]
271
+ when "Integer"
272
+ conditions << "#{var_name}.is_a?(Integer)"
273
+ when "String"
274
+ conditions << "#{var_name}.is_a?(String)"
275
+ when "Float"
276
+ conditions << "#{var_name}.is_a?(Float)"
277
+ when "Numeric"
278
+ conditions << "#{var_name}.is_a?(Numeric)"
279
+ when "Array"
280
+ conditions << "#{var_name}.is_a?(Array)"
281
+ end
282
+
283
+ # Constraint checks
284
+ type_info[:constraints].each do |constraint|
285
+ if constraint.respond_to?(:validation_code)
286
+ code = constraint.validation_code(var_name)
287
+ conditions << code unless code.empty?
288
+ end
289
+ end
290
+
291
+ conditions.join(" && ")
292
+ end
293
+
294
+ private
295
+
296
+ def parse_condition(base_type, condition)
297
+ constraints = []
298
+
299
+ # Numeric comparison: > N, < N, >= N, <= N
300
+ if condition.match?(/^([<>]=?)\s*(\d+(?:\.\d+)?)$/)
301
+ match = condition.match(/^([<>]=?)\s*(\d+(?:\.\d+)?)$/)
302
+ op = match[1]
303
+ value = match[2].include?(".") ? match[2].to_f : match[2].to_i
304
+
305
+ case op
306
+ when ">"
307
+ constraints << NumericRangeConstraint.new(base_type: base_type, min: value + 1)
308
+ when ">="
309
+ constraints << NumericRangeConstraint.new(base_type: base_type, min: value)
310
+ when "<"
311
+ constraints << NumericRangeConstraint.new(base_type: base_type, max: value - 1)
312
+ when "<="
313
+ constraints << NumericRangeConstraint.new(base_type: base_type, max: value)
314
+ end
315
+ end
316
+
317
+ # Range: min..max
318
+ if condition.match?(/^(\d+)\.\.(\d+)$/)
319
+ match = condition.match(/^(\d+)\.\.(\d+)$/)
320
+ constraints << NumericRangeConstraint.new(
321
+ base_type: base_type,
322
+ min: match[1].to_i,
323
+ max: match[2].to_i
324
+ )
325
+ end
326
+
327
+ # Pattern: /regex/
328
+ if condition.match?(/^\/(.+)\/$/)
329
+ match = condition.match(/^\/(.+)\/$/)
330
+ constraints << PatternConstraint.new(base_type: base_type, pattern: match[1])
331
+ end
332
+
333
+ # Length constraint: length > N, length == N, etc.
334
+ if condition.match?(/^length\s*([<>=]+)\s*(\d+)$/)
335
+ match = condition.match(/^length\s*([<>=]+)\s*(\d+)$/)
336
+ op = match[1]
337
+ value = match[2].to_i
338
+
339
+ case op
340
+ when "=="
341
+ constraints << LengthConstraint.new(base_type: base_type, exact_length: value)
342
+ when ">="
343
+ constraints << LengthConstraint.new(base_type: base_type, min_length: value)
344
+ when "<="
345
+ constraints << LengthConstraint.new(base_type: base_type, max_length: value)
346
+ when ">"
347
+ constraints << LengthConstraint.new(base_type: base_type, min_length: value + 1)
348
+ when "<"
349
+ constraints << LengthConstraint.new(base_type: base_type, max_length: value - 1)
350
+ end
351
+ end
352
+
353
+ # Predicate: positive?, negative?, empty?, etc.
354
+ if condition.match?(/^(\w+)\?$/)
355
+ match = condition.match(/^(\w+)\?$/)
356
+ constraints << PredicateConstraint.new(base_type: base_type, predicate: "#{match[1]}?".to_sym)
357
+ end
358
+
359
+ constraints
360
+ end
361
+
362
+ def matches_base_type?(value, type_name)
363
+ case type_name
364
+ when "Integer" then value.is_a?(Integer)
365
+ when "String" then value.is_a?(String)
366
+ when "Float" then value.is_a?(Float)
367
+ when "Numeric" then value.is_a?(Numeric)
368
+ when "Array" then value.is_a?(Array)
369
+ when "Hash" then value.is_a?(Hash)
370
+ when "Boolean" then value == true || value == false
371
+ when "Symbol" then value.is_a?(Symbol)
372
+ else true # Unknown types pass through
373
+ end
374
+ end
375
+ end
376
+
377
+ # Constrained type registry for managing type constraints
378
+ class ConstrainedTypeRegistry
379
+ attr_reader :types
380
+
381
+ def initialize
382
+ @types = {}
383
+ @checker = ConstraintChecker.new
384
+ end
385
+
386
+ # Register a constrained type from parsed definition
387
+ def register(name, base_type:, constraints: [])
388
+ @types[name] = {
389
+ base_type: base_type,
390
+ constraints: constraints,
391
+ defined_at: caller_locations(1, 1).first
392
+ }
393
+ @checker.register(name, base_type: base_type, constraints: constraints)
394
+ end
395
+
396
+ # Parse and register from source line
397
+ def register_from_source(line)
398
+ result = @checker.parse_constraint(line)
399
+ return false unless result
400
+
401
+ register(result[:name], base_type: result[:base_type], constraints: result[:constraints])
402
+ true
403
+ end
404
+
405
+ # Check if a type is registered
406
+ def registered?(name)
407
+ @types.key?(name)
408
+ end
409
+
410
+ # Get type info
411
+ def get(name)
412
+ @types[name]
413
+ end
414
+
415
+ # Validate value against type
416
+ def validate(type_name, value)
417
+ @checker.validate(type_name, value)
418
+ end
419
+
420
+ # Get validation errors
421
+ def errors
422
+ @checker.errors
423
+ end
424
+
425
+ # Generate validation code
426
+ def validation_code(type_name, var_name)
427
+ @checker.generate_validation_code(type_name, var_name)
428
+ end
429
+
430
+ # List all registered types
431
+ def list
432
+ @types.keys
433
+ end
434
+
435
+ # Clear all registrations
436
+ def clear
437
+ @types.clear
438
+ @checker = ConstraintChecker.new
439
+ end
440
+ end
441
+ end