stackless_integrations/providers/algolia/
application.rs1use std::collections::BTreeMap;
4
5use serde::Serialize;
6use stackless_stripe_projects::catalog::verify::CatalogService;
7use stackless_stripe_projects::provision::ProvisionContext;
8
9use super::FamilyResource;
10use crate::error::IntegrationError;
11use crate::hostable::{ConfigScope, Hostable, IntegrationHosting};
12use crate::registry;
13
14pub const RESOURCE_KIND: &str = "integration-algolia";
15
16#[derive(Debug, Serialize)]
17pub struct AlgoliaApplicationConfig {
18 pub accept_terms: String,
19 pub name: String,
20 pub plan: String,
23 pub region: String,
24}
25
26impl CatalogService for AlgoliaApplicationConfig {
27 const REFERENCE: &'static str = "algolia/application";
28}
29
30#[derive(Debug)]
31pub struct AlgoliaApplication;
32
33impl Hostable for AlgoliaApplication {
34 const PROVIDER: &'static str = "algolia";
35 const HOSTING: IntegrationHosting = IntegrationHosting::Managed;
36 const CONFIG_SCOPE: ConfigScope = ConfigScope::GlobalOnly;
37 const RESOURCE_KIND: &'static str = RESOURCE_KIND;
38 const OUTPUTS: &'static [&'static str] = &["app_id", "api_key"];
39}
40
41impl FamilyResource for AlgoliaApplication {
42 type Config = AlgoliaApplicationConfig;
43 const PROVIDER_PREFIX: &'static str = "ALGOLIA";
44 const OUTPUT_FIELDS: &'static [(&'static str, &'static str, bool)] =
46 &[("APP_ID", "app_id", true), ("API_KEY", "api_key", true)];
47
48 fn build_config(
49 ctx: &ProvisionContext<'_>,
50 ) -> Result<AlgoliaApplicationConfig, IntegrationError> {
51 let config = super::integration_config(ctx)?;
52 Ok(AlgoliaApplicationConfig {
53 accept_terms: super::interp_required(ctx, &config, "accept_terms")?,
54 name: super::interp_required(ctx, &config, "name")?,
55 plan: super::interp_optional(ctx, &config, "plan")?
56 .unwrap_or_else(|| "build".to_owned()),
57 region: super::interp_required(ctx, &config, "region")?,
58 })
59 }
60}
61
62pub fn validate_config(
63 name: &str,
64 config: &BTreeMap<String, toml::Value>,
65) -> Result<(), IntegrationError> {
66 registry::config_string(config, "accept_terms").map_err(|err| {
67 IntegrationError::ConfigInvalid {
68 location: format!("integrations.{name}.accept_terms"),
69 detail: err.to_string(),
70 }
71 })?;
72 registry::config_string(config, "name").map_err(|err| IntegrationError::ConfigInvalid {
73 location: format!("integrations.{name}.name"),
74 detail: err.to_string(),
75 })?;
76 registry::config_string(config, "region").map_err(|err| IntegrationError::ConfigInvalid {
77 location: format!("integrations.{name}.region"),
78 detail: err.to_string(),
79 })?;
80 let _ = (name, config);
81 Ok(())
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87 use crate::ProviderOps;
88 use crate::resource::ResourcePayload;
89 use stackless_core::def::StackDef;
90 use stackless_stripe_projects::stripe::StripeProjects;
91 use stackless_stripe_projects::test_support;
92
93 #[test]
94 fn config_matches_catalog() {
95 const FIXTURE: &str = include_str!(concat!(
96 env!("CARGO_MANIFEST_DIR"),
97 "/../stackless-stripe-projects/tests/fixtures/catalog.json"
98 ));
99 let catalog = stackless_stripe_projects::Catalog::from_json_envelope(FIXTURE).unwrap();
100 let failures = stackless_stripe_projects::verify_service(
101 &catalog,
102 &AlgoliaApplicationConfig {
103 accept_terms: "test-accept_terms".into(),
104 name: "test-name".into(),
105 plan: "build".into(),
106 region: "EU West".into(),
107 },
108 );
109 assert!(
110 failures.is_empty(),
111 "algolia/application catalog gaps:\n{}",
112 failures.join("\n")
113 );
114 }
115
116 const CATALOG_ENVELOPE: &str = r##"{"ok":true,"command":"projects catalog","data":{"last_updated":"2026-07-11T00:00:00Z","services":[{"id":"prvsvc_application","object":"v2.provisioning.provider_service_detail","provider_id":"prvdr_algolia","provider_name":"Algolia","service_id":"application","categories":["database"],"kind":"deployable","scope":"project","availability":"available","development":false,"livemode":true,"pricing":{"type":"paid","paid_pricing":[{"type":"free","configuration":{"accept_terms":"test-accept_terms","plan":"build"}}]},"configuration_schema":{"properties":{"accept_terms":{"type":"string"},"name":{"description":"Name of your Algolia application","type":"string"},"region":{"description":"Where your Algolia application will be created. This cannot be changed after provisioning.","enum":["EU West","US Central","US East","US West","United Kingdom"],"type":"string"}},"required":["name","region","accept_terms"],"type":"object"}}]}}"##;
117
118 fn test_def() -> StackDef {
119 StackDef::parse(
120 r#"
121[stack]
122name = "atto"
123[stack.projects.stripe]
124project = "project_1"
125[integrations.res]
126provider = "algolia"
127plan = "build"
128accept_terms = "test-accept_terms"
129name = "test-name"
130region = "EU West"
131[services.api]
132source = { repo = "r", ref = "main" }
133env = { OUT = "${integrations.res.app_id}" }
134health = { path = "/health" }
135[services.api.local]
136run = "true"
137"#,
138 )
139 .unwrap()
140 }
141
142 #[tokio::test]
143 async fn provision_records_outputs() {
144 let runner = test_support::provision_script(
145 CATALOG_ENVELOPE,
146 serde_json::json!({"ALGOLIA_APP_ID": "val_app_id", "ALGOLIA_API_KEY": "val_api_key"}),
147 0,
148 );
149 let dir = tempfile::tempdir().unwrap();
150 std::fs::write(
151 dir.path().join("stackless.toml"),
152 "[stack]\nname=\"atto\"\n",
153 )
154 .unwrap();
155 let stripe = StripeProjects::new(&runner, dir.path());
156
157 let resource = AlgoliaApplication
158 .provision(
159 &stripe.as_dyn(),
160 &test_def(),
161 dir.path(),
162 "demo",
163 "res",
164 "local",
165 false,
166 )
167 .await
168 .unwrap();
169 assert_eq!(resource.resource_kind, "integration-algolia");
170 let payload: ResourcePayload = serde_json::from_str(&resource.payload).unwrap();
171 assert_eq!(payload.outputs["app_id"], "val_app_id");
172 }
173}