Skip to main content

canton_daml/
value.rs

1//! Conversion between typed Rust values and the Ledger API [`Value`](pb::Value)
2//! (the gRPC wire form). Generated records/variants/enums implement these
3//! traits too; here they are provided for the primitive and container types.
4
5use std::collections::BTreeMap;
6
7use canton_proto::com::daml::ledger::api::v2 as pb;
8
9use crate::primitives::{
10    ContractId, Date, GenMap, Int64, NestedOpt, Numeric, Party, Timestamp, Unit,
11};
12
13/// An error converting a Ledger API [`Value`](pb::Value) into a typed value.
14///
15/// Carries the **path** to the offending spot inside the payload — a decode
16/// failure deep in a nested record reports `owner.address.city: expected Text`
17/// rather than just `expected Text`.
18#[derive(Clone, Debug, PartialEq, Eq)]
19pub struct ValueError {
20    /// Field path from the root of the decoded value, outermost first.
21    path: Vec<String>,
22    message: String,
23}
24
25impl ValueError {
26    /// A conversion error with no path context yet. Generated `FromValue`
27    /// bodies wrap each field decode in [`ValueError::at`], so by the time one
28    /// of these reaches a caller it names the field it came from.
29    #[must_use]
30    pub fn new(message: impl Into<String>) -> Self {
31        Self {
32            path: Vec::new(),
33            message: message.into(),
34        }
35    }
36
37    /// Prepend a field (or index) to the path — called as the error propagates
38    /// out of a nested value, so the path reads outermost-first.
39    #[must_use]
40    pub fn at(mut self, field: impl Into<String>) -> Self {
41        self.path.insert(0, field.into());
42        self
43    }
44
45    /// The failure description, without the path.
46    #[must_use]
47    pub fn message(&self) -> &str {
48        &self.message
49    }
50
51    /// The field path from the root of the value, outermost first; empty when
52    /// the failure was at the root.
53    #[must_use]
54    pub fn path(&self) -> &[String] {
55        &self.path
56    }
57}
58
59impl std::fmt::Display for ValueError {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        if self.path.is_empty() {
62            write!(f, "value conversion error: {}", self.message)
63        } else {
64            write!(
65                f,
66                "value conversion error at `{}`: {}",
67                self.path.join("."),
68                self.message
69            )
70        }
71    }
72}
73
74impl std::error::Error for ValueError {}
75
76/// Lets `?` carry a codec failure straight into the SDK-wide error type, so one
77/// function can call the typed runtime and the ledger client and return
78/// `canton::Result`.
79impl From<ValueError> for canton_core::Error {
80    fn from(error: ValueError) -> Self {
81        canton_core::Error::Payload(Box::new(error))
82    }
83}
84
85/// Encode a typed Rust value as a Ledger API [`Value`](pb::Value).
86pub trait ToValue {
87    /// Encode `self` as a wire `Value`.
88    fn to_value(&self) -> pb::Value;
89}
90
91/// Decode a typed Rust value from a Ledger API [`Value`](pb::Value).
92pub trait FromValue: Sized {
93    /// Decode a wire `Value` into `Self`.
94    ///
95    /// # Errors
96    /// Returns [`ValueError`] if the value's shape does not match `Self`.
97    fn from_value(value: &pb::Value) -> Result<Self, ValueError>;
98}
99
100fn wrap(sum: pb::value::Sum) -> pb::Value {
101    pb::Value { sum: Some(sum) }
102}
103
104fn sum(value: &pb::Value) -> Result<&pb::value::Sum, ValueError> {
105    value
106        .sum
107        .as_ref()
108        .ok_or_else(|| ValueError::new("empty Value (no sum set)"))
109}
110
111/// The name of a `Value`'s kind — `"Record"`, `"Party"`, … — and nothing else.
112///
113/// Deliberately not `{sum:?}`: prost's derived `Debug` prints a value in full,
114/// so a type mismatch on a contract payload would copy party ids, amounts and
115/// free text into the error message. That message travels through
116/// `From<ValueError> for canton_core::Error` into whatever the application
117/// logs, so the leak would land in traces and metrics of code that never asked
118/// to see the payload.
119///
120/// The match is exhaustive on purpose: a `Value` kind added in a future Ledger
121/// API is a build break here, not a silent `"unknown"`.
122fn kind(sum: &pb::value::Sum) -> &'static str {
123    use pb::value::Sum;
124    match sum {
125        Sum::Unit(()) => "Unit",
126        Sum::Bool(_) => "Bool",
127        Sum::Int64(_) => "Int64",
128        Sum::Date(_) => "Date",
129        Sum::Timestamp(_) => "Timestamp",
130        Sum::Numeric(_) => "Numeric",
131        Sum::Party(_) => "Party",
132        Sum::Text(_) => "Text",
133        Sum::ContractId(_) => "ContractId",
134        Sum::Optional(_) => "Optional",
135        Sum::List(_) => "List",
136        Sum::TextMap(_) => "TextMap",
137        Sum::GenMap(_) => "GenMap",
138        Sum::Record(_) => "Record",
139        Sum::Variant(_) => "Variant",
140        Sum::Enum(_) => "Enum",
141    }
142}
143
144fn mismatch(expected: &str, got: &pb::value::Sum) -> ValueError {
145    ValueError::new(format!("expected {expected}, got {}", kind(got)))
146}
147
148// ---- record helpers (used by generated ToValue/FromValue) ------------------
149
150/// Build a bare `Record` from labelled fields. Generated `Template::to_record`
151/// impls call this; [`record`] wraps the result as a [`Value`](pb::Value).
152#[must_use]
153pub fn record_fields(fields: Vec<(&str, pb::Value)>) -> pb::Record {
154    pb::Record {
155        record_id: None,
156        fields: fields
157            .into_iter()
158            .map(|(label, value)| pb::RecordField {
159                label: label.to_string(),
160                value: Some(value),
161            })
162            .collect(),
163    }
164}
165
166/// Build a `Record` [`Value`](pb::Value) from labelled fields. Generated
167/// `ToValue` impls call this.
168#[must_use]
169pub fn record(fields: Vec<(&str, pb::Value)>) -> pb::Value {
170    wrap(pb::value::Sum::Record(record_fields(fields)))
171}
172
173/// Locate a record field by **label or declaration position**, tolerating the
174/// two shapes Canton legitimately produces:
175///
176/// - **non-verbose** output omits labels entirely — the field is matched by its
177///   declaration `index`;
178/// - **normalized** records (Smart Contract Upgrade) omit trailing fields whose
179///   value is an empty `Optional` — the field is *absent*, which is `Ok(None)`
180///   here so an `Optional`-typed field can decode as `None`.
181///
182/// Labels win when present; position applies only to unlabelled fields, so a
183/// verbose record whose trailing fields were normalized away never mis-binds.
184///
185/// # Errors
186/// Returns [`ValueError`] if `value` is not a record.
187pub fn find_field<'a>(
188    value: &'a pb::Value,
189    index: usize,
190    label: &str,
191) -> Result<Option<&'a pb::Value>, ValueError> {
192    match sum(value)? {
193        pb::value::Sum::Record(record) => {
194            if let Some(field) = record
195                .fields
196                .iter()
197                .find(|field| !field.label.is_empty() && field.label == label)
198            {
199                return Ok(field.value.as_ref());
200            }
201            if let Some(field) = record.fields.get(index)
202                && field.label.is_empty()
203            {
204                return Ok(field.value.as_ref());
205            }
206            Ok(None)
207        }
208        other => Err(mismatch("Record", other)),
209    }
210}
211
212/// [`find_field`] for a field the type requires: absence is an error naming the
213/// field. Generated `FromValue` impls call this for non-`Optional` fields.
214///
215/// # Errors
216/// Returns [`ValueError`] if `value` is not a record or the field is absent.
217pub fn required_field<'a>(
218    value: &'a pb::Value,
219    index: usize,
220    label: &str,
221) -> Result<&'a pb::Value, ValueError> {
222    find_field(value, index, label)?
223        .ok_or_else(|| ValueError::new(format!("record has no field `{label}` (index {index})")))
224}
225
226/// Decode an `Optional`-typed record field, treating an **absent** field as
227/// `None`: Canton normalizes records by dropping trailing empty-`Optional`
228/// fields, so absence of an optional field is a legitimate wire shape, not an
229/// error. Generated `FromValue` impls call this for `Optional` fields.
230///
231/// # Errors
232/// Returns [`ValueError`] if `value` is not a record or the present field's
233/// shape does not match `T`.
234pub fn optional_field<T: FromValue + AbsentField>(
235    value: &pb::Value,
236    index: usize,
237    label: &str,
238) -> Result<T, ValueError> {
239    match find_field(value, index, label)? {
240        Some(present) => T::from_value(present),
241        None => Ok(T::absent()),
242    }
243}
244
245/// The decoded form of an **absent** optional record field (`None`).
246/// Implemented by the two Rust spellings of a Daml `Optional` field.
247pub trait AbsentField {
248    /// The value an absent field decodes to.
249    fn absent() -> Self;
250}
251
252impl<T> AbsentField for Option<T> {
253    fn absent() -> Self {
254        None
255    }
256}
257
258impl<T> AbsentField for NestedOpt<T> {
259    fn absent() -> Self {
260        NestedOpt(None)
261    }
262}
263
264/// Decode a bare wire `Record` — the shape `CreatedEvent.create_arguments`
265/// carries — into a typed value. See also
266/// [`Template::from_created_event`](crate::Template::from_created_event),
267/// which wraps this with a template-identity check.
268///
269/// # Errors
270/// Returns [`ValueError`] if the record's shape does not match `T`.
271pub fn from_record<T: FromValue>(record: &pb::Record) -> Result<T, ValueError> {
272    let value = pb::Value {
273        sum: Some(pb::value::Sum::Record(record.clone())),
274    };
275    T::from_value(&value)
276}
277
278// ---- enum / variant helpers (used by generated ToValue/FromValue) ----------
279
280/// A `Unit` value — a nullary variant constructor's payload.
281#[must_use]
282pub fn unit_value() -> pb::Value {
283    wrap(pb::value::Sum::Unit(()))
284}
285
286/// Build an `Enum` value from its constructor name.
287#[must_use]
288pub fn enum_value(constructor: &str) -> pb::Value {
289    wrap(pb::value::Sum::Enum(pb::Enum {
290        enum_id: None,
291        constructor: constructor.to_string(),
292    }))
293}
294
295/// The constructor name of an `Enum` value.
296///
297/// # Errors
298/// Returns [`ValueError`] if `value` is not an enum.
299pub fn enum_constructor(value: &pb::Value) -> Result<&str, ValueError> {
300    match sum(value)? {
301        pb::value::Sum::Enum(enumeration) => Ok(enumeration.constructor.as_str()),
302        other => Err(mismatch("Enum", other)),
303    }
304}
305
306/// Build a `Variant` value from a constructor name and its payload.
307#[must_use]
308pub fn variant_value(constructor: &str, value: pb::Value) -> pb::Value {
309    wrap(pb::value::Sum::Variant(Box::new(pb::Variant {
310        variant_id: None,
311        constructor: constructor.to_string(),
312        value: Some(Box::new(value)),
313    })))
314}
315
316/// The constructor name and payload of a `Variant` value.
317///
318/// # Errors
319/// Returns [`ValueError`] if `value` is not a variant or carries no payload.
320pub fn variant_parts(value: &pb::Value) -> Result<(&str, &pb::Value), ValueError> {
321    match sum(value)? {
322        pb::value::Sum::Variant(variant) => {
323            let payload = variant
324                .value
325                .as_deref()
326                .ok_or_else(|| ValueError::new("variant carries no value"))?;
327            Ok((variant.constructor.as_str(), payload))
328        }
329        other => Err(mismatch("Variant", other)),
330    }
331}
332
333/// A "no such constructor `got` for `type_name`" error, for the fall-through arm
334/// of a generated enum/variant `FromValue`.
335#[must_use]
336pub fn unexpected_constructor(type_name: &str, got: &str) -> ValueError {
337    ValueError::new(format!("`{type_name}` has no constructor `{got}`"))
338}
339
340// ---- primitives -----------------------------------------------------------
341
342impl ToValue for bool {
343    fn to_value(&self) -> pb::Value {
344        wrap(pb::value::Sum::Bool(*self))
345    }
346}
347impl FromValue for bool {
348    fn from_value(value: &pb::Value) -> Result<Self, ValueError> {
349        match sum(value)? {
350            pb::value::Sum::Bool(b) => Ok(*b),
351            other => Err(mismatch("Bool", other)),
352        }
353    }
354}
355
356/// **gRPC only.** The wire form here is right, but `i64`'s serde half is std's:
357/// it writes a JSON *number* and refuses the string the Ledger API sends. Use
358/// [`Int64`](crate::Int64) for anything that also travels as JSON — which is
359/// what generated code uses, and why that newtype exists.
360impl ToValue for i64 {
361    fn to_value(&self) -> pb::Value {
362        wrap(pb::value::Sum::Int64(*self))
363    }
364}
365impl FromValue for i64 {
366    fn from_value(value: &pb::Value) -> Result<Self, ValueError> {
367        match sum(value)? {
368            pb::value::Sum::Int64(n) => Ok(*n),
369            other => Err(mismatch("Int64", other)),
370        }
371    }
372}
373
374impl ToValue for Int64 {
375    fn to_value(&self) -> pb::Value {
376        wrap(pb::value::Sum::Int64(self.0))
377    }
378}
379impl FromValue for Int64 {
380    fn from_value(value: &pb::Value) -> Result<Self, ValueError> {
381        match sum(value)? {
382            pb::value::Sum::Int64(n) => Ok(Int64(*n)),
383            other => Err(mismatch("Int64", other)),
384        }
385    }
386}
387
388impl ToValue for String {
389    fn to_value(&self) -> pb::Value {
390        wrap(pb::value::Sum::Text(self.clone()))
391    }
392}
393impl FromValue for String {
394    fn from_value(value: &pb::Value) -> Result<Self, ValueError> {
395        match sum(value)? {
396            pb::value::Sum::Text(s) => Ok(s.clone()),
397            other => Err(mismatch("Text", other)),
398        }
399    }
400}
401
402/// **gRPC only.** As with `i64`: correct on the wire, but `()` serializes to
403/// JSON `null` where Daml `Unit` is `{}`. Use [`Unit`](crate::Unit) for the
404/// JSON lane.
405impl ToValue for () {
406    fn to_value(&self) -> pb::Value {
407        wrap(pb::value::Sum::Unit(()))
408    }
409}
410impl FromValue for () {
411    fn from_value(value: &pb::Value) -> Result<Self, ValueError> {
412        match sum(value)? {
413            pb::value::Sum::Unit(()) => Ok(()),
414            other => Err(mismatch("Unit", other)),
415        }
416    }
417}
418
419impl ToValue for Unit {
420    fn to_value(&self) -> pb::Value {
421        unit_value()
422    }
423}
424impl FromValue for Unit {
425    fn from_value(value: &pb::Value) -> Result<Self, ValueError> {
426        match sum(value)? {
427            pb::value::Sum::Unit(()) => Ok(Unit),
428            other => Err(mismatch("Unit", other)),
429        }
430    }
431}
432
433impl ToValue for Party {
434    fn to_value(&self) -> pb::Value {
435        wrap(pb::value::Sum::Party(self.0.clone()))
436    }
437}
438impl FromValue for Party {
439    fn from_value(value: &pb::Value) -> Result<Self, ValueError> {
440        match sum(value)? {
441            pb::value::Sum::Party(p) => Ok(Party(p.clone())),
442            other => Err(mismatch("Party", other)),
443        }
444    }
445}
446
447impl ToValue for Numeric {
448    fn to_value(&self) -> pb::Value {
449        wrap(pb::value::Sum::Numeric(self.0.clone()))
450    }
451}
452impl FromValue for Numeric {
453    fn from_value(value: &pb::Value) -> Result<Self, ValueError> {
454        match sum(value)? {
455            pb::value::Sum::Numeric(n) => Ok(Numeric::from_wire(n.clone())),
456            other => Err(mismatch("Numeric", other)),
457        }
458    }
459}
460
461impl ToValue for Timestamp {
462    fn to_value(&self) -> pb::Value {
463        wrap(pb::value::Sum::Timestamp(self.0))
464    }
465}
466impl FromValue for Timestamp {
467    fn from_value(value: &pb::Value) -> Result<Self, ValueError> {
468        match sum(value)? {
469            pb::value::Sum::Timestamp(t) => Ok(Timestamp(*t)),
470            other => Err(mismatch("Timestamp", other)),
471        }
472    }
473}
474
475impl ToValue for Date {
476    fn to_value(&self) -> pb::Value {
477        wrap(pb::value::Sum::Date(self.0))
478    }
479}
480impl FromValue for Date {
481    fn from_value(value: &pb::Value) -> Result<Self, ValueError> {
482        match sum(value)? {
483            pb::value::Sum::Date(d) => Ok(Date(*d)),
484            other => Err(mismatch("Date", other)),
485        }
486    }
487}
488
489impl<T> ToValue for ContractId<T> {
490    fn to_value(&self) -> pb::Value {
491        wrap(pb::value::Sum::ContractId(self.as_str().to_string()))
492    }
493}
494impl<T> FromValue for ContractId<T> {
495    fn from_value(value: &pb::Value) -> Result<Self, ValueError> {
496        match sum(value)? {
497            pb::value::Sum::ContractId(id) => Ok(ContractId::new(id.clone())),
498            other => Err(mismatch("ContractId", other)),
499        }
500    }
501}
502
503// ---- containers -----------------------------------------------------------
504
505// `Box` is transparent to the codec: recursive generated types are boxed for
506// Rust's sake, but a `Box<T>` encodes exactly as its `T`.
507impl<T: ToValue> ToValue for Box<T> {
508    fn to_value(&self) -> pb::Value {
509        T::to_value(self)
510    }
511}
512impl<T: FromValue> FromValue for Box<T> {
513    fn from_value(value: &pb::Value) -> Result<Self, ValueError> {
514        T::from_value(value).map(Box::new)
515    }
516}
517
518impl<T: ToValue> ToValue for Option<T> {
519    fn to_value(&self) -> pb::Value {
520        wrap(pb::value::Sum::Optional(Box::new(pb::Optional {
521            value: self.as_ref().map(|inner| Box::new(inner.to_value())),
522        })))
523    }
524}
525impl<T: FromValue> FromValue for Option<T> {
526    fn from_value(value: &pb::Value) -> Result<Self, ValueError> {
527        match sum(value)? {
528            pb::value::Sum::Optional(opt) => match &opt.value {
529                Some(inner) => Ok(Some(T::from_value(inner)?)),
530                None => Ok(None),
531            },
532            other => Err(mismatch("Optional", other)),
533        }
534    }
535}
536
537// `NestedOpt` is a Daml Optional at a nested position; on the gRPC wire it is a
538// proto `Optional`, exactly like `Option` (only its JSON form differs).
539impl<T: ToValue> ToValue for NestedOpt<T> {
540    fn to_value(&self) -> pb::Value {
541        wrap(pb::value::Sum::Optional(Box::new(pb::Optional {
542            value: self.0.as_ref().map(|inner| Box::new(inner.to_value())),
543        })))
544    }
545}
546impl<T: FromValue> FromValue for NestedOpt<T> {
547    fn from_value(value: &pb::Value) -> Result<Self, ValueError> {
548        match sum(value)? {
549            pb::value::Sum::Optional(opt) => match &opt.value {
550                Some(inner) => Ok(NestedOpt(Some(T::from_value(inner)?))),
551                None => Ok(NestedOpt(None)),
552            },
553            other => Err(mismatch("Optional", other)),
554        }
555    }
556}
557
558impl<T: ToValue> ToValue for Vec<T> {
559    fn to_value(&self) -> pb::Value {
560        wrap(pb::value::Sum::List(pb::List {
561            elements: self.iter().map(ToValue::to_value).collect(),
562        }))
563    }
564}
565impl<T: FromValue> FromValue for Vec<T> {
566    fn from_value(value: &pb::Value) -> Result<Self, ValueError> {
567        match sum(value)? {
568            // The index goes on the path for the same reason a field name does:
569            // without it, one bad element in a list of five hundred reports
570            // only that the list is bad.
571            pb::value::Sum::List(list) => list
572                .elements
573                .iter()
574                .enumerate()
575                .map(|(index, element)| {
576                    T::from_value(element).map_err(|error| error.at(index.to_string()))
577                })
578                .collect(),
579            other => Err(mismatch("List", other)),
580        }
581    }
582}
583
584impl<V: ToValue> ToValue for BTreeMap<String, V> {
585    fn to_value(&self) -> pb::Value {
586        wrap(pb::value::Sum::TextMap(pb::TextMap {
587            entries: self
588                .iter()
589                .map(|(key, value)| pb::text_map::Entry {
590                    key: key.clone(),
591                    value: Some(value.to_value()),
592                })
593                .collect(),
594        }))
595    }
596}
597impl<V: FromValue> FromValue for BTreeMap<String, V> {
598    fn from_value(value: &pb::Value) -> Result<Self, ValueError> {
599        match sum(value)? {
600            pb::value::Sum::TextMap(map) => map
601                .entries
602                .iter()
603                .map(|entry| {
604                    let inner = entry.value.as_ref().ok_or_else(|| {
605                        ValueError::new("TextMap entry has no value").at(entry.key.clone())
606                    })?;
607                    // The key locates the entry better than a position would.
608                    let value =
609                        V::from_value(inner).map_err(|error| error.at(entry.key.clone()))?;
610                    Ok((entry.key.clone(), value))
611                })
612                .collect(),
613            other => Err(mismatch("TextMap", other)),
614        }
615    }
616}
617
618impl<K: ToValue, V: ToValue> ToValue for GenMap<K, V> {
619    fn to_value(&self) -> pb::Value {
620        wrap(pb::value::Sum::GenMap(pb::GenMap {
621            entries: self
622                .0
623                .iter()
624                .map(|(key, value)| pb::gen_map::Entry {
625                    key: Some(key.to_value()),
626                    value: Some(value.to_value()),
627                })
628                .collect(),
629        }))
630    }
631}
632impl<K: FromValue, V: FromValue> FromValue for GenMap<K, V> {
633    fn from_value(value: &pb::Value) -> Result<Self, ValueError> {
634        match sum(value)? {
635            pb::value::Sum::GenMap(map) => {
636                let entries = map
637                    .entries
638                    .iter()
639                    .enumerate()
640                    .map(|(index, entry)| {
641                        // A GenMap key is an arbitrary value, so unlike a
642                        // TextMap there is no name to point at — the position
643                        // is what a reader can act on. `key`/`value` says which
644                        // half of the entry failed.
645                        let at = |part: &str| {
646                            let part = part.to_string();
647                            move |error: ValueError| error.at(part).at(index.to_string())
648                        };
649                        let key = entry
650                            .key
651                            .as_ref()
652                            .ok_or_else(|| ValueError::new("GenMap entry has no key"))
653                            .map_err(at("key"))?;
654                        let value = entry
655                            .value
656                            .as_ref()
657                            .ok_or_else(|| ValueError::new("GenMap entry has no value"))
658                            .map_err(at("value"))?;
659                        Ok((
660                            K::from_value(key).map_err(at("key"))?,
661                            V::from_value(value).map_err(at("value"))?,
662                        ))
663                    })
664                    .collect::<Result<Vec<_>, ValueError>>()?;
665                Ok(GenMap(entries))
666            }
667            other => Err(mismatch("GenMap", other)),
668        }
669    }
670}