biors 0.58.0

Command-line tools for bio-rs biological AI model input workflows.
use super::serve_http::{HttpRequest, HttpResponse};
use biors_core::service::{
    service_health_document, service_openapi_document, validate_service_batch_sequence_request,
    ServiceBatchSequenceValidateRequest,
};

pub(crate) fn handle_service_request(
    request: HttpRequest,
    version: &str,
    base_url: &str,
) -> HttpResponse {
    match (request.method.as_str(), request.path.as_str()) {
        ("GET", "/health") => HttpResponse::json(200, &service_health_document(version)),
        ("GET", "/openapi.json") => {
            HttpResponse::json(200, &service_openapi_document(version, base_url))
        }
        ("POST", "/v0/batch/sequence/validate") => handle_batch_sequence_validate(request),
        (method, path) if known_path(path) => HttpResponse::error(
            405,
            "service.method_not_allowed",
            format!("{method} is not allowed for {path}"),
            Some(path.to_string()),
        ),
        (_, path) => HttpResponse::error(
            404,
            "service.route_not_found",
            format!("service route '{path}' was not found"),
            Some(path.to_string()),
        ),
    }
}

fn handle_batch_sequence_validate(request: HttpRequest) -> HttpResponse {
    let payload: ServiceBatchSequenceValidateRequest = match serde_json::from_slice(&request.body) {
        Ok(payload) => payload,
        Err(error) => {
            return HttpResponse::error(
                400,
                "service.invalid_json",
                format!("request body is not valid JSON: {error}"),
                None,
            )
        }
    };

    match validate_service_batch_sequence_request(payload) {
        Ok(output) => HttpResponse::json(200, &output),
        Err(error) => HttpResponse::error(422, error.code(), error.to_string(), error.location()),
    }
}

fn known_path(path: &str) -> bool {
    matches!(
        path,
        "/health" | "/openapi.json" | "/v0/batch/sequence/validate"
    )
}

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

    #[test]
    fn health_returns_local_policy() {
        let response = handle_service_request(
            HttpRequest::get("/health"),
            "0.54.0",
            "http://127.0.0.1:8787",
        );
        assert_eq!(response.status, 200);
        let body: Value = serde_json::from_slice(&response.body).expect("health JSON");
        assert_eq!(body["schema_version"], "biors.service_health.v0");
        assert_eq!(body["network_policy"], "local_first_no_external_calls");
    }

    #[test]
    fn batch_sequence_validate_accepts_inline_fasta() {
        let request = HttpRequest::post(
            "/v0/batch/sequence/validate",
            br#"{"kind":"protein","inputs":[{"id":"seqs","fasta_text":">seq1\nACDE\n"}]}"#.to_vec(),
        );
        let response = handle_service_request(request, "0.54.0", "http://127.0.0.1:8787");
        assert_eq!(response.status, 200);
        let body: Value = serde_json::from_slice(&response.body).expect("batch JSON");
        assert_eq!(
            body["schema_version"],
            "biors.service_batch_sequence_validate.v0"
        );
        assert_eq!(body["summary"]["fasta_records"], 1);
    }

    #[test]
    fn batch_sequence_validate_reports_payload_errors() {
        let request = HttpRequest::post(
            "/v0/batch/sequence/validate",
            br#"{"inputs":[{"id":"","fasta_text":">seq1\nACDE\n"}]}"#.to_vec(),
        );
        let response = handle_service_request(request, "0.54.0", "http://127.0.0.1:8787");
        assert_eq!(response.status, 422);
        let body: Value = serde_json::from_slice(&response.body).expect("error JSON");
        assert_eq!(body["error"]["code"], "service.batch.empty_input_id");
    }

    #[test]
    fn batch_sequence_validate_rejects_unknown_fields_and_invalid_ids() {
        let unknown_field = HttpRequest::post(
            "/v0/batch/sequence/validate",
            br#"{"inputs":[{"id":"seq1","fasta_text":">seq1\nACDE\n","extra":true}]}"#.to_vec(),
        );
        let response = handle_service_request(unknown_field, "0.57.0", "http://127.0.0.1:8787");
        assert_eq!(response.status, 400);
        let body: Value = serde_json::from_slice(&response.body).expect("error JSON");
        assert_eq!(body["error"]["code"], "service.invalid_json");

        let invalid_id = HttpRequest::post(
            "/v0/batch/sequence/validate",
            br#"{"inputs":[{"id":"sample 1","fasta_text":">seq1\nACDE\n"}]}"#.to_vec(),
        );
        let response = handle_service_request(invalid_id, "0.57.0", "http://127.0.0.1:8787");
        assert_eq!(response.status, 422);
        let body: Value = serde_json::from_slice(&response.body).expect("error JSON");
        assert_eq!(body["error"]["code"], "service.batch.invalid_input_id");
    }
}