1use crate::item_hash::ItemHash;
2use crate::message::execution::base::{Encoding, ExecutableContent, Interface};
3use crate::message::execution::environment::{FunctionEnvironment, FunctionTriggers};
4use crate::toolkit::serde::{default_some_false, default_true};
5use serde::{Deserialize, Serialize};
6use std::path::PathBuf;
7
8#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
9pub struct FunctionRuntime {
10 #[serde(rename = "ref")]
11 pub reference: ItemHash,
12 #[serde(default = "default_true")]
13 pub use_latest: bool,
14 pub comment: String,
15}
16
17#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
18pub struct CodeContent {
19 pub encoding: Encoding,
20 pub entrypoint: String,
21 #[serde(rename = "ref")]
23 pub reference: ItemHash,
24 #[serde(default, skip_serializing_if = "Option::is_none")]
25 pub interface: Option<Interface>,
26 #[serde(default, skip_serializing_if = "Option::is_none")]
27 pub args: Option<Vec<String>>,
28 #[serde(default)]
29 pub use_latest: bool,
30}
31
32#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
33pub struct DataContent {
34 pub encoding: Encoding,
35 pub mount: PathBuf,
36 #[serde(rename = "ref")]
37 pub reference: ItemHash,
38 #[serde(
39 default = "default_some_false",
40 skip_serializing_if = "Option::is_none"
41 )]
42 pub use_latest: Option<bool>,
43}
44
45#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
46pub struct Export {
47 pub encoding: Encoding,
48 pub mount: PathBuf,
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
57pub enum ProgramType {
58 #[default]
59 #[serde(rename = "vm-function")]
60 VmFunction,
61}
62
63#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
64pub struct ProgramContent {
65 #[serde(default, rename = "type")]
66 pub program_type: ProgramType,
67 #[serde(flatten)]
68 pub base: ExecutableContent,
69 pub code: CodeContent,
71 pub runtime: FunctionRuntime,
73 #[serde(default, skip_serializing_if = "Option::is_none")]
75 pub data: Option<DataContent>,
76 pub environment: FunctionEnvironment,
78 #[serde(default, skip_serializing_if = "Option::is_none")]
80 pub export: Option<Export>,
81 pub on: FunctionTriggers,
83}
84
85impl ProgramContent {
86 pub fn executable_content(&self) -> &ExecutableContent {
87 &self.base
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94 use crate::chain::{Address, Chain, Signature};
95 use crate::message::base_message::MessageContentEnum;
96 use crate::message::execution::environment::MachineResources;
97 use crate::message::execution::volume::{BaseVolume, ImmutableVolume, MachineVolume};
98 use crate::message::{ContentSource, Message, MessageType};
99 use crate::timestamp::Timestamp;
100 use crate::{channel, item_hash};
101 use assert_matches::assert_matches;
102 use memsizes::MiB;
103 use std::collections::HashMap;
104
105 const PROGRAM_FIXTURE: &str = include_str!(concat!(
106 env!("CARGO_MANIFEST_DIR"),
107 "/../../fixtures/messages/program/program.json"
108 ));
109
110 const PROGRAM_WITH_EMPTY_ARRAY_AS_METADATA: &str = include_str!(concat!(
111 env!("CARGO_MANIFEST_DIR"),
112 "/../../fixtures/messages/program/program-with-array-as-metadata.json"
113 ));
114
115 #[test]
116 fn test_deserialize_program_message() {
117 let message: Message = serde_json::from_str(PROGRAM_FIXTURE).unwrap();
118
119 assert_eq!(
120 message.sender,
121 Address::from("0x9C2FD74F9CA2B7C4941690316B0Ebc35ce55c885".to_string())
122 );
123 assert_eq!(message.chain, Chain::Ethereum);
124 assert_eq!(
125 message.signature,
126 Some(Signature::from(
127 "0x421c656709851fba752f323a117bc7a07f175a4dd7faf1d8fc1cd9a99028081a6419f9e8b0a7cd454bfef1c52d1f0675a7a59a7d07eb4ebdb22e18bbaf415f881c".to_string()
128 ))
129 );
130 assert_matches!(message.message_type, MessageType::Program);
131 assert_matches!(
132 message.content_source,
133 ContentSource::Inline { item_content: _ }
134 );
135 assert_eq!(
136 &message.item_hash.to_string(),
137 "acab01087137c68a5e84734e75145482651accf3bea80fb9b723b761639ecc1c"
138 );
139 assert_eq!(message.time, Timestamp::from(1757026128.773));
140 assert_eq!(message.channel, Some(channel!("ALEPH-CLOUDSOLUTIONS")));
141
142 assert_eq!(
144 &message.content.address,
145 &Address::from("0x9C2FD74F9CA2B7C4941690316B0Ebc35ce55c885".to_string())
146 );
147 assert_eq!(&message.content.time, &Timestamp::from(1757026128.773));
148 assert_eq!(message.sent_at(), &message.content.time);
149
150 let program_content = match message.content() {
152 MessageContentEnum::Program(content) => content,
153 other => {
154 panic!("Expected MessageContentEnum::Program, got {:?}", other);
155 }
156 };
157
158 assert!(!program_content.base.allow_amend);
159 assert_eq!(
160 program_content.base.metadata,
161 Some(HashMap::from([(
162 "name".to_string(),
163 serde_json::Value::String("Hoymiles".to_string())
164 )]))
165 );
166 assert_eq!(program_content.base.variables, Some(HashMap::new()));
167 assert_eq!(
168 program_content.base.resources,
169 MachineResources {
170 vcpus: 2,
171 memory: MiB::from(4096),
172 seconds: 30,
173 published_ports: None,
174 }
175 );
176 assert_matches!(program_content.base.authorized_keys, None);
177 assert_eq!(
178 program_content.environment,
179 FunctionEnvironment {
180 reproducible: false,
181 internet: true,
182 aleph_api: true,
183 shared_cache: false,
184 }
185 );
186 assert_eq!(
187 program_content.base.volumes,
188 vec![MachineVolume::Immutable(ImmutableVolume {
189 base: BaseVolume {
190 comment: None,
191 mount: Some(PathBuf::from("/opt/packages"))
192 },
193 reference: item_hash!(
194 "8df728d560ed6e9103b040a6b5fc5417e0a52e890c12977464ebadf9becf1bf6"
195 ),
196 use_latest: true,
197 })]
198 );
199 assert_eq!(program_content.base.replaces, None);
200 assert_eq!(
201 program_content.code,
202 CodeContent {
203 encoding: Encoding::Zip,
204 entrypoint: "main:app".to_string(),
205 reference: item_hash!(
206 "9a4735bca0d3f7032ddd6659c35387b57b470550c931841e6862ece4e9e6523e"
207 ),
208 interface: None,
209 args: None,
210 use_latest: true,
211 }
212 );
213 assert_eq!(
214 program_content.runtime,
215 FunctionRuntime {
216 reference: item_hash!(
217 "63f07193e6ee9d207b7d1fcf8286f9aee34e6f12f101d2ec77c1229f92964696"
218 ),
219 use_latest: true,
220 comment: "Aleph Alpine Linux with Python 3.12".to_string(),
221 }
222 );
223 assert_eq!(program_content.data, None);
224 assert_eq!(program_content.export, None);
225 assert_eq!(
226 program_content.on,
227 FunctionTriggers {
228 http: true,
229 persistent: Some(false)
230 }
231 );
232
233 assert!(!message.confirmed());
235 assert!(message.confirmed_at().is_none());
236 assert!(message.confirmations.is_empty());
237
238 message.verify_item_hash().unwrap();
239 }
240
241 #[test]
242 fn load_program_with_empty_array_as_metadata() {
246 let message: Message = serde_json::from_str(PROGRAM_WITH_EMPTY_ARRAY_AS_METADATA).unwrap();
247
248 let program_content = match message.content() {
250 MessageContentEnum::Program(content) => content,
251 other => {
252 panic!("Expected MessageContentEnum::Program, got {:?}", other);
253 }
254 };
255
256 assert_matches!(program_content.base.metadata, Some(ref map) if map.is_empty());
257 }
258}