use std::sync::Arc; use axum::{ extract::{Path, State}, http::StatusCode, response::IntoResponse, routing::{get, head, post, put}, Json, Router, }; use catalog_registry::service::{ ExtensionMetadata, CatalogRegistry, ImageManifest, PushBlobRequest, }; use ontoref_derive::onto_api; use serde_json::json; /// Application state #[derive(Clone)] pub struct AppState { registry: Arc, } impl AppState { pub fn new(registry: Arc) -> Self { Self { registry } } } /// Error response #[derive(Debug)] pub struct RegistryError { status: StatusCode, message: String, } impl IntoResponse for RegistryError { fn into_response(self) -> axum::response::Response { let body = Json(json!({ "errors": [{ "code": "REGISTRY_ERROR", "message": self.message, }] })); (self.status, body).into_response() } } impl RegistryError { pub fn not_found(msg: impl Into) -> Self { Self { status: StatusCode::NOT_FOUND, message: msg.into(), } } pub fn internal(msg: impl Into) -> Self { Self { status: StatusCode::INTERNAL_SERVER_ERROR, message: msg.into(), } } #[allow(dead_code)] pub fn bad_request(msg: impl Into) -> Self { Self { status: StatusCode::BAD_REQUEST, message: msg.into(), } } } /// Check if blob exists (HEAD /v2//blobs/) #[onto_api( method = "HEAD", path = "/v2/{name}/blobs/{digest}", description = "Check if blob exists (OCI v2)", auth = "bearer", actors = "developer, agent", tags = "oci, blobs", feature = "" )] async fn blob_exists( Path((_name, digest)): Path<(String, String)>, State(state): State, ) -> Result { state .registry .blob_exists(&digest) .await .map_err(|e: anyhow::Error| RegistryError::internal(e.to_string()))?; Ok(StatusCode::OK) } /// Pull blob (GET /v2//blobs/) #[onto_api( method = "GET", path = "/v2/{name}/blobs/{digest}", description = "Pull blob content (OCI v2)", auth = "bearer", actors = "developer, agent", tags = "oci, blobs", feature = "" )] async fn pull_blob( Path((_name, digest)): Path<(String, String)>, State(state): State, ) -> Result, RegistryError> { state .registry .pull_blob(&digest) .await .map_err(|e: anyhow::Error| { if e.to_string().contains("not found") { RegistryError::not_found(e.to_string()) } else { RegistryError::internal(e.to_string()) } }) } /// Push blob (POST /v2//blobs/uploads/) #[onto_api( method = "POST", path = "/v2/{name}/blobs/uploads", description = "Push blob content (OCI v2)", auth = "bearer", actors = "developer", tags = "oci, blobs", feature = "" )] async fn push_blob( Path(_name): Path, State(state): State, Json(req): Json, ) -> Result<(StatusCode, Json), RegistryError> { let response = state .registry .push_blob(req) .await .map_err(|e: anyhow::Error| RegistryError::internal(e.to_string()))?; Ok(( StatusCode::CREATED, Json(json!({ "digest": response.digest, "size": response.size, })), )) } /// Pull manifest (GET /v2//manifests/) #[onto_api( method = "GET", path = "/v2/{name}/manifests/{reference}", description = "Pull extension manifest (OCI v2)", auth = "bearer", actors = "developer, agent", tags = "oci, manifests", feature = "" )] async fn pull_manifest( Path((name, reference)): Path<(String, String)>, State(state): State, ) -> Result<(StatusCode, Json), RegistryError> { let response = state .registry .get_manifest(&format!("{}:{}", name, reference)) .await .map_err(|e: anyhow::Error| { if e.to_string().contains("not found") { RegistryError::not_found(e.to_string()) } else { RegistryError::internal(e.to_string()) } })?; Ok(( StatusCode::OK, Json(json!({ "manifest": response.manifest, "digest": response.digest, "content_type": response.content_type, })), )) } /// Push manifest (PUT /v2//manifests/) #[onto_api( method = "PUT", path = "/v2/{name}/manifests/{reference}", description = "Push extension manifest (OCI v2)", auth = "bearer", actors = "developer", tags = "oci, manifests", feature = "" )] async fn push_manifest( Path((name, reference)): Path<(String, String)>, State(state): State, Json(manifest): Json, ) -> Result<(StatusCode, Json), RegistryError> { let digest = state .registry .put_manifest(format!("{}:{}", name, reference), manifest) .await .map_err(|e: anyhow::Error| RegistryError::internal(e.to_string()))?; Ok(( StatusCode::CREATED, Json(json!({ "digest": digest, })), )) } /// List repositories catalog (GET /v2/_catalog) #[onto_api( method = "GET", path = "/v2/_catalog", description = "List all extension repositories (OCI v2)", auth = "bearer", actors = "developer, agent", tags = "oci, catalog", feature = "" )] async fn list_catalog( State(state): State, ) -> Result, RegistryError> { let extensions = state .registry .list_extensions() .await .map_err(|e| RegistryError::internal(e.to_string()))?; let repositories: Vec = extensions.iter().map(|e| e.name.clone()).collect(); Ok(Json(json!({ "repositories": repositories, }))) } /// List extension tags (GET /v2//tags/list) #[onto_api( method = "GET", path = "/v2/{name}/tags/list", description = "List extension tags (OCI v2)", auth = "bearer", actors = "developer, agent", tags = "oci, tags", feature = "" )] async fn list_tags( Path(name): Path, State(state): State, ) -> Result, RegistryError> { let extension = state .registry .get_extension(&name) .await .map_err(|e: anyhow::Error| { if e.to_string().contains("not found") { RegistryError::not_found(e.to_string()) } else { RegistryError::internal(e.to_string()) } })?; Ok(Json(json!({ "name": name, "tags": [extension.version], }))) } /// Register extension metadata (POST /extensions) #[onto_api( method = "POST", path = "/extensions", description = "Register extension metadata", auth = "bearer", actors = "developer", tags = "extensions", feature = "" )] async fn register_extension( State(state): State, Json(metadata): Json, ) -> Result<(StatusCode, Json), RegistryError> { state .registry .register_extension(metadata.clone()) .await .map_err(|e: anyhow::Error| RegistryError::internal(e.to_string()))?; Ok(( StatusCode::CREATED, Json(json!({ "name": metadata.name, "version": metadata.version, })), )) } /// Get extension metadata (GET /extensions/:name) #[onto_api( method = "GET", path = "/extensions/{name}", description = "Get extension metadata by name", auth = "bearer", actors = "developer, agent", tags = "extensions", feature = "" )] async fn get_extension( Path(name): Path, State(state): State, ) -> Result, RegistryError> { state .registry .get_extension(&name) .await .map_err(|e: anyhow::Error| { if e.to_string().contains("not found") { RegistryError::not_found(e.to_string()) } else { RegistryError::internal(e.to_string()) } }) .map(Json) } /// List all extensions (GET /extensions) #[onto_api( method = "GET", path = "/extensions", description = "List all registered extensions", auth = "bearer", actors = "developer, agent", tags = "extensions", feature = "" )] async fn list_extensions( State(state): State, ) -> Result>, RegistryError> { state .registry .list_extensions() .await .map_err(|e: anyhow::Error| RegistryError::internal(e.to_string())) .map(Json) } /// Health check (GET /health) #[onto_api( method = "GET", path = "/health", description = "Service health check", auth = "none", actors = "developer, agent, ci", tags = "health", feature = "" )] async fn health(State(state): State) -> Result { state .registry .health_check() .await .map_err(|e: anyhow::Error| RegistryError::internal(e.to_string()))?; Ok(StatusCode::OK) } /// Build API router pub fn routes(state: AppState) -> Router { Router::new() // OCI v2 API .route("/v2/{name}/blobs/{digest}", head(blob_exists)) .route("/v2/{name}/blobs/{digest}", get(pull_blob)) .route("/v2/{name}/blobs/uploads", post(push_blob)) .route("/v2/{name}/manifests/{reference}", get(pull_manifest)) .route("/v2/{name}/manifests/{reference}", put(push_manifest)) .route("/v2/_catalog", get(list_catalog)) .route("/v2/{name}/tags/list", get(list_tags)) // Extensions API .route("/extensions", post(register_extension)) .route("/extensions", get(list_extensions)) .route("/extensions/{name}", get(get_extension)) // Health .route("/health", get(health)) .route("/api/v1/health", get(health)) // API catalog .route( "/api/catalog", get(crate::api_catalog::api_catalog), ) .with_state(state) }