use canton_proto::com::daml::ledger::api::v2::Identifier;
use crate::value::{FromValue, ToValue, ValueError};
pub trait Contract {
const PACKAGE_ID: &'static str;
const PACKAGE_NAME: &'static str;
const MODULE_NAME: &'static str;
const ENTITY_NAME: &'static str;
#[must_use]
fn template_id() -> Identifier {
let package = if Self::PACKAGE_NAME.is_empty() {
Self::PACKAGE_ID.to_string()
} else {
format!("#{}", Self::PACKAGE_NAME)
};
Identifier {
package_id: package,
module_name: Self::MODULE_NAME.to_string(),
entity_name: Self::ENTITY_NAME.to_string(),
}
}
}
pub trait Template: Contract + ToValue + FromValue {
fn to_record(&self) -> canton_proto::com::daml::ledger::api::v2::Record;
fn from_created_event(
event: &canton_proto::com::daml::ledger::api::v2::CreatedEvent,
) -> Result<Self, ValueError> {
let id = event.template_id.as_ref().ok_or_else(|| {
ValueError::new(
"created event carries no template id, so it cannot be checked to be this template",
)
})?;
check_identity::<Self>(
&id.module_name,
&id.entity_name,
&event.package_name,
&id.package_id,
)?;
let record = event
.create_arguments
.as_ref()
.ok_or_else(|| ValueError::new("created event carries no create_arguments payload"))?;
crate::value::from_record(record)
}
fn from_json_created_event(event: &serde_json::Value) -> Result<Self, ValueError>
where
Self: serde::de::DeserializeOwned,
{
let inner = event.get("created").unwrap_or(event);
let created = ["CreatedEvent", "createdEvent"]
.iter()
.find_map(|key| inner.get(key))
.unwrap_or(inner);
let id = created
.get("templateId")
.and_then(serde_json::Value::as_str)
.ok_or_else(|| {
ValueError::new(
"created event carries no templateId, so it cannot be checked to be this \
template",
)
})?;
let mut parts = id.rsplitn(3, ':');
let entity = parts.next().unwrap_or_default();
let module = parts.next().unwrap_or_default();
let package = parts.next().unwrap_or_default();
let package_name = created
.get("packageName")
.and_then(serde_json::Value::as_str)
.unwrap_or_default();
check_identity::<Self>(module, entity, package_name, package)?;
let payload = created.get("createArgument").ok_or_else(|| {
ValueError::new("created event carries no createArgument payload")
})?;
serde_json::from_value(payload.clone())
.map_err(|error| ValueError::new(format!("payload did not decode: {error}")))
}
}
fn check_identity<T: Contract + ?Sized>(
module: &str,
entity: &str,
package_name: &str,
package_ref: &str,
) -> Result<(), ValueError> {
if entity != T::ENTITY_NAME || module != T::MODULE_NAME {
return Err(ValueError::new(format!(
"event is {module}:{entity}, expected {}:{}",
T::MODULE_NAME,
T::ENTITY_NAME,
)));
}
if T::PACKAGE_NAME.is_empty() {
return Ok(());
}
let name = if package_name.is_empty() {
package_ref.strip_prefix('#').ok_or_else(|| {
ValueError::new(format!(
"event of {module}:{entity} carries no package name, so it cannot be checked to \
come from `{}`",
T::PACKAGE_NAME
))
})?
} else {
package_name
};
if name != T::PACKAGE_NAME {
return Err(ValueError::new(format!(
"event of {module}:{entity} is from package `{name}`, expected `{}`",
T::PACKAGE_NAME
)));
}
Ok(())
}
pub trait Interface: Contract {
type View: FromValue;
}
pub trait WithKey: Template {
type Key: ToValue;
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used)]
mod json_envelope_tests {
use super::*;
use canton_proto::com::daml::ledger::api::v2 as pb;
use serde::Deserialize;
#[derive(Debug, PartialEq, Deserialize)]
struct Widget {
owner: String,
}
impl Contract for Widget {
const PACKAGE_ID: &'static str = "pkg";
const PACKAGE_NAME: &'static str = "app";
const MODULE_NAME: &'static str = "M";
const ENTITY_NAME: &'static str = "Widget";
}
impl ToValue for Widget {
fn to_value(&self) -> pb::Value {
crate::Unit.to_value()
}
}
impl FromValue for Widget {
fn from_value(_: &pb::Value) -> Result<Self, ValueError> {
Err(ValueError::new("unused"))
}
}
impl Template for Widget {
fn to_record(&self) -> pb::Record {
pb::Record::default()
}
}
fn created() -> serde_json::Value {
serde_json::json!({
"templateId": "whatever-package:M:Widget",
"packageName": "app",
"createArgument": { "owner": "alice" }
})
}
fn with(patch: &[(&str, serde_json::Value)]) -> serde_json::Value {
let mut event = created();
for (key, value) in patch {
if value.is_null() {
event.as_object_mut().expect("object").remove(*key);
} else {
event[*key] = value.clone();
}
}
event
}
fn grpc(package_id: &str, package_name: &str) -> pb::CreatedEvent {
pb::CreatedEvent {
template_id: Some(pb::Identifier {
package_id: package_id.to_string(),
module_name: "M".to_string(),
entity_name: "Widget".to_string(),
}),
package_name: package_name.to_string(),
..Default::default()
}
}
#[test]
fn the_package_name_is_checked_and_the_package_id_is_not() {
use serde_json::json;
assert!(
Widget::from_json_created_event(&with(&[(
"templateId",
json!("another-version:M:Widget")
)]))
.is_ok()
);
let foreign =
Widget::from_json_created_event(&with(&[("packageName", json!("someone-elses-app"))]))
.expect_err("a foreign package must not decode");
assert!(
foreign.to_string().contains("someone-elses-app"),
"{foreign}"
);
let by_name = with(&[
("templateId", json!("#app:M:Widget")),
("packageName", serde_json::Value::Null),
]);
assert!(Widget::from_json_created_event(&by_name).is_ok());
let by_other_name = with(&[
("templateId", json!("#other:M:Widget")),
("packageName", serde_json::Value::Null),
]);
assert!(Widget::from_json_created_event(&by_other_name).is_err());
let error = Widget::from_created_event(&grpc("bbbb", "someone-elses-app"))
.expect_err("a foreign package must not decode");
assert!(error.to_string().contains("someone-elses-app"), "{error}");
}
#[test]
fn an_event_whose_identity_cannot_be_checked_is_refused() {
let no_template = with(&[("templateId", serde_json::Value::Null)]);
assert!(Widget::from_json_created_event(&no_template).is_err());
let no_name = with(&[("packageName", serde_json::Value::Null)]);
let error = Widget::from_json_created_event(&no_name).expect_err("no package name");
assert!(error.to_string().contains("no package name"), "{error}");
let mut event = grpc("bbbb", "app");
event.template_id = None;
let error = Widget::from_created_event(&event).expect_err("no template id");
assert!(error.to_string().contains("no template id"), "{error}");
let error = Widget::from_created_event(&grpc("bbbb", "")).expect_err("no package name");
assert!(error.to_string().contains("no package name"), "{error}");
}
#[test]
fn every_envelope_shape_the_json_api_uses_decodes() {
let want = Widget {
owner: "alice".to_string(),
};
let shapes = [
created(),
serde_json::json!({ "CreatedEvent": created() }),
serde_json::json!({ "createdEvent": created() }),
serde_json::json!({ "created": { "createdEvent": created() } }),
];
for (i, shape) in shapes.iter().enumerate() {
assert_eq!(
Widget::from_json_created_event(shape).expect("shape decodes"),
want,
"envelope shape {i} did not decode"
);
}
}
}