Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
every arm is asserted against the exact line it used to arrive already rendered
with, and a real run whose records will not open is judged from outside the binary.

- **One writer spells `dl --ls --json` and `metadata.json` now, not two.** Both
are `json.dumps(..., indent=2)` documents that have to agree with the Python
build byte for byte, and each was produced by a hundred-line formatter of its
own: the same nine layout methods forwarded to `serde_json`'s pretty printer,
and the same `ensure_ascii` loop, in two files that had to stay
character-for-character equal without anything holding them there. What that
cost is on the record directly above this entry, since the escaping gate was
wrong about DEL in both copies and closing it meant closing it twice. `dl` now
calls core's, and the tree has one indented spelling.

Nothing about either document moves. The pins on `--ls --json` are the same
assertions against the same call they were written against, with the formatter
behind them deleted rather than edited: the shaped listing, the empty document,
an emoji as its surrogate pair, DEL, and the whole of ASCII against the line
`json.dumps` printed for it. `wf` parses that document, so the bar was
byte-identity and not equivalence.

### Fixed

Expand Down
1 change: 1 addition & 0 deletions rust/devlaunch-core/public-api.rest.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2836,6 +2836,7 @@ impl core::fmt::Debug for devlaunch_core::json::JsonKind
pub fn devlaunch_core::json::JsonKind::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Copy for devlaunch_core::json::JsonKind
impl core::marker::StructuralPartialEq for devlaunch_core::json::JsonKind
pub fn devlaunch_core::json::as_python_writes_it_indented(&serde_json::value::Value) -> alloc::string::String
pub mod devlaunch_core::notices
pub trait devlaunch_core::notices::Notices<T>
pub fn devlaunch_core::notices::Notices::say(&mut self, T)
Expand Down
132 changes: 28 additions & 104 deletions rust/devlaunch-core/src/domain/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1083,10 +1083,20 @@ fn resolve_link(path: &Path) -> PathBuf {
}

/// Serialize `document` the way Python's `json.dump(..., indent=2)` does.
///
/// The formatter is the crate's one indented spelling, which lives down in
/// `json` beside the compact one rather than here, because `dl --ls --json`
/// needs the same bytes and used to get them from a second copy (#346). A
/// `Document` is a struct rather than a [`serde_json::Value`], so it goes
/// through the formatter directly: routing it through a `Value` first would put
/// its field order at the mercy of the map implementation, and the field order
/// is part of what this file is pinned on.
fn encode(document: &Document<'_>) -> Result<Vec<u8>, MetadataError> {
let mut bytes = Vec::new();
let mut serializer =
serde_json::Serializer::with_formatter(&mut bytes, PythonJsonFormatter::default());
let mut serializer = serde_json::Serializer::with_formatter(
&mut bytes,
crate::json::PythonPrettyFormatter::default(),
);
document
.serialize(&mut serializer)
.map_err(|error| MetadataError::Encode {
Expand All @@ -1095,100 +1105,6 @@ fn encode(document: &Document<'_>) -> Result<Vec<u8>, MetadataError> {
Ok(bytes)
}

/// `json.dump(..., indent=2)`, escaping included.
///
/// Two-space indentation is what serde's pretty printer already does; what it
/// does not do is Python's `ensure_ascii`, which spells every character outside
/// `' '..'~'` as a `\uXXXX` escape — the printable range, not "non-ASCII", since
/// DEL is ASCII and Python escapes it too. A branch name with an umlaut in it is
/// enough to make the two builds write different bytes for the same data, so the
/// escaping is matched rather than left to chance.
///
/// The layout is what differs from the compact [`crate::json::PythonFormatter`] —
/// this document is indented and that one is on one line — so the escaping is
/// the crate's one copy of it rather than a second loop here that had to stay
/// character-for-character equal to survive. (The compact formatter also spells
/// floats Python's way; this one does not, which no `Document` can reach today
/// because its numbers are all `i64`.)
#[derive(Default)]
struct PythonJsonFormatter<'indent> {
pretty: serde_json::ser::PrettyFormatter<'indent>,
}

impl serde_json::ser::Formatter for PythonJsonFormatter<'_> {
fn write_string_fragment<W>(&mut self, writer: &mut W, fragment: &str) -> io::Result<()>
where
W: ?Sized + io::Write,
{
crate::json::write_ensure_ascii(writer, fragment)
}

// The rest is the pretty printer's layout, delegated unchanged.

fn begin_array<W>(&mut self, writer: &mut W) -> io::Result<()>
where
W: ?Sized + io::Write,
{
self.pretty.begin_array(writer)
}

fn end_array<W>(&mut self, writer: &mut W) -> io::Result<()>
where
W: ?Sized + io::Write,
{
self.pretty.end_array(writer)
}

fn begin_array_value<W>(&mut self, writer: &mut W, first: bool) -> io::Result<()>
where
W: ?Sized + io::Write,
{
self.pretty.begin_array_value(writer, first)
}

fn end_array_value<W>(&mut self, writer: &mut W) -> io::Result<()>
where
W: ?Sized + io::Write,
{
self.pretty.end_array_value(writer)
}

fn begin_object<W>(&mut self, writer: &mut W) -> io::Result<()>
where
W: ?Sized + io::Write,
{
self.pretty.begin_object(writer)
}

fn end_object<W>(&mut self, writer: &mut W) -> io::Result<()>
where
W: ?Sized + io::Write,
{
self.pretty.end_object(writer)
}

fn begin_object_key<W>(&mut self, writer: &mut W, first: bool) -> io::Result<()>
where
W: ?Sized + io::Write,
{
self.pretty.begin_object_key(writer, first)
}

fn begin_object_value<W>(&mut self, writer: &mut W) -> io::Result<()>
where
W: ?Sized + io::Write,
{
self.pretty.begin_object_value(writer)
}

fn end_object_value<W>(&mut self, writer: &mut W) -> io::Result<()>
where
W: ?Sized + io::Write,
{
self.pretty.end_object_value(writer)
}
}

#[cfg(test)]
mod tests {
//! The Python `test_worktree_storage`, re-pinned here when it retired with the
Expand Down Expand Up @@ -1700,8 +1616,10 @@ mod tests {
#[test]
fn the_indent_two_document_escapes_an_astral_character_as_the_pair() {
let mut bytes = Vec::new();
let mut serializer =
serde_json::Serializer::with_formatter(&mut bytes, PythonJsonFormatter::default());
let mut serializer = serde_json::Serializer::with_formatter(
&mut bytes,
crate::json::PythonPrettyFormatter::default(),
);
json!({ "branch": "feature/br\u{fc}nch", "tags": ["\u{1f680}", "plain"] })
.serialize(&mut serializer)
.expect("a Vec never fails to write");
Expand Down Expand Up @@ -1732,8 +1650,10 @@ mod tests {
#[test]
fn the_indent_two_document_spells_the_serde_escapes_pythons_way() {
let mut bytes = Vec::new();
let mut serializer =
serde_json::Serializer::with_formatter(&mut bytes, PythonJsonFormatter::default());
let mut serializer = serde_json::Serializer::with_formatter(
&mut bytes,
crate::json::PythonPrettyFormatter::default(),
);
json!({
"branch": "a\"b\\c\nd\te\rf\u{8}g\u{c}h",
"tags": ["\u{0}\u{1}\u{1f}", "/slash/", "\u{2028}\u{2029}"],
Expand Down Expand Up @@ -1770,8 +1690,10 @@ mod tests {
#[test]
fn the_indent_two_document_escapes_del_the_way_python_does() {
let mut bytes = Vec::new();
let mut serializer =
serde_json::Serializer::with_formatter(&mut bytes, PythonJsonFormatter::default());
let mut serializer = serde_json::Serializer::with_formatter(
&mut bytes,
crate::json::PythonPrettyFormatter::default(),
);
json!({ "branch": "a\u{7f}b", "tags": ["\u{7f}"] })
.serialize(&mut serializer)
.expect("a Vec never fails to write");
Expand Down Expand Up @@ -1801,8 +1723,10 @@ mod tests {
fn the_indent_two_document_spells_every_ascii_character_pythons_way() {
let all_of_ascii: String = (0u8..=0x7f).map(char::from).collect();
let mut bytes = Vec::new();
let mut serializer =
serde_json::Serializer::with_formatter(&mut bytes, PythonJsonFormatter::default());
let mut serializer = serde_json::Serializer::with_formatter(
&mut bytes,
crate::json::PythonPrettyFormatter::default(),
);
json!({ "branch": all_of_ascii })
.serialize(&mut serializer)
.expect("a Vec never fails to write");
Expand Down
Loading
Loading