Add // or # comments and simple sql query statements

This commit is contained in:
NGnius (Graham) 2021-12-12 14:59:43 -05:00
parent 7a327767f3
commit 03318a0ef5
23 changed files with 1067 additions and 293 deletions

View file

@ -4,7 +4,7 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
rusqlite = { version = "0.26.1" } rusqlite = { version = "0.26.3" }
symphonia = { version = "0.4.0", optional = true, features = [ symphonia = { version = "0.4.0", optional = true, features = [
"aac", "flac", "mp3", "pcm", "vorbis", "isomp4", "ogg", "wav" "aac", "flac", "mp3", "pcm", "vorbis", "isomp4", "ogg", "wav"
] } ] }

View file

@ -1,14 +1,15 @@
use std::fmt::{Debug, Display, Formatter, Error}; use super::processing::database::{MpsDatabaseQuerier, MpsSQLiteExecutor};
use std::fmt::{Debug, Display, Error, Formatter};
#[derive(Debug)] #[derive(Debug)]
pub struct MpsContext { pub struct MpsContext {
pub sqlite_connection: Option<rusqlite::Connection>, pub database: Box<dyn MpsDatabaseQuerier>,
} }
impl Default for MpsContext { impl Default for MpsContext {
fn default() -> Self { fn default() -> Self {
Self { Self {
sqlite_connection: None, // initialized by first SQL statement instead database: Box::new(MpsSQLiteExecutor::default()),
} }
} }
} }
@ -16,7 +17,7 @@ impl Default for MpsContext {
impl std::clone::Clone for MpsContext { impl std::clone::Clone for MpsContext {
fn clone(&self) -> Self { fn clone(&self) -> Self {
Self { Self {
sqlite_connection: None, database: Box::new(MpsSQLiteExecutor::default()),
} }
} }
} }

View file

@ -1,14 +1,17 @@
use std::iter::Iterator;
use std::collections::VecDeque; use std::collections::VecDeque;
use std::path::Path;
use std::fs::File; use std::fs::File;
use std::iter::Iterator;
use std::path::Path;
use super::MpsMusicItem; use super::lang::{MpsLanguageDictionary, MpsLanguageError, MpsOp};
use super::MpsContext;
use super::tokens::MpsToken; use super::tokens::MpsToken;
use super::lang::{MpsOp, MpsLanguageError, MpsLanguageDictionary}; use super::MpsContext;
use super::MpsMusicItem;
pub struct MpsInterpretor<T> where T: crate::tokens::MpsTokenReader { pub struct MpsInterpretor<T>
where
T: crate::tokens::MpsTokenReader,
{
tokenizer: T, tokenizer: T,
buffer: VecDeque<MpsToken>, buffer: VecDeque<MpsToken>,
current_stmt: Option<Box<dyn MpsOp>>, current_stmt: Option<Box<dyn MpsOp>>,
@ -22,7 +25,8 @@ pub fn interpretor<R: std::io::Read>(stream: R) -> MpsInterpretor<crate::tokens:
} }
impl<T> MpsInterpretor<T> impl<T> MpsInterpretor<T>
where T: crate::tokens::MpsTokenReader where
T: crate::tokens::MpsTokenReader,
{ {
pub fn with_vocab(tokenizer: T, vocab: MpsLanguageDictionary) -> Self { pub fn with_vocab(tokenizer: T, vocab: MpsLanguageDictionary) -> Self {
Self { Self {
@ -72,7 +76,8 @@ impl MpsInterpretor<crate::tokens::MpsTokenizer<File>> {
} }
impl<T> Iterator for MpsInterpretor<T> impl<T> Iterator for MpsInterpretor<T>
where T: crate::tokens::MpsTokenReader where
T: crate::tokens::MpsTokenReader,
{ {
type Item = Result<MpsMusicItem, Box<dyn MpsLanguageError>>; type Item = Result<MpsMusicItem, Box<dyn MpsLanguageError>>;
@ -84,21 +89,27 @@ impl<T> Iterator for MpsInterpretor<T>
is_stmt_done = true; is_stmt_done = true;
} }
match next_item { match next_item {
Some(item) => Some(item.map_err(|e| box_error_with_ctx( Some(item) => {
e, self.tokenizer.current_line() Some(item.map_err(|e| box_error_with_ctx(e, self.tokenizer.current_line())))
))), }
None => None None => None,
} }
} else { } else {
if self.tokenizer.end_of_file() { return None; } if self.tokenizer.end_of_file() {
return None;
}
// build new statement // build new statement
let token_result = self.tokenizer.next_statements(1, &mut self.buffer) let token_result = self
.tokenizer
.next_statement(&mut self.buffer)
.map_err(|e| box_error_with_ctx(e, self.tokenizer.current_line())); .map_err(|e| box_error_with_ctx(e, self.tokenizer.current_line()));
match token_result { match token_result {
Ok(_) => {}, Ok(_) => {}
Err(x) => return Some(Err(x)) Err(x) => return Some(Err(x)),
}
if self.tokenizer.end_of_file() && self.buffer.len() == 0 {
return None;
} }
if self.tokenizer.end_of_file() && self.buffer.len() == 0 { return None; }
let stmt = self.vocabulary.try_build_statement(&mut self.buffer); let stmt = self.vocabulary.try_build_statement(&mut self.buffer);
match stmt { match stmt {
Ok(mut stmt) => { Ok(mut stmt) => {
@ -109,17 +120,15 @@ impl<T> Iterator for MpsInterpretor<T>
is_stmt_done = true; is_stmt_done = true;
} }
match next_item { match next_item {
Some(item) => Some(item.map_err(|e| box_error_with_ctx( Some(item) => Some(
e, item.map_err(|e| box_error_with_ctx(e, self.tokenizer.current_line())),
self.tokenizer.current_line() ),
))), None => None,
None => None }
}
Err(e) => {
Some(Err(e).map_err(|e| box_error_with_ctx(e, self.tokenizer.current_line())))
} }
},
Err(e) => Some(Err(e).map_err(|e| box_error_with_ctx(
e,
self.tokenizer.current_line()
)))
} }
}; };
if is_stmt_done { if is_stmt_done {
@ -129,12 +138,17 @@ impl<T> Iterator for MpsInterpretor<T>
} }
} }
fn box_error_with_ctx<E: MpsLanguageError + 'static>(mut error: E, line: usize) -> Box<dyn MpsLanguageError> { fn box_error_with_ctx<E: MpsLanguageError + 'static>(
mut error: E,
line: usize,
) -> Box<dyn MpsLanguageError> {
error.set_line(line); error.set_line(line);
Box::new(error) as Box<dyn MpsLanguageError> Box::new(error) as Box<dyn MpsLanguageError>
} }
pub(crate) fn standard_vocab(vocabulary: &mut MpsLanguageDictionary) { pub(crate) fn standard_vocab(vocabulary: &mut MpsLanguageDictionary) {
vocabulary vocabulary
.add(crate::lang::vocabulary::SqlStatementFactory); .add(crate::lang::vocabulary::SqlStatementFactory)
.add(crate::lang::vocabulary::SimpleSqlStatementFactory)
.add(crate::lang::vocabulary::CommentStatementFactory);
} }

View file

@ -0,0 +1,91 @@
use std::collections::VecDeque;
use std::fmt::{Debug, Display, Error, Formatter};
use std::iter::Iterator;
use crate::MpsContext;
use crate::MpsMusicItem;
use crate::tokens::MpsToken;
use super::{RuntimeError, SyntaxError};
use super::{MpsOp, SimpleMpsOpFactory, MpsOpFactory, BoxedMpsOpFactory};
use super::MpsLanguageDictionary;
use super::utility::assert_token;
#[derive(Debug, Clone)]
pub struct CommentStatement {
comment: String,
context: Option<MpsContext>
}
impl CommentStatement {
/*fn comment_text(&self) -> String {
let mut clone = self.comment.clone();
if clone.starts_with("#") {
clone.replace_range(..1, ""); // remove "#"
} else {
clone.replace_range(..2, ""); // remove "//"
}
clone
}*/
}
impl Display for CommentStatement {
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
write!(f, "{}", self.comment)
}
}
impl Iterator for CommentStatement {
type Item = Result<MpsMusicItem, RuntimeError>;
fn next(&mut self) -> Option<Self::Item> {
None
}
}
impl MpsOp for CommentStatement {
fn enter(&mut self, ctx: MpsContext) {
self.context = Some(ctx)
}
fn escape(&mut self) -> MpsContext {
self.context.take().unwrap()
}
}
pub struct CommentStatementFactory;
impl SimpleMpsOpFactory<CommentStatement> for CommentStatementFactory {
fn is_op_simple(&self, tokens: &VecDeque<MpsToken>) -> bool {
tokens.len() == 1
&& tokens[0].is_comment()
}
fn build_op_simple(
&self,
tokens: &mut VecDeque<MpsToken>,
) -> Result<CommentStatement, SyntaxError> {
let comment = assert_token(|t| match t {
MpsToken::Comment(c) => Some(c),
_ => None
}, MpsToken::Comment("comment".into()), tokens)?;
Ok(CommentStatement {
comment: comment,
context: None
})
}
}
impl BoxedMpsOpFactory for CommentStatementFactory {
fn build_op_boxed(
&self,
tokens: &mut VecDeque<MpsToken>,
dict: &MpsLanguageDictionary,
) -> Result<Box<dyn MpsOp>, SyntaxError> {
self.build_box(tokens, dict)
}
fn is_op_boxed(&self, tokens: &VecDeque<MpsToken>) -> bool {
self.is_op(tokens)
}
}

View file

@ -13,7 +13,9 @@ pub fn generate_default_db() -> rusqlite::Result<rusqlite::Connection> {
let conn = rusqlite::Connection::open(DEFAULT_SQLITE_FILEPATH)?; let conn = rusqlite::Connection::open(DEFAULT_SQLITE_FILEPATH)?;
// skip db building if SQLite file already exists // skip db building if SQLite file already exists
// TODO do a more exhaustive db check to make sure it's actually the correct file // TODO do a more exhaustive db check to make sure it's actually the correct file
if db_exists {return Ok(conn);} if db_exists {
return Ok(conn);
}
// build db tables // build db tables
conn.execute_batch( conn.execute_batch(
"BEGIN; "BEGIN;
@ -50,7 +52,7 @@ pub fn generate_default_db() -> rusqlite::Result<rusqlite::Connection> {
genre_id INTEGER NOT NULL PRIMARY KEY, genre_id INTEGER NOT NULL PRIMARY KEY,
title TEXT title TEXT
); );
COMMIT;" COMMIT;",
)?; )?;
// generate data and store in db // generate data and store in db
#[cfg(feature = "music_library")] #[cfg(feature = "music_library")]
@ -67,7 +69,7 @@ pub fn generate_default_db() -> rusqlite::Result<rusqlite::Connection> {
filename, filename,
metadata, metadata,
genre genre
) VALUES (?, ?, ?, ?, ?, ?, ?)" ) VALUES (?, ?, ?, ?, ?, ?, ?)",
)?; )?;
for song in lib.all_songs() { for song in lib.all_songs() {
song_insert.execute(song.to_params().as_slice())?; song_insert.execute(song.to_params().as_slice())?;
@ -81,7 +83,7 @@ pub fn generate_default_db() -> rusqlite::Result<rusqlite::Connection> {
disc, disc,
duration, duration,
date date
) VALUES (?, ?, ?, ?, ?, ?)" ) VALUES (?, ?, ?, ?, ?, ?)",
)?; )?;
for meta in lib.all_metadata() { for meta in lib.all_metadata() {
metadata_insert.execute(meta.to_params().as_slice())?; metadata_insert.execute(meta.to_params().as_slice())?;
@ -92,7 +94,7 @@ pub fn generate_default_db() -> rusqlite::Result<rusqlite::Connection> {
artist_id, artist_id,
name, name,
genre genre
) VALUES (?, ?, ?)" ) VALUES (?, ?, ?)",
)?; )?;
for artist in lib.all_artists() { for artist in lib.all_artists() {
artist_insert.execute(artist.to_params().as_slice())?; artist_insert.execute(artist.to_params().as_slice())?;
@ -105,7 +107,7 @@ pub fn generate_default_db() -> rusqlite::Result<rusqlite::Connection> {
metadata, metadata,
artist, artist,
genre genre
) VALUES (?, ?, ?, ?, ?)" ) VALUES (?, ?, ?, ?, ?)",
)?; )?;
for album in lib.all_albums() { for album in lib.all_albums() {
album_insert.execute(album.to_params().as_slice())?; album_insert.execute(album.to_params().as_slice())?;
@ -115,13 +117,13 @@ pub fn generate_default_db() -> rusqlite::Result<rusqlite::Connection> {
"INSERT OR REPLACE INTO genres ( "INSERT OR REPLACE INTO genres (
genre_id, genre_id,
title title
) VALUES (?, ?)" ) VALUES (?, ?)",
)?; )?;
for genre in lib.all_genres() { for genre in lib.all_genres() {
genre_insert.execute(genre.to_params().as_slice())?; genre_insert.execute(genre.to_params().as_slice())?;
} }
}, }
Err(e) => println!("Unable to load music from {}: {}", music_path.display(), e) Err(e) => println!("Unable to load music from {}: {}", music_path.display(), e),
} }
} }
Ok(conn) Ok(conn)
@ -140,7 +142,7 @@ pub struct DbMusicItem {
impl DatabaseObj for DbMusicItem { impl DatabaseObj for DbMusicItem {
fn map_row(row: &rusqlite::Row) -> rusqlite::Result<Self> { fn map_row(row: &rusqlite::Row) -> rusqlite::Result<Self> {
Ok(Self{ Ok(Self {
song_id: row.get(0)?, song_id: row.get(0)?,
title: row.get(1)?, title: row.get(1)?,
artist: row.get(2)?, artist: row.get(2)?,
@ -163,7 +165,9 @@ impl DatabaseObj for DbMusicItem {
] ]
} }
fn id(&self) -> u64 {self.song_id} fn id(&self) -> u64 {
self.song_id
}
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -178,7 +182,7 @@ pub struct DbMetaItem {
impl DatabaseObj for DbMetaItem { impl DatabaseObj for DbMetaItem {
fn map_row(row: &rusqlite::Row) -> rusqlite::Result<Self> { fn map_row(row: &rusqlite::Row) -> rusqlite::Result<Self> {
Ok(Self{ Ok(Self {
meta_id: row.get(0)?, meta_id: row.get(0)?,
plays: row.get(1)?, plays: row.get(1)?,
track: row.get(2)?, track: row.get(2)?,
@ -199,7 +203,9 @@ impl DatabaseObj for DbMetaItem {
] ]
} }
fn id(&self) -> u64 {self.meta_id} fn id(&self) -> u64 {
self.meta_id
}
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -211,7 +217,7 @@ pub struct DbArtistItem {
impl DatabaseObj for DbArtistItem { impl DatabaseObj for DbArtistItem {
fn map_row(row: &rusqlite::Row) -> rusqlite::Result<Self> { fn map_row(row: &rusqlite::Row) -> rusqlite::Result<Self> {
Ok(Self{ Ok(Self {
artist_id: row.get(0)?, artist_id: row.get(0)?,
name: row.get(1)?, name: row.get(1)?,
genre: row.get(2)?, genre: row.get(2)?,
@ -219,14 +225,12 @@ impl DatabaseObj for DbArtistItem {
} }
fn to_params(&self) -> Vec<&dyn rusqlite::ToSql> { fn to_params(&self) -> Vec<&dyn rusqlite::ToSql> {
vec![ vec![&self.artist_id, &self.name, &self.genre]
&self.artist_id,
&self.name,
&self.genre,
]
} }
fn id(&self) -> u64 {self.artist_id} fn id(&self) -> u64 {
self.artist_id
}
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -240,7 +244,7 @@ pub struct DbAlbumItem {
impl DatabaseObj for DbAlbumItem { impl DatabaseObj for DbAlbumItem {
fn map_row(row: &rusqlite::Row) -> rusqlite::Result<Self> { fn map_row(row: &rusqlite::Row) -> rusqlite::Result<Self> {
Ok(Self{ Ok(Self {
album_id: row.get(0)?, album_id: row.get(0)?,
title: row.get(1)?, title: row.get(1)?,
metadata: row.get(2)?, metadata: row.get(2)?,
@ -259,7 +263,9 @@ impl DatabaseObj for DbAlbumItem {
] ]
} }
fn id(&self) -> u64 {self.album_id} fn id(&self) -> u64 {
self.album_id
}
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -270,18 +276,17 @@ pub struct DbGenreItem {
impl DatabaseObj for DbGenreItem { impl DatabaseObj for DbGenreItem {
fn map_row(row: &rusqlite::Row) -> rusqlite::Result<Self> { fn map_row(row: &rusqlite::Row) -> rusqlite::Result<Self> {
Ok(Self{ Ok(Self {
genre_id: row.get(0)?, genre_id: row.get(0)?,
title: row.get(1)?, title: row.get(1)?,
}) })
} }
fn to_params(&self) -> Vec<&dyn rusqlite::ToSql> { fn to_params(&self) -> Vec<&dyn rusqlite::ToSql> {
vec![ vec![&self.genre_id, &self.title]
&self.genre_id,
&self.title,
]
} }
fn id(&self) -> u64 {self.genre_id} fn id(&self) -> u64 {
self.genre_id
}
} }

View file

@ -1,28 +1,32 @@
use std::collections::VecDeque; use std::collections::VecDeque;
use crate::tokens::MpsToken;
use super::{BoxedMpsOpFactory, MpsOp};
use super::SyntaxError; use super::SyntaxError;
use super::{BoxedMpsOpFactory, MpsOp};
use crate::tokens::MpsToken;
pub struct MpsLanguageDictionary { pub struct MpsLanguageDictionary {
vocabulary: Vec<Box<dyn BoxedMpsOpFactory>> vocabulary: Vec<Box<dyn BoxedMpsOpFactory>>,
} }
impl MpsLanguageDictionary { impl MpsLanguageDictionary {
pub fn add<T: BoxedMpsOpFactory + 'static>(&mut self, factory: T) -> &mut Self { pub fn add<T: BoxedMpsOpFactory + 'static>(&mut self, factory: T) -> &mut Self {
self.vocabulary.push(Box::new(factory) as Box<dyn BoxedMpsOpFactory>); self.vocabulary
.push(Box::new(factory) as Box<dyn BoxedMpsOpFactory>);
self self
} }
pub fn try_build_statement(&self, tokens: &mut VecDeque<MpsToken>) -> Result<Box<dyn MpsOp>, SyntaxError> { pub fn try_build_statement(
&self,
tokens: &mut VecDeque<MpsToken>,
) -> Result<Box<dyn MpsOp>, SyntaxError> {
for factory in &self.vocabulary { for factory in &self.vocabulary {
if factory.is_op_boxed(tokens) { if factory.is_op_boxed(tokens) {
return factory.build_op_boxed(tokens); return factory.build_op_boxed(tokens, self);
} }
} }
Err(SyntaxError { Err(SyntaxError {
line: 0, line: 0,
token: tokens.pop_front().unwrap() token: tokens.pop_front().unwrap(),
}) })
} }

View file

@ -1,7 +1,7 @@
use std::fmt::{Debug, Display, Formatter, Error}; use std::fmt::{Debug, Display, Error, Formatter};
use crate::tokens::MpsToken;
use super::MpsOp; use super::MpsOp;
use crate::tokens::MpsToken;
#[derive(Debug)] #[derive(Debug)]
pub struct SyntaxError { pub struct SyntaxError {
@ -11,12 +11,18 @@ pub struct SyntaxError {
impl Display for SyntaxError { impl Display for SyntaxError {
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
write!(f, "SyntaxError (line {}): Unexpected {}", &self.line, &self.token) write!(
f,
"SyntaxError (line {}): Unexpected {}",
&self.line, &self.token
)
} }
} }
impl MpsLanguageError for SyntaxError { impl MpsLanguageError for SyntaxError {
fn set_line(&mut self, line: usize) {self.line = line} fn set_line(&mut self, line: usize) {
self.line = line
}
} }
#[derive(Debug)] #[derive(Debug)]
@ -33,7 +39,9 @@ impl Display for RuntimeError {
} }
impl MpsLanguageError for RuntimeError { impl MpsLanguageError for RuntimeError {
fn set_line(&mut self, line: usize) {self.line = line} fn set_line(&mut self, line: usize) {
self.line = line
}
} }
pub trait MpsLanguageError: Display + Debug { pub trait MpsLanguageError: Display + Debug {

View file

@ -1,22 +1,29 @@
mod comment;
mod db_items; mod db_items;
mod dictionary; mod dictionary;
mod error; mod error;
mod operation; mod operation;
mod sql_query; mod sql_query;
mod sql_simple_query;
//mod statement; //mod statement;
pub(crate) mod utility; pub(crate) mod utility;
pub use dictionary::MpsLanguageDictionary; pub use dictionary::MpsLanguageDictionary;
pub use error::{SyntaxError, RuntimeError, MpsLanguageError}; pub use error::{MpsLanguageError, RuntimeError, SyntaxError};
pub use operation::{MpsOp, MpsOpFactory, BoxedMpsOpFactory}; pub use operation::{BoxedMpsOpFactory, MpsOp, MpsOpFactory, SimpleMpsOpFactory};
//pub(crate) use statement::MpsStatement; //pub(crate) use statement::MpsStatement;
pub mod vocabulary { pub mod vocabulary {
pub use super::sql_query::{SqlStatement, SqlStatementFactory}; pub use super::sql_query::{SqlStatement, SqlStatementFactory};
pub use super::sql_simple_query::{SimpleSqlStatement, SimpleSqlStatementFactory};
pub use super::comment::{CommentStatement, CommentStatementFactory};
} }
pub mod db { pub mod db {
pub use super::db_items::{DEFAULT_SQLITE_FILEPATH, generate_default_db, DatabaseObj, DbMusicItem, DbAlbumItem, DbArtistItem, DbMetaItem, DbGenreItem}; pub use super::db_items::{
generate_default_db, DatabaseObj, DbAlbumItem, DbArtistItem, DbGenreItem, DbMetaItem,
DbMusicItem, DEFAULT_SQLITE_FILEPATH,
};
} }
#[cfg(test)] #[cfg(test)]

View file

@ -1,30 +1,66 @@
use std::iter::Iterator;
use std::collections::VecDeque; use std::collections::VecDeque;
use std::fmt::{Debug, Display}; use std::fmt::{Debug, Display};
use std::iter::Iterator;
use crate::MpsMusicItem; use super::MpsLanguageDictionary;
use crate::MpsContext; use super::{RuntimeError, SyntaxError};
use crate::tokens::MpsToken; use crate::tokens::MpsToken;
use super::{SyntaxError, RuntimeError}; use crate::MpsContext;
use crate::MpsMusicItem;
pub trait SimpleMpsOpFactory<T: MpsOp + 'static> {
fn is_op_simple(&self, tokens: &VecDeque<MpsToken>) -> bool;
fn build_op_simple(
&self,
tokens: &mut VecDeque<MpsToken>,
) -> Result<T, SyntaxError>;
}
impl<T: MpsOp + 'static, X: SimpleMpsOpFactory<T> + 'static> MpsOpFactory<T> for X {
fn is_op(&self, tokens: &VecDeque<MpsToken>) -> bool {
self.is_op_simple(tokens)
}
fn build_op(
&self,
tokens: &mut VecDeque<MpsToken>,
_dict: &MpsLanguageDictionary,
) -> Result<T, SyntaxError> {
self.build_op_simple(tokens)
}
}
pub trait MpsOpFactory<T: MpsOp + 'static> { pub trait MpsOpFactory<T: MpsOp + 'static> {
fn is_op(&self, tokens: &VecDeque<MpsToken>) -> bool; fn is_op(&self, tokens: &VecDeque<MpsToken>) -> bool;
fn build_op(&self, tokens: &mut VecDeque<MpsToken>) -> Result<T, SyntaxError>; fn build_op(
&self,
tokens: &mut VecDeque<MpsToken>,
dict: &MpsLanguageDictionary,
) -> Result<T, SyntaxError>;
#[inline] #[inline]
fn build_box(&self, tokens: &mut VecDeque<MpsToken>) -> Result<Box<dyn MpsOp>, SyntaxError> { fn build_box(
Ok(Box::new(self.build_op(tokens)?)) &self,
tokens: &mut VecDeque<MpsToken>,
dict: &MpsLanguageDictionary,
) -> Result<Box<dyn MpsOp>, SyntaxError> {
Ok(Box::new(self.build_op(tokens, dict)?))
} }
} }
pub trait BoxedMpsOpFactory { pub trait BoxedMpsOpFactory {
fn build_op_boxed(&self, tokens: &mut VecDeque<MpsToken>) -> Result<Box<dyn MpsOp>, SyntaxError>; fn build_op_boxed(
&self,
tokens: &mut VecDeque<MpsToken>,
dict: &MpsLanguageDictionary,
) -> Result<Box<dyn MpsOp>, SyntaxError>;
fn is_op_boxed(&self, tokens: &VecDeque<MpsToken>) -> bool; fn is_op_boxed(&self, tokens: &VecDeque<MpsToken>) -> bool;
} }
pub trait MpsOp: Iterator<Item=Result<MpsMusicItem, RuntimeError>> + Debug + Display { pub trait MpsOp: Iterator<Item = Result<MpsMusicItem, RuntimeError>> + Debug + Display {
fn enter(&mut self, ctx: MpsContext); fn enter(&mut self, ctx: MpsContext);
fn escape(&mut self) -> MpsContext; fn escape(&mut self) -> MpsContext;

View file

@ -1,42 +1,43 @@
use std::iter::Iterator;
use std::collections::VecDeque; use std::collections::VecDeque;
use std::fmt::{Debug, Display, Formatter, Error}; use std::fmt::{Debug, Display, Error, Formatter};
use std::iter::Iterator;
use super::utility::{assert_token, assert_token_raw};
use super::{BoxedMpsOpFactory, MpsLanguageDictionary, MpsOp, MpsOpFactory};
use super::{RuntimeError, SyntaxError};
use crate::tokens::MpsToken;
use crate::MpsContext; use crate::MpsContext;
use crate::MpsMusicItem; use crate::MpsMusicItem;
use crate::tokens::MpsToken; //use super::db::*;
use super::{MpsOp, MpsOpFactory, BoxedMpsOpFactory};
use super::{SyntaxError, RuntimeError};
use super::utility::{assert_token, assert_token_raw};
use super::db::*;
#[derive(Debug)] #[derive(Debug)]
pub struct SqlStatement { pub struct SqlStatement {
query: String, query: String,
context: Option<MpsContext>, context: Option<MpsContext>,
rows: Option<Vec<rusqlite::Result<MpsMusicItem>>>, rows: Option<Vec<Result<MpsMusicItem, RuntimeError>>>,
current: usize, current: usize,
} }
impl SqlStatement { impl SqlStatement {
fn map_item(&mut self, increment: bool) -> Option<Result<MpsMusicItem, RuntimeError>> { fn get_item(&mut self, increment: bool) -> Option<Result<MpsMusicItem, RuntimeError>> {
if let Some(rows) = &self.rows { if let Some(rows) = &self.rows {
if increment { if increment {
if self.current == rows.len() { if self.current == rows.len() {
return None return None;
} }
self.current += 1; self.current += 1;
} }
if self.current >= rows.len() { if self.current >= rows.len() {
None None
} else { } else {
//Some(rows[self.current].clone())
match &rows[self.current] { match &rows[self.current] {
Ok(item) => Some(Ok(item.clone())), Ok(item) => Some(Ok(item.clone())),
Err(e) => Some(Err(RuntimeError { Err(e) => Some(Err(RuntimeError {
line: 0, line: e.line,
op: Box::new(self.clone()), op: Box::new(self.clone()),
msg: format!("SQL music item mapping error: {}", e).into(), msg: e.msg.clone(),
})) })),
} }
} }
} else { } else {
@ -46,12 +47,11 @@ impl SqlStatement {
msg: format!("Context error: rows is None").into(), msg: format!("Context error: rows is None").into(),
})) }))
} }
} }
} }
impl MpsOp for SqlStatement { impl MpsOp for SqlStatement {
fn enter(&mut self, ctx: MpsContext){ fn enter(&mut self, ctx: MpsContext) {
self.context = Some(ctx) self.context = Some(ctx)
} }
@ -64,8 +64,8 @@ impl std::clone::Clone for SqlStatement {
fn clone(&self) -> Self { fn clone(&self) -> Self {
Self { Self {
query: self.query.clone(), query: self.query.clone(),
context: self.context.clone(), context: None, // unecessary to include in clone (not used for displaying)
rows: None, // TODO use different Result type so this is cloneable rows: None, // unecessary to include
current: self.current, current: self.current,
} }
} }
@ -77,34 +77,21 @@ impl Iterator for SqlStatement {
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
if self.rows.is_some() { if self.rows.is_some() {
// query has executed, return another result // query has executed, return another result
self.map_item(true) self.get_item(true)
} else { } else {
let self_clone = self.clone();
let ctx = self.context.as_mut().unwrap(); let ctx = self.context.as_mut().unwrap();
// query has not been executed yet // query has not been executed yet
if let None = ctx.sqlite_connection { match ctx
// connection needs to be created .database
match generate_default_db() { .raw(&self.query, &mut move || Box::new(self_clone.clone()))
Ok(conn) => ctx.sqlite_connection = Some(conn), {
Err(e) => return Some(Err(RuntimeError{ Err(e) => return Some(Err(e)),
line: 0, Ok(rows) => {
op: Box::new(self.clone()), self.rows = Some(rows);
msg: format!("SQL connection error: {}", e).into() self.get_item(false)
}))
} }
} }
let conn = ctx.sqlite_connection.as_mut().unwrap();
// execute query
match perform_query(conn, &self.query) {
Ok(items) => {
self.rows = Some(items);
self.map_item(false)
}
Err(e) => Some(Err(RuntimeError{
line: 0,
op: Box::new(self.clone()),
msg: e
}))
}
} }
} }
} }
@ -120,20 +107,30 @@ pub struct SqlStatementFactory;
impl MpsOpFactory<SqlStatement> for SqlStatementFactory { impl MpsOpFactory<SqlStatement> for SqlStatementFactory {
#[inline] #[inline]
fn is_op(&self, tokens: &VecDeque<MpsToken>) -> bool { fn is_op(&self, tokens: &VecDeque<MpsToken>) -> bool {
tokens.len() > 3 && tokens[0].is_sql() tokens.len() > 3
&& tokens[0].is_sql()
&& tokens[1].is_open_bracket()
&& tokens[2].is_literal()
&& tokens[3].is_close_bracket()
} }
#[inline] #[inline]
fn build_op(&self, tokens: &mut VecDeque<MpsToken>) -> Result<SqlStatement, SyntaxError> { fn build_op(
&self,
tokens: &mut VecDeque<MpsToken>,
_dict: &MpsLanguageDictionary,
) -> Result<SqlStatement, SyntaxError> {
// sql ( `some query` ) // sql ( `some query` )
assert_token_raw(MpsToken::Sql, tokens)?; assert_token_raw(MpsToken::Sql, tokens)?;
assert_token_raw(MpsToken::OpenBracket, tokens)?; assert_token_raw(MpsToken::OpenBracket, tokens)?;
let literal = assert_token(|t| { let literal = assert_token(
match t { |t| match t {
MpsToken::Literal(query) => Some(query), MpsToken::Literal(query) => Some(query),
_ => None _ => None,
} },
}, MpsToken::Literal("".into()), tokens)?; MpsToken::Literal("".into()),
tokens,
)?;
assert_token_raw(MpsToken::CloseBracket, tokens)?; assert_token_raw(MpsToken::CloseBracket, tokens)?;
Ok(SqlStatement { Ok(SqlStatement {
query: literal, query: literal,
@ -145,22 +142,15 @@ impl MpsOpFactory<SqlStatement> for SqlStatementFactory {
} }
impl BoxedMpsOpFactory for SqlStatementFactory { impl BoxedMpsOpFactory for SqlStatementFactory {
fn build_op_boxed(&self, tokens: &mut VecDeque<MpsToken>) -> Result<Box<dyn MpsOp>, SyntaxError> { fn build_op_boxed(
self.build_box(tokens) &self,
tokens: &mut VecDeque<MpsToken>,
dict: &MpsLanguageDictionary,
) -> Result<Box<dyn MpsOp>, SyntaxError> {
self.build_box(tokens, dict)
} }
fn is_op_boxed(&self, tokens: &VecDeque<MpsToken>) -> bool { fn is_op_boxed(&self, tokens: &VecDeque<MpsToken>) -> bool {
self.is_op(tokens) self.is_op(tokens)
} }
} }
fn perform_query(
conn: &mut rusqlite::Connection,
query: &str
) -> Result<Vec<rusqlite::Result<MpsMusicItem>>, String> {
let mut stmt = conn.prepare(query)
.map_err(|e| format!("SQLite query error: {}", e))?;
let iter = stmt.query_map([], MpsMusicItem::map_row)
.map_err(|e| format!("SQLite item mapping error: {}", e))?;
Ok(iter.collect())
}

View file

@ -0,0 +1,233 @@
use std::collections::VecDeque;
use std::fmt::{Debug, Display, Error, Formatter};
use std::iter::Iterator;
use super::utility::{assert_token, assert_token_raw};
use super::{BoxedMpsOpFactory, MpsLanguageDictionary, MpsOp, MpsOpFactory};
use super::{RuntimeError, SyntaxError};
use crate::tokens::MpsToken;
use crate::MpsContext;
use crate::MpsMusicItem;
#[derive(Debug, Clone)]
enum QueryMode {
Artist,
Album,
Song,
Genre,
}
impl QueryMode {
fn from_name(name: String) -> Result<Self, SyntaxError> {
match &name as &str {
"artist" => Ok(QueryMode::Artist),
"album" => Ok(QueryMode::Album),
"song" => Ok(QueryMode::Song),
"genre" => Ok(QueryMode::Genre),
_ => Err(SyntaxError {
line: 0,
token: Self::tokenify(name),
}),
}
}
fn is_valid_name(name: &str) -> bool {
match name {
"artist" | "album" | "song" | "genre" => true,
_ => false,
}
}
#[inline]
fn tokenify(name: String) -> MpsToken {
MpsToken::Name(name)
}
#[inline]
fn tokenify_self(&self) -> MpsToken {
MpsToken::Name(
match self {
Self::Artist => "artist",
Self::Album => "album",
Self::Song => "song",
Self::Genre => "genre",
}
.into(),
)
}
}
#[derive(Debug)]
pub struct SimpleSqlStatement {
query: String,
mode: QueryMode,
context: Option<MpsContext>,
rows: Option<Vec<Result<MpsMusicItem, RuntimeError>>>,
current: usize,
}
impl SimpleSqlStatement {
fn get_item(&mut self, increment: bool) -> Option<Result<MpsMusicItem, RuntimeError>> {
if let Some(rows) = &self.rows {
if increment {
if self.current == rows.len() {
return None;
}
self.current += 1;
}
if self.current >= rows.len() {
None
} else {
//Some(rows[self.current].clone())
match &rows[self.current] {
Ok(item) => Some(Ok(item.clone())),
Err(e) => Some(Err(RuntimeError {
line: e.line,
op: Box::new(self.clone()),
msg: e.msg.clone(),
})),
}
}
} else {
Some(Err(RuntimeError {
line: 0,
op: Box::new(self.clone()),
msg: format!("Context error: rows is None").into(),
}))
}
}
}
impl MpsOp for SimpleSqlStatement {
fn enter(&mut self, ctx: MpsContext) {
self.context = Some(ctx)
}
fn escape(&mut self) -> MpsContext {
self.context.take().unwrap()
}
}
impl std::clone::Clone for SimpleSqlStatement {
fn clone(&self) -> Self {
Self {
query: self.query.clone(),
mode: self.mode.clone(),
context: None, // unecessary to include in clone (not used for displaying)
rows: None, // unecessary to include
current: self.current,
}
}
}
impl Iterator for SimpleSqlStatement {
type Item = Result<MpsMusicItem, RuntimeError>;
fn next(&mut self) -> Option<Self::Item> {
if self.rows.is_some() {
// query has executed, return another result
self.get_item(true)
} else {
let self_clone = self.clone();
let ctx = self.context.as_mut().unwrap();
// query has not been executed yet
let query_result = match self.mode {
QueryMode::Artist => ctx
.database
.artist_like(&self.query, &mut move || Box::new(self_clone.clone())),
QueryMode::Album => ctx
.database
.album_like(&self.query, &mut move || Box::new(self_clone.clone())),
QueryMode::Song => ctx
.database
.song_like(&self.query, &mut move || Box::new(self_clone.clone())),
QueryMode::Genre => ctx
.database
.genre_like(&self.query, &mut move || Box::new(self_clone.clone())),
};
match query_result {
Err(e) => return Some(Err(e)),
Ok(rows) => {
self.rows = Some(rows);
self.get_item(false)
}
}
}
}
}
impl Display for SimpleSqlStatement {
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
write!(f, "{}(`{}`)", self.mode.tokenify_self(), &self.query)
}
}
pub struct SimpleSqlStatementFactory;
impl MpsOpFactory<SimpleSqlStatement> for SimpleSqlStatementFactory {
#[inline]
fn is_op(&self, tokens: &VecDeque<MpsToken>) -> bool {
tokens.len() > 3
&& match &tokens[0] {
MpsToken::Name(name) => QueryMode::is_valid_name(name),
_ => false,
}
&& tokens[1].is_open_bracket()
&& tokens[2].is_literal()
&& tokens[3].is_close_bracket()
}
#[inline]
fn build_op(
&self,
tokens: &mut VecDeque<MpsToken>,
_dict: &MpsLanguageDictionary,
) -> Result<SimpleSqlStatement, SyntaxError> {
// artist|album|song|genre ( `like` )
let mode_name = assert_token(
|t| match t {
MpsToken::Name(name) => {
if QueryMode::is_valid_name(&name) {
Some(name)
} else {
None
}
}
_ => None,
},
MpsToken::Name("artist|album|song|genre".into()),
tokens,
)?;
assert_token_raw(MpsToken::OpenBracket, tokens)?;
let literal = assert_token(
|t| match t {
MpsToken::Literal(query) => Some(query),
_ => None,
},
MpsToken::Literal("literal".into()),
tokens,
)?;
assert_token_raw(MpsToken::CloseBracket, tokens)?;
Ok(SimpleSqlStatement {
query: literal,
mode: QueryMode::from_name(mode_name)?,
context: None,
current: 0,
rows: None,
})
}
}
impl BoxedMpsOpFactory for SimpleSqlStatementFactory {
fn build_op_boxed(
&self,
tokens: &mut VecDeque<MpsToken>,
dict: &MpsLanguageDictionary,
) -> Result<Box<dyn MpsOp>, SyntaxError> {
self.build_box(tokens, dict)
}
fn is_op_boxed(&self, tokens: &VecDeque<MpsToken>) -> bool {
self.is_op(tokens)
}
}

View file

@ -1,16 +1,17 @@
mod context; mod context;
mod interpretor; mod interpretor;
mod runner;
mod music_item;
pub mod lang; pub mod lang;
#[cfg(feature = "music_library")] #[cfg(feature = "music_library")]
pub mod music; pub mod music;
mod music_item;
pub mod processing;
mod runner;
pub mod tokens; pub mod tokens;
pub use context::MpsContext; pub use context::MpsContext;
pub use interpretor::{MpsInterpretor, interpretor}; pub use interpretor::{interpretor, MpsInterpretor};
pub use runner::MpsRunner;
pub use music_item::MpsMusicItem; pub use music_item::MpsMusicItem;
pub use runner::MpsRunner;
#[cfg(test)] #[cfg(test)]
mod tests {} mod tests {}

View file

@ -4,8 +4,8 @@ use std::path::Path;
use symphonia::core::io::MediaSourceStream; use symphonia::core::io::MediaSourceStream;
use symphonia::core::probe::Hint; use symphonia::core::probe::Hint;
use crate::lang::db::*;
use super::tag::Tags; use super::tag::Tags;
use crate::lang::db::*;
#[derive(Clone, Default)] #[derive(Clone, Default)]
pub struct MpsLibrary { pub struct MpsLibrary {
@ -55,7 +55,7 @@ impl MpsLibrary {
let path = path.as_ref(); let path = path.as_ref();
if path.is_dir() && depth != 0 { if path.is_dir() && depth != 0 {
for entry in path.read_dir()? { for entry in path.read_dir()? {
self.read_path(entry?.path(), depth-1)?; self.read_path(entry?.path(), depth - 1)?;
} }
} else if path.is_file() { } else if path.is_file() {
self.read_file(path)?; self.read_file(path)?;
@ -72,7 +72,7 @@ impl MpsLibrary {
&Hint::new(), &Hint::new(),
mss, mss,
&Default::default(), &Default::default(),
&Default::default() &Default::default(),
); );
// process audio file, ignoring any processing errors (skip file on error) // process audio file, ignoring any processing errors (skip file on error)
if let Ok(mut probed) = probed { if let Ok(mut probed) = probed {
@ -99,7 +99,9 @@ impl MpsLibrary {
/// generate data structures and links /// generate data structures and links
fn generate_entries(&mut self, tags: &Tags) { fn generate_entries(&mut self, tags: &Tags) {
if tags.len() == 0 { return; } // probably not a valid song, let's skip it if tags.len() == 0 {
return;
} // probably not a valid song, let's skip it
let song_id = self.songs.len() as u64; // guaranteed to be created let song_id = self.songs.len() as u64; // guaranteed to be created
let meta_id = self.metadata.len() as u64; // guaranteed to be created let meta_id = self.metadata.len() as u64; // guaranteed to be created
self.metadata.insert(meta_id, tags.meta(meta_id)); // definitely necessary self.metadata.insert(meta_id, tags.meta(meta_id)); // definitely necessary
@ -107,19 +109,22 @@ impl MpsLibrary {
let mut genre = tags.genre(0); let mut genre = tags.genre(0);
genre.genre_id = Self::find_or_gen_id(&self.genres, &genre.title); genre.genre_id = Self::find_or_gen_id(&self.genres, &genre.title);
if genre.genre_id == self.genres.len() as u64 { if genre.genre_id == self.genres.len() as u64 {
self.genres.insert(Self::sanitise_key(&genre.title), genre.clone()); self.genres
.insert(Self::sanitise_key(&genre.title), genre.clone());
} }
// artist only links to genre, so that can be next // artist only links to genre, so that can be next
let mut artist = tags.artist(0, genre.genre_id); let mut artist = tags.artist(0, genre.genre_id);
artist.artist_id = Self::find_or_gen_id(&self.artists, &artist.name); artist.artist_id = Self::find_or_gen_id(&self.artists, &artist.name);
if artist.artist_id == self.artists.len() as u64 { if artist.artist_id == self.artists.len() as u64 {
self.artists.insert(Self::sanitise_key(&artist.name), artist.clone()); self.artists
.insert(Self::sanitise_key(&artist.name), artist.clone());
} }
// same with album artist // same with album artist
let mut album_artist = tags.album_artist(0, genre.genre_id); let mut album_artist = tags.album_artist(0, genre.genre_id);
album_artist.artist_id = Self::find_or_gen_id(&self.artists, &album_artist.name); album_artist.artist_id = Self::find_or_gen_id(&self.artists, &album_artist.name);
if album_artist.artist_id == self.artists.len() as u64 { if album_artist.artist_id == self.artists.len() as u64 {
self.artists.insert(Self::sanitise_key(&album_artist.name), album_artist.clone()); self.artists
.insert(Self::sanitise_key(&album_artist.name), album_artist.clone());
} }
// album now has all links ready // album now has all links ready
let mut album = tags.album(0, 0, album_artist.artist_id, genre.genre_id); let mut album = tags.album(0, 0, album_artist.artist_id, genre.genre_id);
@ -127,12 +132,22 @@ impl MpsLibrary {
if album.album_id == self.albums.len() as u64 { if album.album_id == self.albums.len() as u64 {
let album_meta = tags.album_meta(self.metadata.len() as u64); let album_meta = tags.album_meta(self.metadata.len() as u64);
album.metadata = album_meta.meta_id; album.metadata = album_meta.meta_id;
self.albums.insert(Self::sanitise_key(&album.title), album.clone()); self.albums
.insert(Self::sanitise_key(&album.title), album.clone());
self.metadata.insert(album_meta.meta_id, album_meta); self.metadata.insert(album_meta.meta_id, album_meta);
} }
//let meta_album_id = self.metadata.len() as u64; //let meta_album_id = self.metadata.len() as u64;
//let album = tags.album(album_id, meta_album_id); //let album = tags.album(album_id, meta_album_id);
self.songs.insert(song_id, tags.song(song_id, artist.artist_id, Some(album.album_id), meta_id, genre.genre_id)); self.songs.insert(
song_id,
tags.song(
song_id,
artist.artist_id,
Some(album.album_id),
meta_id,
genre.genre_id,
),
);
} }
#[inline] #[inline]

View file

@ -24,20 +24,37 @@ impl Tags {
} }
} }
pub fn len(&self) -> usize {self.data.len()} pub fn len(&self) -> usize {
self.data.len()
}
pub fn song(&self, id: u64, artist_id: u64, album_id: Option<u64>, meta_id: u64, genre_id: u64) -> DbMusicItem { pub fn song(
&self,
id: u64,
artist_id: u64,
album_id: Option<u64>,
meta_id: u64,
genre_id: u64,
) -> DbMusicItem {
let default_title = || { let default_title = || {
let extension = self.filename.extension().and_then(|ext| ext.to_str()).unwrap_or(""); let extension = self
self.filename.file_name() .filename
.extension()
.and_then(|ext| ext.to_str())
.unwrap_or("");
self.filename
.file_name()
.and_then(|file| file.to_str()) .and_then(|file| file.to_str())
.and_then(|file| Some(file.replacen(extension, "", 1))) .and_then(|file| Some(file.replacen(extension, "", 1)))
.unwrap_or("Unknown Title".into()) .unwrap_or("Unknown Title".into())
}; };
DbMusicItem { DbMusicItem {
song_id: id, song_id: id,
title: self.data.get("TITLE") title: self
.unwrap_or(&TagType::Unknown).str() .data
.get("TITLE")
.unwrap_or(&TagType::Unknown)
.str()
.and_then(|s| Some(s.to_string())) .and_then(|s| Some(s.to_string()))
.unwrap_or_else(default_title), .unwrap_or_else(default_title),
artist: artist_id, artist: artist_id,
@ -51,18 +68,49 @@ impl Tags {
pub fn meta(&self, id: u64) -> DbMetaItem { pub fn meta(&self, id: u64) -> DbMetaItem {
DbMetaItem { DbMetaItem {
meta_id: id, meta_id: id,
plays: self.data.get("PLAYS").unwrap_or(&TagType::Unknown).uint().unwrap_or(0), plays: self
track: self.data.get("TRACKNUMBER").unwrap_or(&TagType::Unknown).uint().unwrap_or(id), .data
disc: self.data.get("DISCNUMBER").unwrap_or(&TagType::Unknown).uint().unwrap_or(1), .get("PLAYS")
duration: self.data.get("DURATION").unwrap_or(&TagType::Unknown).uint().unwrap_or(0), .unwrap_or(&TagType::Unknown)
date: self.data.get("DATE").unwrap_or(&TagType::Unknown).uint().unwrap_or(0), .uint()
.unwrap_or(0),
track: self
.data
.get("TRACKNUMBER")
.unwrap_or(&TagType::Unknown)
.uint()
.unwrap_or(id),
disc: self
.data
.get("DISCNUMBER")
.unwrap_or(&TagType::Unknown)
.uint()
.unwrap_or(1),
duration: self
.data
.get("DURATION")
.unwrap_or(&TagType::Unknown)
.uint()
.unwrap_or(0),
date: self
.data
.get("DATE")
.unwrap_or(&TagType::Unknown)
.uint()
.unwrap_or(0),
} }
} }
pub fn artist(&self, id: u64, genre_id: u64) -> DbArtistItem { pub fn artist(&self, id: u64, genre_id: u64) -> DbArtistItem {
DbArtistItem { DbArtistItem {
artist_id: id, artist_id: id,
name: self.data.get("ARTIST").unwrap_or(&TagType::Unknown).str().unwrap_or("Unknown Artist").into(), name: self
.data
.get("ARTIST")
.unwrap_or(&TagType::Unknown)
.str()
.unwrap_or("Unknown Artist")
.into(),
genre: genre_id, genre: genre_id,
} }
} }
@ -70,7 +118,13 @@ impl Tags {
pub fn album_artist(&self, id: u64, genre_id: u64) -> DbArtistItem { pub fn album_artist(&self, id: u64, genre_id: u64) -> DbArtistItem {
DbArtistItem { DbArtistItem {
artist_id: id, artist_id: id,
name: self.data.get("ALBUMARTIST").unwrap_or(&TagType::Unknown).str().unwrap_or("Unknown Artist").into(), name: self
.data
.get("ALBUMARTIST")
.unwrap_or(&TagType::Unknown)
.str()
.unwrap_or("Unknown Artist")
.into(),
genre: genre_id, genre: genre_id,
} }
} }
@ -78,7 +132,13 @@ impl Tags {
pub fn album(&self, id: u64, meta_id: u64, artist_id: u64, genre_id: u64) -> DbAlbumItem { pub fn album(&self, id: u64, meta_id: u64, artist_id: u64, genre_id: u64) -> DbAlbumItem {
DbAlbumItem { DbAlbumItem {
album_id: id, album_id: id,
title: self.data.get("ALBUM").unwrap_or(&TagType::Unknown).str().unwrap_or("Unknown Album").into(), title: self
.data
.get("ALBUM")
.unwrap_or(&TagType::Unknown)
.str()
.unwrap_or("Unknown Album")
.into(),
metadata: meta_id, metadata: meta_id,
artist: artist_id, artist: artist_id,
genre: genre_id, genre: genre_id,
@ -88,18 +148,44 @@ impl Tags {
pub fn album_meta(&self, id: u64) -> DbMetaItem { pub fn album_meta(&self, id: u64) -> DbMetaItem {
DbMetaItem { DbMetaItem {
meta_id: id, meta_id: id,
plays: self.data.get("PLAYS").unwrap_or(&TagType::Unknown).uint().unwrap_or(0), plays: self
track: self.data.get("TRACKTOTAL").unwrap_or(&TagType::Unknown).uint().unwrap_or(0), .data
disc: self.data.get("DISCTOTAL").unwrap_or(&TagType::Unknown).uint().unwrap_or(1), .get("PLAYS")
.unwrap_or(&TagType::Unknown)
.uint()
.unwrap_or(0),
track: self
.data
.get("TRACKTOTAL")
.unwrap_or(&TagType::Unknown)
.uint()
.unwrap_or(0),
disc: self
.data
.get("DISCTOTAL")
.unwrap_or(&TagType::Unknown)
.uint()
.unwrap_or(1),
duration: 0, duration: 0,
date: self.data.get("DATE").unwrap_or(&TagType::Unknown).uint().unwrap_or(0), date: self
.data
.get("DATE")
.unwrap_or(&TagType::Unknown)
.uint()
.unwrap_or(0),
} }
} }
pub fn genre(&self, id: u64) -> DbGenreItem { pub fn genre(&self, id: u64) -> DbGenreItem {
DbGenreItem { DbGenreItem {
genre_id: id, genre_id: id,
title: self.data.get("GENRE").unwrap_or(&TagType::Unknown).str().unwrap_or("Unknown Genre").into(), title: self
.data
.get("GENRE")
.unwrap_or(&TagType::Unknown)
.str()
.unwrap_or("Unknown Genre")
.into(),
} }
} }
} }
@ -111,7 +197,7 @@ enum TagType {
I64(i64), I64(i64),
U64(u64), U64(u64),
Str(String), Str(String),
Unknown Unknown,
} }
impl TagType { impl TagType {
@ -130,7 +216,7 @@ impl TagType {
fn str(&self) -> Option<&str> { fn str(&self) -> Option<&str> {
match self { match self {
Self::Str(s) => Some(&s), Self::Str(s) => Some(&s),
_ => None _ => None,
} }
} }
@ -139,7 +225,7 @@ impl TagType {
Self::I64(i) => (*i).try_into().ok(), Self::I64(i) => (*i).try_into().ok(),
Self::U64(u) => Some(*u), Self::U64(u) => Some(*u),
Self::Str(s) => s.parse::<u64>().ok(), Self::Str(s) => s.parse::<u64>().ok(),
_ => None _ => None,
} }
} }
} }

View file

@ -9,7 +9,7 @@ pub struct MpsMusicItem {
impl MpsMusicItem { impl MpsMusicItem {
pub fn map_row(row: &rusqlite::Row) -> rusqlite::Result<Self> { pub fn map_row(row: &rusqlite::Row) -> rusqlite::Result<Self> {
let item = DbMusicItem::map_row(row)?; let item = DbMusicItem::map_row(row)?;
Ok(Self{ Ok(Self {
title: item.title, title: item.title,
filename: item.filename, filename: item.filename,
}) })

View file

@ -0,0 +1,5 @@
mod sql;
pub mod database {
pub use super::sql::{MpsDatabaseQuerier, MpsSQLiteExecutor, QueryOp, QueryResult};
}

View file

@ -0,0 +1,167 @@
use core::fmt::Debug;
use crate::lang::db::*;
use crate::lang::{MpsOp, RuntimeError};
use crate::MpsMusicItem;
pub type QueryResult = Result<Vec<Result<MpsMusicItem, RuntimeError>>, RuntimeError>;
pub type QueryOp = dyn FnMut() -> Box<dyn MpsOp>;
pub trait MpsDatabaseQuerier: Debug {
fn raw(&mut self, query: &str, op: &mut QueryOp) -> QueryResult;
fn artist_like(&mut self, query: &str, op: &mut QueryOp) -> QueryResult;
fn album_like(&mut self, query: &str, op: &mut QueryOp) -> QueryResult;
fn song_like(&mut self, query: &str, op: &mut QueryOp) -> QueryResult;
fn genre_like(&mut self, query: &str, op: &mut QueryOp) -> QueryResult;
}
#[derive(Default, Debug)]
pub struct MpsSQLiteExecutor {
sqlite_connection: Option<rusqlite::Connection>, // initialized by first SQL statement
}
impl MpsSQLiteExecutor {
#[inline]
fn gen_db_maybe(&mut self, op: &mut QueryOp) -> Result<(), RuntimeError> {
if let None = self.sqlite_connection {
// connection needs to be created
match generate_default_db() {
Ok(conn) => {
self.sqlite_connection = Some(conn);
}
Err(e) => {
return Err(RuntimeError {
line: 0,
op: op(),
msg: format!("SQL connection error: {}", e).into(),
})
}
}
}
Ok(())
}
fn music_query_single_param(
&mut self,
query: &str,
param: &str,
op: &mut QueryOp,
) -> QueryResult {
self.gen_db_maybe(op)?;
let conn = self.sqlite_connection.as_mut().unwrap();
match perform_single_param_query(conn, query, param) {
Ok(items) => Ok(items
.into_iter()
.map(|item| {
item.map_err(|e| RuntimeError {
line: 0,
op: op(),
msg: format!("SQL item mapping error: {}", e).into(),
})
})
.collect()),
Err(e) => Err(RuntimeError {
line: 0,
op: op(),
msg: e,
}),
}
}
}
impl MpsDatabaseQuerier for MpsSQLiteExecutor {
fn raw(&mut self, query: &str, op: &mut QueryOp) -> QueryResult {
self.gen_db_maybe(op)?;
let conn = self.sqlite_connection.as_mut().unwrap();
// execute query
match perform_query(conn, query) {
Ok(items) => Ok(items
.into_iter()
.map(|item| {
item.map_err(|e| RuntimeError {
line: 0,
op: op(),
msg: format!("SQL item mapping error: {}", e).into(),
})
})
.collect()),
Err(e) => Err(RuntimeError {
line: 0,
op: op(),
msg: e,
}),
}
}
fn artist_like(&mut self, query: &str, op: &mut QueryOp) -> QueryResult {
let param = &format!("%{}%", query);
let query_stmt =
"SELECT songs.* FROM songs
JOIN artists ON songs.artist = artists.artist_id
JOIN metadata ON songs.metadata = metadata.meta_id
WHERE artists.name like ? ORDER BY songs.album, metadata.track";
self.music_query_single_param(query_stmt, param, op)
}
fn album_like(&mut self, query: &str, op: &mut QueryOp) -> QueryResult {
let param = &format!("%{}%", query);
let query_stmt =
"SELECT songs.* FROM songs
JOIN albums ON songs.album = artists.album_id
JOIN metadata ON songs.metadata = metadata.meta_id
WHERE albums.title like ? ORDER BY songs.album, metadata.track";
self.music_query_single_param(query_stmt, param, op)
}
fn song_like(&mut self, query: &str, op: &mut QueryOp) -> QueryResult {
let param = &format!("%{}%", query);
let query_stmt =
"SELECT songs.* FROM songs
JOIN metadata ON songs.metadata = metadata.meta_id
WHERE songs.title like ? ORDER BY songs.album, metadata.track";
self.music_query_single_param(query_stmt, param, op)
}
fn genre_like(&mut self, query: &str, op: &mut QueryOp) -> QueryResult {
let param = &format!("%{}%", query);
let query_stmt =
"SELECT songs.* FROM songs
JOIN genres ON songs.genre = genres.genre_id
JOIN metadata ON songs.metadata = metadata.meta_id
WHERE genres.title like ? ORDER BY songs.album, metadata.track";
self.music_query_single_param(query_stmt, param, op)
}
}
#[inline]
fn perform_query(
conn: &mut rusqlite::Connection,
query: &str,
) -> Result<Vec<rusqlite::Result<MpsMusicItem>>, String> {
let mut stmt = conn
.prepare(query)
.map_err(|e| format!("SQLite query error: {}", e))?;
let iter = stmt
.query_map([], MpsMusicItem::map_row)
.map_err(|e| format!("SQLite item mapping error: {}", e))?;
Ok(iter.collect())
}
#[inline]
fn perform_single_param_query(
conn: &mut rusqlite::Connection,
query: &str,
param: &str,
) -> Result<Vec<rusqlite::Result<MpsMusicItem>>, String> {
let mut stmt = conn
.prepare_cached(query)
.map_err(|e| format!("SQLite query error: {}", e))?;
let iter = stmt
.query_map([param], MpsMusicItem::map_row)
.map_err(|e| format!("SQLite item mapping error: {}", e))?;
Ok(iter.collect())
}

View file

@ -1,9 +1,9 @@
use std::iter::Iterator;
use std::io::Read; use std::io::Read;
use std::iter::Iterator;
use super::{MpsInterpretor, MpsContext, MpsMusicItem};
use super::tokens::{MpsTokenReader, MpsTokenizer};
use super::lang::{MpsLanguageDictionary, MpsLanguageError}; use super::lang::{MpsLanguageDictionary, MpsLanguageError};
use super::tokens::{MpsTokenReader, MpsTokenizer};
use super::{MpsContext, MpsInterpretor, MpsMusicItem};
pub struct MpsRunnerSettings<T: MpsTokenReader> { pub struct MpsRunnerSettings<T: MpsTokenReader> {
pub vocabulary: MpsLanguageDictionary, pub vocabulary: MpsLanguageDictionary,

View file

@ -1,4 +1,4 @@
use std::fmt::{Debug, Display, Formatter, Error}; use std::fmt::{Debug, Display, Error, Formatter};
use crate::lang::MpsLanguageError; use crate::lang::MpsLanguageError;
@ -11,14 +11,22 @@ pub struct ParseError {
impl Display for ParseError { impl Display for ParseError {
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
write!(f, "ParseError (line {}, column {}): Unexpected {}", &self.line, &self.column, &self.item) write!(
f,
"ParseError (line {}, column {}): Unexpected {}",
&self.line, &self.column, &self.item
)
} }
} }
impl MpsTokenError for ParseError { impl MpsTokenError for ParseError {
fn set_line(&mut self, line: usize) {self.line = line} fn set_line(&mut self, line: usize) {
self.line = line
}
fn set_column(&mut self, column: usize) {self.column = column} fn set_column(&mut self, column: usize) {
self.column = column
}
} }
pub trait MpsTokenError: Display + Debug { pub trait MpsTokenError: Display + Debug {

View file

@ -2,6 +2,6 @@ mod error;
mod token_enum; mod token_enum;
mod tokenizer; mod tokenizer;
pub use error::{ParseError, MpsTokenError}; pub use error::{MpsTokenError, ParseError};
pub use token_enum::MpsToken; pub use token_enum::MpsToken;
pub use tokenizer::{MpsTokenizer, MpsTokenReader}; pub use tokenizer::{MpsTokenReader, MpsTokenizer};

View file

@ -1,4 +1,4 @@
use std::fmt::{Debug, Display, Formatter, Error}; use std::fmt::{Debug, Display, Error, Formatter};
#[derive(Debug, Eq, PartialEq)] #[derive(Debug, Eq, PartialEq)]
pub enum MpsToken { pub enum MpsToken {
@ -8,6 +8,8 @@ pub enum MpsToken {
Comma, Comma,
Literal(String), Literal(String),
Name(String), Name(String),
//Octothorpe,
Comment(String),
} }
impl MpsToken { impl MpsToken {
@ -17,10 +19,11 @@ impl MpsToken {
"(" => Ok(Self::OpenBracket), "(" => Ok(Self::OpenBracket),
")" => Ok(Self::CloseBracket), ")" => Ok(Self::CloseBracket),
"," => Ok(Self::Comma), "," => Ok(Self::Comma),
//"#" => Ok(Self::Octothorpe),
_ => { _ => {
// name validation // name validation
let mut ok = true; let mut ok = true;
for invalid_c in ["-", "+", ","] { for invalid_c in ["-", "+", ",", " ", "/", "\n", "\r", "!", "?"] {
if s.contains(invalid_c) { if s.contains(invalid_c) {
ok = false; ok = false;
break; break;
@ -31,42 +34,56 @@ impl MpsToken {
} else { } else {
Err(s) Err(s)
} }
}, }
} }
} }
pub fn is_sql(&self) -> bool { pub fn is_sql(&self) -> bool {
match self { match self {
Self::Sql => true, Self::Sql => true,
_ => false _ => false,
} }
} }
pub fn is_open_bracket(&self) -> bool { pub fn is_open_bracket(&self) -> bool {
match self { match self {
Self::OpenBracket => true, Self::OpenBracket => true,
_ => false _ => false,
} }
} }
pub fn is_close_bracket(&self) -> bool { pub fn is_close_bracket(&self) -> bool {
match self { match self {
Self::CloseBracket => true, Self::CloseBracket => true,
_ => false _ => false,
} }
} }
pub fn is_literal(&self) -> bool { pub fn is_literal(&self) -> bool {
match self { match self {
Self::Literal(_) => true, Self::Literal(_) => true,
_ => false _ => false,
} }
} }
pub fn is_name(&self) -> bool { pub fn is_name(&self) -> bool {
match self { match self {
Self::Name(_) => true, Self::Name(_) => true,
_ => false _ => false,
}
}
/*pub fn is_octothorpe(&self) -> bool {
match self {
Self::Octothorpe => true,
_ => false,
}
}*/
pub fn is_comment(&self) -> bool {
match self {
Self::Comment(_) => true,
_ => false,
} }
} }
} }
@ -80,6 +97,8 @@ impl Display for MpsToken {
Self::Comma => write!(f, ","), Self::Comma => write!(f, ","),
Self::Literal(s) => write!(f, "\"{}\"", s), Self::Literal(s) => write!(f, "\"{}\"", s),
Self::Name(s) => write!(f, "{}", s), Self::Name(s) => write!(f, "{}", s),
//Self::Octothorpe => write!(f, "#"),
Self::Comment(s) => write!(f, "//{}", s),
} }
} }
} }

View file

@ -1,30 +1,39 @@
use std::collections::VecDeque; use std::collections::VecDeque;
use super::ParseError;
use super::MpsToken; use super::MpsToken;
use super::ParseError;
pub trait MpsTokenReader { pub trait MpsTokenReader {
fn current_line(&self) -> usize; fn current_line(&self) -> usize;
fn current_column(&self) -> usize; fn current_column(&self) -> usize;
fn next_statements(&mut self, count: usize, token_buffer: &mut VecDeque<MpsToken>) -> Result<(), ParseError>; fn next_statement(
&mut self,
token_buffer: &mut VecDeque<MpsToken>,
) -> Result<(), ParseError>;
fn end_of_file(&self) -> bool; fn end_of_file(&self) -> bool;
} }
pub struct MpsTokenizer<R> where R: std::io::Read { pub struct MpsTokenizer<R>
where
R: std::io::Read,
{
reader: R, reader: R,
fsm: ReaderStateMachine, fsm: ReaderStateMachine,
line: usize, line: usize,
column: usize, column: usize,
} }
impl<R> MpsTokenizer<R> where R: std::io::Read { impl<R> MpsTokenizer<R>
where
R: std::io::Read,
{
pub fn new(reader: R) -> Self { pub fn new(reader: R) -> Self {
Self { Self {
reader: reader, reader: reader,
fsm: ReaderStateMachine::Start{}, fsm: ReaderStateMachine::Start {},
line: 0, line: 0,
column: 0, column: 0,
} }
@ -35,7 +44,12 @@ impl<R> MpsTokenizer<R> where R: std::io::Read {
// first read special case // first read special case
// always read before checking if end of statement // always read before checking if end of statement
// since FSM could be from previous (already ended) statement // since FSM could be from previous (already ended) statement
if self.reader.read(&mut byte_buf).map_err(|e| self.error(format!("IO read error: {}", e)))? == 0 { if self
.reader
.read(&mut byte_buf)
.map_err(|e| self.error(format!("IO read error: {}", e)))?
== 0
{
byte_buf[0] = 0; // clear to null char (nothing read is assumed to mean end of file) byte_buf[0] = 0; // clear to null char (nothing read is assumed to mean end of file)
} }
self.do_tracking(byte_buf[0]); self.do_tracking(byte_buf[0]);
@ -48,29 +62,38 @@ impl<R> MpsTokenizer<R> where R: std::io::Read {
} }
// handle parse endings // handle parse endings
match self.fsm { match self.fsm {
ReaderStateMachine::EndLiteral{} => { ReaderStateMachine::EndLiteral {} => {
let literal = String::from_utf8(bigger_buf.clone()) let literal = String::from_utf8(bigger_buf.clone())
.map_err(|e| self.error(format!("UTF-8 encoding error: {}", e)))?; .map_err(|e| self.error(format!("UTF-8 encoding error: {}", e)))?;
buf.push_back(MpsToken::Literal(literal)); buf.push_back(MpsToken::Literal(literal));
bigger_buf.clear(); bigger_buf.clear();
}, },
ReaderStateMachine::EndToken{} => { ReaderStateMachine::EndComment {} => {
let token = String::from_utf8(bigger_buf.clone()) let comment = String::from_utf8(bigger_buf.clone())
.map_err(|e| self.error(format!("UTF-8 encoding error: {}", e)))?; .map_err(|e| self.error(format!("UTF-8 encoding error: {}", e)))?;
buf.push_back( buf.push_back(MpsToken::Comment(comment));
MpsToken::parse_from_string(token)
.map_err(|e| self.error(format!("Invalid token {}", e)))?
);
bigger_buf.clear(); bigger_buf.clear();
}, },
ReaderStateMachine::SingleCharToken{..} => { ReaderStateMachine::EndToken {} => {
let out = bigger_buf.pop().unwrap(); // bracket or comma token if bigger_buf.len() != 0 { // ignore consecutive end tokens
if bigger_buf.len() != 0 { // bracket tokens can be beside other tokens, without separator
let token = String::from_utf8(bigger_buf.clone()) let token = String::from_utf8(bigger_buf.clone())
.map_err(|e| self.error(format!("UTF-8 encoding error: {}", e)))?; .map_err(|e| self.error(format!("UTF-8 encoding error: {}", e)))?;
buf.push_back( buf.push_back(
MpsToken::parse_from_string(token) MpsToken::parse_from_string(token)
.map_err(|e| self.error(format!("Invalid token {}", e)))? .map_err(|e| self.error(format!("Invalid token {}", e)))?,
);
bigger_buf.clear();
}
},
ReaderStateMachine::SingleCharToken { .. } => {
let out = bigger_buf.pop().unwrap(); // bracket or comma token
if bigger_buf.len() != 0 {
// bracket tokens can be beside other tokens, without separator
let token = String::from_utf8(bigger_buf.clone())
.map_err(|e| self.error(format!("UTF-8 encoding error: {}", e)))?;
buf.push_back(
MpsToken::parse_from_string(token)
.map_err(|e| self.error(format!("Invalid token {}", e)))?,
); );
bigger_buf.clear(); bigger_buf.clear();
} }
@ -80,32 +103,42 @@ impl<R> MpsTokenizer<R> where R: std::io::Read {
.map_err(|e| self.error(format!("UTF-8 encoding error: {}", e)))?; .map_err(|e| self.error(format!("UTF-8 encoding error: {}", e)))?;
buf.push_back( buf.push_back(
MpsToken::parse_from_string(token) MpsToken::parse_from_string(token)
.map_err(|e| self.error(format!("Invalid token {}", e)))? .map_err(|e| self.error(format!("Invalid token {}", e)))?,
); );
bigger_buf.clear(); bigger_buf.clear();
}, },
ReaderStateMachine::EndStatement{} => { ReaderStateMachine::EndStatement {} => {
// unnecessary; loop will have already exited // unnecessary; loop will have already exited
}, },
ReaderStateMachine::EndOfFile{} => { ReaderStateMachine::EndOfFile {} => {
// unnecessary; loop will have already exited // unnecessary; loop will have already exited
}, },
_ => {}, ReaderStateMachine::Invalid { .. } => {
let invalid_char = bigger_buf.pop().unwrap(); // invalid single char
Err(self.error(format!("Unexpected character {}", invalid_char)))?;
},
_ => {}
} }
if self.reader.read(&mut byte_buf).map_err(|e| self.error(format!("IO read error: {}", e)))? == 0 { if self
.reader
.read(&mut byte_buf)
.map_err(|e| self.error(format!("IO read error: {}", e)))?
== 0
{
byte_buf[0] = 0; // clear to null char (nothing read is assumed to mean end of file) byte_buf[0] = 0; // clear to null char (nothing read is assumed to mean end of file)
} }
self.do_tracking(byte_buf[0]); self.do_tracking(byte_buf[0]);
self.fsm = self.fsm.next_state(byte_buf[0]); self.fsm = self.fsm.next_state(byte_buf[0]);
} }
// handle end statement // handle end statement
if bigger_buf.len() != 0 { // also end of token if bigger_buf.len() != 0 {
// also end of token
// note: never also end of literal, since those have explicit closing characters // note: never also end of literal, since those have explicit closing characters
let token = String::from_utf8(bigger_buf.clone()) let token = String::from_utf8(bigger_buf.clone())
.map_err(|e| self.error(format!("UTF-8 encoding error: {}", e)))?; .map_err(|e| self.error(format!("UTF-8 encoding error: {}", e)))?;
buf.push_back( buf.push_back(
MpsToken::parse_from_string(token) MpsToken::parse_from_string(token)
.map_err(|e| self.error(format!("Invalid token {}", e)))? .map_err(|e| self.error(format!("Invalid token {}", e)))?,
); );
bigger_buf.clear(); bigger_buf.clear();
} }
@ -132,7 +165,7 @@ impl<R> MpsTokenizer<R> where R: std::io::Read {
impl<R> MpsTokenReader for MpsTokenizer<R> impl<R> MpsTokenReader for MpsTokenizer<R>
where where
R: std::io::Read R: std::io::Read,
{ {
fn current_line(&self) -> usize { fn current_line(&self) -> usize {
self.line self.line
@ -142,8 +175,13 @@ where
self.column self.column
} }
fn next_statements(&mut self, count: usize, buf: &mut VecDeque<MpsToken>) -> Result<(), ParseError> { fn next_statement(
for _ in 0..count { &mut self,
buf: &mut VecDeque<MpsToken>,
) -> Result<(), ParseError> {
// read until buffer gets some tokens, in case multiple end of line tokens are at start of stream
let original_size = buf.len();
while original_size == buf.len() && !self.end_of_file() {
self.read_line(buf)?; self.read_line(buf)?;
} }
Ok(()) Ok(())
@ -156,94 +194,119 @@ where
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
enum ReaderStateMachine { enum ReaderStateMachine {
Start{}, // beginning of machine, no parsing has occured Start {}, // beginning of machine, no parsing has occured
Regular{ Regular {
out: u8, out: u8,
}, // standard }, // standard
Escaped{ Escaped {
inside: char, // literal inside: char, // literal
}, // escape character; applied to next character }, // escape character; applied to next character
StartTickLiteral{}, StartTickLiteral {},
StartQuoteLiteral{}, StartQuoteLiteral {},
InsideTickLiteral{ InsideTickLiteral {
out: u8, out: u8,
}, },
InsideQuoteLiteral{ InsideQuoteLiteral {
out: u8, out: u8,
}, },
SingleCharToken { SingleCharToken {
out: u8, out: u8,
}, },
EndLiteral{}, Slash {out: u8},
EndToken{}, Octothorpe {out: u8},
EndStatement{}, Comment {out: u8},
EndOfFile{}, EndLiteral {},
EndToken {},
EndComment {},
EndStatement {},
EndOfFile {},
Invalid { out: u8 },
} }
impl ReaderStateMachine { impl ReaderStateMachine {
pub fn next_state(self, input: u8) -> Self { pub fn next_state(self, input: u8) -> Self {
let input_char = input as char; let input_char = input as char;
match self { match self {
Self::Start{} Self::Start {}
| Self::Regular{..} | Self::Regular { .. }
| Self::SingleCharToken{..} | Self::SingleCharToken { .. }
| Self::EndLiteral{} | Self::EndLiteral {}
| Self::EndToken{} | Self::EndToken {}
| Self::EndStatement{} => | Self::EndComment {}
match input_char { | Self::EndStatement {}
'\\' => Self::Escaped{inside: '_'}, | Self::Invalid {..} => match input_char {
'`' => Self::StartTickLiteral{}, '\\' => Self::Escaped { inside: '_' },
'"' => Self::StartQuoteLiteral{}, '/' => Self::Slash { out: input },
' ' => Self::EndToken{}, '#' => Self::Octothorpe { out: input },
'\n' | '\r' | ';' => Self::EndStatement{}, '`' => Self::StartTickLiteral {},
'\0' => Self::EndOfFile{}, '"' => Self::StartQuoteLiteral {},
'(' | ')' | ',' => Self::SingleCharToken{out: input}, ' ' => Self::EndToken {},
_ => Self::Regular{out: input}, '\n' | '\r' | ';' => Self::EndStatement {},
'\0' => Self::EndOfFile {},
'(' | ')' | ',' => Self::SingleCharToken { out: input },
_ => Self::Regular { out: input },
}, },
Self::Escaped{inside} => match inside { Self::Escaped { inside } => match inside {
'`' => Self::InsideTickLiteral{out: input}, '`' => Self::InsideTickLiteral { out: input },
'"' => Self::InsideQuoteLiteral{out: input}, '"' => Self::InsideQuoteLiteral { out: input },
'_' | _ => Self::Regular{out: input} '_' | _ => Self::Regular { out: input },
}, },
Self::StartTickLiteral{} Self::StartTickLiteral {} | Self::InsideTickLiteral { .. } => match input_char {
| Self::InsideTickLiteral{..} => '\\' => Self::Escaped { inside: '`' },
match input_char { '`' => Self::EndLiteral {},
'\\' => Self::Escaped{inside: '`'}, '\0' => Self::Invalid { out: input },
'`' => Self::EndLiteral{}, _ => Self::InsideTickLiteral { out: input },
_ => Self::InsideTickLiteral{out: input},
}, },
Self::StartQuoteLiteral{} Self::StartQuoteLiteral {} | Self::InsideQuoteLiteral { .. } => match input_char {
| Self::InsideQuoteLiteral{..} => '\\' => Self::Escaped { inside: '"' },
match input_char { '"' => Self::EndLiteral {},
'\\' => Self::Escaped{inside: '"'}, '\0' => Self::Invalid { out: input },
'"' => Self::EndLiteral{}, _ => Self::InsideQuoteLiteral { out: input },
_ => Self::InsideQuoteLiteral{out: input},
}, },
Self::EndOfFile{} => Self::EndOfFile{}, Self::Slash {..} => match input_char {
'/' => Self::Comment { out: input },
' ' => Self::EndToken {},
'\0' => Self::EndOfFile {},
'\n' | '\r' | ';' => Self::EndStatement {},
_ => Self::Regular { out: input },
},
Self::Octothorpe {..} => match input_char {
'\n' | '\r' | '\0' => Self::EndComment {},
_ => Self::Comment { out: input }
},
Self::Comment {..} => match input_char {
'\n' | '\r' | '\0' => Self::EndComment {},
_ => Self::Comment { out: input },
},
Self::EndOfFile {} => Self::EndOfFile {},
} }
} }
pub fn is_end_statement(&self) -> bool { pub fn is_end_statement(&self) -> bool {
match self { match self {
Self::EndStatement{} => true, Self::EndStatement {} => true,
_ => false _ => false,
} }
} }
pub fn is_end_of_file(&self) -> bool { pub fn is_end_of_file(&self) -> bool {
match self { match self {
Self::EndOfFile{} => true, Self::EndOfFile {} => true,
_ => false _ => false,
} }
} }
pub fn output(&self) -> Option<u8> { pub fn output(&self) -> Option<u8> {
match self { match self {
Self::Regular{ out, ..} Self::Regular { out, .. }
| Self::SingleCharToken{ out, ..} | Self::SingleCharToken { out, .. }
| Self::InsideTickLiteral{ out, ..} | Self::InsideTickLiteral { out, .. }
| Self::InsideQuoteLiteral{ out, ..} => Some(*out), | Self::InsideQuoteLiteral { out, .. }
_ => None | Self::Slash { out, .. }
| Self::Octothorpe { out, ..}
| Self::Comment { out, .. }
| Self::Invalid { out, .. } => Some(*out),
_ => None,
} }
} }
} }

View file

@ -1,8 +1,8 @@
use std::io::Cursor;
use std::collections::VecDeque;
use mps_interpreter::*;
use mps_interpreter::lang::MpsLanguageError; use mps_interpreter::lang::MpsLanguageError;
use mps_interpreter::tokens::{ParseError, MpsToken, MpsTokenizer}; use mps_interpreter::tokens::{MpsToken, MpsTokenizer, ParseError};
use mps_interpreter::*;
use std::collections::VecDeque;
use std::io::Cursor;
#[test] #[test]
fn parse_line() -> Result<(), ParseError> { fn parse_line() -> Result<(), ParseError> {
@ -34,9 +34,8 @@ fn parse_line() -> Result<(), ParseError> {
Ok(()) Ok(())
} }
#[test] fn execute_single_line(line: &str, should_be_emtpy: bool) -> Result<(), Box<dyn MpsLanguageError>> {
fn execute_line() -> Result<(), Box<dyn MpsLanguageError>> { let cursor = Cursor::new(line);
let cursor = Cursor::new("sql(`SELECT * FROM songs ORDER BY artist;`)");
let tokenizer = MpsTokenizer::new(cursor); let tokenizer = MpsTokenizer::new(cursor);
let interpreter = MpsInterpretor::with_standard_vocab(tokenizer); let interpreter = MpsInterpretor::with_standard_vocab(tokenizer);
@ -44,14 +43,36 @@ fn execute_line() -> Result<(), Box<dyn MpsLanguageError>> {
let mut count = 0; let mut count = 0;
for result in interpreter { for result in interpreter {
if let Ok(item) = result { if let Ok(item) = result {
count +=1; count += 1;
if count > 100 {continue;} // no need to spam the rest of the songs if count > 100 {
continue;
} // no need to spam the rest of the songs
println!("Got song `{}` (file: `{}`)", item.title, item.filename); println!("Got song `{}` (file: `{}`)", item.title, item.filename);
} else { } else {
println!("Got error while iterating (executing)"); println!("Got error while iterating (executing)");
result?; result?;
} }
} }
if should_be_emtpy {
assert_eq!(count, 0);
} else {
assert_ne!(count, 0); // database is populated assert_ne!(count, 0); // database is populated
}
Ok(()) Ok(())
} }
#[test]
fn execute_sql_line() -> Result<(), Box<dyn MpsLanguageError>> {
execute_single_line("sql(`SELECT * FROM songs ORDER BY artist;`)", false)
}
#[test]
fn execute_simple_sql_line() -> Result<(), Box<dyn MpsLanguageError>> {
execute_single_line("song(`lov`)", false)
}
#[test]
fn execute_comment_line() -> Result<(), Box<dyn MpsLanguageError>> {
execute_single_line("// this is a comment", true)?;
execute_single_line("# this is a special comment", true)
}