Skip to main content

Mold

The mold package is a struct transformation engine that modifies field values based on struct tags. It operates at both field and struct levels.

How It Works

The mold tag on struct fields triggers transformation functions. CRUD query actions run the transformer on find_one, find_all, find_page, find_tree, and export results before they are returned, so response models can expose derived or translated fields.

Built-in: Code Set Translation

The built-in translate transformer resolves a source field through registered Translator implementations and writes the result to a sibling <Field>Name field. The framework ships one built-in translator: CodeSetTranslator, which handles only codes: kinds such as mold:"translate=codes:status".

Naming note: the vocabulary is "code set", not "dictionary". The tag prefix is codes: (there is no dict: prefix), and the identifiers are CodeSet*, not Dictionary* (see the mapping table below).

type Order struct {
Status string `json:"status" mold:"translate=codes:status"`
StatusName string `json:"statusName" bun:",scanonly"`
}

When a query result contains Status = "active", the transformer asks the code set resolver for code set status and code active, then writes the display name to StatusName.

Auditing models such as orm.FullAuditedModel use mold:"translate=user?" on CreatedBy and UpdatedBy. That tag is an optional hook for a custom user translator; it is not provided by the built-in code set translator.

Interfaces

Transformer

type Transformer interface {
Struct(ctx context.Context, value any) error
Field(ctx context.Context, value any, tags string) error
}

Transformer.Struct requires a non-nil pointer to a struct. Passing a nil value, a non-pointer, a nil pointer, a pointer to a non-struct, or a time.Time value returns an error. Transformer.Field requires a non-nil pointer unless the tag string is empty or "-", in which case it is a no-op.

FieldTransformer

Implement custom field-level transformations:

type FieldTransformer interface {
Tag() string
Transform(ctx context.Context, fl FieldLevel) error
}

StructTransformer

Implement custom struct-level transformations:

type StructTransformer interface {
Transform(ctx context.Context, sl StructLevel) error
}

Interceptor

Redirect transformation to inner values (e.g., sql.NullString → its inner string):

type Interceptor interface {
Intercept(current reflect.Value) (inner reflect.Value)
}

FieldLevel API

Inside a field transformer, FieldLevel provides:

MethodReturnsPurpose
Transformer()TransformerAccess the parent transformer
Name()stringCurrent field name
Parent()reflect.ValueParent struct value
Field()reflect.ValueCurrent field value
Param()stringParameter from tag (e.g., user? in translate=user?)
SiblingField(name)reflect.Value, boolAccess sibling field by name
Struct()reflect.ValueStruct that contains the current field; may be invalid when transforming a standalone field

StructLevel exposes Transformer(), Parent(), and Struct() for struct-level transformers.

Function adapters are also public:

AdapterPurpose
mold.Funcuse a plain function as a field transformer implementation
mold.StructLevelFuncuse a plain function for struct-level transformation
mold.InterceptorFuncuse a plain function as an Interceptor

Tag Format

mold:"function=param"

Multiple transformations:

mold:"function1=param1,function2=param2"

mold:"-" skips a field. dive recurses into slice, array, or map values. For maps, dive,keys,...,endkeys,... applies the tags between keys and endkeys to map keys and the remaining tags to map values. Nested struct fields are traversed automatically, but slice and map elements are transformed only when dive is present. Commas inside a parameter must be escaped as 0x2C.

Built-in: Expression-Derived Fields

The core runtime registers an expr field transformer backed by expression.Engine. It evaluates the expression against the containing struct and decodes the result into the tagged field:

type LineItem struct {
Price float64 `json:"price"`
Qty float64 `json:"qty"`
Total float64 `json:"total" mold:"expr=price * qty"`
}

Fields are evaluated in declaration order, so derived fields can reference sibling fields declared above them. If an expression contains a comma, escape it as 0x2C inside the mold tag. See Expression Engine for the full API.

The expr tag is provided by the expression module through the vef:mold:field_transformers group. It is not provided by the mold module alone. The mold module itself contributes the built-in translate field transformer and the CodeSetTranslator; other field transformers must be registered through the same group or by constructing a custom transformer.

Code Set Resolution

The translate transformer resolves field values through the Translator interface. The framework ships one built-in translator — CodeSetTranslator — that handles kind strings prefixed with codes: (for example, mold:"translate=codes:gender"). If the kind is codes:status?, the built-in translator still supports the full string and resolves code set key status?; it does not strip the ? suffix.

Supported source field shapes are string, *string, signed and unsigned integer types, pointers to those integer types, []string, and *[]string after mold dereferencing. Scalar targets must be string or *string; slice targets must be []string or *[]string. The target field is always the source field name plus Name (<Field>Name). Empty scalar values are skipped, nil source slices leave the target untouched, and empty source slices write an empty target slice.

Custom translators implement:

type Translator interface {
Supports(kind string) bool
Translate(ctx context.Context, kind, value string) (string, error)
}

The code-set resolver and loader interfaces:

type CodeSetResolver interface {
Resolve(ctx context.Context, codeSet, code string) (string, error)
}

type CodeSetLoader interface {
Load(ctx context.Context, codeSet string) (map[string]string, error)
}

CodeSetLoaderFunc lets a plain function satisfy CodeSetLoader.

Enumerable catalog (optional)

A host whose code sets are enumerable can additionally implement mold.CodeSetInspector alongside its loader (or wholesale-replaced resolver):

type CodeSetInspector interface {
ListCodeSets(ctx context.Context) ([]CodeSetInfo, error) // {codeSet, name}
ListCodes(ctx context.Context, codeSet string) ([]CodeInfo, error) // {code, label}
}

CodeSetInfo has fields CodeSet and Name. CodeInfo has fields Code and Label (CodeInfo.Code is the canonical code value, CodeInfo.Label is its display name).

Consumers type-assert for it and degrade gracefully when it is absent. The integration module uses it to power the code-mapping editor's pickers and to validate code map identifiers.

What ? actually means

The ? suffix in mold:"translate=user?" makes the lookup silently skip when no translator supports the full kind string. If a translator matches but its Translate call returns an error, the error is still propagated — the ? is not a "swallow all errors" switch.

So translate=user? requires that you register a custom Translator whose Supports("user?") returns true if you want it to run. Without one, the field is left untouched and no error is returned. A required kind such as translate=user returns an error when no translator supports it.

Cached Resolution

CachedCodeSetResolver wraps a CodeSetLoader (not a CodeSetResolver) with in-process caching, and subscribes to mold.CodeSetChangedEvent for invalidation:

resolver := mold.NewCachedCodeSetResolver(loader, bus)

NewCachedCodeSetResolver panics if the CodeSetLoader or event.Bus is nil. The cache holds entire code sets keyed by the loader's code set and merges concurrent loads for the same key. Resolve returns an empty string without error for an empty code set, an empty code, or a code that is not present in the loaded set.

When the data underlying a code set changes, publish mold.CodeSetChangedEvent{Keys: []string{"..."}} through the event bus to invalidate the matching cache entry.

You can publish the same event through the helper:

err := mold.PublishCodeSetChangedEvent(ctx, bus, "gender", "status")

CodeSetChangedEvent.EventType() returns the framework event type used by the cache invalidation subscriber (vef.translate.code_set.changed).

Calling PublishCodeSetChangedEvent(ctx, bus) without keys asks subscribers to clear their entire code set cache.

The public APIs in this cache path are CachedCodeSetResolver, CodeSetChangedEvent, CodeSetChangedEvent.Keys, PublishCodeSetChangedEvent, and CachedCodeSetResolver.Resolve, which implements CodeSetResolver.Resolve.

Former "dictionary" names

If you are looking for a Dictionary* identifier, its current CodeSet* equivalent is:

Former identifierCurrent identifier
tag mold:"translate=dict:xxx"mold:"translate=codes:xxx"
DictionaryTranslatorCodeSetTranslator
DictionaryResolverCodeSetResolver
DictionaryLoader / DictionaryLoaderFuncCodeSetLoader / CodeSetLoaderFunc
CachedDictionaryResolver / NewCachedDictionaryResolverCachedCodeSetResolver / NewCachedCodeSetResolver
DictionaryChangedEvent / PublishDictionaryChangedEventCodeSetChangedEvent / PublishCodeSetChangedEvent