Skip to main content

lino_rest_api/
codec.rs

1//! Codec layer: every conversion between a [`LinoValue`] and the wire.
2//!
3//! All Links Notation work is delegated to [`lino_objects_codec`], which
4//! produces byte-identical readable output in JavaScript, Python, Rust and C#;
5//! this module adapts it and adds the media-type dispatch described in
6//! `docs/spec/README.md` section 2.
7
8use std::fmt;
9
10use lino_objects_codec::{
11    LinoValue, decode as decode_any, decode_line, encode as encode_readable, encode_compact,
12    encode_line,
13};
14use serde_json::{Map, Number, Value as JsonValue};
15
16use crate::media_type::{
17    JSON_CONTENT_TYPE, LINO_COMPACT_CONTENT_TYPE, LINO_CONTENT_TYPE, LINO_LINE_CONTENT_TYPE,
18    normalize_media_type,
19};
20
21/// Why a value could not be moved between a representation and a [`LinoValue`].
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum CodecError {
24    /// The media type is not a representation of this API.
25    UnsupportedMediaType(String),
26    /// The body is not a well-formed document of its media type.
27    Malformed(String),
28}
29
30impl fmt::Display for CodecError {
31    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
32        match self {
33            CodecError::UnsupportedMediaType(media_type) => {
34                write!(formatter, "Unsupported media type: {media_type}")
35            }
36            CodecError::Malformed(detail) => write!(formatter, "{detail}"),
37        }
38    }
39}
40
41impl std::error::Error for CodecError {}
42
43/// Encode a value as readable, indented Links Notation.
44pub fn encode(value: &LinoValue) -> String {
45    encode_readable(value)
46}
47
48/// Decode readable or compact Links Notation into a value.
49pub fn decode(notation: &str) -> Result<LinoValue, CodecError> {
50    decode_any(notation).map_err(|error| CodecError::Malformed(error.to_string()))
51}
52
53/// Encode a value as single-line readable Links Notation.
54pub fn encode_single_line(value: &LinoValue) -> String {
55    encode_line(value)
56}
57
58/// Decode one line of single-line readable Links Notation.
59pub fn decode_single_line(notation: &str) -> Result<LinoValue, CodecError> {
60    decode_line(notation).map_err(|error| CodecError::Malformed(error.to_string()))
61}
62
63/// Encode a value as compact, type-tagged Links Notation.
64///
65/// This is the only representation that preserves shared object identity and
66/// circular references.
67pub fn encode_compact_notation(value: &LinoValue) -> String {
68    encode_compact(value)
69}
70
71/// Encode a value for a concrete media type.
72pub fn encode_for(value: &LinoValue, media_type: &str) -> Result<String, CodecError> {
73    match normalize_media_type(media_type) {
74        LINO_CONTENT_TYPE => Ok(encode(value)),
75        LINO_LINE_CONTENT_TYPE => Ok(encode_single_line(value)),
76        LINO_COMPACT_CONTENT_TYPE => Ok(encode_compact_notation(value)),
77        JSON_CONTENT_TYPE => Ok(to_json_string(value)),
78        _ => Err(CodecError::UnsupportedMediaType(media_type.to_string())),
79    }
80}
81
82/// Decode a representation of a concrete media type.
83pub fn decode_from(body: &str, media_type: &str) -> Result<LinoValue, CodecError> {
84    match normalize_media_type(media_type) {
85        LINO_CONTENT_TYPE | LINO_COMPACT_CONTENT_TYPE => decode(body),
86        LINO_LINE_CONTENT_TYPE => decode_single_line(body),
87        JSON_CONTENT_TYPE => {
88            let json: JsonValue = serde_json::from_str(body)
89                .map_err(|error| CodecError::Malformed(error.to_string()))?;
90            Ok(from_json(&json))
91        }
92        _ => Err(CodecError::UnsupportedMediaType(media_type.to_string())),
93    }
94}
95
96/// Encode a value as JSON.
97///
98/// The separators and the raw Unicode match `JSON.stringify` byte for byte, so
99/// that the entity tag of a representation (section 7) is the same whichever
100/// implementation of this specification serves it.
101pub fn to_json_string(value: &LinoValue) -> String {
102    let mut out = String::new();
103    write_json(value, &mut out);
104    out
105}
106
107/// Append the JSON form of a value to a buffer.
108fn write_json(value: &LinoValue, out: &mut String) {
109    match value {
110        LinoValue::Null => out.push_str("null"),
111        LinoValue::Bool(flag) => out.push_str(if *flag { "true" } else { "false" }),
112        LinoValue::Int(number) => out.push_str(&number.to_string()),
113        // JSON has no NaN or Infinity; JSON.stringify writes them as null.
114        LinoValue::Float(number) => {
115            if number.is_finite() {
116                out.push_str(&JsonValue::from(*number).to_string());
117            } else {
118                out.push_str("null");
119            }
120        }
121        LinoValue::String(text) => out.push_str(&JsonValue::String(text.clone()).to_string()),
122        LinoValue::Array(items) => {
123            out.push('[');
124            for (index, item) in items.iter().enumerate() {
125                if index > 0 {
126                    out.push(',');
127                }
128                write_json(item, out);
129            }
130            out.push(']');
131        }
132        // Object keys keep the order they were inserted in, as they do in
133        // JavaScript and Python, so the encoded bytes are the same everywhere.
134        LinoValue::Object(entries) => {
135            out.push('{');
136            for (index, (key, item)) in entries.iter().enumerate() {
137                if index > 0 {
138                    out.push(',');
139                }
140                out.push_str(&JsonValue::String(key.clone()).to_string());
141                out.push(':');
142                write_json(item, out);
143            }
144            out.push('}');
145        }
146    }
147}
148
149/// Encode a value as indented JSON, as `JSON.stringify(value, null, 2)` does.
150///
151/// The OpenAPI document is served this way, so that the document a JavaScript,
152/// a Python and a Rust service publish are the same bytes.
153///
154/// # Examples
155///
156/// ```
157/// use lino_rest_api::codec::to_json_pretty;
158/// use lino_rest_api::value::{int, object};
159///
160/// assert_eq!(to_json_pretty(&object([("a", int(1))])), "{\n  \"a\": 1\n}");
161/// ```
162pub fn to_json_pretty(value: &LinoValue) -> String {
163    let mut out = String::new();
164    write_json_pretty(value, 0, &mut out);
165    out
166}
167
168/// Append the indented JSON form of a value to a buffer.
169fn write_json_pretty(value: &LinoValue, depth: usize, out: &mut String) {
170    let outer = "  ".repeat(depth);
171    let inner = "  ".repeat(depth + 1);
172    match value {
173        // An empty collection stays on one line, as `JSON.stringify` writes it.
174        LinoValue::Array(items) if items.is_empty() => out.push_str("[]"),
175        LinoValue::Object(entries) if entries.is_empty() => out.push_str("{}"),
176        LinoValue::Array(items) => {
177            out.push_str("[\n");
178            for (index, item) in items.iter().enumerate() {
179                if index > 0 {
180                    out.push_str(",\n");
181                }
182                out.push_str(&inner);
183                write_json_pretty(item, depth + 1, out);
184            }
185            out.push('\n');
186            out.push_str(&outer);
187            out.push(']');
188        }
189        LinoValue::Object(entries) => {
190            out.push_str("{\n");
191            for (index, (key, item)) in entries.iter().enumerate() {
192                if index > 0 {
193                    out.push_str(",\n");
194                }
195                out.push_str(&inner);
196                out.push_str(&JsonValue::String(key.clone()).to_string());
197                out.push_str(": ");
198                write_json_pretty(item, depth + 1, out);
199            }
200            out.push('\n');
201            out.push_str(&outer);
202            out.push('}');
203        }
204        scalar => write_json(scalar, out),
205    }
206}
207
208/// Convert a [`serde_json::Value`] into a [`LinoValue`], keeping key order.
209pub fn from_json(value: &JsonValue) -> LinoValue {
210    match value {
211        JsonValue::Null => LinoValue::Null,
212        JsonValue::Bool(flag) => LinoValue::Bool(*flag),
213        JsonValue::Number(number) => match number.as_i64() {
214            Some(integer) => LinoValue::Int(integer),
215            None => LinoValue::Float(number.as_f64().unwrap_or(f64::NAN)),
216        },
217        JsonValue::String(text) => LinoValue::String(text.clone()),
218        JsonValue::Array(items) => LinoValue::Array(items.iter().map(from_json).collect()),
219        JsonValue::Object(entries) => LinoValue::Object(
220            entries
221                .iter()
222                .map(|(key, item)| (key.clone(), from_json(item)))
223                .collect(),
224        ),
225    }
226}
227
228/// Convert a [`LinoValue`] into a [`serde_json::Value`], keeping key order.
229pub fn to_json(value: &LinoValue) -> JsonValue {
230    match value {
231        LinoValue::Null => JsonValue::Null,
232        LinoValue::Bool(flag) => JsonValue::Bool(*flag),
233        LinoValue::Int(number) => JsonValue::Number(Number::from(*number)),
234        LinoValue::Float(number) => Number::from_f64(*number)
235            .map(JsonValue::Number)
236            .unwrap_or(JsonValue::Null),
237        LinoValue::String(text) => JsonValue::String(text.clone()),
238        LinoValue::Array(items) => JsonValue::Array(items.iter().map(to_json).collect()),
239        LinoValue::Object(entries) => {
240            let mut map = Map::new();
241            for (key, item) in entries {
242                map.insert(key.clone(), to_json(item));
243            }
244            JsonValue::Object(map)
245        }
246    }
247}