use indexmap::IndexMap;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::pagination::Cursor;
#[derive(Debug, Serialize, JsonSchema)]
#[serde(untagged)]
pub enum ListEntries {
Brief(Vec<String>),
Detailed(IndexMap<String, Value>),
}
impl ListEntries {
#[must_use]
pub fn len(&self) -> usize {
match self {
Self::Brief(v) => v.len(),
Self::Detailed(m) => m.len(),
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
#[must_use]
pub fn as_brief(&self) -> Option<&[String]> {
if let Self::Brief(v) = self { Some(v) } else { None }
}
#[must_use]
pub fn as_detailed(&self) -> Option<&IndexMap<String, Value>> {
if let Self::Detailed(m) = self { Some(m) } else { None }
}
#[must_use]
pub fn into_brief(self) -> Option<Vec<String>> {
if let Self::Brief(v) = self { Some(v) } else { None }
}
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct ListEntriesResponse {
pub entries: ListEntries,
#[serde(rename = "nextCursor", skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<Cursor>,
}
impl ListEntriesResponse {
#[must_use]
pub fn brief(entries: Vec<String>, next_cursor: Option<Cursor>) -> Self {
Self {
entries: ListEntries::Brief(entries),
next_cursor,
}
}
#[must_use]
pub fn detailed(entries: IndexMap<String, Value>, next_cursor: Option<Cursor>) -> Self {
Self {
entries: ListEntries::Detailed(entries),
next_cursor,
}
}
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct MessageResponse {
pub message: String,
}
#[derive(Debug, Default, Deserialize, JsonSchema)]
pub struct ListDatabasesRequest {
#[serde(default)]
pub cursor: Option<Cursor>,
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct ListDatabasesResponse {
pub databases: Vec<String>,
#[serde(rename = "nextCursor", skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<Cursor>,
}
#[derive(Debug, Default, Deserialize, JsonSchema)]
pub struct CreateDatabaseRequest {
pub database: String,
}
#[derive(Debug, Default, Deserialize, JsonSchema)]
pub struct DropDatabaseRequest {
pub database: String,
}
#[derive(Debug, Default, Deserialize, JsonSchema)]
pub struct ListViewsRequest {
#[serde(default)]
pub cursor: Option<Cursor>,
#[serde(default)]
pub database: Option<String>,
}
#[derive(Debug, Default, Deserialize, JsonSchema)]
pub struct ListTriggersRequest {
#[serde(default)]
pub cursor: Option<Cursor>,
#[serde(default)]
pub search: Option<String>,
#[serde(default)]
pub detailed: bool,
#[serde(default)]
pub database: Option<String>,
}
#[derive(Debug, Default, Deserialize, JsonSchema)]
pub struct ListFunctionsRequest {
#[serde(default)]
pub cursor: Option<Cursor>,
#[serde(default)]
pub database: Option<String>,
}
#[derive(Debug, Default, Deserialize, JsonSchema)]
pub struct QueryRequest {
pub query: String,
#[serde(default)]
pub database: Option<String>,
}
#[derive(Debug, Default, Deserialize, JsonSchema)]
pub struct ReadQueryRequest {
pub query: String,
#[serde(default)]
pub cursor: Option<Cursor>,
#[serde(default)]
pub database: Option<String>,
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct QueryResponse {
pub rows: Vec<Value>,
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct ReadQueryResponse {
pub rows: Vec<Value>,
#[serde(rename = "nextCursor", skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<Cursor>,
}
#[derive(Debug, Default, Deserialize, JsonSchema)]
pub struct ExplainQueryRequest {
pub query: String,
#[serde(default)]
pub analyze: bool,
#[serde(default)]
pub database: Option<String>,
}
#[cfg(test)]
mod tests {
use super::{IndexMap, ListEntries, ListEntriesResponse, ListTriggersRequest};
use serde_json::{Value, json};
#[test]
fn list_triggers_request_defaults_to_brief_mode_without_search() {
let req: ListTriggersRequest = serde_json::from_str("{}").expect("empty object should parse");
assert!(req.search.is_none());
assert!(!req.detailed, "detailed must default to false");
assert!(req.database.is_none());
}
#[test]
fn list_triggers_request_accepts_search_and_detailed() {
let req: ListTriggersRequest = serde_json::from_str(r#"{"search": "audit", "detailed": true}"#).expect("parse");
assert_eq!(req.search.as_deref(), Some("audit"));
assert!(req.detailed);
}
#[test]
fn list_triggers_request_accepts_database_and_inner_fields() {
let req: ListTriggersRequest =
serde_json::from_str(r#"{"database": "mydb", "search": "audit", "detailed": true}"#).expect("parse");
assert_eq!(req.database.as_deref(), Some("mydb"));
assert_eq!(req.search.as_deref(), Some("audit"));
assert!(req.detailed);
}
#[test]
fn brief_serializes_as_bare_string_array() {
let entries = ListEntries::Brief(vec!["customers".into(), "orders".into()]);
assert_eq!(serde_json::to_value(&entries).unwrap(), json!(["customers", "orders"]));
}
#[test]
fn detailed_serializes_as_keyed_object() {
let entries = ListEntries::Detailed(IndexMap::from([("orders".into(), json!({"kind": "TABLE"}))]));
assert_eq!(
serde_json::to_value(&entries).unwrap(),
json!({"orders": {"kind": "TABLE"}})
);
}
#[test]
fn brief_empty_serializes_as_empty_array() {
assert_eq!(serde_json::to_value(ListEntries::Brief(Vec::new())).unwrap(), json!([]));
}
#[test]
fn detailed_empty_serializes_as_empty_object() {
assert_eq!(
serde_json::to_value(ListEntries::Detailed(IndexMap::new())).unwrap(),
json!({})
);
}
#[test]
fn detailed_preserves_insertion_order() {
let map = IndexMap::from([
("c".into(), json!({})),
("a".into(), json!({})),
("b".into(), json!({})),
]);
let s = serde_json::to_string(&ListEntries::Detailed(map)).unwrap();
let positions = ["\"c\"", "\"a\"", "\"b\""].map(|k| s.find(k).expect(k));
assert!(positions.is_sorted(), "insertion order not preserved: {s}");
}
#[test]
fn list_entries_response_brief_serializes_with_entries_key() {
let response = ListEntriesResponse::brief(vec!["a".into()], None);
assert_eq!(serde_json::to_value(&response).unwrap(), json!({"entries": ["a"]}));
}
#[test]
fn list_entries_response_detailed_serializes_with_entries_key() {
let map = IndexMap::from([("a".into(), json!({"kind": "TABLE"}))]);
let response = ListEntriesResponse::detailed(map, None);
assert_eq!(
serde_json::to_value(&response).unwrap(),
json!({"entries": {"a": {"kind": "TABLE"}}})
);
}
#[test]
fn list_entries_response_omits_next_cursor_when_none() {
let response = ListEntriesResponse::brief(vec!["a".into()], None);
let value = serde_json::to_value(&response).unwrap();
assert!(
value.get("nextCursor").is_none(),
"nextCursor must be omitted when None"
);
}
#[test]
fn as_brief_and_as_detailed_unwrap_correct_variant() {
let brief = ListEntries::Brief(vec!["a".into()]);
assert_eq!(brief.as_brief(), Some(&["a".into()][..]));
assert!(brief.as_detailed().is_none());
let det = ListEntries::Detailed(IndexMap::from([("x".into(), json!(1))]));
assert!(det.as_brief().is_none());
assert_eq!(det.as_detailed().map(IndexMap::len), Some(1));
}
#[test]
fn detailed_payload_strictly_smaller_than_array_form() {
let metadata = json!({
"schema": "public", "kind": "TABLE", "owner": "app", "comment": null,
"columns": [
{"name": "id", "dataType": "bigint", "ordinalPosition": 1, "nullable": false, "default": null, "comment": null},
{"name": "created_at", "dataType": "timestamptz", "ordinalPosition": 2, "nullable": false, "default": "now()", "comment": null},
],
"constraints": [{"name": "pk", "type": "PRIMARY KEY", "columns": ["id"], "definition": "PRIMARY KEY (id)"}],
"indexes": [], "triggers": [],
});
let tables = [
"customers",
"orders",
"items",
"products",
"inventory",
"suppliers",
"shipments",
"invoices",
"payments",
"audits",
];
let new_map: IndexMap<String, Value> = tables.iter().map(|n| ((*n).into(), metadata.clone())).collect();
let old: Vec<Value> = tables
.iter()
.map(|n| {
let mut v = metadata.clone();
v["name"] = json!(n);
v
})
.collect();
let new_len = serde_json::to_vec(&ListEntries::Detailed(new_map)).unwrap().len();
let old_len = serde_json::to_vec(&old).unwrap().len();
assert!(new_len < old_len, "payload not smaller: new={new_len} old={old_len}");
}
}