from enum import StrEnum, auto
from typing import Annotated, Literal, Self, override
from pydantic import Field as PydanticField
from pydantic import (
model_validator,
)
from erc7730.model.base import Model
ArrayIndex = Annotated[
int,
PydanticField(
title="Array index",
description="Index of an element in an array. An index can be negative to count from the end of the array.",
ge=-32767, # TODO to be refined
le=32768, # TODO to be refined
),
]
[docs]
class Field(Model):
"""A path component designating a field in a structured data schema."""
type: Literal["field"] = PydanticField(
"field",
title="Path Component Type",
description="The path component type identifier (discriminator for path components discriminated union).",
)
identifier: str = PydanticField(
title="Field Identifier",
description="The identifier of the referenced field in the structured data schema.",
pattern=r"^[a-zA-Z0-9_]+$",
)
@override
def __str__(self) -> str:
return self.identifier
[docs]
class ArrayElement(Model):
"""A path component designating a single element of an array."""
type: Literal["array_element"] = PydanticField(
"array_element",
title="Path Component Type",
description="The path component type identifier (discriminator for path components discriminated union).",
)
index: ArrayIndex = PydanticField(
title="Array Element",
description="The index of the element in the array. It can be negative to count from the end of the array.",
)
@override
def __str__(self) -> str:
return f"[{self.index}]"
[docs]
class ArraySlice(Model):
"""A path component designating an element range of an array (in which case, the path targets multiple values)."""
type: Literal["array_slice"] = PydanticField(
"array_slice",
title="Path Component Type",
description="The path component type identifier (discriminator for path components discriminated union).",
)
start: ArrayIndex | None = PydanticField(
default=None,
title="Slice Start Index",
description="The start index of the slice (inclusive). Must be lower than the end index. If unset, slice "
"starts from the beginning of the array.",
)
end: ArrayIndex | None = PydanticField(
default=None,
title="Slice End Index",
description="The end index of the slice (exclusive). Must be greater than the start index. If unset, slice "
"ends at the end of the array.",
)
@model_validator(mode="after")
def _validate(self) -> Self:
if (start := self.start) is None or (end := self.end) is None:
return self
if 0 <= end <= start or end <= start < 0:
raise ValueError("Array slice start index must be strictly lower than end index.")
return self
def __str__(self) -> str:
return f"[{'' if self.start is None else self.start}:{'' if self.end is None else self.end}]"
[docs]
class Array(Model):
"""A path component designating all elements of an array (in which case, the path targets multiple values)."""
type: Literal["array"] = PydanticField(
"array",
title="Path Component Type",
description="The path component type identifier (discriminator for path components discriminated union).",
)
@override
def __str__(self) -> str:
return "[]"
[docs]
class ContainerField(StrEnum):
"""
Path applying to the container of the structured data to be signed.
Such paths are prefixed with "@".
"""
VALUE = auto()
"""The native currency value of the transaction containing the structured data."""
FROM = auto()
"""The address of the sender of the transaction / signer of the message."""
TO = auto()
"""The destination address of the containing transaction, ie the target smart contract address."""
DataPathElement = Annotated[
Field | ArrayElement | ArraySlice | Array,
PydanticField(
title="Data Path Element",
description="An element of a data path, applying to the structured data schema (ABI path for contracts, path"
"in the message types itself for EIP-712)",
discriminator="type",
),
]
DescriptorPathElement = Annotated[
Field | ArrayElement,
PydanticField(
title="Descriptor Path Element",
description="An element of a descriptor path, applying to the current file describing the structured data"
"formatting, after merging with includes.",
discriminator="type",
),
]
[docs]
class ContainerPath(Model):
"""
Path applying to the container of the structured data to be signed.
Such paths are prefixed with "@".
"""
type: Literal["container"] = PydanticField(
"container",
title="Path Type",
description="The path type identifier (discriminator for paths discriminated union).",
)
field: ContainerField = PydanticField(
title="Container field",
description="The referenced field in the container, only some well-known values are allowed.",
)
@override
def __str__(self) -> str:
return f"@.{self.field}"
[docs]
class DataPath(Model):
"""
Path applying to the structured data schema (ABI path for contracts, path in the message types itself for
EIP-712).
A data path can reference multiple values if it contains array elements or slices.
Such paths are prefixed with "#".
"""
type: Literal["data"] = PydanticField(
"data", title="Path Type", description="The path type identifier (discriminator for paths discriminated union)."
)
absolute: bool = PydanticField(
title="Absolute",
description="Whether the path is absolute (starting from the structured data root) or relative (starting from"
"the current field).",
)
elements: list[DataPathElement] = PydanticField(
title="Elements",
description="The path elements, as a list of references to be interpreted left to right from the structured"
"data root to reach the referenced value(s).",
)
@override
def __str__(self) -> str:
return f'{"#." if self.absolute else ""}{".".join(str(e) for e in self.elements)}'
@override
def __hash__(self) -> int:
return hash(str(self))
[docs]
class DescriptorPath(Model):
"""
Path applying to the current file describing the structured data formatting, after merging with includes.
A descriptor path can only reference a single value in the document.
Such paths are prefixed with "$".
"""
type: Literal["descriptor"] = PydanticField(
"descriptor",
title="Path Type",
description="The path type identifier (discriminator for paths discriminated union).",
)
elements: list[DescriptorPathElement] = PydanticField(
title="Elements",
description="The path elements, as a list of references to be interpreted left to right from the current file"
"root to reach the referenced value.",
)
@override
def __str__(self) -> str:
return f'$.{".".join(str(e) for e in self.elements)}'
@override
def __hash__(self) -> int:
return hash(str(self))
ROOT_DATA_PATH = DataPath(absolute=True, elements=[])
ROOT_DESCRIPTOR_PATH = DescriptorPath(elements=[])