use std::{
borrow::{Borrow, ToOwned},
fmt,
ops::Deref,
str::FromStr,
};
use crate::RdfDisplay;
#[derive(Debug)]
pub struct InvalidBlankId<T>(pub T);
#[derive(PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct BlankId(str);
impl BlankId {
#[inline(always)]
pub fn new(s: &str) -> Result<&Self, InvalidBlankId<&str>> {
if check(s) {
Ok(unsafe { Self::new_unchecked(s) })
} else {
Err(InvalidBlankId(s))
}
}
#[inline(always)]
pub unsafe fn new_unchecked(s: &str) -> &Self {
unsafe { std::mem::transmute(s) }
}
#[inline(always)]
pub const fn as_str(&self) -> &str {
&self.0
}
#[inline(always)]
pub fn suffix(&self) -> &str {
&self.0[2..]
}
}
impl Deref for BlankId {
type Target = str;
#[inline(always)]
fn deref(&self) -> &str {
self.as_str()
}
}
impl AsRef<str> for BlankId {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl AsRef<[u8]> for BlankId {
fn as_ref(&self) -> &[u8] {
self.as_bytes()
}
}
impl Borrow<str> for BlankId {
fn borrow(&self) -> &str {
self.as_str()
}
}
impl ToOwned for BlankId {
type Owned = BlankIdBuf;
#[inline(always)]
fn to_owned(&self) -> BlankIdBuf {
unsafe { BlankIdBuf::new_unchecked(self.as_str().to_owned()) }
}
}
impl fmt::Display for BlankId {
#[inline(always)]
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.0.fmt(f)
}
}
impl RdfDisplay for BlankId {
#[inline(always)]
fn rdf_fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(&self.0)
}
}
impl fmt::Debug for BlankId {
#[inline(always)]
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.0.fmt(f)
}
}
impl PartialEq<str> for BlankId {
#[inline(always)]
fn eq(&self, other: &str) -> bool {
self.0 == *other
}
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(transparent))]
pub struct BlankIdBuf(String);
impl BlankIdBuf {
#[inline(always)]
pub fn new(s: String) -> Result<Self, InvalidBlankId<String>> {
if check(&s) {
Ok(unsafe { Self::new_unchecked(s) })
} else {
Err(InvalidBlankId(s))
}
}
#[inline(always)]
pub unsafe fn new_unchecked(s: String) -> Self {
unsafe { std::mem::transmute(s) }
}
#[inline(always)]
pub fn from_u8(i: u8) -> Self {
unsafe { Self::new_unchecked(format_blank_int(i)) }
}
#[inline(always)]
pub fn from_u16(i: u16) -> Self {
unsafe { Self::new_unchecked(format_blank_int(i)) }
}
#[inline(always)]
pub fn from_u32(i: u32) -> Self {
unsafe { Self::new_unchecked(format_blank_int(i)) }
}
#[inline(always)]
pub fn from_u64(i: u64) -> Self {
unsafe { Self::new_unchecked(format_blank_int(i)) }
}
#[inline(always)]
pub fn from_suffix(suffix: &str) -> Result<Self, InvalidBlankId<String>> {
Self::new(format!("_:{suffix}"))
}
#[inline(always)]
pub fn as_blank_id_ref(&self) -> &BlankId {
unsafe { BlankId::new_unchecked(&self.0) }
}
}
#[inline(always)]
fn format_blank_int<I: itoa::Integer>(i: I) -> String {
let mut buf = itoa::Buffer::new();
let n = buf.format(i);
let mut s = String::with_capacity(2 + n.len());
s.push_str("_:");
s.push_str(n);
s
}
impl FromStr for BlankIdBuf {
type Err = InvalidBlankId<String>;
fn from_str(s: &str) -> Result<Self, InvalidBlankId<String>> {
Self::new(s.to_owned())
}
}
impl Deref for BlankIdBuf {
type Target = BlankId;
#[inline(always)]
fn deref(&self) -> &BlankId {
self.as_blank_id_ref()
}
}
impl AsRef<BlankId> for BlankIdBuf {
#[inline(always)]
fn as_ref(&self) -> &BlankId {
self.as_blank_id_ref()
}
}
impl Borrow<BlankId> for BlankIdBuf {
#[inline(always)]
fn borrow(&self) -> &BlankId {
self.as_blank_id_ref()
}
}
impl AsRef<str> for BlankIdBuf {
#[inline(always)]
fn as_ref(&self) -> &str {
self.0.as_str()
}
}
impl Borrow<str> for BlankIdBuf {
#[inline(always)]
fn borrow(&self) -> &str {
self.0.as_str()
}
}
impl AsRef<[u8]> for BlankIdBuf {
#[inline(always)]
fn as_ref(&self) -> &[u8] {
self.0.as_bytes()
}
}
impl Borrow<BlankId> for &BlankIdBuf {
#[inline(always)]
fn borrow(&self) -> &BlankId {
self.as_blank_id_ref()
}
}
impl Borrow<str> for &BlankIdBuf {
#[inline(always)]
fn borrow(&self) -> &str {
self.0.as_str()
}
}
impl<'a> From<&'a BlankIdBuf> for &'a BlankId {
#[inline(always)]
fn from(b: &'a BlankIdBuf) -> Self {
b.as_ref()
}
}
impl fmt::Display for BlankIdBuf {
#[inline(always)]
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.0.fmt(f)
}
}
impl RdfDisplay for BlankIdBuf {
#[inline(always)]
fn rdf_fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(&self.0)
}
}
impl fmt::Debug for BlankIdBuf {
#[inline(always)]
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.0.fmt(f)
}
}
impl PartialEq<BlankId> for BlankIdBuf {
fn eq(&self, other: &BlankId) -> bool {
self.as_blank_id_ref() == other
}
}
impl<'a> PartialEq<&'a BlankId> for BlankIdBuf {
fn eq(&self, other: &&'a BlankId) -> bool {
self.as_blank_id_ref() == *other
}
}
impl PartialEq<BlankIdBuf> for &BlankId {
fn eq(&self, other: &BlankIdBuf) -> bool {
*self == other.as_blank_id_ref()
}
}
impl PartialEq<BlankIdBuf> for BlankId {
fn eq(&self, other: &BlankIdBuf) -> bool {
self == other.as_blank_id_ref()
}
}
fn check(s: &str) -> bool {
let bytes = s.as_bytes();
if bytes.len() < 3 || bytes[0] != b'_' || bytes[1] != b':' {
return false;
}
if bytes[bytes.len() - 1] == b'.' {
return false;
}
let body = &bytes[2..];
let seed_len = match body[0] {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'_' | b':' => 1,
c if c < 0x80 => return false,
_ => {
let mut chars = s[2..].chars();
match chars.next() {
Some(ch) if is_pn_char_u(ch) => ch.len_utf8(),
_ => return false,
}
}
};
let rest = &body[seed_len..];
match crate::swar::blankid_body_scan(rest) {
crate::swar::BodyScan::AllValidAscii => true,
crate::swar::BodyScan::InvalidAt(_) => false,
crate::swar::BodyScan::NonAsciiAt(i) => {
let off_in_s = 2 + seed_len + i;
for c in s[off_in_s..].chars() {
if !is_pn_char(c) && c != '.' {
return false;
}
}
true
}
}
}
const fn is_pn_char_base(c: char) -> bool {
matches!(c, 'A'..='Z' | 'a'..='z' | '\u{00c0}'..='\u{00d6}' | '\u{00d8}'..='\u{00f6}' | '\u{00f8}'..='\u{02ff}' | '\u{0370}'..='\u{037d}' | '\u{037f}'..='\u{1fff}' | '\u{200c}'..='\u{200d}' | '\u{2070}'..='\u{218f}' | '\u{2c00}'..='\u{2fef}' | '\u{3001}'..='\u{d7ff}' | '\u{f900}'..='\u{fdcf}' | '\u{fdf0}'..='\u{fffd}' | '\u{10000}'..='\u{effff}')
}
const fn is_pn_char_u(c: char) -> bool {
is_pn_char_base(c) || matches!(c, '_' | ':')
}
const fn is_pn_char(c: char) -> bool {
is_pn_char_u(c) || matches!(c, '-' | '0'..='9' | '\u{00b7}' | '\u{0300}'..='\u{036f}' | '\u{203f}'..='\u{2040}')
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for BlankIdBuf {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct Visitor;
impl<'de> serde::de::Visitor<'de> for Visitor {
type Value = BlankIdBuf;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(formatter, "blank node identifier")
}
fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
BlankIdBuf::new(v).map_err(|InvalidBlankId(unexpected)| E::invalid_value(serde::de::Unexpected::Str(&unexpected), &self))
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
self.visit_string(v.to_owned())
}
fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
self.visit_string(v.to_owned())
}
}
deserializer.deserialize_string(Visitor)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::panic, clippy::expect_used)]
mod tests {
use super::{BlankId, BlankIdBuf};
use crate::RdfDisplay;
fn ok(s: &str) -> bool {
BlankId::new(s).is_ok()
}
#[test]
fn rdf_display_renders_unquoted() {
let buf = BlankIdBuf::new("_:foo".to_owned()).unwrap();
let bid: &BlankId = &buf;
assert_eq!(format!("{}", bid.rdf_display()), "_:foo");
assert_eq!(format!("{}", buf.rdf_display()), "_:foo");
}
#[test]
fn accepts_minimal() {
assert!(ok("_:a"));
assert!(ok("_:_"));
assert!(ok("_:0"));
assert!(ok("_:9"));
assert!(ok("_::"));
}
#[test]
fn rejects_no_prefix() {
assert!(!ok(""));
assert!(!ok("_"));
assert!(!ok("_x"));
assert!(!ok(":a"));
assert!(!ok("_:"));
}
#[test]
fn rejects_bad_seed() {
assert!(!ok("_:."));
assert!(!ok("_:-"));
}
#[test]
fn rejects_trailing_dot() {
assert!(!ok("_:abc."));
assert!(!ok("_:a.b."));
assert!(!ok("_:1."));
}
#[test]
fn accepts_interior_dot() {
assert!(ok("_:a.b"));
assert!(ok("_:a..b"));
assert!(ok("_:1.2.3"));
}
#[test]
fn accepts_unicode_seed() {
assert!(ok("_:é"));
assert!(ok("_:ñabc"));
}
#[test]
fn accepts_unicode_interior() {
assert!(ok("_:abcñdef"));
assert!(ok("_:abc·def"));
}
#[test]
fn rejects_unicode_invalid() {
assert!(!ok("_:abc\u{200B}"));
}
const LENGTHS: &[usize] = &[1, 7, 8, 9, 15, 16, 17, 63, 64, 65, 1000];
#[test]
fn length_sweep_accept() {
for &n in LENGTHS {
let s = format!("_:{}", "a".repeat(n));
assert!(ok(&s), "length {n} should accept");
}
}
#[test]
fn length_sweep_trailing_dot_reject() {
for &n in LENGTHS {
let s = format!("_:{}.", "a".repeat(n));
assert!(!ok(&s), "length {n} with trailing dot should reject");
}
}
#[test]
fn length_sweep_non_ascii_at_every_position() {
for &n in LENGTHS {
for p in 0..n {
let mut body: String = "a".repeat(n);
body.replace_range(p..p + 1, "é");
let s = format!("_:{body}");
assert!(ok(&s), "length {n} pos {p} non-ASCII should accept");
}
}
}
}