stemedb/crates/stemedb-api/src/handlers/subjects.rs
jordan cde30b9213 chore: apply rustfmt formatting across API handlers and core types
Reformats import blocks, function signatures, and expression line wrapping
in stemedb-api handlers, stemedb-core serde/source_record, and serde_helpers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 16:43:45 -07:00

96 lines
3.2 KiB
Rust

//! Handlers for subject and predicate discovery endpoints.
//!
//! These endpoints scan existing Redb indexes to expose the subjects
//! and predicates known to the system, enabling autocomplete/typeahead
//! in the dashboard.
use axum::{
extract::{Path, State},
Json,
};
use tracing::instrument;
use crate::{
dto::subjects::{ListPredicatesResponse, ListSubjectsParams, ListSubjectsResponse},
error::Result,
extractors::QsQuery,
state::AppState,
};
use stemedb_storage::{key_codec, KVStore};
/// List all known subjects, with optional prefix filtering.
///
/// Scans the `\x00SUBJECTS:` index in Redb. Supports prefix filtering
/// via the `q` parameter for typeahead/autocomplete use cases.
#[utoipa::path(
get,
path = "/v1/subjects",
params(
("q" = Option<String>, Query, description = "Prefix filter for subject names"),
("limit" = Option<usize>, Query, description = "Max results (default 100, max 1000)")
),
responses(
(status = 200, description = "List of subjects", body = ListSubjectsResponse),
(status = 500, description = "Internal server error", body = crate::dto::ErrorResponse)
),
tag = "discovery"
)]
#[instrument(skip(state), fields(q = ?params.q, limit = ?params.limit))]
pub async fn list_subjects(
State(state): State<AppState>,
QsQuery(params): QsQuery<ListSubjectsParams>,
) -> Result<Json<ListSubjectsResponse>> {
metrics::counter!("stemedb_queries_total", "endpoint" => "list_subjects").increment(1);
let prefix = if let Some(ref q) = params.q {
key_codec::subjects_index_key(q)
} else {
key_codec::subjects_scan_prefix()
};
let entries = state.store.scan_prefix(&prefix).await?;
let total_count = entries.len();
let limit = params.limit.unwrap_or(100).min(1000);
let subjects: Vec<String> = entries
.iter()
.filter_map(|(k, _)| key_codec::extract_subject_from_subjects_key(k))
.take(limit)
.collect();
Ok(Json(ListSubjectsResponse { subjects, total_count }))
}
/// List all predicates for a given subject.
///
/// Scans the `{subject}\x00SP:` index in Redb to find all predicates
/// that have been asserted for this subject.
#[utoipa::path(
get,
path = "/v1/subjects/{subject}/predicates",
params(
("subject" = String, Path, description = "The subject to list predicates for")
),
responses(
(status = 200, description = "List of predicates for the subject", body = ListPredicatesResponse),
(status = 500, description = "Internal server error", body = crate::dto::ErrorResponse)
),
tag = "discovery"
)]
#[instrument(skip(state), fields(%subject))]
pub async fn list_predicates(
State(state): State<AppState>,
Path(subject): Path<String>,
) -> Result<Json<ListPredicatesResponse>> {
metrics::counter!("stemedb_queries_total", "endpoint" => "list_predicates").increment(1);
let prefix = key_codec::subject_predicate_scan_prefix(&subject);
let entries = state.store.scan_prefix(&prefix).await?;
let predicates: Vec<String> =
entries.iter().filter_map(|(k, _)| key_codec::extract_sp_key(k).map(|(_, p)| p)).collect();
Ok(Json(ListPredicatesResponse { subject, predicates }))
}