Files
.CooCoo ca1b5e2edb Add craft-ecproj-format: the envelope of EffectCraft project files (#409)
* Add craft-ecproj-format: the envelope of EffectCraft project files

An .ecproj file is one JSON object: savedBy, then the saved view's
members, then the project's members, written with two-space indentation.
The crate defines that envelope with the view and the project as type
parameters, so the project model stays in EffectCraft:

- SCHEMA_VERSION (2) and check_schema, which refuses a newer schema
  with EffectCraft's own message.
- File { saved_by, view, project }, serialized with both parts
  flattened, and File::to_json.
- Head, which reads savedBy and schema from a JSON object, and
  saved_by, which reads savedBy alone.

The tests write synthetic projects and compare the text with JSON
written by hand in the shape EffectCraft's writer gives. The workflow
runs fmt, clippy, the tests and a wasm32-unknown-unknown check.

* ci: do not keep the checkout token in the ecproj-format workflow

The checkout step sets persist-credentials: false, as jpeg.yml and media.yml do. No later step in the job uses the token.

* ecproj-format: write compact files, read savedBy as EffectCraft opens it

EffectCraft's Save Compact Project Files setting (effectcraft 07639b5f)
writes .ecproj files on one line with serde_json::to_string.
File::to_json_compact writes the same text, and the docs say the crate
writes and reads both forms.

Head now reads every file EffectCraft's Project::from_json opens: a
savedBy that is not a string, or appears more than once, reads as None,
and a project written as a JSON array reads with its first element as
the schema. saved_by also gives None for a repeated savedBy, as
EffectCraft's saved_by does. File's docs say the crate does not check
values and that serde_json writes NaN and infinity as null.

* ecproj-format: skip a non-string savedBy without keeping it

Head::read and saved_by read savedBy into a serde_json::Value before
dropping any value that is not a string, so a wide or deeply nested
savedBy was held in memory, and one nested past serde_json's recursion
limit made Head::read fail. A non-string savedBy, and every repeated
savedBy, is now skipped element by element with IgnoredAny, as other
members are. It still reads as None.

* ecproj-format: Head::read refuses a newer schema and reads objects only

Head::read is the one reader of a project file's envelope. It returns
ReadError: Json when the text is not one JSON object with a u32 schema
(the array form included), or TooNew when the schema is newer than
SCHEMA_VERSION, holding the file's savedBy so a message can name the
version that wrote it. TooNew reads as before. saved_by() is removed,
and Head no longer implements Deserialize.

TooNew, ReadError and ReadError::TooNew are #[non_exhaustive].
2026-11-01 19:56:26 -05:00

2.5 KiB

craft-ecproj-format

The envelope of EffectCraft .ecproj project files: the members around the project that EffectCraft writes and other apps read. The project model, the saved view and the schema 1 conversion stay in EffectCraft. No unsafe, no panics, and it builds for wasm32-unknown-unknown. The dependencies are serde, serde_json and thiserror.

craft-ecproj-format = { git = "https://github.com/storytold/craft-libs", rev = "<commit>" }

The file

A project file is one UTF-8 JSON object with no newline at the end: savedBy, then the saved view's members (activeComp, openComps, compTimes, each left out when empty), then the project's members, starting with schema. Projects saved before savedBy was added start with schema. EffectCraft indents the object with two spaces, or writes it on one line when Settings ▸ Project ▸ Save Compact Project Files is on. The crate writes and reads both forms.

Use

use craft_ecproj_format::{File, Head, check_schema};

// Writing
check_schema(project.schema)?;
let text = File::new(APP_VERSION, &view, &project).to_json()?;

// Reading
let head = Head::read(&text)?;
  • SCHEMA_VERSION is the newest project schema, 2.
  • check_schema(schema) -> Result<(), TooNew> refuses a schema newer than SCHEMA_VERSION. TooNew reads "project schema 3 is newer than this version supports (2)". It is #[non_exhaustive]: other crates read its schema and supported fields.
  • File { saved_by, view, project } serializes as one object, with view and project flattened into it. to_json() gives the file's text indented with two spaces, and to_json_compact() gives it on one line. Neither checks the values: serde_json writes NaN and infinity as null, so the caller checks its values before saving.
  • Head::read(text) -> Result<Head, ReadError> reads savedBy and schema from one JSON object, and skips other members. savedBy is None when it is absent, is not a string, or appears more than once. Text that is not a JSON object, has no schema that is a u32, or has two schema members gives ReadError::Json. A schema newer than SCHEMA_VERSION gives ReadError::TooNew { too_new, saved_by, .. }, which reads as TooNew does and holds the file's savedBy, so a message can name the version that wrote it. ReadError and its TooNew variant are #[non_exhaustive]: match them with a wildcard arm and ...

.ecprojx files hold the same object encoded as XML, which this crate does not read or write.