Skip to main content

http_nu/
response.rs

1use nu_protocol::Value;
2use std::collections::HashMap;
3use std::path::PathBuf;
4
5#[derive(Clone, Debug, serde::Serialize)]
6#[serde(untagged)]
7pub enum HeaderValue {
8    Single(String),
9    Multiple(Vec<String>),
10}
11
12/// HTTP response metadata extracted from pipeline metadata (`http.response`)
13#[derive(Clone, Debug, Default)]
14pub struct HttpResponseMeta {
15    pub status: Option<u16>,
16    pub headers: HashMap<String, HeaderValue>,
17}
18
19/// Special response types that bypass normal body handling
20#[derive(Clone, Debug)]
21pub struct Response {
22    pub status: u16,
23    pub headers: HashMap<String, HeaderValue>,
24    pub body_type: ResponseBodyType,
25}
26
27#[derive(Clone, Debug)]
28pub enum ResponseBodyType {
29    Normal,
30    Static {
31        root: PathBuf,
32        path: String,
33        fallback: Option<String>,
34    },
35    ReverseProxy {
36        target_url: String,
37        headers: HashMap<String, HeaderValue>,
38        preserve_host: bool,
39        strip_prefix: Option<String>,
40        request_body: Vec<u8>,
41        query: Option<HashMap<String, String>>,
42    },
43}
44
45#[derive(Debug)]
46pub enum ResponseTransport {
47    Empty,
48    Full(Vec<u8>),
49    Stream(tokio::sync::mpsc::Receiver<Vec<u8>>),
50}
51
52pub fn value_to_json(value: &Value) -> serde_json::Value {
53    match value {
54        Value::Nothing { .. } => serde_json::Value::Null,
55        Value::Bool { val, .. } => serde_json::Value::Bool(*val),
56        Value::Int { val, .. } => serde_json::Value::Number((*val).into()),
57        Value::Float { val, .. } => serde_json::Number::from_f64(*val)
58            .map(serde_json::Value::Number)
59            .unwrap_or(serde_json::Value::Null),
60        Value::String { val, .. } => serde_json::Value::String(val.clone()),
61        Value::List { vals, .. } => {
62            serde_json::Value::Array(vals.iter().map(value_to_json).collect())
63        }
64        Value::Record { val, .. } => {
65            let mut map = serde_json::Map::new();
66            for (k, v) in val.iter() {
67                map.insert(k.clone(), value_to_json(v));
68            }
69            serde_json::Value::Object(map)
70        }
71        // Types without a direct JSON analogue (dates, durations, filesizes,
72        // binary, closures, ...) fall back to their string rendering rather
73        // than panicking.
74        other => serde_json::Value::String(
75            other.to_expanded_string(", ", &nu_protocol::Config::default()),
76        ),
77    }
78}
79
80/// Extract HTTP response metadata from pipeline metadata's `http.response` field
81pub fn extract_http_response_meta(
82    metadata: Option<&nu_protocol::PipelineMetadata>,
83) -> HttpResponseMeta {
84    let Some(meta) = metadata else {
85        return HttpResponseMeta::default();
86    };
87
88    let Some(http_response) = meta.custom.get("http.response") else {
89        return HttpResponseMeta::default();
90    };
91
92    let Ok(record) = http_response.as_record() else {
93        return HttpResponseMeta::default();
94    };
95
96    let status = record
97        .get("status")
98        .and_then(|v| v.as_int().ok())
99        .map(|v| v as u16);
100
101    let headers = record
102        .get("headers")
103        .and_then(|v| v.as_record().ok())
104        .map(|headers_record| {
105            let mut map = HashMap::new();
106            for (k, v) in headers_record.iter() {
107                let header_value = match v {
108                    Value::String { val, .. } => HeaderValue::Single(val.clone()),
109                    Value::List { vals, .. } => {
110                        let strings: Vec<String> = vals
111                            .iter()
112                            .filter_map(|v| v.as_str().ok())
113                            .map(|s| s.to_string())
114                            .collect();
115                        HeaderValue::Multiple(strings)
116                    }
117                    _ => continue,
118                };
119                map.insert(k.clone(), header_value);
120            }
121            map
122        })
123        .unwrap_or_default();
124
125    HttpResponseMeta { status, headers }
126}
127
128pub fn value_to_bytes(value: Value) -> Vec<u8> {
129    match value {
130        Value::Nothing { .. } => Vec::new(),
131        Value::String { val, .. } => val.into_bytes(),
132        Value::Int { val, .. } => val.to_string().into_bytes(),
133        Value::Float { val, .. } => val.to_string().into_bytes(),
134        Value::Binary { val, .. } => val,
135        Value::Bool { val, .. } => val.to_string().into_bytes(),
136
137        // Records with __html field are unwrapped to HTML string
138        Value::Record { val, .. } if val.get("__html").is_some() => match val.get("__html") {
139            Some(Value::String { val, .. }) => val.clone().into_bytes(),
140            _ => Vec::new(),
141        },
142
143        // Both Lists and Records are encoded as JSON
144        Value::List { .. } | Value::Record { .. } => serde_json::to_string(&value_to_json(&value))
145            .unwrap_or_else(|_| String::new())
146            .into_bytes(),
147
148        _ => todo!("value_to_bytes: {:?}", value),
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::value_to_json;
155    use nu_protocol::{record, Span, Value};
156
157    // Regression: types without a direct JSON analogue render as their string
158    // form instead of panicking (the fallback arm used to be `todo!()`).
159    #[test]
160    fn unsupported_scalar_falls_back_to_string() {
161        let dur = Value::duration(3_600_000_000_000, Span::test_data());
162        assert!(matches!(value_to_json(&dur), serde_json::Value::String(_)));
163    }
164
165    #[test]
166    fn unsupported_value_nested_in_record_falls_back() {
167        let rec = Value::test_record(record! {
168            "elapsed" => Value::duration(1_000_000_000, Span::test_data()),
169        });
170        let json = value_to_json(&rec);
171        assert!(json.get("elapsed").map(|v| v.is_string()).unwrap_or(false));
172    }
173}