endpoint-gen 1.6.1

Generate Rust code for websocket API endpoints
Documentation
use crate::definitions::{EnumElement, ErrorCodeSchema, GenService};
use crate::rust;
use convert_case::{Case, Casing};
use endpoint_libs::libs::error_code::ErrorCode;
use endpoint_libs::model::Type;
use eyre::bail;
use std::collections::{HashMap, HashSet};

pub fn build_error_code_catalog(custom_error_codes: Vec<ErrorCodeSchema>) -> eyre::Result<Vec<ErrorCodeSchema>> {
    let mut codes = builtin_error_codes();
    codes.extend(custom_error_codes);

    let mut names: HashMap<String, ErrorCodeSchema> = HashMap::new();
    let mut numbers: HashMap<i64, ErrorCodeSchema> = HashMap::new();

    for code in &codes {
        let variant = validate_error_code_variant(&code.name)?;
        if let Some(existing) = names.insert(variant.clone(), code.clone()) {
            bail!(
                "Duplicate error code name '{}': conflicts with '{}'",
                code.name,
                existing.name
            );
        }

        if let Some(existing) = numbers.insert(code.code, code.clone()) {
            bail!(
                "Duplicate error code value {} for '{}' and '{}'",
                code.code,
                existing.name,
                code.name
            );
        }
    }

    codes.sort_by_key(|code| code.code);
    Ok(codes)
}

pub fn validate_reserved_enum_names(enums: &[EnumElement]) -> eyre::Result<()> {
    for enum_element in enums {
        if let Type::Enum { name, .. } = &enum_element.inner {
            if name.to_case(Case::Pascal) == "ErrorCode" {
                bail!("Enum name 'ErrorCode' is reserved for generated endpoint error codes");
            }
        }
    }

    Ok(())
}

pub fn validate_endpoint_error_codes(services: &[GenService], error_codes: &[ErrorCodeSchema]) -> eyre::Result<()> {
    let allowed_variants = error_codes
        .iter()
        .map(|code| validate_error_code_variant(&code.name))
        .collect::<eyre::Result<HashSet<_>>>()?;

    for service in services {
        for endpoint in &service.endpoints {
            for error in &endpoint.schema.errors {
                let variant = rust::error_code_variant_name(error.code.variant());
                if !allowed_variants.contains(&variant) {
                    bail!(
                        "Unknown error code '{}' in endpoint '{}' error '{}'",
                        error.code,
                        endpoint.schema.name,
                        error.name
                    );
                }
            }
        }
    }

    Ok(())
}

fn validate_error_code_variant(name: &str) -> eyre::Result<String> {
    let variant = rust::error_code_variant_name(name);
    let is_valid = variant.chars().next().is_some_and(|c| c.is_ascii_uppercase())
        && variant.chars().all(|c| c.is_ascii_alphanumeric());

    if !is_valid {
        bail!("Invalid error code name '{name}': expected a Rust enum variant name");
    }

    Ok(variant)
}

fn builtin_error_codes() -> Vec<ErrorCodeSchema> {
    vec![
        ErrorCodeSchema::new("BadRequest", ErrorCode::BAD_REQUEST.code() as i64, "Bad request"),
        ErrorCodeSchema::new(
            "Unauthorized",
            ErrorCode::UNAUTHORIZED.code() as i64,
            "Authentication is required",
        ),
        ErrorCodeSchema::new(
            "PaymentRequired",
            ErrorCode::PAYMENT_REQUIRED.code() as i64,
            "Payment is required",
        ),
        ErrorCodeSchema::new("Forbidden", ErrorCode::FORBIDDEN.code() as i64, "Access is forbidden"),
        ErrorCodeSchema::new("NotFound", ErrorCode::NOT_FOUND.code() as i64, "Resource was not found"),
        ErrorCodeSchema::new(
            "MethodNotAllowed",
            ErrorCode::METHOD_NOT_ALLOWED.code() as i64,
            "Method is not allowed",
        ),
        ErrorCodeSchema::new(
            "NotAcceptable",
            ErrorCode::NOT_ACCEPTABLE.code() as i64,
            "Response format is not acceptable",
        ),
        ErrorCodeSchema::new(
            "ProxyAuthenticationRequired",
            ErrorCode::PROXY_AUTHENTICATION_REQUIRED.code() as i64,
            "Proxy authentication is required",
        ),
        ErrorCodeSchema::new(
            "RequestTimeout",
            ErrorCode::REQUEST_TIMEOUT.code() as i64,
            "Request timed out",
        ),
        ErrorCodeSchema::new(
            "Conflict",
            ErrorCode::CONFLICT.code() as i64,
            "Request conflicts with current state",
        ),
        ErrorCodeSchema::new("Gone", ErrorCode::GONE.code() as i64, "Resource is gone"),
        ErrorCodeSchema::new(
            "LengthRequired",
            ErrorCode::LENGTH_REQUIRED.code() as i64,
            "Content length is required",
        ),
        ErrorCodeSchema::new(
            "PreconditionFailed",
            ErrorCode::PRECONDITION_FAILED.code() as i64,
            "Precondition failed",
        ),
        ErrorCodeSchema::new(
            "PayloadTooLarge",
            ErrorCode::PAYLOAD_TOO_LARGE.code() as i64,
            "Payload is too large",
        ),
        ErrorCodeSchema::new("UriTooLong", ErrorCode::URI_TOO_LONG.code() as i64, "URI is too long"),
        ErrorCodeSchema::new(
            "UnsupportedMediaType",
            ErrorCode::UNSUPPORTED_MEDIA_TYPE.code() as i64,
            "Media type is unsupported",
        ),
        ErrorCodeSchema::new(
            "RangeNotSatisfiable",
            ErrorCode::RANGE_NOT_SATISFIABLE.code() as i64,
            "Requested range cannot be satisfied",
        ),
        ErrorCodeSchema::new(
            "ExpectationFailed",
            ErrorCode::EXPECTATION_FAILED.code() as i64,
            "Expectation failed",
        ),
        ErrorCodeSchema::new("ImATeapot", ErrorCode::IM_A_TEAPOT.code() as i64, "I'm a teapot"),
        ErrorCodeSchema::new(
            "MisdirectedRequest",
            ErrorCode::MISDIRECTED_REQUEST.code() as i64,
            "Request was misdirected",
        ),
        ErrorCodeSchema::new(
            "UnprocessableEntity",
            ErrorCode::UNPROCESSABLE_ENTITY.code() as i64,
            "Entity could not be processed",
        ),
        ErrorCodeSchema::new("Locked", ErrorCode::LOCKED.code() as i64, "Resource is locked"),
        ErrorCodeSchema::new(
            "FailedDependency",
            ErrorCode::FAILED_DEPENDENCY.code() as i64,
            "Dependency failed",
        ),
        ErrorCodeSchema::new(
            "UpgradeRequired",
            ErrorCode::UPGRADE_REQUIRED.code() as i64,
            "Request must be upgraded",
        ),
        ErrorCodeSchema::new(
            "PreconditionRequired",
            ErrorCode::PRECONDITION_REQUIRED.code() as i64,
            "Precondition is required",
        ),
        ErrorCodeSchema::new(
            "TooManyRequests",
            ErrorCode::TOO_MANY_REQUESTS.code() as i64,
            "Too many requests",
        ),
        ErrorCodeSchema::new(
            "RequestHeaderFieldsTooLarge",
            ErrorCode::REQUEST_HEADER_FIELDS_TOO_LARGE.code() as i64,
            "Request header fields are too large",
        ),
        ErrorCodeSchema::new(
            "UnavailableForLegalReasons",
            ErrorCode::UNAVAILABLE_FOR_LEGAL_REASONS.code() as i64,
            "Unavailable for legal reasons",
        ),
        ErrorCodeSchema::new(
            "InternalError",
            ErrorCode::INTERNAL_ERROR.code() as i64,
            "Internal server error",
        ),
        ErrorCodeSchema::new(
            "NotImplemented",
            ErrorCode::NOT_IMPLEMENTED.code() as i64,
            "Endpoint is not implemented",
        ),
        ErrorCodeSchema::new("BadGateway", ErrorCode::BAD_GATEWAY.code() as i64, "Bad gateway"),
        ErrorCodeSchema::new(
            "ServiceUnavailable",
            ErrorCode::SERVICE_UNAVAILABLE.code() as i64,
            "Service is unavailable",
        ),
        ErrorCodeSchema::new(
            "GatewayTimeout",
            ErrorCode::GATEWAY_TIMEOUT.code() as i64,
            "Gateway timed out",
        ),
        ErrorCodeSchema::new(
            "HttpVersionNotSupported",
            ErrorCode::HTTP_VERSION_NOT_SUPPORTED.code() as i64,
            "HTTP version is not supported",
        ),
        ErrorCodeSchema::new(
            "VariantAlsoNegotiates",
            ErrorCode::VARIANT_ALSO_NEGOTIATES.code() as i64,
            "Content negotiation variant problem",
        ),
        ErrorCodeSchema::new(
            "InsufficientStorage",
            ErrorCode::INSUFFICIENT_STORAGE.code() as i64,
            "Insufficient storage",
        ),
        ErrorCodeSchema::new(
            "LoopDetected",
            ErrorCode::LOOP_DETECTED.code() as i64,
            "Loop was detected",
        ),
        ErrorCodeSchema::new(
            "NotExtended",
            ErrorCode::NOT_EXTENDED.code() as i64,
            "Request must be extended",
        ),
        ErrorCodeSchema::new(
            "NetworkAuthenticationRequired",
            ErrorCode::NETWORK_AUTHENTICATION_REQUIRED.code() as i64,
            "Network authentication is required",
        ),
    ]
}