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}