use std::borrow::Cow;
use std::collections::BTreeMap;
use std::error::Error;
use std::fmt;
use crate::CodecError;
#[cfg(feature = "schema")]
use schemars::JsonSchema;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::text::NonBlankString;
#[derive(Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "schema", derive(JsonSchema), schemars(with = "String"))]
pub struct DialectId {
value: Cow<'static, str>,
namespace_len: usize,
}
#[derive(Debug, Clone, Copy)]
pub struct StaticDialectId {
value: &'static str,
namespace_len: usize,
}
impl StaticDialectId {
#[must_use]
pub const fn new(id: &'static str) -> Option<Self> {
match dialect_separator(id) {
Some(namespace_len) => Some(Self {
value: id,
namespace_len,
}),
None => None,
}
}
}
impl DialectId {
#[must_use]
pub const fn from_static(id: StaticDialectId) -> Self {
Self {
value: Cow::Borrowed(id.value),
namespace_len: id.namespace_len,
}
}
pub fn parse(id: impl Into<String>) -> Result<Self, DialectIdError> {
let id = id.into();
match dialect_separator(&id) {
Some(namespace_len) => Ok(Self {
value: Cow::Owned(id),
namespace_len,
}),
None => Err(DialectIdError(id)),
}
}
#[must_use]
pub const fn as_str(&self) -> &str {
match &self.value {
Cow::Borrowed(id) => id,
Cow::Owned(id) => id.as_str(),
}
}
#[must_use]
pub fn namespace(&self) -> &str {
self.parts().0
}
pub(crate) fn local(&self) -> &str {
self.parts().1
}
fn parts(&self) -> (&str, &str) {
let (namespace, qualified_name) = self.as_str().split_at(self.namespace_len);
(namespace, &qualified_name[1..])
}
}
#[macro_export]
macro_rules! dialect_id {
($literal:literal $(,)?) => {{
const STATIC_DIALECT_ID: $crate::dialect::StaticDialectId =
match $crate::dialect::StaticDialectId::new($literal) {
Some(id) => id,
None => panic!("invalid pinned dialect id"),
};
$crate::dialect::DialectId::from_static(STATIC_DIALECT_ID)
}};
}
const fn dialect_separator(id: &str) -> Option<usize> {
let bytes = id.as_bytes();
let mut index = 0;
let mut colon = None;
while index < bytes.len() {
if bytes[index] == b':' {
if colon.is_some() || index == 0 || index + 1 == bytes.len() {
return None;
}
colon = Some(index);
}
index += 1;
}
let Some(colon) = colon else {
return None;
};
index = 0;
while index < colon {
let byte = bytes[index];
if !byte.is_ascii_lowercase() && !byte.is_ascii_digit() {
return None;
}
index += 1;
}
if valid_grammar_bytes(bytes, colon + 1) {
Some(colon)
} else {
None
}
}
const fn valid_grammar_bytes(bytes: &[u8], start: usize) -> bool {
if start >= bytes.len() {
return false;
}
let mut index = start;
while index < bytes.len() {
let byte = bytes[index];
if !byte.is_ascii_lowercase() && !byte.is_ascii_digit() && byte != b'-' && byte != b'.' {
return false;
}
index += 1;
}
bytes[start] != b'-' && bytes[bytes.len() - 1] != b'-'
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DialectIdError(String);
impl fmt::Display for DialectIdError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"invalid dialect id {:?}: expected <format>:<name> in lowercase canonical form",
self.0
)
}
}
impl Error for DialectIdError {}
impl fmt::Display for DialectId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl fmt::Debug for DialectId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "DialectId({:?})", self.as_str())
}
}
impl Serialize for DialectId {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for DialectId {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
Self::parse(String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub enum Admission {
Admitted,
Unverified {
using: Grammar,
},
Residual,
Refused,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(try_from = "String")]
pub struct Grammar(String);
impl Grammar {
#[must_use]
pub fn of(dialect: &DialectId) -> Self {
Self(dialect.local().to_owned())
}
pub fn parse(name: impl Into<String>) -> Result<Self, GrammarError> {
let name = name.into();
if valid_grammar_bytes(name.as_bytes(), 0) {
Ok(Self(name))
} else {
Err(GrammarError(name))
}
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl TryFrom<String> for Grammar {
type Error = GrammarError;
fn try_from(name: String) -> Result<Self, Self::Error> {
Self::parse(name)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GrammarError(String);
impl fmt::Display for GrammarError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"invalid grammar name {:?}: expected a format-local name in lowercase canonical form",
self.0
)
}
}
impl Error for GrammarError {}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct DialectMatch {
dialect: DialectId,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
#[serde(deserialize_with = "crate::distinct_keys::btree_map")]
declared: BTreeMap<NonBlankString, String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_instance"
)]
instance: Option<String>,
admission: Admission,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LayerInstance {
Sole,
Tagged,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub struct DialectLayers {
primary: DialectMatch,
extra: Vec<DialectMatch>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct DialectLayersWire {
primary: DialectMatch,
extra: Vec<DialectMatch>,
}
impl<'de> Deserialize<'de> for DialectLayers {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let wire = DialectLayersWire::deserialize(deserializer)?;
let mut layers = Self::of(wire.primary);
for layer in wire.extra {
layers.insert(layer).map_err(|rejected| {
<D::Error as serde::de::Error>::custom(format!(
"duplicate dialect layer key: {rejected:?}"
))
})?;
}
Ok(layers)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DialectLayerError {
Duplicate(DialectMatch),
}
impl fmt::Display for DialectLayerError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Duplicate(layer) => write!(formatter, "duplicate dialect layer key: {layer:?}"),
}
}
}
impl Error for DialectLayerError {}
impl From<DialectLayerError> for CodecError {
fn from(error: DialectLayerError) -> Self {
match error {
DialectLayerError::Duplicate(layer) => {
Self::malformed(format_args!("duplicate dialect layer key: {layer:?}"))
}
}
}
}
impl DialectLayers {
#[must_use]
pub fn of(primary: DialectMatch) -> Self {
Self {
primary,
extra: Vec::new(),
}
}
pub fn insert(&mut self, layer: DialectMatch) -> Result<(), DialectLayerError> {
if Self::same_key(&self.primary, &layer)
|| self
.extra
.iter()
.any(|existing| Self::same_key(existing, &layer))
{
return Err(DialectLayerError::Duplicate(layer));
}
self.extra.push(layer);
Ok(())
}
pub fn with(mut self, layer: DialectMatch) -> Result<Self, DialectLayerError> {
self.insert(layer)?;
Ok(self)
}
fn same_key(existing: &DialectMatch, layer: &DialectMatch) -> bool {
existing.format() == layer.format() && existing.instance == layer.instance
}
#[must_use]
pub fn primary(&self) -> &DialectMatch {
&self.primary
}
pub fn iter(&self) -> impl Iterator<Item = &DialectMatch> {
std::iter::once(&self.primary).chain(&self.extra)
}
}
mod format_identity_sealed {
pub trait Sealed {}
}
pub trait FormatIdentityPayload: format_identity_sealed::Sealed {
fn format(&self) -> &str;
}
impl format_identity_sealed::Sealed for DialectId {}
impl FormatIdentityPayload for DialectId {
fn format(&self) -> &str {
self.namespace()
}
}
impl format_identity_sealed::Sealed for DialectMatch {}
impl FormatIdentityPayload for DialectMatch {
fn format(&self) -> &str {
self.format()
}
}
impl format_identity_sealed::Sealed for DialectLayers {}
impl FormatIdentityPayload for DialectLayers {
fn format(&self) -> &str {
self.primary().format()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(tag = "classification", rename_all = "snake_case", deny_unknown_fields)]
pub enum FormatIdentity<T> {
Classified {
dialects: T,
},
Unclassified {
format: String,
},
}
impl<T: FormatIdentityPayload> FormatIdentity<T> {
#[must_use]
pub const fn classified(payload: T) -> Self {
Self::Classified { dialects: payload }
}
#[must_use]
pub fn unclassified(format: impl Into<String>) -> Self {
Self::Unclassified {
format: format.into(),
}
}
#[must_use]
pub fn format(&self) -> &str {
match self {
Self::Classified { dialects } => dialects.format(),
Self::Unclassified { format } => format,
}
}
#[must_use]
pub const fn classified_payload(&self) -> Option<&T> {
match self {
Self::Classified { dialects } => Some(dialects),
Self::Unclassified { .. } => None,
}
}
}
impl DialectMatch {
fn with_admission(dialect: DialectId, admission: Admission) -> Self {
Self {
dialect,
declared: BTreeMap::new(),
instance: None,
admission,
}
}
#[must_use]
pub fn admitted(dialect: DialectId) -> Self {
Self::with_admission(dialect, Admission::Admitted)
}
#[must_use]
pub fn unverified(dialect: DialectId, using: Grammar) -> Self {
Self::with_admission(dialect, Admission::Unverified { using })
}
#[must_use]
pub fn residual(dialect: DialectId) -> Self {
Self::with_admission(dialect, Admission::Residual)
}
#[must_use]
pub fn refused(dialect: DialectId) -> Self {
Self::with_admission(dialect, Admission::Refused)
}
#[must_use]
pub fn with_declared(mut self, declared: BTreeMap<NonBlankString, String>) -> Self {
self.declared = declared;
self
}
#[must_use]
pub fn with_declared_entry(mut self, key: NonBlankString, value: &str) -> Self {
let _ = self.declared.insert(key, value.to_owned());
self
}
#[must_use]
pub fn with_instance(mut self, instance: impl Into<String>) -> Self {
self.instance = Some(instance.into());
self
}
#[must_use]
pub fn format(&self) -> &str {
self.dialect.namespace()
}
#[must_use]
pub fn dialect(&self) -> &DialectId {
&self.dialect
}
#[must_use]
pub fn declared(&self) -> &BTreeMap<NonBlankString, String> {
&self.declared
}
#[must_use]
pub fn instance(&self) -> Option<&str> {
self.instance.as_deref()
}
#[must_use]
pub fn admission(&self) -> &Admission {
&self.admission
}
#[must_use]
pub fn using(&self) -> Option<DialectId> {
match &self.admission {
Admission::Unverified { using } => Some(self.grammar_id(using)),
Admission::Admitted | Admission::Residual | Admission::Refused => None,
}
}
#[must_use]
pub fn grammar_id(&self, grammar: &Grammar) -> DialectId {
let namespace = self.format();
DialectId {
value: Cow::Owned(format!("{namespace}:{}", grammar.as_str())),
namespace_len: namespace.len(),
}
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use std::collections::BTreeMap;
use super::{Admission, DialectId, DialectLayers, DialectMatch, Grammar, StaticDialectId};
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct DialectIdConformance {
valid: Vec<String>,
invalid: Vec<String>,
}
fn layer(format: &str) -> DialectMatch {
DialectMatch::admitted(DialectId::parse(format!("{format}:known")).unwrap())
}
#[test]
fn a_pinned_id_prints_and_serializes_as_the_plain_string() {
let id = crate::dialect_id!("rhino:archive-80");
assert_eq!(id.to_string(), "rhino:archive-80");
assert_eq!(id.as_str(), "rhino:archive-80");
assert_eq!(serde_json::to_string(&id).unwrap(), "\"rhino:archive-80\"");
assert_eq!(
serde_json::from_str::<DialectId>("\"rhino:archive-80\"").unwrap(),
id
);
}
#[test]
fn static_dialect_admission_returns_none_for_invalid_runtime_text() {
for text in [
"",
"rhino",
"rhino:",
":known",
"Rhino:known",
"a:b:c",
"a:é",
] {
assert!(StaticDialectId::new(text).is_none(), "{text:?}");
}
let admitted = DialectId::from_static(StaticDialectId::new("rhino:archive-80").unwrap());
assert_eq!(admitted.namespace(), "rhino");
assert_eq!(admitted.local(), "archive-80");
assert_eq!(admitted, DialectId::parse("rhino:archive-80").unwrap());
}
#[test]
fn dialect_id_deserialization_rejects_noncanonical_strings() {
for id in [
"rhino",
":archive-80",
"rhino:",
"Rhino:archive-80",
"rhino:archive_80",
"rhino:-archive-80",
] {
serde_json::from_value::<DialectId>(serde_json::json!(id))
.expect_err("a malformed dialect id must be rejected");
}
}
#[test]
fn dialect_id_matches_the_shared_conformance_corpus() {
let cases: DialectIdConformance =
toml::from_str(include_str!("../../../docs/dialect-id-conformance.toml")).unwrap();
for id in cases.valid {
let admitted =
DialectId::parse(id.clone()).unwrap_or_else(|_| panic!("valid dialect id {id:?}"));
assert_eq!(format!("{}:{}", admitted.namespace(), admitted.local()), id);
}
for id in cases.invalid {
assert!(
DialectId::parse(id.clone()).is_err(),
"invalid dialect id {id:?}"
);
}
}
#[test]
fn an_admitted_match_serializes_its_identity() {
let admitted = DialectMatch {
dialect: crate::dialect_id!("rhino:archive-80"),
declared: BTreeMap::new(),
instance: None,
admission: Admission::Admitted,
};
assert_eq!(
serde_json::to_string(&admitted).unwrap(),
"{\"dialect\":\"rhino:archive-80\",\"admission\":\"admitted\"}"
);
}
#[test]
fn a_dialect_match_wire_states_its_format_once_through_its_dialect() {
let restated = serde_json::json!({
"format": "rhino",
"dialect": "rhino:archive-80",
"admission": "admitted",
});
let error = serde_json::from_value::<DialectMatch>(restated)
.expect_err("the format is read from the dialect, so the wire carries no format key");
assert!(
error.to_string().contains("unknown field `format`"),
"{error}"
);
assert_eq!(
DialectMatch::admitted(crate::dialect_id!("rhino:archive-80")).format(),
"rhino"
);
}
#[test]
fn residual_constructor_records_the_absence_of_a_declared_grammar() {
let residual = DialectMatch::residual(crate::dialect_id!("rhino:unknown"));
assert_eq!(residual.admission(), &Admission::Residual);
assert_eq!(residual.using(), None);
assert_eq!(
serde_json::to_string(&residual).unwrap(),
"{\"dialect\":\"rhino:unknown\",\"admission\":\"residual\"}"
);
}
#[test]
fn an_unverified_admission_names_the_grammar_in_use_by_full_id() {
let unverified = DialectMatch::unverified(
crate::dialect_id!("acis:save-format-217"),
Grammar::of(&crate::dialect_id!("acis:save-format-218")),
);
assert_eq!(
unverified.admission(),
&Admission::Unverified {
using: Grammar::of(&crate::dialect_id!("acis:save-format-218")),
}
);
assert_eq!(
unverified.using(),
Some(crate::dialect_id!("acis:save-format-218"))
);
let serialized = serde_json::to_string(&unverified).unwrap();
assert_eq!(
serialized,
"{\"dialect\":\"acis:save-format-217\",\"admission\":{\"unverified\":{\"using\":\"save-format-218\"}}}"
);
assert_eq!(
serde_json::from_str::<DialectMatch>(&serialized).unwrap(),
unverified
);
}
#[test]
fn a_self_named_unverified_grammar_remains_opaque() {
let self_named = serde_json::json!({
"dialect": "rhino:unknown",
"admission": {
"unverified": { "using": "unknown" }
},
});
let matched = serde_json::from_value::<DialectMatch>(self_named)
.expect("core does not infer grammar semantics from an opaque dialect name");
assert_eq!(
matched.admission(),
&Admission::Unverified {
using: Grammar::of(&crate::dialect_id!("rhino:unknown")),
}
);
}
#[test]
fn a_grammar_name_has_no_spelling_for_a_foreign_namespace() {
let malformed = serde_json::json!({
"dialect": "rhino:unknown",
"admission": {
"unverified": { "using": "step:ap242-e3" }
},
});
let error = serde_json::from_value::<DialectMatch>(malformed)
.expect_err("a grammar substitute belongs to the classified format layer");
assert!(
error
.to_string()
.contains("invalid grammar name \"step:ap242-e3\""),
"{error}"
);
}
#[test]
fn grammar_parsing_rejects_a_name_outside_the_format_local_class() {
for name in [
"",
"rhino:archive-80",
"Archive-80",
"archive_80",
"-archive",
] {
assert!(
Grammar::parse(name).is_err(),
"invalid grammar name {name:?}"
);
}
assert_eq!(
Grammar::parse("save-format-218").unwrap(),
Grammar::of(&crate::dialect_id!("acis:save-format-218"))
);
}
#[test]
fn identity_does_not_encode_whether_an_unverified_path_used_a_grammar() {
let dialect = crate::dialect_id!("rhino:archive-80");
let without_grammar = DialectMatch::residual(dialect.clone());
assert_eq!(without_grammar.admission(), &Admission::Residual);
let self_named = DialectMatch::unverified(dialect.clone(), Grammar::of(&dialect));
assert_eq!(
self_named.admission(),
&Admission::Unverified {
using: Grammar::of(&crate::dialect_id!("rhino:archive-80")),
}
);
assert_eq!(self_named.using(), Some(dialect));
}
#[test]
fn dialect_layers_accept_a_same_format_extra_with_an_instance() {
let member = layer("rhino").with_instance("components/member.3dm");
let layers = DialectLayers::of(layer("rhino"))
.with(member.clone())
.expect("distinct dialect layer keys");
let serialized = serde_json::to_value(&layers).unwrap();
assert_eq!(
serde_json::from_value::<DialectLayers>(serialized).unwrap(),
layers
);
assert_eq!(layers.extra, [member]);
}
#[test]
fn dialect_layers_insert_keeps_the_first_extra_layer_for_a_key() {
let first = layer("acis").with_instance("body");
let replacement =
DialectMatch::residual(crate::dialect_id!("acis:other")).with_instance("body");
let mut layers = DialectLayers::of(layer("rhino"))
.with(first.clone())
.expect("distinct dialect layer keys");
assert_eq!(
layers.insert(replacement.clone()),
Err(super::DialectLayerError::Duplicate(replacement))
);
assert_eq!(layers.extra, [first]);
}
#[test]
fn dialect_layers_insert_keeps_the_primary_on_its_own_key() {
let replacement = DialectMatch::residual(crate::dialect_id!("rhino:other"));
let mut layers = DialectLayers::of(layer("rhino"))
.with(layer("acis"))
.expect("distinct dialect layer keys");
assert_eq!(
layers.insert(replacement.clone()),
Err(super::DialectLayerError::Duplicate(replacement))
);
assert_eq!(layers.primary(), &layer("rhino"));
assert_eq!(layers.extra, [layer("acis")]);
}
#[test]
fn dialect_layers_builder_returns_the_colliding_layer() {
let first = layer("acis").with_instance("body");
let replacement =
DialectMatch::residual(crate::dialect_id!("acis:other")).with_instance("body");
let layers = DialectLayers::of(layer("rhino"))
.with(first)
.expect("distinct dialect layer keys");
assert_eq!(
layers.with(replacement.clone()),
Err(super::DialectLayerError::Duplicate(replacement))
);
}
#[test]
fn dialect_layers_keep_same_format_extras_with_distinct_instances() {
let anonymous = layer("acis");
let named = layer("acis").with_instance("body");
let mut layers = DialectLayers::of(layer("rhino"));
assert_eq!(layers.insert(anonymous.clone()), Ok(()));
assert_eq!(layers.insert(named.clone()), Ok(()));
assert_eq!(layers.extra, [anonymous, named]);
}
#[test]
fn dialect_layers_deserialization_rejects_a_repeated_key() {
let serialized = serde_json::json!({
"primary": layer("rhino"),
"extra": [layer("acis"), DialectMatch::residual(crate::dialect_id!("acis:other"))],
});
let error = serde_json::from_value::<DialectLayers>(serialized)
.expect_err("a repeated extra layer key is a read error");
assert!(
error.to_string().contains("duplicate dialect layer key"),
"{error}"
);
}
#[test]
fn dialect_layers_deserialization_rejects_an_extra_on_the_primary_key() {
let serialized = serde_json::json!({
"primary": layer("rhino"),
"extra": [DialectMatch::residual(crate::dialect_id!("rhino:other"))],
});
let error = serde_json::from_value::<DialectLayers>(serialized)
.expect_err("an extra layer on the primary key is a read error");
assert!(
error.to_string().contains("duplicate dialect layer key"),
"{error}"
);
}
#[test]
fn dialect_layers_serialize_with_an_explicit_primary() {
let layers = DialectLayers::of(layer("rhino"))
.with(layer("acis"))
.expect("distinct dialect layer keys");
let serialized = serde_json::to_value(&layers).unwrap();
assert_eq!(
serialized,
serde_json::json!({
"primary": layer("rhino"),
"extra": [layer("acis")],
})
);
assert_eq!(
serde_json::from_value::<DialectLayers>(serialized).unwrap(),
layers
);
assert_eq!(
layers.iter().map(DialectMatch::format).collect::<Vec<_>>(),
["rhino", "acis"]
);
}
}
crate::named_optional_field!(deserialize_instance, String, "instance");