iac-forge 0.1.6

Platform-independent infrastructure-as-code generator from OpenAPI specs
Documentation
//! Per-language SDK naming conventions for emitted backends.
//!
//! When a backend (Crossplane controller, Pulumi resource, etc.) needs to
//! call into a generated client SDK, it must know the exact symbol names
//! the SDK generator emitted. This module centralises those conventions so
//! that backends never re-derive them ad-hoc.
//!
//! # Why a dedicated module
//!
//! The TOML resource specs in `*-terraform-resources` packages reference
//! `OpenAPI` operationIds / schema names (e.g. `authMethodCreateApiKey`) by
//! string. The autogenerated client SDK names its methods using a strict
//! per-language convention — for `openapi-generator-cli`'s `go` template
//! that's "first character uppercase, rest unchanged" against an exported
//! `*V2APIService` receiver. Hand-rolling that conversion in every backend
//! invites drift the moment a new schema name with edge-case casing
//! arrives. One module, one round-trip-tested convention, every backend
//! shares it.
//!
//! # Coverage today
//!
//! `go_*` helpers cover the openapi-generator `go` template (akeyless-go,
//! datadog-go-sdk, splunk-go-sdk and any future pleme-managed Go SDK
//! emitted via `forge-gen --sdks go`). Per-language siblings
//! (`rust_*`, `typescript_*`, `python_*`) can land alongside as backends
//! need them; the public surface is one module so consumers grep
//! `sdk_naming::` and find every SDK convention in one place.

/// Convert an `OpenAPI` operationId / schema name (camelCase) to its
/// Go-exported counterpart (PascalCase).
///
/// This matches `openapi-generator-cli`'s `go` template convention:
/// only the first ASCII letter is uppercased; subsequent characters
/// — including any embedded all-caps acronyms — are preserved as-is.
///
/// ```
/// use iac_forge::sdk_naming::go_method_name;
/// assert_eq!(go_method_name("authMethodCreateApiKey"), "AuthMethodCreateApiKey");
/// assert_eq!(go_method_name("gatewayCreateK8SAuthConfig"), "GatewayCreateK8SAuthConfig");
/// assert_eq!(go_method_name("getAuthMethod"), "GetAuthMethod");
/// assert_eq!(go_method_name("CreateRole"), "CreateRole");
/// assert_eq!(go_method_name(""), "");
/// ```
#[must_use]
pub fn go_method_name(schema_name: &str) -> String {
    let mut chars = schema_name.chars();
    match chars.next() {
        Some(c) => {
            let head: String = c.to_uppercase().collect();
            head + chars.as_str()
        }
        None => String::new(),
    }
}

/// The request-builder type name `openapi-generator-cli` emits for an
/// operation: the method name prefixed `Api` and suffixed `Request`.
///
/// ```
/// use iac_forge::sdk_naming::go_request_type_name;
/// assert_eq!(
///     go_request_type_name("authMethodCreateApiKey"),
///     "ApiAuthMethodCreateApiKeyRequest"
/// );
/// ```
#[must_use]
pub fn go_request_type_name(schema_name: &str) -> String {
    format!("Api{}Request", go_method_name(schema_name))
}

/// The executor method name on the request builder: the method name with
/// an `Execute` suffix. Returned by every `(*V2APIService).{Method}` call
/// chain as the final terminator that performs the HTTP request.
///
/// ```
/// use iac_forge::sdk_naming::go_executor_name;
/// assert_eq!(go_executor_name("authMethodCreateApiKey"), "AuthMethodCreateApiKeyExecute");
/// ```
#[must_use]
pub fn go_executor_name(schema_name: &str) -> String {
    format!("{}Execute", go_method_name(schema_name))
}

/// The Go body-struct type name for a request — same as `go_method_name`
/// because openapi-generator names the body struct after the operationId
/// itself (PascalCase). Provided as a named alias so call sites read
/// intentionally instead of duplicating `go_method_name(...)` for the
/// body type.
///
/// ```
/// use iac_forge::sdk_naming::go_body_type_name;
/// assert_eq!(go_body_type_name("authMethodCreateApiKey"), "AuthMethodCreateApiKey");
/// ```
#[must_use]
pub fn go_body_type_name(schema_name: &str) -> String {
    go_method_name(schema_name)
}

/// The Go output-struct type name for a response. If the spec declares a
/// dedicated `read_response_schema` (akeyless TOML convention), use that;
/// otherwise fall back to `<MethodName>Output` (openapi-generator default).
///
/// ```
/// use iac_forge::sdk_naming::go_output_type_name;
/// assert_eq!(
///     go_output_type_name("authMethodCreateApiKey", None),
///     "AuthMethodCreateApiKeyOutput"
/// );
/// assert_eq!(
///     go_output_type_name("getAuthMethod", Some("AuthMethod")),
///     "AuthMethod"
/// );
/// ```
#[must_use]
pub fn go_output_type_name(schema_name: &str, response_schema: Option<&str>) -> String {
    response_schema.map_or_else(
        || format!("{}Output", go_method_name(schema_name)),
        |r| go_method_name(r),
    )
}

#[cfg(test)]
mod tests {
    use super::*;

    /// The five representative resources the M2 plan calls out — these are
    /// the integration targets the Crossplane provider's first cycle
    /// consumes. Each tuple is (TOML `create_schema`, expected SDK method,
    /// TOML `read_schema`, expected SDK read method, optional
    /// `read_response_schema`, expected output type). Verified against the
    /// actual emitted `pleme-io/akeyless-go/api_v2.go` symbols.
    fn akeyless_targets() -> Vec<(
        &'static str,
        &'static str,
        &'static str,
        &'static str,
        Option<&'static str>,
        &'static str,
    )> {
        vec![
            (
                "authMethodCreateApiKey",
                "AuthMethodCreateApiKey",
                "getAuthMethod",
                "GetAuthMethod",
                Some("AuthMethod"),
                "AuthMethod",
            ),
            (
                "createRole",
                "CreateRole",
                "getRole",
                "GetRole",
                None,
                "GetRoleOutput",
            ),
            (
                "setRoleRule",
                "SetRoleRule",
                "getRole",
                "GetRole",
                None,
                "GetRoleOutput",
            ),
            (
                "createSecret",
                "CreateSecret",
                "getSecretValue",
                "GetSecretValue",
                None,
                "GetSecretValueOutput",
            ),
            (
                "gatewayCreateK8SAuthConfig",
                "GatewayCreateK8SAuthConfig",
                "gatewayGetK8SAuthConfig",
                "GatewayGetK8SAuthConfig",
                Some("GatewayGetK8SAuthConfigOutput"),
                "GatewayGetK8SAuthConfigOutput",
            ),
        ]
    }

    #[test]
    fn five_representative_resources_round_trip() {
        for (create, exp_create, read, exp_read, response, exp_output) in akeyless_targets() {
            assert_eq!(
                go_method_name(create),
                exp_create,
                "create method for {create}"
            );
            assert_eq!(go_method_name(read), exp_read, "read method for {read}");
            assert_eq!(
                go_output_type_name(read, response),
                exp_output,
                "output type for {read}"
            );
        }
    }

    #[test]
    fn request_builder_naming_for_targets() {
        for (create, exp_create, _, _, _, _) in akeyless_targets() {
            assert_eq!(
                go_request_type_name(create),
                format!("Api{exp_create}Request"),
                "request type for {create}",
            );
        }
    }

    #[test]
    fn executor_naming_for_targets() {
        for (create, exp_create, _, _, _, _) in akeyless_targets() {
            assert_eq!(
                go_executor_name(create),
                format!("{exp_create}Execute"),
                "executor for {create}",
            );
        }
    }

    #[test]
    fn empty_input_returns_empty() {
        assert_eq!(go_method_name(""), "");
        assert_eq!(go_request_type_name(""), "ApiRequest");
        assert_eq!(go_executor_name(""), "Execute");
    }

    #[test]
    fn already_pascal_case_is_idempotent() {
        assert_eq!(go_method_name("CreateRole"), "CreateRole");
        assert_eq!(
            go_method_name("AuthMethodCreateApiKey"),
            "AuthMethodCreateApiKey"
        );
    }

    #[test]
    fn unicode_first_char() {
        // Defensive: openapi-generator never emits non-ASCII names but the
        // helper must not panic if it ever sees one.
        assert_eq!(go_method_name("ñame"), "Ñame");
    }
}