Skip to main content

gproxy_protocol/
path.rs

1use http::Method;
2
3use crate::operation::{ContentGenerationKind, Operation, OperationKey, OperationKind, WireFamily};
4use crate::spec::{Matched, PathPattern, Seg};
5
6/// Linear scan over every operation's ingress table. The table is small
7/// and static; a hot-path matcher can replace the scan later without the
8/// signature changing.
9pub fn match_ingress(method: &Method, path: &str) -> Option<Matched> {
10    match_ingress_for(method, path, None)
11}
12
13/// Match with a preferred family when identical vendor paths overlap. The
14/// first canonical row remains the default for callers without wire-profile
15/// evidence; the engine derives a preference from protocol headers.
16pub fn match_ingress_for(
17    method: &Method,
18    path: &str,
19    preferred: Option<WireFamily>,
20) -> Option<Matched> {
21    let path = path.strip_prefix('/')?;
22    let mut fallback = None;
23
24    for (operation, spec) in &crate::specs::REGISTRY {
25        for ingress in spec.ingress {
26            if ingress.method != method {
27                continue;
28            }
29            if let Some(params) = match_path_segments(ingress.pattern, path) {
30                let matched = Matched {
31                    operation: *operation,
32                    kind: ingress.kind,
33                    stream: ingress.stream,
34                    framing: ingress.framing,
35                    upgrade: ingress.upgrade,
36                    params,
37                };
38                if preferred.is_some_and(|family| ingress.kind == OperationKind::Family(family)) {
39                    return Some(matched);
40                }
41                fallback.get_or_insert(matched);
42            }
43        }
44    }
45
46    fallback
47}
48
49/// Match one declared path pattern and return its captures.
50pub fn match_path(pattern: PathPattern, path: &str) -> Option<Vec<(&'static str, String)>> {
51    match_path_segments(pattern, path.strip_prefix('/')?)
52}
53
54/// Canonical upstream method/path for a native operation key. Targets derive
55/// from the ingress registry so transforms do not carry a second path table.
56pub fn request_target(key: OperationKey, model: &str) -> Option<(Method, String)> {
57    let operation = if key.operation() == Operation::StreamGenerateContent
58        && key.kind()
59            != OperationKind::ContentGeneration(ContentGenerationKind::GeminiGenerateContent)
60    {
61        Operation::GenerateContent
62    } else {
63        key.operation()
64    };
65    let ingress = operation
66        .spec()
67        .ingress
68        .iter()
69        .find(|ingress| ingress.kind == key.kind())?;
70    let mut path = String::new();
71    for segment in ingress.pattern.0 {
72        path.push('/');
73        match segment {
74            Seg::Lit(value) => path.push_str(value),
75            Seg::Param("id" | "model") => path.push_str(&encode_segment(model)?),
76            Seg::Param(_) | Seg::Rest(_) => return None,
77            Seg::ParamAction("model", action) => {
78                path.push_str(&encode_segment(model)?);
79                path.push(':');
80                path.push_str(action);
81            }
82            Seg::ParamAction(_, _) => return None,
83        }
84    }
85    Some((ingress.method.clone(), path))
86}
87
88fn encode_segment(value: &str) -> Option<String> {
89    if value.is_empty() {
90        return None;
91    }
92    const HEX: &[u8; 16] = b"0123456789ABCDEF";
93    let mut encoded = String::with_capacity(value.len());
94    for byte in value.bytes() {
95        if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~') {
96            encoded.push(char::from(byte));
97        } else {
98            encoded.push('%');
99            encoded.push(char::from(HEX[usize::from(byte >> 4)]));
100            encoded.push(char::from(HEX[usize::from(byte & 0x0f)]));
101        }
102    }
103    Some(encoded)
104}
105
106fn match_path_segments(pattern: PathPattern, path: &str) -> Option<Vec<(&'static str, String)>> {
107    let mut segments = path.split('/');
108    let mut params = Vec::new();
109
110    for pattern_segment in pattern.0 {
111        let segment = segments.next()?;
112        match pattern_segment {
113            Seg::Lit(expected) if segment == *expected => {}
114            Seg::Lit(_) => return None,
115            Seg::Param(name) if !segment.is_empty() => params.push((*name, segment.to_owned())),
116            Seg::Param(_) => return None,
117            Seg::ParamAction(name, action) => {
118                let value = segment.strip_suffix(action)?.strip_suffix(':')?;
119                if value.is_empty() {
120                    return None;
121                }
122                params.push((*name, value.to_owned()));
123            }
124            Seg::Rest(name) => {
125                if segment.is_empty() {
126                    return None;
127                }
128                let mut rest = segment.to_owned();
129                for segment in segments {
130                    if segment.is_empty() {
131                        return None;
132                    }
133                    rest.push('/');
134                    rest.push_str(segment);
135                }
136                params.push((*name, rest));
137                return Some(params);
138            }
139        }
140    }
141
142    segments.next().is_none().then_some(params)
143}