use topcoat::{
context::Cx,
router::{Body, RouteFuture, error::forbidden},
view::*,
};
use super::super::gate::gate;
use crate::{
error::TabloError,
query_term::clamp_query_term,
resource::Resource,
schema::{OptionLoadError, Schema, option_view},
};
pub(crate) fn resource_options<R: Resource>(cx: &Cx, _body: Body) -> RouteFuture<'_> {
Box::pin(async move {
let resource = gate::<R>(cx)?;
options_response(cx, &resource.form).await
})
}
async fn options_response(cx: &Cx, form: &Schema) -> topcoat::Result<http::Response<Body>> {
let (field, q) = options_query(cx);
let field = field.trim();
if field.is_empty() {
return Err(topcoat::router::error::bad_request("missing field").into());
}
let q = clamp_query_term(&q);
let Some(select) = form
.fields()
.find(|declared| declared.name() == field)
.and_then(|declared| declared.as_choice())
else {
return Err(topcoat::router::error::bad_request("unknown field").into());
};
if !select.is_relationship() {
return Err(topcoat::router::error::bad_request("not a relationship select").into());
}
if !select.is_searchable() {
return Err(topcoat::router::error::bad_request("not searchable").into());
}
match select.search_options(cx, &q).await {
Ok(opts) => {
let options: Vec<_> = opts
.into_iter()
.map(|(value, label)| option_view(cx, value, label, false))
.collect();
let html = view! {
cx =>
for option in options {
(option)
}
}
.single()
.await?
.render(cx);
let res = http::Response::builder()
.status(200)
.header(http::header::CONTENT_TYPE, "text/html; charset=utf-8")
.header("x-content-type-options", "nosniff")
.body(Body::from(html))?;
Ok(res)
}
Err(OptionLoadError::Denied) => Err(forbidden().into()),
Err(OptionLoadError::LoadFailed) => {
Err(TabloError::Infrastructure("option search failed").into())
}
Err(OptionLoadError::Misdeclared) => Err(TabloError::Declaration(
"option search unavailable: the related resource requires a tenant the framework \
cannot scope"
.to_string(),
)
.into()),
Err(OptionLoadError::Overflow) => {
let html =
"<option value=\"\" disabled>Too many results — keep typing</option>".to_string();
let res = http::Response::builder()
.status(200)
.header(http::header::CONTENT_TYPE, "text/html; charset=utf-8")
.header("x-content-type-options", "nosniff")
.body(Body::from(html))?;
Ok(res)
}
}
}
fn options_query(cx: &Cx) -> (String, String) {
let Some(parts) = topcoat::context::try_request_context::<http::request::Parts>(cx) else {
return (String::new(), String::new());
};
let query = parts.uri.query().unwrap_or("");
let mut field = String::new();
let mut q = String::new();
let mut seen_field = false;
let mut seen_q = false;
for (k, v) in form_urlencoded::parse(query.as_bytes()) {
if k == "field" && !seen_field {
field = v.into_owned();
seen_field = true;
} else if k == "q" && !seen_q {
q = v.into_owned();
seen_q = true;
}
}
(field, q)
}
#[cfg(test)]
mod tests;