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"
|
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"
|
||||||
] }
|
] }
|
||||||
|
|
|
@ -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()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
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)?;
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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())
|
|
||||||
}
|
|
||||||
|
|
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 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 {}
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
})
|
})
|
||||||
|
|
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::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,
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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};
|
||||||
|
|
|
@ -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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue