tokens: switch over to using token serialization

This removes all of the old serialization code

Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
William Casarin
2025-01-22 13:13:25 -08:00
parent 5ba06986db
commit d46e526a45

View File

@@ -1,19 +1,15 @@
use std::{collections::HashMap, fmt, str::FromStr};
use enostr::{NoteId, Pubkey};
use enostr::Pubkey;
use nostrdb::Ndb;
use serde::{Deserialize, Serialize};
use strum::IntoEnumIterator;
use strum_macros::EnumIter;
use tracing::{error, info};
use crate::{
accounts::AccountsRoute,
column::{Columns, IntermediaryRoute},
decks::{Deck, Decks, DecksCache},
route::Route,
timeline::{kind::ListKind, AlgoTimeline, PubkeySource, TimelineKind, TimelineRoute},
ui::add_column::{AddAlgoRoute, AddColumnRoute},
timeline::TimelineKind,
Error,
};
@@ -286,9 +282,9 @@ fn serialize_columns(columns: &Columns) -> Vec<Vec<String>> {
for column in columns.columns() {
let mut column_routes = Vec::new();
for route in column.router().routes() {
if let Some(route_str) = serialize_route(route, columns) {
column_routes.push(route_str);
}
let mut writer = TokenWriter::default();
route.serialize_tokens(&mut writer);
column_routes.push(writer.str().to_string());
}
cols_serialized.push(column_routes);
}
@@ -296,27 +292,26 @@ fn serialize_columns(columns: &Columns) -> Vec<Vec<String>> {
cols_serialized
}
fn deserialize_columns(ndb: &Ndb, deck_user: &[u8; 32], serialized: Vec<Vec<String>>) -> Columns {
fn deserialize_columns(ndb: &Ndb, deck_user: &[u8; 32], columns: Vec<Vec<String>>) -> Columns {
let mut cols = Columns::new();
for serialized_routes in serialized {
for column in columns {
let mut cur_routes = Vec::new();
for serialized_route in serialized_routes {
let selections = Selection::from_serialized(&serialized_route);
if let Some(route_intermediary) = selections_to_route(&selections) {
if let Some(ir) = route_intermediary.intermediary_route(ndb, Some(deck_user)) {
match &ir {
IntermediaryRoute::Route(Route::Timeline(TimelineRoute::Thread(_)))
| IntermediaryRoute::Route(Route::Timeline(TimelineRoute::Profile(_))) => {
// Do nothing. TimelineRoute Threads & Profiles not yet supported for deserialization
}
_ => cur_routes.push(ir),
for route in column {
let tokens: Vec<&str> = route.split(":").collect();
let mut parser = TokenParser::new(&tokens);
match CleanIntermediaryRoute::parse_from_tokens(&mut parser) {
Ok(route_intermediary) => {
if let Some(ir) =
route_intermediary.into_intermediary_route(ndb, Some(deck_user))
{
cur_routes.push(ir);
}
}
} else {
error!(
"could not turn selections to RouteIntermediary: {:?}",
selections
);
Err(err) => {
error!("could not turn tokens to RouteIntermediary: {:?}", err);
}
}
}
@@ -328,223 +323,17 @@ fn deserialize_columns(ndb: &Ndb, deck_user: &[u8; 32], serialized: Vec<Vec<Stri
cols
}
/// Different token types for our deck serializer/deserializer
///
/// We have more than one token type so that we can avoid match catch-alls
/// in different parts of our parser.
#[derive(Clone, Debug)]
enum Selection {
Keyword(Keyword),
Algo(AlgoKeyword),
List(ListKeyword),
PubkeySource(PubkeySourceKeyword),
Payload(String),
}
impl FromStr for Selection {
type Err = Error;
fn from_str(serialized: &str) -> Result<Self, Self::Err> {
Ok(parse_selection(serialized))
}
}
#[derive(Clone, PartialEq, Eq, Debug, EnumIter)]
enum AlgoKeyword {
LastPerPubkey,
}
impl AlgoKeyword {
#[inline]
pub fn name(&self) -> &'static str {
match self {
AlgoKeyword::LastPerPubkey => "last_per_pubkey",
}
}
}
#[derive(Clone, PartialEq, Eq, Debug, EnumIter)]
enum ListKeyword {
Contact,
}
impl ListKeyword {
#[inline]
pub fn name(&self) -> &'static str {
match self {
ListKeyword::Contact => "contact",
}
}
}
#[derive(Clone, PartialEq, Eq, Debug, EnumIter)]
enum PubkeySourceKeyword {
Explicit,
DeckAuthor,
}
impl PubkeySourceKeyword {
#[inline]
pub fn name(&self) -> &'static str {
match self {
PubkeySourceKeyword::Explicit => "explicit",
PubkeySourceKeyword::DeckAuthor => "deck_author",
}
}
}
#[derive(Clone, PartialEq, Eq, Debug, EnumIter)]
enum Keyword {
Notifs,
Universe,
Profile,
Hashtag,
Generic,
Thread,
Reply,
Quote,
Account,
Show,
New,
Relay,
Compose,
Column,
AlgoSelection,
NotificationSelection,
ExternalNotifSelection,
HashtagSelection,
Support,
Deck,
Edit,
IndividualSelection,
ExternalIndividualSelection,
}
impl Keyword {
fn name(&self) -> &'static str {
match self {
Keyword::Notifs => "notifs",
Keyword::Universe => "universe",
Keyword::Profile => "profile",
Keyword::Hashtag => "hashtag",
Keyword::Generic => "generic",
Keyword::Thread => "thread",
Keyword::Reply => "reply",
Keyword::Quote => "quote",
Keyword::Account => "account",
Keyword::Show => "show",
Keyword::New => "new",
Keyword::Relay => "relay",
Keyword::Compose => "compose",
Keyword::Column => "column",
Keyword::AlgoSelection => "algo_selection",
Keyword::NotificationSelection => "notification_selection",
Keyword::ExternalNotifSelection => "external_notif_selection",
Keyword::IndividualSelection => "individual_selection",
Keyword::ExternalIndividualSelection => "external_individual_selection",
Keyword::HashtagSelection => "hashtag_selection",
Keyword::Support => "support",
Keyword::Deck => "deck",
Keyword::Edit => "edit",
}
}
}
impl fmt::Display for Keyword {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.name())
}
}
impl fmt::Display for AlgoKeyword {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.name())
}
}
impl fmt::Display for ListKeyword {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.name())
}
}
impl FromStr for PubkeySourceKeyword {
type Err = Error;
fn from_str(serialized: &str) -> Result<Self, Self::Err> {
for keyword in Self::iter() {
if serialized == keyword.name() {
return Ok(keyword);
}
}
Err(Error::Generic(
"Could not convert string to Keyword enum".to_owned(),
))
}
}
impl FromStr for ListKeyword {
type Err = Error;
fn from_str(serialized: &str) -> Result<Self, Self::Err> {
for keyword in Self::iter() {
if serialized == keyword.name() {
return Ok(keyword);
}
}
Err(Error::Generic(
"Could not convert string to Keyword enum".to_owned(),
))
}
}
impl fmt::Display for PubkeySourceKeyword {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.name())
}
}
impl FromStr for AlgoKeyword {
type Err = Error;
fn from_str(serialized: &str) -> Result<Self, Self::Err> {
for keyword in Self::iter() {
if serialized == keyword.name() {
return Ok(keyword);
}
}
Err(Error::Generic(
"Could not convert string to Keyword enum".to_owned(),
))
}
}
impl FromStr for Keyword {
type Err = Error;
fn from_str(serialized: &str) -> Result<Self, Self::Err> {
for keyword in Self::iter() {
if serialized == keyword.name() {
return Ok(keyword);
}
}
Err(Error::Generic(
"Could not convert string to Keyword enum".to_owned(),
))
}
}
enum CleanIntermediaryRoute {
ToTimeline(TimelineKind),
ToRoute(Route),
}
impl CleanIntermediaryRoute {
fn intermediary_route(self, ndb: &Ndb, user: Option<&[u8; 32]>) -> Option<IntermediaryRoute> {
fn into_intermediary_route(
self,
ndb: &Ndb,
user: Option<&[u8; 32]>,
) -> Option<IntermediaryRoute> {
match self {
CleanIntermediaryRoute::ToTimeline(timeline_kind) => Some(IntermediaryRoute::Timeline(
timeline_kind.into_timeline(ndb, user)?,
@@ -554,411 +343,35 @@ impl CleanIntermediaryRoute {
}
}
// TODO: The public-accessible version will be a subset of this
fn serialize_route(route: &Route, columns: &Columns) -> Option<String> {
let mut selections: Vec<Selection> = Vec::new();
match route {
Route::Timeline(timeline_route) => match timeline_route {
TimelineRoute::Timeline(timeline_id) => {
if let Some(timeline) = columns.find_timeline(*timeline_id) {
match &timeline.kind {
TimelineKind::List(list_kind) => match list_kind {
ListKind::Contact(pubkey_source) => {
selections.push(Selection::List(ListKeyword::Contact));
selections.extend(generate_pubkey_selections(pubkey_source));
}
},
TimelineKind::Algo(AlgoTimeline::LastPerPubkey(list_kind)) => {
match list_kind {
ListKind::Contact(pk_src) => {
selections.push(Selection::Algo(AlgoKeyword::LastPerPubkey));
selections.push(Selection::List(ListKeyword::Contact));
selections.extend(generate_pubkey_selections(pk_src));
}
}
}
TimelineKind::Notifications(pubkey_source) => {
selections.push(Selection::Keyword(Keyword::Notifs));
selections.extend(generate_pubkey_selections(pubkey_source));
}
TimelineKind::Profile(pubkey_source) => {
selections.push(Selection::Keyword(Keyword::Profile));
selections.extend(generate_pubkey_selections(pubkey_source));
}
TimelineKind::Universe => {
selections.push(Selection::Keyword(Keyword::Universe))
}
TimelineKind::Thread(root_id) => {
selections.push(Selection::Keyword(Keyword::Thread));
selections.push(Selection::Payload(hex::encode(root_id.bytes())));
}
TimelineKind::Generic => {
selections.push(Selection::Keyword(Keyword::Generic))
}
TimelineKind::Hashtag(hashtag) => {
selections.push(Selection::Keyword(Keyword::Hashtag));
selections.push(Selection::Payload(hashtag.to_string()));
}
}
}
}
TimelineRoute::Thread(note_id) => {
selections.push(Selection::Keyword(Keyword::Thread));
selections.push(Selection::Payload(note_id.hex()));
}
TimelineRoute::Profile(pubkey) => {
selections.push(Selection::Keyword(Keyword::Profile));
selections.push(Selection::PubkeySource(PubkeySourceKeyword::Explicit));
selections.push(Selection::Payload(pubkey.hex()));
}
TimelineRoute::Reply(note_id) => {
selections.push(Selection::Keyword(Keyword::Reply));
selections.push(Selection::Payload(note_id.hex()));
}
TimelineRoute::Quote(note_id) => {
selections.push(Selection::Keyword(Keyword::Quote));
selections.push(Selection::Payload(note_id.hex()));
}
},
Route::Accounts(accounts_route) => {
selections.push(Selection::Keyword(Keyword::Account));
match accounts_route {
AccountsRoute::Accounts => selections.push(Selection::Keyword(Keyword::Show)),
AccountsRoute::AddAccount => selections.push(Selection::Keyword(Keyword::New)),
}
}
Route::Relays => selections.push(Selection::Keyword(Keyword::Relay)),
Route::ComposeNote => selections.push(Selection::Keyword(Keyword::Compose)),
Route::AddColumn(add_column_route) => {
selections.push(Selection::Keyword(Keyword::Column));
match add_column_route {
AddColumnRoute::Base => (),
AddColumnRoute::Algo(algo_route) => match algo_route {
AddAlgoRoute::Base => {
selections.push(Selection::Keyword(Keyword::AlgoSelection))
}
AddAlgoRoute::LastPerPubkey => {
selections.push(Selection::Keyword(Keyword::AlgoSelection));
selections.push(Selection::Algo(AlgoKeyword::LastPerPubkey));
}
},
AddColumnRoute::UndecidedNotification => {
selections.push(Selection::Keyword(Keyword::NotificationSelection))
}
AddColumnRoute::ExternalNotification => {
selections.push(Selection::Keyword(Keyword::ExternalNotifSelection))
}
AddColumnRoute::Hashtag => {
selections.push(Selection::Keyword(Keyword::HashtagSelection))
}
AddColumnRoute::UndecidedIndividual => {
selections.push(Selection::Keyword(Keyword::IndividualSelection))
}
AddColumnRoute::ExternalIndividual => {
selections.push(Selection::Keyword(Keyword::ExternalIndividualSelection))
}
}
}
Route::Support => selections.push(Selection::Keyword(Keyword::Support)),
Route::NewDeck => {
selections.push(Selection::Keyword(Keyword::Deck));
selections.push(Selection::Keyword(Keyword::New));
}
Route::EditDeck(index) => {
selections.push(Selection::Keyword(Keyword::Deck));
selections.push(Selection::Keyword(Keyword::Edit));
selections.push(Selection::Payload(index.to_string()));
}
Route::EditProfile(pubkey) => {
selections.push(Selection::Keyword(Keyword::Profile));
selections.push(Selection::Keyword(Keyword::Edit));
selections.push(Selection::Payload(pubkey.hex()));
}
}
if selections.is_empty() {
None
} else {
Some(
selections
.iter()
.map(|k| k.to_string())
.collect::<Vec<String>>()
.join(":"),
)
}
}
fn generate_pubkey_selections(source: &PubkeySource) -> Vec<Selection> {
let mut selections = Vec::new();
match source {
PubkeySource::Explicit(pubkey) => {
selections.push(Selection::PubkeySource(PubkeySourceKeyword::Explicit));
selections.push(Selection::Payload(pubkey.hex()));
}
PubkeySource::DeckAuthor => {
selections.push(Selection::PubkeySource(PubkeySourceKeyword::DeckAuthor));
}
}
selections
}
/// Parses a selection
fn parse_selection(token: &str) -> Selection {
AlgoKeyword::from_str(token)
.map(Selection::Algo)
.or_else(|_| ListKeyword::from_str(token).map(Selection::List))
.or_else(|_| PubkeySourceKeyword::from_str(token).map(Selection::PubkeySource))
.or_else(|_| Keyword::from_str(token).map(Selection::Keyword))
.unwrap_or_else(|_| Selection::Payload(token.to_owned()))
}
impl Selection {
fn from_serialized(buffer: &str) -> Vec<Self> {
let mut selections = Vec::new();
let seperator = ":";
let sep_len = seperator.len();
let mut pos = 0;
while let Some(offset) = buffer[pos..].find(seperator) {
selections.push(parse_selection(&buffer[pos..pos + offset]));
pos = pos + offset + sep_len;
}
selections.push(parse_selection(&buffer[pos..]));
selections
}
}
/// Parse an explicit:abdef... or deck_author from a Selection token stream.
///
/// Also handle the case where there is nothing. We assume this means deck_author.
fn parse_pubkey_src_selection(tokens: &[Selection]) -> Option<PubkeySource> {
match tokens.first() {
// we handle bare payloads and assume they are explicit pubkey sources
Some(Selection::Payload(hex)) => {
let pk = Pubkey::from_hex(hex.as_str()).ok()?;
Some(PubkeySource::Explicit(pk))
}
Some(Selection::PubkeySource(PubkeySourceKeyword::Explicit)) => {
if let Selection::Payload(hex) = tokens.get(1)? {
let pk = Pubkey::from_hex(hex.as_str()).ok()?;
Some(PubkeySource::Explicit(pk))
} else {
None
}
}
None | Some(Selection::PubkeySource(PubkeySourceKeyword::DeckAuthor)) => {
Some(PubkeySource::DeckAuthor)
}
Some(Selection::Keyword(_kw)) => None,
Some(Selection::Algo(_kw)) => None,
Some(Selection::List(_kw)) => None,
}
}
/// Parse ListKinds from Selections
fn parse_list_kind_selections(tokens: &[Selection]) -> Option<ListKind> {
// only list selections are valid in this position
let list_kw = if let Selection::List(list_kw) = tokens.first()? {
list_kw
} else {
return None;
};
let pubkey_src = parse_pubkey_src_selection(&tokens[1..])?;
Some(match list_kw {
ListKeyword::Contact => ListKind::contact_list(pubkey_src),
})
}
fn selections_to_route(selections: &[Selection]) -> Option<CleanIntermediaryRoute> {
match selections.first()? {
Selection::Keyword(Keyword::AlgoSelection) => {
let r = match selections.get(1) {
None => AddColumnRoute::Algo(AddAlgoRoute::Base),
Some(Selection::Algo(algo_kw)) => match algo_kw {
AlgoKeyword::LastPerPubkey => AddColumnRoute::Algo(AddAlgoRoute::LastPerPubkey),
},
// other keywords are invalid here
Some(_) => {
return None;
}
};
Some(CleanIntermediaryRoute::ToRoute(Route::AddColumn(r)))
}
// Algorithm timelines
Selection::Algo(algo_kw) => {
let timeline_kind = match algo_kw {
AlgoKeyword::LastPerPubkey => {
let list_kind = parse_list_kind_selections(&selections[1..])?;
TimelineKind::last_per_pubkey(list_kind)
}
};
Some(CleanIntermediaryRoute::ToTimeline(timeline_kind))
}
// We never have PubkeySource keywords at the top level
Selection::PubkeySource(_pk_src) => None,
Selection::List(ListKeyword::Contact) => {
// only pubkey/src is allowed in this position
let pubkey_src = parse_pubkey_src_selection(&selections[1..])?;
Some(CleanIntermediaryRoute::ToTimeline(
TimelineKind::contact_list(pubkey_src),
))
}
Selection::Keyword(Keyword::Notifs) => {
let pubkey_src = parse_pubkey_src_selection(&selections[1..])?;
Some(CleanIntermediaryRoute::ToTimeline(
TimelineKind::notifications(pubkey_src),
))
}
Selection::Keyword(Keyword::Profile) => {
// we only expect PubkeySource in this position
let pubkey_src = parse_pubkey_src_selection(&selections[1..])?;
Some(CleanIntermediaryRoute::ToTimeline(TimelineKind::profile(
pubkey_src,
)))
}
Selection::Keyword(Keyword::Universe) => {
Some(CleanIntermediaryRoute::ToTimeline(TimelineKind::Universe))
}
Selection::Keyword(Keyword::Hashtag) => {
if let Selection::Payload(hashtag) = selections.get(1)? {
Some(CleanIntermediaryRoute::ToTimeline(TimelineKind::Hashtag(
hashtag.to_string(),
)))
} else {
None
}
}
Selection::Keyword(Keyword::Generic) => {
Some(CleanIntermediaryRoute::ToTimeline(TimelineKind::Generic))
}
Selection::Keyword(Keyword::Thread) => {
if let Selection::Payload(hex) = selections.get(1)? {
Some(CleanIntermediaryRoute::ToRoute(Route::thread(
NoteId::from_hex(hex.as_str()).ok()?,
)))
} else {
None
}
}
Selection::Keyword(Keyword::Reply) => {
if let Selection::Payload(hex) = selections.get(1)? {
Some(CleanIntermediaryRoute::ToRoute(Route::reply(
NoteId::from_hex(hex.as_str()).ok()?,
)))
} else {
None
}
}
Selection::Keyword(Keyword::Quote) => {
if let Selection::Payload(hex) = selections.get(1)? {
Some(CleanIntermediaryRoute::ToRoute(Route::quote(
NoteId::from_hex(hex.as_str()).ok()?,
)))
} else {
None
}
}
Selection::Keyword(Keyword::Account) => match selections.get(1)? {
Selection::Keyword(Keyword::Show) => Some(CleanIntermediaryRoute::ToRoute(
Route::Accounts(AccountsRoute::Accounts),
)),
Selection::Keyword(Keyword::New) => Some(CleanIntermediaryRoute::ToRoute(
Route::Accounts(AccountsRoute::AddAccount),
)),
_ => None,
},
Selection::Keyword(Keyword::Relay) => Some(CleanIntermediaryRoute::ToRoute(Route::Relays)),
Selection::Keyword(Keyword::Compose) => {
Some(CleanIntermediaryRoute::ToRoute(Route::ComposeNote))
}
Selection::Keyword(Keyword::Column) => match selections.get(1)? {
Selection::Keyword(Keyword::NotificationSelection) => {
Some(CleanIntermediaryRoute::ToRoute(Route::AddColumn(
AddColumnRoute::UndecidedNotification,
)))
}
Selection::Keyword(Keyword::ExternalNotifSelection) => {
Some(CleanIntermediaryRoute::ToRoute(Route::AddColumn(
AddColumnRoute::ExternalNotification,
)))
}
Selection::Keyword(Keyword::HashtagSelection) => Some(CleanIntermediaryRoute::ToRoute(
Route::AddColumn(AddColumnRoute::Hashtag),
)),
Selection::Keyword(Keyword::IndividualSelection) => {
Some(CleanIntermediaryRoute::ToRoute(Route::AddColumn(
AddColumnRoute::UndecidedIndividual,
)))
}
Selection::Keyword(Keyword::ExternalIndividualSelection) => {
Some(CleanIntermediaryRoute::ToRoute(Route::AddColumn(
AddColumnRoute::ExternalIndividual,
)))
}
_ => None,
},
Selection::Keyword(Keyword::Support) => {
Some(CleanIntermediaryRoute::ToRoute(Route::Support))
}
Selection::Keyword(Keyword::Deck) => match selections.get(1)? {
Selection::Keyword(Keyword::New) => {
Some(CleanIntermediaryRoute::ToRoute(Route::NewDeck))
}
Selection::Keyword(Keyword::Edit) => {
if let Selection::Payload(index_str) = selections.get(2)? {
let parsed_index = index_str.parse::<usize>().ok()?;
Some(CleanIntermediaryRoute::ToRoute(Route::EditDeck(
parsed_index,
)))
} else {
None
}
}
_ => None,
},
Selection::Payload(_)
| Selection::Keyword(Keyword::New)
| Selection::Keyword(Keyword::Show)
| Selection::Keyword(Keyword::NotificationSelection)
| Selection::Keyword(Keyword::ExternalNotifSelection)
| Selection::Keyword(Keyword::HashtagSelection)
| Selection::Keyword(Keyword::IndividualSelection)
| Selection::Keyword(Keyword::ExternalIndividualSelection)
| Selection::Keyword(Keyword::Edit) => None,
}
}
impl fmt::Display for Selection {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
impl TokenSerializable for CleanIntermediaryRoute {
fn serialize_tokens(&self, writer: &mut TokenWriter) {
match self {
Selection::Keyword(keyword) => write!(f, "{}", keyword),
Selection::Payload(payload) => write!(f, "{}", payload),
Selection::Algo(algo_kw) => write!(f, "{}", algo_kw),
Selection::List(list_kw) => write!(f, "{}", list_kw),
Selection::PubkeySource(pk_src_kw) => write!(f, "{}", pk_src_kw),
CleanIntermediaryRoute::ToTimeline(tlk) => {
tlk.serialize_tokens(writer);
}
CleanIntermediaryRoute::ToRoute(route) => {
route.serialize_tokens(writer);
}
}
}
fn parse_from_tokens<'a>(parser: &mut TokenParser<'a>) -> Result<Self, ParseError<'a>> {
TokenParser::alt(
parser,
&[
|p| {
Ok(CleanIntermediaryRoute::ToTimeline(
TimelineKind::parse_from_tokens(p)?,
))
},
|p| {
Ok(CleanIntermediaryRoute::ToRoute(Route::parse_from_tokens(
p,
)?))
},
],
)
}
}
#[cfg(test)]