Skip to main content

systemprompt_models/managed/
manifest.rs

1//! Canonical revision manifests bind files, dependencies and provenance.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use serde::{Deserialize, Serialize};
7use std::collections::{BTreeMap, BTreeSet};
8use systemprompt_identifiers::{ResourceRevisionId, SourceSnapshotId};
9
10use super::assets::{AssetDigest, RevisionFiles};
11use super::error::{RevisionBundleError, invalid};
12
13type Result<T> = std::result::Result<T, RevisionBundleError>;
14
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
16#[serde(deny_unknown_fields)]
17pub struct FileEntry {
18    pub digest: AssetDigest,
19    pub bytes: u64,
20    pub media_type: String,
21    pub executable: bool,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
25#[serde(deny_unknown_fields)]
26pub struct DependencyRef {
27    pub revision_id: ResourceRevisionId,
28    pub digest: AssetDigest,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
32#[serde(deny_unknown_fields)]
33pub struct RevisionManifest {
34    pub schema_version: u32,
35    pub snapshot_id: SourceSnapshotId,
36    pub parent_id: Option<ResourceRevisionId>,
37    pub files: BTreeMap<String, FileEntry>,
38    pub dependencies: BTreeMap<String, DependencyRef>,
39}
40
41impl RevisionManifest {
42    pub fn from_files(
43        snapshot_id: SourceSnapshotId,
44        parent_id: Option<ResourceRevisionId>,
45        files: &RevisionFiles,
46        dependencies: BTreeMap<String, DependencyRef>,
47    ) -> Result<Self> {
48        files.validate()?;
49        if dependencies.len() > 256 {
50            return Err(invalid("Revision has more than 256 dependencies"));
51        }
52        let mut ids = BTreeSet::new();
53        for (key, dependency) in &dependencies {
54            validate_key(key)?;
55            if !ids.insert(dependency.revision_id.as_str()) {
56                return Err(invalid("Duplicate dependency revision"));
57            }
58        }
59        Ok(Self {
60            schema_version: 1,
61            snapshot_id,
62            parent_id,
63            files: files
64                .0
65                .iter()
66                .map(|(path, file)| {
67                    (
68                        path.clone(),
69                        FileEntry {
70                            digest: AssetDigest::of(&file.bytes),
71                            bytes: file.bytes.len() as u64,
72                            media_type: file.media_type.clone(),
73                            executable: file.executable,
74                        },
75                    )
76                })
77                .collect(),
78            dependencies,
79        })
80    }
81
82    pub fn digest(&self) -> Result<AssetDigest> {
83        Ok(AssetDigest::of(&serde_jcs::to_vec(self)?))
84    }
85}
86
87pub fn validate_key(key: &str) -> Result<()> {
88    if key.is_empty()
89        || key.len() > 200
90        || !key
91            .bytes()
92            .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'_' | b'-' | b'.' | b'/'))
93    {
94        return Err(invalid("Invalid resource key"));
95    }
96    Ok(())
97}