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 is a practical subset of the HttpRule spec — enough for the common
6//! `get: "/v1/x/{id}"` / `post: "/v1/x" body: "*"` bindings:
7//!   * verbs `get/put/post/delete/patch` and `custom{kind}`,
8//!   * `additional_bindings` (one level),
9//!   * path templates: literal segments, `{field}` / `{field=*}` single-segment
10//!     captures, and `{field=**}` (or a trailing segment) capturing the rest,
11//!     with dotted field paths (`{a.b}`) binding nested fields,
12//!   * `body: "*"` (whole message), `body: "<field>"` (a sub-message field), or none,
13//!   * query params bound to (possibly nested) scalar/repeated fields.
14//!
15//! Not yet handled (see BACKLOG): `response_body`, regex path patterns beyond
16//! `*`/`**`, and non-scalar query binding.
17
18use prost_reflect::prost::Message;
19use prost_reflect::{DescriptorPool, DynamicMessage, Kind, MessageDescriptor, ReflectMessage, Value};
20
21use crate::transcode::TranscodeError;
22
23/// The extension full name that carries an HttpRule on a method.
24const HTTP_EXT: &str = "google.api.http";
25
26/// A parsed path-template segment.
27#[derive(Debug, Clone)]
28enum Segment {
29    /// A fixed path component that must match exactly.
30    Literal(String),
31    /// `{field}` / `{field=*}` — captures exactly one path component into `field`
32    /// (a dotted path into the request message).
33    Single(Vec<String>),
34    /// `{field=**}` — captures the remaining components (slashes preserved).
35    Rest(Vec<String>),
36}
37
38/// How the request body maps onto the message.
39#[derive(Debug, Clone)]
40enum BodyRule {
41    /// `body: "*"` — the whole JSON body is the request message.
42    Wildcard,
43    /// `body: "<field>"` — the JSON body is a single (message) field.
44    Field(String),
45    /// No body.
46    None,
47}
48
49/// One compiled HTTP binding: `(method, template)` -> a gRPC method + how to build
50/// its request message.
51#[derive(Debug, Clone)]
52struct Binding {
53    http_method: String, // upper-case, e.g. "GET"
54    segments: Vec<Segment>,
55    body: BodyRule,
56    grpc_method: String, // "/pkg.Service/Method"
57    input: MessageDescriptor,
58}
59
60/// A captured path variable: the dotted field path it binds to, and its string value —
61/// e.g. `(["user", "id"], "123")` for a `{user.id}` template segment.
62type PathVar = (Vec<String>, String);
63
64/// The transcoded call: which gRPC method to invoke and the encoded request.
65pub struct HttpCall {
66    pub grpc_method: String,
67    pub message: Vec<u8>,
68}
69
70/// A WebSocket route resolved from an annotation URL: the target gRPC method plus
71/// the path/query bindings, used to build each streamed request message.
72pub struct WsBinding {
73    binding: Binding,
74    vars: Vec<PathVar>,
75    query: Option<String>,
76}
77
78impl WsBinding {
79    /// The gRPC method this annotation route maps to.
80    pub fn grpc_method(&self) -> &str {
81        &self.binding.grpc_method
82    }
83
84    /// Whether the route takes a request body. `false` for GET-style server-streams,
85    /// where the request comes entirely from the URL (path + query).
86    pub fn has_body(&self) -> bool {
87        !matches!(self.binding.body, BodyRule::None)
88    }
89
90    /// Build a request message from a body payload, overlaying the URL path/query.
91    pub fn build_message(&self, body: &[u8]) -> Result<Vec<u8>, TranscodeError> {
92        build_message(&self.binding, &self.vars, self.query.as_deref(), body)
93    }
94}
95
96/// A table of HTTP bindings compiled from a descriptor pool.
97#[derive(Clone, Default)]
98pub struct HttpRouter {
99    bindings: Vec<Binding>,
100}
101
102impl HttpRouter {
103    /// Compile all `google.api.http` bindings in the pool. Empty if the pool has
104    /// no annotations (or the extension isn't present).
105    pub fn from_pool(pool: &DescriptorPool) -> Self {
106        let mut bindings = Vec::new();
107        let Some(ext) = pool.get_extension_by_name(HTTP_EXT) else {
108            return Self { bindings };
109        };
110        for service in pool.services() {
111            for method in service.methods() {
112                let opts = method.options();
113                if !opts.has_extension(&ext) {
114                    continue;
115                }
116                let grpc_method = format!("/{}/{}", service.full_name(), method.name());
117                let input = method.input();
118                let rule = opts.get_extension(&ext);
119                if let Some(rule) = rule.as_message() {
120                    collect_rule(&mut bindings, rule, &grpc_method, &input);
121                }
122            }
123        }
124        Self { bindings }
125    }
126
127    pub fn is_empty(&self) -> bool {
128        self.bindings.is_empty()
129    }
130
131    /// Match a WebSocket upgrade path against the annotation bindings. A WS upgrade
132    /// is always an HTTP GET, so this matches on the path only (verb-agnostic).
133    pub fn match_ws(&self, path: &str, query: Option<&str>) -> Option<WsBinding> {
134        for b in &self.bindings {
135            if let Some(vars) = match_segments(&b.segments, path) {
136                return Some(WsBinding {
137                    binding: b.clone(),
138                    vars,
139                    query: query.map(str::to_string),
140                });
141            }
142        }
143        None
144    }
145
146    /// Whether a gRPC method (`/pkg.Service/Method`) has any HTTP annotation.
147    pub fn is_annotated(&self, grpc_method: &str) -> bool {
148        self.bindings.iter().any(|b| b.grpc_method == grpc_method)
149    }
150
151    /// Find a binding matching `(method, path)` and return it plus the captured
152    /// path variables (dotted field path -> value).
153    fn match_request(&self, method: &str, path: &str) -> Option<(&Binding, Vec<PathVar>)> {
154        let want = method.to_ascii_uppercase();
155        for b in &self.bindings {
156            if b.http_method != want {
157                continue;
158            }
159            if let Some(vars) = match_segments(&b.segments, path) {
160                return Some((b, vars));
161            }
162        }
163        None
164    }
165
166    /// Transcode a REST request into a gRPC call, or `None` if no binding matches.
167    pub fn transcode(
168        &self,
169        method: &str,
170        path: &str,
171        query: Option<&str>,
172        body: &[u8],
173    ) -> Result<Option<HttpCall>, TranscodeError> {
174        let Some((binding, vars)) = self.match_request(method, path) else {
175            return Ok(None);
176        };
177        let message = build_message(binding, &vars, query, body)?;
178        Ok(Some(HttpCall { grpc_method: binding.grpc_method.clone(), message }))
179    }
180}
181
182/// Push the top rule and any `additional_bindings` (one level) as bindings.
183fn collect_rule(bindings: &mut Vec<Binding>, rule: &DynamicMessage, grpc_method: &str, input: &MessageDescriptor) {
184    push_binding(bindings, rule, grpc_method, input);
185    if let Some(list) = rule.get_field_by_name("additional_bindings") {
186        if let Some(items) = list.as_list() {
187            for item in items {
188                if let Some(m) = item.as_message() {
189                    push_binding(bindings, m, grpc_method, input);
190                }
191            }
192        }
193    }
194}
195
196fn push_binding(bindings: &mut Vec<Binding>, rule: &DynamicMessage, grpc_method: &str, input: &MessageDescriptor) {
197    let Some((http_method, template)) = verb_and_path(rule) else {
198        return;
199    };
200    bindings.push(Binding {
201        http_method,
202        segments: parse_template(&template),
203        body: body_rule(rule),
204        grpc_method: grpc_method.to_string(),
205        input: input.clone(),
206    });
207}
208
209/// Extract the verb + path from an HttpRule's `pattern` oneof.
210fn verb_and_path(rule: &DynamicMessage) -> Option<(String, String)> {
211    for (field, verb) in [("get", "GET"), ("put", "PUT"), ("post", "POST"), ("delete", "DELETE"), ("patch", "PATCH")] {
212        if let Some(v) = rule.get_field_by_name(field) {
213            if let Some(s) = v.as_str() {
214                if !s.is_empty() {
215                    return Some((verb.to_string(), s.to_string()));
216                }
217            }
218        }
219    }
220    // custom { kind, path }
221    if let Some(v) = rule.get_field_by_name("custom") {
222        if let Some(m) = v.as_message() {
223            let kind = m.get_field_by_name("kind").and_then(|k| k.as_str().map(str::to_string)).unwrap_or_default();
224            let path = m.get_field_by_name("path").and_then(|p| p.as_str().map(str::to_string)).unwrap_or_default();
225            if !kind.is_empty() {
226                return Some((kind.to_ascii_uppercase(), path));
227            }
228        }
229    }
230    None
231}
232
233fn body_rule(rule: &DynamicMessage) -> BodyRule {
234    match rule.get_field_by_name("body").and_then(|b| b.as_str().map(str::to_string)).unwrap_or_default().as_str() {
235        "" => BodyRule::None,
236        "*" => BodyRule::Wildcard,
237        field => BodyRule::Field(field.to_string()),
238    }
239}
240
241/// Parse a path template into segments. A trailing `:verb` is ignored.
242fn parse_template(template: &str) -> Vec<Segment> {
243    let path = template.split(':').next().unwrap_or(template);
244    path.trim_matches('/')
245        .split('/')
246        .filter(|s| !s.is_empty())
247        .map(|seg| {
248            if let Some(inner) = seg.strip_prefix('{').and_then(|s| s.strip_suffix('}')) {
249                let (field, pattern) = inner.split_once('=').unwrap_or((inner, "*"));
250                let field_path: Vec<String> = field.split('.').map(str::to_string).collect();
251                if pattern == "**" {
252                    Segment::Rest(field_path)
253                } else {
254                    Segment::Single(field_path)
255                }
256            } else {
257                Segment::Literal(seg.to_string())
258            }
259        })
260        .collect()
261}
262
263/// Match a request path against a template, returning captured `(field_path, value)`.
264fn match_segments(segments: &[Segment], path: &str) -> Option<Vec<PathVar>> {
265    let parts: Vec<&str> = path.trim_matches('/').split('/').filter(|s| !s.is_empty()).collect();
266    let mut vars = Vec::new();
267    let mut i = 0;
268    for seg in segments {
269        match seg {
270            Segment::Literal(lit) => {
271                if parts.get(i) != Some(&lit.as_str()) {
272                    return None;
273                }
274                i += 1;
275            }
276            Segment::Single(field) => {
277                let part = parts.get(i)?;
278                vars.push((field.clone(), percent_decode(part)));
279                i += 1;
280            }
281            Segment::Rest(field) => {
282                let rest: Vec<String> = parts[i..].iter().map(|p| percent_decode(p)).collect();
283                vars.push((field.clone(), rest.join("/")));
284                i = parts.len();
285            }
286        }
287    }
288    if i == parts.len() {
289        Some(vars)
290    } else {
291        None
292    }
293}
294
295/// Build the encoded request message from a matched binding + inputs.
296fn build_message(
297    binding: &Binding,
298    vars: &[PathVar],
299    query: Option<&str>,
300    body: &[u8],
301) -> Result<Vec<u8>, TranscodeError> {
302    let mut msg = match &binding.body {
303        BodyRule::Wildcard => deserialize_message(binding.input.clone(), body)?,
304        BodyRule::None => DynamicMessage::new(binding.input.clone()),
305        BodyRule::Field(field) => {
306            let mut m = DynamicMessage::new(binding.input.clone());
307            if !body.is_empty() {
308                set_message_field(&mut m, field, body)?;
309            }
310            m
311        }
312    };
313
314    for (field_path, value) in vars {
315        set_by_path(&mut msg, field_path, value)?;
316    }
317
318    // Query params bind fields not carried by a wildcard body.
319    if !matches!(binding.body, BodyRule::Wildcard) {
320        if let Some(q) = query {
321            for (key, value) in parse_query(q) {
322                let field_path: Vec<String> = key.split('.').map(str::to_string).collect();
323                if vars.iter().any(|(fp, _)| *fp == field_path) {
324                    continue; // already set from the path
325                }
326                set_by_path(&mut msg, &field_path, &value)?;
327            }
328        }
329    }
330
331    Ok(msg.encode_to_vec())
332}
333
334fn deserialize_message(desc: MessageDescriptor, json: &[u8]) -> Result<DynamicMessage, TranscodeError> {
335    if json.is_empty() {
336        return Ok(DynamicMessage::new(desc));
337    }
338    let mut de = serde_json::Deserializer::from_slice(json);
339    let msg = DynamicMessage::deserialize(desc, &mut de)?;
340    de.end()?;
341    Ok(msg)
342}
343
344/// Parse `json` into the (message-typed) field `name` and set it.
345fn set_message_field(msg: &mut DynamicMessage, name: &str, json: &[u8]) -> Result<(), TranscodeError> {
346    let field = msg
347        .descriptor()
348        .get_field_by_name(name)
349        .ok_or_else(|| TranscodeError::Http(format!("unknown body field: {name}")))?;
350    match field.kind() {
351        Kind::Message(md) => {
352            let sub = deserialize_message(md, json)?;
353            msg.set_field(&field, Value::Message(sub));
354            Ok(())
355        }
356        _ => Err(TranscodeError::Http(format!("body field {name} must be a message"))),
357    }
358}
359
360/// Set a (possibly nested, possibly repeated) scalar field from a string value.
361fn set_by_path(msg: &mut DynamicMessage, path: &[String], raw: &str) -> Result<(), TranscodeError> {
362    let field = msg
363        .descriptor()
364        .get_field_by_name(&path[0])
365        .ok_or_else(|| TranscodeError::Http(format!("unknown field: {}", path[0])))?;
366
367    if path.len() == 1 {
368        let value = coerce(&field, raw)?;
369        if field.is_list() {
370            if let Some(list) = msg.get_field_mut(&field).as_list_mut() {
371                list.push(value);
372            }
373        } else {
374            msg.set_field(&field, value);
375        }
376        Ok(())
377    } else {
378        let sub = msg
379            .get_field_mut(&field)
380            .as_message_mut()
381            .ok_or_else(|| TranscodeError::Http(format!("field {} is not a message", path[0])))?;
382        set_by_path(sub, &path[1..], raw)
383    }
384}
385
386/// Coerce a string into a scalar `Value` per the field's protobuf kind.
387fn coerce(field: &prost_reflect::FieldDescriptor, raw: &str) -> Result<Value, TranscodeError> {
388    let num = |ok: Option<Value>| ok.ok_or_else(|| TranscodeError::Http(format!("invalid value for {}: {raw:?}", field.name())));
389    Ok(match field.kind() {
390        Kind::String => Value::String(raw.to_string()),
391        Kind::Bool => match raw {
392            "true" | "1" => Value::Bool(true),
393            "false" | "0" => Value::Bool(false),
394            _ => return Err(TranscodeError::Http(format!("invalid bool: {raw:?}"))),
395        },
396        Kind::Int32 | Kind::Sint32 | Kind::Sfixed32 => num(raw.parse().ok().map(Value::I32))?,
397        Kind::Int64 | Kind::Sint64 | Kind::Sfixed64 => num(raw.parse().ok().map(Value::I64))?,
398        Kind::Uint32 | Kind::Fixed32 => num(raw.parse().ok().map(Value::U32))?,
399        Kind::Uint64 | Kind::Fixed64 => num(raw.parse().ok().map(Value::U64))?,
400        Kind::Float => num(raw.parse().ok().map(Value::F32))?,
401        Kind::Double => num(raw.parse().ok().map(Value::F64))?,
402        Kind::Enum(e) => {
403            if let Ok(n) = raw.parse::<i32>() {
404                Value::EnumNumber(n)
405            } else {
406                let v = e
407                    .values()
408                    .find(|v| v.name() == raw)
409                    .ok_or_else(|| TranscodeError::Http(format!("unknown enum value: {raw:?}")))?;
410                Value::EnumNumber(v.number())
411            }
412        }
413        Kind::Bytes => return Err(TranscodeError::Http("bytes fields cannot bind from path/query".into())),
414        Kind::Message(_) => return Err(TranscodeError::Http("cannot bind a scalar to a message field".into())),
415    })
416}
417
418/// Parse a query string into decoded key/value pairs.
419fn parse_query(query: &str) -> Vec<(String, String)> {
420    query
421        .split('&')
422        .filter(|p| !p.is_empty())
423        .map(|pair| {
424            let (k, v) = pair.split_once('=').unwrap_or((pair, ""));
425            (decode_query(k), decode_query(v))
426        })
427        .collect()
428}
429
430fn decode_query(s: &str) -> String {
431    percent_decode(&s.replace('+', " "))
432}
433
434/// Minimal percent-decoding (`%XX`), lossy on invalid UTF-8.
435fn percent_decode(s: &str) -> String {
436    let bytes = s.as_bytes();
437    let mut out = Vec::with_capacity(bytes.len());
438    let mut i = 0;
439    while i < bytes.len() {
440        if bytes[i] == b'%' && i + 2 < bytes.len() {
441            if let Ok(b) = u8::from_str_radix(&s[i + 1..i + 3], 16) {
442                out.push(b);
443                i += 3;
444                continue;
445            }
446        }
447        out.push(bytes[i]);
448        i += 1;
449    }
450    String::from_utf8_lossy(&out).into_owned()
451}