Skip to main content

substrate_data/forms/patch/
format.rs

1//! [`format`]: a value written the way editors write it; [`same`]: values equal as written.
2
3use crate::model::value::Value;
4use crate::text::write_string;
5
6/// A line no longer than this is written on one line by [`format`].
7const ONE_LINE: usize = 72;
8/// One indent step.
9pub(super) const INDENT: &str = "  ";
10
11/// `value` as editors write it at indent `level` (two spaces each):
12/// short lists of numbers and short objects of scalars on one line
13/// (`"margins": [0, 0, 16, 16]`), the rest one entry a line.
14pub fn format(value: &Value, level: usize) -> String {
15    if let Some(one) = inline(value) {
16        return one;
17    }
18    let pad = INDENT.repeat(level + 1);
19    let end = INDENT.repeat(level);
20    match value {
21        Value::List(items) => {
22            let body: Vec<String> = items
23                .iter()
24                .map(|v| format!("{pad}{}", format(v, level + 1)))
25                .collect();
26            format!("[\n{}\n{end}]", body.join(",\n"))
27        }
28        Value::Map(map) => {
29            let body: Vec<String> = map
30                .iter()
31                .map(|(k, v)| format!("{pad}{}: {}", quoted(k), format(v, level + 1)))
32                .collect();
33            format!("{{\n{}\n{end}}}", body.join(",\n"))
34        }
35        other => scalar(other).unwrap_or_default(),
36    }
37}
38
39pub(super) fn quoted(s: &str) -> String {
40    let mut out = String::with_capacity(s.len() + 2);
41    write_string(s, &mut out);
42    out
43}
44
45/// A finite float in the shortest spelling that reads back as exactly it,
46/// and as a float: `1.0`, `-0.0`, `0.1`, `1e-7`, `1e20` — nothing rounded
47/// away, a whole one still a float. (An integer is a `Value::Int`, written
48/// as one; a value that didn't change keeps the source's spelling.)
49pub(super) fn number(n: f64) -> String {
50    crate::text::float_string(n)
51}
52
53fn scalar(v: &Value) -> Option<String> {
54    match v {
55        Value::Bool(b) => Some(b.to_string()),
56        Value::Int(n) => Some(n.to_string()),
57        Value::Float(f) if f.is_finite() => Some(number(*f)),
58        Value::Null | Value::Float(_) => Some("null".into()),
59        Value::List(_) | Value::Map(_) => None,
60        // As the source has it: a typed value (`@file:…`) or an escaped `@@`, unread.
61        Value::Str(s) => Some(quoted(s)),
62        other => Some(crate::text::to_string(other)),
63    }
64}
65
66fn inline(v: &Value) -> Option<String> {
67    if let Some(s) = scalar(v) {
68        return Some(s);
69    }
70    let out = match v {
71        Value::List(items) if items.is_empty() => return Some("[]".into()),
72        Value::List(items) => {
73            if !items
74                .iter()
75                .all(|x| matches!(x, Value::Int(_) | Value::Float(_) | Value::Bool(_)))
76            {
77                return None;
78            }
79            format!(
80                "[{}]",
81                items
82                    .iter()
83                    .filter_map(scalar)
84                    .collect::<Vec<_>>()
85                    .join(", ")
86            )
87        }
88        Value::Map(map) if map.is_empty() => return Some("{}".into()),
89        Value::Map(map) => {
90            let mut parts = Vec::with_capacity(map.len());
91            for (k, x) in map.iter() {
92                if matches!(x, Value::Map(_)) {
93                    return None;
94                }
95                parts.push(format!("{}: {}", quoted(k), inline(x)?));
96            }
97            format!("{{ {} }}", parts.join(", "))
98        }
99        _ => return None,
100    };
101    (out.len() <= ONE_LINE).then_some(out)
102}
103
104/// `value` as compact JSON for a tool: maps in their order, strings as the
105/// source has them (a typed value's text unread).
106pub fn to_json(value: &Value) -> String {
107    let mut out = String::new();
108    json_into(value, &mut out);
109    out
110}
111
112fn json_into(value: &Value, out: &mut String) {
113    match value {
114        Value::List(items) => {
115            out.push('[');
116            for (i, v) in items.iter().enumerate() {
117                if i > 0 {
118                    out.push(',');
119                }
120                json_into(v, out);
121            }
122            out.push(']');
123        }
124        Value::Map(map) => {
125            out.push('{');
126            for (i, (k, v)) in map.iter().enumerate() {
127                if i > 0 {
128                    out.push(',');
129                }
130                write_string(k, out);
131                out.push(':');
132                json_into(v, out);
133            }
134            out.push('}');
135        }
136        Value::Str(s) => write_string(s, out),
137        Value::Float(f) if f.is_finite() => out.push_str(&f.to_string()),
138        Value::Float(_) => out.push_str("null"),
139        other => out.push_str(&crate::text::to_string(other)),
140    }
141}
142
143/// Whether two values are the same, a number however it's spelled (`1` = `1.0`).
144pub fn same(a: &Value, b: &Value) -> bool {
145    match (a, b) {
146        (Value::Int(_) | Value::Float(_), Value::Int(_) | Value::Float(_)) => {
147            // `-0.0` is not `0`: its sign is kept.
148            match (a.as_f64(), b.as_f64()) {
149                (Some(x), Some(y)) => x.to_bits() == y.to_bits(),
150                _ => false,
151            }
152        }
153        (Value::List(x), Value::List(y)) => {
154            x.len() == y.len() && x.iter().zip(y).all(|(p, q)| same(p, q))
155        }
156        (Value::Map(x), Value::Map(y)) => {
157            x.len() == y.len()
158                && x.iter()
159                    .zip(y.iter())
160                    .all(|((ka, p), (kb, q))| ka == kb && same(p, q))
161        }
162        _ => a == b,
163    }
164}