Writing Models¶
Models define the structure of your YAML configuration and provide validation. embgen uses Pydantic for data validation.
Base Configuration¶
All domain configurations must ultimately provide:
name— The human-readable nameoutput_filename— The base filename for generated files
You can either extend BaseConfig or implement these yourself:
from embgen.models import BaseConfig
class MyConfig(BaseConfig):
"""Configuration for my domain.
Inherits:
name: str
file: str | None (optional output filename override)
output_filename: property (returns file or lowercase name)
"""
# Add your domain-specific fields
items: list[str] = []
Defining Models¶
Basic Model¶
# models.py
from typing import Optional
from pydantic import BaseModel
from embgen.models import BaseConfig
class MyConfig(BaseConfig):
"""Top-level configuration."""
description: Optional[str] = None
version: str = "1.0"
items: list["Item"] = []
class Item(BaseModel):
"""An item in the configuration."""
name: str
value: int
enabled: bool = True
This validates YAML like:
name: MyProject
description: Example project
version: "2.0"
items:
- name: foo
value: 42
- name: bar
value: 100
enabled: false
Using Enums¶
from enum import StrEnum
from pydantic import BaseModel
class ItemType(StrEnum):
SIMPLE = "simple"
COMPLEX = "complex"
class Item(BaseModel):
name: str
type: ItemType = ItemType.SIMPLE
YAML usage:
Nested Enumerations¶
For enums that are defined in YAML (not code), use the shared Enum model:
YAML usage:
items:
- name: status
values:
- { name: OK, value: 0, description: "Success" }
- { name: ERROR, value: 1, description: "Failure" }
Validation¶
Field Validators¶
Use Pydantic validators for custom validation logic:
from pydantic import BaseModel, field_validator
class Item(BaseModel):
name: str
value: int
@field_validator("value")
@classmethod
def value_must_be_positive(cls, v: int) -> int:
if v < 0:
raise ValueError("value must be positive")
return v
Cross-Field Validation¶
from pydantic import BaseModel, model_validator
class Config(BaseModel):
min_value: int
max_value: int
@model_validator(mode="after")
def check_range(self) -> "Config":
if self.min_value >= self.max_value:
raise ValueError("min_value must be less than max_value")
return self
Default Value Transformation¶
from typing import Any
from pydantic import BaseModel, field_validator, ValidationInfo
class Argument(BaseModel):
name: str
default: int | str | None = None
enums: list[Enum] | None = None
@field_validator("default", mode="before")
@classmethod
def resolve_enum_default(cls, v: Any, info: ValidationInfo) -> Any:
"""Convert string default to Enum if enums are defined."""
enums = info.data.get("enums")
if enums and isinstance(v, str):
for enum in enums:
if enum.name == v:
return enum
return v
Computed Fields¶
Add properties that are computed from other fields:
from pydantic import BaseModel, computed_field
class Argument(BaseModel):
name: str
type: str # e.g., "B", "H", "I"
@computed_field
def type_size(self) -> int:
"""Get size in bytes for the type."""
sizes = {"B": 1, "H": 2, "I": 4, "Q": 8}
return sizes.get(self.type, 0)
@computed_field
def type_python(self) -> str:
"""Get Python type name."""
types = {"B": "int", "H": "int", "f": "float", "?": "bool"}
return types.get(self.type, "Any")
These computed fields are available in templates:
Complete Example¶
Here's a complete model for a protocol domain:
# models.py
"""Data models for the protocol domain."""
from enum import StrEnum
from typing import Optional
from pydantic import BaseModel, Field, computed_field
from embgen.models import BaseConfig, Enum
class MessageType(StrEnum):
"""Message type enumeration."""
REQUEST = "request"
RESPONSE = "response"
NOTIFICATION = "notification"
class Field(BaseModel):
"""A field within a message."""
name: str
description: Optional[str] = None
type: str # e.g., "uint8", "uint16", "string"
optional: bool = False
enums: Optional[list[Enum]] = None
@computed_field
def c_type(self) -> str:
"""Get C type for this field."""
type_map = {
"uint8": "uint8_t",
"uint16": "uint16_t",
"uint32": "uint32_t",
"int8": "int8_t",
"int16": "int16_t",
"int32": "int32_t",
"string": "char*",
"bool": "bool",
}
return type_map.get(self.type, "void*")
class Message(BaseModel):
"""A protocol message."""
name: str
id: int
type: MessageType = MessageType.REQUEST
description: Optional[str] = None
fields: list[Field] = Field(default_factory=list)
class ProtocolConfig(BaseConfig):
"""Top-level protocol configuration."""
version: str = "1.0"
namespace: Optional[str] = None
messages: list[Message]
@property
def requests(self) -> list[Message]:
"""Get all request messages."""
return [m for m in self.messages if m.type == MessageType.REQUEST]
@property
def responses(self) -> list[Message]:
"""Get all response messages."""
return [m for m in self.messages if m.type == MessageType.RESPONSE]
Example YAML:
name: MyProtocol
version: "2.0"
namespace: myproto
messages:
- name: Connect
id: 1
type: request
description: "Connect to the device"
fields:
- name: device_id
type: uint32
description: "Device identifier"
- name: timeout_ms
type: uint16
optional: true
- name: ConnectResponse
id: 2
type: response
fields:
- name: status
type: uint8
enums:
- { name: OK, value: 0 }
- { name: ERROR, value: 1 }
- { name: TIMEOUT, value: 2 }