use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Discovery {
pub services: Vec<Service>,
#[serde(skip_serializing_if = "Option::is_none")]
pub monorepo: Option<Monorepo>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Service {
pub name: String,
pub dir: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub language: Option<Language>,
#[serde(skip_serializing_if = "Option::is_none")]
pub runtime: Option<RuntimeInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub framework: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub package_manager: Option<PackageManagerInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub language_config: Option<LanguageConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub network: Option<Network>,
#[serde(skip_serializing_if = "Option::is_none")]
pub exec_mode: Option<ExecMode>,
pub commands: Commands,
#[serde(skip_serializing_if = "Option::is_none")]
pub image: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dockerfile: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output_dir: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub env: Vec<EnvVar>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub system_deps: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub volumes: Vec<Volume>,
#[serde(skip_serializing_if = "Option::is_none")]
pub resources: Option<Resources>,
#[serde(skip_serializing_if = "Option::is_none")]
pub replicas: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub restart: Option<Restart>,
#[serde(skip_serializing_if = "Option::is_none")]
pub healthcheck: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub schedule: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub detected_by: Vec<String>,
}
impl Service {
pub fn layer_context(&mut self, ctx: &DirContext) {
if self.language.is_none() {
self.language = ctx.language;
}
if self.runtime.is_none() {
self.runtime.clone_from(&ctx.runtime);
}
if self.framework.is_none() {
self.framework.clone_from(&ctx.framework);
}
if self.package_manager.is_none() {
self.package_manager.clone_from(&ctx.package_manager);
}
if self.language_config.is_none() {
self.language_config.clone_from(&ctx.language_config);
}
if self.output_dir.is_none() {
self.output_dir.clone_from(&ctx.output_dir);
}
self.commands.fill_from(&ctx.commands);
merge_env_vars(&mut self.env, &ctx.env);
merge_string_vecs(&mut self.system_deps, &ctx.system_deps);
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RuntimeInfo {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PackageManagerInfo {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum LanguageConfig {
Node(NodeConfig),
Python(PythonConfig),
Go(GoConfig),
Rust(RustConfig),
Ruby(RubyConfig),
Php(PhpConfig),
Java(JavaConfig),
Elixir(ElixirConfig),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NodeConfig {
pub corepack: bool,
pub has_puppeteer: bool,
pub has_prisma: bool,
pub has_sharp: bool,
pub is_spa: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PythonConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub wsgi_app: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub asgi_app: Option<String>,
pub has_manage_py: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub main_file: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GoConfig {
pub cgo: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub binary_target: Option<String>,
pub workspace: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RustConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub edition: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub binary_name: Option<String>,
pub workspace: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RubyConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub asset_pipeline: Option<String>,
pub needs_node: bool,
pub has_bootsnap: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ElixirConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub erlang_version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub erlang_version_source: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub app_name: Option<String>,
pub has_assets_deploy: bool,
pub has_ecto: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PhpConfig {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub extensions: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JavaConfig {
pub build_tool_wrapper: bool,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Commands {
#[serde(skip_serializing_if = "Option::is_none")]
pub install: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub build: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub start: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dev: Option<String>,
}
impl Commands {
pub fn is_empty(&self) -> bool {
self.install.is_none() && self.build.is_none() && self.start.is_none() && self.dev.is_none()
}
pub fn fill_from(&mut self, other: &Commands) {
if self.install.is_none() {
self.install.clone_from(&other.install);
}
if self.build.is_none() {
self.build.clone_from(&other.build);
}
if self.start.is_none() {
self.start.clone_from(&other.start);
}
if self.dev.is_none() {
self.dev.clone_from(&other.dev);
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EnvVar {
pub key: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub detected_by: Vec<String>,
}
pub fn merge_env_vars(base: &mut Vec<EnvVar>, other: &[EnvVar]) {
for var in other {
if let Some(existing) = base.iter_mut().find(|e| e.key == var.key) {
for d in &var.detected_by {
if !existing.detected_by.contains(d) {
existing.detected_by.push(d.clone());
}
}
} else {
base.push(var.clone());
}
}
}
pub fn merge_string_vecs(base: &mut Vec<String>, other: &[String]) {
for item in other {
if !base.contains(item) {
base.push(item.clone());
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Language {
#[serde(rename = "javascript")]
JavaScript,
#[serde(rename = "typescript")]
TypeScript,
Python,
Go,
Rust,
Ruby,
Php,
Java,
Elixir,
Html,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Network {
Private,
Public,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ExecMode {
Daemon,
Scheduled,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Restart {
Never,
Always,
OnFailure,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Monorepo {
#[serde(rename = "type")]
pub monorepo_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool: Option<String>,
pub packages: HashMap<String, MonorepoPackage>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MonorepoPackage {
pub name: String,
pub dir: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub watch_patterns: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Volume {
pub mount: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub size_mb: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Resources {
#[serde(skip_serializing_if = "Option::is_none")]
pub cpus: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub memory_mb: Option<u32>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct DirContext {
pub dir: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub language: Option<Language>,
#[serde(skip_serializing_if = "Option::is_none")]
pub runtime: Option<RuntimeInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub framework: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub package_manager: Option<PackageManagerInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub language_config: Option<LanguageConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output_dir: Option<String>,
pub commands: Commands,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub env: Vec<EnvVar>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub system_deps: Vec<String>,
}
impl DirContext {
pub fn merge(&mut self, other: &DirContext) {
if self.language.is_none() {
self.language = other.language;
}
if self.runtime.is_none() {
self.runtime.clone_from(&other.runtime);
}
if self.framework.is_none() {
self.framework.clone_from(&other.framework);
}
if self.package_manager.is_none() {
self.package_manager.clone_from(&other.package_manager);
}
if self.language_config.is_none() {
self.language_config.clone_from(&other.language_config);
}
if self.output_dir.is_none() {
self.output_dir.clone_from(&other.output_dir);
}
self.commands.fill_from(&other.commands);
merge_env_vars(&mut self.env, &other.env);
merge_string_vecs(&mut self.system_deps, &other.system_deps);
}
}