use crate::{BlankIdBuf, LocalTerm};
use super::LocalGenerator;
#[derive(Debug, thiserror::Error)]
#[error("invalid blank-id prefix: {0:?}")]
pub struct InvalidBlankPrefix(pub String);
#[derive(Default)]
pub struct Blank {
prefix: String,
count: usize,
}
impl Blank {
pub const fn new() -> Self {
Self {
prefix: String::new(),
count: 0,
}
}
pub const fn new_with_offset(offset: usize) -> Self {
Self {
prefix: String::new(),
count: offset,
}
}
pub fn new_with_prefix(prefix: impl Into<String>) -> Result<Self, InvalidBlankPrefix> {
Self::new_full(prefix, 0)
}
pub fn new_full(prefix: impl Into<String>, offset: usize) -> Result<Self, InvalidBlankPrefix> {
let prefix = prefix.into();
validate_prefix(&prefix)?;
Ok(Self { prefix, count: offset })
}
pub const unsafe fn new_full_unchecked(prefix: String, offset: usize) -> Self {
Self { prefix, count: offset }
}
pub fn prefix(&self) -> &str {
&self.prefix
}
pub const fn count(&self) -> usize {
self.count
}
pub fn next_blank_id(&mut self) -> BlankIdBuf {
let mut buf = itoa::Buffer::new();
let count_str = buf.format(self.count);
let mut s = String::with_capacity(2 + self.prefix.len() + count_str.len());
s.push_str("_:");
s.push_str(&self.prefix);
s.push_str(count_str);
let id = unsafe { BlankIdBuf::new_unchecked(s) };
self.count += 1;
id
}
}
fn validate_prefix(prefix: &str) -> Result<(), InvalidBlankPrefix> {
if prefix.is_empty() {
return Ok(());
}
let mut probe = String::with_capacity(2 + prefix.len() + 1);
probe.push_str("_:");
probe.push_str(prefix);
probe.push('0');
BlankIdBuf::new(probe).map(|_| ()).map_err(|_| InvalidBlankPrefix(prefix.to_owned()))
}
impl LocalGenerator for Blank {
fn next_local_term(&mut self) -> LocalTerm {
LocalTerm::BlankId(self.next_blank_id())
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::panic, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn empty_prefix_generates_valid_ids() {
let mut g = Blank::new();
for _ in 0..10 {
let id = g.next_blank_id();
assert!(id.as_str().starts_with("_:"));
}
}
#[test]
fn valid_prefix_accepted() {
let mut g = Blank::new_with_prefix("foo").unwrap();
let id = g.next_blank_id();
assert_eq!(id.as_str(), "_:foo0");
}
#[test]
fn prefix_with_dot_in_middle_accepted() {
let mut g = Blank::new_with_prefix("a.b").unwrap();
let id = g.next_blank_id();
assert_eq!(id.as_str(), "_:a.b0");
}
#[test]
fn invalid_prefix_rejected_starts_with_dot() {
assert!(Blank::new_with_prefix(".bad").is_err());
}
#[test]
fn invalid_prefix_rejected_starts_with_hyphen() {
assert!(Blank::new_with_prefix("-bad").is_err());
}
#[test]
fn invalid_prefix_rejected_contains_space() {
assert!(Blank::new_with_prefix("a b").is_err());
}
#[test]
fn invalid_prefix_rejected_contains_special() {
assert!(Blank::new_with_prefix("foo!").is_err());
}
}