use std::fmt;
use std::marker::PhantomData;
use substrate_data::{Id, Kind, Reader, Ref, Schema, Shape, Value, ValueRef};
use crate::kinds;
fn split(spelled: &str) -> Option<(&str, &str)> {
spelled.split_once(yog_ids::sep::NAMESPACE)
}
pub struct TagId<K> {
pub id: String,
kind: PhantomData<K>,
}
impl<K> TagId<K> {
pub fn new(id: impl Into<String>) -> Self {
Self {
id: id.into(),
kind: PhantomData,
}
}
}
impl<K> Clone for TagId<K> {
fn clone(&self) -> Self {
Self::new(self.id.clone())
}
}
impl<K> PartialEq for TagId<K> {
fn eq(&self, o: &Self) -> bool {
self.id == o.id
}
}
impl<K: Kind> fmt::Debug for TagId<K> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "@{}:{}:{}", kinds::Tag::NAME, K::NAME, self.id)
}
}
impl<K: Kind> Schema for TagId<K> {
fn shape() -> Shape {
substrate_data::schema::ref_shape::<kinds::Tag>()
}
fn read(v: &ValueRef<'_>, cx: &mut Reader<'_>) -> Option<Self> {
let tag = Id::<kinds::Tag>::read(v, cx)?;
match split(&tag.id) {
Some((kind, id)) if kind == K::NAME => Some(TagId::new(id)),
_ => {
cx.problem(format!(
"`@{}:{}`: this takes a tag of {} — `@{}:{}:<id>`",
kinds::Tag::NAME,
tag.id,
K::NAME,
kinds::Tag::NAME,
K::NAME
));
None
}
}
}
fn write(&self) -> Value {
let spelled = format!("{}{}{}", K::NAME, yog_ids::sep::NAMESPACE, self.id);
Id::<kinds::Tag>::new(spelled).write()
}
}
fn is_tag(v: &ValueRef<'_>) -> bool {
let tag = kinds::Tag::NAME;
match v {
ValueRef::Ref { kind, .. } => kind.as_str() == tag,
ValueRef::Str(s) => s
.strip_prefix(yog_ids::sep::TYPED)
.and_then(|rest| rest.strip_prefix(tag))
.is_some_and(|rest| rest.starts_with(yog_ids::sep::NAMESPACE)),
_ => false,
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum OneOrTag<K: Kind<Id = String>> {
One(Id<K>),
Tag(TagId<K>),
}
pub type ItemIdOrTag = OneOrTag<kinds::Item>;
impl<K: Kind<Id = String>> Schema for OneOrTag<K> {
fn shape() -> Shape {
Shape::Ref {
kinds: &[K::NAME, kinds::Tag::NAME],
id: String::shape,
}
}
fn read(v: &ValueRef<'_>, cx: &mut Reader<'_>) -> Option<Self> {
if let ValueRef::Str(s) = v
&& let Some(tag) = s.strip_prefix(yog_ids::sep::TAG)
{
cx.problem(format!(
"`{s}`: write a tag as `@{}:{}:{tag}`",
kinds::Tag::NAME,
K::NAME
));
return None;
}
if is_tag(v) {
TagId::read(v, cx).map(OneOrTag::Tag)
} else {
Id::read(v, cx).map(OneOrTag::One)
}
}
fn write(&self) -> Value {
match self {
OneOrTag::One(i) => i.write(),
OneOrTag::Tag(t) => t.write(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum AnyRefOrTag {
One(Ref),
Tag(String, String),
}
impl AnyRefOrTag {
pub fn kind(&self) -> &str {
match self {
AnyRefOrTag::One(r) => r.kind.as_str(),
AnyRefOrTag::Tag(kind, _) => kind,
}
}
}
impl fmt::Display for AnyRefOrTag {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AnyRefOrTag::One(r) => r.fmt(f),
AnyRefOrTag::Tag(kind, id) => write!(f, "@{}:{kind}:{id}", kinds::Tag::NAME),
}
}
}
impl Schema for AnyRefOrTag {
fn shape() -> Shape {
Shape::Ref {
kinds: &[],
id: String::shape,
}
}
fn read(v: &ValueRef<'_>, cx: &mut Reader<'_>) -> Option<Self> {
let r = substrate_data::schema::read_ref(v, cx, &[])?;
if r.kind.as_str() != kinds::Tag::NAME {
return Some(AnyRefOrTag::One(r));
}
let Some((kind, id)) = split(r.id.as_str()) else {
cx.problem(format!("`{r}`: a tag is `@tag:<kind>:<id>`"));
return None;
};
Some(AnyRefOrTag::Tag(kind.to_owned(), id.to_owned()))
}
fn write(&self) -> Value {
match self {
AnyRefOrTag::One(r) => Value::Ref(r.clone()),
AnyRefOrTag::Tag(kind, id) => {
Id::<kinds::Tag>::new(format!("{kind}{}{id}", yog_ids::sep::NAMESPACE)).write()
}
}
}
}