use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use crate::datatypes::values::Value;
pub const MAX_ONTOLOGY_CLASSES: usize = 512;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct OntologyStore {
#[serde(default = "default_version")]
pub version: u32,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub classes: BTreeMap<String, ClassDecl>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub relationships: BTreeMap<String, RelationshipDecl>,
}
fn default_version() -> u32 {
1
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct ClassDecl {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub is_a: Option<String>,
#[serde(
rename = "abstract",
default,
skip_serializing_if = "std::ops::Not::not"
)]
pub is_abstract: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub by: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct RelationshipDecl {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub domain: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub range: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub required_properties: Vec<String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub property_types: BTreeMap<String, String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub inverse_name: Option<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub inverse_enforced: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cardinality: Option<CardinalityDecl>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub required: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub transitive: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub ancestry: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub symmetric: bool,
#[serde(default, skip_serializing_if = "Enforcement::is_default")]
pub enforcement: Enforcement,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub enforcement_overrides: BTreeMap<String, Enforcement>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub exempt: BTreeMap<String, Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
impl RelationshipDecl {
pub fn enforcement_for(&self, check: &str) -> Enforcement {
self.enforcement_overrides
.get(check)
.copied()
.unwrap_or(self.enforcement)
}
pub(crate) fn enforcement_summary(&self) -> String {
let base = self.enforcement.as_str().to_string();
if self.enforcement_overrides.is_empty() {
return base;
}
let overrides: Vec<String> = self
.enforcement_overrides
.iter()
.map(|(check, sev)| format!("{check}={}", sev.as_str()))
.collect();
format!("{base}; {}", overrides.join(", "))
}
pub fn exempt_classes(&self, check: &str) -> &[String] {
self.exempt.get(check).map(Vec::as_slice).unwrap_or(&[])
}
pub(crate) fn exempt_summary(&self) -> Option<String> {
if self.exempt.is_empty() {
return None;
}
Some(
self.exempt
.iter()
.map(|(check, classes)| format!("{check}: [{}]", classes.join(", ")))
.collect::<Vec<_>>()
.join("; "),
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum Enforcement {
#[default]
Advisory,
Warn,
Error,
}
impl Enforcement {
fn is_default(&self) -> bool {
*self == Enforcement::Advisory
}
pub fn as_str(&self) -> &'static str {
match self {
Enforcement::Advisory => "advisory",
Enforcement::Warn => "warn",
Enforcement::Error => "error",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct CardinalityDecl {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max: Option<u64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ManagedLabelState {
Closed,
Open,
}
impl ManagedLabelState {
pub fn as_str(&self) -> &'static str {
match self {
ManagedLabelState::Closed => "closed",
ManagedLabelState::Open => "open",
}
}
}
pub fn arc_store_is_empty(store: &std::sync::Arc<OntologyStore>) -> bool {
store.is_empty()
}
impl OntologyStore {
pub fn is_empty(&self) -> bool {
self.classes.is_empty() && self.relationships.is_empty()
}
pub fn ancestors(&self, class: &str) -> Vec<String> {
let mut out = Vec::new();
let mut current = class;
while let Some(parent) = self.classes.get(current).and_then(|c| c.is_a.as_deref()) {
if out.len() > self.classes.len() {
break;
}
out.push(parent.to_string());
current = parent;
}
out
}
pub fn validate(&self) -> Result<(), String> {
if self.classes.len() > MAX_ONTOLOGY_CLASSES {
return Err(format!(
"ontology declares {} classes; the layer is for schema-level vocabularies \
(max {MAX_ONTOLOGY_CLASSES}). Large taxonomies are data — model them as edges \
walked with `*1..` paths, declaring that relationship `ancestry: true`. Do not \
reach for `transitive: true` there: it audits a *stored* closure, so a \
parent-pointer taxonomy reports 100% violations.",
self.classes.len()
));
}
for (name, decl) in &self.classes {
if let Some(parent) = &decl.is_a {
if !self.classes.contains_key(parent) {
return Err(format!(
"class '{name}': is_a target '{parent}' is not a declared class"
));
}
if parent == name {
return Err(format!("class '{name}': is_a itself"));
}
}
}
for name in self.classes.keys() {
let mut steps = 0usize;
let mut current = name.as_str();
while let Some(parent) = self.classes.get(current).and_then(|c| c.is_a.as_deref()) {
steps += 1;
if steps > self.classes.len() {
return Err(format!("class '{name}': is_a chain contains a cycle"));
}
current = parent;
}
}
for (name, decl) in &self.relationships {
for endpoint in [&decl.domain, &decl.range].into_iter().flatten() {
if endpoint.is_empty() {
return Err(format!("relationship '{name}': empty endpoint name"));
}
}
if decl.transitive && decl.ancestry {
return Err(format!(
"relationship '{name}': 'transitive' and 'ancestry' are mutually exclusive. \
'transitive' audits a stored closure — transitivity_violation flags every \
a→b→c with no stored a→c edge. 'ancestry' annotates a parent-pointer \
taxonomy walked with `*1..` and enrolls no check. Declare one."
));
}
for (check, classes) in &decl.exempt {
for class in classes {
if !self.classes.contains_key(class) {
return Err(format!(
"relationship '{name}': exempt['{check}'] names '{class}', which is \
not a declared class. An exemption widens over the declared is_a \
forest, so a name outside it would silently exempt nothing — \
declare the class (abstract or concrete) first."
));
}
}
}
if let Some(card) = &decl.cardinality {
if let (Some(min), Some(max)) = (card.min, card.max) {
if min > max {
return Err(format!(
"relationship '{name}': cardinality min {min} > max {max}"
));
}
}
}
}
Ok(())
}
}
pub fn ontology_from_value(doc: &Value) -> Result<OntologyStore, String> {
let map = as_map(doc).ok_or("ontology document must be a map")?;
reject_unknown(map, &["version", "classes", "relationships"], "ontology")?;
let mut store = OntologyStore {
version: 1,
..Default::default()
};
if let Some(v) = map.get("version") {
store.version = match v {
Value::Int64(n) if *n >= 1 => *n as u32,
_ => return Err("ontology 'version' must be a positive integer".to_string()),
};
}
if let Some(classes) = map.get("classes") {
let classes = as_map(classes).ok_or("ontology 'classes' must be a map")?;
for (name, decl) in classes {
store
.classes
.insert(name.to_string(), class_from_value(name, decl)?);
}
}
if let Some(rels) = map.get("relationships") {
let rels = as_map(rels).ok_or("ontology 'relationships' must be a map")?;
for (name, decl) in rels {
store
.relationships
.insert(name.to_string(), relationship_from_value(name, decl)?);
}
}
store.validate()?;
Ok(store)
}
pub fn ontology_from_json(json: &str) -> Result<OntologyStore, String> {
let parsed: serde_json::Value =
serde_json::from_str(json).map_err(|e| format!("ontology JSON parse: {e}"))?;
let value = crate::param::json_value_to_kglite_value(&parsed);
ontology_from_value(&value)
}
const PROPERTY_TYPE_NAMES: &[&str] = &[
"string",
"str",
"integer",
"int",
"i64",
"int64",
"float",
"double",
"f64",
"number",
"float64",
"boolean",
"bool",
"date",
"datetime",
"timestamp",
"uniqueid",
"point",
"any",
];
pub const CHECK_NAMES: &[&str] = &[
"domain",
"range",
"required",
"required_properties",
"property_types",
"cardinality",
"inverse",
"symmetric",
"transitive",
];
pub const EXEMPTABLE_CHECKS: &[&str] = &["required_properties", "property_types"];
const CLASS_KEYS: &[&str] = &["is_a", "abstract", "description", "by"];
const REL_KEYS: &[&str] = &[
"domain",
"range",
"required_properties",
"property_types",
"inverse_name",
"inverse_enforced",
"cardinality",
"required",
"transitive",
"ancestry",
"symmetric",
"enforcement",
"exempt",
"description",
];
fn class_from_value(name: &str, value: &Value) -> Result<ClassDecl, String> {
let map = as_map(value).ok_or_else(|| format!("class '{name}' must be a map"))?;
reject_unknown(map, CLASS_KEYS, &format!("class '{name}'"))?;
Ok(ClassDecl {
is_a: opt_string(map, "is_a", name)?,
is_abstract: opt_bool(map, "abstract", name)?,
description: opt_string(map, "description", name)?,
by: opt_string(map, "by", name)?,
})
}
fn relationship_from_value(name: &str, value: &Value) -> Result<RelationshipDecl, String> {
let map = as_map(value).ok_or_else(|| format!("relationship '{name}' must be a map"))?;
reject_unknown(map, REL_KEYS, &format!("relationship '{name}'"))?;
let severity = |s: &str| -> Result<Enforcement, String> {
match s {
"advisory" => Ok(Enforcement::Advisory),
"warn" => Ok(Enforcement::Warn),
"error" => Ok(Enforcement::Error),
other => Err(format!(
"relationship '{name}': enforcement '{other}' is not one of \
'advisory', 'warn', 'error'"
)),
}
};
let (enforcement, enforcement_overrides) = match map.get("enforcement") {
None => (Enforcement::Advisory, BTreeMap::new()),
Some(Value::String(s)) => (severity(s)?, BTreeMap::new()),
Some(other) => match as_map(other) {
Some(per_check) => {
let mut overrides = BTreeMap::new();
for (check, sv) in per_check {
if !CHECK_NAMES.contains(&check) {
return Err(format!(
"relationship '{name}': enforcement key '{check}' \
is not a check — use one of {CHECK_NAMES:?}"
));
}
let Value::String(sv) = sv else {
return Err(format!(
"relationship '{name}': enforcement['{check}'] \
must be a severity string"
));
};
overrides.insert(check.to_string(), severity(sv)?);
}
(Enforcement::Advisory, overrides)
}
None => {
return Err(format!(
"relationship '{name}': 'enforcement' must be a severity \
string or a {{check: severity}} map"
))
}
},
};
let cardinality = match map.get("cardinality") {
None => None,
Some(v) => {
let card = as_map(v)
.ok_or_else(|| format!("relationship '{name}': 'cardinality' must be a map"))?;
reject_unknown(
card,
&["min", "max"],
&format!("relationship '{name}' cardinality"),
)?;
Some(CardinalityDecl {
min: opt_u64(card, "min", name)?,
max: opt_u64(card, "max", name)?,
})
}
};
let required_properties = match map.get("required_properties") {
None => Vec::new(),
Some(Value::List(items)) => items
.iter()
.map(|v| match v {
Value::String(s) => Ok(s.clone()),
_ => Err(format!(
"relationship '{name}': 'required_properties' entries must be strings"
)),
})
.collect::<Result<_, _>>()?,
Some(_) => {
return Err(format!(
"relationship '{name}': 'required_properties' must be a list"
))
}
};
let property_types = match map.get("property_types") {
None => BTreeMap::new(),
Some(v) => {
let types = as_map(v)
.ok_or_else(|| format!("relationship '{name}': 'property_types' must be a map"))?;
let mut out = BTreeMap::new();
for (k, tv) in types {
match tv {
Value::String(s) => {
if !PROPERTY_TYPE_NAMES.contains(&s.to_lowercase().as_str()) {
return Err(format!(
"relationship '{name}': 'property_types' entry \
'{k}: {s}' names an unknown type — use one of \
string, integer, float, boolean, date, \
datetime, timestamp, point, any"
));
}
out.insert(k.to_string(), s.clone());
}
_ => {
return Err(format!(
"relationship '{name}': 'property_types' values must be strings"
))
}
}
}
out
}
};
let inverse_name = opt_string(map, "inverse_name", name)?;
let inverse_enforced = opt_bool(map, "inverse_enforced", name)?;
if inverse_enforced && inverse_name.is_none() {
return Err(format!(
"relationship '{name}': 'inverse_enforced' requires 'inverse_name'"
));
}
let exempt = exempt_from_value(name, map.get("exempt"))?;
Ok(RelationshipDecl {
domain: opt_string(map, "domain", name)?,
range: opt_string(map, "range", name)?,
required_properties,
property_types,
inverse_name,
inverse_enforced,
cardinality,
required: opt_bool(map, "required", name)?,
transitive: opt_bool(map, "transitive", name)?,
ancestry: opt_bool(map, "ancestry", name)?,
symmetric: opt_bool(map, "symmetric", name)?,
enforcement,
enforcement_overrides,
exempt,
description: opt_string(map, "description", name)?,
})
}
fn exempt_from_value(
name: &str,
value: Option<&Value>,
) -> Result<BTreeMap<String, Vec<String>>, String> {
let Some(value) = value else {
return Ok(BTreeMap::new());
};
if matches!(value, Value::List(_)) {
return Err(format!(
"relationship '{name}': 'exempt' must be a {{check: [class, ...]}} map, not a list \
— name the check each exemption applies to, e.g. \
exempt: {{required_properties: ['PetregLicence']}}"
));
}
let per_check = as_map(value).ok_or_else(|| {
format!("relationship '{name}': 'exempt' must be a {{check: [class, ...]}} map")
})?;
let mut out = BTreeMap::new();
for (check, classes) in per_check {
if !EXEMPTABLE_CHECKS.contains(&check) {
return Err(exempt_check_refusal(name, check));
}
let Value::List(items) = classes else {
return Err(format!(
"relationship '{name}': exempt['{check}'] must be a list of class names"
));
};
let mut names = Vec::with_capacity(items.len());
for item in items {
match item {
Value::String(s) if !s.is_empty() => names.push(s.clone()),
Value::String(_) => {
return Err(format!(
"relationship '{name}': exempt['{check}'] has an empty class name"
))
}
_ => {
return Err(format!(
"relationship '{name}': exempt['{check}'] entries must be strings"
))
}
}
}
out.insert(check.to_string(), names);
}
Ok(out)
}
fn exempt_check_refusal(name: &str, check: &str) -> String {
let why = match check {
"domain" | "range" | "required" | "cardinality" => {
"that check already tests the domain-side class, so exempting a class would \
disable it for that class outright"
}
"inverse" | "symmetric" | "transitive" => {
"it flags a tuple of nodes rather than one edge, so there is no single \
domain-side class to exempt"
}
_ => {
let suggestion =
crate::graph::mutation::validation::did_you_mean(check, EXEMPTABLE_CHECKS);
return format!(
"relationship '{name}': 'exempt' key '{check}' is not a check name.{suggestion} \
Exemption is accepted for {EXEMPTABLE_CHECKS:?} only."
);
}
};
format!(
"relationship '{name}': 'exempt' does not accept check '{check}' — {why}. Exemption is \
accepted for {EXEMPTABLE_CHECKS:?} only, where the exempted class is the edge's source \
type."
)
}
fn as_map(value: &Value) -> Option<&crate::datatypes::PropMap> {
match value {
Value::Map(map) => Some(map),
_ => None,
}
}
fn opt_string(
map: &crate::datatypes::PropMap,
key: &str,
ctx: &str,
) -> Result<Option<String>, String> {
match map.get(key) {
None | Some(Value::Null) => Ok(None),
Some(Value::String(s)) => Ok(Some(s.clone())),
Some(_) => Err(format!("'{ctx}': '{key}' must be a string")),
}
}
fn opt_bool(map: &crate::datatypes::PropMap, key: &str, ctx: &str) -> Result<bool, String> {
match map.get(key) {
None => Ok(false),
Some(Value::Boolean(b)) => Ok(*b),
Some(_) => Err(format!("'{ctx}': '{key}' must be a boolean")),
}
}
fn opt_u64(map: &crate::datatypes::PropMap, key: &str, ctx: &str) -> Result<Option<u64>, String> {
match map.get(key) {
None => Ok(None),
Some(Value::Int64(n)) if *n >= 0 => Ok(Some(*n as u64)),
Some(_) => Err(format!(
"relationship '{ctx}': cardinality '{key}' must be a non-negative integer"
)),
}
}
fn reject_unknown(
map: &crate::datatypes::PropMap,
accepted: &[&str],
ctx: &str,
) -> Result<(), String> {
for (key, _) in map {
if accepted.contains(&key) {
continue;
}
let suggestion = crate::graph::mutation::validation::did_you_mean(key, accepted);
if !suggestion.is_empty() {
return Err(format!("{ctx}: unknown key '{key}'.{suggestion}"));
}
let list = accepted
.iter()
.map(|k| format!("'{k}'"))
.collect::<Vec<_>>()
.join(", ");
return Err(format!(
"{ctx}: unknown key '{key}'. Accepted keys: {list}."
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(json: &str) -> Result<OntologyStore, String> {
ontology_from_json(json)
}
#[test]
fn round_trips_the_full_grammar() {
let store = parse(
r#"{"version": 1,
"classes": {
"Licensable": {"abstract": true, "description": "d"},
"Licence": {"is_a": "Licensable", "by": "kind"}
},
"relationships": {
"HAS_OPERATOR": {
"domain": "Licensable", "range": "Company",
"required_properties": ["validFrom"],
"property_types": {"validFrom": "date"},
"inverse_name": "OPERATOR_OF",
"cardinality": {"min": 0, "max": 1},
"required": true, "transitive": false, "symmetric": false,
"ancestry": true,
"enforcement": "warn",
"exempt": {"required_properties": ["Licence"]},
"description": "op"
}
}}"#,
)
.unwrap();
assert!(store.classes["Licensable"].is_abstract);
assert_eq!(store.classes["Licence"].is_a.as_deref(), Some("Licensable"));
assert_eq!(store.ancestors("Licence"), vec!["Licensable"]);
let rel = &store.relationships["HAS_OPERATOR"];
assert_eq!(rel.enforcement, Enforcement::Warn);
assert!(rel.ancestry && !rel.transitive);
assert_eq!(rel.cardinality.unwrap().max, Some(1));
assert_eq!(rel.exempt_classes("required_properties"), ["Licence"]);
assert!(rel.exempt_classes("property_types").is_empty());
assert_eq!(
rel.exempt_summary().as_deref(),
Some("required_properties: [Licence]")
);
let json = serde_json::to_string(&store).unwrap();
let back: OntologyStore = serde_json::from_str(&json).unwrap();
assert_eq!(back, store);
}
#[test]
fn rejects_unknown_keys_with_suggestion() {
let err = parse(r#"{"clases": {}}"#).unwrap_err();
assert!(err.contains("classes"), "{err}");
let err = parse(r#"{"classes": {"A": {"isa": "B"}}}"#).unwrap_err();
assert!(err.contains("is_a"), "{err}");
let err = parse(r#"{"relationships": {"R": {"enforcement": "fatal"}}}"#).unwrap_err();
assert!(err.contains("advisory"), "{err}");
}
#[test]
fn rejects_forest_violations() {
let err = parse(r#"{"classes": {"A": {"is_a": "Missing"}}}"#).unwrap_err();
assert!(err.contains("not a declared class"), "{err}");
let err = parse(r#"{"classes": {"A": {"is_a": "B"}, "B": {"is_a": "A"}}}"#).unwrap_err();
assert!(err.contains("cycle"), "{err}");
let err = parse(r#"{"classes": {"A": {"is_a": "A"}}}"#).unwrap_err();
assert!(err.contains("itself"), "{err}");
}
#[test]
fn enforces_the_class_cap() {
let classes: Vec<String> = (0..=MAX_ONTOLOGY_CLASSES)
.map(|i| format!("\"C{i}\": {{}}"))
.collect();
let doc = format!("{{\"classes\": {{{}}}}}", classes.join(","));
let err = parse(&doc).unwrap_err();
assert!(err.contains("schema-level vocabularies"), "{err}");
assert!(err.contains("`ancestry: true`"), "{err}");
assert!(err.contains("stored"), "{err}");
}
#[test]
fn transitive_and_ancestry_are_mutually_exclusive() {
let err = parse(r#"{"relationships": {"P279": {"transitive": true, "ancestry": true}}}"#)
.unwrap_err();
assert!(err.contains("mutually exclusive"), "{err}");
assert!(err.contains("stored closure"), "{err}");
assert!(err.contains("*1.."), "{err}");
for doc in [
r#"{"relationships": {"P279": {"transitive": true}}}"#,
r#"{"relationships": {"P279": {"ancestry": true}}}"#,
] {
parse(doc).unwrap();
}
}
#[test]
fn ancestry_is_not_a_check_name() {
let err = parse(r#"{"relationships": {"R": {"enforcement": {"ancestry": "error"}}}}"#)
.unwrap_err();
assert!(err.contains("is not a check"), "{err}");
let err = parse(
r#"{"classes": {"A": {}},
"relationships": {"R": {"exempt": {"ancestry": ["A"]}}}}"#,
)
.unwrap_err();
assert!(err.contains("not a check name"), "{err}");
assert!(!CHECK_NAMES.contains(&"ancestry"));
assert!(!EXEMPTABLE_CHECKS.contains(&"ancestry"));
}
#[test]
fn cardinality_min_over_max_rejected() {
let err = parse(r#"{"relationships": {"R": {"cardinality": {"min": 2, "max": 1}}}}"#)
.unwrap_err();
assert!(err.contains("min 2 > max 1"), "{err}");
}
#[test]
fn exempt_refuses_the_flat_form_and_unexemptable_checks() {
let flat = parse(
r#"{"classes": {"A": {}},
"relationships": {"R": {"exempt": ["A"]}}}"#,
)
.unwrap_err();
assert!(flat.contains("{check: [class, ...]}"), "{flat}");
for (check, marker) in [
("domain", "already tests the domain-side class"),
("cardinality", "already tests the domain-side class"),
("transitive", "no single domain-side class"),
] {
let err = parse(&format!(
r#"{{"classes": {{"A": {{}}}},
"relationships": {{"R": {{"exempt": {{"{check}": ["A"]}}}}}}}}"#
))
.unwrap_err();
assert!(err.contains(marker), "{check}: {err}");
}
let typo = parse(
r#"{"classes": {"A": {}},
"relationships": {"R": {"exempt": {"required_propertys": ["A"]}}}}"#,
)
.unwrap_err();
assert!(
typo.contains("Did you mean 'required_properties'?"),
"{typo}"
);
}
#[test]
fn exempt_class_must_be_declared() {
let err = parse(
r#"{"classes": {"A": {}},
"relationships": {"R": {"exempt": {"property_types": ["Ghost"]}}}}"#,
)
.unwrap_err();
assert!(err.contains("not a declared class"), "{err}");
}
#[test]
fn empty_store_serializes_to_nothing_extra() {
let store = OntologyStore::default();
assert!(store.is_empty());
assert_eq!(serde_json::to_string(&store).unwrap(), r#"{"version":0}"#);
}
}