use std::fmt;
use serde::de::Error as _;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use unicode_ident::{is_xid_continue, is_xid_start};
use crate::diagnostic::{Diagnostic, DiagnosticCategory};
pub const MAX_LABEL_BYTES: usize = 255;
pub const MAX_THING_IID_HEX_DIGITS: usize = 256;
#[must_use]
pub fn is_canonical_thing_iid(value: &str) -> bool {
value.strip_prefix("0x").is_some_and(|digits| {
!digits.is_empty()
&& digits.len() <= MAX_THING_IID_HEX_DIGITS
&& digits.bytes().all(|byte| byte.is_ascii_hexdigit())
})
}
const TYPEQL_3_12_1_RESERVED_LABELS: [&str; 42] = [
"with",
"given",
"match",
"fetch",
"update",
"define",
"undefine",
"redefine",
"insert",
"put",
"delete",
"end",
"entity",
"relation",
"attribute",
"role",
"asc",
"desc",
"struct",
"fun",
"return",
"alias",
"sub",
"owns",
"as",
"plays",
"relates",
"iid",
"isa",
"links",
"has",
"is",
"or",
"not",
"try",
"in",
"true",
"false",
"of",
"from",
"first",
"last",
];
const TYPEQL_3_12_BUILTIN_FUNCTION_NAMES: [&str; 9] = [
"abs", "ceil", "floor", "iid", "label", "len", "max", "min", "round",
];
fn is_typeql_3_12_1_reserved_label(value: &str) -> bool {
TYPEQL_3_12_1_RESERVED_LABELS.contains(&value)
}
#[must_use]
pub fn is_typeql_3_12_builtin_function_name(value: &str) -> bool {
TYPEQL_3_12_BUILTIN_FUNCTION_NAMES.contains(&value)
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Label(String);
impl Label {
pub fn new(value: impl Into<String>) -> Result<Self, Diagnostic> {
let value = value.into();
let mut chars = value.chars();
let valid = value.len() <= MAX_LABEL_BYTES
&& chars.next().is_some_and(|ch| ch == '_' || is_xid_start(ch))
&& chars.all(|ch| ch == '-' || is_xid_continue(ch))
&& !is_typeql_3_12_1_reserved_label(&value);
if valid {
Ok(Self(value))
} else {
Err(Diagnostic::stable(
DiagnosticCategory::InvalidContract,
"malformed_id",
"identifier label is empty, oversized, reserved, or contains invalid characters",
)
.with_detail(
"maximum_bytes",
i64::try_from(MAX_LABEL_BYTES).unwrap_or(i64::MAX),
))
}
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for Label {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl Serialize for Label {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for Label {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
Self::new(String::deserialize(deserializer)?).map_err(D::Error::custom)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TypeKind {
Entity,
Relation,
Attribute,
Struct,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct TypeId {
kind: TypeKind,
label: Label,
}
impl TypeId {
pub fn new(kind: TypeKind, label: impl Into<String>) -> Result<Self, Diagnostic> {
Ok(Self {
kind,
label: Label::new(label)?,
})
}
pub const fn kind(&self) -> TypeKind {
self.kind
}
pub fn label(&self) -> &Label {
&self.label
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct RoleId {
declaring_relation: Label,
label: Label,
}
impl RoleId {
pub fn new(
declaring_relation: impl Into<String>,
label: impl Into<String>,
) -> Result<Self, Diagnostic> {
Ok(Self {
declaring_relation: Label::new(declaring_relation)?,
label: Label::new(label)?,
})
}
pub fn declaring_relation(&self) -> &Label {
&self.declaring_relation
}
pub fn label(&self) -> &Label {
&self.label
}
}
macro_rules! label_id {
($name:ident, $doc:literal) => {
#[doc = $doc]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct $name(Label);
impl $name {
pub fn new(label: impl Into<String>) -> Result<Self, Diagnostic> {
Ok(Self(Label::new(label)?))
}
pub fn label(&self) -> &Label {
&self.0
}
}
impl fmt::Display for $name {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(formatter)
}
}
};
}
label_id!(AttributeId, "A typed attribute identity.");
label_id!(FunctionId, "A typed function identity.");
label_id!(StructId, "A typed struct identity.");
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn labels_reject_malformed_input_during_deserialization() {
for value in ["", "9person", "person name", "person.", "a²"] {
assert_eq!(
Label::new(value).unwrap_err().code().as_str(),
"malformed_id"
);
}
assert!(serde_json::from_str::<Label>(r#""person name""#).is_err());
}
#[test]
fn labels_follow_typeql_unicode_xid_grammar() {
for value in ["_", "type-with-hyphens", "a·b", "a\u{301}", "℘x"] {
assert_eq!(Label::new(value).unwrap().as_str(), value);
}
}
#[test]
fn labels_reject_the_typeql_3_12_1_reserved_vocabulary() {
for value in TYPEQL_3_12_1_RESERVED_LABELS {
assert_eq!(
Label::new(value).unwrap_err().code().as_str(),
"malformed_id",
"reserved TypeQL word {value:?} must not cross the identifier boundary",
);
}
assert!(Label::new("matching").is_ok());
assert!(Label::new("entity-type").is_ok());
}
#[test]
fn builtin_function_names_remain_contextual_identifiers() {
for value in TYPEQL_3_12_BUILTIN_FUNCTION_NAMES {
assert!(
is_typeql_3_12_builtin_function_name(value),
"missing TypeQL built-in function {value:?}",
);
}
for value in ["absolute", "length", "person_name_length"] {
assert!(!is_typeql_3_12_builtin_function_name(value));
}
assert!(FunctionId::new("abs").is_ok());
assert!(FunctionId::new("label").is_ok());
assert!(FunctionId::new("iid").is_err());
}
#[test]
fn thing_iids_preserve_the_released_bounded_hexadecimal_grammar() {
assert!(is_canonical_thing_iid("0x0"));
assert!(is_canonical_thing_iid("0xAbCdEf"));
assert!(is_canonical_thing_iid(&format!(
"0x{}",
"a".repeat(MAX_THING_IID_HEX_DIGITS)
)));
for malformed in ["", "0x", "0X1", "01", "0x1g", "0x1; delete $x;"] {
assert!(!is_canonical_thing_iid(malformed), "{malformed:?}");
}
assert!(!is_canonical_thing_iid(&format!(
"0x{}",
"a".repeat(MAX_THING_IID_HEX_DIGITS + 1)
)));
}
#[test]
fn malformed_typed_id_wires_fail_closed() {
assert!(serde_json::from_str::<TypeId>(r#"{"kind":"entity","label":"9person"}"#).is_err());
assert!(serde_json::from_str::<TypeId>(r#"{"kind":"future","label":"person"}"#).is_err());
assert!(
serde_json::from_str::<RoleId>(
r#"{"declaring_relation":"9employment","label":"employee"}"#
)
.is_err()
);
}
#[test]
fn role_identity_includes_the_declaring_relation() {
let employee = RoleId::new("employment", "employee").unwrap();
let membership = RoleId::new("membership", "employee").unwrap();
assert_ne!(employee, membership);
}
}