1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5pub struct Discovery {
6 pub services: Vec<Service>,
7 #[serde(skip_serializing_if = "Option::is_none")]
8 pub monorepo: Option<Monorepo>,
9}
10
11#[derive(Debug, Clone, Default, Serialize, Deserialize)]
12pub struct Service {
13 pub name: String,
14 pub dir: String,
15
16 #[serde(skip_serializing_if = "Option::is_none")]
17 pub language: Option<Language>,
18 #[serde(skip_serializing_if = "Option::is_none")]
19 pub runtime: Option<RuntimeInfo>,
20 #[serde(skip_serializing_if = "Option::is_none")]
21 pub framework: Option<String>,
22 #[serde(skip_serializing_if = "Option::is_none")]
23 pub package_manager: Option<PackageManagerInfo>,
24 #[serde(skip_serializing_if = "Option::is_none")]
25 pub language_config: Option<LanguageConfig>,
26
27 #[serde(skip_serializing_if = "Option::is_none")]
28 pub network: Option<Network>,
29 #[serde(skip_serializing_if = "Option::is_none")]
30 pub exec_mode: Option<ExecMode>,
31 pub commands: Commands,
32 #[serde(skip_serializing_if = "Option::is_none")]
33 pub image: Option<String>,
34 #[serde(skip_serializing_if = "Option::is_none")]
35 pub dockerfile: Option<String>,
36
37 #[serde(skip_serializing_if = "Option::is_none")]
38 pub output_dir: Option<String>,
39
40 #[serde(default, skip_serializing_if = "Vec::is_empty")]
41 pub env: Vec<EnvVar>,
42 #[serde(default, skip_serializing_if = "Vec::is_empty")]
43 pub system_deps: Vec<String>,
44 #[serde(default, skip_serializing_if = "Vec::is_empty")]
45 pub volumes: Vec<Volume>,
46 #[serde(skip_serializing_if = "Option::is_none")]
47 pub resources: Option<Resources>,
48
49 #[serde(skip_serializing_if = "Option::is_none")]
50 pub replicas: Option<u32>,
51 #[serde(skip_serializing_if = "Option::is_none")]
52 pub restart: Option<Restart>,
53 #[serde(skip_serializing_if = "Option::is_none")]
54 pub healthcheck: Option<String>,
55 #[serde(skip_serializing_if = "Option::is_none")]
56 pub schedule: Option<String>,
57
58 #[serde(default, skip_serializing_if = "Vec::is_empty")]
59 pub detected_by: Vec<String>,
60}
61
62impl Service {
63 pub fn layer_context(&mut self, ctx: &DirContext) {
66 if self.language.is_none() {
67 self.language = ctx.language;
68 }
69 if self.runtime.is_none() {
70 self.runtime.clone_from(&ctx.runtime);
71 }
72 if self.framework.is_none() {
73 self.framework.clone_from(&ctx.framework);
74 }
75 if self.package_manager.is_none() {
76 self.package_manager.clone_from(&ctx.package_manager);
77 }
78 if self.language_config.is_none() {
79 self.language_config.clone_from(&ctx.language_config);
80 }
81 if self.output_dir.is_none() {
82 self.output_dir.clone_from(&ctx.output_dir);
83 }
84 self.commands.fill_from(&ctx.commands);
85 merge_env_vars(&mut self.env, &ctx.env);
86 merge_string_vecs(&mut self.system_deps, &ctx.system_deps);
87 }
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct RuntimeInfo {
95 pub name: String,
96 #[serde(skip_serializing_if = "Option::is_none")]
97 pub version: Option<String>,
98 #[serde(skip_serializing_if = "Option::is_none")]
99 pub source: Option<String>,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct PackageManagerInfo {
105 pub name: String,
106 #[serde(skip_serializing_if = "Option::is_none")]
107 pub version: Option<String>,
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
113#[serde(tag = "type", rename_all = "snake_case")]
114pub enum LanguageConfig {
115 Node(NodeConfig),
116 Python(PythonConfig),
117 Go(GoConfig),
118 Rust(RustConfig),
119 Ruby(RubyConfig),
120 Php(PhpConfig),
121 Java(JavaConfig),
122 Elixir(ElixirConfig),
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct NodeConfig {
127 pub corepack: bool,
128 pub has_puppeteer: bool,
129 pub has_prisma: bool,
130 pub has_sharp: bool,
131 pub is_spa: bool,
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct PythonConfig {
137 #[serde(skip_serializing_if = "Option::is_none")]
138 pub wsgi_app: Option<String>,
139 #[serde(skip_serializing_if = "Option::is_none")]
140 pub asgi_app: Option<String>,
141 pub has_manage_py: bool,
142 #[serde(skip_serializing_if = "Option::is_none")]
144 pub main_file: Option<String>,
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct GoConfig {
149 pub cgo: bool,
150 #[serde(skip_serializing_if = "Option::is_none")]
151 pub binary_target: Option<String>,
152 pub workspace: bool,
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct RustConfig {
157 #[serde(skip_serializing_if = "Option::is_none")]
158 pub edition: Option<String>,
159 #[serde(skip_serializing_if = "Option::is_none")]
160 pub binary_name: Option<String>,
161 pub workspace: bool,
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct RubyConfig {
167 #[serde(skip_serializing_if = "Option::is_none")]
168 pub asset_pipeline: Option<String>,
169 pub needs_node: bool,
170 pub has_bootsnap: bool,
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct ElixirConfig {
176 #[serde(skip_serializing_if = "Option::is_none")]
177 pub erlang_version: Option<String>,
178 #[serde(skip_serializing_if = "Option::is_none")]
179 pub erlang_version_source: Option<String>,
180 #[serde(skip_serializing_if = "Option::is_none")]
182 pub app_name: Option<String>,
183 pub has_assets_deploy: bool,
185 pub has_ecto: bool,
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct PhpConfig {
191 #[serde(default, skip_serializing_if = "Vec::is_empty")]
192 pub extensions: Vec<String>,
193}
194
195#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct JavaConfig {
197 pub build_tool_wrapper: bool,
198}
199
200#[derive(Debug, Clone, Default, Serialize, Deserialize)]
203pub struct Commands {
204 #[serde(skip_serializing_if = "Option::is_none")]
205 pub install: Option<String>,
206 #[serde(skip_serializing_if = "Option::is_none")]
207 pub build: Option<String>,
208 #[serde(skip_serializing_if = "Option::is_none")]
209 pub start: Option<String>,
210 #[serde(skip_serializing_if = "Option::is_none")]
211 pub dev: Option<String>,
212}
213
214impl Commands {
215 pub fn is_empty(&self) -> bool {
216 self.install.is_none() && self.build.is_none() && self.start.is_none() && self.dev.is_none()
217 }
218
219 pub fn fill_from(&mut self, other: &Commands) {
221 if self.install.is_none() {
222 self.install.clone_from(&other.install);
223 }
224 if self.build.is_none() {
225 self.build.clone_from(&other.build);
226 }
227 if self.start.is_none() {
228 self.start.clone_from(&other.start);
229 }
230 if self.dev.is_none() {
231 self.dev.clone_from(&other.dev);
232 }
233 }
234}
235
236#[derive(Debug, Clone, Serialize, Deserialize)]
239pub struct EnvVar {
240 pub key: String,
241 #[serde(skip_serializing_if = "Option::is_none")]
242 pub default: Option<String>,
243 #[serde(default, skip_serializing_if = "Vec::is_empty")]
244 pub detected_by: Vec<String>,
245}
246
247pub fn merge_env_vars(base: &mut Vec<EnvVar>, other: &[EnvVar]) {
251 for var in other {
252 if let Some(existing) = base.iter_mut().find(|e| e.key == var.key) {
253 for d in &var.detected_by {
255 if !existing.detected_by.contains(d) {
256 existing.detected_by.push(d.clone());
257 }
258 }
259 } else {
260 base.push(var.clone());
261 }
262 }
263}
264
265pub fn merge_string_vecs(base: &mut Vec<String>, other: &[String]) {
267 for item in other {
268 if !base.contains(item) {
269 base.push(item.clone());
270 }
271 }
272}
273
274#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
277#[serde(rename_all = "snake_case")]
278pub enum Language {
279 #[serde(rename = "javascript")]
280 JavaScript,
281 #[serde(rename = "typescript")]
282 TypeScript,
283 Python,
284 Go,
285 Rust,
286 Ruby,
287 Php,
288 Java,
289 Elixir,
290 Html,
291}
292
293#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
294#[serde(rename_all = "snake_case")]
295pub enum Network {
296 Private,
297 Public,
298}
299
300#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
301#[serde(rename_all = "snake_case")]
302pub enum ExecMode {
303 Daemon,
304 Scheduled,
305}
306
307#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
308#[serde(rename_all = "snake_case")]
309pub enum Restart {
310 Never,
311 Always,
312 OnFailure,
313}
314
315#[derive(Debug, Clone, Serialize, Deserialize)]
318pub struct Monorepo {
319 #[serde(rename = "type")]
320 pub monorepo_type: String,
321 #[serde(skip_serializing_if = "Option::is_none")]
322 pub tool: Option<String>,
323 pub packages: HashMap<String, MonorepoPackage>,
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize)]
327pub struct MonorepoPackage {
328 pub name: String,
329 pub dir: String,
330 #[serde(default, skip_serializing_if = "Vec::is_empty")]
331 pub watch_patterns: Vec<String>,
332}
333
334#[derive(Debug, Clone, Serialize, Deserialize)]
335pub struct Volume {
336 pub mount: String,
337 #[serde(skip_serializing_if = "Option::is_none")]
338 pub size_mb: Option<u32>,
339}
340
341#[derive(Debug, Clone, Serialize, Deserialize)]
342pub struct Resources {
343 #[serde(skip_serializing_if = "Option::is_none")]
344 pub cpus: Option<u32>,
345 #[serde(skip_serializing_if = "Option::is_none")]
346 pub memory_mb: Option<u32>,
347}
348
349#[derive(Debug, Clone, Default, Serialize, Deserialize)]
355pub struct DirContext {
356 pub dir: String,
357 #[serde(skip_serializing_if = "Option::is_none")]
358 pub language: Option<Language>,
359 #[serde(skip_serializing_if = "Option::is_none")]
360 pub runtime: Option<RuntimeInfo>,
361 #[serde(skip_serializing_if = "Option::is_none")]
362 pub framework: Option<String>,
363 #[serde(skip_serializing_if = "Option::is_none")]
364 pub package_manager: Option<PackageManagerInfo>,
365 #[serde(skip_serializing_if = "Option::is_none")]
366 pub language_config: Option<LanguageConfig>,
367 #[serde(skip_serializing_if = "Option::is_none")]
368 pub output_dir: Option<String>,
369 pub commands: Commands,
370 #[serde(default, skip_serializing_if = "Vec::is_empty")]
371 pub env: Vec<EnvVar>,
372 #[serde(default, skip_serializing_if = "Vec::is_empty")]
373 pub system_deps: Vec<String>,
374}
375
376impl DirContext {
377 pub fn merge(&mut self, other: &DirContext) {
380 if self.language.is_none() {
381 self.language = other.language;
382 }
383 if self.runtime.is_none() {
384 self.runtime.clone_from(&other.runtime);
385 }
386 if self.framework.is_none() {
387 self.framework.clone_from(&other.framework);
388 }
389 if self.package_manager.is_none() {
390 self.package_manager.clone_from(&other.package_manager);
391 }
392 if self.language_config.is_none() {
393 self.language_config.clone_from(&other.language_config);
394 }
395 if self.output_dir.is_none() {
396 self.output_dir.clone_from(&other.output_dir);
397 }
398 self.commands.fill_from(&other.commands);
399 merge_env_vars(&mut self.env, &other.env);
400 merge_string_vecs(&mut self.system_deps, &other.system_deps);
401 }
402}