Skip to main content

cratestack_client_rust/client/
route.rs

1//! The route a call binds into its COSE envelope (cratestack#1007).
2//!
3//! A sealed request binds the route *template* (`/widgets/{id}`), not the
4//! URL it was sent to, plus the values the template's parameters took: a
5//! gateway may rewrite the path, and the template alone would not tell the
6//! answer for `/widgets/1` from the answer for `/widgets/2`. The client
7//! builds concrete paths, so the generated code says which template a call
8//! is for. For RPC the route is the op id.
9
10/// The route template of one call, and the values of its `{parameters}` in
11/// template order.
12///
13/// The template is the schema's own path string, the same one the server
14/// lists in `ROUTE_TRANSPORTS`, without the mount prefix; for an RPC call it
15/// is the op id (`model.Widget.get`, `procedure.ping`) or `batch`. Without
16/// the `cose` feature, or without an envelope on the client, a `RouteRef`
17/// is ignored.
18///
19/// ```
20/// use cratestack_client_rust::RouteRef;
21///
22/// let id = 7.to_string();
23/// let params = [id.as_str()];
24/// let route = RouteRef::new("/widgets/{id}", &params);
25/// assert_eq!(route.template(), "/widgets/{id}");
26/// assert_eq!(route.params(), ["7"]);
27/// assert_eq!(RouteRef::rpc("model.Widget.list").params().len(), 0);
28/// ```
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub struct RouteRef<'a> {
31    template: &'a str,
32    params: &'a [&'a str],
33}
34
35impl<'a> RouteRef<'a> {
36    /// A REST route: its template and the decoded values of its parameters,
37    /// in template order.
38    pub const fn new(template: &'a str, params: &'a [&'a str]) -> Self {
39        Self { template, params }
40    }
41
42    /// An RPC route: the op id, or `"batch"`. RPC has no path parameters.
43    pub const fn rpc(op_id: &'a str) -> Self {
44        Self {
45            template: op_id,
46            params: &[],
47        }
48    }
49
50    pub const fn template(&self) -> &'a str {
51        self.template
52    }
53
54    pub const fn params(&self) -> &'a [&'a str] {
55        self.params
56    }
57}
58
59/// `segment` as one URL path segment: everything but the RFC 3986 unreserved
60/// characters is percent-encoded, so an id such as `50%off` or `a/b` reaches
61/// the server as the value the seal binds.
62///
63/// ```
64/// use cratestack_client_rust::encode_path_segment;
65///
66/// assert_eq!(encode_path_segment("abc-1.2_3~"), "abc-1.2_3~");
67/// assert_eq!(encode_path_segment("50%off"), "50%25off");
68/// assert_eq!(encode_path_segment("a/b c"), "a%2Fb%20c");
69/// ```
70pub fn encode_path_segment(segment: &str) -> String {
71    use std::fmt::Write as _;
72    let mut out = String::with_capacity(segment.len());
73    for byte in segment.bytes() {
74        if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~') {
75            out.push(char::from(byte));
76        } else {
77            let _ = write!(out, "%{byte:02X}");
78        }
79    }
80    out
81}