radicle/id: Introduce `PayloadUpsert`

Instead of using triples, introduce a `struct` to carry upserts.
This commit is contained in:
Lorenz Leutgeb 2025-10-14 20:27:28 +02:00 committed by Lorenz Leutgeb
parent ec1d754308
commit d3ed4bb497
1 changed files with 41 additions and 27 deletions

View File

@ -139,40 +139,52 @@ where
} }
} }
/// [`Payload`]: super::Payload
/// A change (update or insertion) to particular `key` within a [`Payload`]
/// in a document.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PayloadUpsert {
/// [`Payload`]: super::Payload
/// The identifier for the document [`Payload`].
pub id: PayloadId,
/// [`Payload`]: super::Payload
/// The key within the [`Payload`] that is being updated.
pub key: String,
/// [`Payload`]: super::Payload
/// The value to update within the [`Payload`].
pub value: json::Value,
}
// TODO(finto): I think this API would likely be much nicer if we use [JSON Patch][patch] and [JSON Merge Patch][merge] // TODO(finto): I think this API would likely be much nicer if we use [JSON Patch][patch] and [JSON Merge Patch][merge]
// //
// [patch]: https://datatracker.ietf.org/doc/html/rfc6902 // [patch]: https://datatracker.ietf.org/doc/html/rfc6902
// [merge]: https://datatracker.ietf.org/doc/html/rfc7396 // [merge]: https://datatracker.ietf.org/doc/html/rfc7396
/// Change the payload of the document, using the set of triples: /// [`Payload`]: super::Payload
/// /// Change (update or insert) a key in a [`Payload`] of the document,
/// - [`PayloadId`]: the identifier for the document [`Payload`] /// using the provided `updates`.
/// - [`String`]: the key within the [`Payload`] that is being updated
/// - [`json::Value`]: the value to update the [`Payload`]
/// ///
/// # Errors /// # Errors
/// ///
/// This fails if one of the [`PayloadId`]s does not point to a JSON object as /// This fails if one of the [`PayloadId`]s does not point to a JSON object as
/// its value. /// its value.
///
/// [`Payload`]: super::Payload
pub fn payload( pub fn payload(
mut raw: RawDoc, mut raw: RawDoc,
payload: Vec<(PayloadId, String, json::Value)>, upserts: impl IntoIterator<Item = PayloadUpsert>,
) -> Result<RawDoc, error::PayloadError> { ) -> Result<RawDoc, error::PayloadError> {
for (id, key, val) in payload { for PayloadUpsert { id, key, value } in upserts {
if let Some(ref mut payload) = raw.payload.get_mut(&id) { if let Some(ref mut payload) = raw.payload.get_mut(&id) {
if let Some(obj) = payload.as_object_mut() { if let Some(obj) = payload.as_object_mut() {
if val.is_null() { if value.is_null() {
obj.remove(&key); obj.remove(&key);
} else { } else {
obj.insert(key, val); obj.insert(key, value);
} }
} else { } else {
return Err(error::PayloadError::ExpectedObject { id }); return Err(error::PayloadError::ExpectedObject { id });
} }
} else { } else {
raw.payload raw.payload
.insert(id, serde_json::json!({ key: val }).into()); .insert(id, serde_json::json!({ key: value }).into());
} }
} }
Ok(raw) Ok(raw)
@ -277,21 +289,23 @@ mod test {
test::arbitrary, test::arbitrary,
}; };
use super::PayloadUpsert;
#[test] #[test]
fn test_can_update_crefs() { fn test_can_update_crefs() {
let raw = arbitrary::gen::<RawDoc>(1); let raw = arbitrary::gen::<RawDoc>(1);
let raw = super::payload( let raw = super::payload(
raw, raw,
vec![( [PayloadUpsert {
PayloadId::canonical_refs(), id: PayloadId::canonical_refs(),
"rules".to_string(), key: "rules".to_string(),
json!({ value: json!({
"refs/tags/*": { "refs/tags/*": {
"threshold": 1, "threshold": 1,
"allow": "delegates" "allow": "delegates"
} }
}), }),
)], }],
) )
.unwrap(); .unwrap();
let verified = super::verify(raw); let verified = super::verify(raw);
@ -306,10 +320,10 @@ mod test {
)); ));
let raw = super::payload( let raw = super::payload(
raw, raw,
vec![( [PayloadUpsert {
PayloadId::canonical_refs(), id: PayloadId::canonical_refs(),
"rules".to_string(), key: "rules".to_string(),
json!({ value: json!({
"refs/tags/*": { "refs/tags/*": {
"threshold": 1, "threshold": 1,
"allow": "delegates" "allow": "delegates"
@ -319,7 +333,7 @@ mod test {
"allow": "delegates", "allow": "delegates",
} }
}), }),
)], }],
) )
.unwrap(); .unwrap();
assert!( assert!(
@ -339,16 +353,16 @@ mod test {
)); ));
let raw = super::payload( let raw = super::payload(
raw, raw,
vec![( [PayloadUpsert {
PayloadId::canonical_refs(), id: PayloadId::canonical_refs(),
"rules".to_string(), key: "rules".to_string(),
json!({ value: json!({
"refs/tags/*": { "refs/tags/*": {
"threshold": 1, "threshold": 1,
"allow": "delegates" "allow": "delegates"
} }
}), }),
)], }],
) )
.unwrap(); .unwrap();
let verified = super::verify(raw).unwrap(); let verified = super::verify(raw).unwrap();