1use std::{any::type_name, convert::Infallible, marker::PhantomData};
10
11use http::{StatusCode, header::LOCATION};
12use tablo_core::{Action, FormField, Notification, PanelHandle, RecordForm, Resource, url};
13use topcoat::context::Cx;
14
15use crate::{
16 TestClient,
17 protocol::{
18 EmptyTable, ErrorKind, Group, Row, body_string, declined_message, dirty_form, empty_table,
19 field_error, field_error_message, form_body, groups, input_value, notification_in,
20 response_cookies, row_keys, rows, stale_form,
21 },
22};
23
24pub type FieldOf<R> = <<R as Resource>::Form as RecordForm>::Field;
26
27pub struct ResourceClient<'c, 'a, R> {
48 client: &'c TestClient<'a>,
49 panel: &'c PanelHandle,
50 resource: PhantomData<fn() -> R>,
51}
52
53impl<'c, 'a, R: Resource> ResourceClient<'c, 'a, R> {
54 pub(crate) fn new(client: &'c TestClient<'a>, panel: &'c PanelHandle) -> Self {
55 Self {
56 client,
57 panel,
58 resource: PhantomData,
59 }
60 }
61
62 pub fn url(&self) -> String {
68 resolved::<R>(url::resource::<R>(&self.cx()), "a list")
69 }
70
71 pub async fn list(&self) -> Reply<'c, 'a, FieldOf<R>> {
73 self.get(self.url()).await
74 }
75
76 pub async fn list_with(&self, query: &str) -> Reply<'c, 'a, FieldOf<R>> {
78 self.get(format!("{}?{query}", self.url())).await
79 }
80
81 pub async fn create_page(&self) -> Reply<'c, 'a, FieldOf<R>> {
87 self.get(self.create_url()).await
88 }
89
90 pub fn create(&self) -> Submission<'c, 'a, FieldOf<R>> {
96 Submission::new(self.client, self.create_url(), R::Form::fields())
97 }
98
99 pub async fn view(&self, record: &R::Model) -> Reply<'c, 'a, FieldOf<R>> {
105 let url = resolved::<R>(url::record::<R>(&self.cx(), record), "a detail page");
106 self.get(url).await
107 }
108
109 pub async fn edit_page(&self, record: &R::Model) -> Reply<'c, 'a, FieldOf<R>> {
115 self.get(self.edit_url(record)).await
116 }
117
118 pub fn edit(&self, record: &R::Model) -> Submission<'c, 'a, FieldOf<R>> {
125 Submission::new(self.client, self.edit_url(record), R::Form::fields())
126 }
127
128 pub fn delete(&self, record: &R::Model) -> Submission<'c, 'a, Infallible> {
134 let url = resolved::<R>(url::delete::<R>(&self.cx(), record), "a delete route");
135 Submission::new(self.client, url, Vec::new()).set_key(CONFIRM, "1")
136 }
137
138 pub fn action<A: Action<R>>(&self, record: &R::Model) -> Submission<'c, 'a, Infallible> {
145 let url = url::action::<R, A>(&self.cx(), record).unwrap_or_else(|| {
146 panic!(
147 "`{}` declares no record action `{}`",
148 type_name::<R>(),
149 A::NAME
150 )
151 });
152 Submission::new(self.client, url, Vec::new()).set_key(CONFIRM, "1")
153 }
154
155 pub fn bulk_action<A: Action<R>>(
162 &self,
163 keys: impl IntoIterator<Item = impl AsRef<str>>,
164 ) -> Submission<'c, 'a, Infallible> {
165 let url = url::bulk_action::<R, A>(&self.cx()).unwrap_or_else(|| {
166 panic!(
167 "`{}` declares no bulk action `{}`",
168 type_name::<R>(),
169 A::NAME
170 )
171 });
172 let ids = keys
173 .into_iter()
174 .map(|key| key.as_ref().to_string())
175 .collect::<Vec<_>>()
176 .join(",");
177 Submission::new(self.client, url, Vec::new())
178 .set_key(IDS, &ids)
179 .set_key(CONFIRM, "1")
180 }
181
182 pub fn bulk_action_matching<A: Action<R>>(
190 &self,
191 query: &str,
192 ) -> Submission<'c, 'a, Infallible> {
193 let url = url::bulk_action::<R, A>(&self.cx()).unwrap_or_else(|| {
194 panic!(
195 "`{}` declares no bulk action `{}`",
196 type_name::<R>(),
197 A::NAME
198 )
199 });
200 Submission::new(self.client, url, Vec::new())
201 .set_key(MATCHING, query)
202 .set_key(CONFIRM, "1")
203 }
204
205 fn create_url(&self) -> String {
206 resolved::<R>(url::create::<R>(&self.cx()), "a create page")
207 }
208
209 fn edit_url(&self, record: &R::Model) -> String {
210 resolved::<R>(url::edit::<R>(&self.cx(), record), "an edit page")
211 }
212
213 fn cx(&self) -> Cx {
214 self.panel.enter(&self.panel.context())
215 }
216
217 async fn get(&self, url: String) -> Reply<'c, 'a, FieldOf<R>> {
218 Reply::read(self.client, self.client.get(&url).await, R::Form::fields()).await
219 }
220}
221
222const CONFIRM: &str = "confirm";
224
225const IDS: &str = "ids";
227
228const MATCHING: &str = "matching";
230
231const CREATE_ANOTHER: &str = "create_another";
233
234fn resolved<R>(url: Option<String>, what: &str) -> String {
235 url.unwrap_or_else(|| panic!("the panel serves `{}` no {what}", type_name::<R>()))
236}
237
238#[must_use = "a submission posts nothing until `submit`"]
244pub struct Submission<'c, 'a, K> {
245 client: &'c TestClient<'a>,
246 url: String,
247 fields: Vec<FormField<K>>,
248 pairs: Vec<(String, String)>,
249}
250
251impl<'c, 'a, K: PartialEq> Submission<'c, 'a, K> {
252 fn new(client: &'c TestClient<'a>, url: String, fields: Vec<FormField<K>>) -> Self {
253 Self {
254 client,
255 url,
256 fields,
257 pairs: Vec::new(),
258 }
259 }
260
261 pub fn url(&self) -> &str {
263 &self.url
264 }
265
266 pub fn set(self, field: K, value: &str) -> Self {
273 let key = single_key(&self.fields, &field).to_string();
274 self.set_key(&key, value)
275 }
276
277 pub fn add(mut self, field: K, value: &str) -> Self {
283 let key = single_key(&self.fields, &field).to_string();
284 self.pairs.push((key, value.to_string()));
285 self
286 }
287
288 pub fn set_key(mut self, key: &str, value: &str) -> Self {
290 self.pairs.retain(|(kept, _)| kept != key);
291 self.pairs.push((key.to_string(), value.to_string()));
292 self
293 }
294
295 pub fn create_another(self) -> Self {
298 self.set_key(CREATE_ANOTHER, "1")
299 }
300
301 pub fn picking(mut self) -> Self {
304 let joint = if self.url.contains('?') { '&' } else { '?' };
305 self.url = format!("{}{joint}pick=1", self.url);
306 self
307 }
308
309 pub async fn submit(self) -> Reply<'c, 'a, K> {
311 let pairs: Vec<(&str, &str)> = self
312 .pairs
313 .iter()
314 .map(|(key, value)| (key.as_str(), value.as_str()))
315 .collect();
316 let response = self.client.submit(&self.url, &form_body(&pairs)).await;
317 Reply::read(self.client, response, self.fields).await
318 }
319}
320
321fn single_key<'f, K: PartialEq>(fields: &'f [FormField<K>], field: &K) -> &'f str {
322 let field = fields
323 .iter()
324 .find(|candidate| candidate.field == *field)
325 .expect("a field enum's variant is one of its form's fields");
326 match field.keys.as_slice() {
327 [key] => key,
328 keys => panic!(
329 "`{}` binds the keys {keys:?}: post each with `set_key`",
330 field.name
331 ),
332 }
333}
334
335pub struct Reply<'c, 'a, K> {
340 client: &'c TestClient<'a>,
341 fields: Vec<FormField<K>>,
342 status: StatusCode,
343 location: Option<String>,
344 cookies: Vec<(String, String)>,
345 html: String,
346}
347
348impl<'c, 'a, K: PartialEq> Reply<'c, 'a, K> {
349 async fn read(
350 client: &'c TestClient<'a>,
351 response: http::Response<topcoat::router::Body>,
352 fields: Vec<FormField<K>>,
353 ) -> Self {
354 let location = response
355 .headers()
356 .get(LOCATION)
357 .and_then(|value| value.to_str().ok())
358 .map(str::to_string);
359 let cookies = response_cookies(&response);
360 Self {
361 client,
362 fields,
363 status: response.status(),
364 location,
365 cookies,
366 html: body_string(response).await,
367 }
368 }
369
370 pub fn status(&self) -> StatusCode {
371 self.status
372 }
373
374 pub fn location(&self) -> Option<&str> {
376 self.location.as_deref()
377 }
378
379 pub fn cookies(&self) -> &[(String, String)] {
381 &self.cookies
382 }
383
384 pub fn notification(&self) -> Option<Notification> {
386 notification_in(&self.cookies)
387 }
388
389 pub fn html(&self) -> &str {
390 &self.html
391 }
392
393 pub async fn follow(&self) -> Reply<'c, 'a, K>
399 where
400 K: Clone,
401 {
402 let location = self
403 .location
404 .as_deref()
405 .unwrap_or_else(|| panic!("a {} response redirects nowhere", self.status));
406 let response = self.client.cookies(&self.cookies).get(location).await;
407 Reply::read(self.client, response, self.fields.clone()).await
408 }
409
410 pub fn picked(&self) -> Option<crate::Picked> {
412 crate::picked(&self.html)
413 }
414
415 pub fn create_option_links(&self) -> Vec<crate::CreateOptionLink> {
417 crate::create_option_links(&self.html)
418 }
419
420 pub fn rows(&self) -> Vec<Row> {
422 rows(&self.html)
423 }
424
425 pub fn row_keys(&self) -> Vec<String> {
427 row_keys(&self.html)
428 }
429
430 pub fn groups(&self) -> Vec<Group> {
432 groups(&self.html)
433 }
434
435 pub fn empty_table(&self) -> Option<EmptyTable> {
437 empty_table(&self.html)
438 }
439
440 pub fn tabs(&self) -> Vec<crate::Tab> {
442 crate::tabs(&self.html)
443 }
444
445 pub fn wizard_steps(&self) -> Vec<crate::WizardStep> {
447 crate::wizard_steps(&self.html)
448 }
449
450 pub fn sections(&self) -> Vec<crate::Section> {
452 crate::sections(&self.html)
453 }
454
455 pub fn stale(&self) -> bool {
457 stale_form(&self.html)
458 }
459
460 pub fn declined(&self) -> Option<String> {
463 declined_message(&self.html)
464 }
465
466 pub fn dirty(&self) -> bool {
469 dirty_form(&self.html)
470 }
471
472 pub fn field_error(&self, field: K) -> Option<ErrorKind> {
475 field_error(&self.html, self.first_key(&field))
476 }
477
478 pub fn field_error_message(&self, field: K) -> Option<String> {
481 field_error_message(&self.html, self.first_key(&field))
482 }
483
484 pub fn input_value(&self, field: K) -> Option<String> {
487 input_value(&self.html, self.first_key(&field))
488 }
489
490 fn first_key(&self, field: &K) -> &str {
491 self.fields
492 .iter()
493 .find(|candidate| candidate.field == *field)
494 .and_then(|field| field.keys.first())
495 .expect("a field enum's variant binds a key of its form")
496 }
497}