typesafe

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 17, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package typesafe resolves a gsxmail template's declared props struct with go/types and type-checks the email dialect's expression grammar against the resolved fields. It replaces render-time reflection for everything it can decide at Load time: which props fields exist, what type each one has, and whether an expression is in the email dialect at all. It never touches props values — only types and expression source text — so it has no render-time role; doc.Resolve's reflection-based evaluator keeps doing that job, unchanged, as the second, render-time layer of the same fail-closed guarantee.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func BareFieldName

func BareFieldName(src, propsName string) (string, bool)

BareFieldName reports whether src is exactly a bare props field read (propsName.Field, with no other operator), and if so, the field's name. EM013's message needs this fast path: a bare read of a non-scalar field gets the specific "has type %s; ... format structs before rendering" message, rather than the generic EM010 catch-all a nested occurrence would fall through to.

func CheckExpr

func CheckExpr(src, templateName, propsName string, props *Props, bindings map[string]Binding, helpers map[string]any) (Kind, *Violation)

CheckExpr type-checks src — a raw Go expression, as recorded on an ir.Attr's Expr field or an ir.Node's NodeExpr Text — against the v1 email dialect: props field reads, the loop bindings an enclosing <Each as="name"> introduces (including one level of field reads on a struct-element binding, "row.Cells"), string/int/float literals, string + concatenation, comparisons on scalars, len(), and calls to a helper registered in helpers. templateName names the template being checked (EM012's message); propsName is its declared props parameter name. props is nil when the template declares no resolvable props type, in which case every props field read reports EM012. bindings maps a loop binding's name to its element Binding; pass nil outside an <Each> body. CheckExpr returns the expression's resolved Kind and the first dialect violation found, if any — never both.

func CheckSlicePath

func CheckSlicePath(label, src, templateName, propsName string, props *Props, bindings map[string]Binding) (elem Binding, v *Violation)

CheckSlicePath type-checks src as a bare slice-valued path — the shape <Each of={...}>, <email.StatTable header={...}>, and <email.StatRow cells={...}> all require (EM032): a bare props or loop-binding field read whose resolved Kind is KindSlice. label names the attribute for the message, exactly as it should read inside "<%s> requires a slice or array props path" — "Each of" reproduces EM032's original, pinned wording for <Each>; StatTable and StatRow's callers pass their own tag and attribute name so the same rule reads correctly for header and cells too. CheckSlicePath returns the slice element's own Binding (so a caller such as <Each> can extend its own bindings map with it) and the first violation found, if any.

func ParseBareSelector

func ParseBareSelector(src string) (root, field string, ok bool)

ParseBareSelector parses src as a bare "root.field" reference, or a bare "root" identifier with no selector, and reports the two names. It takes no view on whether root is the props parameter or a loop binding — that judgment needs context (the props type, the active bindings) this parse step does not have; ResolveFieldPath and CheckSlicePath supply it. ok is false for anything else (a call, an index, a literal, and so on).

Types

type Binding

type Binding struct {
	Kind   Kind
	Fields *Props
}

Binding is what an <Each as="name"> loop variable resolves to for the rest of its body: name's own Kind (KindString for []string, KindOther or a scalar Kind for []T, ...), and, when the slice's element is a struct, that struct's own field map so the checker can validate name.Field reads the same way it validates props.Field reads. Fields is nil for a scalar-element loop (name has no fields to read).

type Field

type Field struct {
	Kind Kind
	// GoType is the field's declared Go type, printed relative to its own
	// package (so a same-package struct field prints as "Roster", not
	// "emails.Roster"), for EM013's and EM032's messages.
	GoType string
	// ElemKind and ElemType describe a KindSlice field's element type; they
	// are the zero value otherwise.
	ElemKind Kind
	ElemType string
	// ElemProps resolves ElemType's own fields when a KindSlice field's
	// element is itself a struct (nil otherwise, including for a scalar
	// element type). <Each as="row"> over such a field binds "row" to a
	// value whose own fields (row.Cells, row.IsKeystone, ...) this drives —
	// the email dialect's answer to "nested prop reads" for loop bodies,
	// scoped to one level of nesting.
	ElemProps *Props
}

Field is one resolved struct field.

func ResolveFieldPath

func ResolveFieldPath(root, field, propsName string, props *Props, bindings map[string]Binding) (Field, bool)

ResolveFieldPath resolves a bare selector's root/field pair (as ParseBareSelector returns them) against propsName and bindings: root == propsName reads props.field; root naming a bindings key reads that loop variable's own field — the binding participates in expression resolution inside the body. field == "" (a bare loop-variable reference with no selector, such as <Each of=... as="tag"> then {tag}) resolves to the binding's own Kind, with no further Fields — a scalar-element loop has nothing to select from. ok is false when root names neither the props parameter nor a known binding, or when the named field does not exist on whichever side matched.

type Kind

type Kind int

Kind is the resolved dialect type of a struct field or an expression, as far as the email dialect's rules care: which types may be interpolated (EM013) and which may back an <Each> loop (EM032).

const (
	KindUnknown Kind = iota
	KindString
	KindInt
	KindFloat
	KindBool
	KindSlice
	KindOther
)

func (Kind) IsScalar

func (k Kind) IsScalar() bool

IsScalar reports whether k is one of the four types the email dialect may interpolate into text (spec EM013): string, integer, float, or bool.

func (Kind) String

func (k Kind) String() string

String names k for diagnostic messages.

type Props

type Props struct {
	// Name is the struct's declared type name (EM012/EM013's "type %s").
	Name   string
	Fields map[string]Field
}

Props is a props struct go/types resolved from the *.go files beside a template's .gsx source.

type Resolver

type Resolver struct {
	// contains filtered or unexported fields
}

Resolver resolves named props struct types from the *.go files in a template directory, parsing and type-checking each directory's package at most once. A Resolver is not safe for concurrent use; Load builds one per call and discards it afterward.

func NewResolver

func NewResolver(fsys fs.FS) *Resolver

NewResolver returns a Resolver that reads Go source from fsys, with no known real directory backing it (NewResolverAt's own doc comment explains what that costs). Prefer NewResolverAt whenever fsys is backed by a real directory on disk — every gsxmail.Load caller that passes os.DirFS(dir) should pass dir along too, through Options.Dir.

func NewResolverAt

func NewResolverAt(fsys fs.FS, root string) *Resolver

NewResolverAt returns a Resolver that reads Go source from fsys, which is rooted at the real, on-disk directory root (typically the same dir string a caller passed to os.DirFS(dir) to build fsys in the first place). root may be relative or absolute; NewResolverAt makes it absolute internally.

Setting root matters because a props.go file that imports another package — the module gsxmail itself lives in, a third-party dependency, even the standard library — needs go/types' underlying source importer to resolve that import from a real filesystem path. Package fs.FS's virtual paths carry no such thing, so without root, that resolution falls back to interpreting the virtual path as relative to the process's own current working directory: it happens to work when CWD is the module root (or another directory a relative walk-up reaches it from) and fails everywhere else, with no clue why (this is exactly the failure mode `gsxmail check` on an importer-generated template hit outside its own module — see CHANGELOG.md for the full history). With root set, every parsed file's recorded position carries its real absolute path instead, so resolution no longer depends on the process's working directory at all.

This does not make resolution fully module-aware — go/importer's "source" mode still uses go/build's legacy, largely GOPATH-shaped package-finding logic, not go/packages' go-list-backed one — so a props.go importing a package that only a `replace` directive or a non-standard module layout makes reachable can still fail to resolve even with root set correctly. That residual case surfaces as EM192 (its real cause included), never as a misleading EM012, and is documented in the README's own "Props type resolution" section.

func (*Resolver) Resolve

func (r *Resolver) Resolve(dir, typeName string) (*Props, error)

Resolve returns the struct type named typeName declared among the *.go files in dir (a template's own directory: gsxmail templates are one fs.FS-loadable package per directory). typeName may carry a leading "*" (a pointer props parameter); it is stripped before lookup. An empty typeName, or a typeName that is not a plain identifier (a map type, a generic instantiation, and so on), returns (nil, nil): the caller then skips every Load-time field/type check for that template and relies on doc.Resolve's render-time reflection path alone.

type Violation

type Violation struct {
	Code    string
	Message string
}

Violation is one email-dialect rule an expression breaks: an EM code plus its exact spec-section-8 message. It carries no source position; package lint attaches File/Line/Col from the enclosing ir.Node, since neither ir.Attr nor an expression's own AST carries one gosx stamps.

func CheckInterpolation

func CheckInterpolation(src, templateName, propsName string, props *Props, bindings map[string]Binding, helpers map[string]any) *Violation

CheckInterpolation type-checks src as an attribute-hole or text-hole expression whose resolved value must be interpolatable text (spec EM013): a string, integer, float, or bool. It is CheckExpr plus that one additional rule, with a fast path for the common case — a bare props or loop-binding field read — so a non-scalar field gets EM013's specific message instead of the generic EM010 catch-all. bindings is the same loop-binding scope CheckExpr takes; pass nil outside an <Each> body.

func (*Violation) Error

func (v *Violation) Error() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL