use crate::{
db::{
relation::{
RelationFieldCardinality, RelationFieldMetadata, relation_field_metadata_for_model_iter,
},
schema::{
AcceptedSchemaSnapshot, PersistedFieldKind, PersistedIndexKeyItemSnapshot,
PersistedIndexKeySnapshot, PersistedNestedLeafSnapshot, PersistedRelationStrength,
SchemaFieldDefault, SchemaFieldSlot, field_type_from_persisted_kind,
},
},
model::{
entity::EntityModel,
field::{FieldDatabaseDefault, FieldKind, FieldModel, RelationStrength},
},
};
use candid::CandidType;
use serde::Deserialize;
use sha2::{Digest, Sha256};
use std::fmt::Write;
const ENTITY_FIELD_DESCRIPTION_NO_SLOT: u16 = u16::MAX;
#[cfg_attr(
doc,
doc = "EntitySchemaDescription\n\nStable describe payload for one entity model."
)]
#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
pub struct EntitySchemaDescription {
pub(crate) entity_path: String,
pub(crate) entity_name: String,
pub(crate) primary_key: String,
pub(crate) primary_key_fields: Vec<String>,
pub(crate) fields: Vec<EntityFieldDescription>,
pub(crate) indexes: Vec<EntityIndexDescription>,
pub(crate) relations: Vec<EntityRelationDescription>,
}
#[cfg_attr(
doc,
doc = "EntitySchemaCheckDescription\n\nGenerated-vs-accepted schema description payload for one entity."
)]
#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
pub struct EntitySchemaCheckDescription {
pub(crate) generated: EntitySchemaDescription,
pub(crate) accepted: EntitySchemaDescription,
}
impl EntitySchemaCheckDescription {
#[must_use]
pub const fn new(
generated: EntitySchemaDescription,
accepted: EntitySchemaDescription,
) -> Self {
Self {
generated,
accepted,
}
}
#[must_use]
pub const fn generated(&self) -> &EntitySchemaDescription {
&self.generated
}
#[must_use]
pub const fn accepted(&self) -> &EntitySchemaDescription {
&self.accepted
}
}
impl EntitySchemaDescription {
#[must_use]
pub fn new(
entity_path: String,
entity_name: String,
primary_key: String,
fields: Vec<EntityFieldDescription>,
indexes: Vec<EntityIndexDescription>,
relations: Vec<EntityRelationDescription>,
) -> Self {
Self::new_with_primary_key_fields(
entity_path,
entity_name,
primary_key.clone(),
vec![primary_key],
fields,
indexes,
relations,
)
}
#[must_use]
pub const fn new_with_primary_key_fields(
entity_path: String,
entity_name: String,
primary_key: String,
primary_key_fields: Vec<String>,
fields: Vec<EntityFieldDescription>,
indexes: Vec<EntityIndexDescription>,
relations: Vec<EntityRelationDescription>,
) -> Self {
Self {
entity_path,
entity_name,
primary_key,
primary_key_fields,
fields,
indexes,
relations,
}
}
#[must_use]
pub const fn entity_path(&self) -> &str {
self.entity_path.as_str()
}
#[must_use]
pub const fn entity_name(&self) -> &str {
self.entity_name.as_str()
}
#[must_use]
pub const fn primary_key(&self) -> &str {
self.primary_key.as_str()
}
#[must_use]
pub const fn primary_key_fields(&self) -> &[String] {
self.primary_key_fields.as_slice()
}
#[must_use]
pub const fn fields(&self) -> &[EntityFieldDescription] {
self.fields.as_slice()
}
#[must_use]
pub const fn indexes(&self) -> &[EntityIndexDescription] {
self.indexes.as_slice()
}
#[must_use]
pub const fn relations(&self) -> &[EntityRelationDescription] {
self.relations.as_slice()
}
}
#[cfg_attr(
doc,
doc = "EntityFieldDescription\n\nOne field entry in a describe payload."
)]
#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
pub struct EntityFieldDescription {
pub(crate) name: String,
pub(crate) slot: u16,
pub(crate) kind: String,
pub(crate) nullable: bool,
pub(crate) primary_key: bool,
pub(crate) queryable: bool,
pub(crate) origin: String,
}
impl EntityFieldDescription {
#[must_use]
pub const fn new(
name: String,
slot: Option<u16>,
kind: String,
nullable: bool,
primary_key: bool,
queryable: bool,
origin: String,
) -> Self {
let slot = match slot {
Some(slot) => slot,
None => ENTITY_FIELD_DESCRIPTION_NO_SLOT,
};
Self {
name,
slot,
kind,
nullable,
primary_key,
queryable,
origin,
}
}
#[must_use]
pub const fn name(&self) -> &str {
self.name.as_str()
}
#[must_use]
pub const fn slot(&self) -> Option<u16> {
if self.slot == ENTITY_FIELD_DESCRIPTION_NO_SLOT {
None
} else {
Some(self.slot)
}
}
#[must_use]
pub const fn kind(&self) -> &str {
self.kind.as_str()
}
#[must_use]
pub const fn nullable(&self) -> bool {
self.nullable
}
#[must_use]
pub const fn primary_key(&self) -> bool {
self.primary_key
}
#[must_use]
pub const fn queryable(&self) -> bool {
self.queryable
}
#[must_use]
pub const fn origin(&self) -> &str {
self.origin.as_str()
}
}
#[cfg_attr(
doc,
doc = "EntityIndexDescription\n\nOne index entry in a describe payload."
)]
#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
pub struct EntityIndexDescription {
pub(crate) name: String,
pub(crate) unique: bool,
pub(crate) fields: Vec<String>,
pub(crate) origin: String,
}
impl EntityIndexDescription {
#[must_use]
pub const fn new(name: String, unique: bool, fields: Vec<String>, origin: String) -> Self {
Self {
name,
unique,
fields,
origin,
}
}
#[must_use]
pub const fn name(&self) -> &str {
self.name.as_str()
}
#[must_use]
pub const fn unique(&self) -> bool {
self.unique
}
#[must_use]
pub const fn fields(&self) -> &[String] {
self.fields.as_slice()
}
#[must_use]
pub const fn origin(&self) -> &str {
self.origin.as_str()
}
}
#[cfg_attr(
doc,
doc = "EntityRelationDescription\n\nOne relation entry in a describe payload."
)]
#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
pub struct EntityRelationDescription {
pub(crate) field: String,
pub(crate) target_path: String,
pub(crate) target_entity_name: String,
pub(crate) target_store_path: String,
pub(crate) strength: EntityRelationStrength,
pub(crate) cardinality: EntityRelationCardinality,
}
impl EntityRelationDescription {
#[must_use]
pub const fn new(
field: String,
target_path: String,
target_entity_name: String,
target_store_path: String,
strength: EntityRelationStrength,
cardinality: EntityRelationCardinality,
) -> Self {
Self {
field,
target_path,
target_entity_name,
target_store_path,
strength,
cardinality,
}
}
#[must_use]
pub const fn field(&self) -> &str {
self.field.as_str()
}
#[must_use]
pub const fn target_path(&self) -> &str {
self.target_path.as_str()
}
#[must_use]
pub const fn target_entity_name(&self) -> &str {
self.target_entity_name.as_str()
}
#[must_use]
pub const fn target_store_path(&self) -> &str {
self.target_store_path.as_str()
}
#[must_use]
pub const fn strength(&self) -> EntityRelationStrength {
self.strength
}
#[must_use]
pub const fn cardinality(&self) -> EntityRelationCardinality {
self.cardinality
}
}
#[cfg_attr(doc, doc = "EntityRelationStrength\n\nDescribe relation strength.")]
#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
pub enum EntityRelationStrength {
Strong,
Weak,
}
#[cfg_attr(
doc,
doc = "EntityRelationCardinality\n\nDescribe relation cardinality."
)]
#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
pub enum EntityRelationCardinality {
Single,
List,
Set,
}
#[cfg_attr(
doc,
doc = "Build one stable entity-schema description from one runtime `EntityModel`."
)]
#[must_use]
pub(in crate::db) fn describe_entity_model(model: &EntityModel) -> EntitySchemaDescription {
let fields = describe_entity_fields(model);
let primary_key_fields = primary_key_field_names_from_model(model);
let primary_key = render_primary_key_fields(primary_key_fields.as_slice());
describe_entity_model_from_description_rows(
model.path,
model.entity_name,
primary_key.as_str(),
primary_key_fields,
fields,
describe_entity_indexes_from_model(model),
describe_entity_relations_from_model(model),
)
}
#[cfg_attr(
doc,
doc = "Build one entity-schema description using accepted persisted schema slot metadata."
)]
#[must_use]
pub(in crate::db) fn describe_entity_model_with_persisted_schema(
model: &EntityModel,
schema: &AcceptedSchemaSnapshot,
) -> EntitySchemaDescription {
let fields = describe_entity_fields_with_persisted_schema(schema);
let primary_key_fields = schema.primary_key_field_names();
let primary_key_fields = if primary_key_fields.is_empty() {
vec![model.primary_key.name.to_string()]
} else {
primary_key_fields
.into_iter()
.map(str::to_string)
.collect::<Vec<_>>()
};
let primary_key = render_primary_key_fields(primary_key_fields.as_slice());
describe_entity_model_from_description_rows(
schema.entity_path(),
schema.entity_name(),
primary_key.as_str(),
primary_key_fields,
fields,
describe_entity_indexes_with_persisted_schema(schema),
describe_entity_relations_with_persisted_schema(schema),
)
}
fn describe_entity_model_from_description_rows(
entity_path: &str,
entity_name: &str,
primary_key: &str,
primary_key_fields: Vec<String>,
fields: Vec<EntityFieldDescription>,
indexes: Vec<EntityIndexDescription>,
relations: Vec<EntityRelationDescription>,
) -> EntitySchemaDescription {
EntitySchemaDescription::new_with_primary_key_fields(
entity_path.to_string(),
entity_name.to_string(),
primary_key.to_string(),
primary_key_fields,
fields,
indexes,
relations,
)
}
fn describe_entity_relations_from_model(model: &EntityModel) -> Vec<EntityRelationDescription> {
relation_field_metadata_for_model_iter(model)
.map(relation_description_from_metadata)
.collect()
}
fn primary_key_field_names_from_model(model: &EntityModel) -> Vec<String> {
model
.primary_key_model()
.fields()
.iter()
.map(|field| field.name.to_string())
.collect()
}
fn render_primary_key_fields(fields: &[String]) -> String {
fields.join(", ")
}
fn describe_entity_indexes_from_model(model: &EntityModel) -> Vec<EntityIndexDescription> {
let mut indexes = Vec::with_capacity(model.indexes.len());
for index in model.indexes {
indexes.push(EntityIndexDescription::new(
index.name().to_string(),
index.is_unique(),
index
.fields()
.iter()
.map(|field| (*field).to_string())
.collect(),
"generated".to_string(),
));
}
indexes
}
fn describe_entity_indexes_with_persisted_schema(
schema: &AcceptedSchemaSnapshot,
) -> Vec<EntityIndexDescription> {
schema
.persisted_snapshot()
.indexes()
.iter()
.map(|index| {
EntityIndexDescription::new(
index.name().to_string(),
index.unique(),
describe_persisted_index_fields(index.key()),
if index.generated() {
"generated".to_string()
} else {
"ddl".to_string()
},
)
})
.collect()
}
fn describe_persisted_index_fields(key: &PersistedIndexKeySnapshot) -> Vec<String> {
match key {
PersistedIndexKeySnapshot::FieldPath(paths) => paths
.iter()
.map(|field_path| field_path.path().join("."))
.collect(),
PersistedIndexKeySnapshot::Items(items) => items
.iter()
.map(|item| match item {
PersistedIndexKeyItemSnapshot::FieldPath(field_path) => field_path.path().join("."),
PersistedIndexKeyItemSnapshot::Expression(expression) => {
expression.canonical_text().to_string()
}
})
.collect(),
}
}
#[must_use]
pub(in crate::db) fn describe_entity_fields(model: &EntityModel) -> Vec<EntityFieldDescription> {
describe_entity_fields_with_slot_lookup(model, |slot, _field| {
Some(u16::try_from(slot).expect("generated field slot should fit in u16"))
})
}
#[cfg_attr(
doc,
doc = "Build field descriptors using accepted persisted schema slot metadata."
)]
#[must_use]
pub(in crate::db) fn describe_entity_fields_with_persisted_schema(
schema: &AcceptedSchemaSnapshot,
) -> Vec<EntityFieldDescription> {
let snapshot = schema.persisted_snapshot();
let mut fields = Vec::with_capacity(snapshot.fields().len());
for field in snapshot.fields() {
let primary_key = snapshot.primary_key_field_ids().contains(&field.id());
let slot = snapshot
.row_layout()
.slot_for_field(field.id())
.map(SchemaFieldSlot::get);
let mut kind = summarize_persisted_field_kind(field.kind());
write_schema_default_summary(&mut kind, field.default());
let metadata = DescribeFieldMetadata::new(
kind,
field.nullable(),
field_type_from_persisted_kind(field.kind())
.value_kind()
.is_queryable(),
field_origin_label(field.generated()),
);
push_described_field_row(&mut fields, field.name(), slot, primary_key, None, metadata);
if !field.nested_leaves().is_empty() {
describe_persisted_nested_leaves(
&mut fields,
field.nested_leaves(),
field_origin_label(field.generated()),
);
}
}
fields
}
fn describe_entity_fields_with_slot_lookup(
model: &EntityModel,
mut slot_for_field: impl FnMut(usize, &FieldModel) -> Option<u16>,
) -> Vec<EntityFieldDescription> {
let mut fields = Vec::with_capacity(model.fields.len());
let primary_key_fields = primary_key_field_names_from_model(model);
for (slot, field) in model.fields.iter().enumerate() {
let primary_key = primary_key_fields
.iter()
.any(|primary_key_field| primary_key_field == field.name);
describe_field_recursive(
&mut fields,
field.name,
slot_for_field(slot, field),
field,
primary_key,
None,
None,
);
}
fields
}
struct DescribeFieldMetadata {
kind: String,
nullable: bool,
queryable: bool,
origin: String,
}
impl DescribeFieldMetadata {
const fn new(kind: String, nullable: bool, queryable: bool, origin: String) -> Self {
Self {
kind,
nullable,
queryable,
origin,
}
}
}
fn describe_field_recursive(
fields: &mut Vec<EntityFieldDescription>,
name: &str,
slot: Option<u16>,
field: &FieldModel,
primary_key: bool,
tree_prefix: Option<&'static str>,
metadata_override: Option<DescribeFieldMetadata>,
) {
let metadata = metadata_override.unwrap_or_else(|| {
let mut kind = summarize_field_kind(&field.kind);
write_model_default_summary(&mut kind, field.database_default());
DescribeFieldMetadata::new(
kind,
field.nullable(),
field.kind.value_kind().is_queryable(),
"generated".to_string(),
)
});
push_described_field_row(fields, name, slot, primary_key, tree_prefix, metadata);
describe_generated_nested_fields(fields, field.nested_fields());
}
fn push_described_field_row(
fields: &mut Vec<EntityFieldDescription>,
name: &str,
slot: Option<u16>,
primary_key: bool,
tree_prefix: Option<&'static str>,
metadata: DescribeFieldMetadata,
) {
let display_name = if let Some(prefix) = tree_prefix {
format!("{prefix}{name}")
} else {
name.to_string()
};
fields.push(EntityFieldDescription::new(
display_name,
slot,
metadata.kind,
metadata.nullable,
primary_key,
metadata.queryable,
metadata.origin,
));
}
fn describe_generated_nested_fields(
fields: &mut Vec<EntityFieldDescription>,
nested_fields: &[FieldModel],
) {
for (index, nested) in nested_fields.iter().enumerate() {
let prefix = if index + 1 == nested_fields.len() {
"└─ "
} else {
"├─ "
};
describe_field_recursive(
fields,
nested.name(),
None,
nested,
false,
Some(prefix),
None,
);
}
}
fn describe_persisted_nested_leaves(
fields: &mut Vec<EntityFieldDescription>,
nested_leaves: &[PersistedNestedLeafSnapshot],
origin: String,
) {
for (index, leaf) in nested_leaves.iter().enumerate() {
let prefix = if index + 1 == nested_leaves.len() {
"└─ "
} else {
"├─ "
};
let name = leaf.path().last().map_or("", String::as_str);
let metadata = DescribeFieldMetadata::new(
summarize_persisted_field_kind(leaf.kind()),
leaf.nullable(),
field_type_from_persisted_kind(leaf.kind())
.value_kind()
.is_queryable(),
origin.clone(),
);
push_described_field_row(fields, name, None, false, Some(prefix), metadata);
}
}
fn field_origin_label(generated: bool) -> String {
if generated {
"generated".to_string()
} else {
"ddl".to_string()
}
}
fn describe_entity_relations_with_persisted_schema(
schema: &AcceptedSchemaSnapshot,
) -> Vec<EntityRelationDescription> {
schema
.persisted_snapshot()
.fields()
.iter()
.filter_map(relation_description_from_persisted_field)
.collect()
}
fn relation_description_from_persisted_field(
field: &crate::db::schema::PersistedFieldSnapshot,
) -> Option<EntityRelationDescription> {
let relation = persisted_relation_description_metadata(field.kind())?;
Some(EntityRelationDescription::new(
field.name().to_string(),
relation.target_path.to_string(),
relation.target_entity_name.to_string(),
relation.target_store_path.to_string(),
relation.strength,
relation.cardinality,
))
}
struct PersistedRelationDescriptionMetadata<'a> {
target_path: &'a str,
target_entity_name: &'a str,
target_store_path: &'a str,
strength: EntityRelationStrength,
cardinality: EntityRelationCardinality,
}
fn persisted_relation_description_metadata(
kind: &PersistedFieldKind,
) -> Option<PersistedRelationDescriptionMetadata<'_>> {
const fn from_relation_kind(
kind: &PersistedFieldKind,
cardinality: EntityRelationCardinality,
) -> Option<PersistedRelationDescriptionMetadata<'_>> {
let PersistedFieldKind::Relation {
target_path,
target_entity_name,
target_store_path,
strength,
..
} = kind
else {
return None;
};
Some(PersistedRelationDescriptionMetadata {
target_path: target_path.as_str(),
target_entity_name: target_entity_name.as_str(),
target_store_path: target_store_path.as_str(),
strength: entity_relation_strength_from_persisted(*strength),
cardinality,
})
}
match kind {
PersistedFieldKind::Relation { .. } => {
from_relation_kind(kind, EntityRelationCardinality::Single)
}
PersistedFieldKind::List(inner) => {
from_relation_kind(inner, EntityRelationCardinality::List)
}
PersistedFieldKind::Set(inner) => from_relation_kind(inner, EntityRelationCardinality::Set),
_ => None,
}
}
const fn entity_relation_strength_from_persisted(
strength: PersistedRelationStrength,
) -> EntityRelationStrength {
match strength {
PersistedRelationStrength::Strong => EntityRelationStrength::Strong,
PersistedRelationStrength::Weak => EntityRelationStrength::Weak,
}
}
fn relation_description_from_metadata(
metadata: RelationFieldMetadata,
) -> EntityRelationDescription {
let strength = match metadata.strength() {
RelationStrength::Strong => EntityRelationStrength::Strong,
RelationStrength::Weak => EntityRelationStrength::Weak,
};
let cardinality = match metadata.cardinality() {
RelationFieldCardinality::Single => EntityRelationCardinality::Single,
RelationFieldCardinality::List => EntityRelationCardinality::List,
RelationFieldCardinality::Set => EntityRelationCardinality::Set,
};
EntityRelationDescription::new(
metadata.field_name().to_string(),
metadata.target_path().to_string(),
metadata.target_entity_name().to_string(),
metadata.target_store_path().to_string(),
strength,
cardinality,
)
}
#[cfg_attr(doc, doc = "Render one stable field-kind label for describe output.")]
fn summarize_field_kind(kind: &FieldKind) -> String {
let mut out = String::new();
write_field_kind_summary(&mut out, kind);
out
}
fn write_field_kind_summary(out: &mut String, kind: &FieldKind) {
if let Some(name) = kind.describe_kind_name() {
out.push_str(name);
return;
}
match kind {
FieldKind::Blob { max_len } => {
write_length_bounded_field_kind_summary(out, "blob", *max_len);
}
FieldKind::Decimal { scale } => {
let _ = write!(out, "decimal(scale={scale})");
}
FieldKind::IntBig { max_bytes } => {
write_byte_bounded_field_kind_summary(out, "int_big", *max_bytes);
}
FieldKind::Enum { path, .. } => {
out.push_str("enum(");
out.push_str(path);
out.push(')');
}
FieldKind::Text { max_len } => {
write_length_bounded_field_kind_summary(out, "text", *max_len);
}
FieldKind::Relation {
target_entity_name,
key_kind,
strength,
..
} => {
out.push_str("relation(target=");
out.push_str(target_entity_name);
out.push_str(", key=");
write_field_kind_summary(out, key_kind);
out.push_str(", strength=");
out.push_str(summarize_relation_strength(*strength));
out.push(')');
}
FieldKind::List(inner) => {
out.push_str("list<");
write_field_kind_summary(out, inner);
out.push('>');
}
FieldKind::Set(inner) => {
out.push_str("set<");
write_field_kind_summary(out, inner);
out.push('>');
}
FieldKind::Map { key, value } => {
out.push_str("map<");
write_field_kind_summary(out, key);
out.push_str(", ");
write_field_kind_summary(out, value);
out.push('>');
}
FieldKind::Structured { .. } => {
out.push_str("structured");
}
FieldKind::Account
| FieldKind::Bool
| FieldKind::Date
| FieldKind::Duration
| FieldKind::Float32
| FieldKind::Float64
| FieldKind::Int8
| FieldKind::Int16
| FieldKind::Int32
| FieldKind::Int64
| FieldKind::Int128
| FieldKind::Principal
| FieldKind::Subaccount
| FieldKind::Timestamp
| FieldKind::Nat8
| FieldKind::Nat16
| FieldKind::Nat32
| FieldKind::Nat64
| FieldKind::Nat128
| FieldKind::Ulid
| FieldKind::Unit => unreachable!("schema describe invariant"),
FieldKind::NatBig { max_bytes } => {
write_byte_bounded_field_kind_summary(out, "nat_big", *max_bytes);
}
}
}
trait DescribeKindName {
fn describe_kind_name(&self) -> Option<&'static str>;
}
impl DescribeKindName for FieldKind {
fn describe_kind_name(&self) -> Option<&'static str> {
Some(match self {
Self::Account => "account",
Self::Bool => "bool",
Self::Date => "date",
Self::Duration => "duration",
Self::Float32 => "float32",
Self::Float64 => "float64",
Self::Int8 => "int8",
Self::Int16 => "int16",
Self::Int32 => "int32",
Self::Int64 => "int64",
Self::Int128 => "int128",
Self::Principal => "principal",
Self::Subaccount => "subaccount",
Self::Timestamp => "timestamp",
Self::Nat8 => "nat8",
Self::Nat16 => "nat16",
Self::Nat32 => "nat32",
Self::Nat64 => "nat64",
Self::Nat128 => "nat128",
Self::Ulid => "ulid",
Self::Unit => "unit",
Self::Blob { .. }
| Self::Decimal { .. }
| Self::Enum { .. }
| Self::IntBig { .. }
| Self::NatBig { .. }
| Self::Text { .. }
| Self::Relation { .. }
| Self::List(_)
| Self::Set(_)
| Self::Map { .. }
| Self::Structured { .. } => return None,
})
}
}
fn write_length_bounded_field_kind_summary(
out: &mut String,
kind_name: &str,
max_len: Option<u32>,
) {
out.push_str(kind_name);
if let Some(max_len) = max_len {
out.push_str("(max_len=");
out.push_str(&max_len.to_string());
out.push(')');
} else {
out.push_str("(unbounded)");
}
}
fn write_byte_bounded_field_kind_summary(out: &mut String, kind_name: &str, max_bytes: u32) {
out.push_str(kind_name);
out.push_str("(max_bytes=");
out.push_str(&max_bytes.to_string());
out.push(')');
}
fn write_model_default_summary(out: &mut String, default: FieldDatabaseDefault) {
match default {
FieldDatabaseDefault::None => {}
FieldDatabaseDefault::EncodedSlotPayload(payload) => {
write_encoded_default_payload_summary(out, payload);
}
}
}
fn write_schema_default_summary(out: &mut String, default: &SchemaFieldDefault) {
if let Some(payload) = default.slot_payload() {
write_encoded_default_payload_summary(out, payload);
}
}
fn write_encoded_default_payload_summary(out: &mut String, payload: &[u8]) {
let _ = write!(
out,
" default=slot_payload(bytes={}, sha256={})",
payload.len(),
short_default_payload_fingerprint(payload),
);
}
fn short_default_payload_fingerprint(payload: &[u8]) -> String {
let digest = Sha256::digest(payload);
let mut out = String::with_capacity(16);
for byte in &digest[..8] {
let _ = write!(out, "{byte:02x}");
}
out
}
#[cfg_attr(
doc,
doc = "Render one stable field-kind label from accepted persisted schema metadata."
)]
fn summarize_persisted_field_kind(kind: &PersistedFieldKind) -> String {
let mut out = String::new();
write_persisted_field_kind_summary(&mut out, kind);
out
}
fn write_persisted_field_kind_summary(out: &mut String, kind: &PersistedFieldKind) {
if let Some(name) = kind.describe_kind_name() {
out.push_str(name);
return;
}
match kind {
PersistedFieldKind::Blob { max_len } => {
write_length_bounded_field_kind_summary(out, "blob", *max_len);
}
PersistedFieldKind::Decimal { scale } => {
let _ = write!(out, "decimal(scale={scale})");
}
PersistedFieldKind::IntBig { max_bytes } => {
write_byte_bounded_field_kind_summary(out, "int_big", *max_bytes);
}
PersistedFieldKind::Enum { path, .. } => {
out.push_str("enum(");
out.push_str(path);
out.push(')');
}
PersistedFieldKind::Text { max_len } => {
write_length_bounded_field_kind_summary(out, "text", *max_len);
}
PersistedFieldKind::Relation {
target_entity_name,
key_kind,
strength,
..
} => {
out.push_str("relation(target=");
out.push_str(target_entity_name);
out.push_str(", key=");
write_persisted_field_kind_summary(out, key_kind);
out.push_str(", strength=");
out.push_str(summarize_persisted_relation_strength(*strength));
out.push(')');
}
PersistedFieldKind::List(inner) => {
out.push_str("list<");
write_persisted_field_kind_summary(out, inner);
out.push('>');
}
PersistedFieldKind::Set(inner) => {
out.push_str("set<");
write_persisted_field_kind_summary(out, inner);
out.push('>');
}
PersistedFieldKind::Map { key, value } => {
out.push_str("map<");
write_persisted_field_kind_summary(out, key);
out.push_str(", ");
write_persisted_field_kind_summary(out, value);
out.push('>');
}
PersistedFieldKind::Structured { .. } => {
out.push_str("structured");
}
PersistedFieldKind::Account
| PersistedFieldKind::Bool
| PersistedFieldKind::Date
| PersistedFieldKind::Duration
| PersistedFieldKind::Float32
| PersistedFieldKind::Float64
| PersistedFieldKind::Int8
| PersistedFieldKind::Int16
| PersistedFieldKind::Int32
| PersistedFieldKind::Int64
| PersistedFieldKind::Int128
| PersistedFieldKind::Principal
| PersistedFieldKind::Subaccount
| PersistedFieldKind::Timestamp
| PersistedFieldKind::Nat8
| PersistedFieldKind::Nat16
| PersistedFieldKind::Nat32
| PersistedFieldKind::Nat64
| PersistedFieldKind::Nat128
| PersistedFieldKind::Ulid
| PersistedFieldKind::Unit => unreachable!("schema describe invariant"),
PersistedFieldKind::NatBig { max_bytes } => {
write_byte_bounded_field_kind_summary(out, "nat_big", *max_bytes);
}
}
}
impl DescribeKindName for PersistedFieldKind {
fn describe_kind_name(&self) -> Option<&'static str> {
Some(match self {
Self::Account => "account",
Self::Bool => "bool",
Self::Date => "date",
Self::Duration => "duration",
Self::Float32 => "float32",
Self::Float64 => "float64",
Self::Int8 => "int8",
Self::Int16 => "int16",
Self::Int32 => "int32",
Self::Int64 => "int64",
Self::Int128 => "int128",
Self::Principal => "principal",
Self::Subaccount => "subaccount",
Self::Timestamp => "timestamp",
Self::Nat8 => "nat8",
Self::Nat16 => "nat16",
Self::Nat32 => "nat32",
Self::Nat64 => "nat64",
Self::Nat128 => "nat128",
Self::Ulid => "ulid",
Self::Unit => "unit",
Self::Blob { .. }
| Self::Decimal { .. }
| Self::Enum { .. }
| Self::IntBig { .. }
| Self::NatBig { .. }
| Self::Text { .. }
| Self::Relation { .. }
| Self::List(_)
| Self::Set(_)
| Self::Map { .. }
| Self::Structured { .. } => return None,
})
}
}
#[cfg_attr(
doc,
doc = "Render one stable relation-strength label from persisted schema metadata."
)]
const fn summarize_persisted_relation_strength(
strength: PersistedRelationStrength,
) -> &'static str {
match strength {
PersistedRelationStrength::Strong => "strong",
PersistedRelationStrength::Weak => "weak",
}
}
#[cfg_attr(
doc,
doc = "Render one stable relation-strength label for field-kind summaries."
)]
const fn summarize_relation_strength(strength: RelationStrength) -> &'static str {
match strength {
RelationStrength::Strong => "strong",
RelationStrength::Weak => "weak",
}
}
#[cfg(test)]
mod tests;