Skip to main content

zerodds_types/dynamic/
try_construct.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! XTypes 1.3 §7.5.4.1.2 — TryConstruct-Apply (C4.7).
4//!
5//! When a value does not fit the target member on decode or a setter
6//! call (e.g. a string longer than the bound, a sequence over its
7//! max length, an enum value outside the value range), the
8//! `try_construct` strategy decides what happens:
9//!
10//! - `Discard` — discard the value, the member stays unset.
11//! - `UseDefault` — ignore the value, set `member.default_value`.
12//! - `Trim` — truncate to the bound (strings + sequences); for other
13//!   bound violations fall back to discard.
14//!
15//! This logic is evaluated **only** when a bound violation actually
16//! exists — un-bounded setters (member type without a `bound` limit)
17//! stay unchanged.
18
19use alloc::string::ToString;
20use alloc::vec::Vec;
21
22use super::data::DynamicValue;
23use super::descriptor::{TryConstructKind, TypeKind};
24use super::type_::DynamicTypeMember;
25
26/// Result of a try-construct evaluation.
27#[derive(Debug, Clone, PartialEq)]
28pub enum TryConstructOutcome {
29    /// The value is within bounds — `set` may write it unchanged.
30    Accept(DynamicValue),
31    /// The value is discarded — the member stays unset.
32    Discard,
33    /// The value is replaced by the default_value.
34    UseDefault(DynamicValue),
35    /// The value is truncated to the bound.
36    Trim(DynamicValue),
37}
38
39/// Applies the `try_construct` strategy to a setter value.
40/// If there is no bound violation, the function returns
41/// `Accept(value)` unchanged.
42#[must_use]
43pub fn apply_try_construct(member: &DynamicTypeMember, value: DynamicValue) -> TryConstructOutcome {
44    let descriptor = member.descriptor();
45    let bound_max = bound_max_length(member);
46    let target_kind = member.dynamic_type().kind();
47
48    let violation = detect_violation(&value, target_kind, bound_max);
49    if violation.is_none() {
50        return TryConstructOutcome::Accept(value);
51    }
52
53    match descriptor.try_construct {
54        TryConstructKind::Discard => TryConstructOutcome::Discard,
55        TryConstructKind::UseDefault => {
56            match parse_default(descriptor.default_value.as_deref(), target_kind) {
57                Some(default) => TryConstructOutcome::UseDefault(default),
58                None => TryConstructOutcome::Discard,
59            }
60        }
61        TryConstructKind::Trim => match trim_value(value, target_kind, bound_max) {
62            Some(trimmed) => TryConstructOutcome::Trim(trimmed),
63            None => TryConstructOutcome::Discard,
64        },
65    }
66}
67
68/// Returns the `max_length` bound from the member type, if relevant.
69/// `0` as a value (spec §7.5.1.2.4: 0 = unbounded) is treated as
70/// `None` — un-bounded setters bypass the apply logic.
71fn bound_max_length(member: &DynamicTypeMember) -> Option<usize> {
72    let descriptor = member.dynamic_type().descriptor();
73    match descriptor.kind {
74        TypeKind::String8 | TypeKind::String16 | TypeKind::Sequence | TypeKind::Map => descriptor
75            .bound
76            .first()
77            .copied()
78            .filter(|&b| b != 0)
79            .map(|b| b as usize),
80        TypeKind::Array => {
81            // An array has fixed dimensions — the bound is the product of all dims.
82            if descriptor.bound.is_empty() {
83                None
84            } else {
85                Some(descriptor.bound.iter().product::<u32>() as usize)
86            }
87        }
88        _ => None,
89    }
90}
91
92#[derive(Debug, PartialEq)]
93enum Violation {
94    StringTooLong,
95    SequenceTooLong,
96    ArrayLengthMismatch,
97}
98
99fn detect_violation(
100    value: &DynamicValue,
101    target_kind: TypeKind,
102    bound_max: Option<usize>,
103) -> Option<Violation> {
104    let max = bound_max?;
105    match (value, target_kind) {
106        (DynamicValue::String(s), TypeKind::String8) if s.len() > max => {
107            Some(Violation::StringTooLong)
108        }
109        (DynamicValue::WString(s), TypeKind::String16) if s.len() > max => {
110            Some(Violation::StringTooLong)
111        }
112        (DynamicValue::Sequence(s), TypeKind::Sequence) if s.len() > max => {
113            Some(Violation::SequenceTooLong)
114        }
115        (DynamicValue::Sequence(s), TypeKind::Array) if s.len() != max => {
116            Some(Violation::ArrayLengthMismatch)
117        }
118        _ => None,
119    }
120}
121
122fn trim_value(
123    value: DynamicValue,
124    target_kind: TypeKind,
125    bound_max: Option<usize>,
126) -> Option<DynamicValue> {
127    let max = bound_max?;
128    match (value, target_kind) {
129        (DynamicValue::String(mut s), TypeKind::String8) => {
130            // String trim on a byte boundary, but never in the middle of
131            // a UTF-8 codepoint. We coerce to the next-smaller char
132            // boundary.
133            if s.len() > max {
134                let mut cut = max;
135                while !s.is_char_boundary(cut) && cut > 0 {
136                    cut -= 1;
137                }
138                s.truncate(cut);
139            }
140            Some(DynamicValue::String(s))
141        }
142        (DynamicValue::WString(mut s), TypeKind::String16) => {
143            if s.len() > max {
144                s.truncate(max);
145            }
146            Some(DynamicValue::WString(s))
147        }
148        (DynamicValue::Sequence(mut s), TypeKind::Sequence) => {
149            if s.len() > max {
150                s.truncate(max);
151            }
152            Some(DynamicValue::Sequence(s))
153        }
154        // Array length mismatch: no meaningful trim, because an array
155        // has an exact dimension. Fall back to discard.
156        _ => None,
157    }
158}
159
160fn parse_default(default_str: Option<&str>, kind: TypeKind) -> Option<DynamicValue> {
161    let s = default_str?;
162    match kind {
163        TypeKind::Boolean => match s {
164            "TRUE" | "true" | "1" => Some(DynamicValue::Bool(true)),
165            "FALSE" | "false" | "0" => Some(DynamicValue::Bool(false)),
166            _ => None,
167        },
168        TypeKind::Byte | TypeKind::UInt8 => s.parse::<u8>().ok().map(DynamicValue::UInt8),
169        TypeKind::Int8 => s.parse::<i8>().ok().map(DynamicValue::Int8),
170        TypeKind::Int16 => s.parse::<i16>().ok().map(DynamicValue::Int16),
171        TypeKind::UInt16 => s.parse::<u16>().ok().map(DynamicValue::UInt16),
172        TypeKind::Int32 | TypeKind::Enumeration => s.parse::<i32>().ok().map(DynamicValue::Int32),
173        TypeKind::UInt32 => s.parse::<u32>().ok().map(DynamicValue::UInt32),
174        TypeKind::Int64 => s.parse::<i64>().ok().map(DynamicValue::Int64),
175        TypeKind::UInt64 => s.parse::<u64>().ok().map(DynamicValue::UInt64),
176        TypeKind::Float32 => s.parse::<f32>().ok().map(DynamicValue::Float32),
177        TypeKind::Float64 => s.parse::<f64>().ok().map(DynamicValue::Float64),
178        TypeKind::String8 => Some(DynamicValue::String(s.to_string())),
179        TypeKind::String16 => Some(DynamicValue::WString(s.encode_utf16().collect::<Vec<_>>())),
180        _ => None,
181    }
182}
183
184#[cfg(test)]
185#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
186mod tests {
187    use super::*;
188    use crate::dynamic::builder::{DynamicTypeBuilder, DynamicTypeBuilderFactory};
189    use crate::dynamic::descriptor::{MemberDescriptor, TypeDescriptor};
190    use alloc::boxed::Box;
191
192    fn make_struct_with_bounded_string(
193        max_len: u32,
194        try_construct: TryConstructKind,
195        default_value: Option<&str>,
196    ) -> crate::dynamic::DynamicType {
197        let mut builder = DynamicTypeBuilder::new(TypeDescriptor::structure("TestStruct"));
198        let mut string_desc = TypeDescriptor::primitive(TypeKind::String8, "string");
199        string_desc.bound = alloc::vec![max_len];
200        let mut member = MemberDescriptor::new("name", 1, string_desc);
201        member.try_construct = try_construct;
202        member.default_value = default_value.map(|s| s.to_string());
203        builder.add_member(member).unwrap();
204        builder.build().unwrap()
205    }
206
207    fn make_struct_with_bounded_seq(
208        max_len: u32,
209        try_construct: TryConstructKind,
210    ) -> crate::dynamic::DynamicType {
211        let mut builder = DynamicTypeBuilder::new(TypeDescriptor::structure("TestSeq"));
212        let mut seq_desc = TypeDescriptor::primitive(TypeKind::Sequence, "sequence");
213        seq_desc.bound = alloc::vec![max_len];
214        seq_desc.element_type = Some(Box::new(TypeDescriptor::primitive(TypeKind::Int32, "long")));
215        let mut member = MemberDescriptor::new("ids", 1, seq_desc);
216        member.try_construct = try_construct;
217        builder.add_member(member).unwrap();
218        builder.build().unwrap()
219    }
220
221    #[test]
222    fn discard_drops_too_long_string() {
223        let ty = make_struct_with_bounded_string(5, TryConstructKind::Discard, None);
224        let member = ty.member_by_id(1).unwrap();
225        let outcome = apply_try_construct(member, DynamicValue::String("toolong".into()));
226        assert_eq!(outcome, TryConstructOutcome::Discard);
227    }
228
229    #[test]
230    fn use_default_replaces_too_long_string() {
231        let ty = make_struct_with_bounded_string(5, TryConstructKind::UseDefault, Some("hello"));
232        let member = ty.member_by_id(1).unwrap();
233        let outcome = apply_try_construct(member, DynamicValue::String("toolong".into()));
234        match outcome {
235            TryConstructOutcome::UseDefault(DynamicValue::String(s)) => assert_eq!(s, "hello"),
236            other => panic!("expected UseDefault(\"hello\"), got {other:?}"),
237        }
238    }
239
240    #[test]
241    fn use_default_falls_back_to_discard_when_no_default() {
242        let ty = make_struct_with_bounded_string(5, TryConstructKind::UseDefault, None);
243        let member = ty.member_by_id(1).unwrap();
244        let outcome = apply_try_construct(member, DynamicValue::String("toolong".into()));
245        assert_eq!(outcome, TryConstructOutcome::Discard);
246    }
247
248    #[test]
249    fn trim_truncates_string_to_bound() {
250        let ty = make_struct_with_bounded_string(5, TryConstructKind::Trim, None);
251        let member = ty.member_by_id(1).unwrap();
252        let outcome = apply_try_construct(member, DynamicValue::String("hello world".into()));
253        match outcome {
254            TryConstructOutcome::Trim(DynamicValue::String(s)) => assert_eq!(s, "hello"),
255            other => panic!("expected Trim(\"hello\"), got {other:?}"),
256        }
257    }
258
259    #[test]
260    fn trim_respects_utf8_codepoint_boundaries() {
261        // "héllo" with é = 2 bytes. Bound 3 would trim in the middle of é →
262        // must fall back to 2 bytes ("h" + start of é → boundary 1 → "h").
263        let ty = make_struct_with_bounded_string(3, TryConstructKind::Trim, None);
264        let member = ty.member_by_id(1).unwrap();
265        let outcome = apply_try_construct(member, DynamicValue::String("héllo".into()));
266        match outcome {
267            TryConstructOutcome::Trim(DynamicValue::String(s)) => {
268                assert!(s.is_char_boundary(s.len()));
269                assert!(s.len() <= 3);
270                // Mit "h" (1 byte) + é (2 byte) = 3 byte char-boundary.
271                assert_eq!(s, "hé");
272            }
273            other => panic!("expected Trim, got {other:?}"),
274        }
275    }
276
277    #[test]
278    fn accept_when_value_within_bound() {
279        let ty = make_struct_with_bounded_string(10, TryConstructKind::Discard, None);
280        let member = ty.member_by_id(1).unwrap();
281        let outcome = apply_try_construct(member, DynamicValue::String("ok".into()));
282        match outcome {
283            TryConstructOutcome::Accept(DynamicValue::String(s)) => assert_eq!(s, "ok"),
284            other => panic!("expected Accept, got {other:?}"),
285        }
286    }
287
288    #[test]
289    fn unbounded_string_always_accepts() {
290        let ty = make_struct_with_bounded_string(0, TryConstructKind::Discard, None);
291        let member = ty.member_by_id(1).unwrap();
292        // bound = 0 = unbounded → no violation, no apply.
293        let outcome =
294            apply_try_construct(member, DynamicValue::String("a-very-long-string".into()));
295        match outcome {
296            TryConstructOutcome::Accept(_) => {}
297            other => panic!("expected Accept (unbounded), got {other:?}"),
298        }
299    }
300
301    #[test]
302    fn discard_drops_too_long_sequence() {
303        let ty = make_struct_with_bounded_seq(3, TryConstructKind::Discard);
304        let member = ty.member_by_id(1).unwrap();
305        let elements: Vec<_> = (0..5)
306            .map(|i| {
307                let prim = DynamicTypeBuilderFactory::get_primitive_type(TypeKind::Int32).unwrap();
308                let mut d = crate::dynamic::DynamicData::new(prim.clone());
309                d.set_int32_value(0, i).ok();
310                d
311            })
312            .collect();
313        let outcome = apply_try_construct(member, DynamicValue::Sequence(elements));
314        assert_eq!(outcome, TryConstructOutcome::Discard);
315    }
316
317    #[test]
318    fn trim_truncates_sequence_to_bound() {
319        let ty = make_struct_with_bounded_seq(3, TryConstructKind::Trim);
320        let member = ty.member_by_id(1).unwrap();
321        let elements: Vec<_> = (0..5)
322            .map(|i| {
323                let prim = DynamicTypeBuilderFactory::get_primitive_type(TypeKind::Int32).unwrap();
324                let mut d = crate::dynamic::DynamicData::new(prim.clone());
325                d.set_int32_value(0, i).ok();
326                d
327            })
328            .collect();
329        let outcome = apply_try_construct(member, DynamicValue::Sequence(elements));
330        match outcome {
331            TryConstructOutcome::Trim(DynamicValue::Sequence(s)) => assert_eq!(s.len(), 3),
332            other => panic!("expected Trim(seq[3]), got {other:?}"),
333        }
334    }
335
336    #[test]
337    fn parse_default_int32_works() {
338        let v = parse_default(Some("42"), TypeKind::Int32);
339        assert_eq!(v, Some(DynamicValue::Int32(42)));
340    }
341
342    #[test]
343    fn parse_default_bool_accepts_canonical_forms() {
344        assert_eq!(
345            parse_default(Some("TRUE"), TypeKind::Boolean),
346            Some(DynamicValue::Bool(true))
347        );
348        assert_eq!(
349            parse_default(Some("false"), TypeKind::Boolean),
350            Some(DynamicValue::Bool(false))
351        );
352    }
353
354    #[test]
355    fn parse_default_invalid_returns_none() {
356        assert_eq!(parse_default(Some("not-a-number"), TypeKind::Int32), None);
357    }
358}