Field Attributes & References
Introduction
In Go, a struct field carries metadata only as a string tag (`json:"title"`), read by reflection and never checked. And there is no way to name a field as a value — code that filters, sorts or queries by a field passes its name as a string ("Title", "author.name"), which the compiler can't check either.
Vuka adds typed attributes on fields, and Type.Field as a typed reference to a field. Both are general-purpose — forms, validation, sorting, JSON, in-memory filters, query builders — and plain Go underneath. See also the ORM, which declares its models and queries with them.
Declaring and Referencing Fields
This sample declares a Post with field attributes, then builds predicates and an ordering from field references:
type Post struct {
Timestamps
ID int64 @PK
Title string `json:"title"` @Char{Max: 200}
Author FK[User] @Rel{OnDelete: Cascade}
Views int @Default(0)
}
popular := vuka.And(Post.Title.Contains("go"), Post.Views.Gt(100))
byAda := Post.Author.Name.Eq("ada")
vuka.SortBy(posts, Post.Views.Desc())Field Attributes
An attribute goes at the end of its field, after the tag if there is one:
Title string `json:"title"` @Char{Max: 200} @IndexOne form only: an attribute before the tag, or on a line of its own inside the struct, is an error that shows the right place. Any number may follow a field, an embedded field included; A, B int @X gives both fields the attribute.
Attribute Values
An attribute is a Go value, type-checked like a declaration's attribute: @T{…} (a composite literal), @T (T's zero value), or @f(…) (a conversion or any call, such as @Default(0)). A wrong field or type is a compile error on the attribute.
The built-ins (@doc, @deprecated, @export) are for declarations. A generic type's fields carry attributes when it is a live component (it embeds ui.Live, or is a live shape): they are written into its LiveSchema method, a method of the generic type, so each instantiation has its own. Any other generic type's would be recorded for the type at package level, which needs its type arguments (E0206).
Reading Field Attributes
Attributes leave the struct — the Go type is exactly the fields and tags — and are recorded at init, for any struct that has them, with no type decorator needed. These functions read them:
vuka.FieldAttrs[T]() | map[string][]any, by field name; a model's include its Schema method's, its model attributes on the embedded orm.Model (vuka.FieldAttrsDeclarer) |
vuka.FieldsOf[T]() | []vuka.Field: name, type, tag, Attrs |
f.Attr(&x) | fills x with the field's attribute of x's type |
t.Fields[i].Attrs | the same, in a type decorator's *vuka.Type |
A type decorator runs after its type's attributes are recorded.
Schema-aware libraries record field attributes through their generated metadata. For live shapes, declare field attributes and decorated hooks directly in the .vuka.html header; see hooks.
Field References
Post.Title, where Post is a struct type and Title one of its fields, is a value: a typed reference to the field. Go has no meaning for it (only Type.Method is a method expression), so Vuka can give it one without ambiguity. Statics and method expressions keep theirs; a name that is both a field and a static reached through an embedded type is an error.
Following Relations and Embedded Fields
A reference goes on through the field's type:
- into a struct, or a pointer to one:
Order.Customer.Name; - through a type implementing
vuka.Related[U]—Related() *U— intoU, which is how a foreign-key type such asFK[User]letsPost.Author.Namereach the user's fields (Related wins over the type's own fields); - through embedded structs, as Go promotes fields:
Post.Createdfrom an embeddedTimestamps.
Reference Types
The reference's type depends on the last field's type, so only the predicates that make sense type-check:
| Field type | Reference | Adds |
|---|---|---|
| any | vuka.Ref[T, V] (RefTo) | Eq, Ne, In, Asc, Desc, Get, Lookup, Name, Path, Attrs, Attr |
| numbers | vuka.OrderedRef[T, V] (OrderedRefTo) | Lt, Le, Gt, Ge |
| strings | vuka.StringRef[T, V] (StringRefTo) | those, and Contains, StartsWith, EndsWith |
a Compare(V) int method (time.Time) | vuka.CompareRef[T, V] (CompareRefTo) | Lt, Le, Gt, Ge |
pointers, slices, maps, interfaces, Option | vuka.NullableRef[T, V] (NullableRefTo) | IsNil, NotNil |
T is the type the reference starts from, also after a relation: Post.Author.Name is a vuka.StringRef[Post, string].
An ORM model is different: its fields are the typed fields orm generate writes, so Post.Title is PostFields.Title, an orm.TextField[string], and its conditions are the engine's.
Predicates and Orderings
A predicate is data — a vuka.Pred[T] with an Op (vuka.OpEq, vuka.OpContains, …), the field's Path and the Value — combined with vuka.And, vuka.Or and vuka.Not. A query builder translates it; p.Match(t) evaluates it in memory, and p.String() writes it. This sample combines three predicates and prints the result:
q := vuka.Or(vuka.And(Post.Title.Contains("go"), Post.Views.Gt(100)), Post.Home.IsNil())
fmt.Println(q) // ((Title contains "go" AND Views > 100) OR Home IS NIL)
q.Match(post) // true or falseBecause a predicate is typed by T, an API taking vuka.Pred[Post] rejects a predicate on User at compile time. A comparison is false when a nil pointer or relation is on the way to its field (IsNil is true).
Asc() and Desc() give a vuka.Order[T]; vuka.SortBy(xs, orders…) sorts a slice by them, stably, the first order deciding.
p.Path (and ref.FieldPath()) is a vuka.FieldPath: Names() (["Author", "Name"]), Fields() (each step's reflect.StructField), Index(), Attrs() of the last field, and Value(root).
What It Becomes
A reference such as Post.Author.Name compiles to this:
vuka.StringRefTo(func(p *Post) *string { return &p.Author.Related().Name })The field by address, the call plain Go writes: the compiler checks the fields and the editor's hover, go to definition and rename land on them. RefTo runs the function once per instantiation, on a value of Post made for it, and finds the field the address is of; OrderedRefTo, StringRefTo, CompareRefTo and NullableRefTo are the other kinds.
An unknown field is an error with a suggestion (Post has no field Titel; did you mean Title?), as is an unexported field of another package's type, or a name more than one embedded struct supplies.
Compile errors — from vuka build, the editor or Vuka itself — quote a reference as the source writes it: cannot use Post.Views (value of struct type vuka.OrderedRef[Post, int]) as int value, as they quote statics as Post.Limit.
NOTE
References to the fields of standard-library types (http.Cookie.Name) are found only when the package is type-checked for another reason; references inside an attribute's arguments aren't lowered.