use topcoat::{
context::Cx,
router::{
Body, RouteFuture,
error::{forbidden, not_found},
path_param_segment,
},
view::*,
};
use super::{super::gate::gate, mutation::header_allowed};
use crate::{
error::TabloError,
resource::{InputSpec, Resource},
schema::{OptionLoadError, Schema, option_view},
table::clamp_query_term,
};
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
})
}
pub(crate) fn resource_action_options<R: Resource>(cx: &Cx, _body: Body) -> RouteFuture<'_> {
Box::pin(async move {
let resource = gate::<R>(cx)?;
let name = path_param_segment(cx, "action");
let input = if let Some(header) = resource.header_actions.find(name) {
if !header_allowed(cx, &resource, header) {
return Err(forbidden().into());
}
header.input
} else if let Some(action) = resource.actions.find(name) {
if !resource.can(cx, action.resource_wide) {
return Err(forbidden().into());
}
action.input
} else {
return Err(not_found().into());
};
input_options(cx, input).await
})
}
pub(crate) async fn input_options(
cx: &Cx,
spec: InputSpec,
) -> topcoat::Result<http::Response<Body>> {
options_response(cx, &(spec.schema)()).await
}
async fn options_response(cx: &Cx, form: &Schema) -> topcoat::Result<http::Response<Body>> {
let OptionsQuery { field, q, parent } = 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_multiple() || (!select.is_searchable() && select.parent_key().is_none()) {
return Err(topcoat::router::error::bad_request("not searchable").into());
}
match select.search_options(cx, &q, Some(&parent)).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")
.header("x-options-overflow", "true")
.body(Body::from(html))?;
Ok(res)
}
}
}
#[derive(Default)]
struct OptionsQuery {
field: String,
q: String,
parent: String,
}
fn options_query(cx: &Cx) -> OptionsQuery {
let mut out = OptionsQuery::default();
let Some(parts) = topcoat::context::try_request_context::<http::request::Parts>(cx) else {
return out;
};
let query = parts.uri.query().unwrap_or("");
let (mut seen_field, mut seen_q, mut seen_parent) = (false, false, false);
for (k, v) in form_urlencoded::parse(query.as_bytes()) {
let (slot, seen) = match k.as_ref() {
"field" => (&mut out.field, &mut seen_field),
"q" => (&mut out.q, &mut seen_q),
"parent" => (&mut out.parent, &mut seen_parent),
_ => continue,
};
if !*seen {
*slot = v.into_owned();
*seen = true;
}
}
out
}
#[cfg(test)]
mod tests;