openehr-sdt 0.0.128

openEHR Simplified Data Template formats (FLAT / STRUCTURED / Web Template), and template-independent RM-instance validation, hand-written over the openehr-its wire layer
Documentation
// SPDX-FileCopyrightText: Cadasto B.V.
// SPDX-License-Identifier: BUSL-1.1

//! Errors for the Simplified Formats layer.
//!
//! Every MUST-level rejection in the Simplified Formats specification
//! (`ITS-REST/docs/simplified_formats/`) has its own variant so callers can
//! branch on the outcome without string matching; the wire layer maps these
//! to `422`/`400` per the ITS-REST operation contracts.

/// An error in Simplified-Format (FLAT / STRUCTURED / Web Template) handling.
#[derive(Debug, thiserror::Error)]
pub enum FlatError {
    /// The operational template lacked something the Web Template builder
    /// requires.
    #[error("invalid operational template: {0}")]
    InvalidTemplate(String),

    /// An OPT 1.4 XML document failed to parse.
    #[error("failed to parse OPT 1.4 XML")]
    OptParse(#[from] openehr_its::xml::runtime::XmlError),

    /// A value could not be serialized to JSON.
    #[error("failed to serialize to JSON")]
    Serialize(#[from] serde_json::Error),

    /// A FLAT key is not syntactically valid (ITS-REST simplified_formats
    /// master04 §Flat format syntax rules).
    #[error("malformed simplified path {path:?}: {reason}")]
    MalformedPath {
        /// The offending key as received.
        path: String,
        /// What rule the key breaks.
        reason: String,
    },

    /// A path does not resolve to a node of the target Web Template
    /// (master04 §Validation: "Field identifiers match WT metadata
    /// structure").
    #[error("unknown simplified path: {0}")]
    UnknownPath(String),

    /// An attribute suffix is not defined for the RM type at its path
    /// (master05 per-type tables).
    #[error("unknown attribute suffix |{suffix} for {rm_type} at {path}")]
    UnknownSuffix {
        /// The RM type of the node the path resolved to.
        rm_type: String,
        /// The offending suffix (without the pipe).
        suffix: String,
        /// The path the suffix was attached to.
        path: String,
    },

    /// `|other` combined with `|code`/`|value`/`|terminology` on one leaf —
    /// the combination servers MUST reject (master04 §Open Value-Sets and
    /// the `|other` Suffix).
    #[error("|other is mutually exclusive with |code/|value/|terminology at {0}")]
    OtherSuffixConflict(String),

    /// `|other` used where the value-set constraint is closed
    /// (`listOpen: false`) — MUST be rejected (master04 §Open Value-Sets
    /// and the `|other` Suffix).
    #[error("|other not allowed at {0}: the value set is closed")]
    OtherOnClosedValueSet(String),

    /// A mandatory context field is absent (master04 §Context: "Mandatory:
    /// language, territory").
    #[error("mandatory ctx field {0} is required (master04 §Context: language and territory)")]
    MissingContext(&'static str),

    /// A `|code` on a closed value-set leaf names a code outside the bound
    /// list and carries no explicit `|value` to supply the text
    /// (master04 §Validation: "Terminology bindings are valid").
    #[error("coded value is not a member of the bound value set: '{code}' at {path}")]
    CodeNotInValueSet {
        /// The simplified path of the coded leaf.
        path: String,
        /// The code the document supplied.
        code: String,
    },

    /// A `ctx/` key outside the vocabulary of master06-context_information.
    #[error("unknown context key ctx/{0}")]
    UnknownContext(String),

    /// A FLAT key the RM types 1..1 is absent from the submitted document:
    /// either a `|suffix` the master05 mapping table marks `Required: yes` —
    /// e.g. all three of §LINK's `|type`, `|meaning` and `|target`
    /// (`RM/docs/UML/classes/org.openehr.rm.common.link.adoc` §Attributes) —
    /// or a `ctx/` key standing in for a mandatory attribute of the RM object
    /// it builds, e.g. `ctx/participation_function:<i>` for
    /// `PARTICIPATION.function`
    /// (`RM/docs/UML/classes/org.openehr.rm.common.participation.adoc`
    /// §Attributes).
    #[error("{key} is required")]
    MissingRequiredSuffix {
        /// The full FLAT key the client omitted, e.g. `…/_link:0|meaning` or
        /// `ctx/participation_function:0`.
        key: String,
    },

    /// A FLAT or STRUCTURED write decomposes a value of an RM type the
    /// Simplified Formats define no form for: ITS-REST
    /// `simplified_formats/master05-rm_mapping.adoc` has a mapping table per
    /// supported type (§DV_TEXT to §DV_INTERVAL) and none for `DV_STATE`.
    /// Only the master04 §Raw canonical JSON bypass (`|raw`) carries one.
    #[error(
        "{path} is a {rm_type}, for which the simplified formats define no form \
         (simplified_formats master05-rm_mapping has no {rm_type} table); \
         send it as canonical JSON with |raw (master04 §Raw canonical JSON)"
    )]
    NoSimplifiedForm {
        /// The RM type of the node the path resolved to.
        rm_type: String,
        /// The simplified path of the node.
        path: String,
    },

    /// A value has the wrong JSON type or an unparsable encoding for its
    /// slot (e.g. a non-numeric `|magnitude`, a malformed compact
    /// participation-identifier list).
    #[error("invalid value at {path}: {reason}")]
    InvalidValue {
        /// The simplified path of the offending value.
        path: String,
        /// What is wrong with it.
        reason: String,
    },

    /// A `|raw` value carries no `_type`, so it cannot be read as an RM
    /// object (master04 §Raw canonical JSON: "The raw JSON value must include
    /// the `_type` property indicating the openEHR type").
    #[error("invalid |raw payload at {path}: the value must be a canonical-JSON object with _type")]
    RawWithoutType {
        /// The simplified path of the `|raw` key.
        path: String,
    },

    /// A `|raw` payload is an RM object the attribute does not admit, or
    /// stands beside other content at its node.
    #[error("invalid |raw payload at {path}: {reason}")]
    InvalidRaw {
        /// The simplified path of the `|raw` key.
        path: String,
        /// What is wrong with the payload.
        reason: String,
    },

    /// The input or produced structure is not the shape the converter
    /// expects (e.g. the RM value at a template path is not the declared RM
    /// type, or a STRUCTURED document is not a JSON object).
    #[error("simplified-format conversion error: {0}")]
    Conversion(String),

    /// An example COMPOSITION was asked of a template whose root is not a
    /// COMPOSITION (an ENTRY or a SECTION, for example), which no COMPOSITION
    /// conforms to.
    #[error(
        "the template root {root} is not a COMPOSITION, so no example COMPOSITION exists for it"
    )]
    NoExampleForRoot {
        /// The template root's RM type.
        root: String,
    },

    /// A party build was asked of a template whose root is not a party class
    /// (`PERSON`, `ORGANISATION`, `GROUP`, `AGENT` or `ROLE`).
    #[error("the template root {root} is not a PARTY class, so no party builds from it")]
    NotAPartyTemplate {
        /// The template root's RM type.
        root: String,
    },

    /// A party (or other non-COMPOSITION root) document carries a `ctx/` key:
    /// the `ctx/` vocabulary is the COMPOSITION context (ITS-REST
    /// `simplified_formats/master06`), and such a root has none.
    #[error(
        "only a COMPOSITION document takes ctx/ keys: ctx/ is the COMPOSITION context vocabulary"
    )]
    ContextOnParty,

    /// A built party does not decode as its root class; the strict canonical
    /// reader's refusal is the source.
    #[error("the built party does not decode as {class}")]
    PartyDecode {
        /// The party class the build targeted.
        class: String,
        /// The canonical-JSON reader's refusal.
        #[source]
        source: openehr_its::json::JsonParseError,
    },
}