1use prost_reflect::prost::Message;
28use prost_reflect::{
29 DescriptorPool, DynamicMessage, FieldDescriptor, Kind, MessageDescriptor, ReflectMessage, Value,
30};
31
32use crate::transcode::TranscodeError;
33
34const HTTP_EXT: &str = "google.api.http";
36
37#[derive(Debug, Clone)]
43enum Segment {
44 Literal(String),
46 AnyOne,
48 AnyRest,
50 Capture(Vec<String>, Vec<Segment>),
54}
55
56#[derive(Debug, Clone)]
58enum BodyRule {
59 Wildcard,
61 Field(String),
63 None,
65}
66
67#[derive(Debug, Clone)]
70struct Binding {
71 http_method: String, segments: Vec<Segment>,
73 custom_verb: String,
76 body: BodyRule,
77 response_body: String,
80 grpc_method: String, input: MessageDescriptor,
82}
83
84type PathVar = (Vec<String>, String);
87
88pub struct HttpCall {
90 pub grpc_method: String,
91 pub message: Vec<u8>,
92 pub response_body: String,
95}
96
97pub struct WsBinding {
100 binding: Binding,
101 vars: Vec<PathVar>,
102 query: Option<String>,
103}
104
105impl WsBinding {
106 pub fn grpc_method(&self) -> &str {
108 &self.binding.grpc_method
109 }
110
111 pub fn has_body(&self) -> bool {
114 !matches!(self.binding.body, BodyRule::None)
115 }
116
117 pub fn response_body(&self) -> &str {
120 &self.binding.response_body
121 }
122
123 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#[derive(Clone, Default)]
131pub struct HttpRouter {
132 bindings: Vec<Binding>,
133}
134
135impl HttpRouter {
136 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 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 pub fn is_annotated(&self, grpc_method: &str) -> bool {
181 self.bindings.iter().any(|b| b.grpc_method == grpc_method)
182 }
183
184 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 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 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
221fn 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
254fn 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 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
286fn 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
311fn 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
327fn 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
343fn plain_segment(seg: &str) -> Segment {
345 match seg {
346 "*" => Segment::AnyOne,
347 "**" => Segment::AnyRest,
348 _ => Segment::Literal(seg.to_string()),
349 }
350}
351
352fn 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
381fn 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 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
412fn 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 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; }
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
461pub(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
471fn 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
488fn wkt_json_shape(md: &MessageDescriptor) -> Option<bool> {
494 match md.full_name() {
495 "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 "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
513fn 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 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
525fn 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 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
568fn 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 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
602fn 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
618fn 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 #[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 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 assert!(match_segments(&cancel, &verb, "/v1/things/5").is_none());
658 assert!(match_segments(&cancel, &verb, "/v1/things/5:archive").is_none());
659
660 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 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 #[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 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 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 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 #[test]
703 fn field_names_resolve_by_proto_or_json_name() {
704 let pool = DescriptorPool::decode(testecho::FILE_DESCRIPTOR_SET).expect("descriptor set");
705 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 assert!(field_by_any_name(&desc, "responseBodyy").is_none());
718 }
719
720 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 #[test]
766 fn custom_kind_star_matches_any_verb() {
767 let mut bindings = Vec::new();
768 let input = binder_fixture();
769 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 assert!(router.match_request("GET", "/v1/other").is_none());
788 }
789
790 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 #[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 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 let mut msg = DynamicMessage::new(desc);
885 assert!(set_by_path(&mut msg, &["at".to_string()], "not-a-timestamp").is_err());
886 }
887
888 #[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 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 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 #[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 let mut msg = DynamicMessage::new(desc);
932 assert!(set_message_field(&mut msg, "nested.id", br#""x""#).is_err());
933 }
934
935 #[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 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 assert!(match_segments(&rest, &verb, "/v1/things/7").is_some());
953 }
954}