diff --git a/README.md b/README.md index 3c344d9..ef87580 100644 --- a/README.md +++ b/README.md @@ -50,14 +50,15 @@ Note: **PyPI package names use hyphens** (`datasworn-community-classic`) but **P ## Status -Functional against Pydantic 2.13, all 10 packages tested. Two known codegen bugs from `datamodel-code-generator` are patched by `scripts/post_process_models.py` — run it after any `models.py` regeneration: +Functional against Pydantic 2.13, all 10 packages tested. Three codegen quirks from `datamodel-code-generator` are patched by `scripts/post_process_models.py` — run it after any `models.py` regeneration: 1. **String `pattern` constraint on `date` fields.** `SourceInfo.date` is typed as `datetime.date` but the schema's `pattern: "[0-9]{4}-…"` is emitted onto the `Field()`. Pydantic 2.13 rejects a string-only constraint on a non-string field. The post-processor strips just those patterns. -2. **Empty `Features` / `Dangers` / `Denizens` stubs.** Codegen emits `class Denizens(BaseModel): pass` and `class Features(BaseModel): pass`, then references them where the JSON actually contains a list of concrete items (`DelveSiteDenizen`, `DelveSiteDomainFeature`, `DelveSiteThemeFeature`, etc.). The post-processor rewrites the field types to `list[X]` and deletes the empty stubs. +2. **Empty `Features` / `Dangers` / `Denizens` stubs.** Codegen emits `class Denizens(BaseModel): pass` then references it where the JSON actually contains a list of concrete items. The post-processor rewrites the field types to `list[X]` and deletes the empty stubs. +3. **`RootModel[str]` wrappers on ID types.** The generator wraps every `` in a `RootModel[str]` class, forcing consumers to write `some_thing.id.root` instead of `some_thing.id`. The post-processor converts each to `TypeAlias = Annotated[str, Field(pattern=…)]`, so IDs read as plain strings while Pydantic still validates the pattern on load. Still outstanding (nice-to-have, not blocking): -- **`RootModel[str]` wrappers on ID types.** `.id` currently reads via `._id`; a separate post-process step in the upstream fork converted these to plain type aliases (`RulesetId: TypeAlias = str`). Not ported yet. +- **Additional discriminated-union bases without generated subtypes.** `MoveEnhancement`, `EmbeddedMove`, `AssetControlField`, `AssetOptionField`, `RulesPackage`, `Choices`, `RollableValue`, and ~5 others share the same "empty base with `extra='allow'`" codegen quirk, but their concrete subtype classes weren't generated at all — so a post-process pass can't just wire them into a union yet. Needs upstream codegen work or a bigger post-processor. ## Development diff --git a/packages/core/src/datasworn/core/models.py b/packages/core/src/datasworn/core/models.py index 319345d..814b426 100644 --- a/packages/core/src/datasworn/core/models.py +++ b/packages/core/src/datasworn/core/models.py @@ -6,7 +6,7 @@ from datetime import date as date_aliased from enum import Enum -from typing import Annotated, Any, Literal +from typing import Annotated, Any, Literal, TypeAlias, Union from pydantic import AnyUrl, BaseModel, ConfigDict, EmailStr, Field, RootModel @@ -35,92 +35,84 @@ class AssetAbilityControlField(BaseModel): field_type: FieldType -class AssetAbilityId(RootModel[str]): - root: Annotated[ - str, - Field( - description='A unique ID representing an AssetAbility object.', - pattern='^asset\\.ability:([a-z][a-z0-9_]*(?:\\/[a-z][a-z0-9_]*){2,5})\\.(\\d+)$', - title='AssetAbilityId', - ), - ] - - -class AssetAbilityIdWildcard(RootModel[str]): - root: Annotated[ - str, - Field( - description='A wildcarded AssetAbilityId that can be used to match multiple AssetAbility objects.', - pattern='^asset\\.ability:((?:[a-z][a-z0-9_]*|\\*)(?:\\/(?:[a-z][a-z0-9_]*|\\*|\\*\\*)){2,5})\\.(\\d+|\\*)$', - title='AssetAbilityIdWildcard', - ), - ] - - -class AssetAbilityMoveConditionId(RootModel[str]): - root: Annotated[ - str, - Field( - description='A unique ID representing an AssetAbilityMoveCondition object.', - pattern='^asset\\.ability\\.move\\.condition:([a-z][a-z0-9_]*(?:\\/[a-z][a-z0-9_]*){2,5})\\.(\\d+)\\.([a-z][a-z0-9_]*|\\*)\\.(\\d+)$', - title='AssetAbilityMoveConditionId', - ), - ] - - -class AssetAbilityMoveConditionIdWildcard(RootModel[str]): - root: Annotated[ - str, - Field( - description='A wildcarded AssetAbilityMoveConditionId that can be used to match multiple AssetAbilityMoveCondition objects.', - pattern='^asset\\.ability\\.move\\.condition:((?:[a-z][a-z0-9_]*|\\*)(?:\\/(?:[a-z][a-z0-9_]*|\\*|\\*\\*)){2,5})\\.(\\d+|\\*)\\.([a-z][a-z0-9_]*|\\*)\\.(\\d+|\\*)$', - title='AssetAbilityMoveConditionIdWildcard', - ), - ] - - -class AssetAbilityMoveId(RootModel[str]): - root: Annotated[ - str, - Field( - description='A unique ID representing an AssetAbilityMove object.', - pattern='^asset\\.ability\\.move:([a-z][a-z0-9_]*(?:\\/[a-z][a-z0-9_]*){2,5})\\.(\\d+)\\.([a-z][a-z0-9_]*|\\*)$', - title='AssetAbilityMoveId', - ), - ] - - -class AssetAbilityMoveIdWildcard(RootModel[str]): - root: Annotated[ - str, - Field( - description='A wildcarded AssetAbilityMoveId that can be used to match multiple AssetAbilityMove objects.', - pattern='^asset\\.ability\\.move:((?:[a-z][a-z0-9_]*|\\*)(?:\\/(?:[a-z][a-z0-9_]*|\\*|\\*\\*)){2,5})\\.(\\d+|\\*)\\.([a-z][a-z0-9_]*|\\*)$', - title='AssetAbilityMoveIdWildcard', - ), - ] - - -class AssetAbilityMoveOutcomeId(RootModel[str]): - root: Annotated[ - str, - Field( - description='A unique ID representing an AssetAbilityMoveOutcome object.', - pattern='^asset\\.ability\\.move\\.outcome:([a-z][a-z0-9_]*(?:\\/[a-z][a-z0-9_]*){2,5})\\.(\\d+)\\.([a-z][a-z0-9_]*|\\*)\\.([a-z][a-z0-9_]*|\\*)$', - title='AssetAbilityMoveOutcomeId', - ), - ] - - -class AssetAbilityMoveOutcomeIdWildcard(RootModel[str]): - root: Annotated[ - str, - Field( - description='A wildcarded AssetAbilityMoveOutcomeId that can be used to match multiple AssetAbilityMoveOutcome objects.', - pattern='^asset\\.ability\\.move\\.outcome:((?:[a-z][a-z0-9_]*|\\*)(?:\\/(?:[a-z][a-z0-9_]*|\\*|\\*\\*)){2,5})\\.(\\d+|\\*)\\.([a-z][a-z0-9_]*|\\*)\\.([a-z][a-z0-9_]*|\\*)$', - title='AssetAbilityMoveOutcomeIdWildcard', - ), - ] +AssetAbilityId: TypeAlias = Annotated[ + str, + Field( + description='A unique ID representing an AssetAbility object.', + pattern='^asset\\.ability:([a-z][a-z0-9_]*(?:\\/[a-z][a-z0-9_]*){2,5})\\.(\\d+)$', + title='AssetAbilityId', + ), +] + + +AssetAbilityIdWildcard: TypeAlias = Annotated[ + str, + Field( + description='A wildcarded AssetAbilityId that can be used to match multiple AssetAbility objects.', + pattern='^asset\\.ability:((?:[a-z][a-z0-9_]*|\\*)(?:\\/(?:[a-z][a-z0-9_]*|\\*|\\*\\*)){2,5})\\.(\\d+|\\*)$', + title='AssetAbilityIdWildcard', + ), +] + + +AssetAbilityMoveConditionId: TypeAlias = Annotated[ + str, + Field( + description='A unique ID representing an AssetAbilityMoveCondition object.', + pattern='^asset\\.ability\\.move\\.condition:([a-z][a-z0-9_]*(?:\\/[a-z][a-z0-9_]*){2,5})\\.(\\d+)\\.([a-z][a-z0-9_]*|\\*)\\.(\\d+)$', + title='AssetAbilityMoveConditionId', + ), +] + + +AssetAbilityMoveConditionIdWildcard: TypeAlias = Annotated[ + str, + Field( + description='A wildcarded AssetAbilityMoveConditionId that can be used to match multiple AssetAbilityMoveCondition objects.', + pattern='^asset\\.ability\\.move\\.condition:((?:[a-z][a-z0-9_]*|\\*)(?:\\/(?:[a-z][a-z0-9_]*|\\*|\\*\\*)){2,5})\\.(\\d+|\\*)\\.([a-z][a-z0-9_]*|\\*)\\.(\\d+|\\*)$', + title='AssetAbilityMoveConditionIdWildcard', + ), +] + + +AssetAbilityMoveId: TypeAlias = Annotated[ + str, + Field( + description='A unique ID representing an AssetAbilityMove object.', + pattern='^asset\\.ability\\.move:([a-z][a-z0-9_]*(?:\\/[a-z][a-z0-9_]*){2,5})\\.(\\d+)\\.([a-z][a-z0-9_]*|\\*)$', + title='AssetAbilityMoveId', + ), +] + + +AssetAbilityMoveIdWildcard: TypeAlias = Annotated[ + str, + Field( + description='A wildcarded AssetAbilityMoveId that can be used to match multiple AssetAbilityMove objects.', + pattern='^asset\\.ability\\.move:((?:[a-z][a-z0-9_]*|\\*)(?:\\/(?:[a-z][a-z0-9_]*|\\*|\\*\\*)){2,5})\\.(\\d+|\\*)\\.([a-z][a-z0-9_]*|\\*)$', + title='AssetAbilityMoveIdWildcard', + ), +] + + +AssetAbilityMoveOutcomeId: TypeAlias = Annotated[ + str, + Field( + description='A unique ID representing an AssetAbilityMoveOutcome object.', + pattern='^asset\\.ability\\.move\\.outcome:([a-z][a-z0-9_]*(?:\\/[a-z][a-z0-9_]*){2,5})\\.(\\d+)\\.([a-z][a-z0-9_]*|\\*)\\.([a-z][a-z0-9_]*|\\*)$', + title='AssetAbilityMoveOutcomeId', + ), +] + + +AssetAbilityMoveOutcomeIdWildcard: TypeAlias = Annotated[ + str, + Field( + description='A wildcarded AssetAbilityMoveOutcomeId that can be used to match multiple AssetAbilityMoveOutcome objects.', + pattern='^asset\\.ability\\.move\\.outcome:((?:[a-z][a-z0-9_]*|\\*)(?:\\/(?:[a-z][a-z0-9_]*|\\*|\\*\\*)){2,5})\\.(\\d+|\\*)\\.([a-z][a-z0-9_]*|\\*)\\.([a-z][a-z0-9_]*|\\*)$', + title='AssetAbilityMoveOutcomeIdWildcard', + ), +] class FieldType1(Enum): @@ -134,48 +126,44 @@ class AssetAbilityOptionField(BaseModel): field_type: FieldType1 -class AssetAbilityOracleRollableId(RootModel[str]): - root: Annotated[ - str, - Field( - description='A unique ID representing an AssetAbilityOracleRollable object.', - pattern='^asset\\.ability\\.oracle_rollable:([a-z][a-z0-9_]*(?:\\/[a-z][a-z0-9_]*){2,5})\\.(\\d+)\\.([a-z][a-z0-9_]*|\\*)$', - title='AssetAbilityOracleRollableId', - ), - ] +AssetAbilityOracleRollableId: TypeAlias = Annotated[ + str, + Field( + description='A unique ID representing an AssetAbilityOracleRollable object.', + pattern='^asset\\.ability\\.oracle_rollable:([a-z][a-z0-9_]*(?:\\/[a-z][a-z0-9_]*){2,5})\\.(\\d+)\\.([a-z][a-z0-9_]*|\\*)$', + title='AssetAbilityOracleRollableId', + ), +] -class AssetAbilityOracleRollableIdWildcard(RootModel[str]): - root: Annotated[ - str, - Field( - description='A wildcarded AssetAbilityOracleRollableId that can be used to match multiple AssetAbilityOracleRollable objects.', - pattern='^asset\\.ability\\.oracle_rollable:((?:[a-z][a-z0-9_]*|\\*)(?:\\/(?:[a-z][a-z0-9_]*|\\*|\\*\\*)){2,5})\\.(\\d+|\\*)\\.([a-z][a-z0-9_]*|\\*)$', - title='AssetAbilityOracleRollableIdWildcard', - ), - ] +AssetAbilityOracleRollableIdWildcard: TypeAlias = Annotated[ + str, + Field( + description='A wildcarded AssetAbilityOracleRollableId that can be used to match multiple AssetAbilityOracleRollable objects.', + pattern='^asset\\.ability\\.oracle_rollable:((?:[a-z][a-z0-9_]*|\\*)(?:\\/(?:[a-z][a-z0-9_]*|\\*|\\*\\*)){2,5})\\.(\\d+|\\*)\\.([a-z][a-z0-9_]*|\\*)$', + title='AssetAbilityOracleRollableIdWildcard', + ), +] -class AssetAbilityOracleRollableRowId(RootModel[str]): - root: Annotated[ - str, - Field( - description='A unique ID representing an AssetAbilityOracleRollableRow object.', - pattern='^asset\\.ability\\.oracle_rollable\\.row:([a-z][a-z0-9_]*(?:\\/[a-z][a-z0-9_]*){2,5})\\.(\\d+)\\.([a-z][a-z0-9_]*|\\*)\\.(\\d+)$', - title='AssetAbilityOracleRollableRowId', - ), - ] +AssetAbilityOracleRollableRowId: TypeAlias = Annotated[ + str, + Field( + description='A unique ID representing an AssetAbilityOracleRollableRow object.', + pattern='^asset\\.ability\\.oracle_rollable\\.row:([a-z][a-z0-9_]*(?:\\/[a-z][a-z0-9_]*){2,5})\\.(\\d+)\\.([a-z][a-z0-9_]*|\\*)\\.(\\d+)$', + title='AssetAbilityOracleRollableRowId', + ), +] -class AssetAbilityOracleRollableRowIdWildcard(RootModel[str]): - root: Annotated[ - str, - Field( - description='A wildcarded AssetAbilityOracleRollableRowId that can be used to match multiple AssetAbilityOracleRollableRow objects.', - pattern='^asset\\.ability\\.oracle_rollable\\.row:((?:[a-z][a-z0-9_]*|\\*)(?:\\/(?:[a-z][a-z0-9_]*|\\*|\\*\\*)){2,5})\\.(\\d+|\\*)\\.([a-z][a-z0-9_]*|\\*)\\.(\\d+|\\*)$', - title='AssetAbilityOracleRollableRowIdWildcard', - ), - ] +AssetAbilityOracleRollableRowIdWildcard: TypeAlias = Annotated[ + str, + Field( + description='A wildcarded AssetAbilityOracleRollableRowId that can be used to match multiple AssetAbilityOracleRollableRow objects.', + pattern='^asset\\.ability\\.oracle_rollable\\.row:((?:[a-z][a-z0-9_]*|\\*)(?:\\/(?:[a-z][a-z0-9_]*|\\*|\\*\\*)){2,5})\\.(\\d+|\\*)\\.([a-z][a-z0-9_]*|\\*)\\.(\\d+|\\*)$', + title='AssetAbilityOracleRollableRowIdWildcard', + ), +] class Max(RootModel[int]): @@ -188,26 +176,24 @@ class Max(RootModel[int]): ] = None -class AssetCollectionId(RootModel[str]): - root: Annotated[ - str, - Field( - description='A unique ID representing an AssetCollection object.', - pattern='^asset_collection:([a-z][a-z0-9_]*(?:\\/[a-z][a-z0-9_]*){1,4})$', - title='AssetCollectionId', - ), - ] +AssetCollectionId: TypeAlias = Annotated[ + str, + Field( + description='A unique ID representing an AssetCollection object.', + pattern='^asset_collection:([a-z][a-z0-9_]*(?:\\/[a-z][a-z0-9_]*){1,4})$', + title='AssetCollectionId', + ), +] -class AssetCollectionIdWildcard(RootModel[str]): - root: Annotated[ - str, - Field( - description='A wildcarded AssetCollectionId that can be used to match multiple AssetCollection objects.', - pattern='^asset_collection:((?:[a-z][a-z0-9_]*|\\*)(?:\\/(?:[a-z][a-z0-9_]*|\\*|\\*\\*)){1,4})$', - title='AssetCollectionIdWildcard', - ), - ] +AssetCollectionIdWildcard: TypeAlias = Annotated[ + str, + Field( + description='A wildcarded AssetCollectionId that can be used to match multiple AssetCollection objects.', + pattern='^asset_collection:((?:[a-z][a-z0-9_]*|\\*)(?:\\/(?:[a-z][a-z0-9_]*|\\*|\\*\\*)){1,4})$', + title='AssetCollectionIdWildcard', + ), +] class FieldType2(Enum): @@ -252,26 +238,24 @@ class AssetControlFieldEnhancement(BaseModel): field_type: FieldType4 -class AssetId(RootModel[str]): - root: Annotated[ - str, - Field( - description='A unique ID representing an Asset object.', - pattern='^asset:([a-z][a-z0-9_]*(?:\\/[a-z][a-z0-9_]*){2,5})$', - title='AssetId', - ), - ] +AssetId: TypeAlias = Annotated[ + str, + Field( + description='A unique ID representing an Asset object.', + pattern='^asset:([a-z][a-z0-9_]*(?:\\/[a-z][a-z0-9_]*){2,5})$', + title='AssetId', + ), +] -class AssetIdWildcard(RootModel[str]): - root: Annotated[ - str, - Field( - description='A wildcarded AssetId that can be used to match multiple Asset objects.', - pattern='^asset:((?:[a-z][a-z0-9_]*|\\*)(?:\\/(?:[a-z][a-z0-9_]*|\\*|\\*\\*)){2,5})$', - title='AssetIdWildcard', - ), - ] +AssetIdWildcard: TypeAlias = Annotated[ + str, + Field( + description='A wildcarded AssetId that can be used to match multiple Asset objects.', + pattern='^asset:((?:[a-z][a-z0-9_]*|\\*)(?:\\/(?:[a-z][a-z0-9_]*|\\*|\\*\\*)){2,5})$', + title='AssetIdWildcard', + ), +] class FieldType5(Enum): @@ -287,48 +271,44 @@ class AssetOptionField(BaseModel): field_type: FieldType5 -class AtlasCollectionId(RootModel[str]): - root: Annotated[ - str, - Field( - description='A unique ID representing an AtlasCollection object.', - pattern='^atlas_collection:([a-z][a-z0-9_]*(?:\\/[a-z][a-z0-9_]*){1,4})$', - title='AtlasCollectionId', - ), - ] +AtlasCollectionId: TypeAlias = Annotated[ + str, + Field( + description='A unique ID representing an AtlasCollection object.', + pattern='^atlas_collection:([a-z][a-z0-9_]*(?:\\/[a-z][a-z0-9_]*){1,4})$', + title='AtlasCollectionId', + ), +] -class AtlasCollectionIdWildcard(RootModel[str]): - root: Annotated[ - str, - Field( - description='A wildcarded AtlasCollectionId that can be used to match multiple AtlasCollection objects.', - pattern='^atlas_collection:((?:[a-z][a-z0-9_]*|\\*)(?:\\/(?:[a-z][a-z0-9_]*|\\*|\\*\\*)){1,4})$', - title='AtlasCollectionIdWildcard', - ), - ] +AtlasCollectionIdWildcard: TypeAlias = Annotated[ + str, + Field( + description='A wildcarded AtlasCollectionId that can be used to match multiple AtlasCollection objects.', + pattern='^atlas_collection:((?:[a-z][a-z0-9_]*|\\*)(?:\\/(?:[a-z][a-z0-9_]*|\\*|\\*\\*)){1,4})$', + title='AtlasCollectionIdWildcard', + ), +] -class AtlasEntryId(RootModel[str]): - root: Annotated[ - str, - Field( - description='A unique ID representing an AtlasEntry object.', - pattern='^atlas_entry:([a-z][a-z0-9_]*(?:\\/[a-z][a-z0-9_]*){2,5})$', - title='AtlasEntryId', - ), - ] +AtlasEntryId: TypeAlias = Annotated[ + str, + Field( + description='A unique ID representing an AtlasEntry object.', + pattern='^atlas_entry:([a-z][a-z0-9_]*(?:\\/[a-z][a-z0-9_]*){2,5})$', + title='AtlasEntryId', + ), +] -class AtlasEntryIdWildcard(RootModel[str]): - root: Annotated[ - str, - Field( - description='A wildcarded AtlasEntryId that can be used to match multiple AtlasEntry objects.', - pattern='^atlas_entry:((?:[a-z][a-z0-9_]*|\\*)(?:\\/(?:[a-z][a-z0-9_]*|\\*|\\*\\*)){2,5})$', - title='AtlasEntryIdWildcard', - ), - ] +AtlasEntryIdWildcard: TypeAlias = Annotated[ + str, + Field( + description='A wildcarded AtlasEntryId that can be used to match multiple AtlasEntry objects.', + pattern='^atlas_entry:((?:[a-z][a-z0-9_]*|\\*)(?:\\/(?:[a-z][a-z0-9_]*|\\*|\\*\\*)){2,5})$', + title='AtlasEntryIdWildcard', + ), +] class ChallengeRank(Enum): @@ -385,194 +365,177 @@ class DelveSiteDenizenFrequency(Enum): unforeseen = 'unforeseen' -class DelveSiteDenizenId(RootModel[str]): - root: Annotated[ - str, - Field( - description='A unique ID representing a DelveSiteDenizen object.', - pattern='^delve_site\\.denizen:([a-z][a-z0-9_]*\\/[a-z][a-z0-9_]*)\\.(\\d+)$', - title='DelveSiteDenizenId', - ), - ] - - -class DelveSiteDenizenIdWildcard(RootModel[str]): - root: Annotated[ - str, - Field( - description='A wildcarded DelveSiteDenizenId that can be used to match multiple DelveSiteDenizen objects.', - pattern='^delve_site\\.denizen:((?:[a-z][a-z0-9_]*|\\*)\\/[a-z][a-z0-9_]*|\\/\\*|\\/\\*\\*)\\.(\\d+|\\*)$', - title='DelveSiteDenizenIdWildcard', - ), - ] - - - - -class DelveSiteDomainDangerId(RootModel[str]): - root: Annotated[ - str, - Field( - description='A unique ID representing a DelveSiteDomainDanger object.', - pattern='^delve_site_domain\\.danger:([a-z][a-z0-9_]*\\/[a-z][a-z0-9_]*)\\.(\\d+)$', - title='DelveSiteDomainDangerId', - ), - ] - - -class DelveSiteDomainDangerIdWildcard(RootModel[str]): - root: Annotated[ - str, - Field( - description='A wildcarded DelveSiteDomainDangerId that can be used to match multiple DelveSiteDomainDanger objects.', - pattern='^delve_site_domain\\.danger:((?:[a-z][a-z0-9_]*|\\*)\\/[a-z][a-z0-9_]*|\\/\\*|\\/\\*\\*)\\.(\\d+|\\*)$', - title='DelveSiteDomainDangerIdWildcard', - ), - ] - - -class DelveSiteDomainFeatureId(RootModel[str]): - root: Annotated[ - str, - Field( - description='A unique ID representing a DelveSiteDomainFeature object.', - pattern='^delve_site_domain\\.feature:([a-z][a-z0-9_]*\\/[a-z][a-z0-9_]*)\\.(\\d+)$', - title='DelveSiteDomainFeatureId', - ), - ] - - -class DelveSiteDomainFeatureIdWildcard(RootModel[str]): - root: Annotated[ - str, - Field( - description='A wildcarded DelveSiteDomainFeatureId that can be used to match multiple DelveSiteDomainFeature objects.', - pattern='^delve_site_domain\\.feature:((?:[a-z][a-z0-9_]*|\\*)\\/[a-z][a-z0-9_]*|\\/\\*|\\/\\*\\*)\\.(\\d+|\\*)$', - title='DelveSiteDomainFeatureIdWildcard', - ), - ] - - -class DelveSiteDomainId(RootModel[str]): - root: Annotated[ - str, - Field( - description='A unique ID representing a DelveSiteDomain object.', - pattern='^delve_site_domain:([a-z][a-z0-9_]*\\/[a-z][a-z0-9_]*)$', - title='DelveSiteDomainId', - ), - ] - - -class DelveSiteDomainIdWildcard(RootModel[str]): - root: Annotated[ - str, - Field( - description='A wildcarded DelveSiteDomainId that can be used to match multiple DelveSiteDomain objects.', - pattern='^delve_site_domain:((?:[a-z][a-z0-9_]*|\\*)\\/[a-z][a-z0-9_]*|\\/\\*|\\/\\*\\*)$', - title='DelveSiteDomainIdWildcard', - ), - ] - - -class DelveSiteId(RootModel[str]): - root: Annotated[ - str, - Field( - description='A unique ID representing a DelveSite object.', - pattern='^delve_site:([a-z][a-z0-9_]*\\/[a-z][a-z0-9_]*)$', - title='DelveSiteId', - ), - ] - - -class DelveSiteIdWildcard(RootModel[str]): - root: Annotated[ - str, - Field( - description='A wildcarded DelveSiteId that can be used to match multiple DelveSite objects.', - pattern='^delve_site:((?:[a-z][a-z0-9_]*|\\*)\\/[a-z][a-z0-9_]*|\\/\\*|\\/\\*\\*)$', - title='DelveSiteIdWildcard', - ), - ] - - -class DelveSiteThemeDangerId(RootModel[str]): - root: Annotated[ - str, - Field( - description='A unique ID representing a DelveSiteThemeDanger object.', - pattern='^delve_site_theme\\.danger:([a-z][a-z0-9_]*\\/[a-z][a-z0-9_]*)\\.(\\d+)$', - title='DelveSiteThemeDangerId', - ), - ] - - -class DelveSiteThemeDangerIdWildcard(RootModel[str]): - root: Annotated[ - str, - Field( - description='A wildcarded DelveSiteThemeDangerId that can be used to match multiple DelveSiteThemeDanger objects.', - pattern='^delve_site_theme\\.danger:((?:[a-z][a-z0-9_]*|\\*)\\/[a-z][a-z0-9_]*|\\/\\*|\\/\\*\\*)\\.(\\d+|\\*)$', - title='DelveSiteThemeDangerIdWildcard', - ), - ] - - -class DelveSiteThemeFeatureId(RootModel[str]): - root: Annotated[ - str, - Field( - description='A unique ID representing a DelveSiteThemeFeature object.', - pattern='^delve_site_theme\\.feature:([a-z][a-z0-9_]*\\/[a-z][a-z0-9_]*)\\.(\\d+)$', - title='DelveSiteThemeFeatureId', - ), - ] - - -class DelveSiteThemeFeatureIdWildcard(RootModel[str]): - root: Annotated[ - str, - Field( - description='A wildcarded DelveSiteThemeFeatureId that can be used to match multiple DelveSiteThemeFeature objects.', - pattern='^delve_site_theme\\.feature:((?:[a-z][a-z0-9_]*|\\*)\\/[a-z][a-z0-9_]*|\\/\\*|\\/\\*\\*)\\.(\\d+|\\*)$', - title='DelveSiteThemeFeatureIdWildcard', - ), - ] - - -class DelveSiteThemeId(RootModel[str]): - root: Annotated[ - str, - Field( - description='A unique ID representing a DelveSiteTheme object.', - pattern='^delve_site_theme:([a-z][a-z0-9_]*\\/[a-z][a-z0-9_]*)$', - title='DelveSiteThemeId', - ), - ] - - -class DelveSiteThemeIdWildcard(RootModel[str]): - root: Annotated[ - str, - Field( - description='A wildcarded DelveSiteThemeId that can be used to match multiple DelveSiteTheme objects.', - pattern='^delve_site_theme:((?:[a-z][a-z0-9_]*|\\*)\\/[a-z][a-z0-9_]*|\\/\\*|\\/\\*\\*)$', - title='DelveSiteThemeIdWildcard', - ), - ] - - -class DiceExpression(RootModel[str]): - root: Annotated[ - str, - Field( - description='A simple dice roll expression with an optional (positive or negative) modifer.', - examples=['1d100', '1d6+2', '2d10'], - pattern='([1-9][0-9]*)d([1-9][0-9]*)([+-]([1-9][0-9]*))?', - title='DiceExpression', - ), - ] +DelveSiteDenizenId: TypeAlias = Annotated[ + str, + Field( + description='A unique ID representing a DelveSiteDenizen object.', + pattern='^delve_site\\.denizen:([a-z][a-z0-9_]*\\/[a-z][a-z0-9_]*)\\.(\\d+)$', + title='DelveSiteDenizenId', + ), +] + + +DelveSiteDenizenIdWildcard: TypeAlias = Annotated[ + str, + Field( + description='A wildcarded DelveSiteDenizenId that can be used to match multiple DelveSiteDenizen objects.', + pattern='^delve_site\\.denizen:((?:[a-z][a-z0-9_]*|\\*)\\/[a-z][a-z0-9_]*|\\/\\*|\\/\\*\\*)\\.(\\d+|\\*)$', + title='DelveSiteDenizenIdWildcard', + ), +] + + + + +DelveSiteDomainDangerId: TypeAlias = Annotated[ + str, + Field( + description='A unique ID representing a DelveSiteDomainDanger object.', + pattern='^delve_site_domain\\.danger:([a-z][a-z0-9_]*\\/[a-z][a-z0-9_]*)\\.(\\d+)$', + title='DelveSiteDomainDangerId', + ), +] + + +DelveSiteDomainDangerIdWildcard: TypeAlias = Annotated[ + str, + Field( + description='A wildcarded DelveSiteDomainDangerId that can be used to match multiple DelveSiteDomainDanger objects.', + pattern='^delve_site_domain\\.danger:((?:[a-z][a-z0-9_]*|\\*)\\/[a-z][a-z0-9_]*|\\/\\*|\\/\\*\\*)\\.(\\d+|\\*)$', + title='DelveSiteDomainDangerIdWildcard', + ), +] + + +DelveSiteDomainFeatureId: TypeAlias = Annotated[ + str, + Field( + description='A unique ID representing a DelveSiteDomainFeature object.', + pattern='^delve_site_domain\\.feature:([a-z][a-z0-9_]*\\/[a-z][a-z0-9_]*)\\.(\\d+)$', + title='DelveSiteDomainFeatureId', + ), +] + + +DelveSiteDomainFeatureIdWildcard: TypeAlias = Annotated[ + str, + Field( + description='A wildcarded DelveSiteDomainFeatureId that can be used to match multiple DelveSiteDomainFeature objects.', + pattern='^delve_site_domain\\.feature:((?:[a-z][a-z0-9_]*|\\*)\\/[a-z][a-z0-9_]*|\\/\\*|\\/\\*\\*)\\.(\\d+|\\*)$', + title='DelveSiteDomainFeatureIdWildcard', + ), +] + + +DelveSiteDomainId: TypeAlias = Annotated[ + str, + Field( + description='A unique ID representing a DelveSiteDomain object.', + pattern='^delve_site_domain:([a-z][a-z0-9_]*\\/[a-z][a-z0-9_]*)$', + title='DelveSiteDomainId', + ), +] + + +DelveSiteDomainIdWildcard: TypeAlias = Annotated[ + str, + Field( + description='A wildcarded DelveSiteDomainId that can be used to match multiple DelveSiteDomain objects.', + pattern='^delve_site_domain:((?:[a-z][a-z0-9_]*|\\*)\\/[a-z][a-z0-9_]*|\\/\\*|\\/\\*\\*)$', + title='DelveSiteDomainIdWildcard', + ), +] + + +DelveSiteId: TypeAlias = Annotated[ + str, + Field( + description='A unique ID representing a DelveSite object.', + pattern='^delve_site:([a-z][a-z0-9_]*\\/[a-z][a-z0-9_]*)$', + title='DelveSiteId', + ), +] + + +DelveSiteIdWildcard: TypeAlias = Annotated[ + str, + Field( + description='A wildcarded DelveSiteId that can be used to match multiple DelveSite objects.', + pattern='^delve_site:((?:[a-z][a-z0-9_]*|\\*)\\/[a-z][a-z0-9_]*|\\/\\*|\\/\\*\\*)$', + title='DelveSiteIdWildcard', + ), +] + + +DelveSiteThemeDangerId: TypeAlias = Annotated[ + str, + Field( + description='A unique ID representing a DelveSiteThemeDanger object.', + pattern='^delve_site_theme\\.danger:([a-z][a-z0-9_]*\\/[a-z][a-z0-9_]*)\\.(\\d+)$', + title='DelveSiteThemeDangerId', + ), +] + + +DelveSiteThemeDangerIdWildcard: TypeAlias = Annotated[ + str, + Field( + description='A wildcarded DelveSiteThemeDangerId that can be used to match multiple DelveSiteThemeDanger objects.', + pattern='^delve_site_theme\\.danger:((?:[a-z][a-z0-9_]*|\\*)\\/[a-z][a-z0-9_]*|\\/\\*|\\/\\*\\*)\\.(\\d+|\\*)$', + title='DelveSiteThemeDangerIdWildcard', + ), +] + + +DelveSiteThemeFeatureId: TypeAlias = Annotated[ + str, + Field( + description='A unique ID representing a DelveSiteThemeFeature object.', + pattern='^delve_site_theme\\.feature:([a-z][a-z0-9_]*\\/[a-z][a-z0-9_]*)\\.(\\d+)$', + title='DelveSiteThemeFeatureId', + ), +] + + +DelveSiteThemeFeatureIdWildcard: TypeAlias = Annotated[ + str, + Field( + description='A wildcarded DelveSiteThemeFeatureId that can be used to match multiple DelveSiteThemeFeature objects.', + pattern='^delve_site_theme\\.feature:((?:[a-z][a-z0-9_]*|\\*)\\/[a-z][a-z0-9_]*|\\/\\*|\\/\\*\\*)\\.(\\d+|\\*)$', + title='DelveSiteThemeFeatureIdWildcard', + ), +] + + +DelveSiteThemeId: TypeAlias = Annotated[ + str, + Field( + description='A unique ID representing a DelveSiteTheme object.', + pattern='^delve_site_theme:([a-z][a-z0-9_]*\\/[a-z][a-z0-9_]*)$', + title='DelveSiteThemeId', + ), +] + + +DelveSiteThemeIdWildcard: TypeAlias = Annotated[ + str, + Field( + description='A wildcarded DelveSiteThemeId that can be used to match multiple DelveSiteTheme objects.', + pattern='^delve_site_theme:((?:[a-z][a-z0-9_]*|\\*)\\/[a-z][a-z0-9_]*|\\/\\*|\\/\\*\\*)$', + title='DelveSiteThemeIdWildcard', + ), +] + + +DiceExpression: TypeAlias = Annotated[ + str, + Field( + description='A simple dice roll expression with an optional (positive or negative) modifer.', + examples=['1d100', '1d6+2', '2d10'], + pattern='([1-9][0-9]*)d([1-9][0-9]*)([+-]([1-9][0-9]*))?', + title='DiceExpression', + ), +] class DiceRange(BaseModel): @@ -580,15 +543,14 @@ class DiceRange(BaseModel): max: Annotated[int, Field(description='High end of the dice range.')] -class DictKey(RootModel[str]): - root: Annotated[ - str, - Field( - description='A `snake_case` key used in a Datasworn dictionary object.', - pattern='^[a-z][a-z0-9_]*$', - title='DictKey', - ), - ] +DictKey: TypeAlias = Annotated[ + str, + Field( + description='A `snake_case` key used in a Datasworn dictionary object.', + pattern='^[a-z][a-z0-9_]*$', + title='DictKey', + ), +] class RollType(Enum): @@ -605,15 +567,14 @@ class EmbeddedMove(BaseModel): roll_type: RollType -class EmbeddedMoveIdWildcard(RootModel[str]): - root: Annotated[ - str, - Field( - description='A wildcarded AssetAbilityMoveId that can be used to match multiple AssetAbilityMove objects.', - pattern='^asset\\.ability\\.move:((?:[a-z][a-z0-9_]*|\\*)(?:\\/(?:[a-z][a-z0-9_]*|\\*|\\*\\*)){2,5})\\.(\\d+|\\*)\\.([a-z][a-z0-9_]*|\\*)$', - title='EmbeddedMoveIdWildcard', - ), - ] +EmbeddedMoveIdWildcard: TypeAlias = Annotated[ + str, + Field( + description='A wildcarded AssetAbilityMoveId that can be used to match multiple AssetAbilityMove objects.', + pattern='^asset\\.ability\\.move:((?:[a-z][a-z0-9_]*|\\*)(?:\\/(?:[a-z][a-z0-9_]*|\\*|\\*\\*)){2,5})\\.(\\d+|\\*)\\.([a-z][a-z0-9_]*|\\*)$', + title='EmbeddedMoveIdWildcard', + ), +] class RecommendedRolls(BaseModel): @@ -630,195 +591,179 @@ class OracleType(Enum): column_text3 = 'column_text3' -class EmbeddedOracleRollable(BaseModel): - model_config = ConfigDict( - extra='allow', - ) - oracle_type: OracleType +EmbeddedOracleRollable: TypeAlias = Annotated[ + Union["EmbeddedOracleColumnText", "EmbeddedOracleColumnText2", "EmbeddedOracleColumnText3", "EmbeddedOracleTableText", "EmbeddedOracleTableText2", "EmbeddedOracleTableText3"], + Field(discriminator="oracle_type"), +] class EmbedOnlyType(Enum): ability = 'ability' condition = 'condition' - outcome = 'outcome' - option = 'option' - row = 'row' - feature = 'feature' - danger = 'danger' - denizen = 'denizen' - variant = 'variant' - - -class ExpansionId(RootModel[str]): - root: Annotated[ - str, - Field( - description='The ID of a Datasworn package that relies on an external package to provide its ruleset.', - examples=['delve', 'sundered_isles'], - pattern='^[a-z][a-z0-9_]*$', - title='ExpansionId', - ), - ] - - -class Tags(BaseModel): - core: Annotated[CoreTags | None, Field(alias='_core')] = None - - -class MarkdownTemplateString(RootModel[str]): - root: Annotated[ - str, - Field( - description='A rich text string in Markdown with replaced values from oracle roll results.\n\nThe custom syntax `{{some_row_key>some_oracle_table_id}}` should be replaced by the `some_row_key` string of a rolled oracle table. This is usually the `text` key, for example `{{text>oracle_rollable:starforged/core/action}}`\n', - title='MarkdownTemplateString', - ), - ] - - -class Move(BaseModel): - model_config = ConfigDict( - extra='allow', - ) - roll_type: RollType - - -class MoveCategoryId(RootModel[str]): - root: Annotated[ - str, - Field( - description='A unique ID representing a MoveCategory object.', - pattern='^move_category:([a-z][a-z0-9_]*(?:\\/[a-z][a-z0-9_]*){1,4})$', - title='MoveCategoryId', - ), - ] - - -class MoveCategoryIdWildcard(RootModel[str]): - root: Annotated[ - str, - Field( - description='A wildcarded MoveCategoryId that can be used to match multiple MoveCategory objects.', - pattern='^move_category:((?:[a-z][a-z0-9_]*|\\*)(?:\\/(?:[a-z][a-z0-9_]*|\\*|\\*\\*)){1,4})$', - title='MoveCategoryIdWildcard', - ), - ] - - -class MoveConditionId(RootModel[str]): - root: Annotated[ - str, - Field( - description='A unique ID representing a MoveCondition object.', - pattern='^move\\.condition:([a-z][a-z0-9_]*(?:\\/[a-z][a-z0-9_]*){2,5})\\.(\\d+)$', - title='MoveConditionId', - ), - ] + outcome = 'outcome' + option = 'option' + row = 'row' + feature = 'feature' + danger = 'danger' + denizen = 'denizen' + variant = 'variant' -class MoveConditionIdWildcard(RootModel[str]): - root: Annotated[ - str, - Field( - description='A wildcarded MoveConditionId that can be used to match multiple MoveCondition objects.', - pattern='^move\\.condition:((?:[a-z][a-z0-9_]*|\\*)(?:\\/(?:[a-z][a-z0-9_]*|\\*|\\*\\*)){2,5})\\.(\\d+|\\*)$', - title='MoveConditionIdWildcard', - ), - ] +ExpansionId: TypeAlias = Annotated[ + str, + Field( + description='The ID of a Datasworn package that relies on an external package to provide its ruleset.', + examples=['delve', 'sundered_isles'], + pattern='^[a-z][a-z0-9_]*$', + title='ExpansionId', + ), +] -class MoveEnhancement(BaseModel): - model_config = ConfigDict( - extra='allow', - ) - roll_type: RollType +class Tags(BaseModel): + core: Annotated[CoreTags | None, Field(alias='_core')] = None -class MoveId(RootModel[str]): - root: Annotated[ - str, - Field( - description='A unique ID representing a Move object.', - pattern='^move:([a-z][a-z0-9_]*(?:\\/[a-z][a-z0-9_]*){2,5})$', - title='MoveId', - ), - ] +MarkdownTemplateString: TypeAlias = Annotated[ + str, + Field( + description='A rich text string in Markdown with replaced values from oracle roll results.\n\nThe custom syntax `{{some_row_key>some_oracle_table_id}}` should be replaced by the `some_row_key` string of a rolled oracle table. This is usually the `text` key, for example `{{text>oracle_rollable:starforged/core/action}}`\n', + title='MarkdownTemplateString', + ), +] -class MoveIdWildcard(RootModel[str]): - root: Annotated[ - str, - Field( - description='A wildcarded MoveId that can be used to match multiple Move objects.', - pattern='^move:((?:[a-z][a-z0-9_]*|\\*)(?:\\/(?:[a-z][a-z0-9_]*|\\*|\\*\\*)){2,5})$', - title='MoveIdWildcard', - ), - ] +Move: TypeAlias = Annotated[ + Union["MoveActionRoll", "MoveNoRoll", "MoveProgressRoll", "MoveSpecialTrack"], + Field(discriminator="roll_type"), +] -class MoveOracleRollableId(RootModel[str]): - root: Annotated[ - str, - Field( - description='A unique ID representing a MoveOracleRollable object.', - pattern='^move\\.oracle_rollable:([a-z][a-z0-9_]*(?:\\/[a-z][a-z0-9_]*){2,5})\\.([a-z][a-z0-9_]*|\\*)$', - title='MoveOracleRollableId', - ), - ] +MoveCategoryId: TypeAlias = Annotated[ + str, + Field( + description='A unique ID representing a MoveCategory object.', + pattern='^move_category:([a-z][a-z0-9_]*(?:\\/[a-z][a-z0-9_]*){1,4})$', + title='MoveCategoryId', + ), +] -class MoveOracleRollableIdWildcard(RootModel[str]): - root: Annotated[ - str, - Field( - description='A wildcarded MoveOracleRollableId that can be used to match multiple MoveOracleRollable objects.', - pattern='^move\\.oracle_rollable:((?:[a-z][a-z0-9_]*|\\*)(?:\\/(?:[a-z][a-z0-9_]*|\\*|\\*\\*)){2,5})\\.([a-z][a-z0-9_]*|\\*)$', - title='MoveOracleRollableIdWildcard', - ), - ] +MoveCategoryIdWildcard: TypeAlias = Annotated[ + str, + Field( + description='A wildcarded MoveCategoryId that can be used to match multiple MoveCategory objects.', + pattern='^move_category:((?:[a-z][a-z0-9_]*|\\*)(?:\\/(?:[a-z][a-z0-9_]*|\\*|\\*\\*)){1,4})$', + title='MoveCategoryIdWildcard', + ), +] -class MoveOracleRollableRowId(RootModel[str]): - root: Annotated[ - str, - Field( - description='A unique ID representing a MoveOracleRollableRow object.', - pattern='^move\\.oracle_rollable\\.row:([a-z][a-z0-9_]*(?:\\/[a-z][a-z0-9_]*){2,5})\\.([a-z][a-z0-9_]*|\\*)\\.(\\d+)$', - title='MoveOracleRollableRowId', - ), - ] +MoveConditionId: TypeAlias = Annotated[ + str, + Field( + description='A unique ID representing a MoveCondition object.', + pattern='^move\\.condition:([a-z][a-z0-9_]*(?:\\/[a-z][a-z0-9_]*){2,5})\\.(\\d+)$', + title='MoveConditionId', + ), +] -class MoveOracleRollableRowIdWildcard(RootModel[str]): - root: Annotated[ - str, - Field( - description='A wildcarded MoveOracleRollableRowId that can be used to match multiple MoveOracleRollableRow objects.', - pattern='^move\\.oracle_rollable\\.row:((?:[a-z][a-z0-9_]*|\\*)(?:\\/(?:[a-z][a-z0-9_]*|\\*|\\*\\*)){2,5})\\.([a-z][a-z0-9_]*|\\*)\\.(\\d+|\\*)$', - title='MoveOracleRollableRowIdWildcard', - ), - ] +MoveConditionIdWildcard: TypeAlias = Annotated[ + str, + Field( + description='A wildcarded MoveConditionId that can be used to match multiple MoveCondition objects.', + pattern='^move\\.condition:((?:[a-z][a-z0-9_]*|\\*)(?:\\/(?:[a-z][a-z0-9_]*|\\*|\\*\\*)){2,5})\\.(\\d+|\\*)$', + title='MoveConditionIdWildcard', + ), +] -class MoveOutcomeId(RootModel[str]): - root: Annotated[ - str, - Field( - description='A unique ID representing a MoveOutcome object.', - pattern='^move\\.outcome:([a-z][a-z0-9_]*(?:\\/[a-z][a-z0-9_]*){2,5})\\.([a-z][a-z0-9_]*|\\*)$', - title='MoveOutcomeId', - ), - ] +class MoveEnhancement(BaseModel): + model_config = ConfigDict( + extra='allow', + ) + roll_type: RollType -class MoveOutcomeIdWildcard(RootModel[str]): - root: Annotated[ - str, - Field( - description='A wildcarded MoveOutcomeId that can be used to match multiple MoveOutcome objects.', - pattern='^move\\.outcome:((?:[a-z][a-z0-9_]*|\\*)(?:\\/(?:[a-z][a-z0-9_]*|\\*|\\*\\*)){2,5})\\.([a-z][a-z0-9_]*|\\*)$', - title='MoveOutcomeIdWildcard', - ), - ] +MoveId: TypeAlias = Annotated[ + str, + Field( + description='A unique ID representing a Move object.', + pattern='^move:([a-z][a-z0-9_]*(?:\\/[a-z][a-z0-9_]*){2,5})$', + title='MoveId', + ), +] + + +MoveIdWildcard: TypeAlias = Annotated[ + str, + Field( + description='A wildcarded MoveId that can be used to match multiple Move objects.', + pattern='^move:((?:[a-z][a-z0-9_]*|\\*)(?:\\/(?:[a-z][a-z0-9_]*|\\*|\\*\\*)){2,5})$', + title='MoveIdWildcard', + ), +] + + +MoveOracleRollableId: TypeAlias = Annotated[ + str, + Field( + description='A unique ID representing a MoveOracleRollable object.', + pattern='^move\\.oracle_rollable:([a-z][a-z0-9_]*(?:\\/[a-z][a-z0-9_]*){2,5})\\.([a-z][a-z0-9_]*|\\*)$', + title='MoveOracleRollableId', + ), +] + + +MoveOracleRollableIdWildcard: TypeAlias = Annotated[ + str, + Field( + description='A wildcarded MoveOracleRollableId that can be used to match multiple MoveOracleRollable objects.', + pattern='^move\\.oracle_rollable:((?:[a-z][a-z0-9_]*|\\*)(?:\\/(?:[a-z][a-z0-9_]*|\\*|\\*\\*)){2,5})\\.([a-z][a-z0-9_]*|\\*)$', + title='MoveOracleRollableIdWildcard', + ), +] + + +MoveOracleRollableRowId: TypeAlias = Annotated[ + str, + Field( + description='A unique ID representing a MoveOracleRollableRow object.', + pattern='^move\\.oracle_rollable\\.row:([a-z][a-z0-9_]*(?:\\/[a-z][a-z0-9_]*){2,5})\\.([a-z][a-z0-9_]*|\\*)\\.(\\d+)$', + title='MoveOracleRollableRowId', + ), +] + + +MoveOracleRollableRowIdWildcard: TypeAlias = Annotated[ + str, + Field( + description='A wildcarded MoveOracleRollableRowId that can be used to match multiple MoveOracleRollableRow objects.', + pattern='^move\\.oracle_rollable\\.row:((?:[a-z][a-z0-9_]*|\\*)(?:\\/(?:[a-z][a-z0-9_]*|\\*|\\*\\*)){2,5})\\.([a-z][a-z0-9_]*|\\*)\\.(\\d+|\\*)$', + title='MoveOracleRollableRowIdWildcard', + ), +] + + +MoveOutcomeId: TypeAlias = Annotated[ + str, + Field( + description='A unique ID representing a MoveOutcome object.', + pattern='^move\\.outcome:([a-z][a-z0-9_]*(?:\\/[a-z][a-z0-9_]*){2,5})\\.([a-z][a-z0-9_]*|\\*)$', + title='MoveOutcomeId', + ), +] + + +MoveOutcomeIdWildcard: TypeAlias = Annotated[ + str, + Field( + description='A wildcarded MoveOutcomeId that can be used to match multiple MoveOutcome objects.', + pattern='^move\\.outcome:((?:[a-z][a-z0-9_]*|\\*)(?:\\/(?:[a-z][a-z0-9_]*|\\*|\\*\\*)){2,5})\\.([a-z][a-z0-9_]*|\\*)$', + title='MoveOutcomeIdWildcard', + ), +] class MoveRollType(Enum): @@ -836,70 +781,64 @@ class NonCollectableType(Enum): truth = 'truth' -class NpcCollectionId(RootModel[str]): - root: Annotated[ - str, - Field( - description='A unique ID representing a NpcCollection object.', - pattern='^npc_collection:([a-z][a-z0-9_]*(?:\\/[a-z][a-z0-9_]*){1,4})$', - title='NpcCollectionId', - ), - ] - - -class NpcCollectionIdWildcard(RootModel[str]): - root: Annotated[ - str, - Field( - description='A wildcarded NpcCollectionId that can be used to match multiple NpcCollection objects.', - pattern='^npc_collection:((?:[a-z][a-z0-9_]*|\\*)(?:\\/(?:[a-z][a-z0-9_]*|\\*|\\*\\*)){1,4})$', - title='NpcCollectionIdWildcard', - ), - ] - - -class NpcId(RootModel[str]): - root: Annotated[ - str, - Field( - description='A unique ID representing a Npc object.', - pattern='^npc:([a-z][a-z0-9_]*(?:\\/[a-z][a-z0-9_]*){2,5})$', - title='NpcId', - ), - ] - - -class NpcIdWildcard(RootModel[str]): - root: Annotated[ - str, - Field( - description='A wildcarded NpcId that can be used to match multiple Npc objects.', - pattern='^npc:((?:[a-z][a-z0-9_]*|\\*)(?:\\/(?:[a-z][a-z0-9_]*|\\*|\\*\\*)){2,5})$', - title='NpcIdWildcard', - ), - ] - - -class NpcVariantId(RootModel[str]): - root: Annotated[ - str, - Field( - description='A unique ID representing a NpcVariant object.', - pattern='^npc\\.variant:([a-z][a-z0-9_]*(?:\\/[a-z][a-z0-9_]*){2,5})\\.([a-z][a-z0-9_]*|\\*)$', - title='NpcVariantId', - ), - ] - - -class NpcVariantIdWildcard(RootModel[str]): - root: Annotated[ - str, - Field( - description='A wildcarded NpcVariantId that can be used to match multiple NpcVariant objects.', - pattern='^npc\\.variant:((?:[a-z][a-z0-9_]*|\\*)(?:\\/(?:[a-z][a-z0-9_]*|\\*|\\*\\*)){2,5})\\.([a-z][a-z0-9_]*|\\*)$', - title='NpcVariantIdWildcard', - ), - ] +NpcCollectionId: TypeAlias = Annotated[ + str, + Field( + description='A unique ID representing a NpcCollection object.', + pattern='^npc_collection:([a-z][a-z0-9_]*(?:\\/[a-z][a-z0-9_]*){1,4})$', + title='NpcCollectionId', + ), +] + + +NpcCollectionIdWildcard: TypeAlias = Annotated[ + str, + Field( + description='A wildcarded NpcCollectionId that can be used to match multiple NpcCollection objects.', + pattern='^npc_collection:((?:[a-z][a-z0-9_]*|\\*)(?:\\/(?:[a-z][a-z0-9_]*|\\*|\\*\\*)){1,4})$', + title='NpcCollectionIdWildcard', + ), +] + + +NpcId: TypeAlias = Annotated[ + str, + Field( + description='A unique ID representing a Npc object.', + pattern='^npc:([a-z][a-z0-9_]*(?:\\/[a-z][a-z0-9_]*){2,5})$', + title='NpcId', + ), +] + + +NpcIdWildcard: TypeAlias = Annotated[ + str, + Field( + description='A wildcarded NpcId that can be used to match multiple Npc objects.', + pattern='^npc:((?:[a-z][a-z0-9_]*|\\*)(?:\\/(?:[a-z][a-z0-9_]*|\\*|\\*\\*)){2,5})$', + title='NpcIdWildcard', + ), +] + + +NpcVariantId: TypeAlias = Annotated[ + str, + Field( + description='A unique ID representing a NpcVariant object.', + pattern='^npc\\.variant:([a-z][a-z0-9_]*(?:\\/[a-z][a-z0-9_]*){2,5})\\.([a-z][a-z0-9_]*|\\*)$', + title='NpcVariantId', + ), +] + + +NpcVariantIdWildcard: TypeAlias = Annotated[ + str, + Field( + description='A wildcarded NpcVariantId that can be used to match multiple NpcVariant objects.', + pattern='^npc\\.variant:((?:[a-z][a-z0-9_]*|\\*)(?:\\/(?:[a-z][a-z0-9_]*|\\*|\\*\\*)){2,5})\\.([a-z][a-z0-9_]*|\\*)$', + title='NpcVariantIdWildcard', + ), +] class OracleType1(Enum): @@ -910,33 +849,30 @@ class OracleType1(Enum): table_shared_text3 = 'table_shared_text3' -class OracleCollection(BaseModel): - model_config = ConfigDict( - extra='allow', - ) - oracle_type: OracleType1 +OracleCollection: TypeAlias = Annotated[ + Union["OracleTablesCollection", "OracleTableSharedRolls", "OracleTableSharedText", "OracleTableSharedText2", "OracleTableSharedText3"], + Field(discriminator="oracle_type"), +] -class OracleCollectionId(RootModel[str]): - root: Annotated[ - str, - Field( - description='A unique ID representing an OracleCollection object.', - pattern='^oracle_collection:([a-z][a-z0-9_]*(?:\\/[a-z][a-z0-9_]*){1,4})$', - title='OracleCollectionId', - ), - ] +OracleCollectionId: TypeAlias = Annotated[ + str, + Field( + description='A unique ID representing an OracleCollection object.', + pattern='^oracle_collection:([a-z][a-z0-9_]*(?:\\/[a-z][a-z0-9_]*){1,4})$', + title='OracleCollectionId', + ), +] -class OracleCollectionIdWildcard(RootModel[str]): - root: Annotated[ - str, - Field( - description='A wildcarded OracleCollectionId that can be used to match multiple OracleCollection objects.', - pattern='^oracle_collection:((?:[a-z][a-z0-9_]*|\\*)(?:\\/(?:[a-z][a-z0-9_]*|\\*|\\*\\*)){1,4})$', - title='OracleCollectionIdWildcard', - ), - ] +OracleCollectionIdWildcard: TypeAlias = Annotated[ + str, + Field( + description='A wildcarded OracleCollectionId that can be used to match multiple OracleCollection objects.', + pattern='^oracle_collection:((?:[a-z][a-z0-9_]*|\\*)(?:\\/(?:[a-z][a-z0-9_]*|\\*|\\*\\*)){1,4})$', + title='OracleCollectionIdWildcard', + ), +] class OracleDuplicateBehavior(Enum): @@ -964,55 +900,50 @@ class OracleType2(Enum): column_text3 = 'column_text3' -class OracleRollable(BaseModel): - model_config = ConfigDict( - extra='allow', - ) - oracle_type: OracleType2 +OracleRollable: TypeAlias = Annotated[ + Union["OracleColumnText", "OracleColumnText2", "OracleColumnText3", "OracleTableText", "OracleTableText2", "OracleTableText3"], + Field(discriminator="oracle_type"), +] -class OracleRollableId(RootModel[str]): - root: Annotated[ - str, - Field( - description='A unique ID representing an OracleRollable object.', - pattern='^oracle_rollable:([a-z][a-z0-9_]*(?:\\/[a-z][a-z0-9_]*){2,5})$', - title='OracleRollableId', - ), - ] +OracleRollableId: TypeAlias = Annotated[ + str, + Field( + description='A unique ID representing an OracleRollable object.', + pattern='^oracle_rollable:([a-z][a-z0-9_]*(?:\\/[a-z][a-z0-9_]*){2,5})$', + title='OracleRollableId', + ), +] -class OracleRollableIdWildcard(RootModel[str]): - root: Annotated[ - str, - Field( - description='A wildcarded OracleRollableId that can be used to match multiple OracleRollable objects.', - pattern='^oracle_rollable:((?:[a-z][a-z0-9_]*|\\*)(?:\\/(?:[a-z][a-z0-9_]*|\\*|\\*\\*)){2,5})$', - title='OracleRollableIdWildcard', - ), - ] +OracleRollableIdWildcard: TypeAlias = Annotated[ + str, + Field( + description='A wildcarded OracleRollableId that can be used to match multiple OracleRollable objects.', + pattern='^oracle_rollable:((?:[a-z][a-z0-9_]*|\\*)(?:\\/(?:[a-z][a-z0-9_]*|\\*|\\*\\*)){2,5})$', + title='OracleRollableIdWildcard', + ), +] -class OracleRollableRowId(RootModel[str]): - root: Annotated[ - str, - Field( - description='A unique ID representing an OracleRollableRow object.', - pattern='^oracle_rollable\\.row:([a-z][a-z0-9_]*(?:\\/[a-z][a-z0-9_]*){2,5})\\.(\\d+)$', - title='OracleRollableRowId', - ), - ] +OracleRollableRowId: TypeAlias = Annotated[ + str, + Field( + description='A unique ID representing an OracleRollableRow object.', + pattern='^oracle_rollable\\.row:([a-z][a-z0-9_]*(?:\\/[a-z][a-z0-9_]*){2,5})\\.(\\d+)$', + title='OracleRollableRowId', + ), +] -class OracleRollableRowIdWildcard(RootModel[str]): - root: Annotated[ - str, - Field( - description='A wildcarded OracleRollableRowId that can be used to match multiple OracleRollableRow objects.', - pattern='^oracle_rollable\\.row:((?:[a-z][a-z0-9_]*|\\*)(?:\\/(?:[a-z][a-z0-9_]*|\\*|\\*\\*)){2,5})\\.(\\d+|\\*)$', - title='OracleRollableRowIdWildcard', - ), - ] +OracleRollableRowIdWildcard: TypeAlias = Annotated[ + str, + Field( + description='A wildcarded OracleRollableRowId that can be used to match multiple OracleRollableRow objects.', + pattern='^oracle_rollable\\.row:((?:[a-z][a-z0-9_]*|\\*)(?:\\/(?:[a-z][a-z0-9_]*|\\*|\\*\\*)){2,5})\\.(\\d+|\\*)$', + title='OracleRollableRowIdWildcard', + ), +] class Template(BaseModel): @@ -1048,11 +979,10 @@ class OracleType3(Enum): table_text3 = 'table_text3' -class OracleRollableTable(BaseModel): - model_config = ConfigDict( - extra='allow', - ) - oracle_type: OracleType3 +OracleRollableTable: TypeAlias = Annotated[ + Union["OracleTableText", "OracleTableText2", "OracleTableText3"], + Field(discriminator="oracle_type"), +] class OracleRollTemplate(BaseModel): @@ -1259,26 +1189,24 @@ class ProgressTrackTypeInfo(BaseModel): controls: dict[str, dict[str, Any]] | None = {} -class RarityId(RootModel[str]): - root: Annotated[ - str, - Field( - description='A unique ID representing a Rarity object.', - pattern='^rarity:([a-z][a-z0-9_]*\\/[a-z][a-z0-9_]*)$', - title='RarityId', - ), - ] +RarityId: TypeAlias = Annotated[ + str, + Field( + description='A unique ID representing a Rarity object.', + pattern='^rarity:([a-z][a-z0-9_]*\\/[a-z][a-z0-9_]*)$', + title='RarityId', + ), +] -class RarityIdWildcard(RootModel[str]): - root: Annotated[ - str, - Field( - description='A wildcarded RarityId that can be used to match multiple Rarity objects.', - pattern='^rarity:((?:[a-z][a-z0-9_]*|\\*)\\/[a-z][a-z0-9_]*|\\/\\*|\\/\\*\\*)$', - title='RarityIdWildcard', - ), - ] +RarityIdWildcard: TypeAlias = Annotated[ + str, + Field( + description='A wildcarded RarityId that can be used to match multiple Rarity objects.', + pattern='^rarity:((?:[a-z][a-z0-9_]*|\\*)\\/[a-z][a-z0-9_]*|\\/\\*|\\/\\*\\*)$', + title='RarityIdWildcard', + ), +] class Using(Enum): @@ -1298,16 +1226,15 @@ class RollableValue(BaseModel): using: Using -class RulesetId(RootModel[str]): - root: Annotated[ - str, - Field( - description='The ID of standalone Datasworn package that describes its own ruleset.', - examples=['classic', 'starforged'], - pattern='^[a-z][a-z0-9_]*$', - title='RulesetId', - ), - ] +RulesetId: TypeAlias = Annotated[ + str, + Field( + description='The ID of standalone Datasworn package that describes its own ruleset.', + examples=['classic', 'starforged'], + pattern='^[a-z][a-z0-9_]*$', + title='RulesetId', + ), +] class Type(Enum): @@ -1352,14 +1279,13 @@ class SelectValueFieldChoice(BaseModel): using: Using -class SemanticVersion(RootModel[str]): - root: Annotated[ - str, - Field( - pattern='^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$', - title='SemanticVersion', - ), - ] +SemanticVersion: TypeAlias = Annotated[ + str, + Field( + pattern='^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$', + title='SemanticVersion', + ), +] class SpecialTrackRollMethod(Enum): @@ -1745,103 +1671,94 @@ class TriggerSpecialTrackConditionOption(BaseModel): ] -class TruthId(RootModel[str]): - root: Annotated[ - str, - Field( - description='A unique ID representing a Truth object.', - pattern='^truth:([a-z][a-z0-9_]*\\/[a-z][a-z0-9_]*)$', - title='TruthId', - ), - ] - - -class TruthIdWildcard(RootModel[str]): - root: Annotated[ - str, - Field( - description='A wildcarded TruthId that can be used to match multiple Truth objects.', - pattern='^truth:((?:[a-z][a-z0-9_]*|\\*)\\/[a-z][a-z0-9_]*|\\/\\*|\\/\\*\\*)$', - title='TruthIdWildcard', - ), - ] - - -class TruthOptionId(RootModel[str]): - root: Annotated[ - str, - Field( - description='A unique ID representing a TruthOption object.', - pattern='^truth\\.option:([a-z][a-z0-9_]*\\/[a-z][a-z0-9_]*)\\.(\\d+)$', - title='TruthOptionId', - ), - ] - - -class TruthOptionIdWildcard(RootModel[str]): - root: Annotated[ - str, - Field( - description='A wildcarded TruthOptionId that can be used to match multiple TruthOption objects.', - pattern='^truth\\.option:((?:[a-z][a-z0-9_]*|\\*)\\/[a-z][a-z0-9_]*|\\/\\*|\\/\\*\\*)\\.(\\d+|\\*)$', - title='TruthOptionIdWildcard', - ), - ] - - -class TruthOptionOracleRollableId(RootModel[str]): - root: Annotated[ - str, - Field( - description='A unique ID representing a TruthOptionOracleRollable object.', - pattern='^truth\\.option\\.oracle_rollable:([a-z][a-z0-9_]*\\/[a-z][a-z0-9_]*)\\.(\\d+)\\.([a-z][a-z0-9_]*|\\*)$', - title='TruthOptionOracleRollableId', - ), - ] - - -class TruthOptionOracleRollableIdWildcard(RootModel[str]): - root: Annotated[ - str, - Field( - description='A wildcarded TruthOptionOracleRollableId that can be used to match multiple TruthOptionOracleRollable objects.', - pattern='^truth\\.option\\.oracle_rollable:((?:[a-z][a-z0-9_]*|\\*)\\/[a-z][a-z0-9_]*|\\/\\*|\\/\\*\\*)\\.(\\d+|\\*)\\.([a-z][a-z0-9_]*|\\*)$', - title='TruthOptionOracleRollableIdWildcard', - ), - ] - - -class TruthOptionOracleRollableRowId(RootModel[str]): - root: Annotated[ - str, - Field( - description='A unique ID representing a TruthOptionOracleRollableRow object.', - pattern='^truth\\.option\\.oracle_rollable\\.row:([a-z][a-z0-9_]*\\/[a-z][a-z0-9_]*)\\.(\\d+)\\.([a-z][a-z0-9_]*|\\*)\\.(\\d+)$', - title='TruthOptionOracleRollableRowId', - ), - ] - - -class TruthOptionOracleRollableRowIdWildcard(RootModel[str]): - root: Annotated[ - str, - Field( - description='A wildcarded TruthOptionOracleRollableRowId that can be used to match multiple TruthOptionOracleRollableRow objects.', - pattern='^truth\\.option\\.oracle_rollable\\.row:((?:[a-z][a-z0-9_]*|\\*)\\/[a-z][a-z0-9_]*|\\/\\*|\\/\\*\\*)\\.(\\d+|\\*)\\.([a-z][a-z0-9_]*|\\*)\\.(\\d+|\\*)$', - title='TruthOptionOracleRollableRowIdWildcard', - ), - ] - - -class WebpImageUrl(RootModel[str]): - root: Annotated[ - str, - Field( - description='A relative (local) URL pointing to a raster image in the WEBP format.', - pattern='\\.webp$', - title='WebpImageUrl', - ), - ] +TruthId: TypeAlias = Annotated[ + str, + Field( + description='A unique ID representing a Truth object.', + pattern='^truth:([a-z][a-z0-9_]*\\/[a-z][a-z0-9_]*)$', + title='TruthId', + ), +] + + +TruthIdWildcard: TypeAlias = Annotated[ + str, + Field( + description='A wildcarded TruthId that can be used to match multiple Truth objects.', + pattern='^truth:((?:[a-z][a-z0-9_]*|\\*)\\/[a-z][a-z0-9_]*|\\/\\*|\\/\\*\\*)$', + title='TruthIdWildcard', + ), +] + + +TruthOptionId: TypeAlias = Annotated[ + str, + Field( + description='A unique ID representing a TruthOption object.', + pattern='^truth\\.option:([a-z][a-z0-9_]*\\/[a-z][a-z0-9_]*)\\.(\\d+)$', + title='TruthOptionId', + ), +] + + +TruthOptionIdWildcard: TypeAlias = Annotated[ + str, + Field( + description='A wildcarded TruthOptionId that can be used to match multiple TruthOption objects.', + pattern='^truth\\.option:((?:[a-z][a-z0-9_]*|\\*)\\/[a-z][a-z0-9_]*|\\/\\*|\\/\\*\\*)\\.(\\d+|\\*)$', + title='TruthOptionIdWildcard', + ), +] + + +TruthOptionOracleRollableId: TypeAlias = Annotated[ + str, + Field( + description='A unique ID representing a TruthOptionOracleRollable object.', + pattern='^truth\\.option\\.oracle_rollable:([a-z][a-z0-9_]*\\/[a-z][a-z0-9_]*)\\.(\\d+)\\.([a-z][a-z0-9_]*|\\*)$', + title='TruthOptionOracleRollableId', + ), +] + + +TruthOptionOracleRollableIdWildcard: TypeAlias = Annotated[ + str, + Field( + description='A wildcarded TruthOptionOracleRollableId that can be used to match multiple TruthOptionOracleRollable objects.', + pattern='^truth\\.option\\.oracle_rollable:((?:[a-z][a-z0-9_]*|\\*)\\/[a-z][a-z0-9_]*|\\/\\*|\\/\\*\\*)\\.(\\d+|\\*)\\.([a-z][a-z0-9_]*|\\*)$', + title='TruthOptionOracleRollableIdWildcard', + ), +] + + +TruthOptionOracleRollableRowId: TypeAlias = Annotated[ + str, + Field( + description='A unique ID representing a TruthOptionOracleRollableRow object.', + pattern='^truth\\.option\\.oracle_rollable\\.row:([a-z][a-z0-9_]*\\/[a-z][a-z0-9_]*)\\.(\\d+)\\.([a-z][a-z0-9_]*|\\*)\\.(\\d+)$', + title='TruthOptionOracleRollableRowId', + ), +] + + +TruthOptionOracleRollableRowIdWildcard: TypeAlias = Annotated[ + str, + Field( + description='A wildcarded TruthOptionOracleRollableRowId that can be used to match multiple TruthOptionOracleRollableRow objects.', + pattern='^truth\\.option\\.oracle_rollable\\.row:((?:[a-z][a-z0-9_]*|\\*)\\/[a-z][a-z0-9_]*|\\/\\*|\\/\\*\\*)\\.(\\d+|\\*)\\.([a-z][a-z0-9_]*|\\*)\\.(\\d+|\\*)$', + title='TruthOptionOracleRollableRowIdWildcard', + ), +] + + +WebpImageUrl: TypeAlias = Annotated[ + str, + Field( + description='A relative (local) URL pointing to a raster image in the WEBP format.', + pattern='\\.webp$', + title='WebpImageUrl', + ), +] class DataswornV010(RootModel[RulesPackage]): @@ -2308,16 +2225,15 @@ class ConditionMeterField(BaseModel): ] = None -class ConditionMeterKey(RootModel[str]): - root: Annotated[ - str, - Field( - description='A basic, rollable player character resource specified by the ruleset.', - examples=['health', 'spirit', 'supply'], - pattern='^[a-z][a-z0-9_]*$', - title='ConditionMeterKey', - ), - ] +ConditionMeterKey: TypeAlias = Annotated[ + str, + Field( + description='A basic, rollable player character resource specified by the ruleset.', + examples=['health', 'spirit', 'supply'], + pattern='^[a-z][a-z0-9_]*$', + title='ConditionMeterKey', + ), +] class ConditionMeterRule(BaseModel): diff --git a/scripts/post_process_models.py b/scripts/post_process_models.py index e7cb0e9..d17d136 100644 --- a/scripts/post_process_models.py +++ b/scripts/post_process_models.py @@ -162,6 +162,232 @@ def _rewrite_stub_fields(source: str) -> tuple[str, int]: return new_source, hits + stub_hits +# --- Fix 3: RootModel[str] wrappers → TypeAlias ----------------------------- +# +# The generator emits every ID type as a class: +# +# class AssetAbilityId(RootModel[str]): +# root: Annotated[ +# str, +# Field( +# description='A unique ID representing an AssetAbility object.', +# pattern='^asset\\.ability:...', +# title='AssetAbilityId', +# ), +# ] +# +# Which forces every consumer to write `some_asset.id.root` instead of +# `some_asset.id`. Converting these to type aliases with the pattern preserved +# via `Annotated` gives back the ergonomic path (`.id` is a plain string) while +# keeping Pydantic validation on the pattern intact — Pydantic honors +# `Annotated[str, Field(pattern=...)]` inside model fields the same as it +# honors `RootModel[str]`. +# +# We deliberately only convert the `RootModel[str]` variant. `RootModel[int]`, +# `RootModel[list[X]]`, and `RootModel[Union[A, B]]` all have runtime shape +# that a plain type alias can't replicate — they stay untouched. + +# Matches an entire `class (RootModel[str]):\n root: Annotated[\n +# str,\n Field(\n ,\n ),\n ]` block and +# captures the class name and the Field kwargs. +_ROOT_MODEL_STR_RE = re.compile( + r"^class (\w+)\(RootModel\[str\]\):\n" + r" root: Annotated\[\n" + r" str,\n" + r" Field\(\n" + r"((?: .+\n)+)" + r" \),\n" + r" \]\n", + re.MULTILINE, +) + + +def _rewrite_root_model_str(source: str) -> tuple[str, int]: + """Rewrite each `class (RootModel[str]): root: Annotated[str, Field(…)]` + block as `: TypeAlias = Annotated[str, Field(…)]`. + """ + hits = 0 + + def _sub(match: re.Match[str]) -> str: + nonlocal hits + hits += 1 + class_name = match.group(1) + field_kwargs_block = match.group(2) + # The kwargs are already indented 12 spaces. Re-indent to 4 for the + # inline Field(…) call in the alias. + deindented = "\n".join( + line[8:] if line.startswith(" " * 8) else line + for line in field_kwargs_block.rstrip("\n").split("\n") + ) + return ( + f"{class_name}: TypeAlias = Annotated[\n" + f" str,\n" + f" Field(\n" + f"{deindented}\n" + f" ),\n" + f"]\n" + ) + + new_source = _ROOT_MODEL_STR_RE.sub(_sub, source) + return new_source, hits + + +def _ensure_type_alias_import(source: str) -> str: + """Make sure `TypeAlias` is imported from `typing`. Adds it if missing.""" + # Existing typing import line. + typing_import_re = re.compile(r"^from typing import (.+)$", re.MULTILINE) + match = typing_import_re.search(source) + if not match: + return source # unusual — bail + names = [n.strip() for n in match.group(1).split(",")] + if "TypeAlias" in names: + return source + names.append("TypeAlias") + names.sort() + new_line = "from typing import " + ", ".join(names) + return source[: match.start()] + new_line + source[match.end() :] + + +# --- Fix 4: resolve empty discriminated-union base classes ------------------ +# +# The generator emits every discriminated union in the schema as an empty base +# class with `extra='allow'`, holding only the discriminator field: +# +# class Move(BaseModel): +# model_config = ConfigDict(extra='allow') +# roll_type: RollType +# +# The concrete subtype classes (MoveActionRoll, MoveNoRoll, …) *do* get +# generated, they're just not wired up. Fields typed as `Move` end up +# validating against the empty base — every actual field on the concrete +# subtype lands in `__pydantic_extra__` instead of as an attribute. So +# `move.id` doesn't work; you have to reach `move.__pydantic_extra__["_id"]`. +# +# Fix: replace each broken base with a discriminated `Annotated[Union[…], +# Field(discriminator=…)]` alias. Forward-ref subtypes as strings because the +# concrete subclass definitions come later in the file. +# +# Not exhaustive — we handle the four bases whose subtype classes actually +# exist in the generated output. Bases like `MoveEnhancement`, `EmbeddedMove`, +# `RulesPackage`, `AssetControlField` etc. also have the same shape but their +# subtype classes weren't generated at all; fixing those needs upstream +# codegen work. + +_DISCRIMINATED_UNIONS: list[tuple[str, str, str, list[str]]] = [ + # (base_class, discriminator_field, discriminator_type_alias, subtype_class_names) + ( + "Move", + "roll_type", + "RollType", + ["MoveActionRoll", "MoveNoRoll", "MoveProgressRoll", "MoveSpecialTrack"], + ), + ( + "OracleRollable", + "oracle_type", + "OracleType2", + [ + "OracleColumnText", + "OracleColumnText2", + "OracleColumnText3", + "OracleTableText", + "OracleTableText2", + "OracleTableText3", + ], + ), + ( + "EmbeddedOracleRollable", + "oracle_type", + "OracleType", + [ + "EmbeddedOracleColumnText", + "EmbeddedOracleColumnText2", + "EmbeddedOracleColumnText3", + "EmbeddedOracleTableText", + "EmbeddedOracleTableText2", + "EmbeddedOracleTableText3", + ], + ), + ( + "OracleCollection", + "oracle_type", + "OracleType1", + [ + "OracleTablesCollection", + "OracleTableSharedRolls", + "OracleTableSharedText", + "OracleTableSharedText2", + "OracleTableSharedText3", + ], + ), + ( + # Nested union inside OracleRollable — codegen emits it as its own + # empty base with the `table_*` variants underneath. OracleCollection's + # `contents` field type-refs OracleRollableTable directly, so this + # matters even after OracleRollable is unioned. + "OracleRollableTable", + "oracle_type", + "OracleType3", + ["OracleTableText", "OracleTableText2", "OracleTableText3"], + ), +] + + +def _rewrite_discriminated_union_bases(source: str) -> tuple[str, int]: + """Replace each broken empty discriminated-union base with a proper + `Annotated[Union[...], Field(discriminator=…)]` alias. + + Uses string forward references for subtype names since the concrete + subclass definitions come later in the generated file. + """ + hits = 0 + for base, disc, disc_type, subtypes in _DISCRIMINATED_UNIONS: + # Expected shape of the broken base: + # class Move(BaseModel): + # model_config = ConfigDict( + # extra='allow', + # ) + # roll_type: RollType + # + # No other fields — that's what makes it the broken pattern rather than + # a legit base with `extra='allow'` (like SourceInfo, Ruleset, etc.). + expected = ( + f"class {base}(BaseModel):\n" + f" model_config = ConfigDict(\n" + f" extra='allow',\n" + f" )\n" + f" {disc}: {disc_type}\n" + ) + if expected not in source: + continue + union_members = ", ".join(f'"{s}"' for s in subtypes) + replacement = ( + f"{base}: TypeAlias = Annotated[\n" + f" Union[{union_members}],\n" + f' Field(discriminator="{disc}"),\n' + f"]\n" + ) + source = source.replace(expected, replacement, 1) + hits += 1 + return source, hits + + +def _ensure_union_import(source: str) -> str: + """Make sure `Union` is imported from `typing` (used by the discriminated + union rewrite; may already be imported or not depending on the schema). + """ + typing_import_re = re.compile(r"^from typing import (.+)$", re.MULTILINE) + match = typing_import_re.search(source) + if not match: + return source + names = [n.strip() for n in match.group(1).split(",")] + if "Union" in names: + return source + names.append("Union") + names.sort() + new_line = "from typing import " + ", ".join(names) + return source[: match.start()] + new_line + source[match.end() :] + + # --- driver ----------------------------------------------------------------- @@ -179,14 +405,23 @@ def main(argv: list[str]) -> int: after_date, date_hits = _strip_date_patterns(original) after_features, feature_hits = _rewrite_stub_fields(after_date) - - if after_features == original: + after_aliases, alias_hits = _rewrite_root_model_str(after_features) + if alias_hits > 0: + after_aliases = _ensure_type_alias_import(after_aliases) + after_unions, union_hits = _rewrite_discriminated_union_bases(after_aliases) + if union_hits > 0: + after_unions = _ensure_type_alias_import(after_unions) + after_unions = _ensure_union_import(after_unions) + + if after_unions == original: print(f"{path}: no changes (already post-processed)") return 0 - path.write_text(after_features, encoding="utf-8") + path.write_text(after_unions, encoding="utf-8") print( f"{path}: stripped {date_hits} date-field pattern(s), " + f"converted {alias_hits} RootModel[str] wrapper(s) to TypeAlias, " + f"resolved {union_hits} discriminated-union base(s), " f"rewrote {feature_hits} Features/Dangers stub site(s)" ) return 0 diff --git a/tests/test_type_alias_ergonomics.py b/tests/test_type_alias_ergonomics.py new file mode 100644 index 0000000..270e6b5 --- /dev/null +++ b/tests/test_type_alias_ergonomics.py @@ -0,0 +1,88 @@ +"""Test that ID fields read as plain strings after post-processing. + +Before `scripts/post_process_models.py` converts `RootModel[str]` wrappers to +`TypeAlias` (with Annotated pattern preserved), consumers had to write +`rules.id.root` to get the actual string — because `id` was a wrapper object. +These tests lock in the ergonomic path so future models.py regenerations don't +silently regress. +""" + +import importlib +import json +from pathlib import Path + +import pytest +from datasworn.core.models import Ruleset + + +def _load_ruleset(package_name: str) -> Ruleset: + package = importlib.import_module(f"datasworn.{package_name}") + json_file = Path(package.__file__).parent / "json" / f"{package_name}.json" + with json_file.open() as f: + return Ruleset.model_validate(json.load(f)) + + +class TestRulesetIdErgonomics: + """IDs should behave like strings, not RootModel wrappers.""" + + def test_ruleset_id_is_string(self): + rules = _load_ruleset("starforged") + assert isinstance(rules.id, str) + assert rules.id == "starforged" + assert rules.id.startswith("star") + assert len(rules.id) == 10 + + def test_move_id_is_string(self): + rules = _load_ruleset("starforged") + assert rules.moves is not None + + first_category = next(iter(rules.moves.values())) + assert first_category.contents is not None + first_move = next(iter(first_category.contents.values())) + + assert isinstance(first_move.id, str) + assert first_move.id.startswith("move:") + # String operations should work directly + parts = first_move.id.split("/") + assert len(parts) >= 2 + + def test_oracle_id_is_string(self): + rules = _load_ruleset("starforged") + assert rules.oracles is not None + + for collection in rules.oracles.values(): + if collection.contents: + for oracle in collection.contents.values(): + assert isinstance(oracle.id, str) + assert oracle.id.startswith("oracle") + return + pytest.skip("No oracle found in starforged") + + def test_asset_id_is_string(self): + rules = _load_ruleset("starforged") + assert rules.assets is not None + + first_collection = next(iter(rules.assets.values())) + assert first_collection.contents is not None + first_asset = next(iter(first_collection.contents.values())) + + assert isinstance(first_asset.id, str) + assert first_asset.id.startswith("asset:") + # Should be usable in f-strings directly, without .root + message = f"Loading asset: {first_asset.id}" + assert "asset:" in message + + def test_markdown_string_reads_as_str(self): + """MarkdownString is another RootModel[str] type that should now + behave like a plain string on read. + """ + rules = _load_ruleset("starforged") + assert rules.moves is not None + + first_category = next(iter(rules.moves.values())) + assert first_category.contents is not None + first_move = next(iter(first_category.contents.values())) + + if first_move.text: + assert isinstance(first_move.text, str) + assert len(first_move.text) > 0