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