rbdantic 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.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +245 -0
  4. data/.ruby-version +1 -0
  5. data/CHANGELOG.md +5 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +852 -0
  8. data/README_CN.md +852 -0
  9. data/Rakefile +12 -0
  10. data/lib/rbdantic/base/access.rb +105 -0
  11. data/lib/rbdantic/base/dsl.rb +79 -0
  12. data/lib/rbdantic/base/validation.rb +152 -0
  13. data/lib/rbdantic/base.rb +30 -0
  14. data/lib/rbdantic/config.rb +60 -0
  15. data/lib/rbdantic/error_detail.rb +54 -0
  16. data/lib/rbdantic/field.rb +188 -0
  17. data/lib/rbdantic/json_schema/defs_registry.rb +79 -0
  18. data/lib/rbdantic/json_schema/generator.rb +148 -0
  19. data/lib/rbdantic/json_schema/types.rb +98 -0
  20. data/lib/rbdantic/serialization/dumper.rb +133 -0
  21. data/lib/rbdantic/serialization/json_serializer.rb +60 -0
  22. data/lib/rbdantic/validators/field_validator.rb +83 -0
  23. data/lib/rbdantic/validators/model_validator.rb +59 -0
  24. data/lib/rbdantic/validators/types/array.rb +77 -0
  25. data/lib/rbdantic/validators/types/base.rb +78 -0
  26. data/lib/rbdantic/validators/types/boolean.rb +37 -0
  27. data/lib/rbdantic/validators/types/float.rb +32 -0
  28. data/lib/rbdantic/validators/types/hash.rb +54 -0
  29. data/lib/rbdantic/validators/types/integer.rb +28 -0
  30. data/lib/rbdantic/validators/types/model.rb +75 -0
  31. data/lib/rbdantic/validators/types/number.rb +63 -0
  32. data/lib/rbdantic/validators/types/string.rb +70 -0
  33. data/lib/rbdantic/validators/types/symbol.rb +30 -0
  34. data/lib/rbdantic/validators/types/time.rb +33 -0
  35. data/lib/rbdantic/validators/types.rb +63 -0
  36. data/lib/rbdantic/validators/validator_context.rb +43 -0
  37. data/lib/rbdantic/version.rb +5 -0
  38. data/lib/rbdantic.rb +8 -0
  39. data/sig/rbdantic.rbs +4 -0
  40. metadata +84 -0
data/README.md ADDED
@@ -0,0 +1,852 @@
1
+ # Rbdantic
2
+
3
+ **Ruby Data Validation and Settings Management** - A Pydantic-inspired data validation library for Ruby.
4
+
5
+ Rbdantic brings Pydantic's powerful data validation capabilities to Ruby, providing runtime data validation, serialization, and JSON Schema generation with an intuitive DSL.
6
+
7
+ [中文文档](README_CN.md)
8
+
9
+ ## Features
10
+
11
+ - **Base Model Class** - Define data models with type-checked fields
12
+ - **Field Constraints** - Built-in constraints for strings, numbers, and arrays
13
+ - **Custom Validators** - Field-level and model-level validators with multiple modes
14
+ - **Type Coercion** - Automatic type conversion with configurable strictness
15
+ - **Nested Models** - Support for nested model validation
16
+ - **Model Inheritance** - Fields and validators are inherited by subclasses
17
+ - **Model Configuration** - Flexible configuration options (extra fields, frozen models, etc.)
18
+ - **Serialization** - Convert models to Hash or JSON with filtering options
19
+ - **JSON Schema Generation** - Automatic JSON Schema generation for API documentation
20
+ - **Detailed Error Reporting** - Structured validation errors with location paths
21
+
22
+ ## Installation
23
+
24
+ Add to your Gemfile:
25
+
26
+ ```ruby
27
+ gem 'rbdantic'
28
+ ```
29
+
30
+ Or install directly:
31
+
32
+ ```bash
33
+ gem install rbdantic
34
+ ```
35
+
36
+ ## Quick Start
37
+
38
+ ```ruby
39
+ require 'rbdantic'
40
+
41
+ class User < Rbdantic::BaseModel
42
+ field :name, String, min_length: 1, max_length: 100
43
+ field :email, String, pattern: /\A[^@\s]+@[^@\s]+\z/
44
+ field :age, Integer, gt: 0, le: 150
45
+ field :tags, [String], default_factory: -> { [] }
46
+ end
47
+
48
+ # Create a valid user
49
+ user = User.new(
50
+ name: "Alice",
51
+ email: "alice@example.com",
52
+ age: 30
53
+ )
54
+
55
+ puts user.name # => "Alice"
56
+ puts user.age # => 30
57
+ puts user.tags # => []
58
+
59
+ # Serialize to Hash
60
+ puts user.model_dump
61
+ # => { name: "Alice", email: "alice@example.com", age: 30, tags: [] }
62
+
63
+ # Serialize to JSON
64
+ puts user.model_dump_json
65
+ # => {"name":"Alice","email":"alice@example.com","age":30,"tags":[]}
66
+
67
+ # Validation error
68
+ begin
69
+ User.new(name: "", email: "invalid", age: -1)
70
+ rescue Rbdantic::ValidationError => e
71
+ e.errors.each do |err|
72
+ puts "#{err.loc.join('.')}: #{err.msg}"
73
+ end
74
+ # name: String must be at least 1 characters
75
+ # email: String does not match pattern ...
76
+ # age: Value must be greater than 0
77
+ end
78
+ ```
79
+
80
+ ## Field Definition
81
+
82
+ ### Basic Fields
83
+
84
+ ```ruby
85
+ class Product < Rbdantic::BaseModel
86
+ field :id, Integer
87
+ field :name, String
88
+ field :price, Float
89
+ field :active, Rbdantic::Boolean
90
+ end
91
+ ```
92
+
93
+ ### Default Values
94
+
95
+ ```ruby
96
+ class Config < Rbdantic::BaseModel
97
+ # Static default
98
+ field :timeout, Integer, default: 30
99
+
100
+ # Dynamic default (factory)
101
+ field :created_at, Time, default_factory: -> { Time.now }
102
+
103
+ # Optional field (can be nil)
104
+ field :nickname, String, optional: true
105
+ end
106
+ ```
107
+
108
+ ### Field Constraints
109
+
110
+ #### String Constraints
111
+
112
+ ```ruby
113
+ class User < Rbdantic::BaseModel
114
+ field :username, String,
115
+ min_length: 3,
116
+ max_length: 20,
117
+ pattern: /\A[a-zA-Z0-9_]+\z/
118
+ end
119
+ ```
120
+
121
+ #### Numeric Constraints
122
+
123
+ ```ruby
124
+ class Product < Rbdantic::BaseModel
125
+ field :price, Float,
126
+ gt: 0, # greater than
127
+ le: 10000 # less than or equal
128
+
129
+ field :quantity, Integer,
130
+ ge: 0, # greater than or equal
131
+ multiple_of: 1
132
+ end
133
+ ```
134
+
135
+ #### Array Constraints
136
+
137
+ ```ruby
138
+ class Order < Rbdantic::BaseModel
139
+ field :items, [String],
140
+ min_items: 1,
141
+ max_items: 100,
142
+ unique_items: true
143
+
144
+ end
145
+ ```
146
+
147
+ ### Custom Validators in Field
148
+
149
+ ```ruby
150
+ class User < Rbdantic::BaseModel
151
+ # Proc validator returning false on failure
152
+ field :email, String,
153
+ validators: [->(v) { v.include?("@") || false }]
154
+
155
+ # Proc validator returning error message
156
+ field :password, String,
157
+ validators: [->(v) { v.length >= 8 ? nil : "Password must be at least 8 characters" }]
158
+ end
159
+ ```
160
+
161
+ ## Model Configuration
162
+
163
+ Use `model_config` to configure model behavior:
164
+
165
+ ```ruby
166
+ class User < Rbdantic::BaseModel
167
+ model_config(
168
+ extra: :forbid, # reject extra fields
169
+ frozen: true, # immutable after creation
170
+ strict: true, # strict type checking
171
+ coerce_mode: :strict, # no type coercion
172
+ validate_assignment: true # validate on field assignment
173
+ )
174
+
175
+ field :name, String
176
+ end
177
+ ```
178
+
179
+ ### Configuration Options
180
+
181
+ | Option | Values | Description |
182
+ |--------|--------|-------------|
183
+ | `extra` | `:ignore`, `:forbid`, `:allow` | How to handle extra fields not defined |
184
+ | `frozen` | `true`, `false` | Make model immutable after initialization |
185
+ | `strict` | `true`, `false` | Strict type checking (no coercion) |
186
+ | `coerce_mode` | `:strict`, `:coerce` | Enable/disable type coercion |
187
+ | `validate_assignment` | `true`, `false` | Validate when assigning to fields |
188
+
189
+ ### Extra Fields Behavior
190
+
191
+ ```ruby
192
+ # Ignore extra fields (default)
193
+ class ModelA < Rbdantic::BaseModel
194
+ model_config extra: :ignore
195
+ field :name, String
196
+ end
197
+ ModelA.new(name: "test", extra: "data") # extra field is dropped
198
+
199
+ # Forbid extra fields
200
+ class ModelB < Rbdantic::BaseModel
201
+ model_config extra: :forbid
202
+ field :name, String
203
+ end
204
+ ModelB.new(name: "test", extra: "data") # raises ValidationError
205
+
206
+ # Allow extra fields
207
+ class ModelC < Rbdantic::BaseModel
208
+ model_config extra: :allow
209
+ field :name, String
210
+ end
211
+ m = ModelC.new(name: "test", extra: "data")
212
+ m[:extra] # => "data"
213
+ ```
214
+
215
+ ## Validators
216
+
217
+ ### Field Validators
218
+
219
+ Field validators run at different stages:
220
+
221
+ ```ruby
222
+ class User < Rbdantic::BaseModel
223
+ field :email, String
224
+
225
+ # Before validation - can transform value
226
+ field_validator :email, mode: :before do |value, ctx|
227
+ value&.downcase
228
+ end
229
+
230
+ # After validation - validate transformed value
231
+ field_validator :email, mode: :after do |value, ctx|
232
+ raise "Invalid email format" unless value.include?("@")
233
+ value
234
+ end
235
+ end
236
+ ```
237
+
238
+ #### Validator Modes
239
+
240
+ | Mode | Description |
241
+ |------|-------------|
242
+ | `:before` | Runs before type validation, can transform value |
243
+ | `:after` | Runs after type validation, validate the final value |
244
+ | `:plain` | Runs instead of type validation (skips type check) |
245
+ | `:wrap` | Runs after all other validators |
246
+
247
+ ### Model Validators
248
+
249
+ Model validators validate the entire model:
250
+
251
+ ```ruby
252
+ class Account < Rbdantic::BaseModel
253
+ field :password, String
254
+ field :confirm_password, String
255
+
256
+ # Before validator - preprocess input data
257
+ model_validator mode: :before do |data|
258
+ data[:password] = data[:password]&.strip
259
+ data
260
+ end
261
+
262
+ # After validator - validate model state
263
+ model_validator mode: :after do |model|
264
+ if model.password != model.confirm_password
265
+ raise "Passwords do not match"
266
+ end
267
+ end
268
+ end
269
+ ```
270
+
271
+ ## Nested Models
272
+
273
+ Rbdantic supports nested models just like Pydantic, allowing you to build complex data structures with hierarchical validation.
274
+
275
+ ### Single Nested Model
276
+
277
+ ```ruby
278
+ class Address < Rbdantic::BaseModel
279
+ field :street, String, min_length: 1
280
+ field :city, String, min_length: 1
281
+ field :zip_code, String, pattern: /\A\d{5}\z/
282
+ end
283
+
284
+ class User < Rbdantic::BaseModel
285
+ field :name, String
286
+ field :address, Address # nested model type
287
+ end
288
+
289
+ # Create from hash - nested model is automatically validated
290
+ user = User.new(
291
+ name: "Alice",
292
+ address: {
293
+ street: "123 Main St",
294
+ city: "Boston",
295
+ zip_code: "02134"
296
+ }
297
+ )
298
+
299
+ puts user.address.class # => Address
300
+ puts user.address.city # => "Boston"
301
+
302
+ # Or pass a pre-built nested model instance
303
+ address = Address.new(street: "456 Oak Ave", city: "Cambridge", zip_code: "02139")
304
+ user = User.new(name: "Jane", address: address)
305
+
306
+ # Serialize - nested models are recursively dumped
307
+ user.model_dump
308
+ # => { name: "Jane", address: { street: "456 Oak Ave", city: "Cambridge", zip_code: "02139" } }
309
+ ```
310
+
311
+ ### Deeply Nested Models
312
+
313
+ You can nest models at any depth:
314
+
315
+ ```ruby
316
+ class Country < Rbdantic::BaseModel
317
+ field :code, String, pattern: /\A[A-Z]{2}\z/
318
+ field :name, String
319
+ end
320
+
321
+ class City < Rbdantic::BaseModel
322
+ field :name, String
323
+ field :country, Country # nested within nested
324
+ end
325
+
326
+ class Person < Rbdantic::BaseModel
327
+ field :name, String
328
+ field :birthplace, City # two levels of nesting
329
+ end
330
+
331
+ # Create deeply nested structure
332
+ person = Person.new(
333
+ name: "Alice",
334
+ birthplace: {
335
+ name: "Paris",
336
+ country: {
337
+ code: "FR",
338
+ name: "France"
339
+ }
340
+ }
341
+ )
342
+
343
+ puts person.birthplace.country.code # => "FR"
344
+ ```
345
+
346
+ ### Array of Nested Models
347
+
348
+ Use `[Type]` shorthand to validate arrays of nested models:
349
+
350
+ ```ruby
351
+ class Item < Rbdantic::BaseModel
352
+ field :name, String, min_length: 1
353
+ field :quantity, Integer, gt: 0
354
+ field :price, Float, ge: 0
355
+ end
356
+
357
+ class Order < Rbdantic::BaseModel
358
+ field :order_id, String
359
+ field :items, [Item], min_items: 1
360
+ end
361
+
362
+ # Create order with multiple items
363
+ order = Order.new(
364
+ order_id: "ORD-001",
365
+ items: [
366
+ { name: "Widget", quantity: 5, price: 9.99 },
367
+ { name: "Gadget", quantity: 2, price: 19.99 }
368
+ ]
369
+ )
370
+
371
+ puts order.items[0].class # => Item
372
+ puts order.items.length # => 2
373
+
374
+ # Serialize - array items are recursively dumped
375
+ order.model_dump
376
+ # => { order_id: "ORD-001", items: [{ name: "Widget", quantity: 5, price: 9.99 }, ...] }
377
+ ```
378
+
379
+ ### Optional Nested Models
380
+
381
+ ```ruby
382
+ class Profile < Rbdantic::BaseModel
383
+ field :bio, String
384
+ field :avatar_url, String
385
+ end
386
+
387
+ class User < Rbdantic::BaseModel
388
+ field :name, String
389
+ field :profile, Profile, optional: true # can be nil
390
+ end
391
+
392
+ # Without profile
393
+ user = User.new(name: "Bob")
394
+ puts user.profile # => nil
395
+
396
+ # With profile
397
+ user = User.new(name: "Bob", profile: { bio: "Developer", avatar_url: "..." })
398
+ puts user.profile.bio # => "Developer"
399
+ ```
400
+
401
+ ### Nested Model Validation Errors
402
+
403
+ Errors in nested models include the full path:
404
+
405
+ ```ruby
406
+ begin
407
+ User.new(
408
+ name: "Alice",
409
+ address: {
410
+ street: "", # invalid: too short
411
+ city: "Boston",
412
+ zip_code: "invalid" # invalid: pattern mismatch
413
+ }
414
+ )
415
+ rescue Rbdantic::ValidationError => e
416
+ e.errors.each do |err|
417
+ puts "#{err.loc.join('.')} - #{err.msg}"
418
+ end
419
+ # address.street - String must be at least 1 characters
420
+ # address.zip_code - String does not match pattern ...
421
+ end
422
+
423
+ # Deeply nested error path
424
+ begin
425
+ Person.new(
426
+ name: "Bob",
427
+ birthplace: {
428
+ name: "London",
429
+ country: { code: "invalid", name: "UK" }
430
+ }
431
+ )
432
+ rescue Rbdantic::ValidationError => e
433
+ puts e.errors.first.loc # => [:birthplace, :country, :code]
434
+ end
435
+
436
+ # Array item error path
437
+ begin
438
+ Order.new(
439
+ order_id: "ORD-001",
440
+ items: [
441
+ { name: "Widget", quantity: 5, price: 9.99 },
442
+ { name: "", quantity: 0, price: -1 } # invalid item at index 1
443
+ ]
444
+ )
445
+ rescue Rbdantic::ValidationError => e
446
+ e.errors.each do |err|
447
+ puts "#{err.loc.join('.')} - #{err.msg}"
448
+ end
449
+ # items.1.name - String must be at least 1 characters
450
+ # items.1.quantity - Value must be greater than 0
451
+ # items.1.price - Value must be greater than or equal to 0
452
+ end
453
+ ```
454
+
455
+ ### Self-Referencing Models
456
+
457
+ Models can reference themselves for recursive structures:
458
+
459
+ ```ruby
460
+ class TreeNode < Rbdantic::BaseModel
461
+ field :value, String
462
+ field :children, [TreeNode], default_factory: -> { [] }
463
+ end
464
+
465
+ tree = TreeNode.new(
466
+ value: "root",
467
+ children: [
468
+ { value: "child1", children: [{ value: "grandchild1" }] },
469
+ { value: "child2" }
470
+ ]
471
+ )
472
+
473
+ puts tree.children[0].children[0].value # => "grandchild1"
474
+ ```
475
+
476
+ ## Inheritance
477
+
478
+ Fields, validators, and configuration are inherited:
479
+
480
+ ```ruby
481
+ class Animal < Rbdantic::BaseModel
482
+ field :name, String
483
+ field :age, Integer, gt: 0
484
+
485
+ model_config extra: :ignore
486
+ end
487
+
488
+ class Dog < Animal
489
+ field :breed, String # inherits name and age
490
+ end
491
+
492
+ class Cat < Animal
493
+ model_config extra: :allow
494
+ end
495
+ ```
496
+
497
+ **Note:** Child classes inherit parent `model_config` values and can override only the options they need.
498
+
499
+ ## Serialization
500
+
501
+ ### model_dump
502
+
503
+ Convert model to Hash with options:
504
+
505
+ ```ruby
506
+ class User < Rbdantic::BaseModel
507
+ field :name, String
508
+ field :role, String, default: "user"
509
+ field :active, Rbdantic::Boolean, default: true
510
+ end
511
+
512
+ user = User.new(name: "Alice")
513
+
514
+ # Full dump
515
+ user.model_dump
516
+ # => { name: "Alice", role: "user", active: true }
517
+
518
+ # Exclude fields with default values
519
+ user.model_dump(exclude_defaults: true)
520
+ # => { name: "Alice" }
521
+
522
+ # Include specific fields
523
+ user.model_dump(include: [:name])
524
+ # => { name: "Alice" }
525
+
526
+ # Exclude specific fields
527
+ user.model_dump(exclude: [:active])
528
+ # => { name: "Alice", role: "user" }
529
+
530
+ # Exclude unset fields (not provided during initialization)
531
+ user.model_dump(exclude_unset: true)
532
+ # => { name: "Alice" }
533
+ ```
534
+
535
+ ### model_dump_json
536
+
537
+ Convert to JSON string:
538
+
539
+ ```ruby
540
+ user.model_dump_json
541
+ # => {"name":"Alice","role":"user","active":true}
542
+
543
+ # With indentation
544
+ user.model_dump_json(indent: 2)
545
+ # => {
546
+ # "name": "Alice",
547
+ # "role": "user",
548
+ # "active": true
549
+ # }
550
+ ```
551
+
552
+ ## JSON Schema Generation
553
+
554
+ Generate JSON Schema for API documentation:
555
+
556
+ ```ruby
557
+ class User < Rbdantic::BaseModel
558
+ field :id, Integer, gt: 0
559
+ field :name, String, min_length: 1, max_length: 100
560
+ field :email, String, pattern: /\A[^@\s]+@[^@\s]+\z/
561
+ field :age, Integer, optional: true, ge: 0, le: 150
562
+ end
563
+
564
+ schema = User.model_json_schema
565
+ # => {
566
+ # "$schema": "https://json-schema.org/draft/2020-12/schema",
567
+ # "type": "object",
568
+ # "title": "User",
569
+ # "properties": {
570
+ # "id": { "type": "integer", "exclusiveMinimum": 0 },
571
+ # "name": { "type": "string", "minLength": 1, "maxLength": 100 },
572
+ # "email": { "type": "string", "pattern": "^[^@\\s]+@[^@\\s]+$" },
573
+ # "age": { "type": ["integer", "null"], "minimum": 0, "maximum": 150 }
574
+ # },
575
+ # "required": ["id", "name", "email"]
576
+ # }
577
+ ```
578
+
579
+ ## Type Coercion
580
+
581
+ Automatic type conversion when `coerce_mode: :coerce`:
582
+
583
+ ```ruby
584
+ class Config < Rbdantic::BaseModel
585
+ model_config coerce_mode: :coerce
586
+
587
+ field :count, Integer
588
+ field :price, Float
589
+ field :enabled, Rbdantic::Boolean
590
+ end
591
+
592
+ config = Config.new(
593
+ count: "42", # coerced to 42
594
+ price: "19.99", # coerced to 19.99
595
+ enabled: "yes" # coerced to true
596
+ )
597
+
598
+ config.count # => 42 (Integer)
599
+ config.price # => 19.99 (Float)
600
+ config.enabled # => true
601
+ ```
602
+
603
+ ### Supported Coercions
604
+
605
+ | Target Type | Source Examples |
606
+ |-------------|-----------------|
607
+ | `String` | Any value with `to_s` |
608
+ | `Integer` | `"42"`, `42.0` |
609
+ | `Float` | `"3.14"`, `42` |
610
+ | `Rbdantic::Boolean` | `"true"`, `"yes"`, `"on"`, `"1"`, `1`, `"false"`, `"no"`, `"off"`, `"0"`, `0` |
611
+ | `Array` | String with `split`, any value with `to_a` |
612
+ | `Hash` | Array of pairs, any value with `to_h` |
613
+
614
+ ## Validation Errors
615
+
616
+ ValidationError provides detailed error information:
617
+
618
+ ```ruby
619
+ begin
620
+ User.new(name: "", age: -1)
621
+ rescue Rbdantic::ValidationError => e
622
+ e.error_count # => 2
623
+ e.errors # => Array of ErrorDetail
624
+ e.as_json # => { errors: [...], error_count: 2 }
625
+ e.to_h # => same as as_json
626
+
627
+ e.errors.each do |err|
628
+ err.type # => :string_too_short, :value_not_greater_than
629
+ err.loc # => [:name], [:age] (location path)
630
+ err.msg # => "String must be at least..."
631
+ err.input # => "" (original input value)
632
+ end
633
+ end
634
+ ```
635
+
636
+ ## Supported Types
637
+
638
+ | Type | Notes |
639
+ |------|-------|
640
+ | `String` | Built-in string type |
641
+ | `Integer` | Built-in integer type |
642
+ | `Float` | Built-in float type |
643
+ | `Rbdantic::Boolean` | Boolean field accepting true/false |
644
+ | `Symbol` | Ruby symbol, max length 256 chars (DoS protection) |
645
+ | `[Type]` | Array with per-item validation |
646
+ | `Hash` | Key-value hash type |
647
+ | `Time` | Ruby Time type |
648
+ | `Rbdantic::BaseModel` subclass | Nested model validation |
649
+
650
+ **Note:** Use `Rbdantic::Boolean` for public boolean fields.
651
+
652
+ ```ruby
653
+ class Config < Rbdantic::BaseModel
654
+ field :enabled, Rbdantic::Boolean
655
+ field :active, Rbdantic::Boolean, optional: true
656
+ end
657
+ ```
658
+
659
+ ## Format Validation
660
+
661
+ Built-in format validators for common patterns:
662
+
663
+ ```ruby
664
+ class User < Rbdantic::BaseModel
665
+ field :email, String, format: :email # Basic email validation
666
+ field :website, String, format: :uri # URI validation (http/https)
667
+ end
668
+ ```
669
+
670
+ | Format | Pattern |
671
+ |--------|---------|
672
+ | `:email` | Basic email check (user@domain) |
673
+ | `:uri` | HTTP/HTTPS URI |
674
+
675
+ For complex validation, use custom `pattern` regex or `field_validator`.
676
+
677
+ ## Limitations & Security
678
+
679
+ ### Security Limits
680
+
681
+ | Limit | Value | Purpose |
682
+ |-------|-------|---------|
683
+ | Symbol max length | 256 chars | Prevent Symbol DoS attacks |
684
+ | Nested model depth | ~20 levels | Prevent stack overflow |
685
+
686
+ These limits protect against malicious input that could exhaust memory or cause stack overflow.
687
+
688
+ ### Thread Safety
689
+
690
+ Models are thread-safe for read operations after initialization. However:
691
+
692
+ - Validation during initialization is not thread-safe (uses internal state)
693
+ - `validate_assignment` mode uses instance-level locking
694
+ - Avoid sharing model instances across threads during mutation
695
+
696
+ ## Differences from Pydantic
697
+
698
+ | Feature | Pydantic | Rbdantic |
699
+ |---------|----------|----------|
700
+ | Field aliases | `Field(alias="name")` | `alias_name:` plus `by_alias: true` |
701
+ | Computed fields | `@computed_field` | Not supported |
702
+ | Generic models | `BaseModel[T]` | Not supported |
703
+ | Field serialization alias | `serialization_alias` | Uses `alias_name:` and dump/schema `by_alias:` |
704
+ | Model copy/update | `model.copy(update={})` | `copy(deep:)` and `update(**data)` helpers |
705
+ | Discriminated unions | `Annotated[Union, Field(discriminator)]` | Not supported |
706
+ | Custom type adapters | `TypeAdapter` | Use validators instead |
707
+ | Boolean type | `bool` | `Rbdantic::Boolean` |
708
+ | Config class | `BaseModelConfig` | `model_config` hash |
709
+
710
+ ### API Naming Differences
711
+
712
+ | Pydantic | Rbdantic |
713
+ |----------|----------|
714
+ | `Field()` | `field :name, Type, **options` |
715
+ | `@field_validator` | `field_validator :name, mode: ...` |
716
+ | `@model_validator` | `model_validator mode: ...` |
717
+ | `model_config = ConfigDict(...)` | `model_config(...)` |
718
+ | `model_dump()` | `model_dump()` |
719
+ | `model_dump_json()` | `model_dump_json()` |
720
+ | `model_validate()` | `Model.model_validate(data)` |
721
+
722
+ ## Requirements
723
+
724
+ - Ruby >= 2.7 (for keyword arguments and pattern matching)
725
+ - No external dependencies (pure Ruby implementation)
726
+
727
+ ## Error Handling Best Practices
728
+
729
+ ### Catching Specific Field Errors
730
+
731
+ ```ruby
732
+ begin
733
+ User.new(name: "", email: "invalid")
734
+ rescue Rbdantic::ValidationError => e
735
+ # Find errors for specific field
736
+ name_errors = e.errors.select { |err| err.loc.first == :name }
737
+ puts "Name errors: #{name_errors.map(&:msg).join(', ')}"
738
+
739
+ # Group errors by field
740
+ errors_by_field = e.errors.group_by { |err| err.loc.first }
741
+ errors_by_field.each do |field, errs|
742
+ puts "#{field}: #{errs.map(&:msg).join(', ')}"
743
+ end
744
+ end
745
+ ```
746
+
747
+ ### Custom Error Messages
748
+
749
+ Use `field_validator` for custom messages:
750
+
751
+ ```ruby
752
+ class User < Rbdantic::BaseModel
753
+ field :password, String
754
+
755
+ field_validator :password, mode: :after do |value, ctx|
756
+ if value.length < 8
757
+ raise Rbdantic::ValidationError::ErrorDetail.new(
758
+ type: :password_too_short,
759
+ loc: [:password],
760
+ msg: "Password must be at least 8 characters (got #{value.length})",
761
+ input: value
762
+ )
763
+ end
764
+ value
765
+ end
766
+ end
767
+ ```
768
+
769
+ ### Error JSON for APIs
770
+
771
+ ```ruby
772
+ rescue Rbdantic::ValidationError => e
773
+ # Return as JSON for API responses
774
+ status 400
775
+ json e.as_json
776
+ # => { "errors": [...], "error_count": 2 }
777
+ ```
778
+
779
+ ## API Reference
780
+
781
+ ### Rbdantic::BaseModel
782
+
783
+ | Method | Description |
784
+ |--------|-------------|
785
+ | `field(name, type, **options)` | Define a field with type and constraints |
786
+ | `model_config(**options)` | Configure model behavior |
787
+ | `field_validator(name, mode:, &block)` | Define a field-level validator |
788
+ | `model_validator(mode:, &block)` | Define a model-level validator |
789
+ | `model_json_schema(**options)` | Generate JSON Schema |
790
+ | `model_fields` | Returns hash of field definitions |
791
+ | `model_config` | Returns model configuration |
792
+ | `inherited(subclass)` | Hook for inheritance (internal) |
793
+
794
+ ### Instance Methods
795
+
796
+ | Method | Description |
797
+ |--------|-------------|
798
+ | `initialize(data = {})` | Create model with validation |
799
+ | `model_dump(**options)` | Convert to Hash |
800
+ | `model_dump_json(indent: nil)` | Convert to JSON string |
801
+ | `[name]` | Bracket access for field value |
802
+ | `[name] = value` | Bracket assignment for field value |
803
+
804
+ ### Field Options
805
+
806
+ | Option | Type | Description |
807
+ |--------|------|-------------|
808
+ | `default` | Any | Static default value |
809
+ | `default_factory` | Proc | Dynamic default value generator |
810
+ | `optional` | Boolean | Allow nil values |
811
+ | `required` | Boolean | Set to `false` to allow nil (same as `optional: true`) |
812
+ | `validators` | Array | Custom validator Procs |
813
+ | `alias_name` | Symbol | Alternative name for input/output (use with `by_alias: true`) |
814
+ | `format` | Symbol | Built-in format validator (`:email`, `:uri`, `:uuid`) |
815
+ | `min_length` | Integer | Minimum string length |
816
+ | `max_length` | Integer | Maximum string length |
817
+ | `pattern` | Regexp | String pattern match |
818
+ | `gt` | Numeric | Greater than |
819
+ | `ge` | Numeric | Greater than or equal |
820
+ | `lt` | Numeric | Less than |
821
+ | `le` | Numeric | Less than or equal |
822
+ | `multiple_of` | Numeric | Must be multiple of |
823
+ | `min_items` | Integer | Minimum array items |
824
+ | `max_items` | Integer | Maximum array items |
825
+ | `unique_items` | Boolean | Array items must be unique |
826
+
827
+ ## Development
828
+
829
+ After checking out the repo:
830
+
831
+ ```bash
832
+ bin/setup # Install dependencies
833
+ rake spec # Run tests
834
+ bin/console # Interactive prompt
835
+ bundle exec rake install # Install gem locally
836
+ ```
837
+
838
+ ## Contributing
839
+
840
+ Bug reports and pull requests are welcome on GitHub.
841
+
842
+ ## License
843
+
844
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
845
+
846
+ ## Inspiration
847
+
848
+ This library is inspired by [Pydantic](https://github.com/pydantic/pydantic) - the excellent Python data validation library.
849
+
850
+ ## Development Notes
851
+
852
+ This library was primarily developed with AI assistance (Claude), demonstrating how AI tools can accelerate software development while maintaining code quality and comprehensive testing.