yorishiro-server 0.12.1

HTTP server (REST + MCP) for Yorishiro, an MCP-native knowledge store
use axum::Json;
use axum::extract::{Path, State};
use axum::http::StatusCode;
use axum::response::IntoResponse;
use serde::Deserialize;
use utoipa::ToSchema;
use uuid::Uuid;
use yorishiro_core::metaschema::MetaSchemaDefinition;
use yorishiro_core::repositories::tenancy::{
    self, CreateTemplateInput, TemplateRecord, UpdateTemplateInput,
};

use crate::error::ApiError;
use crate::http::controllers::require_tenant_admin;
use crate::http::middleware::auth::AuthContext;
use crate::state::AppState;

#[utoipa::path(
    get,
    path = "/api/template-library",
    responses(
        (status = 200, description = "Templates visible to the caller's tenant (own plus any community-visible ones)", body = Vec<TemplateRecord>),
        (status = 401, description = "Invalid or missing credentials", body = crate::error::ApiErrorBody),
    ),
    tag = "template-library",
)]
pub async fn list_templates(
    State(state): State<AppState>,
    AuthContext(ctx): AuthContext,
) -> Result<Json<Vec<TemplateRecord>>, ApiError> {
    let templates = tenancy::list_templates(&state.identity_pool, ctx.tenant_id).await?;
    Ok(Json(templates))
}

#[utoipa::path(
    get,
    path = "/api/template-library/{id}",
    params(("id" = Uuid, Path, description = "Template ID")),
    responses(
        (status = 200, description = "Template detail", body = TemplateRecord),
        (status = 401, description = "Invalid or missing credentials", body = crate::error::ApiErrorBody),
        (status = 404, description = "Template not found", body = crate::error::ApiErrorBody),
    ),
    tag = "template-library",
)]
pub async fn get_template(
    State(state): State<AppState>,
    AuthContext(ctx): AuthContext,
    Path(id): Path<Uuid>,
) -> Result<Json<TemplateRecord>, ApiError> {
    let template = tenancy::get_template(&state.identity_pool, ctx.tenant_id, id).await?;
    Ok(Json(template))
}

#[derive(Debug, Deserialize, ToSchema)]
pub struct CreateTemplateRequest {
    pub name: String,
    pub description: Option<String>,
    pub definition: MetaSchemaDefinition,
    #[serde(default)]
    pub tags: Vec<String>,
    pub locale: Option<String>,
    pub author: Option<String>,
}

#[utoipa::path(
    post,
    path = "/api/template-library",
    request_body = CreateTemplateRequest,
    responses(
        (status = 201, description = "Template created", body = TemplateRecord),
        (status = 401, description = "Invalid or missing credentials", body = crate::error::ApiErrorBody),
        (status = 403, description = "Not a tenant owner/admin", body = crate::error::ApiErrorBody),
        (status = 409, description = "A template with this name already exists for the tenant", body = crate::error::ApiErrorBody),
        (status = 422, description = "The template definition failed validation", body = crate::error::ApiErrorBody),
    ),
    tag = "template-library",
)]
pub async fn create_template(
    State(state): State<AppState>,
    AuthContext(ctx): AuthContext,
    Json(body): Json<CreateTemplateRequest>,
) -> Result<impl IntoResponse, ApiError> {
    require_tenant_admin(&state, ctx.tenant_id, ctx.user_id).await?;

    let template = tenancy::create_template(
        &state.identity_pool,
        ctx.tenant_id,
        ctx.user_id,
        CreateTemplateInput {
            name: body.name,
            description: body.description,
            definition: body.definition,
            tags: body.tags,
            locale: body.locale,
            author: body.author,
        },
    )
    .await?;
    Ok((StatusCode::CREATED, Json(template)))
}

#[derive(Debug, Deserialize, ToSchema)]
pub struct UpdateTemplateRequest {
    pub name: Option<String>,
    pub description: Option<String>,
    pub definition: Option<MetaSchemaDefinition>,
    pub tags: Option<Vec<String>>,
    pub locale: Option<String>,
}

#[utoipa::path(
    put,
    path = "/api/template-library/{id}",
    params(("id" = Uuid, Path, description = "Template ID")),
    request_body = UpdateTemplateRequest,
    responses(
        (status = 200, description = "Template updated", body = TemplateRecord),
        (status = 401, description = "Invalid or missing credentials", body = crate::error::ApiErrorBody),
        (status = 403, description = "Not a tenant owner/admin", body = crate::error::ApiErrorBody),
        (status = 404, description = "Template not found", body = crate::error::ApiErrorBody),
        (status = 422, description = "The template definition failed validation", body = crate::error::ApiErrorBody),
    ),
    tag = "template-library",
)]
pub async fn update_template(
    State(state): State<AppState>,
    AuthContext(ctx): AuthContext,
    Path(id): Path<Uuid>,
    Json(body): Json<UpdateTemplateRequest>,
) -> Result<Json<TemplateRecord>, ApiError> {
    require_tenant_admin(&state, ctx.tenant_id, ctx.user_id).await?;

    let template = tenancy::update_template(
        &state.identity_pool,
        ctx.tenant_id,
        id,
        UpdateTemplateInput {
            name: body.name,
            description: body.description,
            definition: body.definition,
            tags: body.tags,
            locale: body.locale,
        },
    )
    .await?;
    Ok(Json(template))
}

#[utoipa::path(
    delete,
    path = "/api/template-library/{id}",
    params(("id" = Uuid, Path, description = "Template ID")),
    responses(
        (status = 204, description = "Template deleted"),
        (status = 401, description = "Invalid or missing credentials", body = crate::error::ApiErrorBody),
        (status = 403, description = "Not a tenant owner/admin", body = crate::error::ApiErrorBody),
        (status = 404, description = "Template not found", body = crate::error::ApiErrorBody),
    ),
    tag = "template-library",
)]
pub async fn delete_template(
    State(state): State<AppState>,
    AuthContext(ctx): AuthContext,
    Path(id): Path<Uuid>,
) -> Result<StatusCode, ApiError> {
    require_tenant_admin(&state, ctx.tenant_id, ctx.user_id).await?;
    tenancy::delete_template(&state.identity_pool, ctx.tenant_id, id).await?;
    Ok(StatusCode::NO_CONTENT)
}

#[derive(Debug, Deserialize, ToSchema)]
pub struct ForkTemplateRequest {
    pub name: String,
}

#[utoipa::path(
    post,
    path = "/api/template-library/{id}/fork",
    params(("id" = Uuid, Path, description = "Template ID to fork")),
    request_body = ForkTemplateRequest,
    responses(
        (status = 201, description = "Forked copy of the template, owned by the caller's tenant", body = TemplateRecord),
        (status = 401, description = "Invalid or missing credentials", body = crate::error::ApiErrorBody),
        (status = 403, description = "Not a tenant owner/admin", body = crate::error::ApiErrorBody),
        (status = 404, description = "Source template not found", body = crate::error::ApiErrorBody),
        (status = 409, description = "A template with this name already exists for the tenant", body = crate::error::ApiErrorBody),
    ),
    tag = "template-library",
)]
pub async fn fork_template(
    State(state): State<AppState>,
    AuthContext(ctx): AuthContext,
    Path(id): Path<Uuid>,
    Json(body): Json<ForkTemplateRequest>,
) -> Result<impl IntoResponse, ApiError> {
    require_tenant_admin(&state, ctx.tenant_id, ctx.user_id).await?;

    let template = tenancy::fork_template(
        &state.identity_pool,
        ctx.tenant_id,
        ctx.user_id,
        id,
        body.name,
    )
    .await?;
    Ok((StatusCode::CREATED, Json(template)))
}