Skip to main content

mant_protocol/
query.rs

1//! Query envelope combining one structured input with optional tldr content.
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5
6use mant_ir::{ResolvedContent, TldrDocument};
7
8use crate::{
9    DocumentAddress, DocumentResponse, EntryProjection, MAX_DOCUMENT_SELECTOR_CHARS,
10    MAX_MANUAL_SECTION_CHARS, MAX_SEMANTIC_ENTRY_CHARS, MAX_SOURCE_SELECTOR_CHARS, NodeSelector,
11    SearchCase, SearchScope, SearchSyntax, default_search_limit,
12};
13
14/// Maximum outline selectors accepted by one focused read request.
15pub const MAX_NODE_SELECTORS: usize = 16;
16
17/// Exact schema marker for a complete `ManT` query result.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
19pub enum QuerySchema {
20    /// Query envelope built around `mant.document/v0.10`.
21    #[serde(rename = "mant.query/v0.10")]
22    V0Dot10,
23}
24
25impl QuerySchema {
26    /// Serialized identifier of the current query response contract.
27    pub const ID: &'static str = "mant.query/v0.10";
28}
29
30/// Exact schema marker for a native query request.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
32pub enum RequestSchema {
33    /// Query and projection request accepted through `--request-json`.
34    #[serde(rename = "mant.request/v0.10")]
35    V0Dot10,
36}
37
38impl RequestSchema {
39    /// Serialized identifier of the current request contract.
40    pub const ID: &'static str = "mant.request/v0.10";
41}
42
43/// Source selected by one public query request.
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
45#[serde(
46    tag = "kind",
47    rename_all = "kebab-case",
48    rename_all_fields = "camelCase",
49    deny_unknown_fields
50)]
51pub enum QueryInput {
52    /// Resolve personal Markdown first, then configured sources around the
53    /// priority-zero native-manual baseline.
54    Document {
55        /// Hierarchical catalog path or unqualified component-suffix selector.
56        #[schemars(length(min = 1, max = MAX_DOCUMENT_SELECTOR_CHARS))]
57        selector: String,
58        /// Optional configured Markdown source. It bypasses root documents and manuals.
59        #[schemars(length(min = 1, max = MAX_SOURCE_SELECTOR_CHARS))]
60        #[serde(skip_serializing_if = "Option::is_none")]
61        source: Option<String>,
62        /// Optional native manual category such as `1` or `3p`.
63        #[schemars(length(min = 1, max = MAX_MANUAL_SECTION_CHARS))]
64        #[serde(skip_serializing_if = "Option::is_none")]
65        manual_section: Option<String>,
66    },
67    /// Read and parse one explicit local Markdown or roff file.
68    File {
69        /// Physical path supplied by the caller.
70        path: String,
71        /// Parser-selection policy for the file.
72        format: InputFormat,
73    },
74}
75
76/// Parser selected for an explicit physical input.
77#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
78#[serde(rename_all = "kebab-case")]
79pub enum InputFormat {
80    /// Infer the parser from extension and content conventions.
81    #[default]
82    Auto,
83    /// Parse the input as Markdown.
84    Markdown,
85    /// Parse the input as roff with libmandoc.
86    Roff,
87}
88
89/// Projection requested after loading one complete structured document.
90#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
91#[serde(
92    tag = "kind",
93    rename_all = "kebab-case",
94    rename_all_fields = "camelCase",
95    deny_unknown_fields
96)]
97pub enum QueryView {
98    /// Return the complete structured query bundle.
99    Full {},
100    /// Return a navigable structural projection.
101    Outline {
102        /// Semantic entry material included beneath structural nodes.
103        #[serde(default)]
104        entries: EntryProjection,
105        /// Optional section or entry used as the outline root.
106        #[schemars(length(min = 1, max = MAX_SEMANTIC_ENTRY_CHARS))]
107        #[serde(skip_serializing_if = "Option::is_none")]
108        root: Option<NodeSelector>,
109    },
110    /// Return content selected by one or more node paths, IDs, or aliases.
111    Excerpt {
112        /// Ordered selectors resolved by the engine.
113        #[schemars(length(min = 1, max = MAX_NODE_SELECTORS))]
114        selectors: Vec<NodeSelector>,
115    },
116    /// Resolve exactly one semantic entry and return its complete description.
117    Explain {
118        /// Exact or normalized semantic entry name.
119        #[schemars(length(min = 1, max = MAX_SEMANTIC_ENTRY_CHARS))]
120        entry: String,
121    },
122    /// Search visible document content with bounded pagination.
123    Search {
124        /// Literal or regular-expression search pattern.
125        #[schemars(length(min = 1, max = 4096))]
126        pattern: String,
127        /// Pattern language.
128        #[serde(default)]
129        syntax: SearchSyntax,
130        /// Case-matching policy.
131        #[serde(default)]
132        case: SearchCase,
133        /// Semantic content included in the search.
134        #[serde(default)]
135        scope: SearchScope,
136        /// Require matches to be bounded by word boundaries.
137        #[serde(default)]
138        word: bool,
139        /// Neighboring rendered lines included around each match.
140        #[serde(default)]
141        #[schemars(range(max = 100))]
142        context_lines: u16,
143        /// Maximum number of matches returned.
144        #[serde(default = "default_search_limit")]
145        #[schemars(range(min = 1, max = 10000))]
146        limit: u32,
147        /// Number of matching results skipped before collection.
148        #[serde(default)]
149        offset: u32,
150    },
151}
152
153/// Native use-case input. The engine validates semantic constraints before I/O.
154#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
155#[serde(rename_all = "camelCase", deny_unknown_fields)]
156#[schemars(extend("$id" = "urn:mant:request:v0.10"))]
157pub struct QueryRequest {
158    /// Exact request schema discriminator.
159    pub schema: RequestSchema,
160    /// Document source to resolve.
161    pub input: QueryInput,
162    /// Projection applied after the document is loaded.
163    pub view: QueryView,
164}
165
166/// Versioned full-query result emitted at CLI and request JSON boundaries.
167#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
168#[serde(rename_all = "camelCase")]
169#[schemars(extend("$id" = "urn:mant:query:v0.10"))]
170pub struct QueryBundle {
171    /// Exact response schema discriminator.
172    pub schema: QuerySchema,
173    /// Human-readable selected-document label.
174    pub label: String,
175    /// Exact registered address selected for this query. Direct input paths
176    /// and standard input do not belong to the registered catalog.
177    #[serde(skip_serializing_if = "Option::is_none")]
178    pub address: Option<DocumentAddress>,
179    /// Authoritative structured document, when found.
180    #[serde(skip_serializing_if = "Option::is_none")]
181    pub document: Option<DocumentResponse>,
182    /// Optional quick-reference page resolved alongside the document.
183    #[serde(skip_serializing_if = "Option::is_none")]
184    pub tldr: Option<TldrDocument>,
185}
186
187impl From<&ResolvedContent> for QueryBundle {
188    fn from(content: &ResolvedContent) -> Self {
189        Self {
190            schema: QuerySchema::V0Dot10,
191            label: content.label.clone(),
192            address: content.address.clone(),
193            document: content.document.as_ref().map(Into::into),
194            tldr: content.tldr.clone(),
195        }
196    }
197}
198
199impl From<QueryBundle> for ResolvedContent {
200    fn from(bundle: QueryBundle) -> Self {
201        Self {
202            label: bundle.label,
203            address: bundle.address,
204            document: bundle.document.map(Into::into),
205            tldr: bundle.tldr,
206        }
207    }
208}