Skip to main content

af_workflow/
metadata.rs

1//! Versioned metadata commands; published execution contracts remain immutable.
2use af_context::{InstanceId, WorkflowDefinitionId};
3use serde::{Deserialize, Serialize};
4
5/// Resource whose mutable display metadata is being changed.
6#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(tag = "resource", rename_all = "snake_case", deny_unknown_fields)]
8pub enum WorkflowTarget {
9    /// A definition owned by its author or managed by an administrator.
10    Definition {
11        /// Definition identity.
12        definition_id: WorkflowDefinitionId,
13    },
14    /// An instance owned by the caller.
15    Instance {
16        /// Instance identity.
17        instance_id: InstanceId,
18    },
19}
20/// Compare-and-set metadata command with a durable idempotency receipt.
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22#[serde(deny_unknown_fields)]
23pub struct WorkflowMetadataChange {
24    /// Target resource.
25    pub target: WorkflowTarget,
26    /// Version displayed to the caller.
27    pub expected_version: u64,
28    /// New display name; omitted to preserve it.
29    pub name: Option<String>,
30    /// Archive/unarchive; omitted to preserve it. Active instances cannot be archived.
31    pub archived: Option<bool>,
32    /// Stable command identity, scoped to the authenticated tenant and subject.
33    pub idempotency_key: String,
34}
35impl WorkflowMetadataChange {
36    /// Reject empty patches, oversized names/keys and unrepresentable versions.
37    pub fn validate(&self) -> Result<(), String> {
38        if self.expected_version == 0 || self.expected_version >= i64::MAX as u64 {
39            return Err("workflow metadata version must be within 1..i64::MAX".into());
40        }
41        if self.idempotency_key.trim().is_empty() || self.idempotency_key.len() > 256 {
42            return Err("workflow metadata idempotency key requires 1..256 bytes".into());
43        }
44        if self.name.is_none() && self.archived.is_none() {
45            return Err("workflow metadata patch is empty".into());
46        }
47        if self
48            .name
49            .as_ref()
50            .is_some_and(|name| name.trim().is_empty() || name.chars().count() > 256)
51        {
52            return Err("workflow display name requires 1..256 characters".into());
53        }
54        Ok(())
55    }
56}
57/// Receipt of one metadata command, retained unchanged for retries.
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59pub struct WorkflowMetadata {
60    /// Resource identity.
61    pub target: WorkflowTarget,
62    /// Committed metadata version.
63    pub version: u64,
64    /// Current display name at this version.
65    pub name: String,
66    /// Archive state at this version.
67    pub archived: bool,
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73    #[test]
74    fn metadata_input_is_bounded_and_has_no_identity_override() {
75        let valid = WorkflowMetadataChange {
76            target: WorkflowTarget::Definition {
77                definition_id: "definition".parse().unwrap(),
78            },
79            expected_version: 1,
80            name: Some("名称".into()),
81            archived: None,
82            idempotency_key: "key".into(),
83        };
84        assert!(valid.validate().is_ok());
85        let mut changed = valid.clone();
86        changed.name = None;
87        assert!(changed.validate().is_err());
88        changed.archived = Some(false);
89        assert!(changed.validate().is_ok());
90        for name in [" ".into(), "字".repeat(257)] {
91            changed.name = Some(name);
92            assert!(changed.validate().is_err());
93        }
94        for version in [0, i64::MAX as u64, u64::MAX] {
95            let mut changed = valid.clone();
96            changed.expected_version = version;
97            assert!(changed.validate().is_err());
98        }
99        for key in [" ".into(), "k".repeat(257)] {
100            let mut changed = valid.clone();
101            changed.idempotency_key = key;
102            assert!(changed.validate().is_err());
103        }
104        let mut value = serde_json::to_value(&valid).unwrap();
105        value["subject_id"] = "other".into();
106        assert!(serde_json::from_value::<WorkflowMetadataChange>(value).is_err());
107        let instance = WorkflowTarget::Instance {
108            instance_id: "instance".parse().unwrap(),
109        };
110        assert_eq!(
111            serde_json::from_value::<WorkflowTarget>(serde_json::to_value(&instance).unwrap())
112                .unwrap(),
113            instance
114        );
115    }
116}