mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-27 03:46:50 -06:00
- Variables a library adds to a collection this file extends are now a library update of their own. `library::pending` counts published variables of an extended collection's library collection that have no copy yet; `library.updates` lists them by name without an `id` (no copy yet); `library.acceptUpdates` brings them in, inheriting. Before, they only arrived with the library's next other update. The old special case in `accept_updates` is gone (the pending list now carries them). - Extending an extended collection: a library collection that is itself an extended collection (a brand theme extending a core theme, published) can be extended in another file; values flow down the chain as each file accepts its library's updates, and every level's overrides stay. This already worked because a library's extended collection arrives as a plain copy; it is now checked end to end and documented, and the `sync_extensions` shortcut note says why local chains aren't followed (extending takes library collections only).
158 lines
8.0 KiB
Rust
158 lines
8.0 KiB
Rust
//! Extended collections' tail: variables a library adds to a collection this file extends are an
|
|
//! update of their own, and a library collection that is itself an extended collection can be
|
|
//! extended again, values flowing down the chain as each file accepts updates.
|
|
|
|
use figcraft_doc::*;
|
|
use serde_json::{Value, json};
|
|
|
|
use crate::Session;
|
|
use crate::params::id_of;
|
|
|
|
fn ex(s: &mut Session, cmd: &str, p: Value) -> Value {
|
|
s.execute(cmd, &p).unwrap_or_else(|e| panic!("{cmd} {p}: {e}"))
|
|
}
|
|
|
|
fn var<'a>(s: &'a Session, collection: &str, name: &str) -> &'a Variable {
|
|
let doc = s.doc().unwrap();
|
|
doc.variables.variables.iter().find(|v| v.collection == collection && v.name == name).unwrap()
|
|
}
|
|
|
|
/// `name`'s value in mode `mode` (by name) of `collection`, as text.
|
|
fn value(s: &Session, collection: &str, name: &str, mode: &str) -> String {
|
|
let doc = s.doc().unwrap();
|
|
let m = doc.variables.collection(collection).and_then(|c| c.find_mode(mode)).unwrap().id.clone();
|
|
match var(s, collection, name).values.get(&m) {
|
|
Some(VariableValue::Color(c)) => c.to_hex(),
|
|
Some(VariableValue::Number(n)) => n.to_string(),
|
|
other => format!("{other:?}"),
|
|
}
|
|
}
|
|
|
|
fn healthy(s: &Session) {
|
|
let doc = s.doc().unwrap();
|
|
assert!(doc.validate().is_empty(), "{:?}", doc.validate());
|
|
}
|
|
|
|
fn dir(name: &str) -> std::path::PathBuf {
|
|
let d = std::env::temp_dir().join(format!("figcraft-extchain-{}-{name}", std::process::id()));
|
|
std::fs::create_dir_all(&d).unwrap();
|
|
d
|
|
}
|
|
|
|
/// A library file with "Theme" (Light/Dark: `primary`, `radius`) and "Other" (`size`), published
|
|
/// and saved at `path`; returns (session, library id).
|
|
fn core(path: &str) -> (Session, String) {
|
|
let mut lib = Session::new();
|
|
ex(&mut lib, "variable.createCollection", json!({"name": "Theme", "modes": ["Light", "Dark"]}));
|
|
ex(
|
|
&mut lib,
|
|
"variable.create",
|
|
json!({"collection": "Theme", "name": "primary", "type": "color", "values": {"Light": "#0000ff", "Dark": "#ffffff"}}),
|
|
);
|
|
ex(&mut lib, "variable.create", json!({"collection": "Theme", "name": "radius", "type": "number", "value": 4}));
|
|
ex(&mut lib, "variable.createCollection", json!({"name": "Other"}));
|
|
ex(&mut lib, "variable.create", json!({"collection": "Other", "name": "size", "type": "number", "value": 1}));
|
|
let id = ex(&mut lib, "library.publish", json!({}))["id"].as_str().unwrap().to_string();
|
|
ex(&mut lib, "file.saveAs", json!({"path": path}));
|
|
(lib, id)
|
|
}
|
|
|
|
fn republish(lib: &mut Session) {
|
|
ex(lib, "library.publish", json!({}));
|
|
ex(lib, "file.save", json!({}));
|
|
}
|
|
|
|
#[test]
|
|
fn variables_new_to_an_extended_collection_are_an_update_of_their_own() {
|
|
let d = dir("new");
|
|
let path = d.join("core.figcraft").to_string_lossy().into_owned();
|
|
let (mut lib, lid) = core(&path);
|
|
let mut s = Session::new();
|
|
ex(&mut s, "library.add", json!({"path": path}));
|
|
let ext = ex(&mut s, "library.extendCollection", json!({"library": lid, "collection": "Theme"}))["id"].as_str().unwrap().to_string();
|
|
assert_eq!(ex(&mut s, "library.updates", json!({}))["count"], 0);
|
|
|
|
// The library only adds a variable to the extended collection (and one to a collection this
|
|
// file doesn't use): that alone is an update, listed by name.
|
|
ex(&mut lib, "variable.create", json!({"collection": "Theme", "name": "gap", "type": "number", "value": 8}));
|
|
ex(&mut lib, "variable.create", json!({"collection": "Other", "name": "unused", "type": "number", "value": 2}));
|
|
republish(&mut lib);
|
|
let u = ex(&mut s, "library.updates", json!({}));
|
|
assert_eq!(u["count"], 1, "{u}");
|
|
let row = &u["libraries"][0]["variables"][0];
|
|
assert_eq!((&row["name"], &row["id"]), (&json!("gap"), &Value::Null), "no copy yet: no id");
|
|
// Accepting brings it in, inheriting; then nothing is pending.
|
|
let done = ex(&mut s, "library.acceptUpdates", json!({}));
|
|
assert_eq!(done["variables"], 1);
|
|
let gap = var(&s, &ext, "gap");
|
|
assert!(gap.extends.is_some() && gap.overrides.is_empty());
|
|
assert_eq!(value(&s, &ext, "gap", "Dark"), "8");
|
|
assert_eq!(ex(&mut s, "library.updates", json!({}))["count"], 0);
|
|
// Docs silent: the variable added to a collection nobody here uses stays out.
|
|
assert!(!s.doc().unwrap().variables.variables.iter().any(|v| v.name == "unused"));
|
|
healthy(&s);
|
|
std::fs::remove_dir_all(&d).ok();
|
|
}
|
|
|
|
#[test]
|
|
fn an_extended_collection_can_be_extended_again() {
|
|
let d = dir("chain");
|
|
let core_path = d.join("core.figcraft").to_string_lossy().into_owned();
|
|
let brand_path = d.join("brand.figcraft").to_string_lossy().into_owned();
|
|
let (mut core_lib, core_id) = core(&core_path);
|
|
|
|
// A brand library extends the core theme, overrides a value, adds a variable and publishes.
|
|
let mut brand = Session::new();
|
|
ex(&mut brand, "library.add", json!({"path": core_path}));
|
|
let bext = ex(&mut brand, "library.extendCollection", json!({"library": core_id, "collection": "Theme", "name": "Brand"}))["id"]
|
|
.as_str()
|
|
.unwrap()
|
|
.to_string();
|
|
let bprimary = var(&brand, &bext, "primary").id.clone();
|
|
ex(&mut brand, "variable.set", json!({"variable": bprimary, "mode": "Light", "value": "#ff0000"}));
|
|
ex(&mut brand, "variable.create", json!({"collection": bext, "name": "logo", "type": "number", "value": 3}));
|
|
let brand_id = ex(&mut brand, "library.publish", json!({}))["id"].as_str().unwrap().to_string();
|
|
ex(&mut brand, "file.saveAs", json!({"path": brand_path}));
|
|
|
|
// A product file extends the brand's extended collection: the brand's values, modes and
|
|
// variables come along, and it overrides one of its own.
|
|
let mut s = Session::new();
|
|
ex(&mut s, "library.add", json!({"path": brand_path}));
|
|
let pext = ex(&mut s, "library.extendCollection", json!({"library": brand_id, "collection": "Brand"}))["id"].as_str().unwrap().to_string();
|
|
let modes: Vec<String> = s.doc().unwrap().variables.collection(&pext).unwrap().modes.iter().map(|m| m.name.clone()).collect();
|
|
assert_eq!(modes, ["Light", "Dark"]);
|
|
assert_eq!(value(&s, &pext, "primary", "Light"), "FF0000", "the brand's override is inherited");
|
|
assert_eq!(value(&s, &pext, "primary", "Dark"), "FFFFFF", "the core value is inherited");
|
|
assert_eq!(value(&s, &pext, "logo", "Light"), "3", "the brand's own variable is inherited");
|
|
let radius = var(&s, &pext, "radius").id.clone();
|
|
ex(&mut s, "variable.set", json!({"variable": radius, "mode": "Dark", "value": 12}));
|
|
let r = id_of(&ex(&mut s, "shape.rectangle", json!({"x": 0, "y": 0, "width": 10, "height": 10}))["id"]).unwrap();
|
|
let primary = var(&s, &pext, "primary").id.clone();
|
|
ex(&mut s, "variable.bind", json!({"ids": [r], "field": "fill", "variable": primary}));
|
|
|
|
// The core changes: the brand accepts and republishes, then the product accepts. Values
|
|
// follow down the chain; each level's overrides stay.
|
|
ex(&mut core_lib, "variable.set", json!({"variable": "primary", "mode": "Dark", "value": "#222222"}));
|
|
ex(&mut core_lib, "variable.set", json!({"variable": "radius", "mode": "Light", "value": 6}));
|
|
republish(&mut core_lib);
|
|
ex(&mut brand, "library.acceptUpdates", json!({}));
|
|
assert_eq!(value(&brand, &bext, "primary", "Dark"), "222222");
|
|
assert_eq!(value(&brand, &bext, "primary", "Light"), "FF0000");
|
|
republish(&mut brand);
|
|
assert!(ex(&mut s, "library.updates", json!({}))["count"].as_u64().unwrap() > 0);
|
|
ex(&mut s, "library.acceptUpdates", json!({}));
|
|
assert_eq!(value(&s, &pext, "primary", "Dark"), "222222");
|
|
assert_eq!(value(&s, &pext, "primary", "Light"), "FF0000");
|
|
assert_eq!(value(&s, &pext, "radius", "Light"), "6");
|
|
assert_eq!(value(&s, &pext, "radius", "Dark"), "12", "the product's override stays");
|
|
healthy(&s);
|
|
|
|
// Saved and reopened, the chain's end is the same.
|
|
let user = d.join("product.figcraft").to_string_lossy().into_owned();
|
|
ex(&mut s, "file.saveAs", json!({"path": user}));
|
|
let mut t = Session::empty();
|
|
ex(&mut t, "file.open", json!({"path": user}));
|
|
assert_eq!(t.doc().unwrap().variables, s.doc().unwrap().variables);
|
|
std::fs::remove_dir_all(&d).ok();
|
|
}
|