Add // or # comments and simple sql query statements
This commit is contained in:
parent
7a327767f3
commit
03318a0ef5
23 changed files with 1067 additions and 293 deletions
|
@ -4,7 +4,7 @@ version = "0.1.0"
|
|||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
rusqlite = { version = "0.26.1" }
|
||||
rusqlite = { version = "0.26.3" }
|
||||
symphonia = { version = "0.4.0", optional = true, features = [
|
||||
"aac", "flac", "mp3", "pcm", "vorbis", "isomp4", "ogg", "wav"
|
||||
] }
|
||||
|
|
|
@ -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)]
|
||||
pub struct MpsContext {
|
||||
pub sqlite_connection: Option<rusqlite::Connection>,
|
||||
pub database: Box<dyn MpsDatabaseQuerier>,
|
||||
}
|
||||
|
||||
impl Default for MpsContext {
|
||||
fn default() -> 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 {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
sqlite_connection: None,
|
||||
database: Box::new(MpsSQLiteExecutor::default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
use std::iter::Iterator;
|
||||
use std::collections::VecDeque;
|
||||
use std::path::Path;
|
||||
use std::fs::File;
|
||||
use std::iter::Iterator;
|
||||
use std::path::Path;
|
||||
|
||||
use super::MpsMusicItem;
|
||||
use super::MpsContext;
|
||||
use super::lang::{MpsLanguageDictionary, MpsLanguageError, MpsOp};
|
||||
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,
|
||||
buffer: VecDeque<MpsToken>,
|
||||
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>
|
||||
where T: crate::tokens::MpsTokenReader
|
||||
where
|
||||
T: crate::tokens::MpsTokenReader,
|
||||
{
|
||||
pub fn with_vocab(tokenizer: T, vocab: MpsLanguageDictionary) -> Self {
|
||||
Self {
|
||||
|
@ -72,7 +76,8 @@ impl MpsInterpretor<crate::tokens::MpsTokenizer<File>> {
|
|||
}
|
||||
|
||||
impl<T> Iterator for MpsInterpretor<T>
|
||||
where T: crate::tokens::MpsTokenReader
|
||||
where
|
||||
T: crate::tokens::MpsTokenReader,
|
||||
{
|
||||
type Item = Result<MpsMusicItem, Box<dyn MpsLanguageError>>;
|
||||
|
||||
|
@ -84,21 +89,27 @@ impl<T> Iterator for MpsInterpretor<T>
|
|||
is_stmt_done = true;
|
||||
}
|
||||
match next_item {
|
||||
Some(item) => Some(item.map_err(|e| box_error_with_ctx(
|
||||
e, self.tokenizer.current_line()
|
||||
))),
|
||||
None => None
|
||||
Some(item) => {
|
||||
Some(item.map_err(|e| box_error_with_ctx(e, self.tokenizer.current_line())))
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
} else {
|
||||
if self.tokenizer.end_of_file() { return None; }
|
||||
if self.tokenizer.end_of_file() {
|
||||
return None;
|
||||
}
|
||||
// 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()));
|
||||
match token_result {
|
||||
Ok(_) => {},
|
||||
Err(x) => return Some(Err(x))
|
||||
Ok(_) => {}
|
||||
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);
|
||||
match stmt {
|
||||
Ok(mut stmt) => {
|
||||
|
@ -109,17 +120,15 @@ impl<T> Iterator for MpsInterpretor<T>
|
|||
is_stmt_done = true;
|
||||
}
|
||||
match next_item {
|
||||
Some(item) => Some(item.map_err(|e| box_error_with_ctx(
|
||||
e,
|
||||
self.tokenizer.current_line()
|
||||
))),
|
||||
None => None
|
||||
Some(item) => Some(
|
||||
item.map_err(|e| box_error_with_ctx(e, self.tokenizer.current_line())),
|
||||
),
|
||||
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 {
|
||||
|
@ -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);
|
||||
Box::new(error) as Box<dyn MpsLanguageError>
|
||||
}
|
||||
|
||||
pub(crate) fn standard_vocab(vocabulary: &mut MpsLanguageDictionary) {
|
||||
vocabulary
|
||||
.add(crate::lang::vocabulary::SqlStatementFactory);
|
||||
.add(crate::lang::vocabulary::SqlStatementFactory)
|
||||
.add(crate::lang::vocabulary::SimpleSqlStatementFactory)
|
||||
.add(crate::lang::vocabulary::CommentStatementFactory);
|
||||
}
|
||||
|
|
91
mps-interpreter/src/lang/comment.rs
Normal file
91
mps-interpreter/src/lang/comment.rs
Normal 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)
|
||||
}
|
||||
}
|
|
@ -13,7 +13,9 @@ pub fn generate_default_db() -> rusqlite::Result<rusqlite::Connection> {
|
|||
let conn = rusqlite::Connection::open(DEFAULT_SQLITE_FILEPATH)?;
|
||||
// skip db building if SQLite file already exists
|
||||
// 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
|
||||
conn.execute_batch(
|
||||
"BEGIN;
|
||||
|
@ -50,7 +52,7 @@ pub fn generate_default_db() -> rusqlite::Result<rusqlite::Connection> {
|
|||
genre_id INTEGER NOT NULL PRIMARY KEY,
|
||||
title TEXT
|
||||
);
|
||||
COMMIT;"
|
||||
COMMIT;",
|
||||
)?;
|
||||
// generate data and store in db
|
||||
#[cfg(feature = "music_library")]
|
||||
|
@ -67,7 +69,7 @@ pub fn generate_default_db() -> rusqlite::Result<rusqlite::Connection> {
|
|||
filename,
|
||||
metadata,
|
||||
genre
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?)"
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
)?;
|
||||
for song in lib.all_songs() {
|
||||
song_insert.execute(song.to_params().as_slice())?;
|
||||
|
@ -81,7 +83,7 @@ pub fn generate_default_db() -> rusqlite::Result<rusqlite::Connection> {
|
|||
disc,
|
||||
duration,
|
||||
date
|
||||
) VALUES (?, ?, ?, ?, ?, ?)"
|
||||
) VALUES (?, ?, ?, ?, ?, ?)",
|
||||
)?;
|
||||
for meta in lib.all_metadata() {
|
||||
metadata_insert.execute(meta.to_params().as_slice())?;
|
||||
|
@ -92,7 +94,7 @@ pub fn generate_default_db() -> rusqlite::Result<rusqlite::Connection> {
|
|||
artist_id,
|
||||
name,
|
||||
genre
|
||||
) VALUES (?, ?, ?)"
|
||||
) VALUES (?, ?, ?)",
|
||||
)?;
|
||||
for artist in lib.all_artists() {
|
||||
artist_insert.execute(artist.to_params().as_slice())?;
|
||||
|
@ -105,7 +107,7 @@ pub fn generate_default_db() -> rusqlite::Result<rusqlite::Connection> {
|
|||
metadata,
|
||||
artist,
|
||||
genre
|
||||
) VALUES (?, ?, ?, ?, ?)"
|
||||
) VALUES (?, ?, ?, ?, ?)",
|
||||
)?;
|
||||
for album in lib.all_albums() {
|
||||
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 (
|
||||
genre_id,
|
||||
title
|
||||
) VALUES (?, ?)"
|
||||
) VALUES (?, ?)",
|
||||
)?;
|
||||
for genre in lib.all_genres() {
|
||||
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)
|
||||
|
@ -140,7 +142,7 @@ pub struct DbMusicItem {
|
|||
|
||||
impl DatabaseObj for DbMusicItem {
|
||||
fn map_row(row: &rusqlite::Row) -> rusqlite::Result<Self> {
|
||||
Ok(Self{
|
||||
Ok(Self {
|
||||
song_id: row.get(0)?,
|
||||
title: row.get(1)?,
|
||||
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)]
|
||||
|
@ -178,7 +182,7 @@ pub struct DbMetaItem {
|
|||
|
||||
impl DatabaseObj for DbMetaItem {
|
||||
fn map_row(row: &rusqlite::Row) -> rusqlite::Result<Self> {
|
||||
Ok(Self{
|
||||
Ok(Self {
|
||||
meta_id: row.get(0)?,
|
||||
plays: row.get(1)?,
|
||||
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)]
|
||||
|
@ -211,7 +217,7 @@ pub struct DbArtistItem {
|
|||
|
||||
impl DatabaseObj for DbArtistItem {
|
||||
fn map_row(row: &rusqlite::Row) -> rusqlite::Result<Self> {
|
||||
Ok(Self{
|
||||
Ok(Self {
|
||||
artist_id: row.get(0)?,
|
||||
name: row.get(1)?,
|
||||
genre: row.get(2)?,
|
||||
|
@ -219,14 +225,12 @@ impl DatabaseObj for DbArtistItem {
|
|||
}
|
||||
|
||||
fn to_params(&self) -> Vec<&dyn rusqlite::ToSql> {
|
||||
vec![
|
||||
&self.artist_id,
|
||||
&self.name,
|
||||
&self.genre,
|
||||
]
|
||||
vec![&self.artist_id, &self.name, &self.genre]
|
||||
}
|
||||
|
||||
fn id(&self) -> u64 {self.artist_id}
|
||||
fn id(&self) -> u64 {
|
||||
self.artist_id
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
@ -240,7 +244,7 @@ pub struct DbAlbumItem {
|
|||
|
||||
impl DatabaseObj for DbAlbumItem {
|
||||
fn map_row(row: &rusqlite::Row) -> rusqlite::Result<Self> {
|
||||
Ok(Self{
|
||||
Ok(Self {
|
||||
album_id: row.get(0)?,
|
||||
title: row.get(1)?,
|
||||
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)]
|
||||
|
@ -270,18 +276,17 @@ pub struct DbGenreItem {
|
|||
|
||||
impl DatabaseObj for DbGenreItem {
|
||||
fn map_row(row: &rusqlite::Row) -> rusqlite::Result<Self> {
|
||||
Ok(Self{
|
||||
Ok(Self {
|
||||
genre_id: row.get(0)?,
|
||||
title: row.get(1)?,
|
||||
})
|
||||
}
|
||||
|
||||
fn to_params(&self) -> Vec<&dyn rusqlite::ToSql> {
|
||||
vec![
|
||||
&self.genre_id,
|
||||
&self.title,
|
||||
]
|
||||
vec![&self.genre_id, &self.title]
|
||||
}
|
||||
|
||||
fn id(&self) -> u64 {self.genre_id}
|
||||
fn id(&self) -> u64 {
|
||||
self.genre_id
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,28 +1,32 @@
|
|||
use std::collections::VecDeque;
|
||||
|
||||
use crate::tokens::MpsToken;
|
||||
use super::{BoxedMpsOpFactory, MpsOp};
|
||||
use super::SyntaxError;
|
||||
use super::{BoxedMpsOpFactory, MpsOp};
|
||||
use crate::tokens::MpsToken;
|
||||
|
||||
pub struct MpsLanguageDictionary {
|
||||
vocabulary: Vec<Box<dyn BoxedMpsOpFactory>>
|
||||
vocabulary: Vec<Box<dyn BoxedMpsOpFactory>>,
|
||||
}
|
||||
|
||||
impl MpsLanguageDictionary {
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
if factory.is_op_boxed(tokens) {
|
||||
return factory.build_op_boxed(tokens);
|
||||
return factory.build_op_boxed(tokens, self);
|
||||
}
|
||||
}
|
||||
Err(SyntaxError {
|
||||
line: 0,
|
||||
token: tokens.pop_front().unwrap()
|
||||
token: tokens.pop_front().unwrap(),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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 crate::tokens::MpsToken;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SyntaxError {
|
||||
|
@ -11,12 +11,18 @@ pub struct SyntaxError {
|
|||
|
||||
impl Display for SyntaxError {
|
||||
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 {
|
||||
fn set_line(&mut self, line: usize) {self.line = line}
|
||||
fn set_line(&mut self, line: usize) {
|
||||
self.line = line
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -33,7 +39,9 @@ impl Display 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 {
|
||||
|
|
|
@ -1,22 +1,29 @@
|
|||
mod comment;
|
||||
mod db_items;
|
||||
mod dictionary;
|
||||
mod error;
|
||||
mod operation;
|
||||
mod sql_query;
|
||||
mod sql_simple_query;
|
||||
//mod statement;
|
||||
pub(crate) mod utility;
|
||||
|
||||
pub use dictionary::MpsLanguageDictionary;
|
||||
pub use error::{SyntaxError, RuntimeError, MpsLanguageError};
|
||||
pub use operation::{MpsOp, MpsOpFactory, BoxedMpsOpFactory};
|
||||
pub use error::{MpsLanguageError, RuntimeError, SyntaxError};
|
||||
pub use operation::{BoxedMpsOpFactory, MpsOp, MpsOpFactory, SimpleMpsOpFactory};
|
||||
//pub(crate) use statement::MpsStatement;
|
||||
|
||||
pub mod vocabulary {
|
||||
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 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)]
|
||||
|
|
|
@ -1,30 +1,66 @@
|
|||
use std::iter::Iterator;
|
||||
use std::collections::VecDeque;
|
||||
use std::fmt::{Debug, Display};
|
||||
use std::iter::Iterator;
|
||||
|
||||
use crate::MpsMusicItem;
|
||||
use crate::MpsContext;
|
||||
use super::MpsLanguageDictionary;
|
||||
use super::{RuntimeError, SyntaxError};
|
||||
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> {
|
||||
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]
|
||||
fn build_box(&self, tokens: &mut VecDeque<MpsToken>) -> Result<Box<dyn MpsOp>, SyntaxError> {
|
||||
Ok(Box::new(self.build_op(tokens)?))
|
||||
fn build_box(
|
||||
&self,
|
||||
tokens: &mut VecDeque<MpsToken>,
|
||||
dict: &MpsLanguageDictionary,
|
||||
) -> Result<Box<dyn MpsOp>, SyntaxError> {
|
||||
Ok(Box::new(self.build_op(tokens, dict)?))
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 escape(&mut self) -> MpsContext;
|
||||
|
|
|
@ -1,42 +1,43 @@
|
|||
use std::iter::Iterator;
|
||||
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::MpsMusicItem;
|
||||
use crate::tokens::MpsToken;
|
||||
use super::{MpsOp, MpsOpFactory, BoxedMpsOpFactory};
|
||||
use super::{SyntaxError, RuntimeError};
|
||||
use super::utility::{assert_token, assert_token_raw};
|
||||
use super::db::*;
|
||||
//use super::db::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SqlStatement {
|
||||
query: String,
|
||||
context: Option<MpsContext>,
|
||||
rows: Option<Vec<rusqlite::Result<MpsMusicItem>>>,
|
||||
rows: Option<Vec<Result<MpsMusicItem, RuntimeError>>>,
|
||||
current: usize,
|
||||
}
|
||||
|
||||
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 increment {
|
||||
if self.current == rows.len() {
|
||||
return None
|
||||
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: 0,
|
||||
line: e.line,
|
||||
op: Box::new(self.clone()),
|
||||
msg: format!("SQL music item mapping error: {}", e).into(),
|
||||
}))
|
||||
msg: e.msg.clone(),
|
||||
})),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -46,12 +47,11 @@ impl SqlStatement {
|
|||
msg: format!("Context error: rows is None").into(),
|
||||
}))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
impl MpsOp for SqlStatement {
|
||||
fn enter(&mut self, ctx: MpsContext){
|
||||
fn enter(&mut self, ctx: MpsContext) {
|
||||
self.context = Some(ctx)
|
||||
}
|
||||
|
||||
|
@ -64,8 +64,8 @@ impl std::clone::Clone for SqlStatement {
|
|||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
query: self.query.clone(),
|
||||
context: self.context.clone(),
|
||||
rows: None, // TODO use different Result type so this is cloneable
|
||||
context: None, // unecessary to include in clone (not used for displaying)
|
||||
rows: None, // unecessary to include
|
||||
current: self.current,
|
||||
}
|
||||
}
|
||||
|
@ -77,34 +77,21 @@ impl Iterator for SqlStatement {
|
|||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.rows.is_some() {
|
||||
// query has executed, return another result
|
||||
self.map_item(true)
|
||||
self.get_item(true)
|
||||
} else {
|
||||
let self_clone = self.clone();
|
||||
let ctx = self.context.as_mut().unwrap();
|
||||
// query has not been executed yet
|
||||
if let None = ctx.sqlite_connection {
|
||||
// connection needs to be created
|
||||
match generate_default_db() {
|
||||
Ok(conn) => ctx.sqlite_connection = Some(conn),
|
||||
Err(e) => return Some(Err(RuntimeError{
|
||||
line: 0,
|
||||
op: Box::new(self.clone()),
|
||||
msg: format!("SQL connection error: {}", e).into()
|
||||
}))
|
||||
match ctx
|
||||
.database
|
||||
.raw(&self.query, &mut move || Box::new(self_clone.clone()))
|
||||
{
|
||||
Err(e) => return Some(Err(e)),
|
||||
Ok(rows) => {
|
||||
self.rows = Some(rows);
|
||||
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 {
|
||||
#[inline]
|
||||
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]
|
||||
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` )
|
||||
assert_token_raw(MpsToken::Sql, tokens)?;
|
||||
assert_token_raw(MpsToken::OpenBracket, tokens)?;
|
||||
let literal = assert_token(|t| {
|
||||
match t {
|
||||
let literal = assert_token(
|
||||
|t| match t {
|
||||
MpsToken::Literal(query) => Some(query),
|
||||
_ => None
|
||||
}
|
||||
}, MpsToken::Literal("".into()), tokens)?;
|
||||
_ => None,
|
||||
},
|
||||
MpsToken::Literal("".into()),
|
||||
tokens,
|
||||
)?;
|
||||
assert_token_raw(MpsToken::CloseBracket, tokens)?;
|
||||
Ok(SqlStatement {
|
||||
query: literal,
|
||||
|
@ -145,22 +142,15 @@ impl MpsOpFactory<SqlStatement> for SqlStatementFactory {
|
|||
}
|
||||
|
||||
impl BoxedMpsOpFactory for SqlStatementFactory {
|
||||
fn build_op_boxed(&self, tokens: &mut VecDeque<MpsToken>) -> Result<Box<dyn MpsOp>, SyntaxError> {
|
||||
self.build_box(tokens)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
|
|
233
mps-interpreter/src/lang/sql_simple_query.rs
Normal file
233
mps-interpreter/src/lang/sql_simple_query.rs
Normal 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)
|
||||
}
|
||||
}
|
|
@ -1,16 +1,17 @@
|
|||
mod context;
|
||||
mod interpretor;
|
||||
mod runner;
|
||||
mod music_item;
|
||||
pub mod lang;
|
||||
#[cfg(feature = "music_library")]
|
||||
pub mod music;
|
||||
mod music_item;
|
||||
pub mod processing;
|
||||
mod runner;
|
||||
pub mod tokens;
|
||||
|
||||
pub use context::MpsContext;
|
||||
pub use interpretor::{MpsInterpretor, interpretor};
|
||||
pub use runner::MpsRunner;
|
||||
pub use interpretor::{interpretor, MpsInterpretor};
|
||||
pub use music_item::MpsMusicItem;
|
||||
pub use runner::MpsRunner;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {}
|
||||
|
|
|
@ -4,8 +4,8 @@ use std::path::Path;
|
|||
use symphonia::core::io::MediaSourceStream;
|
||||
use symphonia::core::probe::Hint;
|
||||
|
||||
use crate::lang::db::*;
|
||||
use super::tag::Tags;
|
||||
use crate::lang::db::*;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct MpsLibrary {
|
||||
|
@ -55,7 +55,7 @@ impl MpsLibrary {
|
|||
let path = path.as_ref();
|
||||
if path.is_dir() && depth != 0 {
|
||||
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() {
|
||||
self.read_file(path)?;
|
||||
|
@ -72,7 +72,7 @@ impl MpsLibrary {
|
|||
&Hint::new(),
|
||||
mss,
|
||||
&Default::default(),
|
||||
&Default::default()
|
||||
&Default::default(),
|
||||
);
|
||||
// process audio file, ignoring any processing errors (skip file on error)
|
||||
if let Ok(mut probed) = probed {
|
||||
|
@ -99,7 +99,9 @@ impl MpsLibrary {
|
|||
|
||||
/// generate data structures and links
|
||||
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 meta_id = self.metadata.len() as u64; // guaranteed to be created
|
||||
self.metadata.insert(meta_id, tags.meta(meta_id)); // definitely necessary
|
||||
|
@ -107,19 +109,22 @@ impl MpsLibrary {
|
|||
let mut genre = tags.genre(0);
|
||||
genre.genre_id = Self::find_or_gen_id(&self.genres, &genre.title);
|
||||
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
|
||||
let mut artist = tags.artist(0, genre.genre_id);
|
||||
artist.artist_id = Self::find_or_gen_id(&self.artists, &artist.name);
|
||||
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
|
||||
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);
|
||||
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
|
||||
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 {
|
||||
let album_meta = tags.album_meta(self.metadata.len() as u64);
|
||||
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);
|
||||
}
|
||||
//let meta_album_id = self.metadata.len() as u64;
|
||||
//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]
|
||||
|
|
|
@ -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 extension = self.filename.extension().and_then(|ext| ext.to_str()).unwrap_or("");
|
||||
self.filename.file_name()
|
||||
let extension = self
|
||||
.filename
|
||||
.extension()
|
||||
.and_then(|ext| ext.to_str())
|
||||
.unwrap_or("");
|
||||
self.filename
|
||||
.file_name()
|
||||
.and_then(|file| file.to_str())
|
||||
.and_then(|file| Some(file.replacen(extension, "", 1)))
|
||||
.unwrap_or("Unknown Title".into())
|
||||
};
|
||||
DbMusicItem {
|
||||
song_id: id,
|
||||
title: self.data.get("TITLE")
|
||||
.unwrap_or(&TagType::Unknown).str()
|
||||
title: self
|
||||
.data
|
||||
.get("TITLE")
|
||||
.unwrap_or(&TagType::Unknown)
|
||||
.str()
|
||||
.and_then(|s| Some(s.to_string()))
|
||||
.unwrap_or_else(default_title),
|
||||
artist: artist_id,
|
||||
|
@ -51,18 +68,49 @@ impl Tags {
|
|||
pub fn meta(&self, id: u64) -> DbMetaItem {
|
||||
DbMetaItem {
|
||||
meta_id: id,
|
||||
plays: self.data.get("PLAYS").unwrap_or(&TagType::Unknown).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),
|
||||
plays: self
|
||||
.data
|
||||
.get("PLAYS")
|
||||
.unwrap_or(&TagType::Unknown)
|
||||
.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 {
|
||||
DbArtistItem {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
@ -70,7 +118,13 @@ impl Tags {
|
|||
pub fn album_artist(&self, id: u64, genre_id: u64) -> DbArtistItem {
|
||||
DbArtistItem {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
@ -78,7 +132,13 @@ impl Tags {
|
|||
pub fn album(&self, id: u64, meta_id: u64, artist_id: u64, genre_id: u64) -> DbAlbumItem {
|
||||
DbAlbumItem {
|
||||
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,
|
||||
artist: artist_id,
|
||||
genre: genre_id,
|
||||
|
@ -88,18 +148,44 @@ impl Tags {
|
|||
pub fn album_meta(&self, id: u64) -> DbMetaItem {
|
||||
DbMetaItem {
|
||||
meta_id: id,
|
||||
plays: self.data.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),
|
||||
plays: self
|
||||
.data
|
||||
.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,
|
||||
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 {
|
||||
DbGenreItem {
|
||||
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),
|
||||
U64(u64),
|
||||
Str(String),
|
||||
Unknown
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl TagType {
|
||||
|
@ -130,7 +216,7 @@ impl TagType {
|
|||
fn str(&self) -> Option<&str> {
|
||||
match self {
|
||||
Self::Str(s) => Some(&s),
|
||||
_ => None
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -139,7 +225,7 @@ impl TagType {
|
|||
Self::I64(i) => (*i).try_into().ok(),
|
||||
Self::U64(u) => Some(*u),
|
||||
Self::Str(s) => s.parse::<u64>().ok(),
|
||||
_ => None
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ pub struct MpsMusicItem {
|
|||
impl MpsMusicItem {
|
||||
pub fn map_row(row: &rusqlite::Row) -> rusqlite::Result<Self> {
|
||||
let item = DbMusicItem::map_row(row)?;
|
||||
Ok(Self{
|
||||
Ok(Self {
|
||||
title: item.title,
|
||||
filename: item.filename,
|
||||
})
|
||||
|
|
5
mps-interpreter/src/processing/mod.rs
Normal file
5
mps-interpreter/src/processing/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
mod sql;
|
||||
|
||||
pub mod database {
|
||||
pub use super::sql::{MpsDatabaseQuerier, MpsSQLiteExecutor, QueryOp, QueryResult};
|
||||
}
|
167
mps-interpreter/src/processing/sql.rs
Normal file
167
mps-interpreter/src/processing/sql.rs
Normal 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())
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
use std::iter::Iterator;
|
||||
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::tokens::{MpsTokenReader, MpsTokenizer};
|
||||
use super::{MpsContext, MpsInterpretor, MpsMusicItem};
|
||||
|
||||
pub struct MpsRunnerSettings<T: MpsTokenReader> {
|
||||
pub vocabulary: MpsLanguageDictionary,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use std::fmt::{Debug, Display, Formatter, Error};
|
||||
use std::fmt::{Debug, Display, Error, Formatter};
|
||||
|
||||
use crate::lang::MpsLanguageError;
|
||||
|
||||
|
@ -11,14 +11,22 @@ pub struct ParseError {
|
|||
|
||||
impl Display for ParseError {
|
||||
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 {
|
||||
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 {
|
||||
|
|
|
@ -2,6 +2,6 @@ mod error;
|
|||
mod token_enum;
|
||||
mod tokenizer;
|
||||
|
||||
pub use error::{ParseError, MpsTokenError};
|
||||
pub use error::{MpsTokenError, ParseError};
|
||||
pub use token_enum::MpsToken;
|
||||
pub use tokenizer::{MpsTokenizer, MpsTokenReader};
|
||||
pub use tokenizer::{MpsTokenReader, MpsTokenizer};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use std::fmt::{Debug, Display, Formatter, Error};
|
||||
use std::fmt::{Debug, Display, Error, Formatter};
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum MpsToken {
|
||||
|
@ -8,6 +8,8 @@ pub enum MpsToken {
|
|||
Comma,
|
||||
Literal(String),
|
||||
Name(String),
|
||||
//Octothorpe,
|
||||
Comment(String),
|
||||
}
|
||||
|
||||
impl MpsToken {
|
||||
|
@ -17,10 +19,11 @@ impl MpsToken {
|
|||
"(" => Ok(Self::OpenBracket),
|
||||
")" => Ok(Self::CloseBracket),
|
||||
"," => Ok(Self::Comma),
|
||||
//"#" => Ok(Self::Octothorpe),
|
||||
_ => {
|
||||
// name validation
|
||||
let mut ok = true;
|
||||
for invalid_c in ["-", "+", ","] {
|
||||
for invalid_c in ["-", "+", ",", " ", "/", "\n", "\r", "!", "?"] {
|
||||
if s.contains(invalid_c) {
|
||||
ok = false;
|
||||
break;
|
||||
|
@ -31,42 +34,56 @@ impl MpsToken {
|
|||
} else {
|
||||
Err(s)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_sql(&self) -> bool {
|
||||
match self {
|
||||
Self::Sql => true,
|
||||
_ => false
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_open_bracket(&self) -> bool {
|
||||
match self {
|
||||
Self::OpenBracket => true,
|
||||
_ => false
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_close_bracket(&self) -> bool {
|
||||
match self {
|
||||
Self::CloseBracket => true,
|
||||
_ => false
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_literal(&self) -> bool {
|
||||
match self {
|
||||
Self::Literal(_) => true,
|
||||
_ => false
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_name(&self) -> bool {
|
||||
match self {
|
||||
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::Literal(s) => write!(f, "\"{}\"", s),
|
||||
Self::Name(s) => write!(f, "{}", s),
|
||||
//Self::Octothorpe => write!(f, "#"),
|
||||
Self::Comment(s) => write!(f, "//{}", s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,30 +1,39 @@
|
|||
use std::collections::VecDeque;
|
||||
|
||||
use super::ParseError;
|
||||
use super::MpsToken;
|
||||
use super::ParseError;
|
||||
|
||||
pub trait MpsTokenReader {
|
||||
fn current_line(&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;
|
||||
}
|
||||
|
||||
pub struct MpsTokenizer<R> where R: std::io::Read {
|
||||
pub struct MpsTokenizer<R>
|
||||
where
|
||||
R: std::io::Read,
|
||||
{
|
||||
reader: R,
|
||||
fsm: ReaderStateMachine,
|
||||
line: 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 {
|
||||
Self {
|
||||
reader: reader,
|
||||
fsm: ReaderStateMachine::Start{},
|
||||
fsm: ReaderStateMachine::Start {},
|
||||
line: 0,
|
||||
column: 0,
|
||||
}
|
||||
|
@ -35,7 +44,12 @@ impl<R> MpsTokenizer<R> where R: std::io::Read {
|
|||
// first read special case
|
||||
// always read before checking if end of 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)
|
||||
}
|
||||
self.do_tracking(byte_buf[0]);
|
||||
|
@ -48,29 +62,38 @@ impl<R> MpsTokenizer<R> where R: std::io::Read {
|
|||
}
|
||||
// handle parse endings
|
||||
match self.fsm {
|
||||
ReaderStateMachine::EndLiteral{} => {
|
||||
ReaderStateMachine::EndLiteral {} => {
|
||||
let literal = String::from_utf8(bigger_buf.clone())
|
||||
.map_err(|e| self.error(format!("UTF-8 encoding error: {}", e)))?;
|
||||
buf.push_back(MpsToken::Literal(literal));
|
||||
bigger_buf.clear();
|
||||
},
|
||||
ReaderStateMachine::EndToken{} => {
|
||||
let token = String::from_utf8(bigger_buf.clone())
|
||||
ReaderStateMachine::EndComment {} => {
|
||||
let comment = 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)))?
|
||||
);
|
||||
buf.push_back(MpsToken::Comment(comment));
|
||||
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
|
||||
ReaderStateMachine::EndToken {} => {
|
||||
if bigger_buf.len() != 0 { // ignore consecutive end tokens
|
||||
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)))?
|
||||
.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();
|
||||
}
|
||||
|
@ -80,32 +103,42 @@ impl<R> MpsTokenizer<R> where R: std::io::Read {
|
|||
.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)))?
|
||||
.map_err(|e| self.error(format!("Invalid token {}", e)))?,
|
||||
);
|
||||
bigger_buf.clear();
|
||||
},
|
||||
ReaderStateMachine::EndStatement{} => {
|
||||
ReaderStateMachine::EndStatement {} => {
|
||||
// unnecessary; loop will have already exited
|
||||
},
|
||||
ReaderStateMachine::EndOfFile{} => {
|
||||
ReaderStateMachine::EndOfFile {} => {
|
||||
// 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)
|
||||
}
|
||||
self.do_tracking(byte_buf[0]);
|
||||
self.fsm = self.fsm.next_state(byte_buf[0]);
|
||||
}
|
||||
// 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
|
||||
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)))?
|
||||
.map_err(|e| self.error(format!("Invalid token {}", e)))?,
|
||||
);
|
||||
bigger_buf.clear();
|
||||
}
|
||||
|
@ -132,7 +165,7 @@ impl<R> MpsTokenizer<R> where R: std::io::Read {
|
|||
|
||||
impl<R> MpsTokenReader for MpsTokenizer<R>
|
||||
where
|
||||
R: std::io::Read
|
||||
R: std::io::Read,
|
||||
{
|
||||
fn current_line(&self) -> usize {
|
||||
self.line
|
||||
|
@ -142,8 +175,13 @@ where
|
|||
self.column
|
||||
}
|
||||
|
||||
fn next_statements(&mut self, count: usize, buf: &mut VecDeque<MpsToken>) -> Result<(), ParseError> {
|
||||
for _ in 0..count {
|
||||
fn next_statement(
|
||||
&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)?;
|
||||
}
|
||||
Ok(())
|
||||
|
@ -156,94 +194,119 @@ where
|
|||
|
||||
#[derive(Copy, Clone)]
|
||||
enum ReaderStateMachine {
|
||||
Start{}, // beginning of machine, no parsing has occured
|
||||
Regular{
|
||||
Start {}, // beginning of machine, no parsing has occured
|
||||
Regular {
|
||||
out: u8,
|
||||
}, // standard
|
||||
Escaped{
|
||||
Escaped {
|
||||
inside: char, // literal
|
||||
}, // escape character; applied to next character
|
||||
StartTickLiteral{},
|
||||
StartQuoteLiteral{},
|
||||
InsideTickLiteral{
|
||||
StartTickLiteral {},
|
||||
StartQuoteLiteral {},
|
||||
InsideTickLiteral {
|
||||
out: u8,
|
||||
},
|
||||
InsideQuoteLiteral{
|
||||
InsideQuoteLiteral {
|
||||
out: u8,
|
||||
},
|
||||
SingleCharToken {
|
||||
out: u8,
|
||||
},
|
||||
EndLiteral{},
|
||||
EndToken{},
|
||||
EndStatement{},
|
||||
EndOfFile{},
|
||||
Slash {out: u8},
|
||||
Octothorpe {out: u8},
|
||||
Comment {out: u8},
|
||||
EndLiteral {},
|
||||
EndToken {},
|
||||
EndComment {},
|
||||
EndStatement {},
|
||||
EndOfFile {},
|
||||
Invalid { out: u8 },
|
||||
}
|
||||
|
||||
impl ReaderStateMachine {
|
||||
pub fn next_state(self, input: u8) -> Self {
|
||||
let input_char = input as char;
|
||||
match self {
|
||||
Self::Start{}
|
||||
| Self::Regular{..}
|
||||
| Self::SingleCharToken{..}
|
||||
| Self::EndLiteral{}
|
||||
| Self::EndToken{}
|
||||
| Self::EndStatement{} =>
|
||||
match input_char {
|
||||
'\\' => Self::Escaped{inside: '_'},
|
||||
'`' => Self::StartTickLiteral{},
|
||||
'"' => Self::StartQuoteLiteral{},
|
||||
' ' => Self::EndToken{},
|
||||
'\n' | '\r' | ';' => Self::EndStatement{},
|
||||
'\0' => Self::EndOfFile{},
|
||||
'(' | ')' | ',' => Self::SingleCharToken{out: input},
|
||||
_ => Self::Regular{out: input},
|
||||
Self::Start {}
|
||||
| Self::Regular { .. }
|
||||
| Self::SingleCharToken { .. }
|
||||
| Self::EndLiteral {}
|
||||
| Self::EndToken {}
|
||||
| Self::EndComment {}
|
||||
| Self::EndStatement {}
|
||||
| Self::Invalid {..} => match input_char {
|
||||
'\\' => Self::Escaped { inside: '_' },
|
||||
'/' => Self::Slash { out: input },
|
||||
'#' => Self::Octothorpe { out: input },
|
||||
'`' => Self::StartTickLiteral {},
|
||||
'"' => Self::StartQuoteLiteral {},
|
||||
' ' => Self::EndToken {},
|
||||
'\n' | '\r' | ';' => Self::EndStatement {},
|
||||
'\0' => Self::EndOfFile {},
|
||||
'(' | ')' | ',' => Self::SingleCharToken { out: input },
|
||||
_ => Self::Regular { out: input },
|
||||
},
|
||||
Self::Escaped{inside} => match inside {
|
||||
'`' => Self::InsideTickLiteral{out: input},
|
||||
'"' => Self::InsideQuoteLiteral{out: input},
|
||||
'_' | _ => Self::Regular{out: input}
|
||||
Self::Escaped { inside } => match inside {
|
||||
'`' => Self::InsideTickLiteral { out: input },
|
||||
'"' => Self::InsideQuoteLiteral { out: input },
|
||||
'_' | _ => Self::Regular { out: input },
|
||||
},
|
||||
Self::StartTickLiteral{}
|
||||
| Self::InsideTickLiteral{..} =>
|
||||
match input_char {
|
||||
'\\' => Self::Escaped{inside: '`'},
|
||||
'`' => Self::EndLiteral{},
|
||||
_ => Self::InsideTickLiteral{out: input},
|
||||
Self::StartTickLiteral {} | Self::InsideTickLiteral { .. } => match input_char {
|
||||
'\\' => Self::Escaped { inside: '`' },
|
||||
'`' => Self::EndLiteral {},
|
||||
'\0' => Self::Invalid { out: input },
|
||||
_ => Self::InsideTickLiteral { out: input },
|
||||
},
|
||||
Self::StartQuoteLiteral{}
|
||||
| Self::InsideQuoteLiteral{..} =>
|
||||
match input_char {
|
||||
'\\' => Self::Escaped{inside: '"'},
|
||||
'"' => Self::EndLiteral{},
|
||||
_ => Self::InsideQuoteLiteral{out: input},
|
||||
Self::StartQuoteLiteral {} | Self::InsideQuoteLiteral { .. } => match input_char {
|
||||
'\\' => Self::Escaped { inside: '"' },
|
||||
'"' => Self::EndLiteral {},
|
||||
'\0' => Self::Invalid { 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 {
|
||||
match self {
|
||||
Self::EndStatement{} => true,
|
||||
_ => false
|
||||
Self::EndStatement {} => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_end_of_file(&self) -> bool {
|
||||
match self {
|
||||
Self::EndOfFile{} => true,
|
||||
_ => false
|
||||
Self::EndOfFile {} => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn output(&self) -> Option<u8> {
|
||||
match self {
|
||||
Self::Regular{ out, ..}
|
||||
| Self::SingleCharToken{ out, ..}
|
||||
| Self::InsideTickLiteral{ out, ..}
|
||||
| Self::InsideQuoteLiteral{ out, ..} => Some(*out),
|
||||
_ => None
|
||||
Self::Regular { out, .. }
|
||||
| Self::SingleCharToken { out, .. }
|
||||
| Self::InsideTickLiteral { out, .. }
|
||||
| Self::InsideQuoteLiteral { out, .. }
|
||||
| Self::Slash { out, .. }
|
||||
| Self::Octothorpe { out, ..}
|
||||
| Self::Comment { out, .. }
|
||||
| Self::Invalid { out, .. } => Some(*out),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
use std::io::Cursor;
|
||||
use std::collections::VecDeque;
|
||||
use mps_interpreter::*;
|
||||
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]
|
||||
fn parse_line() -> Result<(), ParseError> {
|
||||
|
@ -34,9 +34,8 @@ fn parse_line() -> Result<(), ParseError> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn execute_line() -> Result<(), Box<dyn MpsLanguageError>> {
|
||||
let cursor = Cursor::new("sql(`SELECT * FROM songs ORDER BY artist;`)");
|
||||
fn execute_single_line(line: &str, should_be_emtpy: bool) -> Result<(), Box<dyn MpsLanguageError>> {
|
||||
let cursor = Cursor::new(line);
|
||||
|
||||
let tokenizer = MpsTokenizer::new(cursor);
|
||||
let interpreter = MpsInterpretor::with_standard_vocab(tokenizer);
|
||||
|
@ -44,14 +43,36 @@ fn execute_line() -> Result<(), Box<dyn MpsLanguageError>> {
|
|||
let mut count = 0;
|
||||
for result in interpreter {
|
||||
if let Ok(item) = result {
|
||||
count +=1;
|
||||
if count > 100 {continue;} // no need to spam the rest of the songs
|
||||
count += 1;
|
||||
if count > 100 {
|
||||
continue;
|
||||
} // no need to spam the rest of the songs
|
||||
println!("Got song `{}` (file: `{}`)", item.title, item.filename);
|
||||
} else {
|
||||
println!("Got error while iterating (executing)");
|
||||
result?;
|
||||
}
|
||||
}
|
||||
if should_be_emtpy {
|
||||
assert_eq!(count, 0);
|
||||
} else {
|
||||
assert_ne!(count, 0); // database is populated
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue