Skip to main content

rama_net/uri/
query_mut.rs

1//! RAII guard for incremental query mutation.
2//!
3//! Created by [`Uri::query_mut`](super::Uri::query_mut). Holds the
4//! Owned representation of the URI and lets callers push pairs (or
5//! bare keys), pop the last pair as an owned [`QueryPair`], drain
6//! all pairs, or remove / replace / retain pairs by name.
7
8use super::component_input::IntoUriComponent;
9use super::encode;
10use super::owned::OwnedUriRef;
11use super::query::{Query, QueryPair, QueryPairRef, QueryRef, form_decode_bytes};
12
13use rama_core::bytes::{Bytes, BytesMut};
14
15/// Mutable view of a [`Uri`](super::Uri)'s query component.
16///
17/// Pushes append to the existing query, auto-encoding bytes outside the
18/// pair grammar. The first push promotes a `None` query to `Some(empty)`
19/// (i.e. adds a `?` to the wire form). Drop releases the borrow.
20pub struct QueryMut<'a> {
21    owned: &'a mut OwnedUriRef,
22}
23
24impl<'a> QueryMut<'a> {
25    #[inline]
26    pub(crate) fn new(owned: &'a mut OwnedUriRef) -> Self {
27        Self { owned }
28    }
29
30    /// Append a `name=value` pair. Both `name` and `value` are
31    /// percent-encoded under the pair policy (encode `&`, `=`, `+`,
32    /// `%`, and everything outside `pchar`).
33    #[expect(
34        clippy::needless_pass_by_value,
35        reason = "by-value matches IntoUriComponent's signature on sibling setters"
36    )]
37    pub fn push_pair(
38        &mut self,
39        name: impl IntoUriComponent,
40        value: impl IntoUriComponent,
41    ) -> &mut Self {
42        let buf = self.buf_for_append();
43        encode::extend_encoded_pair(buf, &name);
44        buf.extend_from_slice(b"=");
45        encode::extend_encoded_pair(buf, &value);
46        self
47    }
48
49    /// Append a bare key (no `=`). Same encoding policy as
50    /// [`push_pair`](Self::push_pair).
51    #[expect(
52        clippy::needless_pass_by_value,
53        reason = "by-value matches IntoUriComponent's signature on sibling setters"
54    )]
55    pub fn push_key(&mut self, name: impl IntoUriComponent) -> &mut Self {
56        let buf = self.buf_for_append();
57        encode::extend_encoded_pair(buf, &name);
58        self
59    }
60
61    /// Remove and return the last pair as an owned [`QueryPair`]. Returns
62    /// `None` when the query is empty or absent.
63    ///
64    /// The pair's bytes are sliced into a refcounted [`Bytes`] from the
65    /// underlying query buffer — no copy.
66    pub fn pop(&mut self) -> Option<QueryPair> {
67        loop {
68            let q = self.owned.query.as_mut()?;
69            if q.bytes.is_empty() {
70                return None;
71            }
72            let pair_bytes = match memchr::memrchr(b'&', &q.bytes) {
73                Some(i) => {
74                    // Split [..i] | [i..]. The tail starts with `&`; trim it.
75                    let mut tail = q.bytes.split_off(i);
76                    let _amp = tail.split_to(1);
77                    tail
78                }
79                None => core::mem::take(&mut q.bytes),
80            };
81            if pair_bytes.is_empty() {
82                // Trailing `&` with nothing after — skip and try again.
83                continue;
84            }
85            return Some(QueryPair::from_raw(pair_bytes.freeze()));
86        }
87    }
88
89    /// Empty the query content and return an iterator yielding the
90    /// removed pairs as owned [`QueryPair`]s.
91    ///
92    /// The query stays `Some(empty)` after this — the `?` remains on
93    /// the wire. Call [`Uri::unset_query`](super::Uri::unset_query) to
94    /// remove the `?` entirely.
95    pub fn drain(&mut self) -> Drain {
96        let bytes = match self.owned.query.as_mut() {
97            Some(q) => core::mem::take(&mut q.bytes).freeze(),
98            None => Bytes::new(),
99        };
100        Drain { bytes, offset: 0 }
101    }
102
103    /// Keep only the pairs for which `keep` returns `true`, preserving
104    /// their order and raw bytes. Empty `&`-fragments (`&&`, leading /
105    /// trailing `&`) are dropped as part of the rebuild.
106    ///
107    /// Like [`drain`](Self::drain), the query stays `Some(_)` (the `?`
108    /// remains on the wire) even when every pair is removed.
109    pub fn retain(&mut self, mut keep: impl FnMut(QueryPairRef<'_>) -> bool) -> &mut Self {
110        let Some(q) = self.owned.query.as_mut() else {
111            return self;
112        };
113        let old = core::mem::take(&mut q.bytes);
114        let mut new = BytesMut::with_capacity(old.len());
115        for pair in QueryRef::new(&old).pairs() {
116            if keep(pair) {
117                if !new.is_empty() {
118                    new.extend_from_slice(b"&");
119                }
120                new.extend_from_slice(pair.raw_bytes());
121            }
122        }
123        q.bytes = new;
124        self
125    }
126
127    /// Remove every pair whose form-decoded name equals `name` (bare keys
128    /// included). Returns the number of pairs removed.
129    ///
130    /// `name` is component text, compared form-decoded on both sides —
131    /// see [`QueryRef::first_value`](super::QueryRef::first_value) for the
132    /// matching rules.
133    #[expect(
134        clippy::needless_pass_by_value,
135        reason = "by-value matches IntoUriComponent's signature on sibling setters; this impl only borrows the input"
136    )]
137    pub fn remove(&mut self, name: impl IntoUriComponent) -> usize {
138        let name = name.as_uri_component_bytes();
139        let pattern = form_decode_bytes(&name).into_owned();
140        let mut removed = 0;
141        self.retain(|pair| {
142            let matches = *form_decode_bytes(pair.name_bytes()) == *pattern;
143            removed += usize::from(matches);
144            !matches
145        });
146        removed
147    }
148
149    /// Replace-or-append: remove every pair named `name` (form-decoded
150    /// comparison, see [`remove`](Self::remove)), then append `name=value`
151    /// under the [`push_pair`](Self::push_pair) encoding policy. The pair
152    /// always ends up last, regardless of where the old ones sat.
153    pub fn set_pair(
154        &mut self,
155        name: impl IntoUriComponent,
156        value: impl IntoUriComponent,
157    ) -> &mut Self {
158        self.remove(&*name.as_uri_component_bytes());
159        self.push_pair(name, value)
160    }
161
162    /// Ensure the query is `Some(_)` and return `&mut BytesMut` for the
163    /// underlying buffer. Inserts `&` if the existing buffer is
164    /// non-empty so the next pair appends correctly.
165    fn buf_for_append(&mut self) -> &mut BytesMut {
166        let q = self.owned.query.get_or_insert_with(|| Query {
167            bytes: BytesMut::new(),
168        });
169        if !q.bytes.is_empty() {
170            q.bytes.extend_from_slice(b"&");
171        }
172        &mut q.bytes
173    }
174}
175
176impl core::fmt::Debug for QueryMut<'_> {
177    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
178        let query = self
179            .owned
180            .query
181            .as_ref()
182            // Safety: parser invariant — query bytes are valid UTF-8.
183            .map(|q| unsafe { core::str::from_utf8_unchecked(&q.bytes) });
184        f.debug_struct("QueryMut").field("query", &query).finish()
185    }
186}
187
188/// Iterator yielding owned [`QueryPair`]s drained from a [`QueryMut`].
189/// Created by [`QueryMut::drain`].
190#[derive(Debug, Clone)]
191pub struct Drain {
192    bytes: Bytes,
193    offset: usize,
194}
195
196impl Iterator for Drain {
197    type Item = QueryPair;
198
199    fn next(&mut self) -> Option<Self::Item> {
200        loop {
201            if self.offset >= self.bytes.len() {
202                return None;
203            }
204            // Find next `&` from current offset.
205            let remaining = &self.bytes[self.offset..];
206            let (start, end) = match memchr::memchr(b'&', remaining) {
207                Some(i) => (self.offset, self.offset + i),
208                None => (self.offset, self.bytes.len()),
209            };
210            self.offset = end + 1; // skip past `&` (or one-past-end if no `&`)
211
212            if start == end {
213                // Empty fragment (`&&`, leading/trailing `&`) — skip.
214                continue;
215            }
216
217            let fragment = self.bytes.slice(start..end);
218            return Some(QueryPair::from_raw(fragment));
219        }
220    }
221}
222
223impl core::iter::FusedIterator for Drain {}