use serde::Serialize;
use serde_json::{Value, json};
use crate::catalog::Catalog;
use crate::error::ProjectsError;
use crate::project::{self, AddedResource};
use crate::stripe::{CommandRunner, StripeProjects};
pub trait CatalogService: Serialize {
const REFERENCE: &'static str;
}
pub async fn add_catalog_resource<C, R>(
stripe: &StripeProjects<R>,
catalog: &Catalog,
config: &C,
resource_name: &str,
) -> Result<AddedResource, ProjectsError>
where
C: CatalogService,
R: CommandRunner,
{
add_catalog_resource_with_paid(stripe, catalog, config, resource_name, false).await
}
pub async fn add_catalog_resource_with_paid<C, R>(
stripe: &StripeProjects<R>,
catalog: &Catalog,
config: &C,
resource_name: &str,
confirm_paid: bool,
) -> Result<AddedResource, ProjectsError>
where
C: CatalogService,
R: CommandRunner,
{
let value = serde_json::to_value(config).map_err(|err| ProjectsError::ProvisionFailed {
resource: resource_name.to_owned(),
detail: format!("config for {} did not serialize: {err}", C::REFERENCE),
})?;
let service = catalog
.lookup(C::REFERENCE)
.ok_or(ProjectsError::CatalogMissing {
reference: C::REFERENCE,
})?;
service
.validate_config(&value)
.map_err(|violations| ProjectsError::ConfigSchema {
reference: C::REFERENCE,
violations,
})?;
let paid = service.requires_confirmation_with_paid(&value, confirm_paid);
ensure_parent_plans(stripe, catalog, service, &value, confirm_paid).await?;
project::add_resource(stripe, C::REFERENCE, resource_name, &value, paid).await
}
async fn ensure_parent_plans<R: CommandRunner>(
stripe: &StripeProjects<R>,
catalog: &Catalog,
service: &crate::catalog::ServiceDetail,
config: &Value,
prefer_paid: bool,
) -> Result<(), ProjectsError> {
for plan_id in service.required_parent_services(config, prefer_paid) {
let reference = format!("{}/{}", service.provider_name.to_ascii_lowercase(), plan_id);
let plan = catalog
.lookup(&reference)
.ok_or_else(|| ProjectsError::ProvisionFailed {
resource: plan_id.clone(),
detail: format!("parent plan {reference} not found in catalog"),
})?;
let needs_paid = plan.requires_confirmation_with_paid(&json!({}), prefer_paid);
let paid = needs_paid && prefer_paid;
project::add_resource(stripe, &reference, &plan_id, &json!({}), paid).await?;
}
Ok(())
}
pub fn requires_confirmation<C>(catalog: &Catalog, config: &C) -> Option<bool>
where
C: CatalogService,
{
let value = serde_json::to_value(config).ok()?;
catalog
.lookup(C::REFERENCE)
.map(|service| service.requires_confirmation(&value))
}
pub fn verify_service<C>(catalog: &Catalog, sample: &C) -> Vec<String>
where
C: CatalogService,
{
let mut out = Vec::new();
let Some(service) = catalog.lookup(C::REFERENCE) else {
out.push(format!("{}: reference not found in catalog", C::REFERENCE));
return out;
};
match serde_json::to_value(sample) {
Ok(value) => {
if let Err(violations) = service.validate_config(&value) {
out.extend(
violations
.into_iter()
.map(|v| format!("{}: {v}", C::REFERENCE)),
);
}
}
Err(err) => out.push(format!(
"{}: sample config did not serialize: {err}",
C::REFERENCE
)),
}
out
}