stackless_stripe_projects/catalog/
verify.rs1use serde::Serialize;
10use serde_json::{Value, json};
11
12use crate::catalog::Catalog;
13use crate::error::ProjectsError;
14use crate::project::{self, AddedResource};
15use crate::stripe::{CommandRunner, StripeProjects};
16
17pub trait CatalogService: Serialize {
19 const REFERENCE: &'static str;
21}
22
23pub async fn add_catalog_resource<C, R>(
27 stripe: &StripeProjects<R>,
28 catalog: &Catalog,
29 config: &C,
30 resource_name: &str,
31) -> Result<AddedResource, ProjectsError>
32where
33 C: CatalogService,
34 R: CommandRunner,
35{
36 add_catalog_resource_with_paid(stripe, catalog, config, resource_name, false).await
37}
38
39pub async fn add_catalog_resource_with_paid<C, R>(
42 stripe: &StripeProjects<R>,
43 catalog: &Catalog,
44 config: &C,
45 resource_name: &str,
46 confirm_paid: bool,
47) -> Result<AddedResource, ProjectsError>
48where
49 C: CatalogService,
50 R: CommandRunner,
51{
52 let value = serde_json::to_value(config).map_err(|err| ProjectsError::ProvisionFailed {
53 resource: resource_name.to_owned(),
54 detail: format!("config for {} did not serialize: {err}", C::REFERENCE),
55 })?;
56 let service = catalog
57 .lookup(C::REFERENCE)
58 .ok_or_else(|| ProjectsError::CatalogMissing {
59 reference: C::REFERENCE.to_owned(),
60 })?;
61 service
62 .validate_config(&value)
63 .map_err(|violations| ProjectsError::ConfigSchema {
64 reference: C::REFERENCE,
65 violations,
66 })?;
67 let paid = service.requires_confirmation_with_paid(&value, confirm_paid);
68 ensure_parent_plans(stripe, catalog, service, &value, confirm_paid).await?;
69 project::add_resource(stripe, C::REFERENCE, resource_name, &value, paid).await
70}
71
72async fn ensure_parent_plans<R: CommandRunner>(
75 stripe: &StripeProjects<R>,
76 catalog: &Catalog,
77 service: &crate::catalog::ServiceDetail,
78 config: &Value,
79 prefer_paid: bool,
80) -> Result<(), ProjectsError> {
81 for plan_id in service.required_parent_services(config, prefer_paid) {
82 let reference = format!("{}/{}", service.provider_name.to_ascii_lowercase(), plan_id);
83 let plan = catalog
84 .lookup(&reference)
85 .ok_or_else(|| ProjectsError::ProvisionFailed {
86 resource: plan_id.clone(),
87 detail: format!("parent plan {reference} not found in catalog"),
88 })?;
89 let needs_paid = plan.requires_confirmation_with_paid(&json!({}), prefer_paid);
90 let paid = needs_paid && prefer_paid;
91 project::add_resource(stripe, &reference, &plan_id, &json!({}), paid).await?;
92 }
93 Ok(())
94}
95
96pub fn requires_confirmation<C>(catalog: &Catalog, config: &C) -> Option<bool>
99where
100 C: CatalogService,
101{
102 let value = serde_json::to_value(config).ok()?;
103 catalog
104 .lookup(C::REFERENCE)
105 .map(|service| service.requires_confirmation(&value))
106}
107
108pub fn verify_service<C>(catalog: &Catalog, sample: &C) -> Vec<String>
112where
113 C: CatalogService,
114{
115 let mut out = Vec::new();
116 let Some(service) = catalog.lookup(C::REFERENCE) else {
117 out.push(format!("{}: reference not found in catalog", C::REFERENCE));
118 return out;
119 };
120 match serde_json::to_value(sample) {
121 Ok(value) => {
122 if let Err(violations) = service.validate_config(&value) {
123 out.extend(
124 violations
125 .into_iter()
126 .map(|v| format!("{}: {v}", C::REFERENCE)),
127 );
128 }
129 }
130 Err(err) => out.push(format!(
131 "{}: sample config did not serialize: {err}",
132 C::REFERENCE
133 )),
134 }
135 out
136}