mod choice;
mod commands;
mod primitives;
mod template;
mod value;
pub use choice::Choice;
pub use commands::{create_command, exercise_by_key_command, exercise_command};
pub use primitives::{
ContractId, Date, GenMap, Int64, NestedOpt, Numeric, NumericParseError, Party, PartyParseError,
TextMap, Timestamp, Unit,
};
pub use template::{Contract, Interface, Template, WithKey};
pub use value::{FromValue, ToValue, ValueError, from_record};
#[doc(hidden)]
pub use value::{
AbsentField, enum_constructor, enum_value, find_field, optional_field, record, record_fields,
required_field, unexpected_constructor, unit_value, variant_parts, variant_value,
};
pub use canton_proto::com::daml::ledger::api::v2::{Record, Value};
pub use serde;
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::assertions_on_constants,
non_camel_case_types
)]
mod tests {
use super::*;
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
struct AppInstall {
#[serde(rename = "provider")]
provider: Party,
#[serde(rename = "amount")]
amount: Numeric,
#[serde(rename = "tags")]
tags: Vec<String>,
#[serde(rename = "noteText")]
note: Option<String>,
}
impl ToValue for AppInstall {
fn to_value(&self) -> Value {
record(vec![
("provider", ToValue::to_value(&self.provider)),
("amount", ToValue::to_value(&self.amount)),
("tags", ToValue::to_value(&self.tags)),
("noteText", ToValue::to_value(&self.note)),
])
}
}
impl FromValue for AppInstall {
fn from_value(value: &Value) -> Result<Self, ValueError> {
Ok(Self {
provider: FromValue::from_value(required_field(value, 0, "provider")?)
.map_err(|e| e.at("provider"))?,
amount: FromValue::from_value(required_field(value, 1, "amount")?)
.map_err(|e| e.at("amount"))?,
tags: FromValue::from_value(required_field(value, 2, "tags")?)
.map_err(|e| e.at("tags"))?,
note: optional_field(value, 3, "noteText").map_err(|e| e.at("noteText"))?,
})
}
}
struct AppInstall_Accept;
struct AppInstalled;
impl Choice<AppInstall> for AppInstall_Accept {
type Return = ContractId<AppInstalled>;
const NAME: &'static str = "Accept";
const CONSUMING: bool = true;
}
impl ToValue for AppInstall_Accept {
fn to_value(&self) -> Value {
record(vec![])
}
}
impl Contract for AppInstall {
const PACKAGE_ID: &'static str = "deadbeef";
const PACKAGE_NAME: &'static str = "app-install";
const MODULE_NAME: &'static str = "Licensing.AppInstall";
const ENTITY_NAME: &'static str = "AppInstall";
}
impl Template for AppInstall {
fn to_record(&self) -> Record {
record_fields(vec![
("provider", ToValue::to_value(&self.provider)),
("amount", ToValue::to_value(&self.amount)),
("tags", ToValue::to_value(&self.tags)),
("noteText", ToValue::to_value(&self.note)),
])
}
}
impl WithKey for AppInstall {
type Key = Party;
}
struct Holding;
struct HoldingView;
struct Holding_Transfer;
impl Contract for Holding {
const PACKAGE_ID: &'static str = "cafef00d";
const PACKAGE_NAME: &'static str = "splice-api-token-holding";
const MODULE_NAME: &'static str = "Splice.Api.Token.HoldingV1";
const ENTITY_NAME: &'static str = "Holding";
}
impl Interface for Holding {
type View = HoldingView;
}
impl FromValue for HoldingView {
fn from_value(_value: &Value) -> Result<Self, ValueError> {
Ok(HoldingView)
}
}
impl Choice<Holding> for Holding_Transfer {
type Return = ();
const NAME: &'static str = "Transfer";
const CONSUMING: bool = true;
}
impl ToValue for Holding_Transfer {
fn to_value(&self) -> Value {
record(vec![])
}
}
#[test]
fn a_nested_decode_failure_carries_the_field_path() {
let wrong = record(vec![
("provider", ToValue::to_value(&Party::new("alice"))),
("amount", ToValue::to_value(&Party::new("not-a-number"))),
("tags", ToValue::to_value(&Vec::<String>::new())),
("noteText", ToValue::to_value(&Option::<String>::None)),
]);
let err = <AppInstall as FromValue>::from_value(&wrong).expect_err("amount is not a Party");
assert_eq!(err.path(), ["amount"], "the failing field must be named");
assert!(err.to_string().contains("amount"), "{err}");
}
#[test]
fn a_mismatch_names_the_kind_and_never_the_payload() {
let secret = "alice::1220deadbeef";
let payload = record(vec![
("owner", crate::Party::new(secret).to_value()),
(
"memo",
"wired 4200 to the numbered account".to_string().to_value(),
),
]);
let err = <String as FromValue>::from_value(&payload).expect_err("a Record is not a Text");
let message = err.to_string();
assert!(message.contains("expected Text"), "{message}");
assert!(
message.contains("Record"),
"should name the kind found: {message}"
);
assert!(!message.contains(secret), "leaked a party id: {message}");
assert!(
!message.contains("4200"),
"leaked a payload field: {message}"
);
assert!(!message.contains("memo"), "leaked a field label: {message}");
let sdk: canton_core::Error = err.into();
assert!(!sdk.to_string().contains(secret), "{sdk}");
}
#[test]
fn record_round_trips_through_value() {
let app = AppInstall {
provider: Party::new("alice::1"),
amount: Numeric("1.50".to_string()),
tags: vec!["defi".to_string(), "wallet".to_string()],
note: Some("hi".to_string()),
};
let back = AppInstall::from_value(&app.to_value()).unwrap();
assert_eq!(back, app);
let empty = record(vec![]);
assert!(AppInstall::from_value(&empty).is_err());
}
#[test]
fn primitives_round_trip_through_value() {
let party = Party::new("alice::1");
assert_eq!(Party::from_value(&party.to_value()).unwrap(), party);
let numeric = Numeric("1.50".to_string());
assert_eq!(Numeric::from_value(&numeric.to_value()).unwrap(), numeric);
assert_eq!(i64::from_value(&42_i64.to_value()).unwrap(), 42);
assert!(bool::from_value(&true.to_value()).unwrap());
}
#[test]
fn containers_round_trip_through_value() {
let list = vec!["a".to_string(), "b".to_string()];
assert_eq!(Vec::<String>::from_value(&list.to_value()).unwrap(), list);
let some: Option<String> = Some("x".to_string());
assert_eq!(
Option::<String>::from_value(&some.to_value()).unwrap(),
some
);
let none: Option<String> = None;
assert_eq!(
Option::<String>::from_value(&none.to_value()).unwrap(),
none
);
let mut map = TextMap::new();
map.insert("k".to_string(), 7_i64);
assert_eq!(TextMap::<i64>::from_value(&map.to_value()).unwrap(), map);
}
#[test]
fn contract_id_is_typed_and_round_trips() {
let cid: ContractId<AppInstalled> = ContractId::new("00abc");
let back: ContractId<AppInstalled> = ContractId::from_value(&cid.to_value()).unwrap();
assert_eq!(back, cid);
assert_eq!(back.as_str(), "00abc");
}
#[test]
fn choice_metadata_is_available() {
assert_eq!(<AppInstall_Accept as Choice<AppInstall>>::NAME, "Accept");
assert!(<AppInstall_Accept as Choice<AppInstall>>::CONSUMING);
}
#[test]
fn wrong_shape_is_an_error() {
let text = "not a party".to_string().to_value();
assert!(Party::from_value(&text).is_err());
}
#[test]
fn newtypes_use_lf_json_encoding() {
use serde_json::json;
assert_eq!(
serde_json::to_value(Party::new("alice::1")).unwrap(),
json!("alice::1")
);
assert_eq!(
serde_json::to_value(Numeric("1.50".to_string())).unwrap(),
json!("1.50")
);
let cid: ContractId<()> = ContractId::new("00abc");
assert_eq!(serde_json::to_value(cid).unwrap(), json!("00abc"));
assert_eq!(serde_json::to_value(Date(0)).unwrap(), json!("1970-01-01"));
assert_eq!(serde_json::to_value(Date(1)).unwrap(), json!("1970-01-02"));
for ts in [Timestamp(0), Timestamp(1_700_000_000_000_000)] {
let text = serde_json::to_string(&ts).unwrap();
assert_eq!(serde_json::from_str::<Timestamp>(&text).unwrap(), ts);
}
for date in [Date(0), Date(20_000), Date(-100)] {
let text = serde_json::to_string(&date).unwrap();
assert_eq!(serde_json::from_str::<Date>(&text).unwrap(), date);
}
}
#[test]
fn int64_uses_the_lf_json_string_form() {
use serde_json::json;
assert_eq!(serde_json::to_value(Int64(5)).unwrap(), json!("5"));
assert_eq!(serde_json::from_str::<Int64>("\"5\"").unwrap(), Int64(5));
assert_eq!(serde_json::from_str::<Int64>("5").unwrap(), Int64(5));
let big = Int64(9_007_199_254_740_993);
assert_eq!(
serde_json::from_str::<Int64>(&serde_json::to_string(&big).unwrap()).unwrap(),
big
);
assert_eq!(Int64::from_value(&Int64(7).to_value()).unwrap(), Int64(7));
}
#[test]
fn numeric_accepts_string_and_number_json() {
use serde_json::json;
assert_eq!(
serde_json::to_value(Numeric("1.50".to_string())).unwrap(),
json!("1.50")
);
assert_eq!(
serde_json::from_str::<Numeric>("\"1.5\"").unwrap(),
Numeric("1.5".to_string())
);
assert_eq!(
serde_json::from_str::<Numeric>("2").unwrap(),
Numeric("2".to_string())
);
}
#[test]
fn nested_optional_uses_the_lf_json_list_form() {
use serde_json::json;
let none: Option<NestedOpt<String>> = None;
let some_none: Option<NestedOpt<String>> = Some(NestedOpt(None));
let some_some: Option<NestedOpt<String>> = Some(NestedOpt(Some("x".to_string())));
assert_eq!(serde_json::to_value(&none).unwrap(), json!(null));
assert_eq!(serde_json::to_value(&some_none).unwrap(), json!([]));
assert_eq!(serde_json::to_value(&some_some).unwrap(), json!(["x"]));
for value in [none, some_none, some_some] {
let text = serde_json::to_string(&value).unwrap();
assert_eq!(
serde_json::from_str::<Option<NestedOpt<String>>>(&text).unwrap(),
value
);
}
let triple: Option<NestedOpt<NestedOpt<String>>> = Some(NestedOpt(Some(NestedOpt(None))));
assert_eq!(serde_json::to_value(&triple).unwrap(), json!([[]]));
}
#[test]
fn nested_optional_grpc_matches_option() {
let some_none: Option<NestedOpt<String>> = Some(NestedOpt(None));
let back: Option<NestedOpt<String>> = FromValue::from_value(&some_none.to_value()).unwrap();
assert_eq!(back, some_none);
let none: Option<NestedOpt<String>> = None;
assert_ne!(none.to_value(), some_none.to_value());
}
#[test]
fn unit_is_the_empty_object_in_json() {
use serde_json::json;
assert_eq!(serde_json::to_value(Unit).unwrap(), json!({}));
assert_eq!(serde_json::from_str::<Unit>("{}").unwrap(), Unit);
assert_eq!(serde_json::from_str::<Unit>("null").unwrap(), Unit);
assert_eq!(Unit::from_value(&Unit.to_value()).unwrap(), Unit);
}
#[test]
fn command_builders_produce_ledger_api_commands() {
use canton_proto::com::daml::ledger::api::v2::command::Command as Cmd;
let app = AppInstall {
provider: Party::new("alice::1"),
amount: Numeric("1.00".to_string()),
tags: vec![],
note: None,
};
let create = create_command(&app);
match create.command {
Some(Cmd::Create(c)) => {
let id = c.template_id.unwrap();
assert_eq!(id.package_id, "#app-install");
assert_eq!(id.module_name, "Licensing.AppInstall");
assert_eq!(id.entity_name, "AppInstall");
assert!(c.create_arguments.is_some(), "payload becomes create args");
}
_ => panic!("expected a Create command"),
}
let cid: ContractId<AppInstall> = ContractId::new("00cid");
let exercise = exercise_command(&cid, &AppInstall_Accept);
match exercise.command {
Some(Cmd::Exercise(e)) => {
assert_eq!(e.contract_id, "00cid");
assert_eq!(e.choice, "Accept");
assert_eq!(e.template_id.unwrap().entity_name, "AppInstall");
assert!(e.choice_argument.is_some());
}
_ => panic!("expected an Exercise command"),
}
let key = Party::new("alice::1");
let by_key = exercise_by_key_command::<AppInstall, _>(&key, &AppInstall_Accept);
match by_key.command {
Some(Cmd::ExerciseByKey(e)) => {
assert_eq!(e.choice, "Accept");
assert_eq!(e.template_id.unwrap().entity_name, "AppInstall");
assert!(
e.contract_key.is_some(),
"key becomes the contract_key value"
);
}
_ => panic!("expected an ExerciseByKey command"),
}
}
#[test]
fn exercise_on_interface_builds_a_command() {
use canton_proto::com::daml::ledger::api::v2::command::Command as Cmd;
let cid: ContractId<Holding> = ContractId::new("00hold");
let cmd = exercise_command(&cid, &Holding_Transfer);
match cmd.command {
Some(Cmd::Exercise(e)) => {
assert_eq!(e.contract_id, "00hold");
assert_eq!(e.choice, "Transfer");
let id = e.template_id.unwrap();
assert_eq!(id.entity_name, "Holding");
assert_eq!(id.package_id, "#splice-api-token-holding");
}
_ => panic!("expected an Exercise command on the interface"),
}
}
fn wire_record(labelled: bool, take: usize) -> Value {
use canton_proto::com::daml::ledger::api::v2 as pb;
let sample = AppInstall {
provider: Party::new("p::1"),
amount: Numeric("1.5".to_string()),
tags: vec!["a".to_string()],
note: None,
};
let Some(pb::value::Sum::Record(mut record)) = sample.to_value().sum else {
panic!("record expected");
};
record.fields.truncate(take);
if !labelled {
for field in &mut record.fields {
field.label = String::new();
}
}
Value {
sum: Some(pb::value::Sum::Record(record)),
}
}
#[test]
fn decodes_verbose_records_by_label() {
let decoded = AppInstall::from_value(&wire_record(true, 4)).unwrap();
assert_eq!(decoded.provider, Party::new("p::1"));
assert_eq!(decoded.note, None);
}
#[test]
fn decodes_non_verbose_records_by_position() {
let decoded = AppInstall::from_value(&wire_record(false, 4)).unwrap();
assert_eq!(decoded.provider, Party::new("p::1"));
assert_eq!(decoded.tags, vec!["a".to_string()]);
}
#[test]
fn decodes_normalized_records_with_trailing_optionals_dropped() {
for labelled in [true, false] {
let decoded = AppInstall::from_value(&wire_record(labelled, 3)).unwrap();
assert_eq!(decoded.note, None, "labelled={labelled}");
assert_eq!(
decoded.amount,
Numeric("1.5".to_string()),
"labelled={labelled}"
);
}
}
#[test]
fn genmap_is_constructible_for_real_daml_key_types() {
let mut map: GenMap<Party, Numeric> = GenMap::new();
assert!(map.is_empty());
map.insert(Party::new("alice::1"), Numeric::from_wire("1.5"));
assert_eq!(map.len(), 1);
assert_eq!(GenMap::<Party, Numeric>::default().len(), 0);
let collected: GenMap<Party, Int64> =
[(Party::new("a::1"), Int64(1))].into_iter().collect();
assert_eq!(collected.iter().count(), 1);
assert_eq!((&collected).into_iter().count(), 1);
assert_eq!(collected.into_iter().count(), 1);
}
#[test]
fn genmap_equality_is_order_insensitive_and_symmetric() {
let a = Party::new("a::1");
let b = Party::new("b::1");
let submitted: GenMap<Party, Int64> =
vec![(a.clone(), Int64(1)), (b.clone(), Int64(2))].into();
let echoed: GenMap<Party, Int64> =
vec![(b.clone(), Int64(2)), (a.clone(), Int64(1))].into();
assert_eq!(submitted, echoed);
let twice: GenMap<Party, Int64> = vec![(a.clone(), Int64(1)), (a.clone(), Int64(1))].into();
let mixed: GenMap<Party, Int64> = vec![(a.clone(), Int64(1)), (b, Int64(2))].into();
assert_ne!(twice, mixed);
assert_ne!(mixed, twice, "equality must be symmetric");
assert_eq!(twice, twice.clone());
}
#[test]
fn timestamp_and_date_convert_through_time_without_saturating() {
use time::macros::{date, datetime};
let at = Timestamp::from_datetime(datetime!(2026-07-31 12:00:00 UTC)).unwrap();
assert_eq!(
at.to_datetime().unwrap(),
datetime!(2026-07-31 12:00:00 UTC)
);
assert_eq!(Timestamp(at.micros()), at);
let before_epoch = datetime!(1969-12-31 23:59:59.999_999_5 UTC);
let floored = Timestamp::from_datetime(before_epoch).unwrap();
assert!(floored.to_datetime().unwrap() <= before_epoch);
let extreme = time::OffsetDateTime::new_utc(date!(9999 - 12 - 31), time::Time::MIDNIGHT);
let converted = Timestamp::from_datetime(extreme).expect("in range for default `time`");
assert_eq!(converted.to_datetime().unwrap(), extreme);
let day = Date::from_date(date!(2026 - 07 - 31)).unwrap();
assert_eq!(day.to_date().unwrap(), date!(2026 - 07 - 31));
assert_eq!(Date::from_date(date!(1970 - 01 - 01)).unwrap().days(), 0);
}
#[test]
fn numeric_parsing_is_strict_for_callers_and_lenient_on_the_wire() {
use std::str::FromStr as _;
assert_eq!(Numeric::from_str("1.5").unwrap().as_str(), "1.5");
assert_eq!(Numeric::try_from("-0.25").unwrap().to_string(), "-0.25");
let error = Numeric::from_str("twelve").unwrap_err();
assert_eq!(error.input(), "twelve");
let wire = Numeric::from_wire("1.5000000000");
assert_eq!(wire, Numeric::from_str("1.5").unwrap());
assert_eq!(Numeric::from_wire("42."), Numeric::from_wire("42"));
assert_eq!(Numeric::from_wire("42."), Numeric::from_wire("42.000"));
}
#[test]
fn value_errors_carry_a_path_and_flow_into_the_sdk_error() {
let error = ValueError::new("expected Text")
.at("city")
.at("address")
.at("owner");
assert_eq!(error.path(), ["owner", "address", "city"]);
assert!(
error.to_string().contains("`owner.address.city`"),
"{error}"
);
assert_eq!(error.message(), "expected Text");
let sdk: canton_core::Error = error.clone().into();
assert!(!sdk.is_retriable(), "a shape mismatch will not fix itself");
assert!(
std::error::Error::source(&sdk)
.map(ToString::to_string)
.unwrap_or_default()
.contains("owner.address.city"),
"the cause chain keeps the path"
);
}
#[test]
fn numeric_compares_by_value_not_by_text() {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let submitted = Numeric("1.5".to_string());
let echoed = Numeric("1.5000000000".to_string());
assert_eq!(submitted, echoed);
let hash = |n: &Numeric| {
let mut hasher = DefaultHasher::new();
n.hash(&mut hasher);
hasher.finish()
};
assert_eq!(hash(&submitted), hash(&echoed), "Eq and Hash must agree");
assert_eq!(Numeric("-0".to_string()), Numeric("0.00".to_string()));
assert_eq!(Numeric("+07.10".to_string()), Numeric("7.1".to_string()));
assert!(Numeric("2".to_string()) < Numeric("10".to_string()));
assert!(Numeric("1.45".to_string()) < Numeric("1.5".to_string()));
assert!(Numeric("-1.5".to_string()) < Numeric("-1.45".to_string()));
assert!(Numeric("-1".to_string()) < Numeric("0.1".to_string()));
assert_ne!(Numeric("abc".to_string()), Numeric("1".to_string()));
assert!(Numeric::parse("1.5").is_ok());
assert!(Numeric::parse("-12.500").is_ok());
assert!(Numeric::parse("1.").is_ok());
for bad in ["", ".5", "1.2.3", "1e5", "abc", "1,5"] {
assert!(Numeric::parse(bad).is_err(), "{bad:?} should be rejected");
}
}
#[test]
fn contract_id_retags_for_interface_exercise() {
use canton_proto::com::daml::ledger::api::v2::command::Command as Cmd;
let concrete: ContractId<AppInstall> = ContractId::new("00abc");
let as_interface: ContractId<Holding> = concrete.retag();
assert_eq!(as_interface.as_str(), "00abc");
let cmd = exercise_command(&as_interface, &Holding_Transfer);
match cmd.command {
Some(Cmd::Exercise(e)) => {
assert_eq!(
e.template_id.unwrap().package_id,
"#splice-api-token-holding"
);
}
_ => panic!("expected an interface exercise"),
}
let mut set = std::collections::HashSet::new();
set.insert(ContractId::<AppInstall>::new("00abc"));
assert!(set.contains(&ContractId::<AppInstall>::new("00abc")));
}
#[test]
fn from_created_event_decodes_the_typed_read_path() {
use canton_proto::com::daml::ledger::api::v2 as pb;
let payload = AppInstall {
provider: Party::new("p::1"),
amount: Numeric("2".to_string()),
tags: vec![],
note: None,
};
let Some(pb::value::Sum::Record(record)) = payload.to_value().sum else {
panic!("record expected");
};
let event = pb::CreatedEvent {
template_id: Some(pb::Identifier {
package_id: "someotherhash".to_string(),
module_name: AppInstall::MODULE_NAME.to_string(),
entity_name: AppInstall::ENTITY_NAME.to_string(),
}),
create_arguments: Some(record),
..Default::default()
};
let decoded = AppInstall::from_created_event(&event).unwrap();
assert_eq!(decoded, payload);
let mut wrong = event.clone();
wrong.template_id.as_mut().unwrap().entity_name = "SomethingElse".to_string();
let error = AppInstall::from_created_event(&wrong).unwrap_err();
assert!(error.message().contains("SomethingElse"), "{error}");
assert!(error.message().contains("AppInstall"), "{error}");
let mut empty = event.clone();
empty.create_arguments = None;
assert!(AppInstall::from_created_event(&empty).is_err());
}
#[test]
fn missing_required_field_is_an_error_naming_the_field() {
let error = AppInstall::from_value(&wire_record(true, 2)).unwrap_err();
assert!(
error.message().contains("tags"),
"error should name the missing field: {error}"
);
}
#[test]
fn from_json_created_event_decodes_and_refuses_the_wrong_template() {
use serde_json::json;
let payload = json!({
"provider": "alice::1220ab",
"amount": "1.5",
"tags": ["a"],
"noteText": null
});
let event = json!({"CreatedEvent": {
"contractId": "00abc",
"templateId": "somepackage:Licensing.AppInstall:AppInstall",
"createArgument": payload,
}});
let decoded = AppInstall::from_json_created_event(&event).unwrap();
assert_eq!(decoded.provider, Party::new("alice::1220ab"));
assert_eq!(decoded.amount, Numeric::from_wire("1.5"));
let bare = json!({
"templateId": "somepackage:Licensing.AppInstall:AppInstall",
"createArgument": payload,
});
assert!(AppInstall::from_json_created_event(&bare).is_ok());
let impostor = json!({"CreatedEvent": {
"templateId": "somepackage:Other.Module:SomethingElse",
"createArgument": payload,
}});
let error = AppInstall::from_json_created_event(&impostor)
.expect_err("a different template must not decode");
assert!(error.to_string().contains("Other.Module"), "{error}");
assert!(
error.to_string().contains("Licensing.AppInstall"),
"{error}"
);
let upgraded = json!({"CreatedEvent": {
"templateId": "adifferentpackageid:Licensing.AppInstall:AppInstall",
"createArgument": payload,
}});
assert!(AppInstall::from_json_created_event(&upgraded).is_ok());
let empty = json!({"CreatedEvent": {"contractId": "00abc"}});
assert!(AppInstall::from_json_created_event(&empty).is_err());
}
#[test]
fn a_container_element_is_named_in_the_path() {
use std::collections::BTreeMap;
let list = wrong_at_index();
let error = <Vec<Party> as FromValue>::from_value(&list).unwrap_err();
assert_eq!(error.path(), ["2"], "{error}");
assert!(error.to_string().contains("expected Party"), "{error}");
let record = record(vec![("holders", list)]);
let error = <AppInstallHolders as FromValue>::from_value(&record).unwrap_err();
assert_eq!(error.path(), ["holders", "2"], "{error}");
assert!(error.to_string().contains("`holders.2`"), "{error}");
let map = Value {
sum: Some(
canton_proto::com::daml::ledger::api::v2::value::Sum::TextMap(
canton_proto::com::daml::ledger::api::v2::TextMap {
entries: vec![canton_proto::com::daml::ledger::api::v2::text_map::Entry {
key: "alice".to_string(),
value: Some(42_i64.to_value()),
}],
},
),
),
};
let error = <BTreeMap<String, Party> as FromValue>::from_value(&map).unwrap_err();
assert_eq!(error.path(), ["alice"], "{error}");
let genmap = Value {
sum: Some(
canton_proto::com::daml::ledger::api::v2::value::Sum::GenMap(
canton_proto::com::daml::ledger::api::v2::GenMap {
entries: vec![canton_proto::com::daml::ledger::api::v2::gen_map::Entry {
key: Some(Party::new("alice::1220ab").to_value()),
value: Some(42_i64.to_value()),
}],
},
),
),
};
let error = <GenMap<Party, Party> as FromValue>::from_value(&genmap).unwrap_err();
assert_eq!(error.path(), ["0", "value"], "{error}");
}
fn wrong_at_index() -> Value {
list(vec![
Party::new("a::1220ab").to_value(),
Party::new("b::1220ab").to_value(),
42_i64.to_value(),
])
}
fn list(elements: Vec<Value>) -> Value {
use canton_proto::com::daml::ledger::api::v2 as pb;
Value {
sum: Some(pb::value::Sum::List(pb::List { elements })),
}
}
#[derive(Debug, PartialEq)]
struct AppInstallHolders {
holders: Vec<Party>,
}
impl FromValue for AppInstallHolders {
fn from_value(value: &Value) -> Result<Self, ValueError> {
Ok(Self {
holders: FromValue::from_value(required_field(value, 0, "holders")?)
.map_err(|e| e.at("holders"))?,
})
}
}
#[test]
fn a_party_id_a_caller_supplies_is_validated() {
assert_eq!("".parse::<Party>(), Err(PartyParseError::Empty));
assert_eq!(
"alice\nbob".parse::<Party>(),
Err(PartyParseError::UnexpectedCharacter { character: '\n' })
);
assert!(matches!(
"x".repeat(256).parse::<Party>(),
Err(PartyParseError::TooLong { length: 256 })
));
let real = "app_provider_quickstart-dimakozoliy-1::\
12204a2925a78214d3a4c81f3b425f89894a42858b9e8f13a8427c68513232cdf640";
assert!(real.parse::<Party>().is_ok(), "{real}");
assert_eq!(real.parse::<Party>().unwrap().as_str(), real);
let odd = Party::new("something\nthe ledger vouched for");
let value = odd.to_value();
assert_eq!(<Party as FromValue>::from_value(&value).unwrap(), odd);
let message = PartyParseError::Empty.to_string();
assert!(message.contains("cannot be empty"), "{message}");
}
}