Move the variable-bag contract (PlaceholderMap, MissingPlaceholderFn,
DefaultMissingMarker) up to the pkg/docforge root (placeholder.go) — it is
format-neutral, consumed by the resolver layer and any future exporter.
The {{key}} substitution grammar (placeholderRegex, PUA preview sentinels,
replacePlaceholders) stays in pkg/docforge/docx: it is the .docx renderer's
own machinery, not a root concern.
New at the root (vars.go):
- VariableResolver{Namespace() string; Populate(bag PlaceholderMap)} —
a PUSH interface, deliberately not pull Resolve(key): some namespaces
emit a data-dependent key set (parties.claimant.0.name, .1.name, … one
per party) that a fixed key-by-key pull can't enumerate.
- ResolverSet + BuildBag() — composes resolvers into one bag, replacing
the hard-coded addFooVars-then-addBarVars sequencing in Build.
paliad side (submission_vars_resolvers.go): seven resolver types wrap the
UNCHANGED addXxxVars push-builders (firm/today/user/procedural_event/
project/parties/deadline), each capturing the entity it needs. The builder
bodies are byte-for-byte untouched, so the bag is identical by
construction; SubmissionVarsService.Build now wires the applicable
resolvers and calls ResolverSet.BuildBag(). Resolvers stay in paliad
because they read paliad's domain model; a second docforge consumer plugs
its own resolvers into a ResolverSet the same way.
Keys()/Catalogue() (the static key list that will data-drive the authoring
palette + kill the hardcoded VARIABLE_GROUPS in submission-draft.ts) is
deferred to the UI slice that consumes it, sourced from the frontend's
existing labels — building it now, ahead of its consumer, would be
speculative (PRD §4 B3 principle).
Verification: go build ./... clean, go vet clean, full module test green.
Alias-parity (procedural_event ≡ rule) and party-form tests pass unchanged
= bag byte-identical.
m/paliad#157
57 lines
2.3 KiB
Go
57 lines
2.3 KiB
Go
package docforge
|
|
|
|
// VariableResolver populates one namespace of the placeholder bag.
|
|
//
|
|
// Each resolver owns a dotted namespace (e.g. "project", "parties") and
|
|
// pushes its keys into a shared PlaceholderMap. The push model — rather
|
|
// than a pull Resolve(key) — is deliberate: some namespaces emit a
|
|
// data-dependent set of keys (a multi-party suit produces
|
|
// parties.claimant.0.name, .1.name, … one per party), which a fixed
|
|
// key-by-key pull interface can't enumerate cleanly. Populate lets each
|
|
// resolver decide its own (possibly dynamic) key set in one pass.
|
|
//
|
|
// The consuming application implements concrete resolvers against its own
|
|
// data sources (paliad resolves project/party/deadline state from its
|
|
// Postgres database); docforge owns only the interface and the
|
|
// composition machinery (ResolverSet). This is the seam a second consumer
|
|
// (e.g. upc-commentary) plugs its own resolvers into without touching the
|
|
// engine.
|
|
type VariableResolver interface {
|
|
// Namespace returns the dotted prefix this resolver owns, e.g.
|
|
// "project". Informational — used for diagnostics and (later) the
|
|
// authoring variable palette's grouping.
|
|
Namespace() string
|
|
|
|
// Populate writes this resolver's keys into bag. Resolvers own
|
|
// disjoint namespaces, so population order across resolvers does not
|
|
// affect the final bag.
|
|
Populate(bag PlaceholderMap)
|
|
}
|
|
|
|
// ResolverSet composes an ordered list of VariableResolvers into a single
|
|
// PlaceholderMap. It is the replacement for hard-coded "call addFooVars,
|
|
// then addBarVars, …" sequencing: a consumer registers the resolvers that
|
|
// apply to a given render and calls BuildBag.
|
|
type ResolverSet struct {
|
|
resolvers []VariableResolver
|
|
}
|
|
|
|
// NewResolverSet builds a set from the given resolvers, in order.
|
|
func NewResolverSet(resolvers ...VariableResolver) *ResolverSet {
|
|
return &ResolverSet{resolvers: resolvers}
|
|
}
|
|
|
|
// Add appends a resolver to the set.
|
|
func (s *ResolverSet) Add(r VariableResolver) { s.resolvers = append(s.resolvers, r) }
|
|
|
|
// BuildBag runs every resolver's Populate into a fresh PlaceholderMap and
|
|
// returns it. Because resolvers own disjoint namespaces, the result is
|
|
// independent of resolver order.
|
|
func (s *ResolverSet) BuildBag() PlaceholderMap {
|
|
bag := PlaceholderMap{}
|
|
for _, r := range s.resolvers {
|
|
r.Populate(bag)
|
|
}
|
|
return bag
|
|
}
|