use std::fmt;
use super::canonical::{CanonicalValue, Checksum};
use super::ids::{InvalidId, ProjectId, ResourceId, TenantId};
use super::resource::{ResourceBody, ResourceKind, ResourceRef, ResourceVersion};
use super::tenancy::{DisplayName, InvalidDisplayName};
pub(super) const SCHEMA_FIELD: &str = "schema";
pub(super) const TENANT_ID_FIELD: &str = "tenant_id";
pub(super) const PROJECT_ID_FIELD: &str = "project_id";
pub(super) const DISPLAY_NAME_FIELD: &str = "display_name";
pub(super) trait BodyError: Sized {
fn kind(reference: ResourceRef, expected: ResourceKind, found: ResourceKind) -> Self;
fn not_inline(reference: ResourceRef) -> Self;
fn not_a_record(reference: ResourceRef) -> Self;
fn schema(reference: ResourceRef, expected: &'static str, found: String) -> Self;
fn missing_field(reference: ResourceRef, field: &'static str) -> Self;
fn unknown_field(reference: ResourceRef, schema: &'static str, field: String) -> Self;
fn reserved_field(reference: ResourceRef, schema: &'static str, field: String) -> Self {
Self::unknown_field(reference, schema, field)
}
fn field_type(reference: ResourceRef, field: &'static str) -> Self;
fn damaged_schema(reference: ResourceRef) -> Self;
fn field_set(reference: ResourceRef, field: &'static str) -> Self {
Self::field_type(reference, field)
}
fn malformed_checksum(reference: ResourceRef, field: &'static str) -> Self {
Self::field_type(reference, field)
}
}
pub(super) trait DisplayNameError: BodyError {
fn malformed_display_name(
reference: ResourceRef,
field: &'static str,
source: InvalidDisplayName,
) -> Self;
}
pub(super) trait IdentifiedBody: BodyError {
fn malformed_id(reference: ResourceRef, field: &'static str, source: InvalidId) -> Self;
fn identity_mismatch(reference: ResourceRef, declared: String, identity: ResourceId) -> Self;
}
pub(super) struct Record<'a, E> {
reference: ResourceRef,
fields: &'a [(String, CanonicalValue)],
error: std::marker::PhantomData<E>,
}
impl<'a, E: BodyError> Record<'a, E> {
pub(super) fn open(
resource: &'a ResourceVersion,
kind: ResourceKind,
schema: &'static str,
known: &[&str],
) -> Result<Self, E> {
Self::open_reserving(resource, kind, schema, known, &[])
}
pub(super) fn open_reserving(
resource: &'a ResourceVersion,
kind: ResourceKind,
schema: &'static str,
known: &[&str],
reserved: &[&str],
) -> Result<Self, E> {
Self::open_any_reserving(resource, kind, &[schema], known, reserved)
.map(|(record, _)| record)
}
pub(super) fn open_any(
resource: &'a ResourceVersion,
kind: ResourceKind,
schemas: &[&'static str],
known: &[&str],
) -> Result<(Self, &'static str), E> {
Self::open_any_reserving(resource, kind, schemas, known, &[])
}
fn open_any_reserving(
resource: &'a ResourceVersion,
kind: ResourceKind,
schemas: &[&'static str],
known: &[&str],
reserved: &[&str],
) -> Result<(Self, &'static str), E> {
let reference = resource.reference;
if reference.kind != kind {
return Err(E::kind(reference, kind, reference.kind));
}
let ResourceBody::Inline(value) = &resource.body else {
return Err(E::not_inline(reference));
};
let CanonicalValue::Map(fields) = value else {
return Err(E::not_a_record(reference));
};
let record = Self {
reference,
fields,
error: std::marker::PhantomData,
};
let CanonicalValue::String(declared) = record.value(SCHEMA_FIELD)? else {
return Err(E::damaged_schema(reference));
};
let declared = declared.as_str();
let schema = *schemas
.iter()
.find(|candidate| **candidate == declared)
.ok_or_else(|| {
E::schema(
reference,
schemas.last().copied().unwrap_or(""),
declared.to_owned(),
)
})?;
if let Some((field, _)) = fields
.iter()
.find(|(field, _)| reserved.contains(&field.as_str()))
{
return Err(E::reserved_field(reference, schema, field.clone()));
}
if let Some((field, _)) = fields
.iter()
.find(|(field, _)| field != SCHEMA_FIELD && !known.contains(&field.as_str()))
{
return Err(E::unknown_field(reference, schema, field.clone()));
}
Ok((record, schema))
}
pub(super) fn nested(
reference: ResourceRef,
schema: &'static str,
field: &'static str,
value: &'a CanonicalValue,
known: &[&str],
) -> Result<Self, E> {
let CanonicalValue::Map(fields) = value else {
return Err(E::field_type(reference, field));
};
if let Some((field, _)) = fields
.iter()
.find(|(field, _)| !known.contains(&field.as_str()))
{
return Err(E::unknown_field(reference, schema, field.clone()));
}
Ok(Self {
reference,
fields,
error: std::marker::PhantomData,
})
}
pub(super) const fn reference(&self) -> ResourceRef {
self.reference
}
pub(super) fn optional_value(&self, field: &'static str) -> Option<&'a CanonicalValue> {
self.fields
.iter()
.find(|(name, _)| name == field)
.map(|(_, value)| value)
}
pub(super) fn value(&self, field: &'static str) -> Result<&'a CanonicalValue, E> {
self.optional_value(field)
.ok_or_else(|| E::missing_field(self.reference, field))
}
pub(super) fn record(
&self,
schema: &'static str,
field: &'static str,
known: &[&str],
) -> Result<Self, E> {
Self::nested(self.reference, schema, field, self.value(field)?, known)
}
pub(super) fn set(&self, field: &'static str) -> Result<&'a [CanonicalValue], E> {
match self.value(field)? {
CanonicalValue::Set(members) => Ok(members),
_ => Err(E::field_type(self.reference, field)),
}
}
pub(super) fn string(&self, field: &'static str) -> Result<&'a str, E> {
match self.value(field)? {
CanonicalValue::String(text) => Ok(text),
_ => Err(E::field_type(self.reference, field)),
}
}
pub(super) fn optional_string(&self, field: &'static str) -> Result<Option<&'a str>, E> {
match self.fields.iter().find(|(name, _)| name == field) {
None => Ok(None),
Some((_, CanonicalValue::String(text))) => Ok(Some(text)),
Some(_) => Err(E::field_type(self.reference, field)),
}
}
pub(super) fn sub_record(
&self,
value: &'a CanonicalValue,
field: &'static str,
schema: &'static str,
known: &[&str],
) -> Result<Self, E> {
let CanonicalValue::Map(fields) = value else {
return Err(E::field_type(self.reference, field));
};
if let Some((key, _)) = fields
.iter()
.find(|(key, _)| !known.contains(&key.as_str()))
{
return Err(E::unknown_field(
self.reference,
schema,
format!("{field}.{key}"),
));
}
Ok(Self {
reference: self.reference,
fields,
error: std::marker::PhantomData,
})
}
pub(super) fn string_set(&self, field: &'static str) -> Result<Vec<&'a str>, E> {
let CanonicalValue::Set(members) = self.value(field)? else {
return Err(E::field_set(self.reference, field));
};
members
.iter()
.map(|member| match member {
CanonicalValue::String(text) => Ok(text.as_str()),
_ => Err(E::field_set(self.reference, field)),
})
.collect()
}
pub(super) fn optional_checksum(&self, field: &'static str) -> Result<Option<Checksum>, E> {
self.optional_string(field)?
.map(|text| {
Checksum::parse(text).map_err(|_| E::malformed_checksum(self.reference, field))
})
.transpose()
}
pub(super) fn integer(&self, field: &'static str) -> Result<u64, E> {
match self.value(field)? {
CanonicalValue::Integer(value) => {
u64::try_from(*value).map_err(|_| E::field_type(self.reference, field))
}
_ => Err(E::field_type(self.reference, field)),
}
}
pub(super) fn signed_integer(&self, field: &'static str) -> Result<i128, E> {
match self.value(field)? {
CanonicalValue::Integer(value) => Ok(*value),
_ => Err(E::field_type(self.reference, field)),
}
}
pub(super) fn optional_integer(&self, field: &'static str) -> Result<Option<u64>, E> {
match self.fields.iter().find(|(name, _)| name == field) {
None => Ok(None),
Some((_, CanonicalValue::Integer(value))) => u64::try_from(*value)
.map(Some)
.map_err(|_| E::field_type(self.reference, field)),
Some(_) => Err(E::field_type(self.reference, field)),
}
}
}
impl<'a, E: IdentifiedBody> Record<'a, E> {
pub(super) fn tenant(&self) -> Result<TenantId, E> {
self.id(TENANT_ID_FIELD, TenantId::parse)
}
pub(super) fn optional_project(&self) -> Result<Option<ProjectId>, E> {
match self.optional_string(PROJECT_ID_FIELD)? {
None => Ok(None),
Some(text) => ProjectId::parse(text)
.map(Some)
.map_err(|source| E::malformed_id(self.reference, PROJECT_ID_FIELD, source)),
}
}
pub(super) fn project(&self) -> Result<ProjectId, E> {
self.id(PROJECT_ID_FIELD, ProjectId::parse)
}
fn id<T>(
&self,
field: &'static str,
parse: impl FnOnce(&str) -> Result<T, InvalidId>,
) -> Result<T, E> {
parse(self.string(field)?).map_err(|source| E::malformed_id(self.reference, field, source))
}
pub(super) fn typed_id<T>(
&self,
field: &'static str,
parse: impl FnOnce(&str) -> Result<T, InvalidId>,
) -> Result<T, E> {
self.id(field, parse)
}
pub(super) fn identity(
&self,
declared: impl fmt::Display,
identity: ResourceId,
) -> Result<(), E> {
if self.reference.id == identity {
Ok(())
} else {
Err(E::identity_mismatch(
self.reference,
declared.to_string(),
self.reference.id,
))
}
}
}
impl<'a, E: DisplayNameError> Record<'a, E> {
pub(super) fn display_name(&self) -> Result<DisplayName, E> {
DisplayName::parse(self.string(DISPLAY_NAME_FIELD)?)
.map_err(|source| E::malformed_display_name(self.reference, DISPLAY_NAME_FIELD, source))
}
pub(super) fn optional_display_name(
&self,
field: &'static str,
) -> Result<Option<DisplayName>, E> {
self.optional_string(field)?
.map(|text| {
DisplayName::parse(text)
.map_err(|source| E::malformed_display_name(self.reference, field, source))
})
.transpose()
}
}