Skip to main content

arete_hash/
projection.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4use crate::{
5    hash_framed_tuple, hash_jcs, Compiler, HashError, HashId, ProgramRelease, ProgramSpec,
6    SdkDefinition, TupleField,
7};
8
9pub const COMPILER_SCHEMA_V1: &str = "arete.compiler/v1";
10pub const SDK_DEFINITION_SCHEMA_V1: &str = "arete.sdk-definition/v1";
11pub const SDK_DEFINITION_PROGRAM_SPEC_INPUT_KIND: &str = "program-spec";
12pub const OSS_DECODER_ENGINE_ID: &str = "arete-oss-generated-decoder/v1";
13pub const PROGRAM_RELEASE_SCHEMA_V1: &str = "arete.program-release/v1";
14pub const HOSTED_MANAGED_RELEASE_PROFILE: &str = "hosted-managed";
15pub const OSS_GENERATED_RELEASE_PROFILE: &str = "oss-generated";
16
17/// Remove the declared top-level self-hash field and no other field.
18///
19/// Nested `artifactHash` fields and all other hash-like fields are retained.
20pub fn project_without_artifact_hash(value: &Value) -> Result<Value, HashError> {
21    let mut projection = value
22        .as_object()
23        .cloned()
24        .ok_or(HashError::InvalidSelfHashProjection)?;
25    projection.remove("artifactHash");
26    Ok(Value::Object(projection))
27}
28
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct CompilerSourceV1 {
31    pub path: String,
32    pub bytes: Vec<u8>,
33}
34
35impl CompilerSourceV1 {
36    pub fn new(path: impl Into<String>, bytes: impl Into<Vec<u8>>) -> Self {
37        Self {
38            path: path.into(),
39            bytes: bytes.into(),
40        }
41    }
42}
43
44/// Frozen v1 identity projection for the OSS SDK compiler source tree.
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct CompilerV1 {
47    pub schema: String,
48    pub sources: Vec<CompilerSourceV1>,
49}
50
51impl CompilerV1 {
52    pub fn new(sources: impl IntoIterator<Item = CompilerSourceV1>) -> Result<Self, HashError> {
53        let mut projection = Self {
54            schema: COMPILER_SCHEMA_V1.to_string(),
55            sources: sources.into_iter().collect(),
56        };
57        projection
58            .sources
59            .sort_by(|left, right| left.path.as_bytes().cmp(right.path.as_bytes()));
60        projection.validate()?;
61        Ok(projection)
62    }
63
64    pub fn hash(&self) -> Result<HashId<Compiler>, HashError> {
65        self.validate()?;
66        let mut fields = Vec::with_capacity(self.sources.len() + 1);
67        fields.push(TupleField::new("schema", self.schema.as_bytes()));
68        fields.extend(
69            self.sources
70                .iter()
71                .map(|source| TupleField::new(&source.path, &source.bytes)),
72        );
73        hash_framed_tuple(&fields)
74    }
75
76    fn validate(&self) -> Result<(), HashError> {
77        if self.schema != COMPILER_SCHEMA_V1 {
78            return Err(HashError::UnknownVersion(self.schema.clone()));
79        }
80        if self.sources.is_empty() {
81            return Err(HashError::InvalidProjection {
82                projection: "compiler",
83                reason: "sources must not be empty".to_string(),
84            });
85        }
86        let mut previous: Option<&[u8]> = None;
87        for source in &self.sources {
88            if source.path.is_empty() || source.path == "schema" {
89                return Err(HashError::InvalidProjection {
90                    projection: "compiler",
91                    reason: format!("invalid source path '{}'", source.path),
92                });
93            }
94            if let Some(previous) = previous {
95                match previous.cmp(source.path.as_bytes()) {
96                    std::cmp::Ordering::Greater => {
97                        return Err(HashError::InvalidProjection {
98                            projection: "compiler",
99                            reason: "sources must be sorted by raw UTF-8 path bytes".to_string(),
100                        })
101                    }
102                    std::cmp::Ordering::Equal => {
103                        return Err(HashError::InvalidProjection {
104                            projection: "compiler",
105                            reason: format!("duplicate source path '{}'", source.path),
106                        })
107                    }
108                    std::cmp::Ordering::Less => {}
109                }
110            }
111            previous = Some(source.path.as_bytes());
112        }
113        Ok(())
114    }
115}
116
117/// Frozen v1 identity projection for one generated program SDK definition.
118#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
119#[serde(rename_all = "camelCase")]
120pub struct SdkDefinitionV1 {
121    pub schema: String,
122    pub input_kind: String,
123    pub input_hash: HashId<ProgramSpec>,
124    pub compiler_hash: HashId<Compiler>,
125}
126
127impl SdkDefinitionV1 {
128    pub fn new(input_hash: HashId<ProgramSpec>, compiler_hash: HashId<Compiler>) -> Self {
129        Self {
130            schema: SDK_DEFINITION_SCHEMA_V1.to_string(),
131            input_kind: SDK_DEFINITION_PROGRAM_SPEC_INPUT_KIND.to_string(),
132            input_hash,
133            compiler_hash,
134        }
135    }
136
137    pub fn hash(&self) -> Result<HashId<SdkDefinition>, HashError> {
138        if self.schema != SDK_DEFINITION_SCHEMA_V1 {
139            return Err(HashError::UnknownVersion(self.schema.clone()));
140        }
141        if self.input_kind != SDK_DEFINITION_PROGRAM_SPEC_INPUT_KIND {
142            return Err(HashError::InvalidProjection {
143                projection: "SDK definition",
144                reason: format!(
145                    "inputKind must be '{}', not '{}'",
146                    SDK_DEFINITION_PROGRAM_SPEC_INPUT_KIND, self.input_kind
147                ),
148            });
149        }
150        hash_jcs(self)
151    }
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize)]
155#[serde(rename_all = "camelCase")]
156pub struct HostedManagedProgramReleaseV1 {
157    pub schema: String,
158    pub release_profile: String,
159    pub program_id: String,
160    pub program_spec_hash: HashId<ProgramSpec>,
161    pub idl_content_hash: HashId<crate::IdlContent>,
162    pub normalized_idl_hash: HashId<crate::IdlNormalized>,
163    pub decoder_abi_version: String,
164    pub decoder_engine_id: String,
165    pub decoder_binding_id: String,
166}
167
168impl HostedManagedProgramReleaseV1 {
169    pub fn new(
170        program_id: impl Into<String>,
171        program_spec_hash: HashId<ProgramSpec>,
172        idl_content_hash: HashId<crate::IdlContent>,
173        normalized_idl_hash: HashId<crate::IdlNormalized>,
174        decoder_abi_version: impl Into<String>,
175        decoder_engine_id: impl Into<String>,
176        decoder_binding_id: impl Into<String>,
177    ) -> Self {
178        Self {
179            schema: PROGRAM_RELEASE_SCHEMA_V1.to_string(),
180            release_profile: HOSTED_MANAGED_RELEASE_PROFILE.to_string(),
181            program_id: program_id.into(),
182            program_spec_hash,
183            idl_content_hash,
184            normalized_idl_hash,
185            decoder_abi_version: decoder_abi_version.into(),
186            decoder_engine_id: decoder_engine_id.into(),
187            decoder_binding_id: decoder_binding_id.into(),
188        }
189    }
190
191    pub fn hash(&self) -> Result<HashId<ProgramRelease>, HashError> {
192        validate_release_projection(
193            &self.schema,
194            &self.release_profile,
195            HOSTED_MANAGED_RELEASE_PROFILE,
196            &self.program_id,
197            &self.decoder_engine_id,
198            Some(&self.decoder_abi_version),
199            Some(&self.decoder_binding_id),
200        )?;
201        hash_jcs(self)
202    }
203}
204
205#[derive(Debug, Clone, Serialize, Deserialize)]
206#[serde(rename_all = "camelCase")]
207pub struct OssGeneratedProgramReleaseV1 {
208    pub schema: String,
209    pub release_profile: String,
210    pub program_id: String,
211    pub program_spec_hash: HashId<ProgramSpec>,
212    pub idl_content_hash: HashId<crate::IdlContent>,
213    pub normalized_idl_hash: HashId<crate::IdlNormalized>,
214    pub decoder_engine_id: String,
215}
216
217impl OssGeneratedProgramReleaseV1 {
218    pub fn new(
219        program_id: impl Into<String>,
220        program_spec_hash: HashId<ProgramSpec>,
221        idl_content_hash: HashId<crate::IdlContent>,
222        normalized_idl_hash: HashId<crate::IdlNormalized>,
223    ) -> Self {
224        Self::with_decoder_engine(
225            program_id,
226            program_spec_hash,
227            idl_content_hash,
228            normalized_idl_hash,
229            OSS_DECODER_ENGINE_ID,
230        )
231    }
232
233    pub fn with_decoder_engine(
234        program_id: impl Into<String>,
235        program_spec_hash: HashId<ProgramSpec>,
236        idl_content_hash: HashId<crate::IdlContent>,
237        normalized_idl_hash: HashId<crate::IdlNormalized>,
238        decoder_engine_id: impl Into<String>,
239    ) -> Self {
240        Self {
241            schema: PROGRAM_RELEASE_SCHEMA_V1.to_string(),
242            release_profile: OSS_GENERATED_RELEASE_PROFILE.to_string(),
243            program_id: program_id.into(),
244            program_spec_hash,
245            idl_content_hash,
246            normalized_idl_hash,
247            decoder_engine_id: decoder_engine_id.into(),
248        }
249    }
250
251    pub fn hash(&self) -> Result<HashId<ProgramRelease>, HashError> {
252        validate_release_projection(
253            &self.schema,
254            &self.release_profile,
255            OSS_GENERATED_RELEASE_PROFILE,
256            &self.program_id,
257            &self.decoder_engine_id,
258            None,
259            None,
260        )?;
261        hash_jcs(self)
262    }
263}
264
265fn validate_release_projection(
266    schema: &str,
267    release_profile: &str,
268    expected_profile: &'static str,
269    program_id: &str,
270    decoder_engine_id: &str,
271    decoder_abi_version: Option<&str>,
272    decoder_binding_id: Option<&str>,
273) -> Result<(), HashError> {
274    if schema != PROGRAM_RELEASE_SCHEMA_V1 {
275        return Err(HashError::UnknownVersion(schema.to_string()));
276    }
277    if release_profile != expected_profile {
278        return Err(HashError::InvalidProjection {
279            projection: "program release",
280            reason: format!("releaseProfile must be '{expected_profile}', not '{release_profile}'"),
281        });
282    }
283    if program_id.is_empty() {
284        return Err(HashError::InvalidProjection {
285            projection: "program release",
286            reason: "programId must not be empty".to_string(),
287        });
288    }
289    if decoder_engine_id.is_empty() {
290        return Err(HashError::InvalidProjection {
291            projection: "program release",
292            reason: "decoderEngineId must not be empty".to_string(),
293        });
294    }
295    if decoder_abi_version.is_some_and(str::is_empty) {
296        return Err(HashError::InvalidProjection {
297            projection: "program release",
298            reason: "decoderAbiVersion must not be empty".to_string(),
299        });
300    }
301    if decoder_binding_id.is_some_and(str::is_empty) {
302        return Err(HashError::InvalidProjection {
303            projection: "program release",
304            reason: "decoderBindingId must not be empty".to_string(),
305        });
306    }
307    Ok(())
308}