1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "strum",
derive(strum::EnumString, strum::EnumIter, strum::IntoStaticStr)
)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
/// Unterscheidung für hoch- und niedrig-kalorisches Gas.
///
/// # Forward compatibility (H2 blends)
///
/// As of the current schema this enum models only `H_GAS` / `L_GAS`. Hydrogen
/// blend qualities expected from the 2026–2028 DVGW G 260 / BNetzA wave are not
/// yet standardized in BO4E; until they are, such wire values decode to
/// [`Gasqualitaet::Unknown`]. Do **not** hard-code a speculative wire string —
/// when BO4E adds the variant it will appear here (and in the CHANGELOG) with
/// its canonical spelling, and lenient decoding will start resolving it.
#[non_exhaustive]
pub enum Gasqualitaet {
#[cfg_attr(feature = "serde", serde(rename = "H_GAS"))]
#[cfg_attr(feature = "strum", strum(serialize = "H_GAS"))]
HGas,
#[cfg_attr(feature = "serde", serde(rename = "L_GAS"))]
#[cfg_attr(feature = "strum", strum(serialize = "L_GAS"))]
LGas,
/// Unknown or future variant — produced when deserializing a value
/// that is not yet known to this version of the library.
#[cfg_attr(feature = "serde", serde(other, rename = "UNKNOWN"))]
#[cfg_attr(feature = "strum", strum(serialize = "UNKNOWN"))]
Unknown,
}
impl Gasqualitaet {
/// All variants defined by the BO4E schema, in declaration order.
///
/// Excludes the forward-compatibility [`Gasqualitaet::Unknown`] catch-all, so this
/// is exactly the set of values that appear on the wire. Available **without**
/// the `strum` feature — use it to drift-guard SQL `CHECK` lists and mappings.
pub const VARIANTS: &'static [Self] = &[Self::HGas, Self::LGas];
/// Number of schema-defined variants (equal to `VARIANTS.len()`), excluding the
/// [`Gasqualitaet::Unknown`] catch-all. Stable for this schema version.
pub const COUNT: usize = Self::VARIANTS.len();
/// Returns an iterator over all **known** variants of `Gasqualitaet`.
///
/// Yields only variants that correspond to values defined in the BO4E schema
/// (i.e. [`Self::VARIANTS`]), never the [`Gasqualitaet::Unknown`] catch-all.
/// Available **without** the `strum` feature.
///
/// # Example
/// ```rust,ignore
/// for v in Gasqualitaet::iter_known() {
/// println!("{}", v.as_wire());
/// }
/// ```
pub fn iter_known() -> impl Iterator<Item = Self> + Clone {
Self::VARIANTS.iter().copied()
}
/// Returns the canonical BO4E wire string (SCREAMING_SNAKE_CASE) for this value.
///
/// [`Gasqualitaet::Unknown`] renders as `"UNKNOWN"`, matching its serialized form.
pub const fn as_wire(&self) -> &'static str {
match self {
Self::HGas => "H_GAS",
Self::LGas => "L_GAS",
Self::Unknown => "UNKNOWN",
}
}
/// **Strictly** parses a BO4E wire string into a known variant.
///
/// Unlike the lenient `serde` / [`FromStr`](std::str::FromStr) path — which maps
/// any unrecognized value (a typo, a legacy code, or a value from a newer schema)
/// to [`Gasqualitaet::Unknown`] — this returns
/// [`Err`](crate::error::UnknownVariant) for values not defined in this schema
/// version, including the literal `"UNKNOWN"`. Use it at the ingest boundary to
/// reject bad values instead of silently degrading them.
///
/// # Example
/// ```rust,ignore
/// assert!(Gasqualitaet::from_wire("NOT_A_REAL_VALUE").is_err());
/// ```
pub fn from_wire(s: &str) -> Result<Self, crate::error::UnknownVariant> {
match s {
"H_GAS" => Ok(Self::HGas),
"L_GAS" => Ok(Self::LGas),
other => Err(crate::error::UnknownVariant::new(other)),
}
}
/// Returns `true` if this value is the forward-compatibility
/// [`Gasqualitaet::Unknown`] catch-all (an out-of-schema value).
pub const fn is_unknown(&self) -> bool {
matches!(self, Self::Unknown)
}
/// Returns `true` if this value is a known, schema-defined variant.
pub const fn is_known(&self) -> bool {
!self.is_unknown()
}
}
impl std::fmt::Display for Gasqualitaet {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_wire())
}
}
impl AsRef<str> for Gasqualitaet {
fn as_ref(&self) -> &str {
self.as_wire()
}
}
#[cfg(feature = "versioned")]
impl crate::bo4e_enum_sealed::Sealed for Gasqualitaet {}
#[cfg(feature = "versioned")]
impl crate::Bo4eEnum for Gasqualitaet {
const VARIANTS: &'static [Self] = Self::VARIANTS;
const COUNT: usize = Self::COUNT;
fn as_wire(&self) -> &'static str {
Self::as_wire(self)
}
fn from_wire(s: &str) -> Result<Self, crate::error::UnknownVariant> {
Self::from_wire(s)
}
fn is_unknown(&self) -> bool {
Self::is_unknown(self)
}
}
#[cfg(feature = "versioned")]
impl crate::Bo4eStrict for Gasqualitaet {
fn collect_unknown_enums(&self, path: &str, out: &mut Vec<String>) {
if self.is_unknown() {
out.push(path.to_owned());
}
}
}
#[cfg(all(feature = "sqlx", feature = "json"))]
impl sqlx::Type<sqlx::Postgres> for Gasqualitaet {
fn type_info() -> sqlx::postgres::PgTypeInfo {
<String as sqlx::Type<sqlx::Postgres>>::type_info()
}
}
/// Encode via the canonical wire string (`as_wire`, always available) — no
/// `serde_json::Value` intermediate, saving an allocation per encode (M-07).
#[cfg(all(feature = "sqlx", feature = "json"))]
impl<'q> sqlx::Encode<'q, sqlx::Postgres> for Gasqualitaet {
fn encode_by_ref(
&self,
buf: &mut <sqlx::Postgres as sqlx::Database>::ArgumentBuffer<'q>,
) -> Result<sqlx::encode::IsNull, sqlx::error::BoxDynError> {
let s: &str = self.as_wire();
<&str as sqlx::Encode<'q, sqlx::Postgres>>::encode_by_ref(&s, buf)
}
}
#[cfg(all(feature = "sqlx", feature = "json"))]
impl<'r> sqlx::Decode<'r, sqlx::Postgres> for Gasqualitaet {
fn decode(
value: <sqlx::Postgres as sqlx::Database>::ValueRef<'r>,
) -> Result<Self, sqlx::error::BoxDynError> {
let s = <String as sqlx::Decode<sqlx::Postgres>>::decode(value)?;
serde_json::from_value(serde_json::Value::String(s))
.map_err(|e| Box::new(e) as sqlx::error::BoxDynError)
}
}
#[cfg(test)]
impl proptest::arbitrary::Arbitrary for Gasqualitaet {
type Parameters = ();
type Strategy = proptest::strategy::BoxedStrategy<Self>;
fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
use proptest::prelude::*;
proptest::sample::select(Self::VARIANTS.to_vec()).boxed()
}
}