use std::{any::type_name, convert::Infallible, marker::PhantomData};
use http::{StatusCode, header::LOCATION};
use tablo_core::{Action, FormField, Notification, PanelHandle, RecordForm, Resource, url};
use topcoat::context::Cx;
use crate::{
TestClient,
protocol::{
EmptyTable, ErrorKind, Group, Row, body_string, declined_message, dirty_form, empty_table,
field_error, field_error_message, form_body, groups, input_value, notification_in,
response_cookies, row_keys, rows, stale_form,
},
};
pub type FieldOf<R> = <<R as Resource>::Form as RecordForm>::Field;
pub struct ResourceClient<'c, 'a, R> {
client: &'c TestClient<'a>,
panel: &'c PanelHandle,
resource: PhantomData<fn() -> R>,
}
impl<'c, 'a, R: Resource> ResourceClient<'c, 'a, R> {
pub(crate) fn new(client: &'c TestClient<'a>, panel: &'c PanelHandle) -> Self {
Self {
client,
panel,
resource: PhantomData,
}
}
pub fn url(&self) -> String {
resolved::<R>(url::resource::<R>(&self.cx()), "a list")
}
pub async fn list(&self) -> Reply<'c, 'a, FieldOf<R>> {
self.get(self.url()).await
}
pub async fn list_with(&self, query: &str) -> Reply<'c, 'a, FieldOf<R>> {
self.get(format!("{}?{query}", self.url())).await
}
pub async fn create_page(&self) -> Reply<'c, 'a, FieldOf<R>> {
self.get(self.create_url()).await
}
pub fn create(&self) -> Submission<'c, 'a, FieldOf<R>> {
Submission::new(self.client, self.create_url(), R::Form::fields())
}
pub async fn view(&self, record: &R::Model) -> Reply<'c, 'a, FieldOf<R>> {
let url = resolved::<R>(url::record::<R>(&self.cx(), record), "a detail page");
self.get(url).await
}
pub async fn edit_page(&self, record: &R::Model) -> Reply<'c, 'a, FieldOf<R>> {
self.get(self.edit_url(record)).await
}
pub fn edit(&self, record: &R::Model) -> Submission<'c, 'a, FieldOf<R>> {
Submission::new(self.client, self.edit_url(record), R::Form::fields())
}
pub fn delete(&self, record: &R::Model) -> Submission<'c, 'a, Infallible> {
let url = resolved::<R>(url::delete::<R>(&self.cx(), record), "a delete route");
Submission::new(self.client, url, Vec::new()).set_key(CONFIRM, "1")
}
pub fn action<A: Action<R>>(&self, record: &R::Model) -> Submission<'c, 'a, Infallible> {
let url = url::action::<R, A>(&self.cx(), record).unwrap_or_else(|| {
panic!(
"`{}` declares no record action `{}`",
type_name::<R>(),
A::NAME
)
});
Submission::new(self.client, url, Vec::new()).set_key(CONFIRM, "1")
}
pub fn bulk_action<A: Action<R>>(
&self,
keys: impl IntoIterator<Item = impl AsRef<str>>,
) -> Submission<'c, 'a, Infallible> {
let url = url::bulk_action::<R, A>(&self.cx()).unwrap_or_else(|| {
panic!(
"`{}` declares no bulk action `{}`",
type_name::<R>(),
A::NAME
)
});
let ids = keys
.into_iter()
.map(|key| key.as_ref().to_string())
.collect::<Vec<_>>()
.join(",");
Submission::new(self.client, url, Vec::new())
.set_key(IDS, &ids)
.set_key(CONFIRM, "1")
}
pub fn bulk_action_matching<A: Action<R>>(
&self,
query: &str,
) -> Submission<'c, 'a, Infallible> {
let url = url::bulk_action::<R, A>(&self.cx()).unwrap_or_else(|| {
panic!(
"`{}` declares no bulk action `{}`",
type_name::<R>(),
A::NAME
)
});
Submission::new(self.client, url, Vec::new())
.set_key(MATCHING, query)
.set_key(CONFIRM, "1")
}
fn create_url(&self) -> String {
resolved::<R>(url::create::<R>(&self.cx()), "a create page")
}
fn edit_url(&self, record: &R::Model) -> String {
resolved::<R>(url::edit::<R>(&self.cx(), record), "an edit page")
}
fn cx(&self) -> Cx {
self.panel.enter(&self.panel.context())
}
async fn get(&self, url: String) -> Reply<'c, 'a, FieldOf<R>> {
Reply::read(self.client, self.client.get(&url).await, R::Form::fields()).await
}
}
const CONFIRM: &str = "confirm";
const IDS: &str = "ids";
const MATCHING: &str = "matching";
const CREATE_ANOTHER: &str = "create_another";
fn resolved<R>(url: Option<String>, what: &str) -> String {
url.unwrap_or_else(|| panic!("the panel serves `{}` no {what}", type_name::<R>()))
}
#[must_use = "a submission posts nothing until `submit`"]
pub struct Submission<'c, 'a, K> {
client: &'c TestClient<'a>,
url: String,
fields: Vec<FormField<K>>,
pairs: Vec<(String, String)>,
}
impl<'c, 'a, K: PartialEq> Submission<'c, 'a, K> {
fn new(client: &'c TestClient<'a>, url: String, fields: Vec<FormField<K>>) -> Self {
Self {
client,
url,
fields,
pairs: Vec::new(),
}
}
pub fn url(&self) -> &str {
&self.url
}
pub fn set(self, field: K, value: &str) -> Self {
let key = single_key(&self.fields, &field).to_string();
self.set_key(&key, value)
}
pub fn add(mut self, field: K, value: &str) -> Self {
let key = single_key(&self.fields, &field).to_string();
self.pairs.push((key, value.to_string()));
self
}
pub fn set_key(mut self, key: &str, value: &str) -> Self {
self.pairs.retain(|(kept, _)| kept != key);
self.pairs.push((key.to_string(), value.to_string()));
self
}
pub fn create_another(self) -> Self {
self.set_key(CREATE_ANOTHER, "1")
}
pub fn picking(mut self) -> Self {
let joint = if self.url.contains('?') { '&' } else { '?' };
self.url = format!("{}{joint}pick=1", self.url);
self
}
pub async fn submit(self) -> Reply<'c, 'a, K> {
let pairs: Vec<(&str, &str)> = self
.pairs
.iter()
.map(|(key, value)| (key.as_str(), value.as_str()))
.collect();
let response = self.client.submit(&self.url, &form_body(&pairs)).await;
Reply::read(self.client, response, self.fields).await
}
}
fn single_key<'f, K: PartialEq>(fields: &'f [FormField<K>], field: &K) -> &'f str {
let field = fields
.iter()
.find(|candidate| candidate.field == *field)
.expect("a field enum's variant is one of its form's fields");
match field.keys.as_slice() {
[key] => key,
keys => panic!(
"`{}` binds the keys {keys:?}: post each with `set_key`",
field.name
),
}
}
pub struct Reply<'c, 'a, K> {
client: &'c TestClient<'a>,
fields: Vec<FormField<K>>,
status: StatusCode,
location: Option<String>,
cookies: Vec<(String, String)>,
html: String,
}
impl<'c, 'a, K: PartialEq> Reply<'c, 'a, K> {
async fn read(
client: &'c TestClient<'a>,
response: http::Response<topcoat::router::Body>,
fields: Vec<FormField<K>>,
) -> Self {
let location = response
.headers()
.get(LOCATION)
.and_then(|value| value.to_str().ok())
.map(str::to_string);
let cookies = response_cookies(&response);
Self {
client,
fields,
status: response.status(),
location,
cookies,
html: body_string(response).await,
}
}
pub fn status(&self) -> StatusCode {
self.status
}
pub fn location(&self) -> Option<&str> {
self.location.as_deref()
}
pub fn cookies(&self) -> &[(String, String)] {
&self.cookies
}
pub fn notification(&self) -> Option<Notification> {
notification_in(&self.cookies)
}
pub fn html(&self) -> &str {
&self.html
}
pub async fn follow(&self) -> Reply<'c, 'a, K>
where
K: Clone,
{
let location = self
.location
.as_deref()
.unwrap_or_else(|| panic!("a {} response redirects nowhere", self.status));
let response = self.client.cookies(&self.cookies).get(location).await;
Reply::read(self.client, response, self.fields.clone()).await
}
pub fn picked(&self) -> Option<crate::Picked> {
crate::picked(&self.html)
}
pub fn create_option_links(&self) -> Vec<crate::CreateOptionLink> {
crate::create_option_links(&self.html)
}
pub fn rows(&self) -> Vec<Row> {
rows(&self.html)
}
pub fn row_keys(&self) -> Vec<String> {
row_keys(&self.html)
}
pub fn groups(&self) -> Vec<Group> {
groups(&self.html)
}
pub fn empty_table(&self) -> Option<EmptyTable> {
empty_table(&self.html)
}
pub fn tabs(&self) -> Vec<crate::Tab> {
crate::tabs(&self.html)
}
pub fn wizard_steps(&self) -> Vec<crate::WizardStep> {
crate::wizard_steps(&self.html)
}
pub fn sections(&self) -> Vec<crate::Section> {
crate::sections(&self.html)
}
pub fn stale(&self) -> bool {
stale_form(&self.html)
}
pub fn declined(&self) -> Option<String> {
declined_message(&self.html)
}
pub fn dirty(&self) -> bool {
dirty_form(&self.html)
}
pub fn field_error(&self, field: K) -> Option<ErrorKind> {
field_error(&self.html, self.first_key(&field))
}
pub fn field_error_message(&self, field: K) -> Option<String> {
field_error_message(&self.html, self.first_key(&field))
}
pub fn input_value(&self, field: K) -> Option<String> {
input_value(&self.html, self.first_key(&field))
}
fn first_key(&self, field: &K) -> &str {
self.fields
.iter()
.find(|candidate| candidate.field == *field)
.and_then(|field| field.keys.first())
.expect("a field enum's variant binds a key of its form")
}
}