use alloc::string::{String, ToString};
use alloc::vec;
use alloc::vec::Vec;
use core::fmt;
use core::num::ParseIntError;
use super::util::{take_string, take_timestamp};
use crate::event::tag::{Tag, TagCodec, TagCodecError, impl_tag_codec_conversions};
use crate::{EventBuilder, Kind, Timestamp};
const URL: &str = "d";
const PUBLISHED_AT: &str = "published_at";
const TITLE: &str = "title";
const HASHTAG: &str = "t";
#[derive(Debug, PartialEq)]
pub enum Error {
ParseInt(ParseIntError),
Codec(TagCodecError),
}
impl core::error::Error for Error {}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ParseInt(e) => e.fmt(f),
Self::Codec(e) => e.fmt(f),
}
}
}
impl From<ParseIntError> for Error {
fn from(e: ParseIntError) -> Self {
Self::ParseInt(e)
}
}
impl From<TagCodecError> for Error {
fn from(e: TagCodecError) -> Self {
Self::Codec(e)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum NipB0Tag {
Url(String),
PublishedAt(Timestamp),
Title(String),
Hashtag(String),
}
impl TagCodec for NipB0Tag {
type Error = Error;
fn parse<I, S>(tag: I) -> Result<Self, Self::Error>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let mut iter = tag.into_iter();
let kind: S = iter.next().ok_or(TagCodecError::missing_tag_kind())?;
match kind.as_ref() {
URL => Ok(Self::Url(take_string(&mut iter, "URL")?)),
PUBLISHED_AT => {
let timestamp: Timestamp = take_timestamp::<_, _, Error>(&mut iter)?;
Ok(Self::PublishedAt(timestamp))
}
TITLE => Ok(Self::Title(take_string(&mut iter, "title")?)),
HASHTAG => Ok(Self::Hashtag(
take_string(&mut iter, "hashtag")?.to_lowercase(),
)),
_ => Err(TagCodecError::Unknown.into()),
}
}
fn to_tag(&self) -> Tag {
match self {
Self::Url(url) => Tag::new(vec![String::from(URL), url.clone()]),
Self::PublishedAt(timestamp) => {
Tag::new(vec![String::from(PUBLISHED_AT), timestamp.to_string()])
}
Self::Title(title) => Tag::new(vec![String::from(TITLE), title.clone()]),
Self::Hashtag(hashtag) => Tag::new(vec![String::from(HASHTAG), hashtag.to_lowercase()]),
}
}
}
impl_tag_codec_conversions!(NipB0Tag);
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct WebBookmark {
pub description: String,
pub url: String,
pub published_at: Option<Timestamp>,
pub title: Option<String>,
pub hashtags: Vec<String>,
}
impl WebBookmark {
#[inline]
pub fn new<T>(description: T, url: T) -> Self
where
T: Into<String>,
{
Self {
description: description.into(),
url: url.into(),
published_at: None,
title: None,
hashtags: Vec::new(),
}
}
#[inline]
pub fn title<T>(mut self, title: T) -> Self
where
T: Into<String>,
{
self.title = Some(title.into());
self
}
#[inline]
pub fn published_at(mut self, timestamp: Timestamp) -> Self {
self.published_at = Some(timestamp);
self
}
pub fn hashtags<T>(mut self, hashtag: T) -> Self
where
T: Into<String>,
{
let hashtag = hashtag.into().to_lowercase();
if !self.hashtags.contains(&hashtag) {
self.hashtags.push(hashtag);
}
self
}
#[allow(clippy::wrong_self_convention)]
pub(crate) fn to_event_builder(self) -> EventBuilder {
let mut tags: Vec<Tag> = vec![NipB0Tag::Url(self.url).into()];
let mut add_if_some = |tag: Option<NipB0Tag>| {
if let Some(tag) = tag {
tags.push(tag.into());
}
};
add_if_some(self.published_at.map(NipB0Tag::PublishedAt));
add_if_some(self.title.map(NipB0Tag::Title));
for hashtag in self.hashtags.into_iter() {
tags.push(NipB0Tag::Hashtag(hashtag).into());
}
EventBuilder::new(Kind::WebBookmark, self.description).tags(tags)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_url_tag() {
let tag = vec!["d", "alice.blog/post"];
let parsed = NipB0Tag::parse(&tag).unwrap();
assert_eq!(parsed, NipB0Tag::Url(String::from("alice.blog/post")));
assert_eq!(parsed.to_tag(), Tag::parse(tag).unwrap());
}
#[test]
fn test_parse_published_at_tag() {
let tag = vec!["published_at", "1738863000"];
let parsed = NipB0Tag::parse(&tag).unwrap();
assert_eq!(parsed, NipB0Tag::PublishedAt(Timestamp::from(1738863000)));
assert_eq!(parsed.to_tag(), Tag::parse(tag).unwrap());
}
#[test]
fn test_parse_title_tag() {
let tag = vec!["title", "Blog insights by Alice"];
let parsed = NipB0Tag::parse(&tag).unwrap();
assert_eq!(
parsed,
NipB0Tag::Title(String::from("Blog insights by Alice"))
);
assert_eq!(parsed.to_tag(), Tag::parse(tag).unwrap());
}
#[test]
fn test_parse_hashtag_tag() {
let tag = vec!["t", "Insight"];
let parsed = NipB0Tag::parse(&tag).unwrap();
assert_eq!(parsed, NipB0Tag::Hashtag(String::from("insight")));
assert_eq!(parsed.to_tag(), Tag::parse(vec!["t", "insight"]).unwrap());
}
}