{"id":44647270,"url":"https://github.com/senojj/hl7","last_synced_at":"2026-02-24T05:01:05.201Z","repository":{"id":338484441,"uuid":"1153172259","full_name":"senojj/hl7","owner":"senojj","description":"A Go HL7 v2.x parsing library","archived":false,"fork":false,"pushed_at":"2026-02-21T23:21:29.000Z","size":3330,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-02-22T01:14:02.830Z","etag":null,"topics":["healthcare","hl7","hl7v2","parser"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/senojj.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-09T01:42:46.000Z","updated_at":"2026-02-21T20:21:47.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/senojj/hl7","commit_stats":null,"previous_names":["senojj/hl7"],"tags_count":8,"template":false,"template_full_name":null,"purl":"pkg:github/senojj/hl7","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/senojj%2Fhl7","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/senojj%2Fhl7/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/senojj%2Fhl7/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/senojj%2Fhl7/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/senojj","download_url":"https://codeload.github.com/senojj/hl7/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/senojj%2Fhl7/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29772391,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-24T04:54:30.205Z","status":"ssl_error","status_checked_at":"2026-02-24T04:53:58.628Z","response_time":75,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["healthcare","hl7","hl7v2","parser"],"created_at":"2026-02-14T20:12:39.538Z","updated_at":"2026-02-24T05:01:05.182Z","avatar_url":"https://github.com/senojj.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# hl7\n\nA Go library for parsing, transforming, and validating HL7 version 2.x messages in ER7 (pipe-delimited) format.\n\nZero external dependencies. Requires Go 1.23+.\n\n```go\nmsg, _ := hl7.ParseMessage(rawBytes)\n\nfmt.Println(msg.Get(\"MSH-9.1\").String())  // \"ADT\"\nfmt.Println(msg.Get(\"PID-5.1\").String())  // \"Smith\"\n```\n\n## When to use this library\n\nThis library is designed for applications that **route, filter, validate, or modify HL7v2 messages** — integration engines, message brokers, audit loggers, and similar infrastructure. It parses structure (segments, fields, components) and optionally validates messages against user-provided schemas.\n\nChoose this library when:\n\n- You need to read a few fields from each message and forward the rest untouched. Parsing is lazy: accessing `MSH-9` never touches `OBX` segments.\n- You process high-throughput message streams and need predictable, low-allocation performance.\n- You want a single dependency-free package rather than a framework with code-generated message types.\n- You need to transform messages (change field values, move data between fields, convert delimiters) without building a full serialization layer.\n- You need to validate messages against custom schemas — checking segment structure, field presence, data type formats, and coded table values.\n\nChoose a different library when:\n\n- You want strongly typed message structures with named fields for specific trigger events.\n\n## Parsing\n\n`ParseMessage` copies the input bytes and splits the message into segments. This is the only step that allocates. All deeper access — fields, components, subcomponents — scans raw bytes on demand with no caching.\n\n```go\nraw := []byte(\"MSH|^~\\\\\u0026|SEND|FAC|RECV|FAC|202401011200||ADT^A01|MSG001|P|2.5.1\\rPID|1||12345^^^MRN||Smith^John||19800101|M\")\n\nmsg, err := hl7.ParseMessage(raw)\nif err != nil {\n    log.Fatal(err)\n}\n```\n\n### Terser-style access\n\nThe `Get` method accepts location strings in the format `SEG-Field.Component.SubComponent`\nand returns a `Value` — a lightweight value type holding the raw bytes and delimiters:\n\n```go\nmsg.Get(\"MSH-9\").String()      // \"ADT^A01\"  (full field, unescaped)\nmsg.Get(\"MSH-9.1\").String()    // \"ADT\"      (first component)\nmsg.Get(\"MSH-9.2\").String()    // \"A01\"      (second component)\nmsg.Get(\"PID-5.1\").String()    // \"Smith\"    (family name)\nmsg.Get(\"PID-3.1\").String()    // \"12345\"    (patient ID)\nmsg.Get(\"PID-3.1.1\").String()  // \"12345\"    (first subcomponent)\nmsg.Get(\"PID-3.1\").Bytes()     // raw bytes without unescaping\n```\n\nSegment occurrence and repetition indices are supported:\n\n```go\nmsg.Get(\"OBX(0)-5\").String()   // first OBX, observation value\nmsg.Get(\"OBX(1)-5\").String()   // second OBX, observation value\nmsg.Get(\"PID-3[0].1\").String() // first repetition of PID-3, component 1\nmsg.Get(\"PID-3[1].1\").String() // second repetition of PID-3, component 1\n```\n\nMissing values return a zero `Value` — `String()` returns `\"\"` and `Bytes()` returns `nil`.\nNo error checking is needed for chained reads.\n\n### Location parsing\n\nLocation strings can be parsed into a `Location` struct and converted back:\n\n```go\nloc, err := hl7.ParseLocation(\"PID-3[1].4.2\")\n// loc.Segment = \"PID\", loc.Field = 3, loc.Repetition = 1,\n// loc.Component = 4, loc.SubComponent = 2\n\nfmt.Println(loc.String()) // \"PID-3[1].4.2\"\n```\n\n### Structural access\n\nFor iteration or when you need more control, walk the type hierarchy directly:\n\n```go\nfor _, seg := range msg.Segments() {\n    fmt.Printf(\"%-3s  %d fields\\n\", seg.Type(), seg.FieldCount())\n}\n\npid := msg.Segments()[1]\nname := pid.Field(5)                      // Field is 0-indexed (0 = segment type)\nfmt.Println(name.Rep(0).Component(1))     // Component is 1-indexed per HL7 convention\nfmt.Println(name.RepetitionCount())\n```\n\n### Null vs empty\n\nHL7 distinguishes between omitted fields (`||`) and explicitly null fields (`|\"\"|`):\n\n```go\nf := seg.Field(7)\nf.IsEmpty()    // true if field was omitted\nf.IsNull()     // true if field is the HL7 null value \"\"\nf.HasValue()   // true if neither empty nor null\n```\n\n`Value` (returned by `Get`) has the same `IsEmpty`, `IsNull`, and `HasValue` methods.\n\n### Character set decoding\n\nFor messages that declare a non-UTF-8 encoding in MSH-18, use `DecodeString` with a\n`ValueDecoder` to convert bytes after unescaping. `DecodeString` is available on `Value`,\n`Field`, `Repetition`, `Component`, and `Subcomponent`:\n\n```go\n// A ValueDecoder is func([]byte) ([]byte, error) — wrap e.g. golang.org/x/text.\nvar decode hl7.ValueDecoder\nswitch msg.Get(\"MSH-18\").String() {\ncase \"8859/1\":\n    decode = latin1ToUTF8 // caller-provided\n}\n\n// Terser-style: decode a specific field value.\nname, err := msg.Get(\"PID-5.1\").DecodeString(decode)\n\n// Hierarchical: decode a component.\nfamily, err := seg.Field(5).Rep(0).Component(1).DecodeString(decode)\n```\n\nWhen `decode` is `nil`, `DecodeString` is equivalent to `String()` with no extra allocation.\nUnescape always runs before the decoder, so the decoder receives resolved bytes. The `\\C..\\`\nand `\\M..\\` charset escape sequences are passed through verbatim; a sophisticated decoder\nmay interpret them, but a simple byte-level decoder will treat them as-is.\n\n## Transforming\n\n`Transform` applies changes to a message and returns a new `*Message`. The original is never modified.\n\n```go\nresult, err := msg.Transform(\n    hl7.Replace(\"PID-5.1\", \"Jones\"),\n    hl7.Replace(\"MSH-10\", \"NEW_CTRL_ID\"),\n    hl7.Null(\"PID-7\"),              // set to HL7 null (\"\")\n    hl7.Omit(\"PID-19\"),             // remove value entirely\n    hl7.Move(\"PID-3\", \"PID-4\"),     // copy PID-4 to PID-3, clear PID-4\n    hl7.Copy(\"PID-6\", \"PID-5\"),     // copy PID-5 to PID-6, preserve PID-5\n)\n```\n\nValues passed to `Replace` are plain text — delimiter characters are escaped automatically.\n\n### Delimiter conversion\n\n`TransformWith` re-encodes the entire message to a different delimiter set:\n\n```go\nnewDelims := hl7.Delimiters{\n    Field: '#', Component: '@', Repetition: '!',\n    Escape: '$', SubComponent: '%',\n}\n\nresult, err := msg.TransformWith(newDelims,\n    hl7.Replace(\"MSH-10\", \"CONVERTED\"),\n)\n```\n\nDelimiter conversion correctly handles escape sequences: `\\F\\` in the source (which represents the literal source field separator `|`) resolves to the character `|` in the output, and is re-escaped only if `|` happens to be a delimiter in the destination set.\n\n### Extending messages\n\nChanges that target fields or segments beyond the current message size automatically extend it:\n\n```go\n// PID has 8 fields; this extends it to include field 30.\nresult, _ := msg.Transform(hl7.Replace(\"PID-30\", \"extended\"))\n\n// ZZZ segment doesn't exist; it will be created.\nresult, _ = msg.Transform(hl7.Replace(\"ZZZ-1\", \"custom\"))\n```\n\n## Building messages\n\n`MessageBuilder` constructs HL7 messages from scratch using terser-style field paths.\n\n```go\nb, err := hl7.NewMessageBuilder()\nif err != nil {\n    log.Fatal(err)\n}\n\nb.Set(\"MSH-9.1\", \"ADT\")\nb.Set(\"MSH-9.2\", \"A01\")\nb.Set(\"MSH-10\", \"CTRL001\")\nb.Set(\"MSH-11\", \"P\")\nb.Set(\"MSH-12\", \"2.5.1\")\nb.Set(\"PID-3.1\", \"12345\")\nb.Set(\"PID-5.1\", \"Smith\")\nb.Set(\"PID-5.2\", \"John\")\nb.Set(\"PV1-2\", \"I\")\n\nmsg, err := b.Build()\n```\n\n`Set` accepts the same location syntax as `Get` — segments, fields, components, subcomponents, and repetitions. Values are plain text and will be escaped automatically. Segments are created on first use.\n\n### Repetitions and subcomponents\n\n```go\nb.Set(\"PID-3[0].1\", \"ID1\")\nb.Set(\"PID-3[1].1\", \"ID2\")\nb.Set(\"PID-3.4.1\", \"AUTH\")\nb.Set(\"PID-3.4.2\", \"SYSTEM\")\n```\n\n### Null values\n\n```go\nb.SetNull(\"PID-7\") // sets to HL7 null (\"\")\n```\n\n### Custom delimiters\n\n```go\nb, err := hl7.NewMessageBuilder(hl7.WithDelimiters(hl7.Delimiters{\n    Field: '#', Component: '@', Repetition: '!',\n    Escape: '$', SubComponent: '%',\n}))\n```\n\n### Reusability\n\nThe builder remains usable after `Build`. Subsequent `Set` calls modify the builder's state, and a new `Build` produces a new independent `*Message`:\n\n```go\nb.Set(\"PID-3\", \"AAA\")\nmsg1, _ := b.Build()\n\nb.Set(\"PID-3\", \"BBB\")\nmsg2, _ := b.Build() // msg1 is unchanged\n```\n\n## Validation\n\n`Validate` checks a parsed message against a user-provided `Schema`. The schema is composed of four optional maps — populate only the categories you need.\n\n```go\nschema := \u0026hl7.Schema{\n    Messages: map[string]*hl7.MessageDef{\n        \"ADT_A01\": {Elements: []hl7.Element{\n            {Segment: \"MSH\", Min: 1, Max: 1},\n            {Segment: \"PID\", Min: 1, Max: 1},\n            {Segment: \"PV1\", Min: 1, Max: 1},\n        }},\n    },\n    Segments: map[string]*hl7.SegmentDef{\n        \"PID\": {Fields: []hl7.FieldDef{\n            {Index: 3, Name: \"Patient Identifier List\", DataType: \"CX\", Required: true},\n            {Index: 5, Name: \"Patient Name\", Required: true, MaxLength: 48},\n            {Index: 8, Name: \"Administrative Sex\", Table: \"0001\"},\n        }},\n    },\n    Tables: map[string]*hl7.TableDef{\n        \"0001\": {Values: map[string]string{\"F\": \"Female\", \"M\": \"Male\", \"U\": \"Unknown\"}},\n    },\n}\n\nresult := msg.Validate(schema)\nif !result.Valid {\n    for _, issue := range result.Issues {\n        fmt.Println(issue) // [error] PID-8: Value \"X\" at PID-8 is not in table 0001 (INVALID_TABLE_VALUE)\n    }\n}\n```\n\n### What gets validated\n\nValidation runs in three phases. Each phase runs only if the relevant definitions exist in the schema:\n\n**Structure** (`schema.Messages`) — Checks segment presence, order, and cardinality against the message definition looked up from MSH-9. Supports nested groups with min/max repetition counts.\n\n**Content** (`schema.Segments`, `schema.DataTypes`, `schema.Tables`) — For each segment with a definition, checks:\n- Exact value assertions (`Value` on fields and components)\n- Required fields are present\n- Field length does not exceed `MaxLength`\n- Non-repeating fields do not repeat\n- Primitive data types match expected format (DT, TM, DTM/TS, NM, SI)\n- Composite data types have required components with valid lengths and formats\n- Coded values exist in the referenced table\n- Custom field checks (`FieldDef.Check`) and segment checks (`SegmentDef.Check`)\n\nValue assertions are checked first. When a value doesn't match, remaining checks (format, table, length) are skipped — they would be noise on a wrong value.\n\n**Custom checks** (`schema.Checks`) — Runs message-level `MessageCheckFunc` functions for cross-segment business rules that cannot be expressed declaratively.\n\n### Composite data types\n\nDefine `DataTypeDef` entries to validate component structure within composite fields:\n\n```go\nschema.DataTypes = map[string]*hl7.DataTypeDef{\n    \"CX\": {Components: []hl7.ComponentDef{\n        {Index: 1, Name: \"ID Number\", Required: true, MaxLength: 15},\n        {Index: 4, Name: \"Assigning Authority\", MaxLength: 20},\n        {Index: 5, Name: \"Identifier Type Code\", Table: \"0203\"},\n    }},\n}\n```\n\n### Value assertions\n\nUse the `Value` field on `FieldDef` or `ComponentDef` to assert that a field or component has an exact expected value:\n\n```go\nschema.Segments = map[string]*hl7.SegmentDef{\n    \"MSH\": {Fields: []hl7.FieldDef{\n        {Index: 12, Name: \"Version ID\", Value: \"2.5.1\"},\n        {Index: 18, Name: \"Character Set\", Value: \"UNICODE UTF-8\"},\n    }},\n}\n```\n\nThis is useful for enforcing interface agreements — version IDs, processing modes, encoding declarations, and other fields that must have specific values. Empty fields are not checked against value assertions.\n\n### Custom check functions\n\nFor validation logic that cannot be expressed declaratively, attach check functions at three levels. All return `[]Issue` and are tagged `json:\"-\"` so they don't interfere with schema serialization.\n\n**Field-level** — runs once per field (after declarative checks), receives the `Field` value:\n\n```go\n{Index: 5, Name: \"Observation Value\", Check: func(f hl7.Field) []hl7.Issue {\n    // Custom range check on a numeric field.\n    val, err := strconv.ParseFloat(f.String(), 64)\n    if err != nil || val \u003c 0 || val \u003e 300 {\n        return []hl7.Issue{{\n            Severity: hl7.SeverityError, Location: \"OBX-5\",\n            Code: \"VALUE_RANGE\", Description: \"value out of range [0, 300]\",\n        }}\n    }\n    return nil\n}}\n```\n\n**Segment-level** — runs once per segment occurrence (after all field checks), receives `*Segment`:\n\n```go\nschema.Segments[\"PID\"] = \u0026hl7.SegmentDef{\n    Check: func(seg *hl7.Segment) []hl7.Issue {\n        if seg.Field(8).HasValue() \u0026\u0026 !seg.Field(7).HasValue() {\n            return []hl7.Issue{{\n                Severity: hl7.SeverityError, Location: \"PID-7\",\n                Code: \"CONDITIONAL_REQUIRED\",\n                Description: \"PID-7 required when PID-8 is present\",\n            }}\n        }\n        return nil\n    },\n}\n```\n\n**Message-level** — runs once per `Validate` call (after all segments), receives `*Message`:\n\n```go\nschema.Checks = []hl7.MessageCheckFunc{\n    func(msg *hl7.Message) []hl7.Issue {\n        if msg.Get(\"MSH-9.1\").String() == \"ORU\" \u0026\u0026 msg.Get(\"OBX-1\").String() == \"\" {\n            return []hl7.Issue{{\n                Severity: hl7.SeverityError, Location: \"OBX\",\n                Code: \"BUSINESS_RULE\",\n                Description: \"ORU messages must contain at least one OBX\",\n            }}\n        }\n        return nil\n    },\n}\n```\n\nCustom checks are additive — they run alongside declarative checks, not instead of them. Empty or null fields skip `FieldDef.Check` entirely.\n\n### Incremental adoption\n\nEach map in the schema is independent. A schema with only `Messages` performs structure validation without checking field contents. A schema with only `Segments` and `Tables` validates field values without checking segment order. This lets you adopt validation incrementally.\n\n### Loading schemas from files\n\nAll schema types have struct tags that support JSON, YAML, and TOML. Use any decoder that unmarshals into Go structs — no special loading function is needed.\n\n```go\n// JSON (encoding/json — stdlib, zero dependencies)\nf, _ := os.Open(\"schema.json\")\nvar schema hl7.Schema\njson.NewDecoder(f).Decode(\u0026schema)\n\n// YAML (gopkg.in/yaml.v3)\nf, _ := os.Open(\"schema.yaml\")\nvar schema hl7.Schema\nyaml.NewDecoder(f).Decode(\u0026schema)\n\n// TOML (github.com/BurntSushi/toml)\nf, _ := os.Open(\"schema.toml\")\nvar schema hl7.Schema\ntoml.NewDecoder(f).Decode(\u0026schema)\n```\n\nA schema file in JSON:\n\n```json\n{\n  \"messages\": {\n    \"ADT_A01\": {\n      \"elements\": [\n        {\"segment\": \"MSH\", \"min\": 1, \"max\": 1},\n        {\"segment\": \"PID\", \"min\": 1, \"max\": 1},\n        {\"segment\": \"PV1\", \"min\": 1, \"max\": 1}\n      ]\n    }\n  },\n  \"segments\": {\n    \"MSH\": {\n      \"fields\": [\n        {\"index\": 12, \"name\": \"Version ID\", \"value\": \"2.5.1\"}\n      ]\n    },\n    \"PID\": {\n      \"fields\": [\n        {\"index\": 3, \"name\": \"Patient Identifier List\", \"type\": \"CX\", \"required\": true},\n        {\"index\": 5, \"name\": \"Patient Name\", \"required\": true, \"max_length\": 48},\n        {\"index\": 8, \"name\": \"Administrative Sex\", \"table\": \"0001\"}\n      ]\n    }\n  },\n  \"tables\": {\n    \"0001\": {\n      \"values\": {\"F\": \"Female\", \"M\": \"Male\", \"U\": \"Unknown\"}\n    }\n  }\n}\n```\n\nThe same schema in YAML:\n\n```yaml\nmessages:\n  ADT_A01:\n    elements:\n      - segment: MSH\n        min: 1\n        max: 1\n      - segment: PID\n        min: 1\n        max: 1\n      - segment: PV1\n        min: 1\n        max: 1\n\nsegments:\n  MSH:\n    fields:\n      - index: 12\n        name: Version ID\n        value: \"2.5.1\"\n  PID:\n    fields:\n      - index: 3\n        name: Patient Identifier List\n        type: CX\n        required: true\n      - index: 5\n        name: Patient Name\n        required: true\n        max_length: 48\n      - index: 8\n        name: Administrative Sex\n        table: \"0001\"\n\ntables:\n  \"0001\":\n    values:\n      F: Female\n      M: Male\n      U: Unknown\n```\n\nSchemas can also be marshaled back to any format for sharing or code generation:\n\n```go\ndata, _ := json.MarshalIndent(schema, \"\", \"  \")\nos.WriteFile(\"schema.json\", data, 0644)\n```\n\n## Reading streams\n\n`Reader` reads messages from an `io.Reader` with support for MLLP framing and raw (MSH-boundary) detection.\n\n### MLLP (typical for TCP connections)\n\n```go\nreader := hl7.NewReader(conn, hl7.WithMode(hl7.ModeMLLP))\n\nerr := reader.EachMessage(func(msg *hl7.Message) error {\n    msgType := msg.Get(\"MSH-9.1\").String()\n    fmt.Println(\"received\", msgType)\n    return nil\n})\nif err != nil {\n    log.Fatal(err)\n}\n```\n\n### Auto-detection\n\n`ModeAuto` (the default) peeks at the first byte to decide between MLLP and raw mode:\n\n```go\nreader := hl7.NewReader(conn) // auto-detect\nmsg, err := reader.ReadMessage()\n```\n\n### Raw message bytes\n\nIf you need the unparsed bytes (for forwarding, logging, etc.):\n\n```go\nraw, err := reader.ReadRawMessage()\n```\n\n## Writing streams\n\n`Writer` writes messages to an `io.Writer` with optional MLLP framing. It is the counterpart to `Reader`.\n\n```go\nwriter := hl7.NewWriter(conn, hl7.WithMLLP())\nerr := writer.WriteMessage(msg)\n```\n\nWithout `WithMLLP()`, messages are written as raw bytes followed by a segment terminator.\n\n### Forwarding without parsing\n\n`WriteRawMessage` writes raw bytes directly — useful for proxying or logging without the cost of parsing:\n\n```go\nraw, _ := reader.ReadRawMessage()\nwriter.WriteRawMessage(raw)\n```\n\n### Read-transform-write\n\nA typical integration engine loop:\n\n```go\nreader := hl7.NewReader(inConn, hl7.WithMode(hl7.ModeMLLP))\nwriter := hl7.NewWriter(outConn, hl7.WithMLLP())\n\nreader.EachMessage(func(msg *hl7.Message) error {\n    modified, _ := msg.Transform(hl7.Replace(\"MSH-5\", \"DEST\"))\n    return writer.WriteMessage(modified)\n})\n```\n\nWrites are zero-allocation when the `Writer` is reused. Each write flushes immediately — HL7 messages are request/response, so buffering across messages is not desirable.\n\n## ACK generation\n\n`Ack` generates an ACK response message from a received message. It swaps sender/receiver fields, copies delimiters, and builds MSH + MSA segments:\n\n```go\nack, err := msg.Ack(hl7.AA, \"ACK001\")\n```\n\nThe first argument is an acknowledgment code (`AA`, `AE`, `AR`, `CA`, `CE`, `CR`). The second is the control ID for the ACK's MSH-10.\n\n### Error responses\n\nUse `WithText` to include an error description in MSA-3:\n\n```go\nack, err := msg.Ack(hl7.AE, \"ACK002\",\n    hl7.WithText(\"PID-3 missing required patient identifier\"))\n```\n\n### Field mapping\n\nThe ACK copies and swaps fields from the original message:\n\n| ACK field | Source |\n|-----------|--------|\n| MSH-3 (Sending App) | Original MSH-5 (Receiving App) |\n| MSH-4 (Sending Facility) | Original MSH-6 (Receiving Facility) |\n| MSH-5 (Receiving App) | Original MSH-3 (Sending App) |\n| MSH-6 (Receiving Facility) | Original MSH-4 (Sending Facility) |\n| MSH-7 (Timestamp) | Current time (or `WithTimestamp(t)`) |\n| MSH-9 (Message Type) | ACK^trigger^ACK |\n| MSH-10 (Control ID) | The `controlID` argument |\n| MSH-11 (Processing ID) | Original MSH-11 |\n| MSH-12 (Version ID) | Original MSH-12 |\n| MSA-1 (Ack Code) | The `code` argument |\n| MSA-2 (Control ID) | Original MSH-10 |\n| MSA-3 (Text) | `WithText` value (if provided) |\n\nThe result is raw `[]byte` suitable for sending directly or writing via a `Writer`:\n\n```go\nwriter := hl7.NewWriter(conn, hl7.WithMLLP())\nwriter.WriteRawMessage(ack)\n```\n\n## Batch and file parsing\n\nHL7 defines batch (BHS/BTS) and file (FHS/FTS) wrapper segments for grouping messages:\n\n```go\nbatch, err := hl7.ParseBatch(data)\nfor _, msg := range batch.Messages {\n    fmt.Println(msg.Get(\"MSH-10\"))\n}\n\nfile, err := hl7.ParseFile(data)\nfor _, batch := range file.Batches {\n    for _, msg := range batch.Messages {\n        fmt.Println(msg.Get(\"MSH-10\"))\n    }\n}\n```\n\nHeader and trailer segments are optional — messages without BHS/BTS wrappers are placed in an implicit batch.\n\n## Escape sequences\n\nEscape processing is deferred until `.String()` is called. The `Unescape` and `Escape` functions are also available directly:\n\n```go\nd := hl7.DefaultDelimiters()\n\n// Unescape: resolve HL7 escape sequences to literal text\ntext := hl7.Unescape([]byte(`Dr\\S\\ Smith \\F\\ MD`), d)\n// text = \"Dr^ Smith | MD\"\n\n// Escape: encode delimiter characters for safe embedding in fields\nencoded := hl7.Escape([]byte(\"value|with^delims\"), d)\n// encoded = `value\\F\\with\\S\\delims`\n```\n\nBoth functions have a zero-allocation fast path when no escape or delimiter characters are present in the input.\n\n## Examples\n\nThe `examples/` directory contains runnable programs demonstrating end-to-end workflows.\n\n### Build and write\n\n`examples/builder` constructs an ADT^A01 message from scratch using `MessageBuilder`, then writes it with MLLP framing:\n\n```sh\ngo run ./examples/builder\n```\n\n### Full workflow\n\n`examples/full` reads an ADT^A01 message from an MLLP stream, validates it against an inbound schema, transforms it (updating fields and copying data), validates the result against an outbound schema, and writes it back with MLLP framing:\n\n```sh\ngo run ./examples/full\n```\n\n## Design tradeoffs\n\n### Scan-on-access instead of eager parsing\n\nEvery call to `Field(n)`, `Component(n)`, or `SubComponent(n)` re-scans raw bytes to find the n-th delimiter. Nothing is cached.\n\n- **Benefit:** Sub-message types are pure value types (~32 bytes each). `ParseMessage` allocates only 3 objects: the byte buffer copy, the segment slice, and the `*Message` struct. There are no per-field or per-component heap allocations.\n- **Cost:** Repeated access to the same field re-scans each time. For typical HL7 segments (\u003c200 bytes), each scan costs ~10-20ns — negligible compared to the ~100-200ns per heap allocation it avoids.\n- **Implication:** If you access the same deeply nested value in a tight loop, extract it to a variable first.\n\n### Immutable messages\n\n`ParseMessage` copies the input buffer. All types hold read-only slices into this owned copy. There are no setters — use `Transform` to produce a modified copy. This makes messages safe for concurrent reads with no synchronization.\n\n### Zero values instead of errors for access\n\nOut-of-range field, component, and subcomponent access returns empty zero values rather than errors. This enables chained access patterns like `seg.Field(5).Rep(0).Component(2).String()` without intermediate nil checks. The cost is that typos in field indices silently return empty strings.\n\n### Transform rebuilds message bytes\n\n`Transform` applies changes by splicing raw bytes in a working buffer, then calls `ParseMessage` on the result. This means every transform pays the cost of a full re-parse (~800ns for a typical message). This is by design: the output is a fully independent `*Message` with its own buffer, and correctness is guaranteed by reusing the battle-tested parse path.\n\n### Schema validation is opt-in\n\nThe parser treats all segments generically — it does not require a schema to parse or access any message. Validation is a separate step via `msg.Validate(schema)` with a user-provided `Schema`. The library does not ship with built-in HL7v2 segment or table definitions. This keeps the parser small and avoids coupling to any particular HL7 version.\n\n## Performance\n\nBenchmarked on Apple M3 Pro (arm64):\n\n| Operation | Time | Allocs | Bytes |\n|-----------|------|--------|-------|\n| ParseMessage (695B ORU_R01) | 837 ns | 3 | 1,088 |\n| Parse + access all fields | 8.1 us | 5 | 1,140 |\n| ParseMessage (minimal MSH) | 93 ns | 3 | 144 |\n| Get accessor (3 lookups) | 329 ns | 6 | 96 |\n| Transform (4 changes) | 1.3 us | 6 | 2,200 |\n| Builder (10 Set + Build) | 1.0 us | 13 | 784 |\n| Validate structure only (ORU_R01) | 257 ns | 3 | 72 |\n| Validate fields only (ORU_R01) | 2.8 us | 11 | 64 |\n| Validate full (ORU_R01) | 3.1 us | 13 | 104 |\n| WriteMessage MLLP | 12 ns | 0 | 0 |\n| WriteMessage raw | 9 ns | 0 | 0 |\n| Ack (ADT^A01) | 442 ns | 3 | 176 |\n\nThe 3 base allocations are the byte buffer copy, the segment slice, and the `*Message` struct. The 2 additional allocations in parse+access come from `Unescape` on fields containing the escape character (MSH-2 always contains `\\`).\n\nValidation operates on raw bytes (avoiding `Unescape` allocations) and defers location string construction to error paths only. On valid messages — the common case — no location strings are built, keeping allocations minimal.\n\nWriter writes are zero-allocation when the `Writer` is reused. The `bufio.Writer` batches framing bytes and payload into a single syscall.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsenojj%2Fhl7","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsenojj%2Fhl7","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsenojj%2Fhl7/lists"}