use std::collections::BTreeMap;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::de::{Deserializer, Error as DeError};
use serde::ser::{Serialize, SerializeMap, Serializer};
use super::{
Command, container::ContainerConfig, dockerfile::DockerfileConfig, healthcheck::Healthcheck,
postgres::PostgresConfig, redis::RedisConfig,
};
use crate::interpolate::{InterpolationContext, Interpolator};
#[derive(Debug, Clone, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ResourceKind {
Postgres(PostgresConfig),
Redis(RedisConfig),
Container(ContainerConfig),
Dockerfile(DockerfileConfig),
}
impl ResourceKind {
#[must_use]
pub fn depends_on(&self) -> &[String] {
match self {
Self::Postgres(c) => &c.depends_on,
Self::Redis(c) => &c.depends_on,
Self::Container(c) => &c.depends_on,
Self::Dockerfile(c) => &c.depends_on,
}
}
#[must_use]
pub fn healthcheck(&self) -> Option<&Healthcheck> {
match self {
Self::Postgres(c) => c.healthcheck.as_ref(),
Self::Redis(c) => c.healthcheck.as_ref(),
Self::Container(c) => c.healthcheck.as_ref(),
Self::Dockerfile(c) => c.healthcheck.as_ref(),
}
}
#[must_use]
pub fn kind_name(&self) -> &'static str {
match self {
Self::Postgres(_) => "postgres",
Self::Redis(_) => "redis",
Self::Container(_) => "container",
Self::Dockerfile(_) => "dockerfile",
}
}
#[must_use]
pub fn interpolatable_strings(&self) -> Vec<String> {
let mut out = Vec::new();
match self {
Self::Container(c) => {
out.push(c.image.clone());
out.extend(c.env.values().cloned());
out.extend(c.volumes.iter().cloned());
if let Some(w) = &c.working_dir {
out.push(w.clone());
}
if let Some(cmd) = &c.command {
out.extend(command_strings(cmd));
}
}
Self::Dockerfile(c) => {
out.push(c.context.clone());
out.push(c.dockerfile.clone());
out.extend(c.env.values().cloned());
out.extend(c.volumes.iter().cloned());
out.extend(c.build_args.values().cloned());
if let Some(t) = &c.target {
out.push(t.clone());
}
if let Some(w) = &c.working_dir {
out.push(w.clone());
}
if let Some(cmd) = &c.command {
out.extend(command_strings(cmd));
}
}
Self::Postgres(c) => {
if let Some(s) = &c.password {
out.push(s.clone());
}
if let Some(s) = &c.database {
out.push(s.clone());
}
if let Some(s) = &c.user {
out.push(s.clone());
}
}
Self::Redis(c) => {
if let Some(s) = &c.password {
out.push(s.clone());
}
}
}
if let Some(hc) = self.healthcheck() {
out.extend(hc.test.iter().cloned());
}
out
}
#[must_use]
pub fn implicit_dependencies(&self) -> Vec<String> {
let ctx = InterpolationContext::new();
let interpolator = Interpolator::new(&ctx);
let mut out: Vec<String> = Vec::new();
for value in self.interpolatable_strings() {
let Ok(references) = interpolator.scan(&value) else {
continue;
};
for reference in references {
if let Some(name) = reference.resource_name()
&& !out.contains(&name)
{
out.push(name);
}
}
}
out
}
#[must_use]
pub fn merged_dependencies(&self, own_name: &str) -> Vec<String> {
let mut dependencies = self.depends_on().to_vec();
for implicit in self.implicit_dependencies() {
if implicit != own_name && !dependencies.contains(&implicit) {
dependencies.push(implicit);
}
}
dependencies
}
}
fn command_strings(command: &Command) -> Vec<String> {
match command {
Command::Single(s) => vec![s.clone()],
Command::Args(args) => args.clone(),
}
}
impl Serialize for ResourceKind {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut map = serializer.serialize_map(Some(1))?;
match self {
Self::Postgres(c) => map.serialize_entry("postgres", c)?,
Self::Redis(c) => map.serialize_entry("redis", c)?,
Self::Container(c) => map.serialize_entry("container", c)?,
Self::Dockerfile(c) => map.serialize_entry("dockerfile", c)?,
}
map.end()
}
}
impl<'de> Deserialize<'de> for ResourceKind {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let entries: BTreeMap<String, serde_norway::Value> = BTreeMap::deserialize(deserializer)?;
let mut iter = entries.into_iter();
let (kind, value) = iter
.next()
.ok_or_else(|| DeError::custom("resource entry must contain exactly one kind"))?;
if iter.next().is_some() {
return Err(DeError::custom(
"resource entry must contain exactly one kind",
));
}
match kind.as_str() {
"postgres" => serde_norway::from_value(value)
.map(Self::Postgres)
.map_err(|e| DeError::custom(e.to_string())),
"redis" => serde_norway::from_value(value)
.map(Self::Redis)
.map_err(|e| DeError::custom(e.to_string())),
"container" => serde_norway::from_value(value)
.map(Self::Container)
.map_err(|e| DeError::custom(e.to_string())),
"dockerfile" => serde_norway::from_value(value)
.map(Self::Dockerfile)
.map_err(|e| DeError::custom(e.to_string())),
other => Err(DeError::custom(format!("unknown resource kind `{other}`"))),
}
}
}