1use http::Method;
2
3use crate::operation::{ContentGenerationKind, Operation, OperationKey, OperationKind, WireFamily};
4use crate::spec::{Matched, PathPattern, Seg};
5
6pub fn match_ingress(method: &Method, path: &str) -> Option<Matched> {
10 match_ingress_for(method, path, None)
11}
12
13pub 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
49pub fn match_path(pattern: PathPattern, path: &str) -> Option<Vec<(&'static str, String)>> {
51 match_path_segments(pattern, path.strip_prefix('/')?)
52}
53
54pub 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}