transform

package
v0.1.23 Latest Latest
Warning

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

Go to latest
Published: Nov 1, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

Documentation

Overview

Package transform applies declarative changes to a message.

This is the layer meant to carry the ordinary work, with scripting kept as a marked escape hatch. The distinction is not stylistic. A step declared in YAML can be validated before it runs, shown in a diff, reviewed by somebody who does not read JavaScript, and reported on; a script can only be executed and hoped about. Most of what Mirth transformers actually do is boring - copy a field, pad an identifier, map a code through a table, clear something that should not leave the building - and all of that belongs here.

Every step names its target explicitly and every step is reversible in the sense that it says what it did, because the most expensive failure in an interface is a transformation nobody knew was happening.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ReadPath

func ReadPath(root *xtree.Node, rawPath string) (string, bool)

ReadPath returns the value at a path, and whether it was present.

func ValueAt

func ValueAt(raw []byte, path string) (string, error)

ValueAt reads one path out of a raw HL7 message.

It exists for the channel test runner, which asserts on values in a transformed message. It goes through the same conversion and the same path resolution the transformation layer uses, so a path in an assertion means exactly what it means in a transformation. Reimplementing field addressing for tests would let a test pass while the transformation it describes was wrong.

Types

type CaseStep

type CaseStep struct {
	Path string `yaml:"path"`
	// To is "upper" or "lower".
	To string `yaml:"to"`
}

CaseStep changes letter case.

type Change

type Change struct {
	Step string `json:"step"`
	Path string `json:"path"`
	From string `json:"from"`
	To   string `json:"to"`
	// Note explains anything surprising, such as a value left unmapped.
	Note string `json:"note,omitempty"`

	// Deliberate marks a step that ran, left the value unchanged, and meant to.
	//
	// Needed because "the value did not change" otherwise means two opposite things. A date conversion with
	// on_error: keep did its job and is reporting the result; a copy from a field the message does not carry did
	// nothing and is the commonest silent bug there is. Without this flag the second was being counted as a change,
	// which told somebody their step worked.
	//
	// A flag rather than an inspection of Note, because a copy always sets a note to record where the value came
	// from, so the presence of a note says nothing about whether anything happened.
	Deliberate bool `json:"deliberate,omitempty"`
}

Change records what a step did, so the interface can show a diff and the message store can keep an account of how a message was altered.

type ClearStep

type ClearStep struct {
	Path string `yaml:"path"`
}

ClearStep empties a field but leaves it present.

type CopyStep

type CopyStep struct {
	From string `yaml:"from"`
	To   string `yaml:"to"`
	// Default is used when the source is empty. Without it, copying an absent
	// value clears the target, which is occasionally wanted and usually not.
	Default string `yaml:"default,omitempty"`
}

CopyStep copies one path to another.

type DateStep

type DateStep struct {
	Path string `yaml:"path"`
	// From and To are Java SimpleDateFormat patterns, the same notation the
	// scripts use, so that one channel does not need two date vocabularies.
	From string `yaml:"from"`
	To   string `yaml:"to"`
	// OnError chooses what happens when the value does not match From:
	// "keep" leaves it alone, "clear" empties it, "fail" stops the message.
	// The default is "fail", because a timestamp that silently did not convert
	// is accepted downstream and then misread.
	OnError string `yaml:"on_error,omitempty"`
}

DateStep reformats a timestamp between two layouts.

type MapStep

type MapStep struct {
	Path  string            `yaml:"path"`
	Table map[string]string `yaml:"table"`

	// Use names a shared table instead of writing one inline.
	//
	// The reason to have both: an inline table is right for a mapping only this channel will ever need, and a
	// shared one is right for the sex codes that forty channels translate identically. Forcing everything into
	// a shared file would make a one-off mapping a two-file change; forcing everything inline is what makes a
	// site maintain forty copies of the same table.
	//
	// A shared table also carries provenance - who decided, when, and why each row exists - which an inline
	// map cannot, because a YAML map has nowhere to put a comment that survives being rewritten by the form.
	Use string `yaml:"use,omitempty"`

	// Default applies when the value is not in the table. When it is empty and
	// Strict is false the original value is kept, which is the safe choice: an
	// unmapped local code preserved is recoverable, an unmapped code silently
	// blanked is not.
	Default string `yaml:"default,omitempty"`
	// Strict makes an unmapped value an error rather than a pass-through. Worth
	// turning on for a field the receiver validates against a closed set.
	Strict bool `yaml:"strict,omitempty"`
	// contains filtered or unexported fields
}

MapStep translates a value through a table.

type PadStep

type PadStep struct {
	Path  string `yaml:"path"`
	Width int    `yaml:"width"`
	With  string `yaml:"with,omitempty"`
	// Right pads on the right instead of the left.
	Right bool `yaml:"right,omitempty"`
	// Truncate shortens a value that is already longer than Width. Off by
	// default, because truncating an identifier produces one that matches the
	// wrong patient.
	Truncate bool `yaml:"truncate,omitempty"`
}

PadStep pads a value to a width.

type Path

type Path struct {
	Segment      string
	Occurrence   int // 1-based; 0 means every occurrence
	Field        int
	Repetition   int // 1-based; 0 means every repetition
	Component    int // 0 means the field itself
	Subcomponent int // 0 means the component itself
	// contains filtered or unexported fields
}

Path addresses a place in a message using the same notation as the filter language, so that a channel does not need two ways of naming a field.

PID-5.1          the first component of PID-5
PID-3(2).1       the first component of the second repetition of PID-3
OBX(3)-5         the fifth field of the third OBX segment
MSH-9.2          the trigger event

The one departure from the filter language is that a path used as a transformation target may need to create what it names, which reading never does. Creation is why the parts are kept separately rather than resolved to a node in one step.

func ParsePath

func ParsePath(s string) (Path, error)

ParsePath reads the notation above.

func (Path) String

func (p Path) String() string

String returns the path as written.

type Pipeline

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

Pipeline is a validated sequence of steps.

func Compile

func Compile(steps []Step) (*Pipeline, error)

Compile validates steps and prepares them for use.

Everything that can fail is made to fail here rather than per message: a bad regular expression, an unknown date pattern, a step with two actions or none.

func CompileWith

func CompileWith(steps []Step, tables *codeset.Set) (*Pipeline, error)

CompileWith compiles steps that may reference shared mapping tables.

A separate entry point rather than changing Compile's signature, because the overwhelming majority of callers have no shared tables and should not have to pass nil. A step naming a table that the set does not contain is an error here rather than at run time: a mapping that silently does nothing produces messages that look plausible and are wrong, which is the worst failure mode available to a transformation.

func (*Pipeline) Apply

func (p *Pipeline) Apply(root *xtree.Node) ([]Change, error)

Apply runs every step against a message tree, returning what changed.

func (*Pipeline) ApplyStep

func (p *Pipeline) ApplyStep(i int, root *xtree.Node) (changes []Change, skip *Skip, err error)

ApplyTraced runs every step and also reports the ones that changed nothing.

A separate entry point rather than a wider return on Apply, because almost every caller does not want the skips and the ones that do are asking a different question. Apply delegates here, so there is one implementation and the traced and untraced paths cannot diverge - which would be the worst possible outcome, since the trace would then describe a run that did not happen. ApplyStep runs a single step from an already-compiled pipeline.

Exists so a trace can apply steps one at a time and keep the message between them. Taking the step from the compiled pipeline rather than recompiling it matters for two reasons: recompiling needs the mapping tables passed in again, and a separately compiled copy could differ from what runs. A trace that disagrees with production is worse than none.

The returned Skip explains a step that ran and did nothing, which is the outcome the caller most needs.

func (*Pipeline) ApplyTraced

func (p *Pipeline) ApplyTraced(root *xtree.Node) ([]Change, []Skip, error)

func (*Pipeline) Len

func (p *Pipeline) Len() int

Len reports how many steps the pipeline has.

func (*Pipeline) Steps

func (p *Pipeline) Steps() []Step

Steps exposes the compiled steps for display.

type RemoveStep

type RemoveStep struct {
	Path string `yaml:"path"`
}

RemoveStep deletes an element entirely.

type ReplaceStep

type ReplaceStep struct {
	Path string `yaml:"path"`
	// Pattern is a Go regular expression, which is RE2: no backtracking, so a
	// pattern cannot take exponential time on a hostile input.
	Pattern string `yaml:"pattern"`
	With    string `yaml:"with"`
	// All replaces every occurrence rather than the first.
	All bool `yaml:"all,omitempty"`
}

ReplaceStep performs a regular-expression substitution.

type SetStep

type SetStep struct {
	Path  string `yaml:"path"`
	Value string `yaml:"value"`
}

SetStep writes a value to a path, creating it if absent.

type Skip

type Skip struct {
	// Step is the step's description, or its kind when it has none.
	Step string `json:"step"`

	// Path is what it addressed.
	Path string `json:"path,omitempty"`

	// Why says what happened.
	Why string `json:"why"`
}

Skip is a step that ran and changed nothing, with the reason.

The most useful thing a trace can carry, and the thing no existing output reported. The commonest fault in a working interface is a step addressing a field the sender does not populate: nothing errors, the message goes on unchanged, and the only evidence is that the count of changes is lower than somebody expected - which requires them to have an expectation.

type Step

type Step struct {
	// Description is free text explaining why this step exists. It is optional
	// but strongly encouraged, and the interface shows it.
	Description string `yaml:"description,omitempty"`

	// When is an optional condition. The step is skipped unless it holds.
	When string `yaml:"when,omitempty"`

	// Set writes a literal value.
	Set *SetStep `yaml:"set,omitempty"`
	// Copy moves a value from one path to another.
	Copy *CopyStep `yaml:"copy,omitempty"`
	// Clear empties a field.
	Clear *ClearStep `yaml:"clear,omitempty"`
	// Remove deletes a segment or element outright.
	Remove *RemoveStep `yaml:"remove,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"`
	// Pad left- or right-pads a value to a fixed width.
	Pad *PadStep `yaml:"pad,omitempty"`
	// Date reformats a timestamp.
	Date *DateStep `yaml:"date,omitempty"`
	// Trim removes surrounding whitespace.
	Trim *TrimStep `yaml:"trim,omitempty"`
	// Case changes a value to upper or lower case.
	Case *CaseStep `yaml:"case,omitempty"`
	// contains filtered or unexported fields
}

Step is one declarative change.

Exactly one action field may be set. The alternative - a single "action" string plus a bag of arguments - moves the error from configuration load to message handling, and an interface that only discovers a broken step when a patient is admitted is worse than one that refuses to start.

func (Step) Describe

func (s Step) Describe() string

Describe renders the step as a sentence.

func (Step) Path

func (s Step) Path() string

path names what a step addresses, for a trace.

A copy reports its destination, because that is the field somebody is looking for when they ask why it is empty. Path is the field this step addresses, or empty when it addresses none.

Exported because a trace has to name it. The path is the single most useful thing about a step that did nothing: almost always the field is absent from the message, and the path is what somebody compares against a real one.

type TrimStep

type TrimStep struct {
	Path string `yaml:"path"`
}

TrimStep removes surrounding whitespace.

Jump to

Keyboard shortcuts

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