Перейти к содержимому

Кто владеет каким местом

prefix структуры говорит, где она сидит, а поля её — пути под этим префиксом. Написать одно и то же место двум структурам ничто не мешает: ключ с точками достаёт так же глубоко, как и префикс с точками.

#[amethystate(prefix = "ui", version = 1)]
pub struct Ui {
#[amestate(path = "panels.left.visible", default = true)]
pub left_panel_visible: bool,
}
#[amethystate(prefix = "ui.panels", version = 1)]
pub struct Panels {
#[amestate(path = "left.visible", default = true)]
pub left_visible: bool,
}

Сложите префикс с ключом, и обе упрутся в один путь: ui.panels.left.visible.

Store держит таблицу занятого, и вторая структура, открывающаяся над тем же местом, не построится:

let _ui = Ui::new_with(&store);
let refused = Panels::try_new_with(&store)
.expect_err("`ui.panels.left.visible` is spelled by both of them");
let OpenStruct::Taken(taken) = &refused else {
panic!("{refused}")
};
let Taken {
at,
wanted_by,
held_at,
held_by,
} = &**taken;
println!("{wanted_by} wants {at}, which {held_by} already holds at {held_at}");

Ошибка — OpenStruct::Taken, и она называет обе стороны по обоим путям: at и wanted_by — у объявления, которому отказали, held_at и held_by — у того, что стоит. Пути расходятся, когда одно объявление достаёт другое через предка — root.b держит root.b.x, — и эта пара и есть весь диагноз.

Ничего не надо выуживать из напечатанного текста. Taken — один из шести вариантов, которыми отвечает конструктор, и match без ветки _ обязан его назвать. Остальные пять — в Ошибках.

Столкновение это кончается двумя способами, и отказ — дешёвый из них. Дорогой выглядит так: две структуры пишут в один путь, одна на каждом сохранении молча затирает значение другой, а чтобы это найти, надо прочесть два объявления, которые друг друга не поминают. Отказ приходит на вызове, который открывает структуру, и называет обе.

Два места пересекаются, когда одно держит другое. В этом всё правило, и оно симметрично: неважно, какое объявлено первым, важно только, что одно поддерево начинает другое.

два места пересекаются
ui и ui.panels да — второе внутри первого
ui.panels и ui да — та же пара, только наоборот
ui.panels и ui.status нет
ui и ui!x нет — ui!x другое имя, просто начинается с тех же букв

На поле чужой структуры префикс тоже не ляжет. Поле — такое же место, как всякое другое, поэтому структура с префиксом root.b получит отказ, если поле b под root уже объявила другая.

Места, которые не встречаются, никто не трогает, как бы близко они ни сидели:

#[amethystate(prefix = "ui.panels.right", version = 1)]
pub struct RightPanel {
#[amestate(default = true)]
pub visible: bool,
}

Ближе уже некуда: Ui держит ui.panels.left.visible, RightPanel — ui.panels.right.visible. Расходятся они на последнем общем уровне, ни один путь не начинает другой, и обе открываются в одном store. А вот Panels, которая метила в ui.panels, получала отказ — её место начинало место Ui.

Три правила — из них следует всё, что выше.

Лист владеет своим путём и всем, что под ним, а держать под ним нечего. Поддерево ему не нужно. Владеет он им затем, чтобы никто не залез под скаляр.

Карта владеет своим путём и всем, что под ним, а занимает один уровень. Ключи у неё появляются на ходу, объявить их заранее некому. Будь место под картой свободным, за каждый ещё не написанный ключ можно было бы спорить.

Структура своим префиксом не владеет — владеют её поля. Пути у неё все объявлены, а до необъявленного ей дела нет: уровень, на котором она стоит, остаётся свободным.

Вот эту разницу и стоит запомнить. Одно объявление:

#[amethystate(prefix = "ui")]
pub struct Panels {
#[amestate(default = {})]
pub open: ReactiveMap<String, u32>,
#[amestate(default = "dark".to_string())]
pub theme: String,
}

и что после него остаётся свободным:

ui · свободен — префикс никто не занимает
├─ theme ▪ объявил Panels; ниже ничего не бывает
├─ open ▪ объявил Panels, картой
│ ├─ left ▪ запись, имя ей дали на ходу
│ └─ right ▪ запись
└─ myplugin · свободен — сюда может писать расширение

ui.myplugin свободен: ui никто не занимал. ui.open.myplugin — нет: ui.open занят.

под префиксом структуры под префиксом карты
ui ui.open
├─ theme объявлено ├─ left запись
├─ open объявлено ├─ right запись
└─ myplugin можно └─ myplugin отказ

«Карта владеет всем поддеревом» и «карта занимает один уровень» — два разных утверждения, и верны оба. ui.open.left — запись карты. ui.open.left.px попадает в её поддерево, но записью не является, — отказ приходит именно поэтому: место не ничьё, просто карта до него не достаёт.

Глубина значения — не глубина store. Запись хранит свой тип целиком, закодированным, по одному пути: структура на пять уровней вглубь — это один ключ, а не пять уровней.

tree.roots.one ▪ один ключ, одно хранимое значение
{ name: "a", children: [ { … }, { … } ] }
что там внутри — дело кодека

Дроп структуры её место не освобождает. Таблица принадлежит store и живёт ровно столько же, сколько он.

Так задумано. Освобождай место дроп — и отказ зависел бы от того, когда значение вышло из области видимости: одна и та же программа то открывалась бы чисто, то падала, смотря по тому, где кончилась привязка let.

let field = StorePath::parse_joined("ui.panels.left.visible")?;
let owner = store.places().declared_by(&field);
println!("{owner:?}");

Ищут по точному пути, а записаны те пути, которые заняты на самом деле: собственный путь поля, а не префикс, под которым оно сидит. Поэтому Ui выше находится по ui.panels.left.visible, а не по ui.

Отвечает он именем схемы — тем самым, которое печатает отказ.