django_query/
mock.rs

1//! # Create Django-style endpoints using [`wiremock`].
2//!
3//! One possible use of this crate is to mock Django endpoints for
4//! development or testing. This may be convenient if you are otherwise
5//! developing in Rust, for example, and want to have CI run unit or
6//! integration tests that involve calls to such a service.
7//!
8//! In order to be most useful as a mock, it needs to provide complete
9//! integrated Django endpoints:
10//! - a HTTP server listening for requests.
11//! - a store of data to serve.
12//! - request parsing to determine result sets.
13//! - Django formatted, paginated output.
14//!
15//! The other modules in this crate provide most of this, when coupled
16//! with [`wiremock`]. This module is mostly concerned with tying
17//! everything together into a whole.
18//!
19//! # Overview
20//!
21//! The main type in this module is [`Endpoint`], which is an
22//! implementor of [`wiremock::Respond`], and can be mounted directly
23//! on a [`wiremock::MockServer`]. It takes a [`RowSource`] which is responsible
24//! for providing a snapshot of data to serve for each request. Each [`Endpoint`]
25//! serves values of one type, and that type must always implement:
26//! - [`Sortable`] so that `"ordering"` requests can be processed.
27//! - [`Filterable`] so that filtering request can be processed
28//!   (`"__in"`, `"__lt"` and so forth).
29//! - [`IntoRow`] so that rows of result data can be produced.
30//!
31//! All three traits must always be implemented, because of the way in
32//! which cargo features interact - they are required to be stricly
33//! additive and adding type bounds decreases the set of types are
34//! permitted, and is thus subtractive.
35//!
36//! The main functionality this module handles itself is pagination, and
37//! it provides a simple limit/offset model.
38//!
39//! Example
40//! ```rust
41//! use django_query::filtering::Filterable;
42//! use django_query::mock::Endpoint;
43//! use django_query::row::IntoRow;
44//! use django_query::sorting::Sortable;
45//! use std::sync::Arc;
46//! use wiremock::{Mock, MockServer, matchers, http::Url};
47//!
48//! #[derive(IntoRow, Filterable, Sortable)]
49//! struct Foo {
50//!   #[django(sort, op(in, lt, gt))]
51//!   a: i32
52//! }
53//!
54//! let foos = (0..20i32).into_iter().map(|a| Foo { a }).collect::<Vec<_>>();
55//!
56//! # tokio_test::block_on( async {
57//! let server = MockServer::start().await;
58//!
59//! Mock::given(matchers::method("GET"))
60//!      .respond_with(Endpoint::new(Arc::new(foos), Some(&server.uri())))
61//!      .mount(&server)
62//!      .await;
63//!
64//! let u = format!("{}?limit=1&offset=5&a__lt=10&ordering=-a", server.uri());
65//! let body: serde_json::Value = reqwest::get(&u)
66//!     .await
67//!     .expect("error getting response")
68//!     .json()
69//!     .await
70//!     .expect("error parsing response");
71//!
72//! let prev = format!("{}/?limit=1&offset=4&a__lt=10&ordering=-a", server.uri());
73//! let next = format!("{}/?limit=1&offset=6&a__lt=10&ordering=-a", server.uri());
74//! assert_eq!(body, serde_json::json!{
75//!   {
76//!     "count": 10,
77//!     "next": next,
78//!     "previous": prev,
79//!     "results": [
80//!       { "a": 4 }
81//!     ]
82//!   }
83//! });
84//! # });
85//! ```
86
87use core::cmp::min;
88use core::fmt::Debug;
89use regex::{Captures, Regex};
90use std::num::ParseIntError;
91use std::str::FromStr;
92
93use log::{debug, trace};
94use thiserror::Error;
95use wiremock::http::Url;
96use wiremock::{Request, Respond, ResponseTemplate};
97
98use crate::filtering::{
99    Filter, Filterable, FilterableWithContext, OperatorSet, OperatorSetWithContext,
100};
101use crate::row::{IntoRow, IntoRowWithContext, Serializer};
102use crate::sorting::{OrderingSet, OrderingSetWithContext, Sortable, SortableWithContext, Sorter};
103
104#[derive(Debug, Error)]
105#[cfg_attr(docsrs, doc(cfg(feature = "wiremock")))]
106/// An error produced by the mock endpoint.
107pub enum MockError {
108    /// A query parameter was provided that wasn't recognised.
109    ///
110    /// It was not a valid filtering operator, a pagination request,
111    /// or an ordering expression.
112    #[error("unknown query parameter `{0}`")]
113    UnknownQueryParameter(String),
114    /// Failed to parse an integer.
115    ///
116    /// This likely arises from limit/offset.
117    #[error("expected integer value in query")]
118    BadIntegerInQuery(#[from] ParseIntError),
119    /// Failed to parse an enum value.
120    #[error("bad value in query: {0}")]
121    BadValue(#[from] strum::ParseError),
122    /// Failed to parse a date.
123    #[error("bad datetime value in query")]
124    BadDateInQuery(#[from] chrono::ParseError),
125    /// A bad sort expression was given.
126    #[error("invalid sort: {0}")]
127    BadSort(#[from] crate::sorting::SorterError),
128}
129
130struct ResponseSet<T> {
131    contents: Vec<T>,
132    total_matches: usize,
133    next: Option<String>,
134    prev: Option<String>,
135}
136
137impl<T> ResponseSet<T> {
138    pub fn new(
139        contents: Vec<T>,
140        total_matches: usize,
141        next: Option<String>,
142        prev: Option<String>,
143    ) -> Self {
144        ResponseSet {
145            contents,
146            total_matches,
147            next,
148            prev,
149        }
150    }
151}
152
153impl<'q, T: IntoRow<'q>> ResponseSet<&T> {
154    pub fn mock_json(&self) -> serde_json::Value {
155        let mut map = serde_json::map::Map::new();
156        map.insert(
157            "count".to_string(),
158            serde_json::Value::Number(serde_json::Number::from(self.total_matches)),
159        );
160        map.insert("results".to_string(), to_rows(&self.contents));
161        map.insert(
162            "next".to_string(),
163            self.next
164                .as_ref()
165                .map(|x| serde_json::Value::String(x.clone()))
166                .unwrap_or(serde_json::Value::Null),
167        );
168        map.insert(
169            "previous".to_string(),
170            self.prev
171                .as_ref()
172                .map(|x| serde_json::Value::String(x.clone()))
173                .unwrap_or(serde_json::Value::Null),
174        );
175
176        serde_json::Value::Object(map)
177    }
178}
179
180fn to_rows<'q, T: IntoRow<'q>>(data: &[&T]) -> serde_json::Value {
181    let mut array = Vec::new();
182    let ser = T::get_serializer();
183    for item in data {
184        array.push(ser.to_json(item));
185    }
186    serde_json::Value::Array(array)
187}
188
189struct Page {
190    offset: usize,
191    limit: usize,
192}
193
194struct PaginatedResponse<'a, T> {
195    data: Vec<&'a T>,
196    total: usize,
197    next: Option<Page>,
198    prev: Option<Page>,
199}
200
201struct ResponseSetBuilder<'a, T> {
202    ordering: Vec<Box<dyn Sorter<T> + 'a>>,
203    filtering: Vec<Box<dyn Filter<T> + 'a>>,
204    limit: Option<usize>,
205    offset: usize,
206}
207
208impl<'a, T> Default for ResponseSetBuilder<'a, T> {
209    fn default() -> Self {
210        Self::new(None)
211    }
212}
213
214impl<'a, T> ResponseSetBuilder<'a, T> {
215    pub fn new(default_limit: Option<usize>) -> Self {
216        ResponseSetBuilder {
217            ordering: Vec::new(),
218            filtering: Vec::new(),
219            limit: default_limit,
220            offset: 0,
221        }
222    }
223
224    pub fn order_by(&mut self, order: Box<dyn Sorter<T> + 'a>) -> &mut Self {
225        self.ordering.push(order);
226        self
227    }
228
229    pub fn filter_by(&mut self, filter: Box<dyn Filter<T> + 'a>) -> &mut Self {
230        self.filtering.push(filter);
231        self
232    }
233
234    pub fn limit(&mut self, limit: usize) -> &mut Self {
235        self.limit = Some(limit);
236        self
237    }
238
239    pub fn offset(&mut self, offset: usize) -> &mut Self {
240        self.offset = offset;
241        self
242    }
243
244    pub fn apply<'b, I: Iterator<Item = &'b T>>(&mut self, iter: I) -> PaginatedResponse<'b, T> {
245        let mut v = Vec::new();
246        for item in iter {
247            v.push(item)
248        }
249
250        for f in &self.filtering {
251            f.filter_ref_vec(&mut v);
252        }
253
254        for order in &self.ordering {
255            order.sort_ref_vec(&mut v);
256        }
257
258        let limit = self.limit.unwrap_or(v.len());
259        let total = v.len();
260
261        let start = min(v.len(), self.offset);
262        let prev = if start > 0 {
263            Some(Page {
264                offset: self.offset - min(self.offset, limit),
265                limit,
266            })
267        } else {
268            None
269        };
270
271        v.drain(..start);
272
273        let end = min(v.len(), limit);
274        let next = if end < v.len() {
275            Some(Page {
276                offset: self.offset + limit,
277                limit,
278            })
279        } else {
280            None
281        };
282
283        v.drain(end..);
284
285        PaginatedResponse {
286            data: v,
287            total,
288            next,
289            prev,
290        }
291    }
292}
293
294fn make_page_url(url: &Url, offset: usize, limit: usize) -> Url {
295    let mut new_url = url.clone();
296
297    let mut new_pairs = new_url.query_pairs_mut();
298    let old_pairs = url.query_pairs();
299
300    let mut offset_seen = false;
301
302    new_pairs.clear();
303    for (key, value) in old_pairs {
304        match key.as_ref() {
305            "limit" => {
306                new_pairs.append_pair("limit", &limit.to_string());
307            }
308            "offset" => {
309                if offset > 0 {
310                    new_pairs.append_pair("offset", &offset.to_string());
311                }
312                offset_seen = true;
313            }
314            _ => {
315                new_pairs.append_pair(key.as_ref(), value.as_ref());
316            }
317        }
318    }
319
320    if !offset_seen && offset > 0 {
321        new_pairs.append_pair("offset", &offset.to_string());
322    }
323
324    new_pairs.finish();
325    drop(new_pairs);
326
327    new_url
328}
329
330/// Provide a collection of objects on demand.
331///
332/// This trait generalises the idea of "something that can provide
333/// data to query".  It's important to note that the data it contains
334/// is owned, and is therefore guaranteed to survive for as long as
335/// needed, in particular for the life of the query. For correctness,
336/// it is expected that the returned data is a snapshot that will not
337/// change.
338///
339/// The reason for this definition is to make it possible to return
340/// views of data without copying, where that makes sense. For example
341/// using `Arc<Vec<T>>` as a [`RowSource`] does not entail copying data.
342/// It's possible to define mutable types that generate snapshots on
343/// demand which also implement `RowSource`, if you need your test
344/// data to evolve.
345///
346/// The main drawback of this definition is that supporting bare
347/// containers becomes very expensive. [`Vec<T>`] is defined to be a
348/// [`RowSource`], but the entire [`Vec`] must be copied for every call to
349/// [`get`](RowSource::get).
350///
351#[cfg_attr(
352    feature = "clone-replace",
353    doc = r##"
354The `"clone-replace"` feature enables a mutable [`RowSource`] using a
355[`clone_replace::CloneReplace`](::clone_replace::CloneReplace). Your
356test code is free to hold a reference to, and mutate, the
357[`CloneReplace`](::clone_replace::CloneReplace) wrapped collection.
358Each query will get an [`Arc<T>`](std::sync::Arc) of a snapshot of the
359state of the collection at the start of the query. You can also
360extract a single [`Vec<T>`] field from a struct and serve that using
361[`CloneReplaceFieldSource`].
362"##
363)]
364#[cfg_attr(
365    all(feature = "clone-replace", feature = "persian-rug"),
366    doc = r##"
367When both `"clone-replace"` and `"persian-rug"` are enabled, you can
368combine a [`CloneReplace`](::clone_replace::CloneReplace) and a
369[`persian_rug::Context`] using a [`CloneReplacePersianRugTableSource`]
370to create a [`RowSource`] for a single type inside the shared state, which
371remains mutable.
372"##
373)]
374#[cfg_attr(docsrs, doc(cfg(feature = "wiremock")))]
375pub trait RowSource
376where
377    Self::Rows: RowSet<Item = Self::Item>,
378    for<'a> &'a <Self::Rows as RowSet>::Target: IntoIterator<Item = &'a Self::Item>,
379{
380    /// The type of the objects this provides.
381    type Item;
382    /// The type of the collection this provides.
383    type Rows;
384    // If this is unowned, then we can handle reference types
385    // seamlessly and avoid copying entire vecs. But it _must_ be
386    // owned for a wide variety of the more interesting cases
387    // (e.g. MutexGuard, Arc) so we pay the price of copies in the
388    // simple cases. I don't think it's possible to express the trait
389    // bounds without GAT to handle both cases cleanly.  Actually,
390    // it's worse than this: you can't implement Mutex at all because
391    // MutexGuard is not fully owned: the lifetime parameter can't be
392    // coped with because the HRTBs aren't powerful enough and GAT
393    // isn't stable. So the best we can hope is probably fully owned
394    // cases (which CloneReplace ought to provide).
395
396    /// Return a new, owned collection of objects, which should now
397    /// remain immutable.
398    fn get(&self) -> Self::Rows;
399}
400
401/// An opaque snapshot produced by a [`RowSource`]
402///
403/// This trait represents an object that provides a fixed iterable
404/// collection on demand. In order to express the idea that a
405/// collection is iterable, and not consumed by being iterated over,
406/// we must describe it as having [`IntoIterator`] implemented for
407/// shared references. A [`RowSet`] in contrast is owned, so that the
408/// snapshot can be repeatedly iterated over, without being consumed
409/// or updated, as required.
410pub trait RowSet {
411    /// The iterable collection type this provides.
412    type Target;
413    /// The type of the items in the collection this provides.
414    type Item;
415    /// Produce a reference to an iterable collection.
416    fn get(&self) -> &Self::Target;
417}
418
419impl<T: Clone> RowSource for Vec<T> {
420    type Item = T;
421    type Rows = Self;
422    fn get(&self) -> Self::Rows {
423        self.clone()
424    }
425}
426
427impl<T> RowSet for Vec<T> {
428    type Item = T;
429    type Target = Self;
430    fn get(&self) -> &Self::Target {
431        self
432    }
433}
434
435impl<T> RowSource for std::sync::Arc<Vec<T>> {
436    type Item = T;
437    type Rows = Self;
438    fn get(&self) -> Self::Rows {
439        self.clone()
440    }
441}
442
443impl<T> RowSet for std::sync::Arc<Vec<T>> {
444    type Item = T;
445    type Target = Vec<T>;
446    fn get(&self) -> &Self::Target {
447        self
448    }
449}
450
451#[cfg(all(feature = "clone-replace", feature = "persian-rug"))]
452pub use self::clone_replace::persian_rug::CloneReplacePersianRugTableSource;
453#[cfg(feature = "clone-replace")]
454pub use self::clone_replace::CloneReplaceFieldSource;
455
456#[doc(hidden)]
457#[cfg(feature = "clone-replace")]
458pub mod clone_replace {
459    use super::{RowSet, RowSource};
460    use ::clone_replace::CloneReplace;
461
462    // Use a Vec wrapped in a CloneReplace as a RowSource.
463
464    impl<T> RowSource for CloneReplace<Vec<T>> {
465        type Item = T;
466        type Rows = std::sync::Arc<Vec<T>>;
467
468        fn get(&self) -> Self::Rows {
469            self.access()
470        }
471    }
472
473    /// Use a [`Vec`] valued field of a [`CloneReplace<T>`] as a [`RowSource`].
474    pub struct CloneReplaceFieldSource<F, T> {
475        data: CloneReplace<T>,
476        getter: F,
477    }
478
479    impl<F, T, U> CloneReplaceFieldSource<F, T>
480    where
481        F: Fn(&T) -> &Vec<U> + Clone,
482    {
483        /// Create a new [`CloneReplaceFieldSource`].
484        ///
485        /// Here, `data` is the [`CloneReplace<T>`] that will provide
486        /// the data, and `getter` is a lambda that can extract a
487        /// [`Vec<U>`] from a `&T`.
488        pub fn new(data: CloneReplace<T>, getter: F) -> Self {
489            Self { data, getter }
490        }
491    }
492
493    impl<F, T, U> RowSource for CloneReplaceFieldSource<F, T>
494    where
495        F: Fn(&T) -> &Vec<U> + Clone,
496    {
497        type Item = U;
498        type Rows = ArcField<F, T>;
499        fn get(&self) -> Self::Rows {
500            ArcField {
501                getter: self.getter.clone(),
502                data: self.data.access(),
503            }
504        }
505    }
506
507    #[doc(hidden)]
508    pub struct ArcField<F, T> {
509        getter: F,
510        data: std::sync::Arc<T>,
511    }
512
513    impl<F, T, U> RowSet for ArcField<F, T>
514    where
515        F: Fn(&T) -> &Vec<U>,
516    {
517        type Item = U;
518        type Target = Vec<U>;
519        fn get(&self) -> &Self::Target {
520            (self.getter)(&self.data)
521        }
522    }
523
524    #[cfg(feature = "persian-rug")]
525    pub mod persian_rug {
526        use super::{RowSet, RowSource};
527        use clone_replace::CloneReplace;
528
529        /// Use a table from a [`persian_rug::Context`] as a
530        /// [`RowSource`]
531        ///
532        /// This [`RowSource`] requires an
533        /// [`EndpointWithContext`](crate::mock::EndpointWithContext)
534        /// to serve it. The [`RowSet`] conveniently doubles as the
535        /// [`persian_rug::Accessor`] object in this implementation as
536        /// required by
537        /// [`EndpointWithContext`](crate::mock::EndpointWithContext).
538        pub struct CloneReplacePersianRugTableSource<F, T> {
539            data: CloneReplace<T>,
540            getter: F,
541        }
542
543        impl<C, F, U> CloneReplacePersianRugTableSource<F, C>
544        where
545            F: Fn(&std::sync::Arc<C>) -> ::persian_rug::TableIterator<'_, U> + Clone,
546            for<'a> &'a PersianRugTable<std::sync::Arc<C>, F>: IntoIterator<Item = &'a U>,
547        {
548            pub fn new(data: CloneReplace<C>, getter: F) -> Self {
549                Self { data, getter }
550            }
551        }
552
553        #[persian_rug::constraints(context = C, access(U))]
554        impl<C, F, U> RowSource for CloneReplacePersianRugTableSource<F, C>
555        where
556            F: Fn(&std::sync::Arc<C>) -> ::persian_rug::TableIterator<'_, U> + Clone,
557            for<'a> &'a PersianRugTable<std::sync::Arc<C>, F>: IntoIterator<Item = &'a U>,
558        {
559            type Item = U;
560            type Rows = PersianRugTable<std::sync::Arc<C>, F>;
561            fn get(&self) -> Self::Rows {
562                PersianRugTable {
563                    getter: self.getter.clone(),
564                    access: self.data.access(),
565                }
566            }
567        }
568
569        #[doc(hidden)]
570        #[derive(Clone)]
571        pub struct PersianRugTable<A: Clone, F: Clone> {
572            access: A,
573            getter: F,
574        }
575
576        #[persian_rug::constraints(context = C, access(U))]
577        impl<A, C, F, U> RowSet for PersianRugTable<A, F>
578        where
579            A: ::persian_rug::Accessor<Context = C>,
580            F: Fn(&A) -> ::persian_rug::TableIterator<'_, U> + Clone,
581        {
582            type Item = U;
583            type Target = Self;
584            fn get(&self) -> &Self::Target {
585                self
586            }
587        }
588
589        impl<A, C, F> ::persian_rug::Accessor for PersianRugTable<A, F>
590        where
591            A: persian_rug::Accessor<Context = C>,
592            C: persian_rug::Context,
593            F: Clone,
594        {
595            type Context = C;
596
597            fn get<T>(&self, what: &persian_rug::Proxy<T>) -> &T
598            where
599                Self::Context: persian_rug::Owner<T>,
600                T: persian_rug::Contextual<Context = Self::Context>,
601            {
602                self.access.get(what)
603            }
604
605            fn get_iter<T>(&self) -> persian_rug::TableIterator<'_, T>
606            where
607                Self::Context: persian_rug::Owner<T>,
608                T: persian_rug::Contextual<Context = Self::Context>,
609            {
610                self.access.get_iter()
611            }
612
613            fn get_proxy_iter<T>(&self) -> persian_rug::TableProxyIterator<'_, T>
614            where
615                Self::Context: persian_rug::Owner<T>,
616                T: persian_rug::Contextual<Context = Self::Context>,
617            {
618                self.access.get_proxy_iter()
619            }
620        }
621
622        #[persian_rug::constraints(context = C, access(U))]
623        impl<'a, A, C, F, U> IntoIterator for &'a PersianRugTable<A, F>
624        where
625            A: persian_rug::Accessor<Context = C>,
626            F: Fn(&'a A) -> persian_rug::TableIterator<'a, U> + Clone,
627            U: 'a,
628        {
629            type Item = &'a U;
630            type IntoIter = persian_rug::TableIterator<'a, U>;
631            fn into_iter(self) -> Self::IntoIter {
632                (self.getter)(&self.access)
633            }
634        }
635    }
636}
637
638fn parse_query<'a, 'q, R, I>(
639    input_url: &Url,
640    output_url: &Url,
641    iter: I,
642    default_limit: Option<usize>,
643) -> Result<ResponseSet<&'a R>, MockError>
644where
645    R: Filterable<'q>,
646    // Note the 'q bound is a consequence of Sortable's implementation for StackedSorter in OrderingSet
647    R: Sortable<'q> + 'q,
648    R: IntoRow<'q>,
649    I: Iterator<Item = &'a R>,
650{
651    let mut rb = ResponseSetBuilder::new(default_limit);
652    let qr = OperatorSet::<R>::new();
653    let sr = OrderingSet::<R>::new();
654    let pairs = input_url.query_pairs();
655    for (key, value) in pairs {
656        match key.as_ref() {
657            "ordering" => {
658                rb.order_by(sr.create_sort(&*value)?);
659            }
660            "offset" => {
661                let v = usize::from_str(value.as_ref())?;
662                rb.offset(v);
663            }
664            "limit" => {
665                let v = usize::from_str(value.as_ref())?;
666                rb.limit(v);
667            }
668            _ => {
669                if let Ok(filter) = qr.create_filter_from_query_pair(&key, &value) {
670                    rb.filter_by(filter);
671                    continue;
672                }
673                return Err(MockError::UnknownQueryParameter(String::from(key.as_ref())));
674            }
675        }
676    }
677    let response = rb.apply(iter);
678    Ok(ResponseSet::new(
679        response.data,
680        response.total,
681        response
682            .next
683            .map(|page| make_page_url(output_url, page.offset, page.limit).to_string()),
684        response
685            .prev
686            .map(|page| make_page_url(output_url, page.offset, page.limit).to_string()),
687    ))
688}
689
690/// A Django-style [`wiremock`] endpoint for a collection of objects.
691///
692/// This is the central type in this crate. [`Endpoint`] implements
693/// [`wiremock::Respond`] and so can be mounted directly into a
694/// [`wiremock::MockServer`]. It contains a [`RowSource`]
695/// which it will query for a fresh data on each query.
696///
697/// The [`Endpoint`] will filter the returned data based on the query
698/// parameters in the URL, by using the [`Filterable`] trait on the
699/// objects. It will then sort the data using the [`Sortable`] trait
700/// on the objects. It will paginate the results as required using
701/// `"limit"` and `"offset"`. Finally, the returned data will be
702/// converted using the [`IntoRow`] trait.
703#[cfg_attr(docsrs, doc(cfg(feature = "wiremock")))]
704pub struct Endpoint<T>
705where
706    T: Send + Sync + RowSource,
707    for<'t> &'t <<T as RowSource>::Rows as RowSet>::Target:
708        IntoIterator<Item = &'t <T as RowSource>::Item>,
709{
710    row_source: T,
711    base_uri: Option<Url>,
712    default_limit: Option<usize>,
713}
714
715impl<'q, T> Endpoint<T>
716where
717    T: Send + Sync + RowSource,
718    for<'t> &'t <<T as RowSource>::Rows as RowSet>::Target:
719        IntoIterator<Item = &'t <T as RowSource>::Item>,
720    <T as RowSource>::Item: Send + Sync + Filterable<'q> + Sortable<'q>,
721{
722    /// Create a new endpoint.
723    ///
724    /// `row_source` is where the data comes from. `base_uri`, if provided, should
725    /// be the wiremock server URI.
726    ///
727    /// `base_uri` is only required because wiremock mangles the
728    /// request URL such that it's not possible to inspect it to see
729    /// how to give out other URLs on the same server; the port
730    /// number, which is usually random when mocking, is lost. Since
731    /// Django includes full absolute URLs for the next and preceding
732    /// pages for a query, mimicking this is impossible without more
733    /// information (i.e. including usable URLs for the next page
734    /// within the query response is impossible).
735    pub fn new(row_source: T, base_uri: Option<&str>) -> Self {
736        Self {
737            row_source,
738            base_uri: base_uri.map(|x| Url::parse(x).unwrap()),
739            default_limit: None,
740        }
741    }
742
743    /// Set the default number of results returned
744    ///
745    /// This is the value used when no limit is specified in the
746    /// query.  This value is configurable in Django; the default
747    /// behaviour of this mock endpoint is to return everything, but
748    /// that makes testing code that uses the default pagination more
749    /// difficult.
750    pub fn default_limit(&mut self, limit: usize) {
751        self.default_limit = Some(limit);
752    }
753}
754
755impl<'q, T> Respond for Endpoint<T>
756where
757    T: Send + Sync + RowSource,
758    for<'t> &'t <<T as RowSource>::Rows as RowSet>::Target:
759        IntoIterator<Item = &'t <T as RowSource>::Item>,
760    // Note the 'q bound is a consequence of Sortable's implementation for StackedSorter in OrderingSet
761    <T as RowSource>::Item: Send + Sync + Filterable<'q> + Sortable<'q> + IntoRow<'q> + 'q,
762{
763    fn respond(&self, request: &Request) -> ResponseTemplate {
764        // All of this is to work around the mess in wiremock that means
765        // request.uri is inaccurate, and can't be used.
766        let mut u = request.url.clone();
767        if let Some(ref base) = self.base_uri {
768            u.set_host(base.host_str()).unwrap();
769            u.set_scheme(base.scheme()).unwrap();
770            u.set_port(base.port()).unwrap();
771        }
772        let res = {
773            let data = self.row_source.get();
774            let res = {
775                let rows = data.get().into_iter();
776                let body = parse_query::<_, _>(&request.url, &u, rows, self.default_limit);
777                match body {
778                    Ok(rs) => {
779                        let bb = rs.mock_json();
780                        ResponseTemplate::new(200).set_body_json(bb)
781                    }
782                    Err(e) => {
783                        debug!("Failed to respond to {}: {}", request.url, e);
784                        ResponseTemplate::new(500).set_body_string(e.to_string())
785                    }
786                }
787            };
788            res
789        };
790        res
791    }
792}
793
794/// Match a Django-style nested endpoint in wiremock
795///
796/// This avoids using regular expressions in the calling code. Here
797/// - `root` is the base of the Django API, like `"/api/v0.2"`
798/// - `parent` is the parent object, like `"bars"`
799/// - `child` is the nested object, like `"foos"`
800///
801/// The above specification matches URL paths like
802///
803///    ``/api/v0.2/bars/1/foos``
804///
805/// See the description of [`NestedEndpointParams`] for more detail.
806#[cfg_attr(docsrs, doc(cfg(feature = "wiremock")))]
807pub fn nested_endpoint_matches(root: &str, parent: &str, child: &str) -> impl wiremock::Match {
808    wiremock::matchers::path_regex(format!(r"^{}/{}/[^/]+/{}/$", root, parent, child))
809}
810
811fn replace_into(target: &str, cap: &Captures<'_>) -> String {
812    let mut res = String::new();
813    cap.expand(target, &mut res);
814    res
815}
816
817struct UrlTransform {
818    regex: Regex,
819    path: String,
820    pairs: Vec<(String, String)>,
821}
822
823impl UrlTransform {
824    pub fn new(pattern: &str, path: &str, pairs: Vec<(&str, &str)>) -> UrlTransform {
825        Self {
826            regex: Regex::new(pattern).unwrap(),
827            path: path.to_string(),
828            pairs: pairs
829                .into_iter()
830                .map(|(x, y)| (x.to_string(), y.to_string()))
831                .collect(),
832        }
833    }
834
835    pub fn transform(&self, url: &Url) -> Url {
836        debug!("Matching {} against {:?}", url, self.regex);
837        if let Some(captures) = self.regex.captures(url.path()) {
838            let mut u = url.clone();
839            u.set_path(&replace_into(&self.path, &captures));
840            for (k, v) in self.pairs.iter() {
841                u.query_pairs_mut()
842                    .append_pair(&replace_into(k, &captures), &replace_into(v, &captures));
843            }
844            u
845        } else {
846            url.clone()
847        }
848    }
849}
850
851/// A Django nested route.
852///
853/// A nested route in Django is a way of specifying that a particular
854/// filter field must be present when querying for a particular object
855/// type, and that the filter type must be exact match. So for
856/// example, if all objects of type `Foo` have a reference to a `Bar`,
857/// and its too expensive or undesirable to query for `Foo`s without
858/// specifying which `Bar` they come from, then you can express this
859/// in Django as a nested route:
860///
861///   ``http://example.com/bars/1/foos``
862///
863/// where in order to ask any question about `Foo`s you must first
864/// identify a particular associated `Bar`.
865///
866/// Since the django-query crate operates on query parameters, the
867/// simplest way to handle this is to convert any relevant parts of
868/// the path into query parameters as required, which is all this type
869/// does. See its construction parameters [`NestedEndpointParams`] for
870/// the details. This type is otherwise identical to [`Endpoint`].
871#[cfg_attr(docsrs, doc(cfg(feature = "wiremock")))]
872pub struct NestedEndpoint<T> {
873    transform: UrlTransform,
874    row_source: T,
875    base_uri: Option<Url>,
876    default_limit: Option<usize>,
877}
878
879/// The construction parameters for [`NestedEndpoint`]
880///
881/// Example:
882/// ```rust
883/// # use django_query::filtering::Filterable;
884/// # use django_query::sorting::Sortable;
885/// # use django_query::row::IntoRow;
886/// use django_query::mock::{nested_endpoint_matches, NestedEndpoint, NestedEndpointParams};
887/// # use std::sync::Arc;
888///
889/// #[derive(Clone, Filterable, IntoRow, Sortable)]
890/// struct Foo {
891///   #[django(traverse, foreign_key="id")]
892///   bar: Arc<Bar>
893/// }
894///
895/// #[derive(Clone, Filterable, IntoRow, Sortable)]
896/// struct Bar {
897///   id: i32
898/// }
899///
900/// # tokio_test::block_on( async {
901/// let s = wiremock::MockServer::start().await;
902///
903/// let foos = vec![Foo { bar: Arc::new( Bar { id: 1 }) }];
904///
905/// wiremock::Mock::given(wiremock::matchers::method("GET"))
906///    .and(nested_endpoint_matches("/api", "bars", "foos"))
907///    .respond_with(NestedEndpoint::new(
908///        foos,
909///        NestedEndpointParams {
910///            root: "/api/",
911///            parent: "bars",
912///            child: "foos",
913///            parent_query: "bar__id",
914///            base_uri: Some(&s.uri())
915///        }
916///    ))
917///    .mount(&s)
918///    .await;
919///
920/// let u = format!("{}/api/bars/1/foos/", s.uri());
921/// let body: serde_json::Value = reqwest::get(&u)
922///     .await
923///     .expect("error getting response")
924///     .json()
925///     .await
926///     .expect("error parsing response");
927///
928/// assert_eq!(body, serde_json::json!{
929///   {
930///     "count": 1,
931///     "next": null,
932///     "previous": null,
933///     "results": [
934///       { "bar": 1 }
935///     ]
936///   }
937/// });
938/// # });
939/// ```
940#[cfg_attr(docsrs, doc(cfg(feature = "wiremock")))]
941pub struct NestedEndpointParams<'a> {
942    /// The base mock path, without trailing slash, for example `"/api/v0.2"`
943    pub root: &'a str,
944    /// The objects this endpoint is nested under, for example `"bars"`.
945    pub parent: &'a str,
946    /// The objects this endpoint serves, for example `"foos"`
947    pub child: &'a str,
948    /// The filter path for the parent from the child, so for example
949    /// `"parent__id"` if the filter expression for a given `Foo` to
950    /// obtain the value used in the query from its `Bar` is
951    /// `"parent__id"`.
952    pub parent_query: &'a str,
953    /// The mock server URI, since wiremock does not make this
954    /// available to us; specifying [`None`] here will break
955    /// pagination (if requested) until wiremock propagates this
956    /// information.
957    pub base_uri: Option<&'a str>,
958}
959
960impl<'q, T> NestedEndpoint<T>
961where
962    T: Send + Sync + RowSource,
963    for<'t> &'t <<T as RowSource>::Rows as RowSet>::Target:
964        IntoIterator<Item = &'t <T as RowSource>::Item>,
965    <T as RowSource>::Item: Send + Sync + Filterable<'q> + Sortable<'q>,
966{
967    /// Create a new [`NestedEndpoint`].
968    ///
969    /// The arguments to this function are wrapped in their own
970    /// structure: [`NestedEndpointParams`].
971    pub fn new(row_source: T, p: NestedEndpointParams<'_>) -> Self {
972        Self {
973            transform: UrlTransform::new(
974                &format!(r"^{}/{}/(?P<parent>[^/]+)/{}/$", p.root, p.parent, p.child),
975                &format!("{}/{}/", p.root, p.child),
976                vec![(&*p.parent_query, "${parent}")],
977            ),
978            row_source,
979            base_uri: p.base_uri.map(|x| Url::parse(x).unwrap()),
980            default_limit: None,
981        }
982    }
983
984    /// Set the default number of results returned
985    ///
986    /// This is the value used when no limit is specified in the
987    /// query.  This value is configurable in Django; the default
988    /// behaviour of this mock endpoint is to return everything, but
989    /// that makes testing code that uses the default pagination more
990    /// difficult.
991    pub fn default_limit(&mut self, limit: usize) {
992        self.default_limit = Some(limit);
993    }
994}
995
996impl<'q, T> Respond for NestedEndpoint<T>
997where
998    T: Send + Sync + RowSource,
999    for<'t> &'t <<T as RowSource>::Rows as RowSet>::Target:
1000        IntoIterator<Item = &'t <T as RowSource>::Item>,
1001    // Note the 'q bound is a consequence of Sortable's implementation for StackedSorter in OrderingSet
1002    <T as RowSource>::Item: Send + Sync + Filterable<'q> + Sortable<'q> + IntoRow<'q> + 'q,
1003{
1004    fn respond(&self, request: &Request) -> ResponseTemplate {
1005        trace!("Request URL: {}", request.url);
1006        let input_url = self.transform.transform(&request.url);
1007        trace!("Transformed URL: {}", input_url);
1008
1009        let data = self.row_source.get();
1010        let mut output_url = request.url.clone();
1011        if let Some(ref base) = self.base_uri {
1012            output_url.set_host(base.host_str()).unwrap();
1013            output_url.set_scheme(base.scheme()).unwrap();
1014            output_url.set_port(base.port()).unwrap();
1015        }
1016        let body = parse_query::<_, _>(
1017            &input_url,
1018            &output_url,
1019            data.get().into_iter(),
1020            self.default_limit,
1021        );
1022        match body {
1023            Ok(rs) => ResponseTemplate::new(200).set_body_json(rs.mock_json()),
1024            Err(e) => {
1025                debug!("Failed to respond to {}: {}", request.url, e);
1026                ResponseTemplate::new(500).set_body_string(e.to_string())
1027            }
1028        }
1029    }
1030}
1031
1032//// Context support
1033
1034impl<'q, T> ResponseSet<&T> {
1035    pub fn mock_json_with_context<A>(&self, access: A) -> serde_json::Value
1036    where
1037        T: IntoRowWithContext<'q, A>,
1038        A: 'q,
1039    {
1040        let mut map = serde_json::map::Map::new();
1041        map.insert(
1042            "count".to_string(),
1043            serde_json::Value::Number(serde_json::Number::from(self.total_matches)),
1044        );
1045        map.insert(
1046            "results".to_string(),
1047            to_rows_with_context(&self.contents, access),
1048        );
1049        map.insert(
1050            "next".to_string(),
1051            self.next
1052                .as_ref()
1053                .map(|x| serde_json::Value::String(x.clone()))
1054                .unwrap_or(serde_json::Value::Null),
1055        );
1056        map.insert(
1057            "previous".to_string(),
1058            self.prev
1059                .as_ref()
1060                .map(|x| serde_json::Value::String(x.clone()))
1061                .unwrap_or(serde_json::Value::Null),
1062        );
1063
1064        serde_json::Value::Object(map)
1065    }
1066}
1067
1068fn to_rows_with_context<'q, T: IntoRowWithContext<'q, A>, A: 'q>(
1069    data: &[&T],
1070    access: A,
1071) -> serde_json::Value {
1072    let mut array = Vec::new();
1073    let ser = T::get_serializer(access);
1074    for item in data {
1075        array.push(ser.to_json(item));
1076    }
1077    serde_json::Value::Array(array)
1078}
1079
1080fn parse_query_with_context<'a, 'q, R, I, A>(
1081    input_url: &Url,
1082    output_url: &Url,
1083    access: A,
1084    iter: I,
1085    default_limit: Option<usize>,
1086) -> Result<ResponseSet<&'a R>, MockError>
1087where
1088    // Note the 'q bound is a consequence of Sortable's implementation for StackedSorter in OrderingSet
1089    R: FilterableWithContext<'q, A> + SortableWithContext<'q, A> + IntoRowWithContext<'q, A> + 'q,
1090    I: Iterator<Item = &'a R>,
1091    A: Clone + 'q,
1092{
1093    let mut rb = ResponseSetBuilder::new(default_limit);
1094    let qr = OperatorSetWithContext::<R, A>::new(access.clone());
1095    let sr = OrderingSetWithContext::<R, A>::new(access);
1096    let pairs = input_url.query_pairs();
1097    for (key, value) in pairs {
1098        match key.as_ref() {
1099            "ordering" => {
1100                rb.order_by(sr.create_sort(&*value)?);
1101            }
1102            "offset" => {
1103                let v = usize::from_str(value.as_ref())?;
1104                rb.offset(v);
1105            }
1106            "limit" => {
1107                let v = usize::from_str(value.as_ref())?;
1108                rb.limit(v);
1109            }
1110            _ => {
1111                if let Ok(filter) = qr.create_filter_from_query_pair(&key, &value) {
1112                    rb.filter_by(filter);
1113                    continue;
1114                }
1115                return Err(MockError::UnknownQueryParameter(String::from(key.as_ref())));
1116            }
1117        }
1118    }
1119    let response = rb.apply(iter);
1120    Ok(ResponseSet::new(
1121        response.data,
1122        response.total,
1123        response
1124            .next
1125            .map(|page| make_page_url(output_url, page.offset, page.limit).to_string()),
1126        response
1127            .prev
1128            .map(|page| make_page_url(output_url, page.offset, page.limit).to_string()),
1129    ))
1130}
1131
1132/// A Django-style endpoint for a collection of objects that require a
1133/// context value.
1134///
1135/// [`EndpointWithContext`] implements
1136/// [`wiremock::Respond`] and so can be mounted directly into a
1137/// [`wiremock::MockServer`]. It contains a [`RowSource`] which it
1138/// will query for a fresh data on each query.
1139///
1140/// The primary distinction between this type and [`Endpoint`] is that
1141/// it can be used with types that require a context object, and so
1142/// relies on the extended traits [`FilterableWithContext`],
1143/// [`IntoRowWithContext`] and [`SortableWithContext`].
1144///
1145/// Any supplied [`RowSource`] needs to generate [`RowSet`] values
1146/// that double as the context value required by these traits.
1147#[cfg_attr(docsrs, doc(cfg(feature = "wiremock")))]
1148pub struct EndpointWithContext<T>
1149where
1150    T: Send + Sync + RowSource,
1151    for<'t> &'t <<T as RowSource>::Rows as RowSet>::Target:
1152        IntoIterator<Item = &'t <T as RowSource>::Item>,
1153{
1154    row_source: T,
1155    base_uri: Option<Url>,
1156    default_limit: Option<usize>,
1157}
1158
1159impl<'q, T> EndpointWithContext<T>
1160where
1161    T: Send + Sync + RowSource,
1162    for<'t> &'t <<T as RowSource>::Rows as RowSet>::Target:
1163        IntoIterator<Item = &'t <T as RowSource>::Item>,
1164    // <T as RowSource>::Item: Send + Sync + FilterableWithContext<'q> + SortableWithContext<'q> ,
1165{
1166    /// Create a new [`EndpointWithContext`].
1167    ///
1168    /// `row_source` is where the data comes from. `base_uri`, if
1169    /// provided, should be the wiremock server URI.
1170    ///
1171    /// `base_uri` is only required because wiremock mangles the
1172    /// request URL such that it's not possible to inspect it to see
1173    /// how to give out other URLs on the same server; the port
1174    /// number, which is usually random when mocking, is lost. Since
1175    /// Django includes full absolute URLs for the next and preceding
1176    /// pages for a query, mimicking this is impossible without more
1177    /// information (i.e. including usable URLs for the next page
1178    /// within the query response is impossible).
1179    pub fn new(row_source: T, base_uri: Option<&str>) -> Self {
1180        Self {
1181            row_source,
1182            base_uri: base_uri.map(|x| Url::parse(x).unwrap()),
1183            default_limit: None,
1184        }
1185    }
1186
1187    /// Set the default number of results returned
1188    ///
1189    /// This is the value used when no limit is specified in the
1190    /// query.  This value is configurable in Django; the default
1191    /// behaviour of this mock endpoint is to return everything, but
1192    /// that makes testing code that uses the default pagination more
1193    /// difficult.
1194    pub fn default_limit(&mut self, limit: usize) {
1195        self.default_limit = Some(limit);
1196    }
1197}
1198
1199impl<'q, T, A, R> Respond for EndpointWithContext<T>
1200where
1201    T: Send + Sync + RowSource<Rows = A, Item = R>,
1202    for<'t> &'t <A as RowSet>::Target: IntoIterator<Item = &'t R>,
1203    // Note the 'q bound is a consequence of Sortable's implementation for StackedSorter in OrderingSet
1204    R: Send
1205        + Sync
1206        + FilterableWithContext<'q, A>
1207        + SortableWithContext<'q, A>
1208        + IntoRowWithContext<'q, A>
1209        + 'q,
1210    A: Clone + 'q + RowSet,
1211{
1212    fn respond(&self, request: &Request) -> ResponseTemplate {
1213        // All of this is to work around the mess in wiremock that means
1214        // request.uri is inaccurate, and can't be used.
1215        let mut u = request.url.clone();
1216        if let Some(ref base) = self.base_uri {
1217            u.set_host(base.host_str()).unwrap();
1218            u.set_scheme(base.scheme()).unwrap();
1219            u.set_port(base.port()).unwrap();
1220        }
1221        let res = {
1222            let access = self.row_source.get();
1223            let res = {
1224                let into_iter = access.clone();
1225                let rows = into_iter.get().into_iter();
1226                let body = parse_query_with_context(
1227                    &request.url,
1228                    &u,
1229                    access.clone(),
1230                    rows,
1231                    self.default_limit,
1232                );
1233                match body {
1234                    Ok(rs) => {
1235                        let bb = rs.mock_json_with_context(access);
1236                        ResponseTemplate::new(200).set_body_json(bb)
1237                    }
1238                    Err(e) => {
1239                        debug!("Failed to respond to {}: {}", request.url, e);
1240                        ResponseTemplate::new(500).set_body_string(e.to_string())
1241                    }
1242                }
1243            };
1244            res
1245        };
1246        res
1247    }
1248}
1249
1250/// A Django nested route for types that need a context value.
1251///
1252/// A nested route in Django is a way of specifying that a particular
1253/// filter field must be present when querying for a particular object
1254/// type, and that the filter type must be exact match. So for
1255/// example, if all objects of type `Foo` have a reference to a `Bar`,
1256/// and its too expensive or undesirable to query for `Foo`s without
1257/// specifying which `Bar` they come from, then you can express this
1258/// in Django as a nested route:
1259///
1260///   ``http://example.com/bars/1/foos``
1261///
1262/// where in order to ask any question about `Foo`s you must first
1263/// identify a particular associated `Bar`.
1264///
1265/// Since the django-query crate operates on query parameters, the
1266/// simplest way to handle this is to convert any relevant parts of
1267/// the path into query parameters as required, which is all this type
1268/// does. See its construction parameters [`NestedEndpointParams`] for
1269/// the details. This type is otherwise identical to
1270/// [`EndpointWithContext`].
1271#[cfg_attr(docsrs, doc(cfg(feature = "wiremock")))]
1272pub struct NestedEndpointWithContext<T>
1273where
1274    T: Send + Sync + RowSource,
1275    for<'t> &'t <<T as RowSource>::Rows as RowSet>::Target:
1276        IntoIterator<Item = &'t <T as RowSource>::Item>,
1277{
1278    transform: UrlTransform,
1279    row_source: T,
1280    base_uri: Option<Url>,
1281    default_limit: Option<usize>,
1282}
1283
1284impl<'q, T> NestedEndpointWithContext<T>
1285where
1286    T: Send + Sync + RowSource,
1287    for<'t> &'t <<T as RowSource>::Rows as RowSet>::Target:
1288        IntoIterator<Item = &'t <T as RowSource>::Item>,
1289    // <T as RowSource>::Item: Send + Sync + FilterableWithContext<'q> + SortableWithContext<'q> ,
1290{
1291    /// Create a new [`NestedEndpointWithContext`].
1292    ///
1293    /// The arguments to this function are wrapped in their own
1294    /// structure: [`NestedEndpointParams`].
1295    pub fn new(row_source: T, p: NestedEndpointParams<'_>) -> Self {
1296        Self {
1297            transform: UrlTransform::new(
1298                &format!(r"^{}/{}/(?P<parent>[^/]+)/{}/$", p.root, p.parent, p.child),
1299                &format!("{}/{}/", p.root, p.child),
1300                vec![(&*p.parent_query, "${parent}")],
1301            ),
1302            row_source,
1303            base_uri: p.base_uri.map(|x| Url::parse(x).unwrap()),
1304            default_limit: None,
1305        }
1306    }
1307
1308    pub fn default_limit(&mut self, limit: usize) {
1309        self.default_limit = Some(limit);
1310    }
1311}
1312
1313impl<'q, T, A, R> Respond for NestedEndpointWithContext<T>
1314where
1315    T: Send + Sync + RowSource<Rows = A, Item = R>,
1316    for<'t> &'t <A as RowSet>::Target: IntoIterator<Item = &'t R>,
1317    // Note the 'q bound is a consequence of Sortable's implementation for StackedSorter in OrderingSet
1318    R: Send
1319        + Sync
1320        + FilterableWithContext<'q, A>
1321        + SortableWithContext<'q, A>
1322        + IntoRowWithContext<'q, A>
1323        + 'q,
1324    A: Clone + 'q + RowSet,
1325{
1326    fn respond(&self, request: &Request) -> ResponseTemplate {
1327        trace!("Request URL: {}", request.url);
1328        let input_url = self.transform.transform(&request.url);
1329        trace!("Transformed URL: {}", input_url);
1330
1331        let mut output_url = request.url.clone();
1332        if let Some(ref base) = self.base_uri {
1333            output_url.set_host(base.host_str()).unwrap();
1334            output_url.set_scheme(base.scheme()).unwrap();
1335            output_url.set_port(base.port()).unwrap();
1336        }
1337
1338        let res = {
1339            let access = self.row_source.get();
1340            let res = {
1341                let into_iter = access.clone();
1342                let rows = into_iter.get().into_iter();
1343                let body = parse_query_with_context(
1344                    &input_url,
1345                    &output_url,
1346                    access.clone(),
1347                    rows,
1348                    self.default_limit,
1349                );
1350                match body {
1351                    Ok(rs) => {
1352                        let bb = rs.mock_json_with_context(access);
1353                        ResponseTemplate::new(200).set_body_json(bb)
1354                    }
1355                    Err(e) => {
1356                        debug!("Failed to respond to {}: {}", request.url, e);
1357                        ResponseTemplate::new(500).set_body_string(e.to_string())
1358                    }
1359                }
1360            };
1361            res
1362        };
1363        res
1364    }
1365}
1366
1367#[cfg(test)]
1368mod tests {
1369    use super::*;
1370
1371    use test_log::test;
1372    use wiremock::http::Url;
1373
1374    #[test]
1375    fn test_transform() {
1376        let u = Url::parse("http://foo.bar/jobs/3235/tests/?name=womble&path=bongle")
1377            .expect("failed to parse url");
1378        let t = UrlTransform::new(r"^/jobs/(\d+)/tests/$", r"/tests/$1", vec![("job", "$1")]);
1379        let v = t.transform(&u);
1380        assert_eq!(
1381            v,
1382            Url::parse("http://foo.bar/tests/3235?name=womble&path=bongle&job=3235")
1383                .expect("failed to parse url")
1384        );
1385
1386        let u = Url::parse("http://foo.bar/jobs/hello/tests/?name=womble&path=bongle")
1387            .expect("failed to parse url");
1388        let v = t.transform(&u);
1389        assert_eq!(v, u);
1390
1391        let u = Url::parse(
1392            "http://foo.bar/jobs/snomble/bomble/tests/sniffle_snaffle?name=womble&path=bongle",
1393        )
1394        .expect("failed to parse url");
1395        let t = UrlTransform::new(
1396            r"^/jobs/(?P<name>.*)/tests/(?P<womble>.*)$",
1397            r"/tests/${name}",
1398            vec![("job", "$womble")],
1399        );
1400        let v = t.transform(&u);
1401        assert_eq!(
1402            v,
1403            Url::parse(
1404                "http://foo.bar/tests/snomble/bomble?name=womble&path=bongle&job=sniffle_snaffle"
1405            )
1406            .expect("failed to parse url")
1407        );
1408
1409        let u = Url::parse("http://foo.bar/jobs/hello/tests/?name=womble&path=bongle")
1410            .expect("failed to parse url");
1411        let v = t.transform(&u);
1412        assert_eq!(
1413            v,
1414            Url::parse("http://foo.bar/tests/hello?name=womble&path=bongle&job=")
1415                .expect("failed to parse url")
1416        );
1417    }
1418}