dwara_core/dataplane/
translation_graphql.rs

1//! REST <-> GraphQL translation (DW-100).
2//!
3//! The [`GraphqlTranslator`] implements `super::ProtocolTranslator` for
4//! routes that front a GraphQL upstream with a REST client (or vice
5//! versa). Two directions:
6//!
7//! - **REST-to-GraphQL** (`kind: rest_to_graphql`): the client sends a
8//!   REST JSON body; the translator builds a GraphQL request from a
9//!   config-supplied query template. The template carries `$variable`
10//!   placeholders that are filled from the JSON body's fields, and the
11//!   filled query plus a `variables` map become the GraphQL-over-HTTP
12//!   POST body the upstream expects.
13//! - **GraphQL-to-REST** (`kind: graphql_to_rest`): the upstream returns
14//!   a GraphQL response envelope `{ "data": { ... } }`; the translator
15//!   unwraps the `data` object and returns it as the REST JSON body the
16//!   client expects. (A GraphQL `errors` array maps to a 502 with the
17//!   errors as the body.)
18//!
19//! ## Template syntax
20//!
21//! The query template is plain GraphQL text with `$variable` references.
22//! Each `$name` in the template is resolved from the inbound JSON body's
23//! top-level `name` field (the value is also placed in the `variables`
24//! map sent to the upstream, so the upstream's own variable resolution
25//! works). A `$name` the body does not supply fails the translation
26//! closed (the request never reaches the upstream with a dangling
27//! variable).
28//!
29//! ## Feature gating
30//!
31//! Compiled under the `protocol_translation` cargo feature.
32
33use bytes::Bytes;
34use hyper::header::{HeaderMap, HeaderValue, CONTENT_TYPE};
35use hyper::{Method, Request, Response, StatusCode};
36use serde_json::Value;
37
38use super::translation::{
39    ProtocolTranslator, TranslatedRequest, TranslatedResponse, TranslationBody, TranslationError,
40};
41use crate::config::GraphqlTranslation;
42
43/// The GraphQL-over-HTTP media type (the upstream expects this on the
44/// request and returns it on the response).
45const GRAPHQL_CONTENT_TYPE: &str = "application/graphql+json";
46
47/// The REST/JSON media type (the client sends and expects).
48const JSON_CONTENT_TYPE: &str = "application/json";
49
50/// The REST-to-GraphQL / GraphQL-to-REST translator.
51#[derive(Debug, Clone)]
52pub struct GraphqlTranslator {
53    /// The direction this translator was built for.
54    direction: GraphqlDirection,
55    /// The query template (REST-to-GraphQL only; empty for the reverse
56    /// direction, which only unwraps the `data` envelope).
57    query_template: String,
58    /// The path to send the GraphQL upstream (REST-to-GraphQL rewrites
59    /// every request to this; default `/graphql`).
60    upstream_path: String,
61}
62
63/// The translation direction.
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum GraphqlDirection {
66    /// A REST client -> GraphQL upstream.
67    RestToGraphql,
68    /// A GraphQL client -> REST upstream (the response path unwraps the
69    /// GraphQL `data` envelope into a REST body).
70    GraphqlToRest,
71}
72
73impl GraphqlTranslator {
74    /// Build a REST-to-GraphQL translator from a route's GraphQL
75    /// translation config.
76    pub fn rest_to_graphql(cfg: &GraphqlTranslation) -> Result<Self, TranslationError> {
77        if cfg.query_template.trim().is_empty() {
78            return Err(TranslationError::SchemaNotFound(
79                "graphql translation requires a query_template".to_string(),
80            ));
81        }
82        Ok(GraphqlTranslator {
83            direction: GraphqlDirection::RestToGraphql,
84            query_template: cfg.query_template.clone(),
85            upstream_path: cfg.upstream_path.clone(),
86        })
87    }
88
89    /// Build a GraphQL-to-REST translator. The reverse direction only
90    /// unwraps the GraphQL `data` envelope on the response path, so no
91    /// query template is required.
92    pub fn graphql_to_rest() -> Self {
93        GraphqlTranslator {
94            direction: GraphqlDirection::GraphqlToRest,
95            query_template: String::new(),
96            upstream_path: String::new(),
97        }
98    }
99
100    /// The configured direction.
101    pub fn direction(&self) -> GraphqlDirection {
102        self.direction
103    }
104}
105
106impl ProtocolTranslator for GraphqlTranslator {
107    fn translate_request(
108        &self,
109        req: &Request<TranslationBody>,
110    ) -> Result<TranslatedRequest, TranslationError> {
111        match self.direction {
112            GraphqlDirection::RestToGraphql => {
113                let body = req.body().as_bytes();
114                let json: Value = serde_json::from_slice(body).map_err(|e| {
115                    TranslationError::InvalidBody(format!("expected a JSON body: {e}"))
116                })?;
117                let obj = json.as_object().ok_or_else(|| {
118                    TranslationError::InvalidBody(
119                        "expected a JSON object for the GraphQL variables".to_string(),
120                    )
121                })?;
122
123                // Collect every $variable referenced by the template and
124                // resolve it from the JSON body. Each resolved value is
125                // placed in the `variables` map the upstream receives.
126                let variables = collect_variables(&self.query_template, obj)?;
127
128                // Substitute the $variables into the query text. The
129                // substituted query is sent verbatim (the variables map
130                // is also sent, so the upstream can resolve either way;
131                // sending both matches Apollo's over-HTTP convention).
132                let query = substitute_template(&self.query_template, &variables);
133
134                let gql_body = serde_json::json!({
135                    "query": query,
136                    "variables": variables,
137                });
138                let body = serde_json::to_vec(&gql_body).map_err(|e| {
139                    TranslationError::TranslationFailed(format!(
140                        "failed to serialize graphql request: {e}"
141                    ))
142                })?;
143
144                let mut headers = HeaderMap::new();
145                headers.insert(CONTENT_TYPE, HeaderValue::from_static(GRAPHQL_CONTENT_TYPE));
146
147                Ok(TranslatedRequest {
148                    method: Method::POST,
149                    path: self.upstream_path.clone(),
150                    headers,
151                    body: Bytes::from(body),
152                })
153            }
154            GraphqlDirection::GraphqlToRest => {
155                // A GraphQL client -> REST upstream: the client already
156                // sent a GraphQL body; pass it through unchanged (the
157                // upstream is the REST backend, so no request translation
158                // is needed -- the response path does the work).
159                let mut headers = HeaderMap::new();
160                if let Some(ct) = req.headers().get(CONTENT_TYPE).cloned() {
161                    headers.insert(CONTENT_TYPE, ct);
162                } else {
163                    headers.insert(CONTENT_TYPE, HeaderValue::from_static(JSON_CONTENT_TYPE));
164                }
165                Ok(TranslatedRequest {
166                    method: req.method().clone(),
167                    path: req.uri().path().to_string(),
168                    headers,
169                    body: req.body().as_bytes().clone(),
170                })
171            }
172        }
173    }
174
175    fn translate_response(
176        &self,
177        resp: &Response<TranslationBody>,
178    ) -> Result<TranslatedResponse, TranslationError> {
179        match self.direction {
180            GraphqlDirection::RestToGraphql => {
181                // The upstream returned a GraphQL response; unwrap the
182                // `data` envelope into a REST body. A GraphQL `errors`
183                // array maps to a 502 with the errors as the body.
184                let body = resp.body().as_bytes();
185                let gql: Value = serde_json::from_slice(body).map_err(|e| {
186                    TranslationError::InvalidBody(format!("expected a graphql JSON response: {e}"))
187                })?;
188                if let Some(errors) = gql.get("errors") {
189                    if !errors.is_null() {
190                        let body = serde_json::to_vec(errors).map_err(|e| {
191                            TranslationError::TranslationFailed(format!(
192                                "failed to serialize graphql errors: {e}"
193                            ))
194                        })?;
195                        let mut headers = HeaderMap::new();
196                        headers.insert(CONTENT_TYPE, HeaderValue::from_static(JSON_CONTENT_TYPE));
197                        return Ok(TranslatedResponse {
198                            status: StatusCode::BAD_GATEWAY,
199                            headers,
200                            body: Bytes::from(body),
201                        });
202                    }
203                }
204                let data = gql.get("data").cloned().unwrap_or(Value::Null);
205                let body = serde_json::to_vec(&data).map_err(|e| {
206                    TranslationError::TranslationFailed(format!(
207                        "failed to serialize rest response: {e}"
208                    ))
209                })?;
210                let mut headers = HeaderMap::new();
211                headers.insert(CONTENT_TYPE, HeaderValue::from_static(JSON_CONTENT_TYPE));
212                Ok(TranslatedResponse {
213                    status: resp.status(),
214                    headers,
215                    body: Bytes::from(body),
216                })
217            }
218            GraphqlDirection::GraphqlToRest => {
219                // The REST upstream returned a plain JSON body; wrap it
220                // in the GraphQL `{ "data": ... }` envelope the client
221                // expects.
222                let body = resp.body().as_bytes();
223                let data: Value = if body.is_empty() {
224                    Value::Null
225                } else {
226                    serde_json::from_slice(body).map_err(|e| {
227                        TranslationError::InvalidBody(format!(
228                            "expected a JSON response from the rest upstream: {e}"
229                        ))
230                    })?
231                };
232                let gql = serde_json::json!({ "data": data });
233                let body = serde_json::to_vec(&gql).map_err(|e| {
234                    TranslationError::TranslationFailed(format!(
235                        "failed to serialize graphql envelope: {e}"
236                    ))
237                })?;
238                let mut headers = HeaderMap::new();
239                headers.insert(CONTENT_TYPE, HeaderValue::from_static(GRAPHQL_CONTENT_TYPE));
240                Ok(TranslatedResponse {
241                    status: resp.status(),
242                    headers,
243                    body: Bytes::from(body),
244                })
245            }
246        }
247    }
248
249    fn content_type_in(&self) -> &str {
250        match self.direction {
251            GraphqlDirection::RestToGraphql => JSON_CONTENT_TYPE,
252            GraphqlDirection::GraphqlToRest => GRAPHQL_CONTENT_TYPE,
253        }
254    }
255
256    fn content_type_out(&self) -> &str {
257        match self.direction {
258            GraphqlDirection::RestToGraphql => GRAPHQL_CONTENT_TYPE,
259            GraphqlDirection::GraphqlToRest => JSON_CONTENT_TYPE,
260        }
261    }
262}
263
264/// Collect every `$variable` referenced by the template and resolve it
265/// from the JSON body. Returns a JSON object mapping variable name ->
266/// value. A variable the body does not supply fails closed.
267fn collect_variables(
268    template: &str,
269    body: &serde_json::Map<String, Value>,
270) -> Result<Value, TranslationError> {
271    let mut vars = serde_json::Map::new();
272    for name in referenced_variables(template) {
273        if vars.contains_key(&name) {
274            continue;
275        }
276        let value = body.get(&name).ok_or_else(|| {
277            TranslationError::TranslationFailed(format!(
278                "graphql template references ${name} but the request body has no such field"
279            ))
280        })?;
281        vars.insert(name.clone(), value.clone());
282    }
283    Ok(Value::Object(vars))
284}
285
286/// Scan a query template for `$variable` references. A variable name is
287/// a `$` followed by one or more GraphQL name characters
288/// (`[A-Za-z_][A-Za-z0-9_]*`). Returns the names in first-reference
289/// order, deduplicated.
290fn referenced_variables(template: &str) -> Vec<String> {
291    let bytes = template.as_bytes();
292    let mut names = Vec::new();
293    let mut seen = std::collections::HashSet::new();
294    let mut i = 0;
295    while i < bytes.len() {
296        if bytes[i] == b'$' && i + 1 < bytes.len() && is_name_start(bytes[i + 1]) {
297            let start = i + 1;
298            let mut j = start;
299            while j < bytes.len() && is_name_char(bytes[j]) {
300                j += 1;
301            }
302            let name = &template[start..j];
303            if seen.insert(name.to_string()) {
304                names.push(name.to_string());
305            }
306            i = j;
307            continue;
308        }
309        i += 1;
310    }
311    names
312}
313
314/// Substitute `$variable` references in the template with the JSON
315/// variable values, serialized as GraphQL literal values (strings
316/// quoted, numbers bare, booleans bare, null bare, objects/arrays
317/// inlined). The substituted query is sent verbatim to the upstream.
318fn substitute_template(template: &str, variables: &Value) -> String {
319    let bytes = template.as_bytes();
320    let mut out = String::with_capacity(template.len());
321    let mut i = 0;
322    while i < bytes.len() {
323        if bytes[i] == b'$' && i + 1 < bytes.len() && is_name_start(bytes[i + 1]) {
324            let start = i + 1;
325            let mut j = start;
326            while j < bytes.len() && is_name_char(bytes[j]) {
327                j += 1;
328            }
329            let name = &template[start..j];
330            if let Some(value) = variables.get(name) {
331                out.push_str(&json_to_graphql_literal(value));
332            } else {
333                // An unresolved variable: leave the reference intact
334                // (the variables map carries it; the upstream resolves).
335                out.push('$');
336                out.push_str(name);
337            }
338            i = j;
339            continue;
340        }
341        out.push(bytes[i] as char);
342        i += 1;
343    }
344    out
345}
346
347/// Render a JSON value as a GraphQL literal value.
348fn json_to_graphql_literal(value: &Value) -> String {
349    match value {
350        Value::Null => "null".to_string(),
351        Value::Bool(b) => b.to_string(),
352        Value::Number(n) => n.to_string(),
353        Value::String(s) => {
354            // A minimal string escape: double-quote and escape the
355            // GraphQL-significant characters. This is sufficient for
356            // template-substituted values; complex strings ride in the
357            // variables map instead.
358            let mut out = String::with_capacity(s.len() + 2);
359            out.push('"');
360            for c in s.chars() {
361                match c {
362                    '"' => out.push_str("\\\""),
363                    '\\' => out.push_str("\\\\"),
364                    '\n' => out.push_str("\\n"),
365                    '\r' => out.push_str("\\r"),
366                    '\t' => out.push_str("\\t"),
367                    c if (c as u32) < 0x20 => {
368                        out.push_str(&format!("\\u{:04x}", c as u32));
369                    }
370                    c => out.push(c),
371                }
372            }
373            out.push('"');
374            out
375        }
376        Value::Array(arr) => {
377            let items: Vec<String> = arr.iter().map(json_to_graphql_literal).collect();
378            format!("[{}]", items.join(", "))
379        }
380        Value::Object(obj) => {
381            let items: Vec<String> = obj
382                .iter()
383                .map(|(k, v)| format!("{}: {}", k, json_to_graphql_literal(v)))
384                .collect();
385            format!("{{{}}}", items.join(", "))
386        }
387    }
388}
389
390fn is_name_start(b: u8) -> bool {
391    b.is_ascii_alphabetic() || b == b'_'
392}
393
394fn is_name_char(b: u8) -> bool {
395    b.is_ascii_alphanumeric() || b == b'_'
396}