Documentation
¶
Overview ¶
Declarative transformation steps, for any message format.
Why this is shared ¶
The step vocabulary - set, copy, clear, map, replace, trim, case, each with a description and an optional condition - has nothing to do with any particular format. Only three things differ between formats: how a path is written, how a value is read, and how it is written back.
This is the same conclusion the filter grammar reached in internal/expr, and for the same reason. X12 had a step engine and NCPDP needed one; copying it would have produced a second four-hundred-line engine to keep in step by hand, and then a third for delimited. The behaviour worth having is subtle enough that a copy would not keep it - the reason the result is re-read after every write, for instance, took a real defect to discover.
What a format supplies ¶
An Accessor: a path compiler, a condition compiler, and an optional veto on steps it cannot honour. Nothing else.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Accessor ¶
type Accessor[M any] interface { // Path compiles one path, or reports why it cannot. Path(raw string) (Path[M], error) // Condition compiles a when expression over this format. Condition(src string) (expr.Expr[M], error) // CheckStep refuses a step this format cannot honour, and returns nil for everything it can. // // For the refusals that are real but specific: a trim on an X12 ISA element can never have an effect, because ISA is // fixed width and the padding is put straight back. Refusing at load beats a step that runs forever and does nothing. CheckStep(s Step) error }
Accessor is how one message format addresses, reads and writes values.
type CaseStep ¶
type CaseStep struct {
Path string `yaml:"path"`
// To is "upper" or "lower".
To string `yaml:"to"`
}
CaseStep changes the case of a value.
type Change ¶
type Change struct {
// Path is where it happened, in canonical form.
Path string
// Description is the step's own explanation, when it gave one.
Description string
// From and To are the values before and after.
From string
To string
}
Change records one alteration, for the trace and the report.
type MapStep ¶
type MapStep struct {
Path string `yaml:"path"`
// Table names a code set loaded with the channel.
Table string `yaml:"table"`
// Default is written when the value is not in the table. Optional.
Default string `yaml:"default,omitempty"`
// OnMissing decides what happens when the value is absent from the table and no default is given.
//
// Defaults to "keep", which leaves the value alone. The alternative is "fail", which stops the message. There is no
// option that silently blanks it, because a claim with an empty payer identifier is rejected downstream in a way
// that points at the wrong system.
OnMissing string `yaml:"on_missing,omitempty"`
}
MapStep translates a value through a named table.
type Path ¶
type Path[M any] struct { // Canonical is the path in canonical form, for change records and error messages. Canonical string // Read returns the value at this path and whether it was present. // // The bool distinguishes absent from empty, which every format here treats as different things: a blank field was // sent deliberately and an absent one was never sent. Read func(M) (string, bool) // Write returns a message with the value written. It must not modify its receiver. Write func(M, string) (M, error) }
Path is a path compiled against one message format.
Closures rather than a string handed back to the accessor on every access, so a path is parsed once at load. The filter grammar measured this and it was worth roughly a third of the cost of an evaluation.
type ReplaceStep ¶
type ReplaceStep struct {
Path string `yaml:"path"`
Pattern string `yaml:"pattern"`
With string `yaml:"with"`
}
ReplaceStep performs a regular-expression substitution.
type Step ¶
type Step struct {
// Description is free text explaining why this step exists. Optional, shown in the interface, and worth writing: the
// next person to read this channel is trying to work out whether the step is still needed.
Description string `yaml:"description,omitempty"`
// When is an optional condition. The step is skipped unless it holds.
//
// The same grammar as any filter in this server - and, or, not, ==, !=, =~, exists, empty, in - because the grammar
// is generic over the format. The only thing that changes between formats is how the path is written.
When string `yaml:"when,omitempty"`
// Set writes a value at a path.
Set *SetStep `yaml:"set,omitempty"`
// Copy takes the value at one path and writes it to another.
Copy *CopyStep `yaml:"copy,omitempty"`
// Clear empties a field, leaving it present.
//
// Not the same as removing it, and these formats make that distinction meaningful: a blank field was sent
// deliberately while an absent one was never sent. There is no remove step for exactly that reason - the honest
// options are to blank the field or to leave it alone.
Clear *ClearStep `yaml:"clear,omitempty"`
// Map translates a value through a lookup table.
Map *MapStep `yaml:"map,omitempty"`
// Replace performs a regular-expression substitution.
Replace *ReplaceStep `yaml:"replace,omitempty"`
// Trim removes surrounding whitespace.
//
// Worth having as its own step rather than a replace: these formats pad fixed-width fields with spaces, so this is
// one of the most common changes an interface needs, and a named step is readable where a pattern is not.
Trim *TrimStep `yaml:"trim,omitempty"`
// Case changes a value to upper or lower case.
Case *CaseStep `yaml:"case,omitempty"`
}
Step is one declarative change.
Exactly one action per step, checked at load. Two actions in one step have no defined order, and a configuration whose meaning depends on which the reader assumes is worse than one that refuses to load.
func (Step) TargetPaths ¶
TargetPaths returns the paths this step writes to, keyed by the configuration key that named each one.
For an Accessor's CheckStep, which usually needs to veto a step by what it writes rather than by which action it is: a format with a derived or fixed-width field cares that something writes to it, not whether the step was a set or a trim.
copy.from is deliberately absent. Reading a field is always allowed; it is writing that a format restricts.
type Steps ¶
type Steps[M any] struct { // contains filtered or unexported fields }
Steps is a compiled sequence, ready to run.
func Compile ¶
Compile prepares a sequence for running.
Everything checkable without a message is checked here: paths parse, patterns compile, tables are named, case directions are recognised, exactly one action per step. A channel with a bad step must refuse to start rather than fail on the first message, because the operator who deployed it is watching at that moment and will not be watching at three the following morning.
func (*Steps[M]) Apply ¶
Apply runs the sequence, returning the result and what changed.
The message given is not modified. Every step works on the output of the last, so a later step sees earlier changes - which is what a reader of the file expects, and the alternative would make a sequence of steps behave differently from the same steps in separate channels.