Skip to main content

grpc_webnext/
httprule.rs

1//! `google.api.http` transcoding: map REST-style `(HTTP method, path)` requests
2//! onto gRPC methods, binding path segments, query params, and the request body
3//! into the request message.
4//!
5//! This implements HttpRule; `doc/HTTPRULE_GAPS.md` is the authoritative statement
6//! of the boundary, and as of 2026-07-28 there are no functional gaps in the method
7//! option — only `HttpRule.selector` (service-config rules), declined.
8//!
9//! `go/webnext/httprule.go` is the deliberately parallel port of this file — same
10//! segment/body model, same matching order, same coercion table — so the two
11//! implementations can be compared side by side. **Change one, change the other in
12//! the same commit.**
13//!
14//! Two ideas carry most of the weight:
15//!
16//!   (`Segment` and `set_from_json` below are private, so they are named here rather than
17//!   linked — this is a tour of the file, not of the public API.)
18//!
19//!   * **Captures are patterns, not segments.** `Segment::Capture` holds its own
20//!     sub-pattern, so `{f}`, `{f=**}` and `{f=shelves/*/books/*}` are one case
21//!     rather than three. Splitting is brace-aware for exactly that reason.
22//!   * **Conversions belong to the JSON decoder.** Anything without a scalar form —
23//!     `bytes`, the well-known types, a `body:` naming a non-message field — is set
24//!     by handing its protobuf-JSON text to the decoder rather than parsed here.
25//!     See `set_from_json`.
26
27use prost_reflect::prost::Message;
28use prost_reflect::{
29    DescriptorPool, DynamicMessage, FieldDescriptor, Kind, MessageDescriptor, ReflectMessage, Value,
30};
31
32use crate::transcode::TranscodeError;
33
34/// The extension full name that carries an HttpRule on a method.
35const HTTP_EXT: &str = "google.api.http";
36
37/// A parsed path-template segment.
38///
39/// Note there is no separate "single capture" / "rest capture": both are a
40/// [`Segment::Capture`] over a sub-pattern, which is what lets a capture span
41/// several segments (`{name=shelves/*/books/*}`). `{f}` is sugar for `{f=*}`.
42#[derive(Debug, Clone)]
43enum Segment {
44    /// A fixed path component that must match exactly.
45    Literal(String),
46    /// A bare `*` — matches exactly one component, capturing nothing.
47    AnyOne,
48    /// A bare `**` — matches the remaining components, capturing nothing.
49    AnyRest,
50    /// `{field=<sub-pattern>}` — binds whatever the sub-pattern consumed, joined
51    /// by `/`, to a (dotted) field path. Sub-segments are never captures
52    /// themselves; the grammar does not nest them.
53    Capture(Vec<String>, Vec<Segment>),
54}
55
56/// How the request body maps onto the message.
57#[derive(Debug, Clone)]
58enum BodyRule {
59    /// `body: "*"` — the whole JSON body is the request message.
60    Wildcard,
61    /// `body: "<field>"` — the JSON body is a single (message) field.
62    Field(String),
63    /// No body.
64    None,
65}
66
67/// One compiled HTTP binding: `(method, template)` -> a gRPC method + how to build
68/// its request message.
69#[derive(Debug, Clone)]
70struct Binding {
71    http_method: String, // upper-case, e.g. "GET"
72    segments: Vec<Segment>,
73    /// The template's trailing `:verb` (without the colon), empty if it has none.
74    /// It is *matched*, not merely stripped — see [`match_segments`].
75    custom_verb: String,
76    body: BodyRule,
77    /// `response_body` — the top-level response field to return instead of the
78    /// whole message. Empty means the whole message.
79    response_body: String,
80    grpc_method: String, // "/pkg.Service/Method"
81    input: MessageDescriptor,
82}
83
84/// A captured path variable: the dotted field path it binds to, and its string value —
85/// e.g. `(["user", "id"], "123")` for a `{user.id}` template segment.
86type PathVar = (Vec<String>, String);
87
88/// The transcoded call: which gRPC method to invoke and the encoded request.
89pub struct HttpCall {
90    pub grpc_method: String,
91    pub message: Vec<u8>,
92    /// The binding's `response_body`, empty for the whole message. Carried on the
93    /// call because the response is encoded long after the binding is matched.
94    pub response_body: String,
95}
96
97/// A WebSocket route resolved from an annotation URL: the target gRPC method plus
98/// the path/query bindings, used to build each streamed request message.
99pub struct WsBinding {
100    binding: Binding,
101    vars: Vec<PathVar>,
102    query: Option<String>,
103}
104
105impl WsBinding {
106    /// The gRPC method this annotation route maps to.
107    pub fn grpc_method(&self) -> &str {
108        &self.binding.grpc_method
109    }
110
111    /// Whether the route takes a request body. `false` for GET-style server-streams,
112    /// where the request comes entirely from the URL (path + query).
113    pub fn has_body(&self) -> bool {
114        !matches!(self.binding.body, BodyRule::None)
115    }
116
117    /// The binding's `response_body` — the top-level response field to return
118    /// instead of the whole message. Empty means the whole message.
119    pub fn response_body(&self) -> &str {
120        &self.binding.response_body
121    }
122
123    /// Build a request message from a body payload, overlaying the URL path/query.
124    pub fn build_message(&self, body: &[u8]) -> Result<Vec<u8>, TranscodeError> {
125        build_message(&self.binding, &self.vars, self.query.as_deref(), body)
126    }
127}
128
129/// A table of HTTP bindings compiled from a descriptor pool.
130#[derive(Clone, Default)]
131pub struct HttpRouter {
132    bindings: Vec<Binding>,
133}
134
135impl HttpRouter {
136    /// Compile all `google.api.http` bindings in the pool. Empty if the pool has
137    /// no annotations (or the extension isn't present).
138    pub fn from_pool(pool: &DescriptorPool) -> Self {
139        let mut bindings = Vec::new();
140        let Some(ext) = pool.get_extension_by_name(HTTP_EXT) else {
141            return Self { bindings };
142        };
143        for service in pool.services() {
144            for method in service.methods() {
145                let opts = method.options();
146                if !opts.has_extension(&ext) {
147                    continue;
148                }
149                let grpc_method = format!("/{}/{}", service.full_name(), method.name());
150                let input = method.input();
151                let rule = opts.get_extension(&ext);
152                if let Some(rule) = rule.as_message() {
153                    collect_rule(&mut bindings, rule, &grpc_method, &input);
154                }
155            }
156        }
157        Self { bindings }
158    }
159
160    pub fn is_empty(&self) -> bool {
161        self.bindings.is_empty()
162    }
163
164    /// Match a WebSocket upgrade path against the annotation bindings. A WS upgrade
165    /// is always an HTTP GET, so this matches on the path only (verb-agnostic).
166    pub fn match_ws(&self, path: &str, query: Option<&str>) -> Option<WsBinding> {
167        for b in &self.bindings {
168            if let Some(vars) = match_segments(&b.segments, &b.custom_verb, path) {
169                return Some(WsBinding {
170                    binding: b.clone(),
171                    vars,
172                    query: query.map(str::to_string),
173                });
174            }
175        }
176        None
177    }
178
179    /// Whether a gRPC method (`/pkg.Service/Method`) has any HTTP annotation.
180    pub fn is_annotated(&self, grpc_method: &str) -> bool {
181        self.bindings.iter().any(|b| b.grpc_method == grpc_method)
182    }
183
184    /// Find a binding matching `(method, path)` and return it plus the captured
185    /// path variables (dotted field path -> value).
186    fn match_request(&self, method: &str, path: &str) -> Option<(&Binding, Vec<PathVar>)> {
187        let want = method.to_ascii_uppercase();
188        for b in &self.bindings {
189            // `custom { kind: "*" }` is HttpRule's "leave the HTTP method
190            // unspecified for this rule" — it matches any verb, not a literal `*`.
191            if b.http_method != want && b.http_method != "*" {
192                continue;
193            }
194            if let Some(vars) = match_segments(&b.segments, &b.custom_verb, path) {
195                return Some((b, vars));
196            }
197        }
198        None
199    }
200
201    /// Transcode a REST request into a gRPC call, or `None` if no binding matches.
202    pub fn transcode(
203        &self,
204        method: &str,
205        path: &str,
206        query: Option<&str>,
207        body: &[u8],
208    ) -> Result<Option<HttpCall>, TranscodeError> {
209        let Some((binding, vars)) = self.match_request(method, path) else {
210            return Ok(None);
211        };
212        let message = build_message(binding, &vars, query, body)?;
213        Ok(Some(HttpCall {
214            grpc_method: binding.grpc_method.clone(),
215            message,
216            response_body: binding.response_body.clone(),
217        }))
218    }
219}
220
221/// Push the top rule and any `additional_bindings` (one level) as bindings.
222fn collect_rule(bindings: &mut Vec<Binding>, rule: &DynamicMessage, grpc_method: &str, input: &MessageDescriptor) {
223    push_binding(bindings, rule, grpc_method, input);
224    if let Some(list) = rule.get_field_by_name("additional_bindings") {
225        if let Some(items) = list.as_list() {
226            for item in items {
227                if let Some(m) = item.as_message() {
228                    push_binding(bindings, m, grpc_method, input);
229                }
230            }
231        }
232    }
233}
234
235fn push_binding(bindings: &mut Vec<Binding>, rule: &DynamicMessage, grpc_method: &str, input: &MessageDescriptor) {
236    let Some((http_method, template)) = verb_and_path(rule) else {
237        return;
238    };
239    let (segments, custom_verb) = parse_template(&template);
240    bindings.push(Binding {
241        http_method,
242        segments,
243        custom_verb,
244        body: body_rule(rule),
245        response_body: rule
246            .get_field_by_name("response_body")
247            .and_then(|v| v.as_str().map(str::to_string))
248            .unwrap_or_default(),
249        grpc_method: grpc_method.to_string(),
250        input: input.clone(),
251    });
252}
253
254/// Extract the verb + path from an HttpRule's `pattern` oneof.
255fn verb_and_path(rule: &DynamicMessage) -> Option<(String, String)> {
256    for (field, verb) in [("get", "GET"), ("put", "PUT"), ("post", "POST"), ("delete", "DELETE"), ("patch", "PATCH")] {
257        if let Some(v) = rule.get_field_by_name(field) {
258            if let Some(s) = v.as_str() {
259                if !s.is_empty() {
260                    return Some((verb.to_string(), s.to_string()));
261                }
262            }
263        }
264    }
265    // custom { kind, path }
266    if let Some(v) = rule.get_field_by_name("custom") {
267        if let Some(m) = v.as_message() {
268            let kind = m.get_field_by_name("kind").and_then(|k| k.as_str().map(str::to_string)).unwrap_or_default();
269            let path = m.get_field_by_name("path").and_then(|p| p.as_str().map(str::to_string)).unwrap_or_default();
270            if !kind.is_empty() {
271                return Some((kind.to_ascii_uppercase(), path));
272            }
273        }
274    }
275    None
276}
277
278fn body_rule(rule: &DynamicMessage) -> BodyRule {
279    match rule.get_field_by_name("body").and_then(|b| b.as_str().map(str::to_string)).unwrap_or_default().as_str() {
280        "" => BodyRule::None,
281        "*" => BodyRule::Wildcard,
282        field => BodyRule::Field(field.to_string()),
283    }
284}
285
286/// Split `s` on `sep`, ignoring separators that appear inside `{...}`.
287///
288/// This is the whole reason multi-segment captures work: splitting the raw
289/// template on `/` first would tear `{name=shelves/*}` into pieces that no longer
290/// look like a capture, which is precisely how they used to compile into dead
291/// literal routes.
292fn split_outside_braces(s: &str, sep: char) -> Vec<&str> {
293    let mut out = Vec::new();
294    let mut depth = 0usize;
295    let mut start = 0usize;
296    for (i, c) in s.char_indices() {
297        match c {
298            '{' => depth += 1,
299            '}' => depth = depth.saturating_sub(1),
300            _ if c == sep && depth == 0 => {
301                out.push(&s[start..i]);
302                start = i + c.len_utf8();
303            }
304            _ => {}
305        }
306    }
307    out.push(&s[start..]);
308    out
309}
310
311/// Parse a path template into segments plus its trailing custom verb
312/// (`/v1/things/{id}:cancel` -> the `cancel`), which the caller matches rather
313/// than discards.
314fn parse_template(template: &str) -> (Vec<Segment>, String) {
315    let parts = split_outside_braces(template, ':');
316    let path = parts[0];
317    let custom_verb = if parts.len() > 1 { parts[1..].join(":") } else { String::new() };
318
319    let segments = split_outside_braces(path.trim_matches('/'), '/')
320        .into_iter()
321        .filter(|s| !s.is_empty())
322        .map(parse_segment)
323        .collect();
324    (segments, custom_verb)
325}
326
327/// One template segment: a capture (with its own sub-pattern) or a plain one.
328fn parse_segment(seg: &str) -> Segment {
329    if let Some(inner) = seg.strip_prefix('{').and_then(|s| s.strip_suffix('}')) {
330        let (field, pattern) = inner.split_once('=').unwrap_or((inner, "*"));
331        let field_path: Vec<String> = field.split('.').map(str::to_string).collect();
332        let subs = pattern
333            .trim_matches('/')
334            .split('/')
335            .filter(|s| !s.is_empty())
336            .map(plain_segment)
337            .collect();
338        return Segment::Capture(field_path, subs);
339    }
340    plain_segment(seg)
341}
342
343/// A non-capturing segment: a wildcard or a literal.
344fn plain_segment(seg: &str) -> Segment {
345    match seg {
346        "*" => Segment::AnyOne,
347        "**" => Segment::AnyRest,
348        _ => Segment::Literal(seg.to_string()),
349    }
350}
351
352/// Match a request path against a template, returning captured `(field_path, value)`.
353///
354/// `custom_verb` is the template's trailing `:verb`, and it is part of the match in
355/// both directions: a binding that declares one matches only paths carrying it (the
356/// verb is stripped before the last segment binds), and a binding that declares none
357/// never matches a path that carries one. Stripping the verb from the *template*
358/// alone would make `/v1/things/{id}:cancel` match the bare `/v1/things/5` and
359/// capture `id = "5:cancel"` from `/v1/things/5:cancel`, with every custom verb on a
360/// resource collapsing onto one template. A genuine colon in path data is
361/// percent-encoded, so it survives this check.
362fn match_segments(segments: &[Segment], custom_verb: &str, path: &str) -> Option<Vec<PathVar>> {
363    let mut parts: Vec<&str> = path.trim_matches('/').split('/').filter(|s| !s.is_empty()).collect();
364    match parts.last() {
365        Some(last) => {
366            let (head, verb) = last.split_once(':').unwrap_or((last, ""));
367            if verb != custom_verb || (head.is_empty() && last.contains(':')) {
368                return None;
369            }
370            let index = parts.len() - 1;
371            parts[index] = head;
372        }
373        None if !custom_verb.is_empty() => return None,
374        None => {}
375    }
376    let mut vars = Vec::new();
377    let end = match_list(segments, &parts, 0, &mut vars)?;
378    (end == parts.len()).then_some(vars)
379}
380
381/// Match `segments` against `parts[i..]`, collecting captures. Returns the index
382/// just past what was consumed, or `None` if the pattern does not fit.
383fn match_list(segments: &[Segment], parts: &[&str], mut i: usize, vars: &mut Vec<PathVar>) -> Option<usize> {
384    for seg in segments {
385        match seg {
386            Segment::Literal(lit) => {
387                if parts.get(i) != Some(&lit.as_str()) {
388                    return None;
389                }
390                i += 1;
391            }
392            Segment::AnyOne => {
393                parts.get(i)?;
394                i += 1;
395            }
396            Segment::AnyRest => i = parts.len(),
397            Segment::Capture(field, subs) => {
398                let start = i;
399                i = match_list(subs, parts, i, vars)?;
400                // The captured value is everything the sub-pattern consumed. Each
401                // component is decoded separately, then joined — so an encoded `%2F`
402                // inside a component never reads as a separator.
403                let value =
404                    parts[start..i].iter().map(|p| percent_decode(p)).collect::<Vec<_>>().join("/");
405                vars.push((field.clone(), value));
406            }
407        }
408    }
409    Some(i)
410}
411
412/// Build the encoded request message from a matched binding + inputs.
413fn build_message(
414    binding: &Binding,
415    vars: &[PathVar],
416    query: Option<&str>,
417    body: &[u8],
418) -> Result<Vec<u8>, TranscodeError> {
419    let mut msg = match &binding.body {
420        BodyRule::Wildcard => deserialize_message(binding.input.clone(), body)?,
421        BodyRule::None => DynamicMessage::new(binding.input.clone()),
422        BodyRule::Field(field) => {
423            let mut m = DynamicMessage::new(binding.input.clone());
424            if !body.is_empty() {
425                set_message_field(&mut m, field, body)?;
426            }
427            m
428        }
429    };
430
431    for (field_path, value) in vars {
432        set_by_path(&mut msg, field_path, value)?;
433    }
434
435    // Query params bind fields not carried by a wildcard body.
436    if !matches!(binding.body, BodyRule::Wildcard) {
437        if let Some(q) = query {
438            for (key, value) in parse_query(q) {
439                let field_path: Vec<String> = key.split('.').map(str::to_string).collect();
440                if vars.iter().any(|(fp, _)| *fp == field_path) {
441                    continue; // already set from the path
442                }
443                set_by_path(&mut msg, &field_path, &value)?;
444            }
445        }
446    }
447
448    Ok(msg.encode_to_vec())
449}
450
451fn deserialize_message(desc: MessageDescriptor, json: &[u8]) -> Result<DynamicMessage, TranscodeError> {
452    if json.is_empty() {
453        return Ok(DynamicMessage::new(desc));
454    }
455    let mut de = serde_json::Deserializer::from_slice(json);
456    let msg = DynamicMessage::deserialize(desc, &mut de)?;
457    de.end()?;
458    Ok(msg)
459}
460
461/// Resolve a field by its `.proto` name, falling back to its JSON (lowerCamelCase)
462/// name. URLs are written by hand — in an annotation template by the service author,
463/// in a query string by the caller — and both conventions turn up in practice, so
464/// both resolve. grpc-gateway does the same. The proto name wins on a collision,
465/// which can only happen if a message deliberately names one field like another's
466/// JSON name.
467pub(crate) fn field_by_any_name(desc: &MessageDescriptor, name: &str) -> Option<FieldDescriptor> {
468    desc.get_field_by_name(name).or_else(|| desc.get_field_by_json_name(name))
469}
470
471/// Set one field by handing its protobuf-JSON text to the *decoder*, rather than
472/// converting by hand.
473///
474/// This is the same move `response_body` makes on the way out, in reverse: the
475/// library already knows how every field shape is spelled in JSON — base64 for
476/// `bytes`, RFC 3339 for a `Timestamp`, `"a,b"` for a `FieldMask`, `"3.5s"` for a
477/// `Duration` — and re-deriving those rules by hand in two languages is how the
478/// implementations would drift. So build `{"<jsonName>": <text>}`, decode it into
479/// a scratch message, and lift the field across.
480fn set_from_json(msg: &mut DynamicMessage, field: &FieldDescriptor, json_text: &str) -> Result<(), TranscodeError> {
481    let doc = format!("{{{}:{}}}", serde_json::to_string(field.json_name()).unwrap(), json_text);
482    let scratch = deserialize_message(msg.descriptor(), doc.as_bytes())
483        .map_err(|e| TranscodeError::Http(format!("invalid value for {}: {e}", field.name())))?;
484    msg.set_field(field, scratch.get_field(field).into_owned());
485    Ok(())
486}
487
488/// Whether a message field has a canonical protobuf-JSON **string** form, so a
489/// bare URL value can be bound to it by quoting. These are the well-known types a
490/// REST URL realistically carries — `?update_mask=a,b`, `?ttl=3.5s`,
491/// `?since=2026-01-01T00:00:00Z`. Any other message is still refused: a query
492/// parameter cannot carry an arbitrary submessage.
493fn wkt_json_shape(md: &MessageDescriptor) -> Option<bool> {
494    match md.full_name() {
495        // Encoded as a JSON string — quote the raw value.
496        "google.protobuf.Timestamp"
497        | "google.protobuf.Duration"
498        | "google.protobuf.FieldMask"
499        | "google.protobuf.StringValue"
500        | "google.protobuf.BytesValue"
501        | "google.protobuf.Int64Value"
502        | "google.protobuf.UInt64Value" => Some(true),
503        // Encoded as a bare JSON number/bool — pass the raw value through.
504        "google.protobuf.BoolValue"
505        | "google.protobuf.Int32Value"
506        | "google.protobuf.UInt32Value"
507        | "google.protobuf.FloatValue"
508        | "google.protobuf.DoubleValue" => Some(false),
509        _ => None,
510    }
511}
512
513/// Parse `json` into the top-level field `name` and set it.
514fn set_message_field(msg: &mut DynamicMessage, name: &str, json: &[u8]) -> Result<(), TranscodeError> {
515    let field = field_by_any_name(&msg.descriptor(), name)
516        .ok_or_else(|| TranscodeError::Http(format!("unknown body field: {name}")))?;
517    // Any top-level field, not just a message: `body: "content"` on a `string` takes
518    // a JSON string, `body: "items"` on a repeated field takes an array. HttpRule
519    // requires only that the field be top-level.
520    let text = std::str::from_utf8(json)
521        .map_err(|_| TranscodeError::Http(format!("body field {name}: not valid UTF-8")))?;
522    set_from_json(msg, &field, text)
523}
524
525/// Set a (possibly nested, possibly repeated) scalar field from a string value.
526fn set_by_path(msg: &mut DynamicMessage, path: &[String], raw: &str) -> Result<(), TranscodeError> {
527    let field = field_by_any_name(&msg.descriptor(), &path[0])
528        .ok_or_else(|| TranscodeError::Http(format!("unknown field: {}", path[0])))?;
529
530    if path.len() == 1 {
531        // Kinds with no scalar `Value` form bind through the JSON decoder instead:
532        // `bytes` (base64) and the string-shaped well-known types.
533        match field.kind() {
534            Kind::Bytes => {
535                return set_from_json(msg, &field, &serde_json::to_string(raw).unwrap());
536            }
537            Kind::Message(md) if !field.is_list() && !field.is_map() => {
538                return match wkt_json_shape(&md) {
539                    Some(true) => set_from_json(msg, &field, &serde_json::to_string(raw).unwrap()),
540                    Some(false) => set_from_json(msg, &field, raw),
541                    None => Err(TranscodeError::Http(format!(
542                        "cannot bind a path/query value to message field {} ({})",
543                        field.name(),
544                        md.full_name()
545                    ))),
546                };
547            }
548            _ => {}
549        }
550        let value = coerce(&field, raw)?;
551        if field.is_list() {
552            if let Some(list) = msg.get_field_mut(&field).as_list_mut() {
553                list.push(value);
554            }
555        } else {
556            msg.set_field(&field, value);
557        }
558        Ok(())
559    } else {
560        let sub = msg
561            .get_field_mut(&field)
562            .as_message_mut()
563            .ok_or_else(|| TranscodeError::Http(format!("field {} is not a message", path[0])))?;
564        set_by_path(sub, &path[1..], raw)
565    }
566}
567
568/// Coerce a string into a scalar `Value` per the field's protobuf kind.
569fn coerce(field: &prost_reflect::FieldDescriptor, raw: &str) -> Result<Value, TranscodeError> {
570    let num = |ok: Option<Value>| ok.ok_or_else(|| TranscodeError::Http(format!("invalid value for {}: {raw:?}", field.name())));
571    Ok(match field.kind() {
572        Kind::String => Value::String(raw.to_string()),
573        Kind::Bool => match raw {
574            "true" | "1" => Value::Bool(true),
575            "false" | "0" => Value::Bool(false),
576            _ => return Err(TranscodeError::Http(format!("invalid bool: {raw:?}"))),
577        },
578        Kind::Int32 | Kind::Sint32 | Kind::Sfixed32 => num(raw.parse().ok().map(Value::I32))?,
579        Kind::Int64 | Kind::Sint64 | Kind::Sfixed64 => num(raw.parse().ok().map(Value::I64))?,
580        Kind::Uint32 | Kind::Fixed32 => num(raw.parse().ok().map(Value::U32))?,
581        Kind::Uint64 | Kind::Fixed64 => num(raw.parse().ok().map(Value::U64))?,
582        Kind::Float => num(raw.parse().ok().map(Value::F32))?,
583        Kind::Double => num(raw.parse().ok().map(Value::F64))?,
584        Kind::Enum(e) => {
585            if let Ok(n) = raw.parse::<i32>() {
586                Value::EnumNumber(n)
587            } else {
588                let v = e
589                    .values()
590                    .find(|v| v.name() == raw)
591                    .ok_or_else(|| TranscodeError::Http(format!("unknown enum value: {raw:?}")))?;
592                Value::EnumNumber(v.number())
593            }
594        }
595        // Unreachable: `set_by_path` routes these to the JSON decoder above. Kept
596        // so the match stays exhaustive over `Kind` rather than falling through.
597        Kind::Bytes => return Err(TranscodeError::Http("bytes must bind through the JSON decoder".into())),
598        Kind::Message(_) => return Err(TranscodeError::Http("cannot bind a scalar to a message field".into())),
599    })
600}
601
602/// Parse a query string into decoded key/value pairs.
603fn parse_query(query: &str) -> Vec<(String, String)> {
604    query
605        .split('&')
606        .filter(|p| !p.is_empty())
607        .map(|pair| {
608            let (k, v) = pair.split_once('=').unwrap_or((pair, ""));
609            (decode_query(k), decode_query(v))
610        })
611        .collect()
612}
613
614fn decode_query(s: &str) -> String {
615    percent_decode(&s.replace('+', " "))
616}
617
618/// Minimal percent-decoding (`%XX`), lossy on invalid UTF-8.
619fn percent_decode(s: &str) -> String {
620    let bytes = s.as_bytes();
621    let mut out = Vec::with_capacity(bytes.len());
622    let mut i = 0;
623    while i < bytes.len() {
624        if bytes[i] == b'%' && i + 2 < bytes.len() {
625            if let Ok(b) = u8::from_str_radix(&s[i + 1..i + 3], 16) {
626                out.push(b);
627                i += 3;
628                continue;
629            }
630        }
631        out.push(bytes[i]);
632        i += 1;
633    }
634    String::from_utf8_lossy(&out).into_owned()
635}
636
637#[cfg(test)]
638mod tests {
639    use super::*;
640
641    /// A trailing `:verb` is matched, not merely stripped from the template.
642    /// Without that, `{id}:cancel` would also answer the bare resource URL and
643    /// would capture `id = "5:cancel"` — and every custom verb on one resource
644    /// would collide on a single template. The Go router is pinned by the same
645    /// case (`go/webnext/httprule_test.go::TestMatchSegmentsCustomVerb`).
646    #[test]
647    fn custom_verb_is_part_of_the_match() {
648        let (cancel, verb) = parse_template("/v1/things/{id}:cancel");
649        assert_eq!(verb, "cancel");
650
651        // The verb must be present, and it does not leak into the capture.
652        let vars = match_segments(&cancel, &verb, "/v1/things/5:cancel").expect("should match");
653        assert_eq!(vars, vec![(vec!["id".to_string()], "5".to_string())]);
654
655        // The bare resource URL belongs to a different binding, and so does a
656        // different verb on the same resource.
657        assert!(match_segments(&cancel, &verb, "/v1/things/5").is_none());
658        assert!(match_segments(&cancel, &verb, "/v1/things/5:archive").is_none());
659
660        // Symmetrically, a verb-less binding does not swallow a custom-verb URL.
661        let (plain, plain_verb) = parse_template("/v1/things/{id}");
662        assert!(plain_verb.is_empty());
663        assert!(match_segments(&plain, &plain_verb, "/v1/things/5:cancel").is_none());
664
665        // A percent-encoded colon is data, not a verb separator, so it still binds.
666        let vars = match_segments(&plain, &plain_verb, "/v1/things/urn%3Afoo").expect("should match");
667        assert_eq!(vars, vec![(vec!["id".to_string()], "urn:foo".to_string())]);
668    }
669
670    /// A capture may span several segments — `{name=shelves/*/books/*}`, the
671    /// canonical Google resource-name shape. The captured value is everything the
672    /// sub-pattern consumed, joined by `/`. The Go router is pinned by the same
673    /// case (`httprule_test.go::TestMultiSegmentCapture`).
674    #[test]
675    fn multi_segment_capture() {
676        let (segments, verb) = parse_template("/v1/{name=shelves/*/books/*}");
677        let vars = match_segments(&segments, &verb, "/v1/shelves/1/books/2").expect("should match");
678        assert_eq!(vars, vec![(vec!["name".to_string()], "shelves/1/books/2".to_string())]);
679
680        // The sub-pattern is a pattern, not a prefix: the shape has to line up.
681        assert_eq!(segments.len(), 2);
682        assert!(match_segments(&segments, &verb, "/v1/shelves/1/books").is_none());
683        assert!(match_segments(&segments, &verb, "/v1/shelves/1/books/2/3").is_none());
684        assert!(match_segments(&segments, &verb, "/v1/racks/1/books/2").is_none());
685
686        // A trailing `**` inside a capture takes the remainder.
687        let (segments, verb) = parse_template("/v1/{name=files/**}");
688        let vars = match_segments(&segments, &verb, "/v1/files/a/b/c.txt").expect("should match");
689        assert_eq!(vars, vec![(vec!["name".to_string()], "files/a/b/c.txt".to_string())]);
690
691        // A custom verb still binds after a multi-segment capture, which is only
692        // possible because the `:` split ignores colons inside braces.
693        let (segments, verb) = parse_template("/v1/{name=shelves/*}:archive");
694        assert_eq!(verb, "archive");
695        let vars = match_segments(&segments, &verb, "/v1/shelves/7:archive").expect("should match");
696        assert_eq!(vars, vec![(vec!["name".to_string()], "shelves/7".to_string())]);
697    }
698
699    /// Field names resolve by `.proto` name or by JSON (lowerCamelCase) name, since
700    /// both turn up in hand-written URLs. The Go router is pinned by the same case
701    /// (`httprule_test.go::TestFieldNamesResolveByProtoOrJSONName`).
702    #[test]
703    fn field_names_resolve_by_proto_or_json_name() {
704        let pool = DescriptorPool::decode(testecho::FILE_DESCRIPTOR_SET).expect("descriptor set");
705        // `HttpRule` itself is in the pool (echo.proto imports the annotations) and,
706        // unlike the Echo messages, has multi-word fields — so the two spellings
707        // genuinely differ here rather than coinciding.
708        let desc = pool.get_message_by_name("google.api.HttpRule").expect("HttpRule");
709
710        let by_proto = field_by_any_name(&desc, "response_body").expect("proto name");
711        let by_json = field_by_any_name(&desc, "responseBody").expect("json name");
712        assert_eq!(by_proto.number(), by_json.number(), "both spellings must reach one field");
713
714        assert!(field_by_any_name(&desc, "additional_bindings").is_some());
715        assert!(field_by_any_name(&desc, "additionalBindings").is_some());
716        // A name that is neither still fails.
717        assert!(field_by_any_name(&desc, "responseBodyy").is_none());
718    }
719
720    /// A synthetic message with the two kinds that bind through the JSON decoder
721    /// rather than through `coerce`: `bytes`, and a message field.
722    fn binder_fixture() -> MessageDescriptor {
723        use prost_reflect::prost_types::{
724            field_descriptor_proto::{Label, Type},
725            DescriptorProto, FieldDescriptorProto, FileDescriptorProto, FileDescriptorSet,
726        };
727        let field = |name: &str, number: i32, ty: Type, type_name: Option<&str>| FieldDescriptorProto {
728            name: Some(name.to_string()),
729            number: Some(number),
730            label: Some(Label::Optional as i32),
731            r#type: Some(ty as i32),
732            type_name: type_name.map(str::to_string),
733            ..Default::default()
734        };
735        let file = FileDescriptorProto {
736            name: Some("binder_fixture.proto".to_string()),
737            package: Some("webnext.binder.test".to_string()),
738            syntax: Some("proto3".to_string()),
739            message_type: vec![
740                DescriptorProto {
741                    name: Some("Nested".to_string()),
742                    field: vec![field("id", 1, Type::String, None)],
743                    ..Default::default()
744                },
745                DescriptorProto {
746                    name: Some("Req".to_string()),
747                    field: vec![
748                        field("blob", 1, Type::Bytes, None),
749                        field("nested", 2, Type::Message, Some(".webnext.binder.test.Nested")),
750                    ],
751                    ..Default::default()
752                },
753            ],
754            ..Default::default()
755        };
756        DescriptorPool::from_file_descriptor_set(FileDescriptorSet { file: vec![file] })
757            .expect("build fixture pool")
758            .get_message_by_name("webnext.binder.test.Req")
759            .expect("Req")
760    }
761
762    /// `custom { kind: "*" }` means "leave the HTTP method unspecified for this
763    /// rule", so the binding answers every verb rather than a literal `*`. Go is
764    /// pinned by the same case (`TestCustomKindStarMatchesAnyVerb`).
765    #[test]
766    fn custom_kind_star_matches_any_verb() {
767        let mut bindings = Vec::new();
768        let input = binder_fixture();
769        // `push_binding` takes the rule as a DynamicMessage, so build one: a
770        // `custom` pattern whose kind is `*`.
771        let pool = DescriptorPool::decode(testecho::FILE_DESCRIPTOR_SET).expect("descriptor set");
772        let rule_desc = pool.get_message_by_name("google.api.HttpRule").expect("HttpRule");
773        let custom_desc = pool.get_message_by_name("google.api.CustomHttpPattern").expect("custom");
774        let mut custom = DynamicMessage::new(custom_desc);
775        custom.set_field_by_name("kind", Value::String("*".into()));
776        custom.set_field_by_name("path", Value::String("/v1/any".into()));
777        let mut rule = DynamicMessage::new(rule_desc);
778        rule.set_field_by_name("custom", Value::Message(custom));
779
780        push_binding(&mut bindings, &rule, "/test.Svc/Any", &input);
781        let router = HttpRouter { bindings };
782
783        for verb in ["GET", "POST", "DELETE", "PATCH"] {
784            assert!(router.match_request(verb, "/v1/any").is_some(), "{verb} should match");
785        }
786        // It is still a route, not a catch-all: the path must match.
787        assert!(router.match_request("GET", "/v1/other").is_none());
788    }
789
790    /// A pool containing the well-known types a REST URL realistically carries.
791    /// They are declared by hand (each is tiny) because prost-reflect keys its
792    /// protobuf-JSON handling off the *full name*, so a faithful declaration is all
793    /// it needs — and a synthetic pool keeps this test free of a codegen dependency.
794    fn wkt_fixture() -> MessageDescriptor {
795        use prost_reflect::prost_types::{
796            field_descriptor_proto::{Label, Type},
797            DescriptorProto, FieldDescriptorProto, FileDescriptorProto, FileDescriptorSet,
798        };
799        let f = |name: &str, number: i32, ty: Type, label: Label, type_name: Option<&str>| {
800            FieldDescriptorProto {
801                name: Some(name.to_string()),
802                number: Some(number),
803                label: Some(label as i32),
804                r#type: Some(ty as i32),
805                type_name: type_name.map(str::to_string),
806                ..Default::default()
807            }
808        };
809        let wkt = |file: &str, msg: &str, fields: Vec<FieldDescriptorProto>| FileDescriptorProto {
810            name: Some(file.to_string()),
811            package: Some("google.protobuf".to_string()),
812            syntax: Some("proto3".to_string()),
813            message_type: vec![DescriptorProto {
814                name: Some(msg.to_string()),
815                field: fields,
816                ..Default::default()
817            }],
818            ..Default::default()
819        };
820
821        let files = vec![
822            wkt("google/protobuf/field_mask.proto", "FieldMask",
823                vec![f("paths", 1, Type::String, Label::Repeated, None)]),
824            wkt("google/protobuf/duration.proto", "Duration",
825                vec![f("seconds", 1, Type::Int64, Label::Optional, None),
826                     f("nanos", 2, Type::Int32, Label::Optional, None)]),
827            wkt("google/protobuf/timestamp.proto", "Timestamp",
828                vec![f("seconds", 1, Type::Int64, Label::Optional, None),
829                     f("nanos", 2, Type::Int32, Label::Optional, None)]),
830            wkt("google/protobuf/wrappers.proto", "StringValue",
831                vec![f("value", 1, Type::String, Label::Optional, None)]),
832            FileDescriptorProto {
833                name: Some("wkt_fixture.proto".to_string()),
834                package: Some("webnext.wkt.test".to_string()),
835                syntax: Some("proto3".to_string()),
836                dependency: vec![
837                    "google/protobuf/field_mask.proto".to_string(),
838                    "google/protobuf/duration.proto".to_string(),
839                    "google/protobuf/timestamp.proto".to_string(),
840                    "google/protobuf/wrappers.proto".to_string(),
841                ],
842                message_type: vec![DescriptorProto {
843                    name: Some("Req".to_string()),
844                    field: vec![
845                        f("mask", 1, Type::Message, Label::Optional, Some(".google.protobuf.FieldMask")),
846                        f("ttl", 2, Type::Message, Label::Optional, Some(".google.protobuf.Duration")),
847                        f("at", 3, Type::Message, Label::Optional, Some(".google.protobuf.Timestamp")),
848                        f("note", 4, Type::Message, Label::Optional, Some(".google.protobuf.StringValue")),
849                    ],
850                    ..Default::default()
851                }],
852                ..Default::default()
853            },
854        ];
855        DescriptorPool::from_file_descriptor_set(FileDescriptorSet { file: files })
856            .expect("build wkt pool")
857            .get_message_by_name("webnext.wkt.test.Req")
858            .expect("Req")
859    }
860
861    /// The well-known types a REST URL carries bind from a query param, spelled the
862    /// way protobuf-JSON spells them — `?update_mask=a,b` being the canonical case.
863    /// Go is pinned by `TestQueryBindsWellKnownTypes`. This lives in unit tests
864    /// rather than the conformance matrix on purpose: see the note in
865    /// `conformance/proto/conformance.proto`.
866    #[test]
867    fn well_known_types_bind_from_a_query() {
868        let desc = wkt_fixture();
869        for (field, raw) in [
870            ("mask", "a,b.c"),
871            ("ttl", "3.500s"),
872            ("at", "2026-01-01T00:00:00Z"),
873            ("note", "hi"),
874        ] {
875            let mut msg = DynamicMessage::new(desc.clone());
876            set_by_path(&mut msg, &[field.to_string()], raw).unwrap_or_else(|e| panic!("{field}={raw}: {e}"));
877            // Round-trip through the encoder: the value must come back as the same
878            // canonical spelling it went in as.
879            let json = serde_json::to_string(&msg.transcode_to_dynamic()).expect("encode");
880            assert!(json.contains(raw), "{field}={raw} round-tripped as {json}");
881        }
882
883        // A malformed value is an error, not a silent default.
884        let mut msg = DynamicMessage::new(desc);
885        assert!(set_by_path(&mut msg, &["at".to_string()], "not-a-timestamp").is_err());
886    }
887
888    /// `bytes` binds from a URL through the *JSON decoder*, so it is spelled exactly
889    /// as protobuf-JSON spells it — base64 — rather than guessed at. An arbitrary
890    /// submessage is still refused. Go is pinned by the same cases
891    /// (`TestQueryBindsWellKnownTypes{,ButNotArbitraryMessages}`).
892    #[test]
893    fn bytes_binds_from_a_url_and_arbitrary_messages_do_not() {
894        let desc = binder_fixture();
895
896        let mut msg = DynamicMessage::new(desc.clone());
897        set_by_path(&mut msg, &["blob".to_string()], "AQID").expect("base64 binds");
898        let field = desc.get_field_by_name("blob").unwrap();
899        assert_eq!(msg.get_field(&field).as_bytes().unwrap().as_ref(), &[1u8, 2, 3]);
900
901        let mut msg = DynamicMessage::new(desc.clone());
902        assert!(set_by_path(&mut msg, &["blob".to_string()], "!!!").is_err(), "malformed base64");
903
904        // An arbitrary submessage cannot come from a query param, and the error names
905        // the type it refused.
906        let mut msg = DynamicMessage::new(desc);
907        let err = set_by_path(&mut msg, &["nested".to_string()], "{}").unwrap_err();
908        assert!(format!("{err}").contains("Nested"), "{err}");
909        // The supported spelling for the same intent:
910        let mut msg = DynamicMessage::new(binder_fixture());
911        set_by_path(&mut msg, &["nested".to_string(), "id".to_string()], "x").expect("dotted binds");
912    }
913
914    /// `body: "<field>"` may name ANY top-level field, not only a singular message —
915    /// HttpRule requires only that it be top-level, which is also why a dotted body
916    /// path stays refused.
917    #[test]
918    fn body_may_name_any_top_level_field() {
919        let desc = binder_fixture();
920
921        let mut msg = DynamicMessage::new(desc.clone());
922        set_message_field(&mut msg, "blob", br#""AQID""#).expect("scalar body field");
923        let field = desc.get_field_by_name("blob").unwrap();
924        assert_eq!(msg.get_field(&field).as_bytes().unwrap().as_ref(), &[1u8, 2, 3]);
925
926        let mut msg = DynamicMessage::new(desc.clone());
927        set_message_field(&mut msg, "nested", br#"{"id":"inner"}"#).expect("message body field");
928
929        // A dotted path is not a top-level field, so it stays refused — by the spec,
930        // not by omission.
931        let mut msg = DynamicMessage::new(desc);
932        assert!(set_message_field(&mut msg, "nested.id", br#""x""#).is_err());
933    }
934
935    /// Bare `*` and `**` — the HttpRule grammar's unnamed wildcards — match without
936    /// capturing anything.
937    #[test]
938    fn bare_wildcard_segments() {
939        let (one, verb) = parse_template("/v1/*/things/{id}");
940        assert!(matches!(one[1], Segment::AnyOne));
941        let vars = match_segments(&one, &verb, "/v1/anything/things/7").expect("should match");
942        assert_eq!(vars, vec![(vec!["id".to_string()], "7".to_string())]);
943        // Exactly one segment: neither zero nor two.
944        assert!(match_segments(&one, &verb, "/v1/things/7").is_none());
945        assert!(match_segments(&one, &verb, "/v1/a/b/things/7").is_none());
946
947        let (rest, verb) = parse_template("/v1/things/{id}/**");
948        assert!(matches!(rest[3], Segment::AnyRest));
949        let vars = match_segments(&rest, &verb, "/v1/things/7/a/b/c").expect("should match");
950        assert_eq!(vars, vec![(vec!["id".to_string()], "7".to_string())]);
951        // `**` also matches an empty remainder.
952        assert!(match_segments(&rest, &verb, "/v1/things/7").is_some());
953    }
954}