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, empty_table, field_error,
19 field_error_message, form_body, groups, input_value, notification_in, response_cookies,
20 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 fn create_url(&self) -> String {
183 resolved::<R>(url::create::<R>(&self.cx()), "a create page")
184 }
185
186 fn edit_url(&self, record: &R::Model) -> String {
187 resolved::<R>(url::edit::<R>(&self.cx(), record), "an edit page")
188 }
189
190 fn cx(&self) -> Cx {
191 self.panel.enter(&self.panel.context())
192 }
193
194 async fn get(&self, url: String) -> Reply<'c, 'a, FieldOf<R>> {
195 Reply::read(self.client, self.client.get(&url).await, R::Form::fields()).await
196 }
197}
198
199const CONFIRM: &str = "confirm";
201
202const IDS: &str = "ids";
204
205fn resolved<R>(url: Option<String>, what: &str) -> String {
206 url.unwrap_or_else(|| panic!("the panel serves `{}` no {what}", type_name::<R>()))
207}
208
209#[must_use = "a submission posts nothing until `submit`"]
215pub struct Submission<'c, 'a, K> {
216 client: &'c TestClient<'a>,
217 url: String,
218 fields: Vec<FormField<K>>,
219 pairs: Vec<(String, String)>,
220}
221
222impl<'c, 'a, K: PartialEq> Submission<'c, 'a, K> {
223 fn new(client: &'c TestClient<'a>, url: String, fields: Vec<FormField<K>>) -> Self {
224 Self {
225 client,
226 url,
227 fields,
228 pairs: Vec::new(),
229 }
230 }
231
232 pub fn url(&self) -> &str {
234 &self.url
235 }
236
237 pub fn set(self, field: K, value: &str) -> Self {
244 let key = single_key(&self.fields, &field).to_string();
245 self.set_key(&key, value)
246 }
247
248 pub fn add(mut self, field: K, value: &str) -> Self {
254 let key = single_key(&self.fields, &field).to_string();
255 self.pairs.push((key, value.to_string()));
256 self
257 }
258
259 pub fn set_key(mut self, key: &str, value: &str) -> Self {
261 self.pairs.retain(|(kept, _)| kept != key);
262 self.pairs.push((key.to_string(), value.to_string()));
263 self
264 }
265
266 pub async fn submit(self) -> Reply<'c, 'a, K> {
268 let pairs: Vec<(&str, &str)> = self
269 .pairs
270 .iter()
271 .map(|(key, value)| (key.as_str(), value.as_str()))
272 .collect();
273 let response = self.client.submit(&self.url, &form_body(&pairs)).await;
274 Reply::read(self.client, response, self.fields).await
275 }
276}
277
278fn single_key<'f, K: PartialEq>(fields: &'f [FormField<K>], field: &K) -> &'f str {
279 let field = fields
280 .iter()
281 .find(|candidate| candidate.field == *field)
282 .expect("a field enum's variant is one of its form's fields");
283 match field.keys.as_slice() {
284 [key] => key,
285 keys => panic!(
286 "`{}` binds the keys {keys:?}: post each with `set_key`",
287 field.name
288 ),
289 }
290}
291
292pub struct Reply<'c, 'a, K> {
297 client: &'c TestClient<'a>,
298 fields: Vec<FormField<K>>,
299 status: StatusCode,
300 location: Option<String>,
301 cookies: Vec<(String, String)>,
302 html: String,
303}
304
305impl<'c, 'a, K: PartialEq> Reply<'c, 'a, K> {
306 async fn read(
307 client: &'c TestClient<'a>,
308 response: http::Response<topcoat::router::Body>,
309 fields: Vec<FormField<K>>,
310 ) -> Self {
311 let location = response
312 .headers()
313 .get(LOCATION)
314 .and_then(|value| value.to_str().ok())
315 .map(str::to_string);
316 let cookies = response_cookies(&response);
317 Self {
318 client,
319 fields,
320 status: response.status(),
321 location,
322 cookies,
323 html: body_string(response).await,
324 }
325 }
326
327 pub fn status(&self) -> StatusCode {
328 self.status
329 }
330
331 pub fn location(&self) -> Option<&str> {
333 self.location.as_deref()
334 }
335
336 pub fn cookies(&self) -> &[(String, String)] {
338 &self.cookies
339 }
340
341 pub fn notification(&self) -> Option<Notification> {
343 notification_in(&self.cookies)
344 }
345
346 pub fn html(&self) -> &str {
347 &self.html
348 }
349
350 pub async fn follow(&self) -> Reply<'c, 'a, K>
356 where
357 K: Clone,
358 {
359 let location = self
360 .location
361 .as_deref()
362 .unwrap_or_else(|| panic!("a {} response redirects nowhere", self.status));
363 let response = self.client.cookies(&self.cookies).get(location).await;
364 Reply::read(self.client, response, self.fields.clone()).await
365 }
366
367 pub fn rows(&self) -> Vec<Row> {
369 rows(&self.html)
370 }
371
372 pub fn row_keys(&self) -> Vec<String> {
374 row_keys(&self.html)
375 }
376
377 pub fn groups(&self) -> Vec<Group> {
379 groups(&self.html)
380 }
381
382 pub fn empty_table(&self) -> Option<EmptyTable> {
384 empty_table(&self.html)
385 }
386
387 pub fn tabs(&self) -> Vec<crate::Tab> {
389 crate::tabs(&self.html)
390 }
391
392 pub fn stale(&self) -> bool {
394 stale_form(&self.html)
395 }
396
397 pub fn field_error(&self, field: K) -> Option<ErrorKind> {
400 field_error(&self.html, self.first_key(&field))
401 }
402
403 pub fn field_error_message(&self, field: K) -> Option<String> {
406 field_error_message(&self.html, self.first_key(&field))
407 }
408
409 pub fn input_value(&self, field: K) -> Option<String> {
412 input_value(&self.html, self.first_key(&field))
413 }
414
415 fn first_key(&self, field: &K) -> &str {
416 self.fields
417 .iter()
418 .find(|candidate| candidate.field == *field)
419 .and_then(|field| field.keys.first())
420 .expect("a field enum's variant binds a key of its form")
421 }
422}