use super::types::{CapabilityValue, PropertyValueKind};
use crate::compat::{format, String, Vec};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RecordField {
pub name: &'static str,
pub kind: PropertyValueKind,
pub required: bool,
}
impl RecordField {
pub const fn required(name: &'static str, kind: PropertyValueKind) -> Self {
Self { name, kind, required: true }
}
pub const fn optional(name: &'static str, kind: PropertyValueKind) -> Self {
Self { name, kind, required: false }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RecordSchema {
pub fields: &'static [RecordField],
pub id_field: &'static str,
pub ids_unique: bool,
pub ids_required: bool,
}
impl RecordSchema {
pub fn field(&self, name: &str) -> Option<&'static RecordField> {
self.fields.iter().find(|field| field.name == name)
}
pub fn field_index(&self, name: &str) -> Option<usize> {
self.fields.iter().position(|field| field.name == name)
}
pub fn names(&self) -> Vec<&'static str> {
self.fields.iter().map(|field| field.name).collect()
}
pub fn id_field_schema(&self) -> Option<&'static RecordField> {
self.field(self.id_field)
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct RecordValue {
fields: Vec<(String, CapabilityValue)>,
}
impl RecordValue {
pub fn new() -> Self {
Self { fields: Vec::new() }
}
pub fn with(mut self, name: impl Into<String>, value: CapabilityValue) -> Self {
self.fields.push((name.into(), value));
self
}
pub fn get(&self, name: &str) -> Option<&CapabilityValue> {
self.fields.iter().find(|(key, _)| key == name).map(|(_, value)| value)
}
pub fn fields(&self) -> &[(String, CapabilityValue)] {
&self.fields
}
pub fn names(&self) -> Vec<&str> {
self.fields.iter().map(|(key, _)| key.as_str()).collect()
}
pub fn without(mut self, name: &str) -> Self {
self.remove(name);
self
}
pub fn len(&self) -> usize {
self.fields.len()
}
pub fn is_empty(&self) -> bool {
self.fields.is_empty()
}
pub fn text(&self, name: &str) -> &str {
match self.get(name) {
Some(CapabilityValue::String(text)) => text.as_str(),
_ => "",
}
}
fn remove(&mut self, name: &str) -> Option<CapabilityValue> {
let index = self.fields.iter().position(|(key, _)| key == name)?;
Some(self.fields.remove(index).1)
}
fn insert_at(&mut self, position: usize, name: String, value: CapabilityValue) {
let position = position.min(self.fields.len());
self.fields.insert(position, (name, value));
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct RecordList {
schema: &'static RecordSchema,
items: Vec<RecordValue>,
}
impl RecordList {
pub const fn new(schema: &'static RecordSchema) -> Self {
Self { schema, items: Vec::new() }
}
pub fn from_items(schema: &'static RecordSchema, items: Vec<RecordValue>) -> Self {
Self { schema, items }
}
pub const fn schema(&self) -> &'static RecordSchema {
self.schema
}
pub fn items(&self) -> &[RecordValue] {
&self.items
}
pub fn len(&self) -> usize {
self.items.len()
}
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
pub fn get(&self, index: usize) -> Option<&RecordValue> {
self.items.get(index)
}
pub fn id_of(&self, index: usize) -> Option<&str> {
let record = self.items.get(index)?;
match record.get(self.schema.id_field) {
Some(CapabilityValue::String(text)) if !text.is_empty() => Some(text.as_str()),
_ => None,
}
}
pub fn index_of_id(&self, id: &str) -> Option<usize> {
self.items.iter().position(|record| record.text(self.schema.id_field) == id)
}
pub fn get_by_id(&self, id: &str) -> Option<&RecordValue> {
self.items.iter().find(|record| record.text(self.schema.id_field) == id)
}
pub fn push(&mut self, record: RecordValue) {
self.items.push(record);
}
pub fn move_item(&mut self, from: usize, to: usize) -> Result<(), RecordViolation> {
if from >= self.items.len() {
return Err(RecordViolation::IndexOutOfRange {
operation: RecordOperation::MoveItem,
index: from,
len: self.items.len(),
});
}
if to >= self.items.len() {
return Err(RecordViolation::IndexOutOfRange {
operation: RecordOperation::MoveItem,
index: to,
len: self.items.len(),
});
}
if from == to {
return Ok(());
}
let record = self.items.remove(from);
self.items.insert(to, record);
Ok(())
}
pub fn move_field(&mut self, name: &str, to: usize) -> Result<(), RecordViolation> {
let Some(source) = self.schema.field_index(name) else {
return Err(RecordViolation::UnknownFieldName { field: String::from(name) });
};
if to >= self.schema.fields.len() {
return Err(RecordViolation::IndexOutOfRange {
operation: RecordOperation::MoveField,
index: to,
len: self.schema.fields.len(),
});
}
if source == to {
return Ok(());
}
for record in &mut self.items {
if let Some(value) = record.remove(name) {
record.insert_at(to, String::from(name), value);
}
}
Ok(())
}
pub fn with_schema(self, schema: &'static RecordSchema) -> Self {
Self { schema, items: self.items }
}
pub fn validate_against(&self, schema: &'static RecordSchema) -> Result<(), RecordViolation> {
if schema.id_field_schema().is_none() {
return Err(RecordViolation::SchemaHasNoIdField { field: schema.id_field });
}
for (index, record) in self.items.iter().enumerate() {
for (name, value) in record.fields() {
let Some(field) = schema.field(name) else {
return Err(RecordViolation::UnknownField { index, field: name.clone() });
};
if !value_matches_kind(value, field.kind) {
return Err(RecordViolation::WrongKind {
index,
field: name.clone(),
expected: field.kind,
});
}
}
for field in schema.fields {
if field.required && record.get(field.name).is_none() {
return Err(RecordViolation::MissingField { index, field: field.name });
}
}
if let Some(repeated) = repeated_field(record) {
return Err(RecordViolation::RepeatedField { index, field: repeated });
}
}
self.check_ids(schema)
}
pub fn validate(&self) -> Result<(), RecordViolation> {
self.validate_against(self.schema)
}
fn check_ids(&self, schema: &'static RecordSchema) -> Result<(), RecordViolation> {
for (index, record) in self.items.iter().enumerate() {
let id = record.text(schema.id_field);
if schema.ids_required && id.is_empty() {
return Err(RecordViolation::EmptyId { index });
}
if !schema.ids_unique || id.is_empty() {
continue;
}
if let Some(first) =
self.items[..index].iter().position(|earlier| earlier.text(schema.id_field) == id)
{
return Err(RecordViolation::DuplicateId {
id: String::from(id),
first,
second: index,
});
}
}
Ok(())
}
pub fn without_id(&self, id: &str) -> Option<Self> {
let index = self.index_of_id(id)?;
let mut items = self.items.clone();
items.remove(index);
Some(Self { schema: self.schema, items })
}
}
impl Default for RecordList {
fn default() -> Self {
Self::new(&EMPTY_RECORD_SCHEMA)
}
}
pub const EMPTY_RECORD_SCHEMA: RecordSchema =
RecordSchema { fields: &[], id_field: "", ids_unique: false, ids_required: false };
fn repeated_field(record: &RecordValue) -> Option<String> {
let fields = record.fields();
for (position, (name, _)) in fields.iter().enumerate() {
if fields[position + 1..].iter().any(|(other, _)| other == name) {
return Some(name.clone());
}
}
None
}
fn value_matches_kind(value: &CapabilityValue, kind: PropertyValueKind) -> bool {
match kind {
PropertyValueKind::Bool => matches!(value, CapabilityValue::Bool(_)),
PropertyValueKind::Int => matches!(value, CapabilityValue::Int(_)),
PropertyValueKind::UInt => matches!(value, CapabilityValue::UInt(_)),
PropertyValueKind::Float => matches!(value, CapabilityValue::Float(_)),
PropertyValueKind::Number => {
matches!(
value,
CapabilityValue::Int(_) | CapabilityValue::UInt(_) | CapabilityValue::Float(_)
)
}
PropertyValueKind::String | PropertyValueKind::Enum => {
matches!(value, CapabilityValue::String(_))
}
PropertyValueKind::Color => matches!(value, CapabilityValue::Color(_)),
PropertyValueKind::Rect => matches!(value, CapabilityValue::Rect(_)),
PropertyValueKind::IconSpec => matches!(value, CapabilityValue::IconSpec(_)),
PropertyValueKind::Records => false,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RecordOperation {
MoveItem,
MoveField,
}
impl RecordOperation {
pub const fn token(self) -> &'static str {
match self {
Self::MoveItem => "move item",
Self::MoveField => "move field",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RecordViolation {
SchemaHasNoIdField {
field: &'static str,
},
MissingField {
index: usize,
field: &'static str,
},
UnknownField {
index: usize,
field: String,
},
RepeatedField {
index: usize,
field: String,
},
WrongKind {
index: usize,
field: String,
expected: PropertyValueKind,
},
EmptyId {
index: usize,
},
DuplicateId {
id: String,
first: usize,
second: usize,
},
IndexOutOfRange {
operation: RecordOperation,
index: usize,
len: usize,
},
UnknownFieldName {
field: String,
},
}
impl RecordViolation {
pub const fn index(&self) -> Option<usize> {
match self {
Self::MissingField { index, .. }
| Self::UnknownField { index, .. }
| Self::RepeatedField { index, .. }
| Self::WrongKind { index, .. }
| Self::EmptyId { index } => Some(*index),
Self::DuplicateId { second, .. } => Some(*second),
Self::IndexOutOfRange { index, .. } => Some(*index),
Self::SchemaHasNoIdField { .. } | Self::UnknownFieldName { .. } => None,
}
}
}
impl core::fmt::Display for RecordViolation {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::SchemaHasNoIdField { field } => write!(
f,
"the record schema names `{field}` as its identity field but does not declare it"
),
Self::MissingField { index, field } => {
write!(f, "record {index} is missing the required field `{field}`")
}
Self::UnknownField { index, field } => {
write!(f, "record {index} carries the undeclared field `{field}`")
}
Self::RepeatedField { index, field } => {
write!(f, "record {index} carries the field `{field}` more than once")
}
Self::WrongKind { index, field, expected } => write!(
f,
"record {index} field `{field}` is not the declared kind `{}`",
super::designer_manifest::value_kind_token(*expected)
),
Self::EmptyId { index } => {
write!(f, "record {index} has an empty id, which the schema requires")
}
Self::DuplicateId { id, first, second } => write!(
f,
"records {first} and {second} claim the same id `{id}`, which the schema requires to \
be unique"
),
Self::IndexOutOfRange { operation, index, len } => {
write!(f, "cannot {} to {index}: the list has {len} positions", operation.token())
}
Self::UnknownFieldName { field } => {
write!(f, "the schema does not declare a field named `{field}`")
}
}
}
}
pub fn as_record_list(value: &CapabilityValue) -> Option<&RecordList> {
match value {
CapabilityValue::Records(list) => Some(list),
_ => None,
}
}
pub fn describe_records(list: &RecordList) -> String {
format!("{} record(s) of schema {}", list.len(), list.schema().names().join(","))
}
#[cfg(test)]
mod tests {
use super::*;
const CHIP_ITEM: RecordSchema = RecordSchema {
fields: &[
RecordField::required("id", PropertyValueKind::String),
RecordField::required("label", PropertyValueKind::String),
RecordField::optional("selected", PropertyValueKind::Bool),
],
id_field: "id",
ids_unique: true,
ids_required: true,
};
fn text(value: &str) -> CapabilityValue {
CapabilityValue::String(String::from(value))
}
fn item(id: &str, label: &str, selected: bool) -> RecordValue {
RecordValue::new()
.with("id", text(id))
.with("label", text(label))
.with("selected", CapabilityValue::Bool(selected))
}
fn ids(list: &RecordList) -> Vec<String> {
(0..list.len())
.map(|index| String::from(list.id_of(index).expect("every item has an id")))
.collect()
}
fn list() -> RecordList {
RecordList::from_items(
&CHIP_ITEM,
vec![item("a", "Alpha", false), item("b", "Beta", true), item("c", "Gamma", false)],
)
}
#[test]
fn a_conforming_list_validates() {
let list = list();
assert_eq!(list.validate(), Ok(()));
assert_eq!(list.len(), 3);
assert_eq!(list.schema().names(), vec!["id", "label", "selected"]);
assert_eq!(list.id_of(1), Some("b"));
assert_eq!(list.index_of_id("c"), Some(2));
assert_eq!(list.id_of(3), None);
}
#[test]
fn a_move_reorders_without_changing_identity() {
let mut list = list();
let before = ids(&list);
list.move_item(0, 2).expect("both indices are in range");
assert_eq!(list.id_of(0), Some("b"));
assert_eq!(list.id_of(1), Some("c"));
assert_eq!(list.id_of(2), Some("a"));
assert_eq!(list.get_by_id("a").expect("still present").text("label"), "Alpha");
assert!(matches!(
list.get_by_id("b").expect("still present").get("selected"),
Some(CapabilityValue::Bool(true))
));
let mut after = ids(&list);
let mut sorted_before = before;
after.sort();
sorted_before.sort();
assert_eq!(after, sorted_before);
}
#[test]
fn a_move_that_cannot_happen_leaves_the_list_alone() {
let mut list = list();
let unchanged = list.clone();
assert_eq!(list.move_item(1, 1), Ok(()));
assert_eq!(list, unchanged);
assert_eq!(
list.move_item(3, 0),
Err(RecordViolation::IndexOutOfRange {
operation: RecordOperation::MoveItem,
index: 3,
len: 3
})
);
assert_eq!(list, unchanged);
assert_eq!(
list.move_item(0, 9),
Err(RecordViolation::IndexOutOfRange {
operation: RecordOperation::MoveItem,
index: 9,
len: 3
})
);
assert_eq!(list, unchanged);
}
#[test]
fn a_column_move_reorders_every_record_and_keeps_the_ids_stable() {
let mut list = list();
let before = ids(&list);
list.move_field("label", 0).expect("label is declared and 0 is in range");
for record in list.items() {
assert_eq!(record.names(), vec!["label", "id", "selected"]);
}
assert_eq!(list.get(0).expect("first record").text("label"), "Alpha");
assert_eq!(list.get(0).expect("first record").text("id"), "a");
assert_eq!(ids(&list), before);
assert_eq!(list.validate(), Ok(()));
}
#[test]
fn a_column_move_skips_a_record_that_does_not_carry_the_field() {
let mut list = RecordList::from_items(
&CHIP_ITEM,
vec![item("a", "Alpha", true), item("b", "Beta", false).without("selected")],
);
list.move_field("id", 2).expect("id is declared and 2 is in range");
assert_eq!(list.get(0).expect("first").names(), vec!["label", "selected", "id"]);
assert_eq!(list.get(1).expect("second").names(), vec!["label", "id"]);
assert_eq!(ids(&list), vec![String::from("a"), String::from("b")]);
assert_eq!(list.validate(), Ok(()));
}
#[test]
fn a_column_move_that_cannot_happen_leaves_the_list_alone() {
let mut list = list();
let unchanged = list.clone();
assert_eq!(
list.move_field("roles", 0),
Err(RecordViolation::UnknownFieldName { field: String::from("roles") })
);
assert_eq!(list, unchanged);
assert_eq!(
list.move_field("label", 3),
Err(RecordViolation::IndexOutOfRange {
operation: RecordOperation::MoveField,
index: 3,
len: 3
})
);
assert_eq!(list, unchanged);
}
#[test]
fn every_schema_rule_is_reported_with_its_record_and_field() {
let missing =
RecordList::from_items(&CHIP_ITEM, vec![item("a", "A", false).without("label")]);
assert_eq!(
missing.validate(),
Err(RecordViolation::MissingField { index: 0, field: "label" })
);
let unknown =
RecordList::from_items(&CHIP_ITEM, vec![item("a", "A", false).with("rol", text("x"))]);
assert_eq!(
unknown.validate(),
Err(RecordViolation::UnknownField { index: 0, field: String::from("rol") })
);
let wrong_kind = RecordList::from_items(
&CHIP_ITEM,
vec![item("a", "A", false).with("selected", CapabilityValue::UInt(1))],
);
assert_eq!(
wrong_kind.validate(),
Err(RecordViolation::WrongKind {
index: 0,
field: String::from("selected"),
expected: PropertyValueKind::Bool
})
);
let empty_id = RecordList::from_items(
&CHIP_ITEM,
vec![RecordValue::new().with("id", text("")).with("label", text("A"))],
);
assert_eq!(empty_id.validate(), Err(RecordViolation::EmptyId { index: 0 }));
let duplicated =
RecordList::from_items(&CHIP_ITEM, vec![item("a", "A", false), item("a", "B", false)]);
assert_eq!(
duplicated.validate(),
Err(RecordViolation::DuplicateId { id: String::from("a"), first: 0, second: 1 })
);
let repeated = RecordList::from_items(
&CHIP_ITEM,
vec![item("a", "A", false).with("label", text("B"))],
);
assert_eq!(
repeated.validate(),
Err(RecordViolation::RepeatedField { index: 0, field: String::from("label") })
);
}
#[test]
fn a_schema_without_its_own_identity_field_is_refused() {
const BROKEN: RecordSchema = RecordSchema {
fields: &[RecordField::required("label", PropertyValueKind::String)],
id_field: "id",
ids_unique: true,
ids_required: true,
};
let list =
RecordList::from_items(&BROKEN, vec![RecordValue::new().with("label", text("A"))]);
assert_eq!(list.validate(), Err(RecordViolation::SchemaHasNoIdField { field: "id" }));
}
#[test]
fn a_repeated_id_is_accepted_when_the_schema_allows_it() {
const ALLOWING: RecordSchema = RecordSchema {
fields: &[RecordField::required("id", PropertyValueKind::String)],
id_field: "id",
ids_unique: false,
ids_required: true,
};
let list = RecordList::from_items(
&ALLOWING,
vec![
RecordValue::new().with("id", text("a")),
RecordValue::new().with("id", text("a")),
],
);
assert_eq!(list.validate(), Ok(()));
assert_eq!(list.index_of_id("a"), Some(0));
}
#[test]
fn an_optional_field_may_be_absent() {
let list =
RecordList::from_items(&CHIP_ITEM, vec![item("a", "A", false).without("selected")]);
assert_eq!(list.validate(), Ok(()));
assert!(list.get(0).expect("record").get("selected").is_none());
assert_eq!(list.get(0).expect("record").text("selected"), "");
assert!(!list.is_empty());
assert!(RecordList::new(&CHIP_ITEM).is_empty());
assert_eq!(list.get(7), None);
}
#[test]
fn removal_is_by_identity() {
let list = list();
let without_b = list.without_id("b").expect("b is present");
assert_eq!(without_b.len(), 2);
assert_eq!(without_b.index_of_id("b"), None);
assert_eq!(list.len(), 3);
assert_eq!(list.without_id("zzz"), None);
}
#[test]
fn violations_describe_themselves() {
for violation in [
RecordViolation::SchemaHasNoIdField { field: "id" },
RecordViolation::MissingField { index: 2, field: "label" },
RecordViolation::UnknownField { index: 1, field: String::from("rol") },
RecordViolation::RepeatedField { index: 0, field: String::from("id") },
RecordViolation::WrongKind {
index: 3,
field: String::from("selected"),
expected: PropertyValueKind::Bool,
},
RecordViolation::EmptyId { index: 0 },
RecordViolation::DuplicateId { id: String::from("a"), first: 0, second: 4 },
RecordViolation::IndexOutOfRange {
operation: RecordOperation::MoveItem,
index: 9,
len: 3,
},
RecordViolation::UnknownFieldName { field: String::from("roles") },
] {
let rendered = format!("{violation}");
assert!(!rendered.is_empty(), "{violation:?} has no message");
}
assert_eq!(RecordViolation::MissingField { index: 2, field: "label" }.index(), Some(2));
assert_eq!(
RecordViolation::DuplicateId { id: String::from("a"), first: 0, second: 4 }.index(),
Some(4),
"a duplicate id points at the record that repeated it"
);
assert_eq!(
RecordViolation::SchemaHasNoIdField { field: "id" }.index(),
None,
"a schema defect is not a complaint about one row"
);
}
#[test]
fn the_accessor_reads_only_a_record_list() {
let value = CapabilityValue::Records(list());
assert_eq!(as_record_list(&value).expect("a record list").len(), 3);
assert_eq!(as_record_list(&CapabilityValue::Null), None);
assert_eq!(
describe_records(as_record_list(&value).expect("list")),
"3 record(s) of schema id,label,selected"
);
}
#[test]
fn a_default_list_claims_no_schema() {
let list = RecordList::default();
assert!(list.is_empty());
assert_eq!(list.validate(), Err(RecordViolation::SchemaHasNoIdField { field: "" }));
}
#[test]
fn a_nested_record_list_is_not_a_valid_field_value() {
let value = CapabilityValue::Records(list());
assert!(!value_matches_kind(&value, PropertyValueKind::Records));
assert!(value_matches_kind(&CapabilityValue::Bool(true), PropertyValueKind::Bool));
}
}