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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +245 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +852 -0
- data/README_CN.md +852 -0
- data/Rakefile +12 -0
- data/lib/rbdantic/base/access.rb +105 -0
- data/lib/rbdantic/base/dsl.rb +79 -0
- data/lib/rbdantic/base/validation.rb +152 -0
- data/lib/rbdantic/base.rb +30 -0
- data/lib/rbdantic/config.rb +60 -0
- data/lib/rbdantic/error_detail.rb +54 -0
- data/lib/rbdantic/field.rb +188 -0
- data/lib/rbdantic/json_schema/defs_registry.rb +79 -0
- data/lib/rbdantic/json_schema/generator.rb +148 -0
- data/lib/rbdantic/json_schema/types.rb +98 -0
- data/lib/rbdantic/serialization/dumper.rb +133 -0
- data/lib/rbdantic/serialization/json_serializer.rb +60 -0
- data/lib/rbdantic/validators/field_validator.rb +83 -0
- data/lib/rbdantic/validators/model_validator.rb +59 -0
- data/lib/rbdantic/validators/types/array.rb +77 -0
- data/lib/rbdantic/validators/types/base.rb +78 -0
- data/lib/rbdantic/validators/types/boolean.rb +37 -0
- data/lib/rbdantic/validators/types/float.rb +32 -0
- data/lib/rbdantic/validators/types/hash.rb +54 -0
- data/lib/rbdantic/validators/types/integer.rb +28 -0
- data/lib/rbdantic/validators/types/model.rb +75 -0
- data/lib/rbdantic/validators/types/number.rb +63 -0
- data/lib/rbdantic/validators/types/string.rb +70 -0
- data/lib/rbdantic/validators/types/symbol.rb +30 -0
- data/lib/rbdantic/validators/types/time.rb +33 -0
- data/lib/rbdantic/validators/types.rb +63 -0
- data/lib/rbdantic/validators/validator_context.rb +43 -0
- data/lib/rbdantic/version.rb +5 -0
- data/lib/rbdantic.rb +8 -0
- data/sig/rbdantic.rbs +4 -0
- metadata +84 -0
data/README_CN.md
ADDED
|
@@ -0,0 +1,852 @@
|
|
|
1
|
+
# Rbdantic
|
|
2
|
+
|
|
3
|
+
**Ruby 数据验证与设置管理** - 一个受 Pydantic 启发的 Ruby 数据验证库。
|
|
4
|
+
|
|
5
|
+
Rbdantic 将 Pydantic 强大的数据验证能力引入 Ruby,提供运行时数据验证、序列化和 JSON Schema 生成,配合直观的 DSL 语法。
|
|
6
|
+
|
|
7
|
+
[English Documentation](README.md)
|
|
8
|
+
|
|
9
|
+
## 功能特性
|
|
10
|
+
|
|
11
|
+
- **基础模型类** - 定义带有类型检查字段的数据模型
|
|
12
|
+
- **字段约束** - 内置字符串、数字和数组约束
|
|
13
|
+
- **自定义验证器** - 支持多种模式的字段级和模型级验证器
|
|
14
|
+
- **类型强制转换** - 可配置严格程度的自动类型转换
|
|
15
|
+
- **嵌套模型** - 支持嵌套模型验证
|
|
16
|
+
- **模型继承** - 子类继承字段和验证器
|
|
17
|
+
- **模型配置** - 灵活的配置选项(额外字段、冻结模型等)
|
|
18
|
+
- **序列化** - 支持过滤选项的 Hash 或 JSON 转换
|
|
19
|
+
- **JSON Schema 生成** - 自动生成 API 文档所需的 JSON Schema
|
|
20
|
+
- **详细错误报告** - 带位置路径的结构化验证错误
|
|
21
|
+
|
|
22
|
+
## 安装
|
|
23
|
+
|
|
24
|
+
添加到 Gemfile:
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
gem 'rbdantic'
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
或直接安装:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
gem install rbdantic
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## 快速入门
|
|
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
|
+
# 创建有效用户
|
|
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
|
+
# 序列化为 Hash
|
|
60
|
+
puts user.model_dump
|
|
61
|
+
# => { name: "Alice", email: "alice@example.com", age: 30, tags: [] }
|
|
62
|
+
|
|
63
|
+
# 序列化为 JSON
|
|
64
|
+
puts user.model_dump_json
|
|
65
|
+
# => {"name":"Alice","email":"alice@example.com","age":30,"tags":[]}
|
|
66
|
+
|
|
67
|
+
# 验证错误
|
|
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
|
+
## 字段定义
|
|
81
|
+
|
|
82
|
+
### 基本字段
|
|
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
|
+
### 默认值
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
class Config < Rbdantic::BaseModel
|
|
97
|
+
# 静态默认值
|
|
98
|
+
field :timeout, Integer, default: 30
|
|
99
|
+
|
|
100
|
+
# 动态默认值(工厂)
|
|
101
|
+
field :created_at, Time, default_factory: -> { Time.now }
|
|
102
|
+
|
|
103
|
+
# 可选字段(可以为 nil)
|
|
104
|
+
field :nickname, String, optional: true
|
|
105
|
+
end
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### 字段约束
|
|
109
|
+
|
|
110
|
+
#### 字符串约束
|
|
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
|
+
#### 数字约束
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
class Product < Rbdantic::BaseModel
|
|
125
|
+
field :price, Float,
|
|
126
|
+
gt: 0, # 大于
|
|
127
|
+
le: 10000 # 小于或等于
|
|
128
|
+
|
|
129
|
+
field :quantity, Integer,
|
|
130
|
+
ge: 0, # 大于或等于
|
|
131
|
+
multiple_of: 1
|
|
132
|
+
end
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
#### 数组约束
|
|
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
|
+
### 字段内自定义验证器
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
class User < Rbdantic::BaseModel
|
|
151
|
+
# Proc 验证器,返回 false 表示失败
|
|
152
|
+
field :email, String,
|
|
153
|
+
validators: [->(v) { v.include?("@") || false }]
|
|
154
|
+
|
|
155
|
+
# Proc 验证器,返回错误消息
|
|
156
|
+
field :password, String,
|
|
157
|
+
validators: [->(v) { v.length >= 8 ? nil : "密码长度至少8个字符" }]
|
|
158
|
+
end
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## 模型配置
|
|
162
|
+
|
|
163
|
+
使用 `model_config` 配置模型行为:
|
|
164
|
+
|
|
165
|
+
```ruby
|
|
166
|
+
class User < Rbdantic::BaseModel
|
|
167
|
+
model_config(
|
|
168
|
+
extra: :forbid, # 拒绝额外字段
|
|
169
|
+
frozen: true, # 创建后不可变
|
|
170
|
+
strict: true, # 严格类型检查
|
|
171
|
+
coerce_mode: :strict, # 不进行类型转换
|
|
172
|
+
validate_assignment: true # 字段赋值时验证
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
field :name, String
|
|
176
|
+
end
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### 配置选项
|
|
180
|
+
|
|
181
|
+
| 选项 | 可选值 | 说明 |
|
|
182
|
+
|------|--------|------|
|
|
183
|
+
| `extra` | `:ignore`, `:forbid`, `:allow` | 如何处理未定义的额外字段 |
|
|
184
|
+
| `frozen` | `true`, `false` | 初始化后冻结模型使其不可变 |
|
|
185
|
+
| `strict` | `true`, `false` | 严格类型检查(不转换类型) |
|
|
186
|
+
| `coerce_mode` | `:strict`, `:coerce` | 启用/禁用类型强制转换 |
|
|
187
|
+
| `validate_assignment` | `true`, `false` | 字段赋值时进行验证 |
|
|
188
|
+
|
|
189
|
+
### 额外字段行为
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
# 忽略额外字段(默认)
|
|
193
|
+
class ModelA < Rbdantic::BaseModel
|
|
194
|
+
model_config extra: :ignore
|
|
195
|
+
field :name, String
|
|
196
|
+
end
|
|
197
|
+
ModelA.new(name: "test", extra: "data") # extra 字段被丢弃
|
|
198
|
+
|
|
199
|
+
# 禁止额外字段
|
|
200
|
+
class ModelB < Rbdantic::BaseModel
|
|
201
|
+
model_config extra: :forbid
|
|
202
|
+
field :name, String
|
|
203
|
+
end
|
|
204
|
+
ModelB.new(name: "test", extra: "data") # 抛出 ValidationError
|
|
205
|
+
|
|
206
|
+
# 允许额外字段
|
|
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
|
+
## 验证器
|
|
216
|
+
|
|
217
|
+
### 字段验证器
|
|
218
|
+
|
|
219
|
+
字段验证器在不同阶段运行:
|
|
220
|
+
|
|
221
|
+
```ruby
|
|
222
|
+
class User < Rbdantic::BaseModel
|
|
223
|
+
field :email, String
|
|
224
|
+
|
|
225
|
+
# 验证前 - 可转换值
|
|
226
|
+
field_validator :email, mode: :before do |value, ctx|
|
|
227
|
+
value&.downcase
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# 验证后 - 验证转换后的值
|
|
231
|
+
field_validator :email, mode: :after do |value, ctx|
|
|
232
|
+
raise "邮箱格式无效" unless value.include?("@")
|
|
233
|
+
value
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
#### 验证器模式
|
|
239
|
+
|
|
240
|
+
| 模式 | 说明 |
|
|
241
|
+
|------|------|
|
|
242
|
+
| `:before` | 类型验证前运行,可转换值 |
|
|
243
|
+
| `:after` | 类型验证后运行,验证最终值 |
|
|
244
|
+
| `:plain` | 替代类型验证运行(跳过类型检查) |
|
|
245
|
+
| `:wrap` | 所有其他验证器之后运行 |
|
|
246
|
+
|
|
247
|
+
### 模型验证器
|
|
248
|
+
|
|
249
|
+
模型验证器验证整个模型:
|
|
250
|
+
|
|
251
|
+
```ruby
|
|
252
|
+
class Account < Rbdantic::BaseModel
|
|
253
|
+
field :password, String
|
|
254
|
+
field :confirm_password, String
|
|
255
|
+
|
|
256
|
+
# 前置验证器 - 预处理输入数据
|
|
257
|
+
model_validator mode: :before do |data|
|
|
258
|
+
data[:password] = data[:password]&.strip
|
|
259
|
+
data
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# 后置验证器 - 验证模型状态
|
|
263
|
+
model_validator mode: :after do |model|
|
|
264
|
+
if model.password != model.confirm_password
|
|
265
|
+
raise "密码不匹配"
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## 嵌套模型
|
|
272
|
+
|
|
273
|
+
Rbdantic 像 Pydantic 一样支持嵌套模型,让你可以构建带有层级验证的复杂数据结构。
|
|
274
|
+
|
|
275
|
+
### 单层嵌套模型
|
|
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 # 嵌套模型类型
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# 从哈希创建 - 嵌套模型自动验证
|
|
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
|
+
# 或传入已构建的嵌套模型实例
|
|
303
|
+
address = Address.new(street: "456 Oak Ave", city: "Cambridge", zip_code: "02139")
|
|
304
|
+
user = User.new(name: "Jane", address: address)
|
|
305
|
+
|
|
306
|
+
# 序列化 - 嵌套模型递归输出
|
|
307
|
+
user.model_dump
|
|
308
|
+
# => { name: "Jane", address: { street: "456 Oak Ave", city: "Cambridge", zip_code: "02139" } }
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### 多层嵌套模型
|
|
312
|
+
|
|
313
|
+
可以任意深度嵌套模型:
|
|
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 # 嵌套中的嵌套
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
class Person < Rbdantic::BaseModel
|
|
327
|
+
field :name, String
|
|
328
|
+
field :birthplace, City # 两层嵌套
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# 创建多层嵌套结构
|
|
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
|
+
### 嵌套模型数组
|
|
347
|
+
|
|
348
|
+
使用 `[Type]` 简写验证嵌套模型数组:
|
|
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
|
+
# 创建包含多个商品的订单
|
|
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
|
+
# 序列化 - 数组元素递归输出
|
|
375
|
+
order.model_dump
|
|
376
|
+
# => { order_id: "ORD-001", items: [{ name: "Widget", quantity: 5, price: 9.99 }, ...] }
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### 可选嵌套模型
|
|
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 # 可以是 nil
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
# 不带 profile
|
|
393
|
+
user = User.new(name: "Bob")
|
|
394
|
+
puts user.profile # => nil
|
|
395
|
+
|
|
396
|
+
# 带 profile
|
|
397
|
+
user = User.new(name: "Bob", profile: { bio: "Developer", avatar_url: "..." })
|
|
398
|
+
puts user.profile.bio # => "Developer"
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### 嵌套模型验证错误
|
|
402
|
+
|
|
403
|
+
嵌套模型中的错误包含完整路径:
|
|
404
|
+
|
|
405
|
+
```ruby
|
|
406
|
+
begin
|
|
407
|
+
User.new(
|
|
408
|
+
name: "Alice",
|
|
409
|
+
address: {
|
|
410
|
+
street: "", # 无效: 太短
|
|
411
|
+
city: "Boston",
|
|
412
|
+
zip_code: "invalid" # 无效: 模式不匹配
|
|
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
|
+
# 多层嵌套错误路径
|
|
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
|
+
# 数组元素错误路径
|
|
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 } # 索引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
|
+
### 自引用模型
|
|
456
|
+
|
|
457
|
+
模型可以引用自身实现递归结构:
|
|
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
|
+
## 继承
|
|
477
|
+
|
|
478
|
+
字段、验证器和配置均可继承:
|
|
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 # 继承 name 和 age
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
class Cat < Animal
|
|
493
|
+
model_config extra: :allow
|
|
494
|
+
end
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
**注意:** 子类会继承父类的 `model_config`,只需要覆盖想修改的配置项。
|
|
498
|
+
|
|
499
|
+
## 序列化
|
|
500
|
+
|
|
501
|
+
### model_dump
|
|
502
|
+
|
|
503
|
+
将模型转换为 Hash,支持多种选项:
|
|
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
|
+
# 完整输出
|
|
515
|
+
user.model_dump
|
|
516
|
+
# => { name: "Alice", role: "user", active: true }
|
|
517
|
+
|
|
518
|
+
# 排除默认值字段
|
|
519
|
+
user.model_dump(exclude_defaults: true)
|
|
520
|
+
# => { name: "Alice" }
|
|
521
|
+
|
|
522
|
+
# 只包含指定字段
|
|
523
|
+
user.model_dump(include: [:name])
|
|
524
|
+
# => { name: "Alice" }
|
|
525
|
+
|
|
526
|
+
# 排除指定字段
|
|
527
|
+
user.model_dump(exclude: [:active])
|
|
528
|
+
# => { name: "Alice", role: "user" }
|
|
529
|
+
|
|
530
|
+
# 排除未设置字段(初始化时未提供的)
|
|
531
|
+
user.model_dump(exclude_unset: true)
|
|
532
|
+
# => { name: "Alice" }
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
### model_dump_json
|
|
536
|
+
|
|
537
|
+
转换为 JSON 字符串:
|
|
538
|
+
|
|
539
|
+
```ruby
|
|
540
|
+
user.model_dump_json
|
|
541
|
+
# => {"name":"Alice","role":"user","active":true}
|
|
542
|
+
|
|
543
|
+
# 带缩进
|
|
544
|
+
user.model_dump_json(indent: 2)
|
|
545
|
+
# => {
|
|
546
|
+
# "name": "Alice",
|
|
547
|
+
# "role": "user",
|
|
548
|
+
# "active": true
|
|
549
|
+
# }
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
## JSON Schema 生成
|
|
553
|
+
|
|
554
|
+
为 API 文档自动生成 JSON Schema:
|
|
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
|
+
## 类型强制转换
|
|
580
|
+
|
|
581
|
+
当设置 `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", # 转换为 42
|
|
594
|
+
price: "19.99", # 转换为 19.99
|
|
595
|
+
enabled: "yes" # 转换为 true
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
config.count # => 42 (Integer)
|
|
599
|
+
config.price # => 19.99 (Float)
|
|
600
|
+
config.enabled # => true
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
### 支持的类型转换
|
|
604
|
+
|
|
605
|
+
| 目标类型 | 源示例 |
|
|
606
|
+
|----------|--------|
|
|
607
|
+
| `String` | 任何有 `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` | 可用 `split` 分割的字符串,任何有 `to_a` 方法的值 |
|
|
612
|
+
| `Hash` | 键值对数组,任何有 `to_h` 方法的值 |
|
|
613
|
+
|
|
614
|
+
## 验证错误
|
|
615
|
+
|
|
616
|
+
ValidationError 提供详细的错误信息:
|
|
617
|
+
|
|
618
|
+
```ruby
|
|
619
|
+
begin
|
|
620
|
+
User.new(name: "", age: -1)
|
|
621
|
+
rescue Rbdantic::ValidationError => e
|
|
622
|
+
e.error_count # => 2
|
|
623
|
+
e.errors # => ErrorDetail 数组
|
|
624
|
+
e.as_json # => { errors: [...], error_count: 2 }
|
|
625
|
+
e.to_h # => 同 as_json
|
|
626
|
+
|
|
627
|
+
e.errors.each do |err|
|
|
628
|
+
err.type # => :string_too_short, :value_not_greater_than
|
|
629
|
+
err.loc # => [:name], [:age] (位置路径)
|
|
630
|
+
err.msg # => "String must be at least..."
|
|
631
|
+
err.input # => "" (原始输入值)
|
|
632
|
+
end
|
|
633
|
+
end
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
## 支持的类型
|
|
637
|
+
|
|
638
|
+
| 类型 | 说明 |
|
|
639
|
+
|------|------|
|
|
640
|
+
| `String` | 内置字符串类型 |
|
|
641
|
+
| `Integer` | 内置整数类型 |
|
|
642
|
+
| `Float` | 内置浮点数类型 |
|
|
643
|
+
| `Rbdantic::Boolean` | 布尔字段,接受 true/false |
|
|
644
|
+
| `Symbol` | Ruby 符号,最大长度 256 字符(防止 DoS 攻击) |
|
|
645
|
+
| `[Type]` | 带元素校验的数组 |
|
|
646
|
+
| `Hash` | 键值哈希类型 |
|
|
647
|
+
| `Time` | Ruby Time 类型 |
|
|
648
|
+
| `Rbdantic::BaseModel` 子类 | 嵌套模型验证 |
|
|
649
|
+
|
|
650
|
+
**注意:** 对外布尔字段统一使用 `Rbdantic::Boolean`。
|
|
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
|
+
## 格式验证
|
|
660
|
+
|
|
661
|
+
内置常用格式的验证器:
|
|
662
|
+
|
|
663
|
+
```ruby
|
|
664
|
+
class User < Rbdantic::BaseModel
|
|
665
|
+
field :email, String, format: :email # 基础邮箱验证
|
|
666
|
+
field :website, String, format: :uri # URI 验证 (http/https)
|
|
667
|
+
end
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
| 格式 | 模式 |
|
|
671
|
+
|------|------|
|
|
672
|
+
| `:email` | 基础邮箱检查 (user@domain) |
|
|
673
|
+
| `:uri` | HTTP/HTTPS URI |
|
|
674
|
+
|
|
675
|
+
复杂验证请使用自定义 `pattern` 正则或 `field_validator`。
|
|
676
|
+
|
|
677
|
+
## 限制与安全
|
|
678
|
+
|
|
679
|
+
### 安全限制
|
|
680
|
+
|
|
681
|
+
| 限制 | 值 | 目的 |
|
|
682
|
+
|------|-----|------|
|
|
683
|
+
| Symbol 最大长度 | 256 字符 | 防止 Symbol DoS 攻击 |
|
|
684
|
+
| 嵌套模型深度 | ~20 层 | 防止栈溢出 |
|
|
685
|
+
|
|
686
|
+
这些限制防止恶意输入耗尽内存或导致栈溢出。
|
|
687
|
+
|
|
688
|
+
### 线程安全
|
|
689
|
+
|
|
690
|
+
模型初始化后的读取操作是线程安全的。但需注意:
|
|
691
|
+
|
|
692
|
+
- 初始化过程中的验证不是线程安全的(使用内部状态)
|
|
693
|
+
- `validate_assignment` 模式使用实例级锁
|
|
694
|
+
- 变更期间避免跨线程共享模型实例
|
|
695
|
+
|
|
696
|
+
## 与 Pydantic 的差异
|
|
697
|
+
|
|
698
|
+
| 功能 | Pydantic | Rbdantic |
|
|
699
|
+
|------|----------|----------|
|
|
700
|
+
| 字段别名 | `Field(alias="name")` | `alias_name:` 配合 `by_alias: true` |
|
|
701
|
+
| 计算字段 | `@computed_field` | 不支持 |
|
|
702
|
+
| 泛型模型 | `BaseModel[T]` | 不支持 |
|
|
703
|
+
| 序列化别名 | `serialization_alias` | 使用 `alias_name:` 与 dump/schema 的 `by_alias:` |
|
|
704
|
+
| 模型复制/更新 | `model.copy(update={})` | 提供 `copy(deep:)` 与 `update(**data)` 辅助方法 |
|
|
705
|
+
| 判断联合类型 | `Annotated[Union, Field(discriminator)]` | 不支持 |
|
|
706
|
+
| 自定义类型适配器 | `TypeAdapter` | 使用验证器替代 |
|
|
707
|
+
| 布尔类型 | `bool` | `Rbdantic::Boolean` |
|
|
708
|
+
| 配置类 | `BaseModelConfig` | `model_config` 哈希 |
|
|
709
|
+
|
|
710
|
+
### API 命名差异
|
|
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
|
+
## 系统要求
|
|
723
|
+
|
|
724
|
+
- Ruby >= 2.7(支持关键字参数和模式匹配)
|
|
725
|
+
- 无外部依赖(纯 Ruby 实现)
|
|
726
|
+
|
|
727
|
+
## 错误处理最佳实践
|
|
728
|
+
|
|
729
|
+
### 捕获特定字段错误
|
|
730
|
+
|
|
731
|
+
```ruby
|
|
732
|
+
begin
|
|
733
|
+
User.new(name: "", email: "invalid")
|
|
734
|
+
rescue Rbdantic::ValidationError => e
|
|
735
|
+
# 查找特定字段的错误
|
|
736
|
+
name_errors = e.errors.select { |err| err.loc.first == :name }
|
|
737
|
+
puts "名称错误: #{name_errors.map(&:msg).join(', ')}"
|
|
738
|
+
|
|
739
|
+
# 按字段分组错误
|
|
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
|
+
### 自定义错误消息
|
|
748
|
+
|
|
749
|
+
使用 `field_validator` 自定义消息:
|
|
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: "密码至少需要8个字符(当前#{value.length}个)",
|
|
761
|
+
input: value
|
|
762
|
+
)
|
|
763
|
+
end
|
|
764
|
+
value
|
|
765
|
+
end
|
|
766
|
+
end
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
### API 错误 JSON 响应
|
|
770
|
+
|
|
771
|
+
```ruby
|
|
772
|
+
rescue Rbdantic::ValidationError => e
|
|
773
|
+
# 返回 JSON 用于 API 响应
|
|
774
|
+
status 400
|
|
775
|
+
json e.as_json
|
|
776
|
+
# => { "errors": [...], "error_count": 2 }
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
## API 参考
|
|
780
|
+
|
|
781
|
+
### Rbdantic::BaseModel 类方法
|
|
782
|
+
|
|
783
|
+
| 方法 | 说明 |
|
|
784
|
+
|------|------|
|
|
785
|
+
| `field(name, type, **options)` | 定义字段及其类型和约束 |
|
|
786
|
+
| `model_config(**options)` | 配置模型行为 |
|
|
787
|
+
| `field_validator(name, mode:, &block)` | 定义字段级验证器 |
|
|
788
|
+
| `model_validator(mode:, &block)` | 定义模型级验证器 |
|
|
789
|
+
| `model_json_schema(**options)` | 生成 JSON Schema |
|
|
790
|
+
| `model_fields` | 返回字段定义哈希 |
|
|
791
|
+
| `model_config` | 返回模型配置 |
|
|
792
|
+
| `inherited(subclass)` | 继承钩子(内部使用) |
|
|
793
|
+
|
|
794
|
+
### 实例方法
|
|
795
|
+
|
|
796
|
+
| 方法 | 说明 |
|
|
797
|
+
|------|------|
|
|
798
|
+
| `initialize(data = {})` | 创建并验证模型 |
|
|
799
|
+
| `model_dump(**options)` | 转换为 Hash |
|
|
800
|
+
| `model_dump_json(indent: nil)` | 转换为 JSON 字符串 |
|
|
801
|
+
| `[name]` | 括号访问字段值 |
|
|
802
|
+
| `[name] = value` | 括号赋值字段值 |
|
|
803
|
+
|
|
804
|
+
### 字段选项
|
|
805
|
+
|
|
806
|
+
| 选项 | 类型 | 说明 |
|
|
807
|
+
|------|------|------|
|
|
808
|
+
| `default` | Any | 静态默认值 |
|
|
809
|
+
| `default_factory` | Proc | 动态默认值生成器 |
|
|
810
|
+
| `optional` | Boolean | 允许 nil 值 |
|
|
811
|
+
| `required` | Boolean | 设为 `false` 允许 nil(等同于 `optional: true`) |
|
|
812
|
+
| `validators` | Array | 自定义验证器 Proc |
|
|
813
|
+
| `alias_name` | Symbol | 输入/输出的替代名称(配合 `by_alias: true` 使用) |
|
|
814
|
+
| `format` | Symbol | 内置格式验证器(`:email`、`:uri`、`:uuid`) |
|
|
815
|
+
| `min_length` | Integer | 字符串最小长度 |
|
|
816
|
+
| `max_length` | Integer | 字符串最大长度 |
|
|
817
|
+
| `pattern` | Regexp | 字符串正则匹配 |
|
|
818
|
+
| `gt` | Numeric | 大于 |
|
|
819
|
+
| `ge` | Numeric | 大于或等于 |
|
|
820
|
+
| `lt` | Numeric | 小于 |
|
|
821
|
+
| `le` | Numeric | 小于或等于 |
|
|
822
|
+
| `multiple_of` | Numeric | 必须是该数的倍数 |
|
|
823
|
+
| `min_items` | Integer | 数组最小元素数 |
|
|
824
|
+
| `max_items` | Integer | 数组最大元素数 |
|
|
825
|
+
| `unique_items` | Boolean | 数组元素必须唯一 |
|
|
826
|
+
|
|
827
|
+
## 开发
|
|
828
|
+
|
|
829
|
+
检出仓库后:
|
|
830
|
+
|
|
831
|
+
```bash
|
|
832
|
+
bin/setup # 安装依赖
|
|
833
|
+
rake spec # 运行测试
|
|
834
|
+
bin/console # 交互式提示
|
|
835
|
+
bundle exec rake install # 本地安装 gem
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
## 贡献
|
|
839
|
+
|
|
840
|
+
欢迎提交 Bug 报告和 Pull Request。
|
|
841
|
+
|
|
842
|
+
## 许可证
|
|
843
|
+
|
|
844
|
+
本 gem 基于 [MIT 许可证](https://opensource.org/licenses/MIT) 开源。
|
|
845
|
+
|
|
846
|
+
## 致谢
|
|
847
|
+
|
|
848
|
+
本库受 [Pydantic](https://github.com/pydantic/pydantic) 启发 - 优秀的 Python 数据验证库。
|
|
849
|
+
|
|
850
|
+
## 开发说明
|
|
851
|
+
|
|
852
|
+
本库主要由 AI (Claude) 协助开发,展示了 AI 工具如何加速软件开发,同时保持代码质量和全面测试。
|