Skip to main content

gestalt/public/
rest_mapping.rs

1//! Protobuf-JSON request mapping for the public REST transport.
2
3use serde_json::{Map, Value};
4
5use crate::public::generated::metadata::PublicField;
6use crate::rpc_support::{GestaltError, gestalt_error_code};
7
8/// Substitutes `{param}` placeholders in an HTTP path template.
9pub fn substitute_path(
10    pattern: &str,
11    request: &Value,
12    path_fields: &[PublicField],
13) -> Result<String, GestaltError> {
14    let Some(object) = request.as_object() else {
15        return Ok(pattern.to_string());
16    };
17    let mut out = pattern.to_string();
18    for field in path_fields {
19        let token = format!("{{{}}}", field.name);
20        let value = object
21            .get(field.json_name)
22            .or_else(|| object.get(field.name))
23            .ok_or_else(|| {
24                GestaltError::new(
25                    gestalt_error_code::INVALID_ARGUMENT,
26                    format!("missing path parameter {}", field.name),
27                )
28            })?;
29        let segment = scalar_to_string(value).ok_or_else(|| {
30            GestaltError::new(
31                gestalt_error_code::INVALID_ARGUMENT,
32                format!("unsupported path parameter type for {}", field.name),
33            )
34        })?;
35        out = out.replace(&token, &urlencoding::encode(&segment));
36    }
37    Ok(out)
38}
39
40fn is_path_field(field: &str, path_fields: &[PublicField]) -> bool {
41    path_fields
42        .iter()
43        .any(|path_field| path_field.name == field || path_field.json_name == field)
44}
45
46fn retained_fields<'a>(
47    request: &'a Map<String, Value>,
48    path_fields: &[PublicField],
49) -> impl Iterator<Item = (&'a String, &'a Value)> {
50    request
51        .iter()
52        .filter(move |(key, value)| !value.is_null() && !is_path_field(key, path_fields))
53}
54
55/// Builds query-string pairs from a protobuf JSON request object.
56pub fn build_query_pairs(
57    request: &Map<String, Value>,
58    path_fields: &[PublicField],
59) -> Vec<(String, String)> {
60    let mut pairs = Vec::new();
61    for (key, value) in retained_fields(request, path_fields) {
62        encode_query_value(key, value, &mut pairs);
63    }
64    pairs
65}
66
67/// Builds the JSON request body for REST calls.
68pub fn build_body_map(
69    request: &Map<String, Value>,
70    path_fields: &[PublicField],
71) -> Map<String, Value> {
72    retained_fields(request, path_fields)
73        .map(|(key, value)| (key.clone(), value.clone()))
74        .collect()
75}
76
77fn encode_query_value(key: &str, value: &Value, out: &mut Vec<(String, String)>) {
78    match value {
79        Value::Null => {}
80        Value::Array(items) => {
81            for item in items {
82                encode_query_value(key, item, out);
83            }
84        }
85        Value::Object(nested) => {
86            for (nested_key, nested_value) in nested {
87                encode_query_value(&format!("{key}.{nested_key}"), nested_value, out);
88            }
89        }
90        _ => {
91            if let Some(text) = scalar_to_string(value) {
92                out.push((key.to_string(), text));
93            }
94        }
95    }
96}
97
98/// Encodes query pairs as an `application/x-www-form-urlencoded` string.
99pub fn encode_query_string(pairs: &[(String, String)]) -> String {
100    pairs
101        .iter()
102        .map(|(key, value)| {
103            format!(
104                "{}={}",
105                urlencoding::encode(key),
106                urlencoding::encode(value)
107            )
108        })
109        .collect::<Vec<_>>()
110        .join("&")
111}
112
113fn scalar_to_string(value: &Value) -> Option<String> {
114    match value {
115        Value::String(text) => Some(text.clone()),
116        Value::Number(number) => Some(number.to_string()),
117        Value::Bool(flag) => Some(flag.to_string()),
118        _ => None,
119    }
120}