1use std::collections::HashMap;
2
3use anyhow::Context;
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6
7use super::{entity::EntityDescriptorConst, AnyEntity, Entity, WorkloadV2};
8
9pub const APP_ID_PREFIX: &str = "da_";
10
11#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
13pub struct AppMeta {
14 pub app_id: String,
15 pub app_version_id: String,
16}
17
18impl AppMeta {
19 pub const ANNOTATION_BACKEND_APP_ID: &'static str = "wasmer.io/app_id";
21
22 pub const ANNOTATION_BACKEND_APP_VERSION_ID: &'static str = "wasmer.io/app_version_id";
24
25 pub fn try_from_annotations(
27 map: &HashMap<String, serde_json::Value>,
28 ) -> Result<Self, anyhow::Error> {
29 let app_id = map
30 .get(Self::ANNOTATION_BACKEND_APP_ID)
31 .context("missing annotation for app id")?
32 .as_str()
33 .context("app id annotation is not a string")?
34 .to_string();
35
36 let app_version_id = map
37 .get(Self::ANNOTATION_BACKEND_APP_VERSION_ID)
38 .context("missing annotation for version id")?
39 .as_str()
40 .context("version id annotation is not a string")?
41 .to_string();
42
43 Ok(Self {
44 app_id,
45 app_version_id,
46 })
47 }
48
49 pub fn try_from_entity<T>(entity: &Entity<T>) -> Result<Self, anyhow::Error> {
50 Self::try_from_annotations(&entity.meta.annotations)
51 }
52
53 pub fn new(app_id: String, app_version_id: String) -> Self {
54 Self {
55 app_id,
56 app_version_id,
57 }
58 }
59
60 pub fn to_annotations_map(self) -> HashMap<String, serde_json::Value> {
61 let mut map = HashMap::new();
62 map.insert(
63 Self::ANNOTATION_BACKEND_APP_ID.to_string(),
64 serde_json::Value::String(self.app_id),
65 );
66 map.insert(
67 Self::ANNOTATION_BACKEND_APP_VERSION_ID.to_string(),
68 serde_json::Value::String(self.app_version_id),
69 );
70 map
71 }
72}
73
74#[derive(Serialize, Deserialize, JsonSchema, PartialEq, Eq, Clone, Debug)]
78pub struct AppV1Spec {
79 #[serde(default)]
82 #[serde(skip_serializing_if = "Vec::is_empty")]
83 pub aliases: Vec<String>,
84
85 #[serde(default)]
87 #[serde(skip_serializing_if = "Vec::is_empty")]
88 pub domains: Vec<String>,
89
90 pub workload: WorkloadV2,
92}
93
94#[derive(Serialize, Deserialize, JsonSchema, PartialEq, Eq, Clone, Debug)]
95pub struct AppStateV1 {}
96
97impl EntityDescriptorConst for AppV1Spec {
98 const NAMESPACE: &'static str = "wasmer.io";
99 const NAME: &'static str = "App";
100 const VERSION: &'static str = "1-alpha1";
101 const KIND: &'static str = "wasmer.io/App.v1";
102
103 type Spec = AppV1Spec;
104 type State = AppStateV1;
105}
106
107pub type AppV1 = super::Entity<AppV1Spec, AnyEntity>;
108
109#[cfg(test)]
110mod tests {
111 use pretty_assertions::assert_eq;
112
113 use crate::schema::{EntityMeta, EnvVarV1};
114
115 use super::*;
116
117 #[test]
119 fn test_deser_app_v1_sparse() {
120 let inp = r#"
121kind: wasmer.io/App.v1
122meta:
123 name: my-app
124spec:
125 workload:
126 source: theduke/amaze
127 domains:
128 - a.com
129 - b.com
130"#;
131
132 let a1 = serde_yaml::from_str::<AppV1>(inp).unwrap();
133
134 assert_eq!(
135 a1,
136 AppV1 {
137 meta: EntityMeta::new("my-app"),
138 spec: AppV1Spec {
139 aliases: Vec::new(),
140 domains: vec!["a.com".to_string(), "b.com".to_string()],
141 workload: crate::schema::WorkloadV2 {
142 source: "theduke/amaze".parse().unwrap(),
143 capabilities: Default::default(),
144 },
145 },
146 children: None,
147 },
148 );
149 }
150
151 #[test]
152 fn test_deser_app_v1_full() {
153 let inp = r#"
154kind: wasmer.io/App.v1
155meta:
156 name: my-app
157 description: hello
158 labels:
159 "my/label": "value"
160 annotations:
161 "my/annotation": {nested: [1, 2, 3]}
162spec:
163 aliases:
164 - a
165 - b
166 workload:
167 source: "theduke/my-app"
168"#;
169
170 let a1 = serde_yaml::from_str::<AppV1>(inp).unwrap();
171
172 let expected = AppV1 {
173 meta: EntityMeta {
174 uid: None,
175 name: "my-app".to_string(),
176 description: Some("hello".to_string()),
177 labels: vec![("my/label".to_string(), "value".to_string())]
178 .into_iter()
179 .collect(),
180 annotations: vec![(
181 "my/annotation".to_string(),
182 serde_json::json!({
183 "nested": [1, 2, 3],
184 }),
185 )]
186 .into_iter()
187 .collect(),
188 parent: None,
189 },
190 spec: AppV1Spec {
191 aliases: vec!["a".to_string(), "b".to_string()],
192 domains: Vec::new(),
193 workload: WorkloadV2 {
194 source: "theduke/my-app".parse().unwrap(),
195 capabilities: Default::default(),
196 },
197 },
198 children: None,
199 };
200
201 assert_eq!(a1, expected,);
202 }
203
204 #[test]
205 fn test_deser_app_v1_with_cli_and_env_cap() {
206 let raw = r#"
207kind: wasmer.io/App.v1
208meta:
209 description: ''
210 name: christoph/pyenv-dump
211spec:
212 workload:
213 capabilities:
214 wasi:
215 cli_args:
216 - /src/main.py
217 env_vars:
218 - name: PORT
219 value: '80'
220 source: wasmer-tests/python-env-dump@0.3.6
221
222"#;
223
224 let a1 = serde_yaml::from_str::<AppV1>(raw).unwrap();
225
226 let expected = AppV1 {
227 meta: EntityMeta {
228 uid: None,
229 name: "christoph/pyenv-dump".to_string(),
230 description: Some("".to_string()),
231 labels: Default::default(),
232 annotations: Default::default(),
233 parent: None,
234 },
235 spec: AppV1Spec {
236 aliases: Vec::new(),
237 domains: Vec::new(),
238 workload: WorkloadV2 {
239 source: "wasmer-tests/python-env-dump@0.3.6".parse().unwrap(),
240 capabilities: crate::schema::CapabilityMapV1 {
241 wasi: Some(crate::schema::CapabilityWasiV1 {
242 cli_args: Some(vec!["/src/main.py".to_string()]),
243 env_vars: Some(vec![EnvVarV1 {
244 name: "PORT".to_string(),
245 source: crate::schema::EnvVarSourceV1::Value("80".to_string()),
246 }]),
247 }),
248 ..Default::default()
249 },
250 },
251 },
252 children: None,
253 };
254
255 assert_eq!(a1, expected,);
256 }
257}