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 std::fmt;
4
5use crate::model::value::{Map, Value};
6use crate::text::write_string;
7
8/// A line no longer than this is written on one line by [`format`].
9const ONE_LINE: usize = 72;
10/// One indent step.
11pub(super) const INDENT: &str = "  ";
12
13/// `value` as editors write it at indent `level` (two spaces each):
14/// short lists of numbers and short objects of scalars on one line
15/// (`"margins": [0, 0, 16, 16]`), the rest one entry a line.
16pub fn format(value: &Value, level: usize) -> String {
17    let mut out = String::new();
18    write_value(value, level, &mut out);
19    out
20}
21
22/// `value` onto `out`: on one line if it fits, else an entry a line.
23fn write_value(value: &Value, level: usize, out: &mut String) {
24    let start = out.len();
25    if write_inline(value, out) {
26        return;
27    }
28    out.truncate(start);
29    match value {
30        Value::List(items) => {
31            out.push_str("[\n");
32            for (i, v) in items.iter().enumerate() {
33                if i > 0 {
34                    out.push_str(",\n");
35                }
36                push_indent(out, level + 1);
37                write_value(v, level + 1, out);
38            }
39            out.push('\n');
40            push_indent(out, level);
41            out.push(']');
42        }
43        Value::Map(map) => {
44            out.push_str("{\n");
45            for (i, (k, v)) in map.iter().enumerate() {
46                if i > 0 {
47                    out.push_str(",\n");
48                }
49                push_indent(out, level + 1);
50                write_string(k, out);
51                out.push_str(": ");
52                write_value(v, level + 1, out);
53            }
54            out.push('\n');
55            push_indent(out, level);
56            out.push('}');
57        }
58        other => {
59            write_scalar(other, out);
60        }
61    }
62}
63
64fn push_indent(out: &mut String, level: usize) {
65    for _ in 0..level {
66        out.push_str(INDENT);
67    }
68}
69
70pub(super) fn quoted(s: &str) -> String {
71    let mut out = String::with_capacity(s.len() + 2);
72    write_string(s, &mut out);
73    out
74}
75
76/// A finite float in the shortest spelling that reads back as exactly it,
77/// and as a float: `1.0`, `-0.0`, `0.1`, `1e-7`, `1e20` — nothing rounded
78/// away, a whole one still a float. (An integer is a `Value::Int`, written
79/// as one; a value that didn't change keeps the source's spelling.)
80pub(super) fn number(n: f64) -> String {
81    crate::text::float_string(n)
82}
83
84/// A scalar onto `out`; `false` (nothing written) for a list or a map.
85fn write_scalar(v: &Value, out: &mut String) -> bool {
86    match v {
87        Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
88        Value::Int(n) => {
89            fmt::Write::write_fmt(out, format_args!("{n}")).ok();
90        }
91        Value::Float(f) if f.is_finite() => out.push_str(&number(*f)),
92        Value::Null | Value::Float(_) => out.push_str("null"),
93        Value::List(_) | Value::Map(_) => return false,
94        // As the source has it: a typed value (`@file:…`) or an escaped `@@`, unread.
95        Value::Str(s) => write_string(s, out),
96        other => out.push_str(&crate::text::to_string(other)),
97    }
98    true
99}
100
101/// `v` onto `out` on one line, if it can be and fits ([`ONE_LINE`]); else
102/// `false`, with part of it written.
103fn write_inline(v: &Value, out: &mut String) -> bool {
104    if write_scalar(v, out) {
105        return true;
106    }
107    let start = out.len();
108    let written = match v {
109        Value::List(items) if items.is_empty() => {
110            out.push_str("[]");
111            return true;
112        }
113        Value::List(items) => write_numbers(items, out),
114        Value::Map(map) if map.is_empty() => {
115            out.push_str("{}");
116            return true;
117        }
118        Value::Map(map) => write_scalars(map, out),
119        _ => false,
120    };
121    written && out.len() - start <= ONE_LINE
122}
123
124/// A list of numbers and bools as `[1, 2]`; `false` if it has anything else.
125fn write_numbers(items: &[Value], out: &mut String) -> bool {
126    if !items
127        .iter()
128        .all(|x| matches!(x, Value::Int(_) | Value::Float(_) | Value::Bool(_)))
129    {
130        return false;
131    }
132    out.push('[');
133    for (i, x) in items.iter().enumerate() {
134        if i > 0 {
135            out.push_str(", ");
136        }
137        write_scalar(x, out);
138    }
139    out.push(']');
140    true
141}
142
143/// A map with no map in it as `{ "a": 1 }`; `false` if an entry can't be on one line.
144fn write_scalars(map: &Map, out: &mut String) -> bool {
145    out.push_str("{ ");
146    for (i, (k, x)) in map.iter().enumerate() {
147        if matches!(x, Value::Map(_)) {
148            return false;
149        }
150        if i > 0 {
151            out.push_str(", ");
152        }
153        write_string(k, out);
154        out.push_str(": ");
155        if !write_inline(x, out) {
156            return false;
157        }
158    }
159    out.push_str(" }");
160    true
161}
162
163/// `value` as compact JSON for a tool: maps in their order, strings as the
164/// source has them (a typed value's text unread).
165pub fn to_json(value: &Value) -> String {
166    let mut out = String::new();
167    json_into(value, &mut out);
168    out
169}
170
171fn json_into(value: &Value, out: &mut String) {
172    match value {
173        Value::List(items) => {
174            out.push('[');
175            for (i, v) in items.iter().enumerate() {
176                if i > 0 {
177                    out.push(',');
178                }
179                json_into(v, out);
180            }
181            out.push(']');
182        }
183        Value::Map(map) => {
184            out.push('{');
185            for (i, (k, v)) in map.iter().enumerate() {
186                if i > 0 {
187                    out.push(',');
188                }
189                write_string(k, out);
190                out.push(':');
191                json_into(v, out);
192            }
193            out.push('}');
194        }
195        Value::Str(s) => write_string(s, out),
196        Value::Float(f) if f.is_finite() => out.push_str(&f.to_string()),
197        Value::Float(_) => out.push_str("null"),
198        other => out.push_str(&crate::text::to_string(other)),
199    }
200}
201
202/// Whether two values are the same, a number however it's spelled (`1` = `1.0`).
203pub fn same(a: &Value, b: &Value) -> bool {
204    match (a, b) {
205        (Value::Int(_) | Value::Float(_), Value::Int(_) | Value::Float(_)) => {
206            // `-0.0` is not `0`: its sign is kept.
207            match (a.as_f64(), b.as_f64()) {
208                (Some(x), Some(y)) => x.to_bits() == y.to_bits(),
209                _ => false,
210            }
211        }
212        (Value::List(x), Value::List(y)) => {
213            x.len() == y.len() && x.iter().zip(y).all(|(p, q)| same(p, q))
214        }
215        (Value::Map(x), Value::Map(y)) => {
216            x.len() == y.len()
217                && x.iter()
218                    .zip(y.iter())
219                    .all(|((ka, p), (kb, q))| ka == kb && same(p, q))
220        }
221        _ => a == b,
222    }
223}