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
17pub 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#[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#[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}