easy_talk 3.0.0 → 3.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 +4 -4
- data/CHANGELOG.md +30 -0
- data/README.md +649 -2
- data/lib/easy_talk/builders/object_builder.rb +95 -1
- data/lib/easy_talk/builders/typed_array_builder.rb +5 -2
- data/lib/easy_talk/configuration.rb +21 -1
- data/lib/easy_talk/keywords.rb +2 -0
- data/lib/easy_talk/model.rb +52 -1
- data/lib/easy_talk/property.rb +68 -2
- data/lib/easy_talk/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b091eaff6c33ddcc23f0c5d0147bf28f94a7421c95a0ca47ad917af8e42fad20
|
|
4
|
+
data.tar.gz: 9a6c0de5afff453a566940b7b1684e2075e70b53379b2bf3b7583a46ca370c58
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 32a49bfa38a5279340b6699ff26d8ac415c1574f83da1488d403d422d4b49cd4aea94d5424d858c5ae2326e914c4e83995a97eb67fe4b005543e6f8984d6a60c
|
|
7
|
+
data.tar.gz: 02ad31d7b34aac67a4777a709ffb286487675f28e6a2c18757dd87e7a006743ab6cd0f75b817fdb6abb23d3f9ef63384fcbd5f063e1eb2d0dc8a06c7107ae0e0
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,33 @@
|
|
|
1
|
+
## [3.1.0] - 2025-12-18
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
- **JSON Schema `$schema` Keyword Support**: Added ability to declare which JSON Schema draft version schemas conform to
|
|
5
|
+
- New `schema_version` configuration option supporting Draft-04, Draft-06, Draft-07, Draft 2019-09, and Draft 2020-12
|
|
6
|
+
- Global configuration via `EasyTalk.configure { |c| c.schema_version = :draft202012 }`
|
|
7
|
+
- Per-model override using `schema_version` keyword in `define_schema` block
|
|
8
|
+
- Support for custom schema URIs
|
|
9
|
+
- `$schema` only appears at root level (not in nested models)
|
|
10
|
+
- Default is `:none` for backward compatibility
|
|
11
|
+
|
|
12
|
+
- **JSON Schema `$id` Keyword Support**: Added ability to provide a unique identifier URI for schemas
|
|
13
|
+
- New `schema_id` configuration option for setting schema identifiers
|
|
14
|
+
- Global configuration via `EasyTalk.configure { |c| c.schema_id = 'https://example.com/schema.json' }`
|
|
15
|
+
- Per-model override using `schema_id` keyword in `define_schema` block
|
|
16
|
+
- Supports absolute URIs, relative URIs, and URN formats
|
|
17
|
+
- `$id` only appears at root level (not in nested models)
|
|
18
|
+
- Default is `nil` for backward compatibility
|
|
19
|
+
|
|
20
|
+
- **JSON Schema `$ref` and `$defs` Support**: Added ability to reference reusable schema definitions for nested models
|
|
21
|
+
- New `use_refs` configuration option to globally enable `$ref` for nested EasyTalk models
|
|
22
|
+
- Global configuration via `EasyTalk.configure { |c| c.use_refs = true }`
|
|
23
|
+
- Per-property override using `ref: true` or `ref: false` constraint
|
|
24
|
+
- Nested models are automatically added to `$defs` when `$ref` is enabled
|
|
25
|
+
- Supports direct model properties, `T::Array[Model]`, and `T.nilable(Model)` types
|
|
26
|
+
- Nilable models with `$ref` use `anyOf` with `$ref` and `null` type
|
|
27
|
+
- Multiple references to the same model only create one `$defs` entry
|
|
28
|
+
- Additional constraints (title, description) can be combined with `$ref`
|
|
29
|
+
- Default is `false` for backward compatibility (nested schemas are inlined)
|
|
30
|
+
|
|
1
31
|
## [3.0.0] - 2025-01-03
|
|
2
32
|
|
|
3
33
|
### BREAKING CHANGES
|
data/README.md
CHANGED
|
@@ -16,6 +16,9 @@ EasyTalk is a Ruby library that simplifies defining and generating JSON Schema.
|
|
|
16
16
|
* **Schema Composition**: Define EasyTalk models and reference them in other EasyTalk models to create complex schemas.
|
|
17
17
|
* **Enhanced Model Integration**: Automatic instantiation of nested EasyTalk models from hash attributes.
|
|
18
18
|
* **Flexible Configuration**: Global and per-model configuration options for fine-tuned control.
|
|
19
|
+
* **JSON Schema Version Support**: Configure the `$schema` keyword to declare which JSON Schema draft version your schemas conform to (Draft-04 through Draft 2020-12).
|
|
20
|
+
* **Schema Identification**: Configure the `$id` keyword to provide a unique identifier URI for your schemas.
|
|
21
|
+
* **Schema References**: Use `$ref` and `$defs` for reusable schema definitions, reducing duplication when models are used in multiple places.
|
|
19
22
|
|
|
20
23
|
### Use Cases
|
|
21
24
|
- API request/response validation
|
|
@@ -589,6 +592,10 @@ EasyTalk.configure do |config|
|
|
|
589
592
|
config.default_additional_properties = false # Control additional properties on all models
|
|
590
593
|
config.nilable_is_optional = false # Makes T.nilable properties also optional
|
|
591
594
|
config.auto_validations = true # Automatically generate ActiveModel validations
|
|
595
|
+
config.schema_version = :none # JSON Schema version for $schema keyword
|
|
596
|
+
# Options: :none, :draft202012, :draft201909, :draft7, :draft6, :draft4
|
|
597
|
+
config.schema_id = nil # Base URI for $id keyword (nil = no $id)
|
|
598
|
+
config.use_refs = false # Use $ref for nested models instead of inlining
|
|
592
599
|
end
|
|
593
600
|
```
|
|
594
601
|
|
|
@@ -1168,17 +1175,657 @@ bundle exec rubocop
|
|
|
1168
1175
|
### Contributing Guidelines
|
|
1169
1176
|
Bug reports and pull requests are welcome on GitHub at https://github.com/sergiobayona/easy_talk.
|
|
1170
1177
|
|
|
1178
|
+
## JSON Schema Version (`$schema` Keyword)
|
|
1179
|
+
|
|
1180
|
+
The `$schema` keyword declares which JSON Schema dialect (draft version) a schema conforms to. EasyTalk supports configuring this at both the global and per-model level.
|
|
1181
|
+
|
|
1182
|
+
### Why Use `$schema`?
|
|
1183
|
+
|
|
1184
|
+
The `$schema` keyword:
|
|
1185
|
+
- Declares the JSON Schema version your schema is written against
|
|
1186
|
+
- Helps validators understand which specification to use
|
|
1187
|
+
- Enables tooling to provide appropriate validation and autocomplete
|
|
1188
|
+
- Documents the schema dialect for consumers of your API
|
|
1189
|
+
|
|
1190
|
+
### Supported Draft Versions
|
|
1191
|
+
|
|
1192
|
+
EasyTalk supports the following JSON Schema draft versions:
|
|
1193
|
+
|
|
1194
|
+
| Symbol | JSON Schema Version | URI |
|
|
1195
|
+
|--------|---------------------|-----|
|
|
1196
|
+
| `:draft202012` | Draft 2020-12 (latest) | `https://json-schema.org/draft/2020-12/schema` |
|
|
1197
|
+
| `:draft201909` | Draft 2019-09 | `https://json-schema.org/draft/2019-09/schema` |
|
|
1198
|
+
| `:draft7` | Draft-07 | `http://json-schema.org/draft-07/schema#` |
|
|
1199
|
+
| `:draft6` | Draft-06 | `http://json-schema.org/draft-06/schema#` |
|
|
1200
|
+
| `:draft4` | Draft-04 | `http://json-schema.org/draft-04/schema#` |
|
|
1201
|
+
| `:none` | No `$schema` output (default) | N/A |
|
|
1202
|
+
|
|
1203
|
+
### Global Configuration
|
|
1204
|
+
|
|
1205
|
+
Configure the schema version globally to apply to all models:
|
|
1206
|
+
|
|
1207
|
+
```ruby
|
|
1208
|
+
EasyTalk.configure do |config|
|
|
1209
|
+
config.schema_version = :draft202012 # Use JSON Schema Draft 2020-12
|
|
1210
|
+
end
|
|
1211
|
+
```
|
|
1212
|
+
|
|
1213
|
+
With this configuration, all models will include `$schema` in their output:
|
|
1214
|
+
|
|
1215
|
+
```ruby
|
|
1216
|
+
class User
|
|
1217
|
+
include EasyTalk::Model
|
|
1218
|
+
|
|
1219
|
+
define_schema do
|
|
1220
|
+
property :name, String
|
|
1221
|
+
end
|
|
1222
|
+
end
|
|
1223
|
+
|
|
1224
|
+
User.json_schema
|
|
1225
|
+
# => {
|
|
1226
|
+
# "$schema" => "https://json-schema.org/draft/2020-12/schema",
|
|
1227
|
+
# "type" => "object",
|
|
1228
|
+
# "properties" => { "name" => { "type" => "string" } },
|
|
1229
|
+
# "required" => ["name"],
|
|
1230
|
+
# "additionalProperties" => false
|
|
1231
|
+
# }
|
|
1232
|
+
```
|
|
1233
|
+
|
|
1234
|
+
### Per-Model Configuration
|
|
1235
|
+
|
|
1236
|
+
Override the global setting for individual models using the `schema_version` keyword in the schema definition:
|
|
1237
|
+
|
|
1238
|
+
```ruby
|
|
1239
|
+
class LegacyModel
|
|
1240
|
+
include EasyTalk::Model
|
|
1241
|
+
|
|
1242
|
+
define_schema do
|
|
1243
|
+
schema_version :draft7 # Use Draft-07 for this specific model
|
|
1244
|
+
property :name, String
|
|
1245
|
+
end
|
|
1246
|
+
end
|
|
1247
|
+
|
|
1248
|
+
LegacyModel.json_schema
|
|
1249
|
+
# => {
|
|
1250
|
+
# "$schema" => "http://json-schema.org/draft-07/schema#",
|
|
1251
|
+
# "type" => "object",
|
|
1252
|
+
# ...
|
|
1253
|
+
# }
|
|
1254
|
+
```
|
|
1255
|
+
|
|
1256
|
+
### Disabling `$schema` for Specific Models
|
|
1257
|
+
|
|
1258
|
+
If you have a global schema version configured but want to exclude `$schema` from a specific model, use `:none`:
|
|
1259
|
+
|
|
1260
|
+
```ruby
|
|
1261
|
+
EasyTalk.configure do |config|
|
|
1262
|
+
config.schema_version = :draft202012 # Global default
|
|
1263
|
+
end
|
|
1264
|
+
|
|
1265
|
+
class InternalModel
|
|
1266
|
+
include EasyTalk::Model
|
|
1267
|
+
|
|
1268
|
+
define_schema do
|
|
1269
|
+
schema_version :none # No $schema for this model
|
|
1270
|
+
property :data, String
|
|
1271
|
+
end
|
|
1272
|
+
end
|
|
1273
|
+
|
|
1274
|
+
InternalModel.json_schema
|
|
1275
|
+
# => {
|
|
1276
|
+
# "type" => "object",
|
|
1277
|
+
# "properties" => { "data" => { "type" => "string" } },
|
|
1278
|
+
# ...
|
|
1279
|
+
# }
|
|
1280
|
+
# Note: No "$schema" key present
|
|
1281
|
+
```
|
|
1282
|
+
|
|
1283
|
+
### Custom Schema URIs
|
|
1284
|
+
|
|
1285
|
+
You can also specify a custom URI if you're using a custom meta-schema or a different schema registry:
|
|
1286
|
+
|
|
1287
|
+
```ruby
|
|
1288
|
+
class CustomModel
|
|
1289
|
+
include EasyTalk::Model
|
|
1290
|
+
|
|
1291
|
+
define_schema do
|
|
1292
|
+
schema_version 'https://my-company.com/schemas/v1/meta-schema.json'
|
|
1293
|
+
property :id, String
|
|
1294
|
+
end
|
|
1295
|
+
end
|
|
1296
|
+
```
|
|
1297
|
+
|
|
1298
|
+
### Nested Models
|
|
1299
|
+
|
|
1300
|
+
The `$schema` keyword only appears at the root level of the schema. When you have nested EasyTalk models, only the top-level model's `json_schema` output will include `$schema`:
|
|
1301
|
+
|
|
1302
|
+
```ruby
|
|
1303
|
+
EasyTalk.configure do |config|
|
|
1304
|
+
config.schema_version = :draft202012
|
|
1305
|
+
end
|
|
1306
|
+
|
|
1307
|
+
class Address
|
|
1308
|
+
include EasyTalk::Model
|
|
1309
|
+
define_schema do
|
|
1310
|
+
property :city, String
|
|
1311
|
+
end
|
|
1312
|
+
end
|
|
1313
|
+
|
|
1314
|
+
class User
|
|
1315
|
+
include EasyTalk::Model
|
|
1316
|
+
define_schema do
|
|
1317
|
+
property :name, String
|
|
1318
|
+
property :address, Address
|
|
1319
|
+
end
|
|
1320
|
+
end
|
|
1321
|
+
|
|
1322
|
+
User.json_schema
|
|
1323
|
+
# => {
|
|
1324
|
+
# "$schema" => "https://json-schema.org/draft/2020-12/schema", # Only at root
|
|
1325
|
+
# "type" => "object",
|
|
1326
|
+
# "properties" => {
|
|
1327
|
+
# "name" => { "type" => "string" },
|
|
1328
|
+
# "address" => {
|
|
1329
|
+
# "type" => "object", # No $schema here
|
|
1330
|
+
# "properties" => { "city" => { "type" => "string" } },
|
|
1331
|
+
# ...
|
|
1332
|
+
# }
|
|
1333
|
+
# },
|
|
1334
|
+
# ...
|
|
1335
|
+
# }
|
|
1336
|
+
```
|
|
1337
|
+
|
|
1338
|
+
### Default Behavior
|
|
1339
|
+
|
|
1340
|
+
By default, `schema_version` is set to `:none`, meaning no `$schema` keyword is included in the generated schemas. This maintains backward compatibility with previous versions of EasyTalk.
|
|
1341
|
+
|
|
1342
|
+
### Best Practices
|
|
1343
|
+
|
|
1344
|
+
1. **Choose a version appropriate for your validators**: If you're using a specific JSON Schema validator, check which drafts it supports.
|
|
1345
|
+
|
|
1346
|
+
2. **Use Draft 2020-12 for new projects**: It's the latest stable version with the most features.
|
|
1347
|
+
|
|
1348
|
+
3. **Be consistent**: Use global configuration for consistency across your application, and only override per-model when necessary.
|
|
1349
|
+
|
|
1350
|
+
4. **Consider your consumers**: If your schemas are consumed by external systems, ensure they support the draft version you're using.
|
|
1351
|
+
|
|
1352
|
+
## Schema Identifier (`$id` Keyword)
|
|
1353
|
+
|
|
1354
|
+
The `$id` keyword provides a unique identifier for your JSON Schema document. EasyTalk supports configuring this at both the global and per-model level.
|
|
1355
|
+
|
|
1356
|
+
### Why Use `$id`?
|
|
1357
|
+
|
|
1358
|
+
The `$id` keyword:
|
|
1359
|
+
- Establishes a unique URI identifier for the schema
|
|
1360
|
+
- Enables referencing schemas from other documents via `$ref`
|
|
1361
|
+
- Provides a base URI for resolving relative references within the schema
|
|
1362
|
+
- Documents the canonical location of the schema
|
|
1363
|
+
|
|
1364
|
+
### Global Configuration
|
|
1365
|
+
|
|
1366
|
+
Configure the schema ID globally to apply to all models:
|
|
1367
|
+
|
|
1368
|
+
```ruby
|
|
1369
|
+
EasyTalk.configure do |config|
|
|
1370
|
+
config.schema_id = 'https://example.com/schemas/base.json'
|
|
1371
|
+
end
|
|
1372
|
+
```
|
|
1373
|
+
|
|
1374
|
+
With this configuration, all models will include `$id` in their output:
|
|
1375
|
+
|
|
1376
|
+
```ruby
|
|
1377
|
+
class User
|
|
1378
|
+
include EasyTalk::Model
|
|
1379
|
+
|
|
1380
|
+
define_schema do
|
|
1381
|
+
property :name, String
|
|
1382
|
+
end
|
|
1383
|
+
end
|
|
1384
|
+
|
|
1385
|
+
User.json_schema
|
|
1386
|
+
# => {
|
|
1387
|
+
# "$id" => "https://example.com/schemas/base.json",
|
|
1388
|
+
# "type" => "object",
|
|
1389
|
+
# "properties" => { "name" => { "type" => "string" } },
|
|
1390
|
+
# "required" => ["name"],
|
|
1391
|
+
# "additionalProperties" => false
|
|
1392
|
+
# }
|
|
1393
|
+
```
|
|
1394
|
+
|
|
1395
|
+
### Per-Model Configuration
|
|
1396
|
+
|
|
1397
|
+
Override the global setting for individual models using the `schema_id` keyword in the schema definition:
|
|
1398
|
+
|
|
1399
|
+
```ruby
|
|
1400
|
+
class User
|
|
1401
|
+
include EasyTalk::Model
|
|
1402
|
+
|
|
1403
|
+
define_schema do
|
|
1404
|
+
schema_id 'https://example.com/schemas/user.schema.json'
|
|
1405
|
+
property :name, String
|
|
1406
|
+
property :email, String
|
|
1407
|
+
end
|
|
1408
|
+
end
|
|
1409
|
+
|
|
1410
|
+
User.json_schema
|
|
1411
|
+
# => {
|
|
1412
|
+
# "$id" => "https://example.com/schemas/user.schema.json",
|
|
1413
|
+
# "type" => "object",
|
|
1414
|
+
# ...
|
|
1415
|
+
# }
|
|
1416
|
+
```
|
|
1417
|
+
|
|
1418
|
+
### Disabling `$id` for Specific Models
|
|
1419
|
+
|
|
1420
|
+
If you have a global schema ID configured but want to exclude `$id` from a specific model, use `:none`:
|
|
1421
|
+
|
|
1422
|
+
```ruby
|
|
1423
|
+
EasyTalk.configure do |config|
|
|
1424
|
+
config.schema_id = 'https://example.com/schemas/default.json'
|
|
1425
|
+
end
|
|
1426
|
+
|
|
1427
|
+
class InternalModel
|
|
1428
|
+
include EasyTalk::Model
|
|
1429
|
+
|
|
1430
|
+
define_schema do
|
|
1431
|
+
schema_id :none # No $id for this model
|
|
1432
|
+
property :data, String
|
|
1433
|
+
end
|
|
1434
|
+
end
|
|
1435
|
+
|
|
1436
|
+
InternalModel.json_schema
|
|
1437
|
+
# => {
|
|
1438
|
+
# "type" => "object",
|
|
1439
|
+
# "properties" => { "data" => { "type" => "string" } },
|
|
1440
|
+
# ...
|
|
1441
|
+
# }
|
|
1442
|
+
# Note: No "$id" key present
|
|
1443
|
+
```
|
|
1444
|
+
|
|
1445
|
+
### Combining `$schema` and `$id`
|
|
1446
|
+
|
|
1447
|
+
When both `$schema` and `$id` are configured, they appear in the standard order (`$schema` first, then `$id`):
|
|
1448
|
+
|
|
1449
|
+
```ruby
|
|
1450
|
+
class Product
|
|
1451
|
+
include EasyTalk::Model
|
|
1452
|
+
|
|
1453
|
+
define_schema do
|
|
1454
|
+
schema_version :draft202012
|
|
1455
|
+
schema_id 'https://example.com/schemas/product.schema.json'
|
|
1456
|
+
property :name, String
|
|
1457
|
+
property :price, Float
|
|
1458
|
+
end
|
|
1459
|
+
end
|
|
1460
|
+
|
|
1461
|
+
Product.json_schema
|
|
1462
|
+
# => {
|
|
1463
|
+
# "$schema" => "https://json-schema.org/draft/2020-12/schema",
|
|
1464
|
+
# "$id" => "https://example.com/schemas/product.schema.json",
|
|
1465
|
+
# "type" => "object",
|
|
1466
|
+
# ...
|
|
1467
|
+
# }
|
|
1468
|
+
```
|
|
1469
|
+
|
|
1470
|
+
### Nested Models
|
|
1471
|
+
|
|
1472
|
+
The `$id` keyword only appears at the root level of the schema. When you have nested EasyTalk models, only the top-level model's `json_schema` output will include `$id`:
|
|
1473
|
+
|
|
1474
|
+
```ruby
|
|
1475
|
+
EasyTalk.configure do |config|
|
|
1476
|
+
config.schema_id = 'https://example.com/schemas/user.json'
|
|
1477
|
+
end
|
|
1478
|
+
|
|
1479
|
+
class Address
|
|
1480
|
+
include EasyTalk::Model
|
|
1481
|
+
define_schema do
|
|
1482
|
+
property :city, String
|
|
1483
|
+
end
|
|
1484
|
+
end
|
|
1485
|
+
|
|
1486
|
+
class User
|
|
1487
|
+
include EasyTalk::Model
|
|
1488
|
+
define_schema do
|
|
1489
|
+
property :name, String
|
|
1490
|
+
property :address, Address
|
|
1491
|
+
end
|
|
1492
|
+
end
|
|
1493
|
+
|
|
1494
|
+
User.json_schema
|
|
1495
|
+
# => {
|
|
1496
|
+
# "$id" => "https://example.com/schemas/user.json", # Only at root
|
|
1497
|
+
# "type" => "object",
|
|
1498
|
+
# "properties" => {
|
|
1499
|
+
# "name" => { "type" => "string" },
|
|
1500
|
+
# "address" => {
|
|
1501
|
+
# "type" => "object", # No $id here
|
|
1502
|
+
# "properties" => { "city" => { "type" => "string" } },
|
|
1503
|
+
# ...
|
|
1504
|
+
# }
|
|
1505
|
+
# },
|
|
1506
|
+
# ...
|
|
1507
|
+
# }
|
|
1508
|
+
```
|
|
1509
|
+
|
|
1510
|
+
### URI Formats
|
|
1511
|
+
|
|
1512
|
+
The `$id` accepts various URI formats:
|
|
1513
|
+
|
|
1514
|
+
```ruby
|
|
1515
|
+
# Absolute URI (recommended for published schemas)
|
|
1516
|
+
schema_id 'https://example.com/schemas/user.schema.json'
|
|
1517
|
+
|
|
1518
|
+
# Relative URI
|
|
1519
|
+
schema_id 'user.schema.json'
|
|
1520
|
+
|
|
1521
|
+
# URN format
|
|
1522
|
+
schema_id 'urn:example:user-schema'
|
|
1523
|
+
```
|
|
1524
|
+
|
|
1525
|
+
### Default Behavior
|
|
1526
|
+
|
|
1527
|
+
By default, `schema_id` is set to `nil`, meaning no `$id` keyword is included in the generated schemas. This maintains backward compatibility with previous versions of EasyTalk.
|
|
1528
|
+
|
|
1529
|
+
### Best Practices
|
|
1530
|
+
|
|
1531
|
+
1. **Use absolute URIs for published schemas**: This ensures global uniqueness and enables external references.
|
|
1532
|
+
|
|
1533
|
+
2. **Follow a consistent naming convention**: For example, `https://yourdomain.com/schemas/{model-name}.schema.json`.
|
|
1534
|
+
|
|
1535
|
+
3. **Keep IDs stable**: Once published, avoid changing schema IDs as external systems may reference them.
|
|
1536
|
+
|
|
1537
|
+
4. **Combine with `$schema`**: When publishing schemas, include both `$schema` (for validation) and `$id` (for identification).
|
|
1538
|
+
|
|
1539
|
+
## Schema References (`$ref` and `$defs`)
|
|
1540
|
+
|
|
1541
|
+
The `$ref` keyword allows you to reference reusable schema definitions, reducing duplication when the same model is used in multiple places. EasyTalk supports automatic `$ref` generation for nested models.
|
|
1542
|
+
|
|
1543
|
+
### Why Use `$ref`?
|
|
1544
|
+
|
|
1545
|
+
The `$ref` keyword:
|
|
1546
|
+
- Reduces schema duplication when the same model appears multiple times
|
|
1547
|
+
- Produces cleaner, more organized schemas
|
|
1548
|
+
- Improves schema readability and maintainability
|
|
1549
|
+
- Aligns with JSON Schema best practices for reusable definitions
|
|
1550
|
+
|
|
1551
|
+
### Default Behavior (Inline Schemas)
|
|
1552
|
+
|
|
1553
|
+
By default, EasyTalk inlines nested model schemas directly:
|
|
1554
|
+
|
|
1555
|
+
```ruby
|
|
1556
|
+
class Address
|
|
1557
|
+
include EasyTalk::Model
|
|
1558
|
+
define_schema do
|
|
1559
|
+
property :street, String
|
|
1560
|
+
property :city, String
|
|
1561
|
+
end
|
|
1562
|
+
end
|
|
1563
|
+
|
|
1564
|
+
class Person
|
|
1565
|
+
include EasyTalk::Model
|
|
1566
|
+
define_schema do
|
|
1567
|
+
property :name, String
|
|
1568
|
+
property :address, Address
|
|
1569
|
+
end
|
|
1570
|
+
end
|
|
1571
|
+
|
|
1572
|
+
Person.json_schema
|
|
1573
|
+
# => {
|
|
1574
|
+
# "type" => "object",
|
|
1575
|
+
# "properties" => {
|
|
1576
|
+
# "name" => { "type" => "string" },
|
|
1577
|
+
# "address" => {
|
|
1578
|
+
# "type" => "object",
|
|
1579
|
+
# "properties" => {
|
|
1580
|
+
# "street" => { "type" => "string" },
|
|
1581
|
+
# "city" => { "type" => "string" }
|
|
1582
|
+
# },
|
|
1583
|
+
# ...
|
|
1584
|
+
# }
|
|
1585
|
+
# },
|
|
1586
|
+
# ...
|
|
1587
|
+
# }
|
|
1588
|
+
```
|
|
1589
|
+
|
|
1590
|
+
### Enabling `$ref` References
|
|
1591
|
+
|
|
1592
|
+
#### Global Configuration
|
|
1593
|
+
|
|
1594
|
+
Enable `$ref` generation for all nested models:
|
|
1595
|
+
|
|
1596
|
+
```ruby
|
|
1597
|
+
EasyTalk.configure do |config|
|
|
1598
|
+
config.use_refs = true
|
|
1599
|
+
end
|
|
1600
|
+
```
|
|
1601
|
+
|
|
1602
|
+
With this configuration, nested models are referenced via `$ref` and their definitions are placed in `$defs`:
|
|
1603
|
+
|
|
1604
|
+
```ruby
|
|
1605
|
+
Person.json_schema
|
|
1606
|
+
# => {
|
|
1607
|
+
# "type" => "object",
|
|
1608
|
+
# "properties" => {
|
|
1609
|
+
# "name" => { "type" => "string" },
|
|
1610
|
+
# "address" => { "$ref" => "#/$defs/Address" }
|
|
1611
|
+
# },
|
|
1612
|
+
# "$defs" => {
|
|
1613
|
+
# "Address" => {
|
|
1614
|
+
# "type" => "object",
|
|
1615
|
+
# "properties" => {
|
|
1616
|
+
# "street" => { "type" => "string" },
|
|
1617
|
+
# "city" => { "type" => "string" }
|
|
1618
|
+
# },
|
|
1619
|
+
# ...
|
|
1620
|
+
# }
|
|
1621
|
+
# },
|
|
1622
|
+
# ...
|
|
1623
|
+
# }
|
|
1624
|
+
```
|
|
1625
|
+
|
|
1626
|
+
#### Per-Property Configuration
|
|
1627
|
+
|
|
1628
|
+
You can also enable `$ref` for specific properties using the `ref: true` constraint:
|
|
1629
|
+
|
|
1630
|
+
```ruby
|
|
1631
|
+
class Person
|
|
1632
|
+
include EasyTalk::Model
|
|
1633
|
+
define_schema do
|
|
1634
|
+
property :name, String
|
|
1635
|
+
property :address, Address, ref: true # Use $ref for this property
|
|
1636
|
+
end
|
|
1637
|
+
end
|
|
1638
|
+
```
|
|
1639
|
+
|
|
1640
|
+
Or disable `$ref` for specific properties when it's enabled globally:
|
|
1641
|
+
|
|
1642
|
+
```ruby
|
|
1643
|
+
EasyTalk.configure do |config|
|
|
1644
|
+
config.use_refs = true
|
|
1645
|
+
end
|
|
1646
|
+
|
|
1647
|
+
class Person
|
|
1648
|
+
include EasyTalk::Model
|
|
1649
|
+
define_schema do
|
|
1650
|
+
property :name, String
|
|
1651
|
+
property :address, Address, ref: false # Inline this property despite global setting
|
|
1652
|
+
end
|
|
1653
|
+
end
|
|
1654
|
+
```
|
|
1655
|
+
|
|
1656
|
+
### Arrays of Models
|
|
1657
|
+
|
|
1658
|
+
When using `$ref` with arrays of models, the `$ref` applies to the array items:
|
|
1659
|
+
|
|
1660
|
+
```ruby
|
|
1661
|
+
EasyTalk.configure do |config|
|
|
1662
|
+
config.use_refs = true
|
|
1663
|
+
end
|
|
1664
|
+
|
|
1665
|
+
class Company
|
|
1666
|
+
include EasyTalk::Model
|
|
1667
|
+
define_schema do
|
|
1668
|
+
property :name, String
|
|
1669
|
+
property :addresses, T::Array[Address]
|
|
1670
|
+
end
|
|
1671
|
+
end
|
|
1672
|
+
|
|
1673
|
+
Company.json_schema
|
|
1674
|
+
# => {
|
|
1675
|
+
# "type" => "object",
|
|
1676
|
+
# "properties" => {
|
|
1677
|
+
# "name" => { "type" => "string" },
|
|
1678
|
+
# "addresses" => {
|
|
1679
|
+
# "type" => "array",
|
|
1680
|
+
# "items" => { "$ref" => "#/$defs/Address" }
|
|
1681
|
+
# }
|
|
1682
|
+
# },
|
|
1683
|
+
# "$defs" => {
|
|
1684
|
+
# "Address" => { ... }
|
|
1685
|
+
# },
|
|
1686
|
+
# ...
|
|
1687
|
+
# }
|
|
1688
|
+
```
|
|
1689
|
+
|
|
1690
|
+
You can also use the per-property `ref` constraint with arrays:
|
|
1691
|
+
|
|
1692
|
+
```ruby
|
|
1693
|
+
property :addresses, T::Array[Address], ref: true
|
|
1694
|
+
```
|
|
1695
|
+
|
|
1696
|
+
### Nilable Models with `$ref`
|
|
1697
|
+
|
|
1698
|
+
When using `$ref` with nilable model types, EasyTalk uses `anyOf` to combine the reference with the null type:
|
|
1699
|
+
|
|
1700
|
+
```ruby
|
|
1701
|
+
EasyTalk.configure do |config|
|
|
1702
|
+
config.use_refs = true
|
|
1703
|
+
end
|
|
1704
|
+
|
|
1705
|
+
class Person
|
|
1706
|
+
include EasyTalk::Model
|
|
1707
|
+
define_schema do
|
|
1708
|
+
property :name, String
|
|
1709
|
+
property :address, T.nilable(Address)
|
|
1710
|
+
end
|
|
1711
|
+
end
|
|
1712
|
+
|
|
1713
|
+
Person.json_schema
|
|
1714
|
+
# => {
|
|
1715
|
+
# "type" => "object",
|
|
1716
|
+
# "properties" => {
|
|
1717
|
+
# "name" => { "type" => "string" },
|
|
1718
|
+
# "address" => {
|
|
1719
|
+
# "anyOf" => [
|
|
1720
|
+
# { "$ref" => "#/$defs/Address" },
|
|
1721
|
+
# { "type" => "null" }
|
|
1722
|
+
# ]
|
|
1723
|
+
# }
|
|
1724
|
+
# },
|
|
1725
|
+
# "$defs" => {
|
|
1726
|
+
# "Address" => { ... }
|
|
1727
|
+
# },
|
|
1728
|
+
# ...
|
|
1729
|
+
# }
|
|
1730
|
+
```
|
|
1731
|
+
|
|
1732
|
+
### Multiple References to the Same Model
|
|
1733
|
+
|
|
1734
|
+
When the same model is used multiple times, it only appears once in `$defs`:
|
|
1735
|
+
|
|
1736
|
+
```ruby
|
|
1737
|
+
class Person
|
|
1738
|
+
include EasyTalk::Model
|
|
1739
|
+
define_schema do
|
|
1740
|
+
property :name, String
|
|
1741
|
+
property :home_address, Address, ref: true
|
|
1742
|
+
property :work_address, Address, ref: true
|
|
1743
|
+
property :shipping_addresses, T::Array[Address], ref: true
|
|
1744
|
+
end
|
|
1745
|
+
end
|
|
1746
|
+
|
|
1747
|
+
Person.json_schema
|
|
1748
|
+
# => {
|
|
1749
|
+
# "type" => "object",
|
|
1750
|
+
# "properties" => {
|
|
1751
|
+
# "name" => { "type" => "string" },
|
|
1752
|
+
# "home_address" => { "$ref" => "#/$defs/Address" },
|
|
1753
|
+
# "work_address" => { "$ref" => "#/$defs/Address" },
|
|
1754
|
+
# "shipping_addresses" => {
|
|
1755
|
+
# "type" => "array",
|
|
1756
|
+
# "items" => { "$ref" => "#/$defs/Address" }
|
|
1757
|
+
# }
|
|
1758
|
+
# },
|
|
1759
|
+
# "$defs" => {
|
|
1760
|
+
# "Address" => { ... } # Only defined once
|
|
1761
|
+
# },
|
|
1762
|
+
# ...
|
|
1763
|
+
# }
|
|
1764
|
+
```
|
|
1765
|
+
|
|
1766
|
+
### Combining `$ref` with Other Constraints
|
|
1767
|
+
|
|
1768
|
+
You can add additional constraints alongside `$ref`:
|
|
1769
|
+
|
|
1770
|
+
```ruby
|
|
1771
|
+
class Person
|
|
1772
|
+
include EasyTalk::Model
|
|
1773
|
+
define_schema do
|
|
1774
|
+
property :address, Address, ref: true, description: "Primary address", title: "Main Address"
|
|
1775
|
+
end
|
|
1776
|
+
end
|
|
1777
|
+
|
|
1778
|
+
Person.json_schema["properties"]["address"]
|
|
1779
|
+
# => {
|
|
1780
|
+
# "$ref" => "#/$defs/Address",
|
|
1781
|
+
# "description" => "Primary address",
|
|
1782
|
+
# "title" => "Main Address"
|
|
1783
|
+
# }
|
|
1784
|
+
```
|
|
1785
|
+
|
|
1786
|
+
### Interaction with `compose`
|
|
1787
|
+
|
|
1788
|
+
When using `compose` with `T::AllOf`, `T::AnyOf`, or `T::OneOf`, the composed models are also placed in `$defs`:
|
|
1789
|
+
|
|
1790
|
+
```ruby
|
|
1791
|
+
class Employee
|
|
1792
|
+
include EasyTalk::Model
|
|
1793
|
+
define_schema do
|
|
1794
|
+
compose T::AllOf[Person, EmployeeDetails]
|
|
1795
|
+
property :badge_number, String
|
|
1796
|
+
end
|
|
1797
|
+
end
|
|
1798
|
+
```
|
|
1799
|
+
|
|
1800
|
+
If you also have properties using `$ref`, both the composed models and property models will appear in `$defs`.
|
|
1801
|
+
|
|
1802
|
+
### Best Practices
|
|
1803
|
+
|
|
1804
|
+
1. **Use global configuration for consistency**: If you prefer `$ref` style, enable it globally rather than per-property.
|
|
1805
|
+
|
|
1806
|
+
2. **Consider schema consumers**: Some JSON Schema validators and tools work better with inlined schemas, while others prefer `$ref`. Choose based on your use case.
|
|
1807
|
+
|
|
1808
|
+
3. **Use `$ref` for frequently reused models**: If a model appears in many places, `$ref` reduces schema size and improves maintainability.
|
|
1809
|
+
|
|
1810
|
+
4. **Keep inline for simple, single-use models**: For models used only once, inlining may be more readable.
|
|
1811
|
+
|
|
1812
|
+
### Default Behavior
|
|
1813
|
+
|
|
1814
|
+
By default, `use_refs` is set to `false`, meaning nested models are inlined. This maintains backward compatibility with previous versions of EasyTalk.
|
|
1815
|
+
|
|
1171
1816
|
## JSON Schema Compatibility
|
|
1172
1817
|
|
|
1173
1818
|
### Supported Versions
|
|
1174
|
-
EasyTalk
|
|
1819
|
+
EasyTalk supports generating schemas compatible with JSON Schema Draft-04 through Draft 2020-12. Use the `schema_version` configuration option to declare which version your schemas conform to (see [JSON Schema Version](#json-schema-version-schema-keyword) above).
|
|
1820
|
+
|
|
1821
|
+
While EasyTalk allows you to specify any draft version via the `$schema` keyword, the generated schema structure is generally compatible across versions. Some newer draft features may require manual adjustment.
|
|
1175
1822
|
|
|
1176
1823
|
### Specification Compliance
|
|
1177
1824
|
To learn about current capabilities, see the [spec/easy_talk/examples](https://github.com/sergiobayona/easy_talk/tree/main/spec/easy_talk/examples) folder. The examples illustrate how EasyTalk generates JSON Schema in different scenarios.
|
|
1178
1825
|
|
|
1179
1826
|
### Known Limitations
|
|
1180
1827
|
- Limited support for custom formats
|
|
1181
|
-
-
|
|
1828
|
+
- Some draft-specific keywords may not be supported
|
|
1182
1829
|
- Complex composition scenarios may require manual adjustment
|
|
1183
1830
|
|
|
1184
1831
|
## License
|
|
@@ -39,6 +39,9 @@ module EasyTalk
|
|
|
39
39
|
# We'll collect required property names in this Set
|
|
40
40
|
@required_properties = Set.new
|
|
41
41
|
|
|
42
|
+
# Collect models that are referenced via $ref for $defs generation
|
|
43
|
+
@ref_models = Set.new
|
|
44
|
+
|
|
42
45
|
# Usually the name is a string (class name). Fallback to :klass if nil.
|
|
43
46
|
name_for_builder = schema_definition.name ? schema_definition.name.to_sym : :klass
|
|
44
47
|
|
|
@@ -60,12 +63,20 @@ module EasyTalk
|
|
|
60
63
|
# Start with a copy of the raw schema
|
|
61
64
|
merged = @original_schema.dup
|
|
62
65
|
|
|
66
|
+
# Remove schema_version and schema_id as they're handled separately in json_schema output
|
|
67
|
+
merged.delete(:schema_version)
|
|
68
|
+
merged.delete(:schema_id)
|
|
69
|
+
|
|
63
70
|
# Extract and build sub-schemas first (handles allOf/anyOf/oneOf references, etc.)
|
|
64
71
|
process_subschemas(merged)
|
|
65
72
|
|
|
66
73
|
# Build :properties into a final form (and find "required" props)
|
|
74
|
+
# This also collects models that use $ref into @ref_models
|
|
67
75
|
merged[:properties] = build_properties(merged.delete(:properties))
|
|
68
76
|
|
|
77
|
+
# Add $defs for any models that are referenced via $ref
|
|
78
|
+
add_ref_model_defs(merged) if @ref_models.any?
|
|
79
|
+
|
|
69
80
|
# Populate the final "required" array from @required_properties
|
|
70
81
|
merged[:required] = @required_properties.to_a if @required_properties.any?
|
|
71
82
|
|
|
@@ -127,6 +138,7 @@ module EasyTalk
|
|
|
127
138
|
##
|
|
128
139
|
# Builds a single property. Could be a nested schema if it has sub-properties,
|
|
129
140
|
# or a standard scalar property (String, Integer, etc.).
|
|
141
|
+
# Also tracks EasyTalk models that should be added to $defs when using $ref.
|
|
130
142
|
#
|
|
131
143
|
def build_property(prop_name, prop_options)
|
|
132
144
|
@property_cache ||= {}
|
|
@@ -135,9 +147,91 @@ module EasyTalk
|
|
|
135
147
|
@property_cache[prop_name] ||= begin
|
|
136
148
|
# Remove optional constraints from the property
|
|
137
149
|
constraints = prop_options[:constraints].except(:optional)
|
|
150
|
+
prop_type = prop_options[:type]
|
|
151
|
+
|
|
152
|
+
# Track models that will use $ref for later $defs generation
|
|
153
|
+
collect_ref_models(prop_type, constraints)
|
|
154
|
+
|
|
138
155
|
# Normal property: e.g. { type: String, constraints: {...} }
|
|
139
|
-
Property.new(prop_name,
|
|
156
|
+
Property.new(prop_name, prop_type, constraints)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
##
|
|
161
|
+
# Collects EasyTalk models that will be referenced via $ref.
|
|
162
|
+
# These models need to be added to $defs in the final schema.
|
|
163
|
+
#
|
|
164
|
+
def collect_ref_models(prop_type, constraints)
|
|
165
|
+
# Check if this type should use $ref
|
|
166
|
+
if should_collect_ref?(prop_type, constraints)
|
|
167
|
+
@ref_models.add(prop_type)
|
|
168
|
+
# Handle typed arrays with EasyTalk model items
|
|
169
|
+
elsif typed_array_with_model?(prop_type)
|
|
170
|
+
inner_type = prop_type.type.raw_type
|
|
171
|
+
@ref_models.add(inner_type) if should_collect_ref?(inner_type, constraints)
|
|
172
|
+
# Handle nilable types
|
|
173
|
+
elsif nilable_with_model?(prop_type)
|
|
174
|
+
actual_type = T::Utils::Nilable.get_underlying_type(prop_type)
|
|
175
|
+
@ref_models.add(actual_type) if should_collect_ref?(actual_type, constraints)
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
##
|
|
180
|
+
# Determines if a type should be collected for $ref based on config and constraints.
|
|
181
|
+
#
|
|
182
|
+
def should_collect_ref?(check_type, constraints)
|
|
183
|
+
return false unless easytalk_model?(check_type)
|
|
184
|
+
|
|
185
|
+
# Per-property constraint takes precedence
|
|
186
|
+
return constraints[:ref] if constraints.key?(:ref)
|
|
187
|
+
|
|
188
|
+
# Fall back to global configuration
|
|
189
|
+
EasyTalk.configuration.use_refs
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
##
|
|
193
|
+
# Checks if a type is an EasyTalk model.
|
|
194
|
+
#
|
|
195
|
+
def easytalk_model?(check_type)
|
|
196
|
+
check_type.is_a?(Class) &&
|
|
197
|
+
check_type.respond_to?(:schema) &&
|
|
198
|
+
check_type.respond_to?(:ref_template) &&
|
|
199
|
+
defined?(EasyTalk::Model) &&
|
|
200
|
+
check_type.include?(EasyTalk::Model)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
##
|
|
204
|
+
# Checks if type is a typed array containing an EasyTalk model.
|
|
205
|
+
#
|
|
206
|
+
def typed_array_with_model?(prop_type)
|
|
207
|
+
return false unless prop_type.is_a?(T::Types::TypedArray)
|
|
208
|
+
|
|
209
|
+
inner_type = prop_type.type.raw_type
|
|
210
|
+
easytalk_model?(inner_type)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
##
|
|
214
|
+
# Checks if type is nilable and contains an EasyTalk model.
|
|
215
|
+
#
|
|
216
|
+
def nilable_with_model?(prop_type)
|
|
217
|
+
return false unless prop_type.respond_to?(:types)
|
|
218
|
+
return false unless prop_type.types.all? { |t| t.respond_to?(:raw_type) }
|
|
219
|
+
return false unless prop_type.types.any? { |t| t.raw_type == NilClass }
|
|
220
|
+
|
|
221
|
+
actual_type = T::Utils::Nilable.get_underlying_type(prop_type)
|
|
222
|
+
easytalk_model?(actual_type)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
##
|
|
226
|
+
# Adds $defs entries for all collected ref models.
|
|
227
|
+
#
|
|
228
|
+
def add_ref_model_defs(schema_hash)
|
|
229
|
+
definitions = @ref_models.each_with_object({}) do |model, acc|
|
|
230
|
+
acc[model.name] = model.schema
|
|
140
231
|
end
|
|
232
|
+
|
|
233
|
+
existing_defs = schema_hash[:defs] || {}
|
|
234
|
+
schema_hash[:defs] = existing_defs.merge(definitions)
|
|
141
235
|
end
|
|
142
236
|
|
|
143
237
|
##
|
|
@@ -14,7 +14,8 @@ module EasyTalk
|
|
|
14
14
|
max_items: { type: Integer, key: :maxItems },
|
|
15
15
|
unique_items: { type: T::Boolean, key: :uniqueItems },
|
|
16
16
|
enum: { type: T::Array[T.untyped], key: :enum },
|
|
17
|
-
const: { type: T::Array[T.untyped], key: :const }
|
|
17
|
+
const: { type: T::Array[T.untyped], key: :const },
|
|
18
|
+
ref: { type: T::Boolean, key: :ref }
|
|
18
19
|
}.freeze
|
|
19
20
|
|
|
20
21
|
attr_reader :type
|
|
@@ -35,7 +36,9 @@ module EasyTalk
|
|
|
35
36
|
sig { returns(T::Hash[Symbol, T.untyped]) }
|
|
36
37
|
def schema
|
|
37
38
|
super.tap do |schema|
|
|
38
|
-
|
|
39
|
+
# Pass ref constraint to items if present (for nested model references)
|
|
40
|
+
item_constraints = @options&.slice(:ref) || {}
|
|
41
|
+
schema[:items] = Property.new(@name, inner_type, item_constraints).build
|
|
39
42
|
end
|
|
40
43
|
end
|
|
41
44
|
|
|
@@ -2,12 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
module EasyTalk
|
|
4
4
|
class Configuration
|
|
5
|
-
|
|
5
|
+
# JSON Schema draft version URIs
|
|
6
|
+
SCHEMA_VERSIONS = {
|
|
7
|
+
draft202012: 'https://json-schema.org/draft/2020-12/schema',
|
|
8
|
+
draft201909: 'https://json-schema.org/draft/2019-09/schema',
|
|
9
|
+
draft7: 'http://json-schema.org/draft-07/schema#',
|
|
10
|
+
draft6: 'http://json-schema.org/draft-06/schema#',
|
|
11
|
+
draft4: 'http://json-schema.org/draft-04/schema#'
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
attr_accessor :default_additional_properties, :nilable_is_optional, :auto_validations, :schema_version, :schema_id,
|
|
15
|
+
:use_refs
|
|
6
16
|
|
|
7
17
|
def initialize
|
|
8
18
|
@default_additional_properties = false
|
|
9
19
|
@nilable_is_optional = false
|
|
10
20
|
@auto_validations = true
|
|
21
|
+
@schema_version = :none
|
|
22
|
+
@schema_id = nil
|
|
23
|
+
@use_refs = false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Returns the URI for the configured schema version, or nil if :none
|
|
27
|
+
def schema_uri
|
|
28
|
+
return nil if @schema_version == :none
|
|
29
|
+
|
|
30
|
+
SCHEMA_VERSIONS[@schema_version] || @schema_version.to_s
|
|
11
31
|
end
|
|
12
32
|
end
|
|
13
33
|
|
data/lib/easy_talk/keywords.rb
CHANGED
data/lib/easy_talk/model.rb
CHANGED
|
@@ -155,12 +155,63 @@ module EasyTalk
|
|
|
155
155
|
end
|
|
156
156
|
|
|
157
157
|
# Returns the JSON schema for the model.
|
|
158
|
+
# This is the final output that includes the $schema keyword if configured.
|
|
158
159
|
#
|
|
159
160
|
# @return [Hash] The JSON schema for the model.
|
|
160
161
|
def json_schema
|
|
161
|
-
@json_schema ||=
|
|
162
|
+
@json_schema ||= build_json_schema
|
|
162
163
|
end
|
|
163
164
|
|
|
165
|
+
private
|
|
166
|
+
|
|
167
|
+
# Builds the final JSON schema with optional $schema and $id keywords.
|
|
168
|
+
def build_json_schema
|
|
169
|
+
result = schema.as_json
|
|
170
|
+
schema_uri = resolve_schema_uri
|
|
171
|
+
id_uri = resolve_schema_id
|
|
172
|
+
|
|
173
|
+
# Build prefix hash with $schema and $id (in that order per JSON Schema convention)
|
|
174
|
+
prefix = {}
|
|
175
|
+
prefix['$schema'] = schema_uri if schema_uri
|
|
176
|
+
prefix['$id'] = id_uri if id_uri
|
|
177
|
+
|
|
178
|
+
return result if prefix.empty?
|
|
179
|
+
|
|
180
|
+
prefix.merge(result)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Resolves the schema URI from per-model setting or global config.
|
|
184
|
+
def resolve_schema_uri
|
|
185
|
+
model_version = @schema_definition&.schema&.dig(:schema_version)
|
|
186
|
+
|
|
187
|
+
if model_version
|
|
188
|
+
# Per-model override - :none means explicitly no $schema
|
|
189
|
+
return nil if model_version == :none
|
|
190
|
+
|
|
191
|
+
Configuration::SCHEMA_VERSIONS[model_version] || model_version.to_s
|
|
192
|
+
else
|
|
193
|
+
# Fall back to global configuration
|
|
194
|
+
EasyTalk.configuration.schema_uri
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Resolves the schema ID from per-model setting or global config.
|
|
199
|
+
def resolve_schema_id
|
|
200
|
+
model_id = @schema_definition&.schema&.dig(:schema_id)
|
|
201
|
+
|
|
202
|
+
if model_id
|
|
203
|
+
# Per-model override - :none means explicitly no $id
|
|
204
|
+
return nil if model_id == :none
|
|
205
|
+
|
|
206
|
+
model_id.to_s
|
|
207
|
+
else
|
|
208
|
+
# Fall back to global configuration
|
|
209
|
+
EasyTalk.configuration.schema_id
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
public
|
|
214
|
+
|
|
164
215
|
# Define the schema for the model using the provided block.
|
|
165
216
|
#
|
|
166
217
|
# @yield The block to define the schema.
|
data/lib/easy_talk/property.rb
CHANGED
|
@@ -101,22 +101,31 @@ module EasyTalk
|
|
|
101
101
|
# This method handles different types of properties:
|
|
102
102
|
# - Nilable types (can be null)
|
|
103
103
|
# - Types with dedicated builders
|
|
104
|
-
# - Types that implement their own schema method
|
|
104
|
+
# - Types that implement their own schema method (EasyTalk models)
|
|
105
105
|
# - Default fallback to 'object' type
|
|
106
106
|
#
|
|
107
|
+
# When use_refs is enabled (globally or per-property), EasyTalk models
|
|
108
|
+
# are referenced via $ref instead of being inlined.
|
|
109
|
+
#
|
|
107
110
|
# @return [Hash] The complete JSON Schema property definition
|
|
108
111
|
#
|
|
109
112
|
# @example Simple string property
|
|
110
113
|
# property = Property.new(:name, 'String')
|
|
111
114
|
# property.build # => {"type"=>"string"}
|
|
112
115
|
#
|
|
113
|
-
# @example Complex nested schema
|
|
116
|
+
# @example Complex nested schema (inlined)
|
|
114
117
|
# address = Address.new # A class with a .schema method
|
|
115
118
|
# property = Property.new(:shipping_address, address, description: "Shipping address")
|
|
116
119
|
# property.build # => Address schema merged with the description constraint
|
|
120
|
+
#
|
|
121
|
+
# @example Nested schema with $ref
|
|
122
|
+
# property = Property.new(:shipping_address, Address, ref: true)
|
|
123
|
+
# property.build # => {"$ref"=>"#/$defs/Address", ...constraints}
|
|
117
124
|
def build
|
|
118
125
|
if nilable_type?
|
|
119
126
|
build_nilable_schema
|
|
127
|
+
elsif should_use_ref?
|
|
128
|
+
build_ref_schema
|
|
120
129
|
elsif builder
|
|
121
130
|
args = builder.collection_type? ? [name, type, constraints] : [name, constraints]
|
|
122
131
|
builder.new(*args).build
|
|
@@ -193,6 +202,14 @@ module EasyTalk
|
|
|
193
202
|
|
|
194
203
|
return { type: 'null' } unless actual_type
|
|
195
204
|
|
|
205
|
+
# Check if the underlying type is an EasyTalk model that should use $ref
|
|
206
|
+
if easytalk_model?(actual_type) && should_use_ref_for_type?(actual_type)
|
|
207
|
+
# Use anyOf with $ref and null type
|
|
208
|
+
ref_constraints = constraints.except(:ref, :optional)
|
|
209
|
+
schema = { anyOf: [{ '$ref': actual_type.ref_template }, { type: 'null' }] }
|
|
210
|
+
return ref_constraints.empty? ? schema : schema.merge(ref_constraints)
|
|
211
|
+
end
|
|
212
|
+
|
|
196
213
|
# Create a property with the actual type
|
|
197
214
|
non_nil_schema = Property.new(name, actual_type, constraints).build
|
|
198
215
|
|
|
@@ -201,5 +218,54 @@ module EasyTalk
|
|
|
201
218
|
type: [non_nil_schema[:type], 'null'].compact
|
|
202
219
|
)
|
|
203
220
|
end
|
|
221
|
+
|
|
222
|
+
# Determines if $ref should be used for the current type.
|
|
223
|
+
#
|
|
224
|
+
# @return [Boolean] true if $ref should be used, false otherwise
|
|
225
|
+
# @api private
|
|
226
|
+
def should_use_ref?
|
|
227
|
+
return false unless easytalk_model?(type)
|
|
228
|
+
|
|
229
|
+
should_use_ref_for_type?(type)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Determines if $ref should be used for a given type based on constraints and config.
|
|
233
|
+
#
|
|
234
|
+
# @param check_type [Class] The type to check
|
|
235
|
+
# @return [Boolean] true if $ref should be used, false otherwise
|
|
236
|
+
# @api private
|
|
237
|
+
def should_use_ref_for_type?(check_type)
|
|
238
|
+
return false unless easytalk_model?(check_type)
|
|
239
|
+
|
|
240
|
+
# Per-property constraint takes precedence
|
|
241
|
+
return constraints[:ref] if constraints.key?(:ref)
|
|
242
|
+
|
|
243
|
+
# Fall back to global configuration
|
|
244
|
+
EasyTalk.configuration.use_refs
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Checks if a type is an EasyTalk model.
|
|
248
|
+
#
|
|
249
|
+
# @param check_type [Object] The type to check
|
|
250
|
+
# @return [Boolean] true if the type is an EasyTalk model
|
|
251
|
+
# @api private
|
|
252
|
+
def easytalk_model?(check_type)
|
|
253
|
+
check_type.is_a?(Class) &&
|
|
254
|
+
check_type.respond_to?(:schema) &&
|
|
255
|
+
check_type.respond_to?(:ref_template) &&
|
|
256
|
+
defined?(EasyTalk::Model) &&
|
|
257
|
+
check_type.include?(EasyTalk::Model)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Builds a $ref schema for an EasyTalk model.
|
|
261
|
+
#
|
|
262
|
+
# @return [Hash] A schema with $ref pointing to the model's definition
|
|
263
|
+
# @api private
|
|
264
|
+
def build_ref_schema
|
|
265
|
+
# Remove ref and optional from constraints as they're not JSON Schema keywords
|
|
266
|
+
ref_constraints = constraints.except(:ref, :optional)
|
|
267
|
+
schema = { '$ref': type.ref_template }
|
|
268
|
+
ref_constraints.empty? ? schema : schema.merge(ref_constraints)
|
|
269
|
+
end
|
|
204
270
|
end
|
|
205
271
|
end
|
data/lib/easy_talk/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: easy_talk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sergio Bayona
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: activemodel
|
|
@@ -147,7 +147,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
147
147
|
- !ruby/object:Gem::Version
|
|
148
148
|
version: '0'
|
|
149
149
|
requirements: []
|
|
150
|
-
rubygems_version: 3.
|
|
150
|
+
rubygems_version: 3.7.2
|
|
151
151
|
specification_version: 4
|
|
152
152
|
summary: Generate json-schema from Ruby classes with ActiveModel integration.
|
|
153
153
|
test_files: []
|