Skip to main content

ng_gateway_sdk/
value.rs

1use crate::DataType;
2use base64::Engine as _;
3use bytes::Bytes;
4use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
5use std::{borrow::Cow, sync::Arc};
6
7/// Error returned when converting an `NGValue` into a concrete Rust primitive.
8///
9/// This is designed for protocol codecs and driver control-plane logic.
10#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
11pub enum NGValueCastError {
12    /// Value is not a number (int/float).
13    #[error("expected numeric value, got {actual:?}")]
14    NotNumeric { actual: DataType },
15    /// Numeric value is NaN/Inf and cannot be represented in target type.
16    #[error("numeric value is not finite")]
17    NotFinite,
18    /// Numeric value is out of the representable range of the target type.
19    #[error("numeric value out of range for {target}")]
20    OutOfRange { target: &'static str },
21    /// Strict type mismatch for non-numeric conversions.
22    #[error("type mismatch: expected {expected:?}, got {actual:?}")]
23    TypeMismatch {
24        expected: DataType,
25        actual: DataType,
26    },
27    /// String/Binary value cannot be parsed into the target numeric type.
28    #[error("failed to parse {target} from string: {value}")]
29    ParseError { target: &'static str, value: String },
30    /// Binary value cannot be interpreted as UTF-8 for parsing/formatting.
31    #[error("binary value is not valid UTF-8 for {target}")]
32    InvalidUtf8 { target: &'static str },
33    /// Binary value length does not match the expected fixed-width for this cast.
34    #[error("binary length mismatch for {target}: expected {expected} bytes, got {len}")]
35    InvalidBinaryLength {
36        target: &'static str,
37        expected: &'static str,
38        len: usize,
39    },
40}
41
42/// JSON encoding strategy for binary values.
43///
44/// This only affects `NGValue::Binary`.
45#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
46pub enum BinaryJsonEncoding {
47    /// Encode binary as Base64 string.
48    Base64,
49    /// Encode binary as hex string.
50    ///
51    /// This is the **global default** encoding.
52    #[default]
53    Hex,
54}
55
56/// JSON encoding strategy for timestamp values.
57///
58/// This only affects `NGValue::Timestamp`.
59#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
60pub enum TimestampJsonEncoding {
61    /// Encode timestamp as Unix milliseconds (number).
62    #[default]
63    UnixMs,
64    /// Encode timestamp as RFC3339 string (UTC).
65    ///
66    /// NOTE: This requires conversion to `chrono::DateTime<Utc>` at encoding time.
67    Rfc3339Utc,
68}
69
70/// JSON encoding options for `NGValue`.
71///
72/// This is intentionally lightweight and copyable so callers can keep it on stack.
73#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
74pub struct NGValueJsonOptions {
75    /// Binary encoding strategy.
76    pub binary: BinaryJsonEncoding,
77    /// Timestamp encoding strategy.
78    pub timestamp: TimestampJsonEncoding,
79}
80
81/// A strongly-typed runtime value for telemetry/attributes.
82///
83/// # Performance goals
84/// - No `serde_json::Value` on hot paths
85/// - Shared string storage (`Arc<str>`) to reduce cloning cost
86/// - Zero-copy binary payloads (`Bytes`)
87///
88/// # Timestamp semantics
89/// `NGValue::Timestamp(i64)` represents **Unix time in milliseconds** by default.
90#[derive(Clone, Debug, PartialEq)]
91pub enum NGValue {
92    Boolean(bool),
93    Int8(i8),
94    UInt8(u8),
95    Int16(i16),
96    UInt16(u16),
97    Int32(i32),
98    UInt32(u32),
99    Int64(i64),
100    UInt64(u64),
101    Float32(f32),
102    Float64(f64),
103    String(Arc<str>),
104    Binary(Bytes),
105    Timestamp(i64),
106}
107
108/// Serialize `NGValue` into JSON with **default** encoding semantics.
109///
110/// # Semantics (must be stable)
111/// This implementation is intentionally aligned with `NGValue::to_json_value(NGValueJsonOptions::default())`:
112/// - `Binary` is encoded as **hex** string
113/// - `Timestamp` is encoded as **Unix milliseconds** number
114/// - Non-finite floats (NaN/Inf) are encoded as **null**
115///
116/// # Why
117/// Northward plugins write JSON on a hot path. Implementing `Serialize` allows
118/// `serde_json::to_writer` to stream values without allocating intermediate
119/// `serde_json::Value` objects.
120impl Serialize for NGValue {
121    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
122    where
123        S: Serializer,
124    {
125        match self {
126            NGValue::Boolean(v) => serializer.serialize_bool(*v),
127            NGValue::Int8(v) => serializer.serialize_i8(*v),
128            NGValue::UInt8(v) => serializer.serialize_u8(*v),
129            NGValue::Int16(v) => serializer.serialize_i16(*v),
130            NGValue::UInt16(v) => serializer.serialize_u16(*v),
131            NGValue::Int32(v) => serializer.serialize_i32(*v),
132            NGValue::UInt32(v) => serializer.serialize_u32(*v),
133            NGValue::Int64(v) => serializer.serialize_i64(*v),
134            NGValue::UInt64(v) => serializer.serialize_u64(*v),
135            NGValue::Float32(v) => {
136                if v.is_finite() {
137                    serializer.serialize_f32(*v)
138                } else {
139                    serializer.serialize_unit()
140                }
141            }
142            NGValue::Float64(v) => {
143                if v.is_finite() {
144                    serializer.serialize_f64(*v)
145                } else {
146                    serializer.serialize_unit()
147                }
148            }
149            NGValue::String(v) => serializer.serialize_str(v.as_ref()),
150            NGValue::Binary(v) => {
151                let s = hex::encode(v.as_ref());
152                serializer.serialize_str(&s)
153            }
154            NGValue::Timestamp(v) => serializer.serialize_i64(*v),
155        }
156    }
157}
158
159impl NGValue {
160    /// Return the corresponding SDK `DataType` for this value.
161    #[inline]
162    pub fn data_type(&self) -> DataType {
163        match self {
164            NGValue::Boolean(_) => DataType::Boolean,
165            NGValue::Int8(_) => DataType::Int8,
166            NGValue::UInt8(_) => DataType::UInt8,
167            NGValue::Int16(_) => DataType::Int16,
168            NGValue::UInt16(_) => DataType::UInt16,
169            NGValue::Int32(_) => DataType::Int32,
170            NGValue::UInt32(_) => DataType::UInt32,
171            NGValue::Int64(_) => DataType::Int64,
172            NGValue::UInt64(_) => DataType::UInt64,
173            NGValue::Float32(_) => DataType::Float32,
174            NGValue::Float64(_) => DataType::Float64,
175            NGValue::String(_) => DataType::String,
176            NGValue::Binary(_) => DataType::Binary,
177            NGValue::Timestamp(_) => DataType::Timestamp,
178        }
179    }
180
181    /// Validate whether this value matches the expected `DataType`.
182    ///
183    /// This is a strict check (no implicit casting).
184    #[inline]
185    pub fn validate_datatype(&self, expected: DataType) -> bool {
186        self.data_type() == expected
187    }
188
189    /// Convert this typed value into a `serde_json::Value` for northbound protocols.
190    ///
191    /// This conversion is intended for encoding stage (northbound plugins) and
192    /// should not be used on the hot path inside collectors/snapshots.
193    pub fn to_json_value(&self, opts: NGValueJsonOptions) -> serde_json::Value {
194        match self {
195            NGValue::Boolean(v) => serde_json::Value::Bool(*v),
196            NGValue::Int8(v) => serde_json::Value::Number((*v as i64).into()),
197            NGValue::UInt8(v) => serde_json::Value::Number((*v as u64).into()),
198            NGValue::Int16(v) => serde_json::Value::Number((*v as i64).into()),
199            NGValue::UInt16(v) => serde_json::Value::Number((*v as u64).into()),
200            NGValue::Int32(v) => serde_json::Value::Number((*v as i64).into()),
201            NGValue::UInt32(v) => serde_json::Value::Number((*v as u64).into()),
202            NGValue::Int64(v) => serde_json::Value::Number((*v).into()),
203            NGValue::UInt64(v) => serde_json::Value::Number((*v).into()),
204            NGValue::Float32(v) => {
205                serde_json::Number::from_f64(*v as f64).map_or(serde_json::Value::Null, Into::into)
206            }
207            NGValue::Float64(v) => {
208                serde_json::Number::from_f64(*v).map_or(serde_json::Value::Null, Into::into)
209            }
210            NGValue::String(v) => serde_json::Value::String(v.to_string()),
211            NGValue::Binary(v) => {
212                let s = match opts.binary {
213                    BinaryJsonEncoding::Base64 => {
214                        base64::engine::general_purpose::STANDARD.encode(v.as_ref())
215                    }
216                    BinaryJsonEncoding::Hex => hex::encode(v.as_ref()),
217                };
218                serde_json::Value::String(s)
219            }
220            NGValue::Timestamp(v) => match opts.timestamp {
221                TimestampJsonEncoding::UnixMs => serde_json::Value::Number((*v).into()),
222                TimestampJsonEncoding::Rfc3339Utc => {
223                    // Avoid panicking on out-of-range timestamp by falling back to number.
224                    match chrono::DateTime::<chrono::Utc>::from_timestamp_millis(*v) {
225                        Some(dt) => serde_json::Value::String(dt.to_rfc3339()),
226                        None => serde_json::Value::Number((*v).into()),
227                    }
228                }
229            },
230        }
231    }
232
233    /// Try to convert a JSON scalar into a strongly-typed `NGValue` with an expected `DataType`.
234    ///
235    /// # Purpose
236    /// This helper exists as a **transitional compatibility layer** while migrating
237    /// southbound drivers away from `serde_json::Value` hot paths.
238    ///
239    /// # Rules
240    /// - Strict: no implicit allocations for non-string inputs (e.g. a number will NOT be
241    ///   converted into `NGValue::String`)
242    /// - Strict: arrays/objects are rejected (return `None`)
243    /// - Numeric conversions perform range checks for the target integer width
244    ///
245    /// # Performance
246    /// - Intended to be used on the encoding boundary only (not inside core snapshot/update hot loops)
247    /// - For true zero-allocation hot paths, drivers should produce `NGValue` directly.
248    #[inline]
249    pub fn try_from_json_scalar(expected: DataType, v: &serde_json::Value) -> Option<Self> {
250        match expected {
251            DataType::Boolean => {
252                if let Some(b) = v.as_bool() {
253                    return Some(NGValue::Boolean(b));
254                }
255                if let Some(i) = v.as_i64() {
256                    return Some(NGValue::Boolean(i != 0));
257                }
258                if let Some(u) = v.as_u64() {
259                    return Some(NGValue::Boolean(u != 0));
260                }
261                None
262            }
263            DataType::Int8 => v
264                .as_i64()
265                .and_then(|n| i8::try_from(n).ok())
266                .map(NGValue::Int8),
267            DataType::UInt8 => v
268                .as_u64()
269                .and_then(|n| u8::try_from(n).ok())
270                .map(NGValue::UInt8),
271            DataType::Int16 => v
272                .as_i64()
273                .and_then(|n| i16::try_from(n).ok())
274                .map(NGValue::Int16),
275            DataType::UInt16 => v
276                .as_u64()
277                .and_then(|n| u16::try_from(n).ok())
278                .map(NGValue::UInt16),
279            DataType::Int32 => v
280                .as_i64()
281                .and_then(|n| i32::try_from(n).ok())
282                .map(NGValue::Int32),
283            DataType::UInt32 => v
284                .as_u64()
285                .and_then(|n| u32::try_from(n).ok())
286                .map(NGValue::UInt32),
287            DataType::Int64 => v.as_i64().map(NGValue::Int64),
288            DataType::UInt64 => v.as_u64().map(NGValue::UInt64),
289            DataType::Float32 => v.as_f64().map(|n| NGValue::Float32(n as f32)),
290            DataType::Float64 => v.as_f64().map(NGValue::Float64),
291            DataType::String => v.as_str().map(|s| NGValue::String(Arc::<str>::from(s))),
292            DataType::Binary => {
293                let s = v.as_str()?;
294                let st = s.trim();
295                // Prefer hex with 0x prefix (common in industrial drivers).
296                let bytes = if st.starts_with("0x") || st.starts_with("0X") {
297                    let hex = &st[2..];
298                    if hex.is_empty() {
299                        Vec::new()
300                    } else {
301                        hex::decode(hex).ok()?
302                    }
303                } else {
304                    // Fallback: base64
305                    base64::engine::general_purpose::STANDARD.decode(st).ok()?
306                };
307                Some(NGValue::Binary(Bytes::from(bytes)))
308            }
309            DataType::Timestamp => {
310                if let Some(i) = v.as_i64() {
311                    return Some(NGValue::Timestamp(i));
312                }
313                if let Some(u) = v.as_u64() {
314                    return Some(NGValue::Timestamp(u as i64));
315                }
316                if let Some(s) = v.as_str() {
317                    if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(s.trim()) {
318                        return Some(NGValue::Timestamp(dt.timestamp_millis()));
319                    }
320                }
321                None
322            }
323        }
324    }
325}
326
327// === Best-practice cast APIs for protocol codecs ===
328//
329// Policy:
330// - Integer targets accept integer/float inputs; floats are rounded (`round()`).
331// - Boolean is strict: only `NGValue::Boolean` is accepted (no implicit numeric mapping).
332#[inline]
333fn parse_bool_from_str(s: &str) -> Option<bool> {
334    let st = s.trim().to_ascii_lowercase();
335    match st.as_str() {
336        "true" | "1" | "on" | "yes" | "y" | "t" => Some(true),
337        "false" | "0" | "off" | "no" | "n" | "f" => Some(false),
338        _ => None,
339    }
340}
341
342#[inline]
343fn binary_len_err(target: &'static str, expected: &'static str, len: usize) -> NGValueCastError {
344    NGValueCastError::InvalidBinaryLength {
345        target,
346        expected,
347        len,
348    }
349}
350
351#[inline]
352fn binary_to_u8(b: &Bytes, target: &'static str) -> Result<u8, NGValueCastError> {
353    if !b.is_empty() {
354        Ok(b[0])
355    } else {
356        Err(binary_len_err(target, "1", b.len()))
357    }
358}
359
360#[inline]
361fn binary_to_i8(b: &Bytes, target: &'static str) -> Result<i8, NGValueCastError> {
362    if !b.is_empty() {
363        Ok(i8::from_be_bytes([b[0]]))
364    } else {
365        Err(binary_len_err(target, "1", b.len()))
366    }
367}
368
369#[inline]
370fn binary_to_i16_be(b: &Bytes, target: &'static str) -> Result<i16, NGValueCastError> {
371    if b.len() >= 2 {
372        Ok(i16::from_be_bytes([b[0], b[1]]))
373    } else {
374        Err(binary_len_err(target, "2", b.len()))
375    }
376}
377
378#[inline]
379fn binary_to_i32_be(b: &Bytes, target: &'static str) -> Result<i32, NGValueCastError> {
380    if b.len() >= 4 {
381        Ok(i32::from_be_bytes([b[0], b[1], b[2], b[3]]))
382    } else {
383        Err(binary_len_err(target, "4", b.len()))
384    }
385}
386
387#[inline]
388fn binary_to_i64_be(b: &Bytes, target: &'static str) -> Result<i64, NGValueCastError> {
389    if b.len() >= 8 {
390        Ok(i64::from_be_bytes([
391            b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
392        ]))
393    } else {
394        Err(binary_len_err(target, "8", b.len()))
395    }
396}
397
398#[inline]
399fn binary_to_u16_be(b: &Bytes, target: &'static str) -> Result<u16, NGValueCastError> {
400    if b.len() >= 2 {
401        Ok(u16::from_be_bytes([b[0], b[1]]))
402    } else {
403        Err(binary_len_err(target, "2", b.len()))
404    }
405}
406
407#[inline]
408fn binary_to_u32_be(b: &Bytes, target: &'static str) -> Result<u32, NGValueCastError> {
409    if b.len() >= 4 {
410        Ok(u32::from_be_bytes([b[0], b[1], b[2], b[3]]))
411    } else {
412        Err(binary_len_err(target, "4", b.len()))
413    }
414}
415
416#[inline]
417fn binary_to_u64_be(b: &Bytes, target: &'static str) -> Result<u64, NGValueCastError> {
418    if b.len() >= 8 {
419        Ok(u64::from_be_bytes([
420            b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
421        ]))
422    } else {
423        Err(binary_len_err(target, "8", b.len()))
424    }
425}
426
427#[inline]
428fn parse_f64_from_str(target: &'static str, s: &str) -> Result<f64, NGValueCastError> {
429    let st = s.trim();
430    if st.is_empty() {
431        return Err(NGValueCastError::ParseError {
432            target,
433            value: st.to_string(),
434        });
435    }
436
437    // Support common "0x..." hex integers (e.g. configs / PLC parameters).
438    let parsed = if let Some(hex) = st.strip_prefix("0x").or(st.strip_prefix("0X")) {
439        let n = u64::from_str_radix(hex.trim(), 16).map_err(|_| NGValueCastError::ParseError {
440            target,
441            value: st.to_string(),
442        })?;
443        n as f64
444    } else {
445        st.parse::<f64>()
446            .map_err(|_| NGValueCastError::ParseError {
447                target,
448                value: st.to_string(),
449            })?
450    };
451
452    if !parsed.is_finite() {
453        return Err(NGValueCastError::NotFinite);
454    }
455    Ok(parsed)
456}
457
458#[inline]
459fn parse_i64_from_str(target: &'static str, s: &str) -> Result<i64, NGValueCastError> {
460    let st = s.trim();
461    if st.is_empty() {
462        return Err(NGValueCastError::ParseError {
463            target,
464            value: st.to_string(),
465        });
466    }
467
468    // Fast path: decimal integer.
469    if let Ok(n) = st.parse::<i64>() {
470        return Ok(n);
471    }
472
473    // Hex integer, allow +/-0x...
474    let (sign, rest) = match st.as_bytes().first().copied() {
475        Some(b'+') => (1i128, &st[1..]),
476        Some(b'-') => (-1i128, &st[1..]),
477        _ => (1i128, st),
478    };
479    if let Some(hex) = rest.strip_prefix("0x").or(rest.strip_prefix("0X")) {
480        let u = u64::from_str_radix(hex.trim(), 16).map_err(|_| NGValueCastError::ParseError {
481            target,
482            value: st.to_string(),
483        })? as i128;
484        let signed = sign * u;
485        if signed < i64::MIN as i128 || signed > i64::MAX as i128 {
486            return Err(NGValueCastError::OutOfRange { target });
487        }
488        return Ok(signed as i64);
489    }
490
491    // Fallback: parse float then apply the same rounding policy as float->int casts.
492    let f = parse_f64_from_str(target, st)?;
493    let r = f.round();
494    if r < i64::MIN as f64 || r > i64::MAX as f64 {
495        return Err(NGValueCastError::OutOfRange { target });
496    }
497    Ok(r as i64)
498}
499
500#[inline]
501fn parse_u64_from_str(target: &'static str, s: &str) -> Result<u64, NGValueCastError> {
502    let st = s.trim();
503    if st.is_empty() {
504        return Err(NGValueCastError::ParseError {
505            target,
506            value: st.to_string(),
507        });
508    }
509
510    // Fast path: decimal integer.
511    if let Ok(n) = st.parse::<u64>() {
512        return Ok(n);
513    }
514
515    // Hex integer (no sign for u64).
516    if let Some(hex) = st.strip_prefix("0x").or(st.strip_prefix("0X")) {
517        return u64::from_str_radix(hex.trim(), 16).map_err(|_| NGValueCastError::ParseError {
518            target,
519            value: st.to_string(),
520        });
521    }
522
523    // Fallback: parse float then apply rounding.
524    let f = parse_f64_from_str(target, st)?;
525    let r = f.round();
526    if r < 0.0 || r > u64::MAX as f64 {
527        return Err(NGValueCastError::OutOfRange { target });
528    }
529    Ok(r as u64)
530}
531
532impl TryFrom<&NGValue> for bool {
533    type Error = NGValueCastError;
534
535    #[inline]
536    fn try_from(v: &NGValue) -> Result<Self, Self::Error> {
537        match v {
538            NGValue::Boolean(b) => Ok(*b),
539            // Numeric-like: 0 => false, non-zero => true
540            NGValue::Int8(x) => Ok(*x != 0),
541            NGValue::UInt8(x) => Ok(*x != 0),
542            NGValue::Int16(x) => Ok(*x != 0),
543            NGValue::UInt16(x) => Ok(*x != 0),
544            NGValue::Int32(x) => Ok(*x != 0),
545            NGValue::UInt32(x) => Ok(*x != 0),
546            NGValue::Int64(x) => Ok(*x != 0),
547            NGValue::UInt64(x) => Ok(*x != 0),
548            NGValue::Float32(x) => {
549                let f = *x as f64;
550                if !f.is_finite() {
551                    return Err(NGValueCastError::NotFinite);
552                }
553                Ok(f != 0.0)
554            }
555            NGValue::Float64(x) => {
556                if !x.is_finite() {
557                    return Err(NGValueCastError::NotFinite);
558                }
559                Ok(*x != 0.0)
560            }
561            NGValue::Timestamp(ms) => Ok(*ms != 0),
562            NGValue::String(s) => {
563                parse_bool_from_str(s.as_ref()).ok_or(NGValueCastError::ParseError {
564                    target: "bool",
565                    value: s.to_string(),
566                })
567            }
568            // Binary: treat as raw bytes, not UTF-8 digits.
569            // - empty => false
570            // - any non-zero byte => true
571            NGValue::Binary(b) => Ok(b.as_ref().iter().any(|x| *x != 0)),
572        }
573    }
574}
575
576impl TryFrom<&NGValue> for i8 {
577    type Error = NGValueCastError;
578
579    #[inline]
580    fn try_from(v: &NGValue) -> Result<Self, Self::Error> {
581        match v {
582            NGValue::Boolean(x) => Ok(if *x { 1 } else { 0 }),
583            NGValue::Int8(x) => Ok(*x),
584            NGValue::UInt8(x) => {
585                i8::try_from(*x).map_err(|_| NGValueCastError::OutOfRange { target: "i8" })
586            }
587            NGValue::Int16(x) => {
588                i8::try_from(*x).map_err(|_| NGValueCastError::OutOfRange { target: "i8" })
589            }
590            NGValue::UInt16(x) => {
591                i8::try_from(*x).map_err(|_| NGValueCastError::OutOfRange { target: "i8" })
592            }
593            NGValue::Int32(x) => {
594                i8::try_from(*x).map_err(|_| NGValueCastError::OutOfRange { target: "i8" })
595            }
596            NGValue::UInt32(x) => {
597                i8::try_from(*x).map_err(|_| NGValueCastError::OutOfRange { target: "i8" })
598            }
599            NGValue::Int64(x) => {
600                i8::try_from(*x).map_err(|_| NGValueCastError::OutOfRange { target: "i8" })
601            }
602            NGValue::UInt64(x) => {
603                i8::try_from(*x).map_err(|_| NGValueCastError::OutOfRange { target: "i8" })
604            }
605            NGValue::Float32(x) => {
606                let r = (*x as f64).round();
607                if !r.is_finite() {
608                    return Err(NGValueCastError::NotFinite);
609                }
610                if r >= i8::MIN as f64 && r <= i8::MAX as f64 {
611                    Ok(r as i8)
612                } else {
613                    Err(NGValueCastError::OutOfRange { target: "i8" })
614                }
615            }
616            NGValue::Float64(x) => {
617                let r = x.round();
618                if !r.is_finite() {
619                    return Err(NGValueCastError::NotFinite);
620                }
621                if r >= i8::MIN as f64 && r <= i8::MAX as f64 {
622                    Ok(r as i8)
623                } else {
624                    Err(NGValueCastError::OutOfRange { target: "i8" })
625                }
626            }
627            NGValue::Timestamp(ms) => {
628                i8::try_from(*ms).map_err(|_| NGValueCastError::OutOfRange { target: "i8" })
629            }
630            NGValue::String(s) => {
631                let n = parse_i64_from_str("i8", s.as_ref())?;
632                i8::try_from(n).map_err(|_| NGValueCastError::OutOfRange { target: "i8" })
633            }
634            NGValue::Binary(b) => binary_to_i8(b, "i8"),
635        }
636    }
637}
638
639impl TryFrom<&NGValue> for u8 {
640    type Error = NGValueCastError;
641
642    #[inline]
643    fn try_from(v: &NGValue) -> Result<Self, Self::Error> {
644        match v {
645            NGValue::Boolean(x) => {
646                if *x {
647                    Ok(1)
648                } else {
649                    Ok(0)
650                }
651            }
652            NGValue::UInt8(x) => Ok(*x),
653            NGValue::Int8(x) => {
654                u8::try_from(*x).map_err(|_| NGValueCastError::OutOfRange { target: "u8" })
655            }
656            NGValue::UInt16(x) => {
657                u8::try_from(*x).map_err(|_| NGValueCastError::OutOfRange { target: "u8" })
658            }
659            NGValue::Int16(x) => {
660                u8::try_from(*x).map_err(|_| NGValueCastError::OutOfRange { target: "u8" })
661            }
662            NGValue::UInt32(x) => {
663                u8::try_from(*x).map_err(|_| NGValueCastError::OutOfRange { target: "u8" })
664            }
665            NGValue::Int32(x) => {
666                u8::try_from(*x).map_err(|_| NGValueCastError::OutOfRange { target: "u8" })
667            }
668            NGValue::UInt64(x) => {
669                u8::try_from(*x).map_err(|_| NGValueCastError::OutOfRange { target: "u8" })
670            }
671            NGValue::Int64(x) => {
672                u8::try_from(*x).map_err(|_| NGValueCastError::OutOfRange { target: "u8" })
673            }
674            NGValue::Float32(x) => {
675                let r = (*x as f64).round();
676                if !r.is_finite() {
677                    return Err(NGValueCastError::NotFinite);
678                }
679                if r >= 0.0 && r <= u8::MAX as f64 {
680                    Ok(r as u8)
681                } else {
682                    Err(NGValueCastError::OutOfRange { target: "u8" })
683                }
684            }
685            NGValue::Float64(x) => {
686                let r = x.round();
687                if !r.is_finite() {
688                    return Err(NGValueCastError::NotFinite);
689                }
690                if r >= 0.0 && r <= u8::MAX as f64 {
691                    Ok(r as u8)
692                } else {
693                    Err(NGValueCastError::OutOfRange { target: "u8" })
694                }
695            }
696            NGValue::Timestamp(ms) => {
697                u8::try_from(*ms).map_err(|_| NGValueCastError::OutOfRange { target: "u8" })
698            }
699            NGValue::String(s) => {
700                let n = parse_u64_from_str("u8", s.as_ref())?;
701                u8::try_from(n).map_err(|_| NGValueCastError::OutOfRange { target: "u8" })
702            }
703            NGValue::Binary(b) => binary_to_u8(b, "u8"),
704        }
705    }
706}
707
708impl TryFrom<&NGValue> for u16 {
709    type Error = NGValueCastError;
710
711    #[inline]
712    fn try_from(v: &NGValue) -> Result<Self, Self::Error> {
713        match v {
714            NGValue::Boolean(x) => Ok(if *x { 1 } else { 0 }),
715            NGValue::UInt16(x) => Ok(*x),
716            NGValue::UInt8(x) => Ok(*x as u16),
717            NGValue::Int8(x) => {
718                u16::try_from(*x).map_err(|_| NGValueCastError::OutOfRange { target: "u16" })
719            }
720            NGValue::Int16(x) => {
721                u16::try_from(*x).map_err(|_| NGValueCastError::OutOfRange { target: "u16" })
722            }
723            NGValue::Int32(x) => {
724                u16::try_from(*x).map_err(|_| NGValueCastError::OutOfRange { target: "u16" })
725            }
726            NGValue::UInt32(x) => {
727                u16::try_from(*x).map_err(|_| NGValueCastError::OutOfRange { target: "u16" })
728            }
729            NGValue::Int64(x) => {
730                u16::try_from(*x).map_err(|_| NGValueCastError::OutOfRange { target: "u16" })
731            }
732            NGValue::UInt64(x) => {
733                u16::try_from(*x).map_err(|_| NGValueCastError::OutOfRange { target: "u16" })
734            }
735            NGValue::Float32(x) => {
736                let r = (*x as f64).round();
737                if !r.is_finite() {
738                    return Err(NGValueCastError::NotFinite);
739                }
740                if r >= 0.0 && r <= u16::MAX as f64 {
741                    Ok(r as u16)
742                } else {
743                    Err(NGValueCastError::OutOfRange { target: "u16" })
744                }
745            }
746            NGValue::Float64(x) => {
747                let r = x.round();
748                if !r.is_finite() {
749                    return Err(NGValueCastError::NotFinite);
750                }
751                if r >= 0.0 && r <= u16::MAX as f64 {
752                    Ok(r as u16)
753                } else {
754                    Err(NGValueCastError::OutOfRange { target: "u16" })
755                }
756            }
757            NGValue::Timestamp(ms) => {
758                u16::try_from(*ms).map_err(|_| NGValueCastError::OutOfRange { target: "u16" })
759            }
760            NGValue::String(s) => {
761                let n = parse_u64_from_str("u16", s.as_ref())?;
762                u16::try_from(n).map_err(|_| NGValueCastError::OutOfRange { target: "u16" })
763            }
764            NGValue::Binary(b) => binary_to_u16_be(b, "u16"),
765        }
766    }
767}
768
769impl TryFrom<&NGValue> for i16 {
770    type Error = NGValueCastError;
771
772    #[inline]
773    fn try_from(v: &NGValue) -> Result<Self, Self::Error> {
774        match v {
775            NGValue::Boolean(x) => {
776                if *x {
777                    Ok(1)
778                } else {
779                    Ok(0)
780                }
781            }
782            NGValue::Int16(x) => Ok(*x),
783            NGValue::Int8(x) => Ok(*x as i16),
784            NGValue::UInt8(x) => Ok(*x as i16),
785            NGValue::UInt16(x) => {
786                i16::try_from(*x).map_err(|_| NGValueCastError::OutOfRange { target: "i16" })
787            }
788            NGValue::Int32(x) => {
789                i16::try_from(*x).map_err(|_| NGValueCastError::OutOfRange { target: "i16" })
790            }
791            NGValue::UInt32(x) => {
792                i16::try_from(*x).map_err(|_| NGValueCastError::OutOfRange { target: "i16" })
793            }
794            NGValue::Int64(x) => {
795                i16::try_from(*x).map_err(|_| NGValueCastError::OutOfRange { target: "i16" })
796            }
797            NGValue::UInt64(x) => {
798                i16::try_from(*x).map_err(|_| NGValueCastError::OutOfRange { target: "i16" })
799            }
800            NGValue::Float32(x) => {
801                let r = (*x as f64).round();
802                if !r.is_finite() {
803                    return Err(NGValueCastError::NotFinite);
804                }
805                if r >= i16::MIN as f64 && r <= i16::MAX as f64 {
806                    Ok(r as i16)
807                } else {
808                    Err(NGValueCastError::OutOfRange { target: "i16" })
809                }
810            }
811            NGValue::Float64(x) => {
812                let r = x.round();
813                if !r.is_finite() {
814                    return Err(NGValueCastError::NotFinite);
815                }
816                if r >= i16::MIN as f64 && r <= i16::MAX as f64 {
817                    Ok(r as i16)
818                } else {
819                    Err(NGValueCastError::OutOfRange { target: "i16" })
820                }
821            }
822            NGValue::Timestamp(ms) => {
823                i16::try_from(*ms).map_err(|_| NGValueCastError::OutOfRange { target: "i16" })
824            }
825            NGValue::String(s) => {
826                let n = parse_i64_from_str("i16", s.as_ref())?;
827                i16::try_from(n).map_err(|_| NGValueCastError::OutOfRange { target: "i16" })
828            }
829            NGValue::Binary(b) => binary_to_i16_be(b, "i16"),
830        }
831    }
832}
833
834impl TryFrom<&NGValue> for i32 {
835    type Error = NGValueCastError;
836
837    #[inline]
838    fn try_from(v: &NGValue) -> Result<Self, Self::Error> {
839        match v {
840            NGValue::Boolean(x) => {
841                if *x {
842                    Ok(1)
843                } else {
844                    Ok(0)
845                }
846            }
847            NGValue::Int32(x) => Ok(*x),
848            NGValue::Int16(x) => Ok(*x as i32),
849            NGValue::Int8(x) => Ok(*x as i32),
850            NGValue::UInt8(x) => Ok(*x as i32),
851            NGValue::UInt16(x) => Ok(*x as i32),
852            NGValue::UInt32(x) => {
853                i32::try_from(*x).map_err(|_| NGValueCastError::OutOfRange { target: "i32" })
854            }
855            NGValue::Int64(x) => {
856                i32::try_from(*x).map_err(|_| NGValueCastError::OutOfRange { target: "i32" })
857            }
858            NGValue::UInt64(x) => {
859                i32::try_from(*x).map_err(|_| NGValueCastError::OutOfRange { target: "i32" })
860            }
861            NGValue::Float32(x) => {
862                let r = (*x as f64).round();
863                if !r.is_finite() {
864                    return Err(NGValueCastError::NotFinite);
865                }
866                if r >= i32::MIN as f64 && r <= i32::MAX as f64 {
867                    Ok(r as i32)
868                } else {
869                    Err(NGValueCastError::OutOfRange { target: "i32" })
870                }
871            }
872            NGValue::Float64(x) => {
873                let r = x.round();
874                if !r.is_finite() {
875                    return Err(NGValueCastError::NotFinite);
876                }
877                if r >= i32::MIN as f64 && r <= i32::MAX as f64 {
878                    Ok(r as i32)
879                } else {
880                    Err(NGValueCastError::OutOfRange { target: "i32" })
881                }
882            }
883            NGValue::Timestamp(ms) => {
884                i32::try_from(*ms).map_err(|_| NGValueCastError::OutOfRange { target: "i32" })
885            }
886            NGValue::String(s) => {
887                let n = parse_i64_from_str("i32", s.as_ref())?;
888                i32::try_from(n).map_err(|_| NGValueCastError::OutOfRange { target: "i32" })
889            }
890            NGValue::Binary(b) => binary_to_i32_be(b, "i32"),
891        }
892    }
893}
894
895impl TryFrom<&NGValue> for u32 {
896    type Error = NGValueCastError;
897
898    #[inline]
899    fn try_from(v: &NGValue) -> Result<Self, Self::Error> {
900        match v {
901            NGValue::Boolean(x) => Ok(if *x { 1 } else { 0 }),
902            NGValue::UInt32(x) => Ok(*x),
903            NGValue::UInt16(x) => Ok(*x as u32),
904            NGValue::UInt8(x) => Ok(*x as u32),
905            NGValue::Int8(x) => {
906                u32::try_from(*x).map_err(|_| NGValueCastError::OutOfRange { target: "u32" })
907            }
908            NGValue::Int16(x) => {
909                u32::try_from(*x).map_err(|_| NGValueCastError::OutOfRange { target: "u32" })
910            }
911            NGValue::Int32(x) => {
912                u32::try_from(*x).map_err(|_| NGValueCastError::OutOfRange { target: "u32" })
913            }
914            NGValue::Int64(x) => {
915                u32::try_from(*x).map_err(|_| NGValueCastError::OutOfRange { target: "u32" })
916            }
917            NGValue::UInt64(x) => {
918                u32::try_from(*x).map_err(|_| NGValueCastError::OutOfRange { target: "u32" })
919            }
920            NGValue::Float32(x) => {
921                let r = (*x as f64).round();
922                if !r.is_finite() {
923                    return Err(NGValueCastError::NotFinite);
924                }
925                if r >= 0.0 && r <= u32::MAX as f64 {
926                    Ok(r as u32)
927                } else {
928                    Err(NGValueCastError::OutOfRange { target: "u32" })
929                }
930            }
931            NGValue::Float64(x) => {
932                let r = x.round();
933                if !r.is_finite() {
934                    return Err(NGValueCastError::NotFinite);
935                }
936                if r >= 0.0 && r <= u32::MAX as f64 {
937                    Ok(r as u32)
938                } else {
939                    Err(NGValueCastError::OutOfRange { target: "u32" })
940                }
941            }
942            NGValue::Timestamp(ms) => {
943                u32::try_from(*ms).map_err(|_| NGValueCastError::OutOfRange { target: "u32" })
944            }
945            NGValue::String(s) => {
946                let n = parse_u64_from_str("u32", s.as_ref())?;
947                u32::try_from(n).map_err(|_| NGValueCastError::OutOfRange { target: "u32" })
948            }
949            NGValue::Binary(b) => binary_to_u32_be(b, "u32"),
950        }
951    }
952}
953
954impl TryFrom<&NGValue> for i64 {
955    type Error = NGValueCastError;
956
957    #[inline]
958    fn try_from(v: &NGValue) -> Result<Self, Self::Error> {
959        match v {
960            NGValue::Boolean(x) => {
961                if *x {
962                    Ok(1)
963                } else {
964                    Ok(0)
965                }
966            }
967            NGValue::Int64(x) => Ok(*x),
968            NGValue::Int32(x) => Ok(*x as i64),
969            NGValue::Int16(x) => Ok(*x as i64),
970            NGValue::Int8(x) => Ok(*x as i64),
971            NGValue::UInt8(x) => Ok(*x as i64),
972            NGValue::UInt16(x) => Ok(*x as i64),
973            NGValue::UInt32(x) => Ok(*x as i64),
974            NGValue::UInt64(x) => {
975                i64::try_from(*x).map_err(|_| NGValueCastError::OutOfRange { target: "i64" })
976            }
977            NGValue::Float32(x) => {
978                let r = (*x as f64).round();
979                if !r.is_finite() {
980                    return Err(NGValueCastError::NotFinite);
981                }
982                if r >= i64::MIN as f64 && r <= i64::MAX as f64 {
983                    Ok(r as i64)
984                } else {
985                    Err(NGValueCastError::OutOfRange { target: "i64" })
986                }
987            }
988            NGValue::Float64(x) => {
989                let r = x.round();
990                if !r.is_finite() {
991                    return Err(NGValueCastError::NotFinite);
992                }
993                if r >= i64::MIN as f64 && r <= i64::MAX as f64 {
994                    Ok(r as i64)
995                } else {
996                    Err(NGValueCastError::OutOfRange { target: "i64" })
997                }
998            }
999            NGValue::Timestamp(ms) => Ok(*ms),
1000            NGValue::String(s) => parse_i64_from_str("i64", s.as_ref()),
1001            NGValue::Binary(b) => binary_to_i64_be(b, "i64"),
1002        }
1003    }
1004}
1005
1006impl TryFrom<&NGValue> for u64 {
1007    type Error = NGValueCastError;
1008
1009    #[inline]
1010    fn try_from(v: &NGValue) -> Result<Self, Self::Error> {
1011        match v {
1012            NGValue::Boolean(x) => {
1013                if *x {
1014                    Ok(1)
1015                } else {
1016                    Ok(0)
1017                }
1018            }
1019            NGValue::UInt64(x) => Ok(*x),
1020            NGValue::UInt32(x) => Ok(*x as u64),
1021            NGValue::UInt16(x) => Ok(*x as u64),
1022            NGValue::UInt8(x) => Ok(*x as u64),
1023            NGValue::Int8(x) => {
1024                u64::try_from(*x).map_err(|_| NGValueCastError::OutOfRange { target: "u64" })
1025            }
1026            NGValue::Int16(x) => {
1027                u64::try_from(*x).map_err(|_| NGValueCastError::OutOfRange { target: "u64" })
1028            }
1029            NGValue::Int32(x) => {
1030                u64::try_from(*x).map_err(|_| NGValueCastError::OutOfRange { target: "u64" })
1031            }
1032            NGValue::Int64(x) => {
1033                u64::try_from(*x).map_err(|_| NGValueCastError::OutOfRange { target: "u64" })
1034            }
1035            NGValue::Float32(x) => {
1036                let r = (*x as f64).round();
1037                if !r.is_finite() {
1038                    return Err(NGValueCastError::NotFinite);
1039                }
1040                if r >= 0.0 && r <= u64::MAX as f64 {
1041                    Ok(r as u64)
1042                } else {
1043                    Err(NGValueCastError::OutOfRange { target: "u64" })
1044                }
1045            }
1046            NGValue::Float64(x) => {
1047                let r = x.round();
1048                if !r.is_finite() {
1049                    return Err(NGValueCastError::NotFinite);
1050                }
1051                if r >= 0.0 && r <= u64::MAX as f64 {
1052                    Ok(r as u64)
1053                } else {
1054                    Err(NGValueCastError::OutOfRange { target: "u64" })
1055                }
1056            }
1057            NGValue::Timestamp(ms) => {
1058                u64::try_from(*ms).map_err(|_| NGValueCastError::OutOfRange { target: "u64" })
1059            }
1060            NGValue::String(s) => parse_u64_from_str("u64", s.as_ref()),
1061            NGValue::Binary(b) => binary_to_u64_be(b, "u64"),
1062        }
1063    }
1064}
1065
1066impl TryFrom<&NGValue> for f64 {
1067    type Error = NGValueCastError;
1068
1069    #[inline]
1070    fn try_from(v: &NGValue) -> Result<Self, Self::Error> {
1071        let f = match v {
1072            NGValue::Boolean(x) => {
1073                if *x {
1074                    1.0
1075                } else {
1076                    0.0
1077                }
1078            }
1079            NGValue::Int8(x) => *x as f64,
1080            NGValue::UInt8(x) => *x as f64,
1081            NGValue::Int16(x) => *x as f64,
1082            NGValue::UInt16(x) => *x as f64,
1083            NGValue::Int32(x) => *x as f64,
1084            NGValue::UInt32(x) => *x as f64,
1085            NGValue::Int64(x) => *x as f64,
1086            NGValue::UInt64(x) => *x as f64,
1087            NGValue::Float32(x) => *x as f64,
1088            NGValue::Float64(x) => *x,
1089            NGValue::Timestamp(ms) => *ms as f64,
1090            NGValue::String(s) => parse_f64_from_str("f64", s.as_ref())?,
1091            NGValue::Binary(b) => match b.len() {
1092                4 => {
1093                    let f = f32::from_be_bytes([b[0], b[1], b[2], b[3]]);
1094                    f as f64
1095                }
1096                8 => f64::from_be_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]]),
1097                other => return Err(binary_len_err("f64", "4 or 8", other)),
1098            },
1099        };
1100
1101        if !f.is_finite() {
1102            return Err(NGValueCastError::NotFinite);
1103        }
1104        Ok(f)
1105    }
1106}
1107
1108impl TryFrom<&NGValue> for f32 {
1109    type Error = NGValueCastError;
1110
1111    #[inline]
1112    fn try_from(v: &NGValue) -> Result<Self, Self::Error> {
1113        let f = f64::try_from(v)?;
1114        // f64::try_from already checks NotFinite
1115        if f < f32::MIN as f64 || f > f32::MAX as f64 {
1116            return Err(NGValueCastError::OutOfRange { target: "f32" });
1117        }
1118        Ok(f as f32)
1119    }
1120}
1121
1122impl TryFrom<&NGValue> for String {
1123    type Error = NGValueCastError;
1124
1125    #[inline]
1126    fn try_from(v: &NGValue) -> Result<Self, Self::Error> {
1127        match v {
1128            NGValue::String(s) => Ok(s.to_string()),
1129            NGValue::Binary(b) => Ok(base64::engine::general_purpose::STANDARD.encode(b.as_ref())),
1130            NGValue::Timestamp(ms) => {
1131                match chrono::DateTime::<chrono::Utc>::from_timestamp_millis(*ms) {
1132                    Some(dt) => Ok(dt.to_rfc3339()),
1133                    None => Ok(ms.to_string()),
1134                }
1135            }
1136            NGValue::Boolean(b) => Ok(b.to_string()),
1137            NGValue::Int8(n) => Ok(n.to_string()),
1138            NGValue::UInt8(n) => Ok(n.to_string()),
1139            NGValue::Int16(n) => Ok(n.to_string()),
1140            NGValue::UInt16(n) => Ok(n.to_string()),
1141            NGValue::Int32(n) => Ok(n.to_string()),
1142            NGValue::UInt32(n) => Ok(n.to_string()),
1143            NGValue::Int64(n) => Ok(n.to_string()),
1144            NGValue::UInt64(n) => Ok(n.to_string()),
1145            NGValue::Float32(n) => Ok(n.to_string()),
1146            NGValue::Float64(n) => Ok(n.to_string()),
1147        }
1148    }
1149}
1150
1151/// Best-effort string view for telemetry encoding / logging.
1152///
1153/// Unlike `TryFrom<&NGValue> for &str` (which can only borrow `NGValue::String`),
1154/// this conversion can represent **all** `NGValue` variants by allocating when needed.
1155impl<'a> TryFrom<&'a NGValue> for Cow<'a, str> {
1156    type Error = NGValueCastError;
1157
1158    #[inline]
1159    fn try_from(v: &'a NGValue) -> Result<Self, Self::Error> {
1160        Ok(match v {
1161            NGValue::String(s) => Cow::Borrowed(s.as_ref()),
1162            NGValue::Binary(b) => {
1163                Cow::Owned(base64::engine::general_purpose::STANDARD.encode(b.as_ref()))
1164            }
1165            NGValue::Timestamp(ms) => {
1166                match chrono::DateTime::<chrono::Utc>::from_timestamp_millis(*ms) {
1167                    Some(dt) => Cow::Owned(dt.to_rfc3339()),
1168                    None => Cow::Owned(ms.to_string()),
1169                }
1170            }
1171            NGValue::Boolean(b) => Cow::Owned(b.to_string()),
1172            NGValue::Int8(n) => Cow::Owned(n.to_string()),
1173            NGValue::UInt8(n) => Cow::Owned(n.to_string()),
1174            NGValue::Int16(n) => Cow::Owned(n.to_string()),
1175            NGValue::UInt16(n) => Cow::Owned(n.to_string()),
1176            NGValue::Int32(n) => Cow::Owned(n.to_string()),
1177            NGValue::UInt32(n) => Cow::Owned(n.to_string()),
1178            NGValue::Int64(n) => Cow::Owned(n.to_string()),
1179            NGValue::UInt64(n) => Cow::Owned(n.to_string()),
1180            NGValue::Float32(n) => Cow::Owned(n.to_string()),
1181            NGValue::Float64(n) => Cow::Owned(n.to_string()),
1182        })
1183    }
1184}
1185
1186impl TryFrom<&NGValue> for Bytes {
1187    type Error = NGValueCastError;
1188
1189    /// Canonical byte encoding (big-endian for numeric primitives).
1190    ///
1191    /// This avoids cloning the whole `NGValue` when the caller already has a reference.
1192    /// Note: `Bytes::clone()` is cheap (ref-counted).
1193    #[inline]
1194    fn try_from(v: &NGValue) -> Result<Self, Self::Error> {
1195        Ok(match v {
1196            NGValue::Binary(b) => b.clone(),
1197            NGValue::String(s) => Bytes::copy_from_slice(s.as_ref().as_bytes()),
1198            NGValue::Boolean(b) => Bytes::from(vec![if *b { 1 } else { 0 }]),
1199            NGValue::Int8(n) => Bytes::from(vec![*n as u8]),
1200            NGValue::UInt8(n) => Bytes::from(vec![*n]),
1201            NGValue::Int16(n) => Bytes::copy_from_slice(&n.to_be_bytes()),
1202            NGValue::UInt16(n) => Bytes::copy_from_slice(&n.to_be_bytes()),
1203            NGValue::Int32(n) => Bytes::copy_from_slice(&n.to_be_bytes()),
1204            NGValue::UInt32(n) => Bytes::copy_from_slice(&n.to_be_bytes()),
1205            NGValue::Int64(n) => Bytes::copy_from_slice(&n.to_be_bytes()),
1206            NGValue::UInt64(n) => Bytes::copy_from_slice(&n.to_be_bytes()),
1207            NGValue::Float32(n) => Bytes::copy_from_slice(&n.to_be_bytes()),
1208            NGValue::Float64(n) => Bytes::copy_from_slice(&n.to_be_bytes()),
1209            NGValue::Timestamp(ms) => Bytes::copy_from_slice(&ms.to_be_bytes()),
1210        })
1211    }
1212}
1213
1214impl TryFrom<&NGValue> for Vec<u8> {
1215    type Error = NGValueCastError;
1216
1217    /// Canonical byte encoding (big-endian for numeric primitives).
1218    ///
1219    /// This avoids cloning the whole `NGValue` when the caller already has a reference.
1220    #[inline]
1221    fn try_from(v: &NGValue) -> Result<Self, Self::Error> {
1222        Ok(match v {
1223            NGValue::Binary(b) => b.as_ref().to_vec(),
1224            NGValue::String(s) => s.as_ref().as_bytes().to_vec(),
1225            NGValue::Boolean(b) => vec![if *b { 1 } else { 0 }],
1226            NGValue::Int8(n) => vec![*n as u8],
1227            NGValue::UInt8(n) => vec![*n],
1228            NGValue::Int16(n) => n.to_be_bytes().to_vec(),
1229            NGValue::UInt16(n) => n.to_be_bytes().to_vec(),
1230            NGValue::Int32(n) => n.to_be_bytes().to_vec(),
1231            NGValue::UInt32(n) => n.to_be_bytes().to_vec(),
1232            NGValue::Int64(n) => n.to_be_bytes().to_vec(),
1233            NGValue::UInt64(n) => n.to_be_bytes().to_vec(),
1234            NGValue::Float32(n) => n.to_be_bytes().to_vec(),
1235            NGValue::Float64(n) => n.to_be_bytes().to_vec(),
1236            NGValue::Timestamp(ms) => ms.to_be_bytes().to_vec(),
1237        })
1238    }
1239}
1240
1241impl TryFrom<NGValue> for Bytes {
1242    type Error = NGValueCastError;
1243
1244    /// Canonical byte encoding (big-endian for numeric primitives).
1245    #[inline]
1246    fn try_from(v: NGValue) -> Result<Self, Self::Error> {
1247        Ok(match v {
1248            NGValue::Binary(b) => b,
1249            NGValue::String(s) => Bytes::copy_from_slice(s.as_ref().as_bytes()),
1250            NGValue::Boolean(b) => Bytes::from(vec![if b { 1 } else { 0 }]),
1251            NGValue::Int8(n) => Bytes::from(vec![n as u8]),
1252            NGValue::UInt8(n) => Bytes::from(vec![n]),
1253            NGValue::Int16(n) => Bytes::copy_from_slice(&n.to_be_bytes()),
1254            NGValue::UInt16(n) => Bytes::copy_from_slice(&n.to_be_bytes()),
1255            NGValue::Int32(n) => Bytes::copy_from_slice(&n.to_be_bytes()),
1256            NGValue::UInt32(n) => Bytes::copy_from_slice(&n.to_be_bytes()),
1257            NGValue::Int64(n) => Bytes::copy_from_slice(&n.to_be_bytes()),
1258            NGValue::UInt64(n) => Bytes::copy_from_slice(&n.to_be_bytes()),
1259            NGValue::Float32(n) => Bytes::copy_from_slice(&n.to_be_bytes()),
1260            NGValue::Float64(n) => Bytes::copy_from_slice(&n.to_be_bytes()),
1261            NGValue::Timestamp(ms) => Bytes::copy_from_slice(&ms.to_be_bytes()),
1262        })
1263    }
1264}
1265
1266impl TryFrom<NGValue> for Vec<u8> {
1267    type Error = NGValueCastError;
1268
1269    /// Canonical byte encoding (big-endian for numeric primitives).
1270    #[inline]
1271    fn try_from(v: NGValue) -> Result<Self, Self::Error> {
1272        Ok(match v {
1273            NGValue::Binary(b) => b.as_ref().to_vec(),
1274            NGValue::String(s) => s.as_ref().as_bytes().to_vec(),
1275            NGValue::Boolean(b) => vec![if b { 1 } else { 0 }],
1276            NGValue::Int8(n) => vec![n as u8],
1277            NGValue::UInt8(n) => vec![n],
1278            NGValue::Int16(n) => n.to_be_bytes().to_vec(),
1279            NGValue::UInt16(n) => n.to_be_bytes().to_vec(),
1280            NGValue::Int32(n) => n.to_be_bytes().to_vec(),
1281            NGValue::UInt32(n) => n.to_be_bytes().to_vec(),
1282            NGValue::Int64(n) => n.to_be_bytes().to_vec(),
1283            NGValue::UInt64(n) => n.to_be_bytes().to_vec(),
1284            NGValue::Float32(n) => n.to_be_bytes().to_vec(),
1285            NGValue::Float64(n) => n.to_be_bytes().to_vec(),
1286            NGValue::Timestamp(ms) => ms.to_be_bytes().to_vec(),
1287        })
1288    }
1289}
1290
1291impl<'de> Deserialize<'de> for NGValue {
1292    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1293    where
1294        D: Deserializer<'de>,
1295    {
1296        // This deserializer is only intended for non-hot-path usages (e.g. debugging).
1297        // It cannot precisely infer integer widths, so it chooses Int64/Float64 where needed.
1298        let v = serde_json::Value::deserialize(deserializer)?;
1299        match v {
1300            serde_json::Value::Bool(b) => Ok(NGValue::Boolean(b)),
1301            serde_json::Value::Number(n) => {
1302                if let Some(i) = n.as_i64() {
1303                    Ok(NGValue::Int64(i))
1304                } else if let Some(u) = n.as_u64() {
1305                    Ok(NGValue::UInt64(u))
1306                } else if let Some(f) = n.as_f64() {
1307                    Ok(NGValue::Float64(f))
1308                } else {
1309                    Err(de::Error::custom("invalid JSON number"))
1310                }
1311            }
1312            serde_json::Value::String(s) => Ok(NGValue::String(Arc::<str>::from(s))),
1313            serde_json::Value::Null => {
1314                Err(de::Error::custom("null cannot be converted to NGValue"))
1315            }
1316            serde_json::Value::Array(_) | serde_json::Value::Object(_) => Err(de::Error::custom(
1317                "array/object cannot be converted to NGValue without type information",
1318            )),
1319        }
1320    }
1321}
1322
1323/// A single point update.
1324///
1325/// `point_id` is the primary key for all hot-path operations.
1326#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1327pub struct PointValue {
1328    /// Point identifier (primary key).
1329    pub point_id: i32,
1330    /// Stable point key within a device (string identifier).
1331    pub point_key: Arc<str>,
1332    /// Strongly-typed value.
1333    pub value: NGValue,
1334}