minco-contract 1.12.0

OpenAPI-first contract validation, operation inventory, and deterministic Rust binding generation for Minco
Documentation
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum HttpMethod {
    Get,
    Post,
    Put,
    Patch,
    Delete,
    Options,
    Head,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ResourceAction {
    Create,
    List,
    Read,
    Update,
    Delete,
}

impl ResourceAction {
    pub(crate) fn from_openapi(value: &str) -> Option<Self> {
        match value {
            "create" => Some(Self::Create),
            "list" => Some(Self::List),
            "read" => Some(Self::Read),
            "update" => Some(Self::Update),
            "delete" => Some(Self::Delete),
            _ => None,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OwnedResourceOperation {
    pub operation_id: String,
    pub name: String,
    pub action: ResourceAction,
}

impl HttpMethod {
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Get => "GET",
            Self::Post => "POST",
            Self::Put => "PUT",
            Self::Patch => "PATCH",
            Self::Delete => "DELETE",
            Self::Options => "OPTIONS",
            Self::Head => "HEAD",
        }
    }

    pub fn from_openapi(value: &str) -> Option<Self> {
        match value {
            "get" => Some(Self::Get),
            "post" => Some(Self::Post),
            "put" => Some(Self::Put),
            "patch" => Some(Self::Patch),
            "delete" => Some(Self::Delete),
            "options" => Some(Self::Options),
            "head" => Some(Self::Head),
            _ => None,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct ContractOperation {
    pub operation_id: &'static str,
    pub method: HttpMethod,
    pub path: &'static str,
    pub authenticated: bool,
    pub idempotent: bool,
}

impl ContractOperation {
    pub const fn new(
        operation_id: &'static str,
        method: HttpMethod,
        path: &'static str,
        authenticated: bool,
        idempotent: bool,
    ) -> Self {
        Self {
            operation_id,
            method,
            path,
            authenticated,
            idempotent,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OwnedOperation {
    pub operation_id: String,
    pub method: HttpMethod,
    pub path: String,
    pub authenticated: bool,
    pub idempotent: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContractDocument {
    pub source: String,
    pub openapi_version: String,
    pub title: String,
    pub version: String,
    pub sha256: String,
    pub operations: Vec<OwnedOperation>,
    pub schema_names: Vec<String>,
    pub raw: serde_json::Value,
}

impl ContractDocument {
    #[must_use]
    pub fn resource_operations(&self) -> Vec<OwnedResourceOperation> {
        let mut resources = self
            .operations
            .iter()
            .filter_map(|operation| {
                let method = operation.method.as_str().to_ascii_lowercase();
                let metadata = self
                    .raw
                    .pointer(&format!(
                        "/paths/{}/{method}/x-minco-resource",
                        escape_json_pointer(&operation.path)
                    ))?
                    .as_object()?;
                Some(OwnedResourceOperation {
                    operation_id: operation.operation_id.clone(),
                    name: metadata.get("name")?.as_str()?.to_owned(),
                    action: ResourceAction::from_openapi(metadata.get("action")?.as_str()?)?,
                })
            })
            .collect::<Vec<_>>();
        resources.sort_by(|left, right| left.operation_id.cmp(&right.operation_id));
        resources
    }
}

fn escape_json_pointer(value: &str) -> String {
    value.replace('~', "~0").replace('/', "~1")
}