Skip to content

feat(xmlgen): built-in helper library and caller resolvers (2/4) - #201

Open
sthanikan2000 wants to merge 1 commit into
feature/xmlgenfrom
feature/xmlgen-helpers
Open

sthanikan2000 wants to merge 1 commit into
feature/xmlgenfrom
feature/xmlgen-helpers

Conversation

@sthanikan2000

@sthanikan2000 sthanikan2000 commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator

Problem

The engine in #199 renders and escapes, but a template still has to turn form data into the shapes a receiving system expects: a reference the document spreads across four elements, a date in another layout, an enum encoded as a number. With no vocabulary for that, every caller has to supply its own before rendering anything.

Changes

Nine built-in helpers — pure, deterministic, no configuration — plus an extension point for what the data cannot supply at all.

Helper Example Result
part {{ part .ref "/" 0 }} on "OFF1/A/42/2026" OFF1 — past the end gives "", not an error
split {{ range split .codes "," }} iterate the fields
join {{ join .tags "-" }} a-b-c
date {{ date .day "2006-01-02" "1/2/06" }} 3/4/26 — a non-matching layout is an error, never a guess
decimal {{ decimal .fob 2 }} on 1400 1400.00
lookup {{ lookup .flag "yes" "1" "no" "0" }} 1 — unmapped values pass through
zero {{ if zero .gain }} true for nil, "", false, numeric zero
coalesce {{ coalesce .a .b "n/a" }} first value present
trim {{ trim .name }} surrounding whitespace removed
type ResolveFunc func(ctx context.Context, args ...any) (any, error)
type Resolvers map[string]ResolveFunc

func WithResolvers(r Resolvers) Option
func Validate(tmpl []byte, resolverNames ...string) error
  • Shipping the helpers in the box is what keeps Generate(ctx, tmpl, data) sufficient for most templates.
  • Resolvers are for values needing I/O — a code-list description from a database. Each registers under its own name, so a template calling one that was not supplied fails when the template is parsed, not at execution if a branch happens to reach it.
  • Names are validated first because text/template's Funcs panics on a name that is not a Go identifier: a resolver called "code-list" would otherwise take down the process rather than fail the request.
  • zero exists because UseNumber carries numbers as json.Number, a string underneath — so {{ if .quantity }} is true even when the quantity is 0, and a template rendering an empty-value marker for a zero would quietly render the zero instead.
  • Validate checks a template without data, so a broken one fails when it is stored rather than when someone submits.

Testing

cd xmlgen
go test -race ./...
golangci-lint run -c ../.golangci.yml --timeout=5m ./...

138 subtests cumulative (38 added), race-clean, 0 lint issues. Covers each helper's edge case, resolvers receiving the caller's real Go types, ctx round-tripping, a resolver error matching both ErrResolver and the caller's own sentinel, a panicking resolver being recovered, invalid resolver names erroring rather than panicking, and call-counting across untaken branches and variable binding.

Related

Implements #189. Stacked chain — 2 of 4:

#199 engine → #201 (this)#202 golden fixtures → #200 failure paths

The engine renders and escapes, but a template still has to turn form data
into the shapes a receiving system expects. This adds the vocabulary it does
that with.

Nine built-in helpers, pure and deterministic, needing no configuration:
part and split for a value the document spreads across several elements,
date for moving between layouts, decimal for a fixed-decimal amount, lookup
for an enum encoded as something else, zero, coalesce, join and trim. Having
these in the box is what keeps Generate(ctx, tmpl, data) sufficient for most
templates rather than something every caller has to furnish first.

zero earns its place: data decoded with UseNumber carries numbers as
json.Number, a string underneath, so {{ if .quantity }} is true even when the
quantity is 0. Without it a template that renders an empty-value marker for
a zero quietly renders the zero instead.

Resolvers are the escape hatch for what the data cannot supply at all -- a
code-list description from a database, a rate from another service. Each is
registered under its own name, so a template calling one that was not
supplied fails when the template is parsed rather than at execution on
whichever branch happens to reach it. Names are validated first because
text/template's Funcs panics on a name that is not a Go identifier: a
resolver called "code-list" would otherwise take down the process rather
than fail the request. A resolver's own error stays reachable through
text/template's wrapping, so errors.Is matches both ErrResolver and the
caller's sentinel.

Also adds Validate, which checks a template without rendering it, so a
broken one fails when it is stored rather than when someone submits.

Refs #189

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 15, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 2e3e50d2-7d29-46ec-afdd-6e0d88bcbbac


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant