Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 30 additions & 21 deletions src/adcp/types/_legacy_assets.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,46 @@
"""Pure, opt-in normalization of pre-v3 creative asset dictionaries."""

from collections.abc import Mapping
from typing import Any
from functools import cache
from typing import Any, get_args

_KNOWN_ASSET_TYPES = frozenset(
{
"image",
"video",
"audio",
"vast",
"text",
"url",
"html",
"javascript",
"webhook",
"css",
"daast",
"markdown",
"brief",
"catalog",
}
)
from pydantic import RootModel


@cache
def _known_asset_types() -> frozenset[str]:
"""Every ``asset_type`` the pinned schema's canonical asset union routes.

Derived from the generated ``AssetVariant`` arms rather than listed here,
so a schema bump that adds an asset type (as 3.2 added ``display_tag``)
extends key-name inference in the same regeneration. Imported on first use:
the generated asset graph is not needed to import the legacy helpers.
"""
from adcp.types.domains.core.assets.asset_union import AssetVariant

union = get_args(AssetVariant)[0]
types: set[str] = set()
for arm in get_args(union):
# A multi-shape asset (``vast``, ``display_tag``, ...) is a RootModel
# over per-shape classes that share one ``asset_type`` literal.
shapes = (
get_args(arm.model_fields["root"].annotation) if issubclass(arm, RootModel) else (arm,)
)
for shape in shapes:
types.update(get_args(shape.model_fields["asset_type"].annotation))
return frozenset(types)


def infer_asset_type(asset_key: str, asset: Mapping[str, Any]) -> str | None:
"""Infer a missing discriminator from an exact key, then field presence.

Type-name keys take precedence over fields. Otherwise URL plus both
Keys naming any asset_type the pinned schema declares (``display_tag``,
``card``, ...) take precedence over fields. Otherwise URL plus both
dimensions implies image, URL alone implies url, and content implies text.
Unknown keys without those fields return None for validation to diagnose.
This function does not inspect or replace an existing discriminator.
"""
if asset_key in _KNOWN_ASSET_TYPES:
if asset_key in _known_asset_types():
return asset_key
if "url" in asset:
return "image" if "width" in asset and "height" in asset else "url"
Expand Down
23 changes: 23 additions & 0 deletions tests/test_legacy_asset_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,29 @@ def test_rules_match_both_adapters(key, asset, inferred, normalized):
assert asset == original


def test_display_tag_key_is_inferred_as_display_tag():
# #1624: a 3.2 display_tag asset was inferred as text from its content.
tag = "<script src='https://ads.example/tag.js'></script>"
assert coerce_legacy_assets({"display_tag": {"content": tag}}) == {
"display_tag": {"content": tag, "asset_type": "display_tag"}
}
assert coerce_legacy_assets({"display_tag": {"url": "https://ads.example/tag"}}) == {
"display_tag": {"url": "https://ads.example/tag", "asset_type": "display_tag"}
}


def test_key_inference_covers_every_asset_type_the_schema_declares():
from adcp.types.domains.core import format as format_module

# The format slot union derives its own vocabulary from the same pin; the
# creative-asset vocabulary key inference uses must not lag behind it.
declared = format_module._KNOWN_BASE_INDIVIDUAL_ASSET_TYPES
assert "display_tag" in declared
for asset_type in declared:
assert infer_asset_type(asset_type, {"content": "x"}) == asset_type
assert infer_asset_type(asset_type, {"url": "https://cdn.example/a"}) == asset_type


def test_public_reexports_and_mapping_immutability():
assert legacy.infer_asset_type is infer_asset_type
assert legacy.coerce_legacy_asset is coerce_legacy_asset
Expand Down
Loading