Skip to main content

fit/
value.rs

1//! Typed (post-transform) field values and message types — the public
2//! surface produced by [`crate::TypedDecoder`].
3//!
4//! [`Value`] is the union of every shape a fully-transformed field can
5//! take: a typed scalar, a string, a resolved enum name, a converted
6//! datetime, or an array. Compared to [`crate::RawValue`] it is consumer-
7//! friendly: scale/offset already applied, enums already named, datetimes
8//! already wall-clock.
9
10use std::borrow::Cow;
11
12#[cfg(feature = "chrono")]
13use chrono::{DateTime, Utc};
14
15/// A fully-transformed field value.
16#[derive(Debug, Clone, PartialEq)]
17pub enum Value {
18    /// Invalid or unsupported field value.
19    Invalid,
20    /// Untransformed signed integer (no scale/offset applied).
21    SInt(i64),
22    /// Untransformed unsigned integer.
23    UInt(u64),
24    /// Either a `Float32`/`Float64` base value, or any numeric value that
25    /// has had non-identity scale/offset applied.
26    Float(f64),
27    /// UTF-8 string.
28    String(String),
29    /// Opaque byte array.
30    Bytes(Vec<u8>),
31    /// Boolean value.
32    Bool(bool),
33    /// Resolved enum value (e.g. `"running"` for `Sport::Running`).
34    ///
35    /// Backed by [`Cow<'static, str>`] so that the common case — names
36    /// returned from the static Profile dispatcher — is a zero-allocation
37    /// borrow, while developer-defined enum values can still own a
38    /// runtime [`String`]. Construct with
39    /// `Value::Enum("running".into())` (works for both `&'static str` and
40    /// `String`); read via `Deref<Target=str>` (`s.is_empty()`,
41    /// `&s[..]`, `&*s`, etc.).
42    Enum(Cow<'static, str>),
43    /// FIT timestamp converted to wall-clock UTC. With the `chrono` feature
44    /// disabled this carries the raw FIT epoch seconds (u32) instead.
45    #[cfg(feature = "chrono")]
46    DateTime(DateTime<Utc>),
47    /// FIT timestamp as raw seconds since the FIT epoch (1989-12-31 UTC).
48    /// Only present when the `chrono` feature is **disabled**.
49    #[cfg(not(feature = "chrono"))]
50    DateTime(u32),
51    /// Multi-element field. Each entry is a [`Value`] of homogeneous type.
52    Array(Vec<Value>),
53}
54
55impl Value {
56    /// Returns `true` if this is [`Value::Invalid`].
57    pub fn is_invalid(&self) -> bool {
58        matches!(self, Value::Invalid)
59    }
60
61    /// Extract an `f64` from `Float`, `UInt`, or `SInt` variants.
62    pub fn as_f64(&self) -> Option<f64> {
63        match self {
64            Value::Float(v) => Some(*v),
65            Value::UInt(v) => Some(*v as f64),
66            Value::SInt(v) => Some(*v as f64),
67            _ => None,
68        }
69    }
70
71    /// Extract an `i64` from `SInt` or non-negative `UInt` variants.
72    pub fn as_i64(&self) -> Option<i64> {
73        match self {
74            Value::SInt(v) => Some(*v),
75            Value::UInt(v) => Some(*v as i64),
76            _ => None,
77        }
78    }
79
80    /// Extract a `u64` from `UInt` or non-negative `SInt` variants.
81    pub fn as_u64(&self) -> Option<u64> {
82        match self {
83            Value::UInt(v) => Some(*v),
84            Value::SInt(v) if *v >= 0 => Some(*v as u64),
85            _ => None,
86        }
87    }
88
89    /// Extract a string slice from `String` or `Enum` variants.
90    pub fn as_str(&self) -> Option<&str> {
91        match self {
92            Value::String(s) => Some(s.as_str()),
93            Value::Enum(s) => Some(s),
94            _ => None,
95        }
96    }
97
98    /// Extract a `DateTime<Utc>` from the `DateTime` variant.
99    #[cfg(feature = "chrono")]
100    pub fn as_datetime(&self) -> Option<DateTime<Utc>> {
101        match self {
102            Value::DateTime(d) => Some(*d),
103            _ => None,
104        }
105    }
106
107    /// Extract the raw FIT epoch seconds from the `DateTime` variant.
108    /// Available only when the `chrono` feature is disabled.
109    #[cfg(not(feature = "chrono"))]
110    pub fn as_datetime(&self) -> Option<u32> {
111        match self {
112            Value::DateTime(s) => Some(*s),
113            _ => None,
114        }
115    }
116}
117
118/// One field of a fully-transformed message.
119#[derive(Debug, Clone)]
120pub struct Field {
121    /// Snake-case canonical name from Profile.xlsx (or, when SubField
122    /// resolution kicks in, the SubField's name). Developer fields carry
123    /// the name from `field_description`, which is an owned `String`.
124    pub name: String,
125    /// Standard or developer field — see [`FieldKind`].
126    pub kind: FieldKind,
127    /// The decoded field value.
128    pub value: Value,
129    /// Display unit if Profile defines one (e.g. `"m/s"`, `"bpm"`).
130    pub units: Option<String>,
131}
132
133/// Provenance of a field — distinguishes Profile-declared standard fields
134/// from runtime-registered developer fields.
135#[derive(Debug, Clone, Copy)]
136pub enum FieldKind {
137    /// A standard field defined in Profile.xlsx.
138    Standard {
139        /// Wire-level field definition number.
140        field_def_num: u8,
141    },
142    /// A developer field; without M6's `field_description` registry, the
143    /// value will be `Value::Bytes` and `name` will be a synthetic placeholder.
144    Developer {
145        /// Wire-level field definition number.
146        field_def_num: u8,
147        /// Index into the developer data ID table.
148        developer_data_index: u8,
149    },
150}
151
152/// A fully-transformed FIT message.
153#[derive(Debug, Clone)]
154pub struct Message {
155    /// Profile-level message number.
156    pub global_mesg_num: u16,
157    /// Snake-case canonical message name from Profile.xlsx.
158    pub name: &'static str,
159    /// Fully-transformed fields.
160    pub fields: Vec<Field>,
161}
162
163impl Message {
164    /// Look up a standard field by its snake-case name. (Returns the first
165    /// match — Profile guarantees field names are unique within a message.)
166    pub fn field(&self, name: &str) -> Option<&Field> {
167        self.fields.iter().find(|f| f.name == name)
168    }
169}