Skip to content

feat: type coercion support in Annotated metadata (Coerce) #14

@fporcari

Description

@fporcari

Summary

The builder validation system supports type checking and validators (Regex, Range) via Annotated metadata, but has no way to transform values before validation. This is needed for polymorphic type acceptance — e.g., date fields that should accept both datetime.date objects and ISO date strings, normalizing to date.

Proposal

Add a Coerce base class alongside existing validators:

from typing import Annotated
from datetime import date

@element(sub_tags='')
def evento(self, data: Annotated[date, DateCoerce()] = None): ...

# All work:
bag.evento(data=date(2024, 1, 15))     # date passthrough
bag.evento(data='2024-01-15')           # str → date coercion
bag.evento(data='2024-01-15::D')        # typed string → date coercion

How it works

  • Coerce subclasses are frozen dataclasses with __call__ (same pattern as Regex/Range)
  • Unlike validators (raise on failure, return None), coercers return the transformed value
  • They live in the same Annotated metadata list — no change to the (base_type, validators, default) tuple format
  • _validate_call_args partitions metadata into coercers (isinstance(m, Coerce)) and validators at runtime
  • Execution order: coercion → type check → validation
  • Coerced values are written back to the attr dict, propagating to the Bag node

Changes needed in builder.py

  1. Add Coerce base class after Range (~line 414)
  2. Modify _validate_call_args to partition metadata and run coercers before type-check
  3. One-line fix in wrapper to propagate coerced node_value
  4. Export Coerce from __init__.py

Motivation

ERPY builders have ~10 date fields across erpy_db_op_builder.py, mag_builder.py, and coge_builder.py currently typed as str. Users want to pass Python date objects (preferred) or ISO strings interchangeably. The same pattern applies to Decimal, int, bool, time, datetime fields.

Notes

  • 100% backward compatible — without coercers in metadata, behavior is identical
  • Generalizable: concrete coercer implementations can live outside genro-builders (e.g., in ERPY using GnrClassCatalog)
  • The _split_annotated function already extracts all callables from Annotated — no change needed there

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions