Skip to main content

hydra_common/
elements.rs

1//! Element taxonomy contract: element classes, kind descriptors, and
2//! attribute schemas (spec §4).
3//!
4//! Everything here is *data*. The only structural vocabulary this layer
5//! owns is [`ElementClass`] — the geometric and referential nature of an
6//! element, which an application must know to render it. Everything else
7//! (what a junction or a subcatchment *is*) travels as opaque ids and
8//! engine-authored text, exactly as the recognition and reportable-output
9//! contracts do.
10
11use serde::{Deserialize, Serialize};
12
13use crate::OptionKind;
14
15/// The geometric and referential nature of an element kind (spec §4.1).
16///
17/// The class list is closed in this revision; extending it is an additive
18/// spec change in this layer, never an engine decision. A subcatchment is
19/// the proof case for [`ElementClass::Region`]: it is neither a node nor a
20/// link, and a taxonomy offering only those two classes would have baked
21/// one engine family's shape into the foundation.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(rename_all = "lowercase")]
24pub enum ElementClass {
25    /// A located element: one coordinate. May anchor `Polyline` ends and
26    /// `Region` discharge references.
27    Point,
28    /// A connecting element: references a from-point and a to-point, with
29    /// optional intermediate vertices.
30    Polyline,
31    /// An areal element: a polygon boundary, with an optional reference to
32    /// a `Point` element it discharges to.
33    Region,
34    /// A non-spatial named object (a curve, a pattern, a time series, a
35    /// control). Enumerable and countable; presentation is
36    /// application-defined and may be engine-specific.
37    Collection,
38}
39
40/// What an element kind does in the network, as distinct from what it is
41/// geometrically (spec §4.3).
42///
43/// This exists because it is the distinction an application must draw to
44/// present an *unsimulated* model at all: before any results exist there is
45/// nothing to colour by, and a network drawn in one uniform tone tells a
46/// reader nothing. `ElementClass` cannot answer it — a pump and a pipe are
47/// both `Polyline`, a reservoir and a junction both `Point` — and kind
48/// cannot either without the application naming kinds it should not know.
49///
50/// Carries no presentation. An application decides what a boundary looks
51/// like; this layer decides only which kinds are boundaries.
52///
53/// Optional on `ElementKind`: some kinds have no role in the flow network,
54/// and the absence is information rather than a gap to be defaulted away.
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
56#[serde(rename_all = "lowercase")]
57pub enum ElementRole {
58    /// Carries flow without imposing a boundary or a control on it — a
59    /// junction, a pipe, a conduit. The bulk of any model.
60    Conveyance,
61    /// Where the model meets what it does not simulate: a fixed head or
62    /// stage, a storage volume, an outfall. Flow enters or leaves here.
63    Boundary,
64    /// Acts on the flow rather than merely passing it — a pump, a valve, a
65    /// weir, an orifice, a flow divider.
66    Control,
67}
68
69/// Descriptor of one element kind in an engine's catalog (spec §4.2).
70///
71/// The catalog is static and model-free, like the block catalog: an
72/// application must be able to build its chrome — tables, filters, layer
73/// toggles, legends — before any model is loaded. `id` follows the block-id
74/// stability rule: removing one, or changing the *meaning* of one, is a
75/// break on the order of a file-format break.
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
77#[serde(rename_all = "camelCase")]
78pub struct ElementKind {
79    /// Stable kind identifier, opaque to this layer.
80    pub id: &'static str,
81    /// Human-facing singular name.
82    pub label: &'static str,
83    /// Human-facing plural name.
84    pub label_plural: &'static str,
85    /// The kind's element class.
86    pub class: ElementClass,
87    /// What the kind does in the network (spec §4.3), or `None` for a kind
88    /// that is not in the flow network at all — a rain gage conveys
89    /// nothing, and a curve or a control rule is not in it to begin with.
90    pub role: Option<ElementRole>,
91    /// One- or two-character glyph for dense UI (markers, chips).
92    pub badge: &'static str,
93}
94
95/// Description of one attribute an application may display for elements of
96/// a kind (spec §4.4).
97///
98/// Reuses the option-descriptor value vocabulary ([`OptionKind`], spec
99/// §3.2.1) and is advisory in exactly that sense: it tells a generic UI
100/// what to show; it is not the validation authority, and an engine remains
101/// free to hold data no schema advertises. This revision describes
102/// attributes for **display**; editability, defaults, and creation flows
103/// are a later additive revision.
104#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
105#[serde(rename_all = "camelCase")]
106pub struct AttributeDescriptor {
107    /// Field name in the element's attribute data. Stable per kind;
108    /// renaming one is a break, like a block id.
109    pub key: String,
110    /// Human-facing name.
111    pub label: String,
112    /// Value shape and bounds.
113    pub kind: OptionKind,
114    /// Key of the physical quantity the value carries (spec §5), or `None`
115    /// for dimensionless or textual attributes.
116    pub quantity: Option<String>,
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn element_class_serialises_lowercase() {
125        assert_eq!(
126            serde_json::to_string(&ElementClass::Region).unwrap(),
127            "\"region\""
128        );
129        assert_eq!(
130            serde_json::to_string(&ElementClass::Collection).unwrap(),
131            "\"collection\""
132        );
133    }
134
135    #[test]
136    fn kind_descriptor_serialises_camel_case() {
137        let kind = ElementKind {
138            id: "k",
139            label: "Kind",
140            label_plural: "Kinds",
141            class: ElementClass::Point,
142            role: Some(ElementRole::Boundary),
143            badge: "K",
144        };
145        let json = serde_json::to_value(kind).unwrap();
146        assert_eq!(json["labelPlural"], "Kinds");
147        assert_eq!(json["class"], "point");
148        assert_eq!(json["role"], "boundary");
149    }
150}