use mant_protocol::{
CatalogDocumentKind, CatalogQuery, ContentSelector, DocumentScope, DocumentSelector,
DocumentTraversal, EntryProjection, QueryInput, QueryRequest, QueryView, SearchCase,
SearchSyntax,
};
use schemars::JsonSchema;
use serde::{Deserialize, de::DeserializeOwned};
pub(super) const MAX_DOCUMENT_CHARS: usize = mant_protocol::MAX_DOCUMENT_SELECTOR_CHARS;
pub(super) const MAX_SELECTORS: usize = mant_protocol::MAX_NODE_SELECTORS;
pub(super) const DEFAULT_FIND_RESULTS: u32 = 50;
pub(super) const DEFAULT_SEARCH_MATCHES: u32 = 20;
pub(super) const MAX_FIND_RESULTS: u32 = 10_000;
pub(super) const MAX_SEARCH_MATCHES: u32 = 100;
pub(super) const DEFAULT_PAGE_CHARS: u32 = 16 * 1024;
pub(super) const MAX_PAGE_CHARS: u32 = 32 * 1024;
pub(super) const MAX_FIND_QUERY_CHARS: usize = 1024;
const MAX_SOURCE_CHARS: usize = 128;
pub(super) const MAX_MANUAL_SECTION_CHARS: usize = 32;
const MAX_PATTERN_CHARS: usize = mant_protocol::MAX_SEARCH_PATTERN_CHARS;
#[derive(Debug, Default, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub(super) struct FindParams {
#[schemars(length(min = 1, max = 1024))]
pub(super) query: Option<String>,
pub(super) syntax: Option<SearchSyntax>,
pub(super) case: Option<SearchCase>,
pub(super) kind: Option<CatalogDocumentKind>,
#[schemars(length(min = 1, max = 128))]
pub(super) source: Option<String>,
#[schemars(length(min = 1, max = 32))]
pub(super) manual_section: Option<String>,
#[schemars(range(min = 1, max = 10_000))]
#[serde(default, deserialize_with = "deserialize_compat_optional_scalar")]
pub(super) max_results: Option<u32>,
#[serde(default, deserialize_with = "deserialize_compat_scalar")]
pub(super) offset: u32,
#[serde(default, deserialize_with = "deserialize_compat_scalar")]
pub(super) start_char: u32,
#[schemars(range(min = 1, max = 32_768))]
#[serde(default, deserialize_with = "deserialize_compat_optional_scalar")]
pub(super) max_chars: Option<u32>,
}
#[derive(Debug, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub(super) struct OutlineParams {
#[schemars(length(min = 1, max = 1024))]
pub(super) document: String,
pub(super) entries: Option<EntryProjection>,
pub(super) root: Option<ContentSelector>,
#[serde(default)]
pub(super) references: mant_protocol::ReferenceProjection,
#[serde(default, deserialize_with = "deserialize_compat_scalar")]
pub(super) start_char: u32,
#[schemars(range(min = 1, max = 32_768))]
#[serde(default, deserialize_with = "deserialize_compat_optional_scalar")]
pub(super) max_chars: Option<u32>,
}
#[derive(Debug, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub(super) struct ReadParams {
#[schemars(length(min = 1, max = 1024))]
pub(super) document: String,
#[schemars(length(min = 1, max = 16))]
#[serde(deserialize_with = "deserialize_selectors")]
pub(super) selectors: Vec<ContentSelector>,
#[serde(default, deserialize_with = "deserialize_compat_scalar")]
pub(super) start_char: u32,
#[schemars(range(min = 1, max = 32_768))]
#[serde(default, deserialize_with = "deserialize_compat_optional_scalar")]
pub(super) max_chars: Option<u32>,
}
#[derive(Debug, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub(super) struct ExplainParams {
#[schemars(length(min = 1, max = 16), inner(length(min = 1, max = 1024)))]
#[serde(deserialize_with = "deserialize_documents")]
pub(super) documents: Vec<String>,
#[serde(default, deserialize_with = "deserialize_compat_scalar")]
pub(super) follow_links: bool,
#[schemars(range(max = 32))]
#[serde(default, deserialize_with = "deserialize_compat_optional_scalar")]
pub(super) max_depth: Option<u16>,
#[schemars(range(min = 1, max = 256))]
#[serde(default, deserialize_with = "deserialize_compat_optional_scalar")]
pub(super) max_documents: Option<u32>,
#[schemars(length(min = 1, max = 512))]
pub(super) entry: String,
#[serde(
default = "mant_protocol::default_explanation_limit",
deserialize_with = "deserialize_compat_scalar"
)]
#[schemars(range(min = 1, max = 256))]
pub(super) max_results: u32,
#[serde(default, deserialize_with = "deserialize_compat_scalar")]
pub(super) offset: u32,
#[serde(
default = "mant_protocol::default_explanation_content_bytes",
deserialize_with = "deserialize_compat_scalar"
)]
#[schemars(range(min = 1, max = 4_194_304))]
pub(super) content_bytes: u32,
#[serde(default, deserialize_with = "deserialize_compat_scalar")]
pub(super) start_char: u32,
#[schemars(range(min = 1, max = 32_768))]
#[serde(default, deserialize_with = "deserialize_compat_optional_scalar")]
pub(super) max_chars: Option<u32>,
}
#[derive(Debug, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub(super) struct SearchParams {
#[schemars(length(min = 1, max = 16), inner(length(min = 1, max = 1024)))]
#[serde(deserialize_with = "deserialize_documents")]
pub(super) documents: Vec<String>,
#[serde(default, deserialize_with = "deserialize_compat_scalar")]
pub(super) follow_links: bool,
#[schemars(range(max = 32))]
#[serde(default, deserialize_with = "deserialize_compat_optional_scalar")]
pub(super) max_depth: Option<u16>,
#[schemars(range(min = 1, max = 256))]
#[serde(default, deserialize_with = "deserialize_compat_optional_scalar")]
pub(super) max_documents: Option<u32>,
#[schemars(length(min = 1, max = 4096))]
pub(super) pattern: String,
pub(super) syntax: Option<SearchSyntax>,
pub(super) case: Option<SearchCase>,
pub(super) scope: Option<mant_protocol::SearchScope>,
#[serde(default, deserialize_with = "deserialize_compat_scalar")]
pub(super) word: bool,
#[serde(default, deserialize_with = "deserialize_compat_scalar")]
#[schemars(range(max = 5))]
pub(super) context_lines: u16,
#[schemars(range(min = 1, max = 100))]
#[serde(default, deserialize_with = "deserialize_compat_optional_scalar")]
pub(super) max_matches: Option<u32>,
#[serde(default, deserialize_with = "deserialize_compat_scalar")]
pub(super) offset: u32,
#[serde(default, deserialize_with = "deserialize_compat_scalar")]
pub(super) start_char: u32,
#[schemars(range(min = 1, max = 32_768))]
#[serde(default, deserialize_with = "deserialize_compat_optional_scalar")]
pub(super) max_chars: Option<u32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct PageRequest {
pub(super) start_char: u32,
pub(super) max_chars: u32,
}
pub(super) struct ValidatedFindParams {
pub(super) query: Option<String>,
pub(super) syntax: SearchSyntax,
pub(super) case: SearchCase,
pub(super) kind: Option<CatalogDocumentKind>,
pub(super) source: Option<String>,
pub(super) manual_section: Option<String>,
pub(super) max_results: u32,
pub(super) offset: u32,
pub(super) page: PageRequest,
}
pub(super) struct ValidatedOutlineParams {
pub(super) document: String,
pub(super) entries: EntryProjection,
pub(super) root: Option<ContentSelector>,
pub(super) references: mant_protocol::ReferenceProjection,
pub(super) page: PageRequest,
}
pub(super) struct ValidatedReadParams {
pub(super) document: String,
pub(super) selectors: Vec<ContentSelector>,
pub(super) page: PageRequest,
}
pub(super) struct ValidatedExplainParams {
pub(super) scope: DocumentScope,
pub(super) entry: String,
pub(super) options: mant_protocol::ExplanationOptions,
pub(super) page: PageRequest,
}
pub(super) struct ValidatedSearchParams {
pub(super) documents: DocumentScope,
pub(super) pattern: String,
pub(super) syntax: SearchSyntax,
pub(super) case: SearchCase,
pub(super) scope: mant_protocol::SearchScope,
pub(super) word: bool,
pub(super) context_lines: u16,
pub(super) max_matches: u32,
pub(super) offset: u32,
pub(super) page: PageRequest,
}
fn deserialize_documents<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
where
D: serde::Deserializer<'de>,
{
deserialize_compat_list(deserializer, "documents")
}
fn deserialize_selectors<'de, D>(deserializer: D) -> Result<Vec<ContentSelector>, D::Error>
where
D: serde::Deserializer<'de>,
{
deserialize_compat_list(deserializer, "selectors")
}
fn deserialize_compat_list<'de, D, T>(deserializer: D, field: &str) -> Result<Vec<T>, D::Error>
where
D: serde::Deserializer<'de>,
T: DeserializeOwned,
{
use serde::de::Error as _;
let value = serde_json::Value::deserialize(deserializer)?;
let value = match value {
serde_json::Value::String(text) => match serde_json::from_str(&text) {
Ok(parsed @ serde_json::Value::Array(_)) => parsed,
_ => serde_json::Value::Array(vec![serde_json::Value::String(text)]),
},
array @ serde_json::Value::Array(_) => array,
item => serde_json::Value::Array(vec![item]),
};
serde_json::from_value(value).map_err(|error| {
D::Error::custom(format!(
"invalid {field}: {error}; use a JSON array of {}",
if field == "selectors" {
"closed path/id selector objects"
} else {
"document selector strings"
}
))
})
}
fn deserialize_compat_scalar<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
D: serde::Deserializer<'de>,
T: DeserializeOwned + Default + std::str::FromStr,
T::Err: std::fmt::Display,
{
use serde::de::Error as _;
let value = serde_json::Value::deserialize(deserializer)?;
match value {
serde_json::Value::Null => Err(D::Error::custom(
"null is not accepted for a required scalar; omit the field to use its default",
)),
serde_json::Value::String(text) => text
.trim()
.to_ascii_lowercase()
.parse()
.map_err(|_| D::Error::custom("invalid stringified scalar value")),
other => serde_json::from_value(other).map_err(D::Error::custom),
}
}
fn deserialize_compat_optional_scalar<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
where
D: serde::Deserializer<'de>,
T: DeserializeOwned + std::str::FromStr,
T::Err: std::fmt::Display,
{
use serde::de::Error as _;
let value = serde_json::Value::deserialize(deserializer)?;
match value {
serde_json::Value::Null => Ok(None),
serde_json::Value::String(text) => text
.trim()
.to_ascii_lowercase()
.parse()
.map(Some)
.map_err(|_| D::Error::custom("invalid stringified scalar value")),
other => serde_json::from_value(other)
.map(Some)
.map_err(D::Error::custom),
}
}
impl FindParams {
pub(super) fn validate(self) -> Result<ValidatedFindParams, String> {
let query = optional_normalized(self.query, "query", MAX_FIND_QUERY_CHARS)?;
let source = optional_normalized(self.source, "source", MAX_SOURCE_CHARS)?;
let manual_section = optional_normalized(
self.manual_section,
"manualSection",
MAX_MANUAL_SECTION_CHARS,
)?;
let max_results = validate_result_limit(
self.max_results,
DEFAULT_FIND_RESULTS,
MAX_FIND_RESULTS,
"maxResults",
)?;
let page = validate_page(self.start_char, self.max_chars)?;
if source.is_some() && manual_section.is_some() {
return Err("source and manualSection cannot be combined".to_owned());
}
Ok(ValidatedFindParams {
query,
syntax: self.syntax.unwrap_or_default(),
case: self.case.unwrap_or_default(),
kind: self.kind,
source,
manual_section,
max_results,
offset: self.offset,
page,
})
}
}
impl OutlineParams {
pub(super) fn validate(self) -> Result<ValidatedOutlineParams, String> {
self.references.validate().map_err(str::to_owned)?;
Ok(ValidatedOutlineParams {
document: bounded_normalized(&self.document, "document", MAX_DOCUMENT_CHARS)?,
entries: self.entries.unwrap_or_default(),
root: self.root.map(validate_selector).transpose()?,
references: self.references,
page: validate_page(self.start_char, self.max_chars)?,
})
}
}
fn validate_selector(selector: ContentSelector) -> Result<ContentSelector, String> {
selector.validate().map_err(|error| error.to_string())?;
Ok(selector)
}
impl ReadParams {
pub(super) fn validate(self) -> Result<ValidatedReadParams, String> {
if self.selectors.is_empty() || self.selectors.len() > MAX_SELECTORS {
return Err(format!(
"selectors must contain between 1 and {MAX_SELECTORS} values"
));
}
let selectors = self
.selectors
.into_iter()
.map(validate_selector)
.collect::<Result<_, _>>()?;
Ok(ValidatedReadParams {
document: bounded_normalized(&self.document, "document", MAX_DOCUMENT_CHARS)?,
selectors,
page: validate_page(self.start_char, self.max_chars)?,
})
}
}
impl ExplainParams {
pub(super) fn validate(self) -> Result<ValidatedExplainParams, String> {
let options = mant_protocol::ExplanationOptions {
limit: self.max_results,
offset: self.offset,
content_bytes: self.content_bytes,
};
let entry = bounded_normalized(
&self.entry,
"entry",
mant_protocol::MAX_SEMANTIC_ENTRY_CHARS,
)?;
mant_query::validate_explanation_query(&mant_protocol::ExplanationQuery {
entry: entry.clone(),
options,
})
.map_err(|error| error.to_string())?;
Ok(ValidatedExplainParams {
options,
scope: validate_scope(
self.documents,
self.follow_links,
self.max_depth,
self.max_documents,
)?,
entry,
page: validate_page(self.start_char, self.max_chars)?,
})
}
}
impl SearchParams {
pub(super) fn validate(self) -> Result<ValidatedSearchParams, String> {
if self.context_lines > 5 {
return Err("contextLines must be between 0 and 5".to_owned());
}
let max_matches = validate_result_limit(
self.max_matches,
DEFAULT_SEARCH_MATCHES,
MAX_SEARCH_MATCHES,
"maxMatches",
)?;
Ok(ValidatedSearchParams {
documents: validate_scope(
self.documents,
self.follow_links,
self.max_depth,
self.max_documents,
)?,
pattern: bounded_exact(&self.pattern, "pattern", MAX_PATTERN_CHARS)?,
syntax: self.syntax.unwrap_or_default(),
case: self.case.unwrap_or_default(),
scope: self.scope.unwrap_or_default(),
word: self.word,
context_lines: self.context_lines,
max_matches,
offset: self.offset,
page: validate_page(self.start_char, self.max_chars)?,
})
}
}
fn validate_result_limit(
value: Option<u32>,
default: u32,
maximum: u32,
field: &str,
) -> Result<u32, String> {
let value = value.unwrap_or(default);
if !(1..=maximum).contains(&value) {
return Err(format!("{field} must be between 1 and {maximum}"));
}
Ok(value)
}
fn validate_page(start_char: u32, max_chars: Option<u32>) -> Result<PageRequest, String> {
let max_chars = max_chars.unwrap_or(DEFAULT_PAGE_CHARS);
if !(1..=MAX_PAGE_CHARS).contains(&max_chars) {
return Err(format!("maxChars must be between 1 and {MAX_PAGE_CHARS}"));
}
Ok(PageRequest {
start_char,
max_chars,
})
}
fn validate_scope(
documents: Vec<String>,
follow_links: bool,
max_depth: Option<u16>,
max_documents: Option<u32>,
) -> Result<DocumentScope, String> {
if documents.is_empty() || documents.len() > mant_protocol::MAX_SCOPE_DOCUMENTS {
return Err(format!(
"documents must contain between 1 and {} values",
mant_protocol::MAX_SCOPE_DOCUMENTS
));
}
if !follow_links && (max_depth.is_some() || max_documents.is_some()) {
return Err("maxDepth and maxDocuments require followLinks=true".to_owned());
}
let documents = documents
.into_iter()
.map(|document| {
bounded_normalized(&document, "document", MAX_DOCUMENT_CHARS).map(|selector| {
DocumentSelector {
selector,
source: None,
manual_section: None,
}
})
})
.collect::<Result<Vec<_>, _>>()?;
let effective_max_documents =
max_documents.unwrap_or(mant_protocol::DEFAULT_SCOPE_DOCUMENT_LIMIT);
if effective_max_documents < u32::try_from(documents.len()).unwrap_or(u32::MAX)
|| effective_max_documents > mant_protocol::MAX_SCOPE_DOCUMENT_LIMIT
{
return Err(format!(
"maxDocuments must include every initial document and not exceed {}",
mant_protocol::MAX_SCOPE_DOCUMENT_LIMIT
));
}
let effective_max_depth = max_depth.unwrap_or(mant_protocol::DEFAULT_SCOPE_DEPTH);
if effective_max_depth > mant_protocol::MAX_SCOPE_DEPTH {
return Err(format!(
"maxDepth must not exceed {}",
mant_protocol::MAX_SCOPE_DEPTH
));
}
Ok(DocumentScope {
documents,
traversal: DocumentTraversal {
follow_links,
max_depth,
max_documents,
},
})
}
pub(super) fn catalog_query(parameters: &ValidatedFindParams) -> CatalogQuery {
CatalogQuery {
pattern: parameters.query.clone(),
syntax: parameters.syntax,
case: parameters.case,
kind: parameters.kind,
source: parameters.source.clone(),
manual_section: parameters.manual_section.clone(),
limit: parameters.max_results,
offset: parameters.offset,
}
}
pub(super) fn request_for(document: String, view: QueryView) -> QueryRequest {
QueryRequest {
schema: mant_protocol::RequestSchema::V0Dot11,
input: QueryInput::Document {
selector: document,
source: None,
manual_section: None,
},
view,
}
}
fn optional_normalized(
value: Option<String>,
field: &str,
max: usize,
) -> Result<Option<String>, String> {
value
.map(|value| bounded_normalized(&value, field, max))
.transpose()
}
fn bounded_normalized(value: &str, field: &str, max: usize) -> Result<String, String> {
let value = value.trim();
bounded_exact(value, field, max)
}
fn bounded_exact(value: &str, field: &str, max: usize) -> Result<String, String> {
if value.is_empty() {
return Err(format!("{field} must not be empty"));
}
if value.chars().count() > max {
return Err(format!(
"{field} must not exceed {max} Unicode scalar values"
));
}
if value.chars().any(char::is_control) {
return Err(format!("{field} must not contain control characters"));
}
Ok(value.to_owned())
}