Validators & Constants#
Validators and shared constants are tightly coupled in this codebase:
regex validators reference character-class constants from
constants.py, and range/enum validators reference both range
constants and ChoiceSet definitions. This doc covers both.
File layout#
netbox_aci_plugin/validators.py: every regex validator instance and every function validator used byvalidators=[...].netbox_aci_plugin/constants.py: shared constants grouped by domain.
Regex validators: Required / Optional pairs#
The name validators ship in two variants:
- Required (
+): matches one or more characters. Pair withblank=Falsemodel fields. - Optional (
*): matches zero or more characters (i.e. accepts the empty string). Pair withblank=Truemodel fields so the schema and clients agree on what an empty value looks like.
ACIPolicyDescriptionValidator is a single optional-only validator (no
Required variant and no Required/Optional suffix in its name).
Both variants share regex character classes from constants.py
(NAME_CHAR_CLASS, DESC_CHAR_CLASS); don't inline regex literals in
the validator file.
from django.core.validators import RegexValidator
from django.utils.translation import gettext_lazy as _
from .constants import DESC_CHAR_CLASS, NAME_CHAR_CLASS
# Required (1+)
ACIPolicyNameRequiredValidator = RegexValidator(
regex=rf"^{NAME_CHAR_CLASS}+$",
message=_(
"Only alphanumeric characters, periods, underscores, colons and "
"hyphens are allowed."
),
code="invalid",
)
# Optional (0+): use on blank=True fields to align schema and clients
ACIPolicyNameOptionalValidator = RegexValidator(
regex=rf"^{NAME_CHAR_CLASS}*$",
message=_(
"Only alphanumeric characters, periods, underscores, colons and "
"hyphens are allowed."
),
code="invalid",
)
Naming: <Domain><Thing><Required|Optional>Validator (PascalCase,
suffix Validator). Required and Optional variants always share the
same error message; the user-facing string doesn't mention
required-vs-optional, only the allowed character set.
Function validators#
Function validators perform range + enum-fallback checks. They go
directly into validators=[...] lists on the model field:
def validate_contract_filter_ip_protocol(value: str) -> None:
"""Validate the IP protocol value for ContractFilterEntry."""
if value in dict(ContractFilterIPProtocolChoices) or value in [
str(i) for i in range(0, 256)
]:
return
try:
number = int(value)
if 0 <= number <= 255:
return
except (ValueError, TypeError):
pass
valid_choices = ", ".join(dict(ContractFilterIPProtocolChoices).keys())
raise ValidationError(
_(
"IP Protocol must be a number between 0 and 255 or"
" one of the following values: {valid_choices}"
).format(valid_choices=valid_choices)
)
Conventions:
- Name:
validate_<domain>_<thing>(value)(snake_case, prefixvalidate_). - Accept either an enum choice or a numeric value in a range.
- On failure, the error message lists the valid choices via
", ".join(dict(<Choices>).keys()). The user shouldn't have to dig through code to discover the allowed values.
Some function validators check value combinations and do not follow the
range + choice shape. validate_contract_filter_tcp_rules
(validators.py) validates that established and unspecified are not
combined with other TCP flags; it has no numeric range and lists no
choices.
Keep f-strings out of _()#
Do not put f-strings inside translation calls. Eager f-strings are
evaluated before the translation lookup runs, so the source msgid
becomes the formatted string and translators cannot match it. Format
after translation instead:
# Bad: f-string evaluates before translation
_(
f"IP Protocol must be a number between 0 and 255 or one of: "
f"{valid_choices}"
)
# Good: translate first, then format
_(
"IP Protocol must be a number between 0 and 255 or one of: {valid_choices}"
).format(valid_choices=valid_choices)
constants.py organization#
Section grouping by domain, with section comments:
#
# Validation
#
ACI_NAME_MAX_LEN: Final[int] = 64
ACI_DESC_MAX_LEN: Final[int] = 128
NAME_CHAR_CLASS: Final[str] = r"[A-Za-z0-9_.:-]"
DESC_CHAR_CLASS: Final[str] = r"[A-Za-z0-9!#$%()*,-./:;@ _{|}~?&+]"
VLAN_VID_MIN: Final[int] = 1
VLAN_VID_MAX: Final[int] = 4094
FABRIC_ID_MIN: Final[int] = 1
FABRIC_ID_MAX: Final[int] = 128
# ...
#
# Contract Relation
#
CONTRACT_RELATION_OBJECT_TYPES = Q(...)
#
# Endpoint Security Group
#
ESG_ENDPOINT_GROUP_SELECTORS_MODELS = Q(...)
ESG_ENDPOINT_SELECTORS_MODELS = Q(...)
Primitive constants#
typing.Finalis applied to the length and char-class constants:ACI_NAME_MAX_LEN: Final[int] = 64,ACI_DESC_MAX_LEN: Final[int] = 128,NAME_CHAR_CLASS: Final[str],DESC_CHAR_CLASS: Final[str](constants.py:13-17).- Range constants follow
<DOMAIN>_<PROPERTY>_<MIN|MAX>:VLAN_VID_MIN,NODE_ID_MAX,FABRIC_ID_MIN. These also carryFinal[int], same as the length constants above (constants.py:19-29).
Q-object content-type filters#
Q objects encoding limit_choices_to predicates for Generic Foreign
Keys live in constants.py. Naming:
<MODEL>_<RELATION>_OBJECT_TYPES: the set of valid target models for a given GFK relation. Example:CONTRACT_RELATION_OBJECT_TYPES.<MODEL>_OBJECT_TYPES: the set of valid target models for a GFK that spans multiple object kinds with no named relation. Example:NODE_OBJECT_TYPES(constants.py, used atnodes.py:64).<MODEL>_<USAGE>_MODELS: the set of valid models for a specific selector or attribute usage. Example:ESG_ENDPOINT_GROUP_SELECTORS_MODELS,USEG_NETWORK_ATTRIBUTES_MODELS.
CONTRACT_RELATION_OBJECT_TYPES = Q(
app_label="netbox_aci_plugin",
model__in=(
"aciendpointgroup",
"aciendpointsecuritygroup",
"aciexternalendpointgroup",
"aciusegendpointgroup",
"acivrf",
),
)
These are referenced from:
- Model field
limit_choices_to=CONTRACT_RELATION_OBJECT_TYPESon the GFK's_typeForeignKey(to="contenttypes.ContentType", ...). - The
_validate_generic_uniqueness()helper inUniqueGenericForeignKeyMixin(see Models - Generic Foreign Key pattern).