Skip to main content

stackless_integrations/providers/algolia/
application.rs

1//! `algolia/application` integration.
2
3use 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    /// Paid-pricing tier selector (`build` / `grow` / `grow-plus`). Not in
21    /// configuration_schema; required to match a catalog paid_pricing entry.
22    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    // Provisional until pinned by `mise run discover algolia/application`.
45    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}