1use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5
6pub use mant_ir::{DocumentAddress, MarkdownOrigin};
7
8use crate::{SearchCase, SearchSyntax};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
12pub enum CatalogSchema {
13 #[serde(rename = "mant.catalog/v7")]
15 V7,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
20#[serde(rename_all = "kebab-case")]
21pub enum CatalogDocumentKind {
22 Markdown,
24 Manual,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
30#[serde(rename_all = "camelCase", deny_unknown_fields)]
31pub struct CatalogQuery {
32 #[serde(skip_serializing_if = "Option::is_none")]
34 pub pattern: Option<String>,
35 #[serde(default)]
37 pub syntax: SearchSyntax,
38 #[serde(default)]
40 pub case: SearchCase,
41 #[serde(skip_serializing_if = "Option::is_none")]
43 pub kind: Option<CatalogDocumentKind>,
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub source: Option<String>,
47 #[serde(skip_serializing_if = "Option::is_none")]
49 pub manual_section: Option<String>,
50 #[serde(default = "default_catalog_limit")]
52 #[schemars(range(min = 1, max = 10000))]
53 pub limit: u32,
54 #[serde(default)]
56 pub offset: u32,
57}
58
59impl Default for CatalogQuery {
60 fn default() -> Self {
61 Self {
62 pattern: None,
63 syntax: SearchSyntax::Literal,
64 case: SearchCase::Insensitive,
65 kind: None,
66 source: None,
67 manual_section: None,
68 limit: default_catalog_limit(),
69 offset: 0,
70 }
71 }
72}
73
74#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
76#[serde(rename_all = "camelCase")]
77pub struct DocumentSummary {
78 pub address: DocumentAddress,
80 pub catalog_path: String,
82}
83
84#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
86#[serde(rename_all = "camelCase")]
87#[schemars(extend("$id" = "urn:mant:catalog:v7"))]
88pub struct DocumentCatalog {
89 pub schema: CatalogSchema,
91 pub total: u32,
93 pub returned: u32,
95 pub offset: u32,
97 pub truncated: bool,
99 #[serde(skip_serializing_if = "Option::is_none")]
101 pub next_offset: Option<u32>,
102 pub documents: Vec<DocumentSummary>,
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
111pub enum CatalogMatchRank {
112 Exact,
114 ComponentSuffix,
116 Prefix,
118 Substring,
120 Unranked,
122}
123
124#[must_use]
127pub fn catalog_literal_match_rank(
128 name: &str,
129 pattern: Option<&str>,
130 case: SearchCase,
131) -> CatalogMatchRank {
132 let Some(pattern) = pattern else {
133 return CatalogMatchRank::Unranked;
134 };
135 let insensitive = case == SearchCase::Insensitive
136 || case == SearchCase::Smart && !pattern.chars().any(char::is_uppercase);
137 let (name, pattern) = if insensitive {
138 (name.to_lowercase(), pattern.to_lowercase())
139 } else {
140 (name.to_owned(), pattern.to_owned())
141 };
142 if name == pattern {
143 CatalogMatchRank::Exact
144 } else if name.ends_with(&format!("/{pattern}")) {
145 CatalogMatchRank::ComponentSuffix
146 } else if name.starts_with(&pattern) {
147 CatalogMatchRank::Prefix
148 } else {
149 CatalogMatchRank::Substring
150 }
151}
152
153#[must_use]
154pub const fn default_catalog_limit() -> u32 {
156 100
157}