Refactor vocab and add simple filters

This commit is contained in:
NGnius (Graham) 2021-12-29 12:12:58 -05:00
parent 9b6472b072
commit 5fab151451
34 changed files with 1973 additions and 126 deletions

View file

@ -8,8 +8,8 @@ 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"
] } ] }
dirs = { version = "4.0.0", optional = true} dirs = { version = "4.0.0" }
[features] [features]
default = [ "music_library" ] default = [ "music_library" ]
music_library = [ "symphonia", "dirs" ] # song metadata parsing and database auto-population music_library = [ "symphonia" ] # song metadata parsing and database auto-population

View file

@ -1,23 +1,18 @@
use super::processing::database::{MpsDatabaseQuerier, MpsSQLiteExecutor}; use super::processing::database::{MpsDatabaseQuerier, MpsSQLiteExecutor};
use super::processing::general::{MpsVariableStorer, MpsOpStorage};
use std::fmt::{Debug, Display, Error, Formatter}; use std::fmt::{Debug, Display, Error, Formatter};
#[derive(Debug)] #[derive(Debug)]
pub struct MpsContext { pub struct MpsContext {
pub database: Box<dyn MpsDatabaseQuerier>, pub database: Box<dyn MpsDatabaseQuerier>,
pub variables: Box<dyn MpsVariableStorer>,
} }
impl Default for MpsContext { impl Default for MpsContext {
fn default() -> Self { fn default() -> Self {
Self { Self {
database: Box::new(MpsSQLiteExecutor::default()), database: Box::new(MpsSQLiteExecutor::default()),
} variables: Box::new(MpsOpStorage::default()),
}
}
impl std::clone::Clone for MpsContext {
fn clone(&self) -> Self {
Self {
database: Box::new(MpsSQLiteExecutor::default()),
} }
} }
} }

View file

@ -113,6 +113,8 @@ where
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) => {
#[cfg(debug_assertions)]
if self.buffer.len() != 0 {panic!("Token buffer was not emptied! (rem: {:?})", self.buffer)}
stmt.enter(self.context.take().unwrap_or_else(|| MpsContext::default())); stmt.enter(self.context.take().unwrap_or_else(|| MpsContext::default()));
self.current_stmt = Some(stmt); self.current_stmt = Some(stmt);
let next_item = self.current_stmt.as_mut().unwrap().next(); let next_item = self.current_stmt.as_mut().unwrap().next();
@ -148,7 +150,14 @@ fn box_error_with_ctx<E: MpsLanguageError + 'static>(
pub(crate) fn standard_vocab(vocabulary: &mut MpsLanguageDictionary) { pub(crate) fn standard_vocab(vocabulary: &mut MpsLanguageDictionary) {
vocabulary vocabulary
// high-priority vocabulary (low-priority may accept this, but will not execute as expected)
.add(crate::lang::vocabulary::filters::empty_filter())
.add(crate::lang::vocabulary::filters::field_filter())
// low-priority (more forgiving statements which may not parse complete statement)
.add(crate::lang::vocabulary::SqlStatementFactory) .add(crate::lang::vocabulary::SqlStatementFactory)
.add(crate::lang::vocabulary::SimpleSqlStatementFactory) .add(crate::lang::vocabulary::SimpleSqlStatementFactory)
.add(crate::lang::vocabulary::CommentStatementFactory); .add(crate::lang::vocabulary::CommentStatementFactory)
.add(crate::lang::vocabulary::RepeatStatementFactory)
.add(crate::lang::vocabulary::AssignStatementFactory)
.add(crate::lang::vocabulary::SqlInitStatementFactory);
} }

View file

@ -1,3 +1,5 @@
use std::path::Path;
pub const DEFAULT_SQLITE_FILEPATH: &str = "metadata.mps.sqlite"; pub const DEFAULT_SQLITE_FILEPATH: &str = "metadata.mps.sqlite";
pub trait DatabaseObj: Sized { pub trait DatabaseObj: Sized {
@ -9,11 +11,25 @@ pub trait DatabaseObj: Sized {
} }
pub fn generate_default_db() -> rusqlite::Result<rusqlite::Connection> { pub fn generate_default_db() -> rusqlite::Result<rusqlite::Connection> {
let db_exists = std::path::Path::new(DEFAULT_SQLITE_FILEPATH).exists(); generate_db(super::utility::music_folder(), DEFAULT_SQLITE_FILEPATH, true)
let conn = rusqlite::Connection::open(DEFAULT_SQLITE_FILEPATH)?; }
pub fn generate_db<P1: AsRef<Path>, P2: AsRef<Path>>(
music_path: P1,
sqlite_path: P2,
generate: bool
) -> rusqlite::Result<rusqlite::Connection> {
let music_path = music_path.as_ref();
let sqlite_path = sqlite_path.as_ref();
let db_exists = std::path::Path::new(sqlite_path).exists();
#[cfg(not(feature = "music_library"))]
let conn = rusqlite::Connection::open(sqlite_path)?;
#[cfg(feature = "music_library")]
let mut conn = rusqlite::Connection::open(sqlite_path)?;
// 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 and database structure
if db_exists { #[cfg(not(feature = "music_library"))]
if db_exists && !generate {
return Ok(conn); return Ok(conn);
} }
// build db tables // build db tables
@ -56,11 +72,18 @@ pub fn generate_default_db() -> rusqlite::Result<rusqlite::Connection> {
)?; )?;
// generate data and store in db // generate data and store in db
#[cfg(feature = "music_library")] #[cfg(feature = "music_library")]
{ if generate {
let music_path = super::utility::music_folder();
match crate::music::build_library(&music_path) { let mut lib = crate::music::MpsLibrary::new();
Ok(lib) => { if db_exists {
let mut song_insert = conn.prepare( crate::music::build_library_from_sqlite(&conn, &mut lib)?;
}
lib.clear_modified();
match crate::music::build_library_from_files(&music_path, &mut lib) {
Ok(_) => if lib.is_modified() {
let transaction = conn.transaction()?;
{
let mut song_insert = transaction.prepare(
"INSERT OR REPLACE INTO songs ( "INSERT OR REPLACE INTO songs (
song_id, song_id,
title, title,
@ -75,7 +98,7 @@ pub fn generate_default_db() -> rusqlite::Result<rusqlite::Connection> {
song_insert.execute(song.to_params().as_slice())?; song_insert.execute(song.to_params().as_slice())?;
} }
let mut metadata_insert = conn.prepare( let mut metadata_insert = transaction.prepare(
"INSERT OR REPLACE INTO metadata ( "INSERT OR REPLACE INTO metadata (
meta_id, meta_id,
plays, plays,
@ -89,7 +112,7 @@ pub fn generate_default_db() -> rusqlite::Result<rusqlite::Connection> {
metadata_insert.execute(meta.to_params().as_slice())?; metadata_insert.execute(meta.to_params().as_slice())?;
} }
let mut artist_insert = conn.prepare( let mut artist_insert = transaction.prepare(
"INSERT OR REPLACE INTO artists ( "INSERT OR REPLACE INTO artists (
artist_id, artist_id,
name, name,
@ -100,7 +123,7 @@ pub fn generate_default_db() -> rusqlite::Result<rusqlite::Connection> {
artist_insert.execute(artist.to_params().as_slice())?; artist_insert.execute(artist.to_params().as_slice())?;
} }
let mut album_insert = conn.prepare( let mut album_insert = transaction.prepare(
"INSERT OR REPLACE INTO albums ( "INSERT OR REPLACE INTO albums (
album_id, album_id,
title, title,
@ -113,7 +136,7 @@ pub fn generate_default_db() -> rusqlite::Result<rusqlite::Connection> {
album_insert.execute(album.to_params().as_slice())?; album_insert.execute(album.to_params().as_slice())?;
} }
let mut genre_insert = conn.prepare( let mut genre_insert = transaction.prepare(
"INSERT OR REPLACE INTO genres ( "INSERT OR REPLACE INTO genres (
genre_id, genre_id,
title title
@ -122,7 +145,9 @@ pub fn generate_default_db() -> rusqlite::Result<rusqlite::Connection> {
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())?;
} }
} }
transaction.commit()?;
},
Err(e) => println!("Unable to load music from {}: {}", music_path.display(), e), Err(e) => println!("Unable to load music from {}: {}", music_path.display(), e),
} }
} }

View file

@ -19,6 +19,7 @@ impl MpsLanguageDictionary {
&self, &self,
tokens: &mut VecDeque<MpsToken>, tokens: &mut VecDeque<MpsToken>,
) -> Result<Box<dyn MpsOp>, SyntaxError> { ) -> Result<Box<dyn MpsOp>, SyntaxError> {
//println!("try_build_statement with tokens {:?}", tokens);
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, self); return factory.build_op_boxed(tokens, self);

View file

@ -1,6 +1,6 @@
use std::fmt::{Debug, Display, Error, Formatter}; use std::fmt::{Debug, Display, Error, Formatter};
use super::MpsOp; use super::PseudoOp;
use crate::tokens::MpsToken; use crate::tokens::MpsToken;
#[derive(Debug)] #[derive(Debug)]
@ -28,7 +28,7 @@ impl MpsLanguageError for SyntaxError {
#[derive(Debug)] #[derive(Debug)]
pub struct RuntimeError { pub struct RuntimeError {
pub line: usize, pub line: usize,
pub op: Box<dyn MpsOp>, pub op: PseudoOp,
pub msg: String, pub msg: String,
} }

View file

@ -0,0 +1,281 @@
use std::collections::VecDeque;
use std::fmt::{Debug, Display, Error, Formatter};
use std::iter::Iterator;
use std::marker::PhantomData;
use crate::MpsContext;
use crate::MpsMusicItem;
use crate::tokens::MpsToken;
use crate::lang::{MpsOp, PseudoOp, BoxedMpsOpFactory};
use crate::lang::{RuntimeError, SyntaxError};
use crate::lang::MpsLanguageDictionary;
use crate::processing::general::MpsType;
use crate::lang::utility::{assert_token_raw, assert_token};
use crate::processing::OpGetter;
pub trait MpsFilterPredicate: Clone + Debug + Display {
fn matches(&mut self, item: &MpsMusicItem, ctx: &mut MpsContext, op: &mut OpGetter) -> Result<bool, RuntimeError>;
}
pub trait MpsFilterFactory<P: MpsFilterPredicate + 'static> {
fn is_filter(&self, tokens: &VecDeque<&MpsToken>) -> bool;
fn build_filter(
&self,
tokens: &mut VecDeque<MpsToken>,
dict: &MpsLanguageDictionary,
) -> Result<P, SyntaxError>;
}
#[derive(Debug, Clone)]
enum VariableOrOp {
Variable(String),
Op(PseudoOp),
}
impl Display for VariableOrOp {
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
match self {
Self::Variable(s) => write!(f, "{}", s),
Self::Op(op) => write!(f, "{}", op)
}
}
}
#[derive(Debug)]
pub struct MpsFilterStatement<P: MpsFilterPredicate + 'static> {
predicate: P,
iterable: VariableOrOp,
context: Option<MpsContext>,
}
impl<P: MpsFilterPredicate + 'static> std::clone::Clone for MpsFilterStatement<P> {
fn clone(&self) -> Self {
Self {
predicate: self.predicate.clone(),
iterable: self.iterable.clone(),
context: None,
}
}
}
impl<P: MpsFilterPredicate + 'static> Display for MpsFilterStatement<P> {
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
write!(f, "{}.({})", self.iterable, self.predicate)
}
}
impl<P: MpsFilterPredicate + 'static> MpsOp for MpsFilterStatement<P> {
fn enter(&mut self, ctx: MpsContext) {
self.context = Some(ctx)
}
fn escape(&mut self) -> MpsContext {
self.context.take().unwrap()
}
}
impl<P: MpsFilterPredicate + 'static> Iterator for MpsFilterStatement<P> {
type Item = Result<MpsMusicItem, RuntimeError>;
fn next(&mut self) -> Option<Self::Item> {
let self_clone = self.clone();
let self_clone2 = self_clone.clone();
let mut op_getter = move || (Box::new(self_clone.clone()) as Box<dyn MpsOp>).into();
//let ctx = self.context.as_mut().unwrap();
match &mut self.iterable {
VariableOrOp::Op(op) =>
match op.try_real() {
Ok(real_op) => {
let ctx = self.context.take().unwrap();
real_op.enter(ctx);
let mut maybe_result = None;
while let Some(item) = real_op.next() {
let mut ctx = real_op.escape();
match item {
Err(e) => {
//self.context = Some(op.escape());
maybe_result = Some(Err(e));
self.context = Some(ctx);
break;
},
Ok(item) => {
let matches_result = self.predicate.matches(&item, &mut ctx, &mut op_getter);
let matches = match matches_result {
Err(e) => {
maybe_result = Some(Err(e));
self.context = Some(ctx);
break;
},
Ok(b) => b,
};
if matches {
//self.context = Some(op.escape());
maybe_result = Some(Ok(item));
self.context = Some(ctx);
break;
}
}
}
real_op.enter(ctx);
}
if self.context.is_none() {
self.context = Some(real_op.escape());
}
maybe_result
},
Err(e) => return Some(Err(e)),
},
VariableOrOp::Variable(variable_name) => {
let mut variable = match self.context.as_mut().unwrap().variables.remove(
&variable_name,
&mut op_getter
) {
Ok(MpsType::Op(op)) => op,
Ok(x) => return Some(Err(RuntimeError {
line: 0,
op: (Box::new(self_clone2.clone()) as Box<dyn MpsOp>).into(),
msg: format!("Expected operation/iterable type in variable {}, got {}", &variable_name, x)
})),
Err(e) => return Some(Err(e))
};
let mut maybe_result = None;
let ctx = self.context.take().unwrap();
variable.enter(ctx);
while let Some(item) = variable.next() {
let mut ctx = variable.escape();
match item {
Err(e) => {
maybe_result = Some(Err(e));
self.context = Some(ctx);
break;
},
Ok(item) => {
let matches_result = self.predicate.matches(&item, &mut ctx, &mut op_getter);
let matches = match matches_result {
Err(e) => {
maybe_result = Some(Err(e));
self.context = Some(ctx);
break;
},
Ok(b) => b,
};
if matches {
maybe_result = Some(Ok(item));
self.context = Some(ctx);
break;
}
}
}
variable.enter(ctx);
}
if self.context.is_none() {
self.context = Some(variable.escape());
}
match self.context.as_mut().unwrap().variables.declare(
&variable_name,
MpsType::Op(variable),
&mut op_getter) {
Err(e) => return Some(Err(e)),
Ok(_) => maybe_result
}
}
}
}
}
pub struct MpsFilterStatementFactory<P: MpsFilterPredicate + 'static, F: MpsFilterFactory<P> + 'static> {
filter_factory: F,
idc: PhantomData<P>,
}
impl<P: MpsFilterPredicate + 'static, F: MpsFilterFactory<P> + 'static> MpsFilterStatementFactory<P, F> {
pub fn new(factory: F) -> Self {
Self {
filter_factory: factory,
idc: PhantomData::default(),
}
}
}
impl<P: MpsFilterPredicate + 'static, F: MpsFilterFactory<P> + 'static> BoxedMpsOpFactory for MpsFilterStatementFactory<P, F> {
fn is_op_boxed(&self, tokens: &VecDeque<MpsToken>) -> bool {
let tokens_len = tokens.len();
if last_open_bracket_is_after_dot(tokens) {
let start_of_predicate = last_dot_before_open_bracket(tokens) + 2; // .(predicate)
if start_of_predicate > tokens_len - 1 {
false
} else {
let tokens2: VecDeque<&MpsToken> = VecDeque::from_iter(tokens.range(start_of_predicate..tokens_len-1));
self.filter_factory.is_filter(&tokens2)
}
} else {
false
}
}
fn build_op_boxed(
&self,
tokens: &mut VecDeque<MpsToken>,
dict: &MpsLanguageDictionary,
) -> Result<Box<dyn MpsOp>, SyntaxError> {
let start_of_op = last_dot_before_open_bracket(tokens);
let op;
if start_of_op == 1 && tokens[0].is_name() {
// variable_name.(predicate)
let variable_name = assert_token(|t| match t {
MpsToken::Name(s) => Some(s),
_ => None
}, MpsToken::Name("variable_name".into()), tokens)?;
op = VariableOrOp::Variable(variable_name);
} else {
// <some other op>.(predicate)
//let mut new_tokens = tokens.range(0..start_of_op).map(|x| x.to_owned()).collect();
let end_tokens = tokens.split_off(start_of_op);
let inner_op = dict.try_build_statement(tokens)?;
tokens.extend(end_tokens);
op = VariableOrOp::Op(inner_op.into());
}
assert_token_raw(MpsToken::Dot, tokens)?;
assert_token_raw(MpsToken::OpenBracket, tokens)?;
let filter = self.filter_factory.build_filter(tokens, dict)?;
assert_token_raw(MpsToken::CloseBracket, tokens)?;
Ok(Box::new(MpsFilterStatement {
predicate: filter,
iterable: op,
context: None,
}))
}
}
fn last_open_bracket_is_after_dot(tokens: &VecDeque<MpsToken>) -> bool {
let mut open_bracket_found = false;
for i in (0..tokens.len()).rev() {
if tokens[i].is_open_bracket() {
open_bracket_found = true;
} else if open_bracket_found {
if tokens[i].is_dot() {
return true
} else {
return false
}
}
}
false
}
fn last_dot_before_open_bracket(tokens: &VecDeque<MpsToken>) -> usize {
let mut open_bracket_found = false;
for i in (0..tokens.len()).rev() {
if tokens[i].is_open_bracket() {
open_bracket_found = true;
} else if open_bracket_found {
if tokens[i].is_dot() {
return i
} else {
return 0
}
}
}
0
}

View file

@ -1,27 +1,28 @@
mod comment;
mod db_items; mod db_items;
mod dictionary; mod dictionary;
mod error; mod error;
mod filter;
mod operation; mod operation;
mod sql_query; mod pseudo_op;
mod sql_simple_query; mod repeated_meme;
//mod statement; //mod statement;
mod type_primitives;
pub(crate) mod utility; pub(crate) mod utility;
pub use dictionary::MpsLanguageDictionary; pub use dictionary::MpsLanguageDictionary;
pub use error::{MpsLanguageError, RuntimeError, SyntaxError}; pub use error::{MpsLanguageError, RuntimeError, SyntaxError};
pub use filter::{MpsFilterFactory, MpsFilterPredicate, MpsFilterStatement, MpsFilterStatementFactory};
pub use operation::{BoxedMpsOpFactory, MpsOp, MpsOpFactory, SimpleMpsOpFactory}; pub use operation::{BoxedMpsOpFactory, MpsOp, MpsOpFactory, SimpleMpsOpFactory};
pub use pseudo_op::PseudoOp;
pub use repeated_meme::{RepeatedTokens, repeated_tokens};
//pub(crate) use statement::MpsStatement; //pub(crate) use statement::MpsStatement;
pub use type_primitives::MpsTypePrimitive;
pub mod vocabulary { 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 mod db {
pub use super::db_items::{ pub use super::db_items::{
generate_default_db, DatabaseObj, DbAlbumItem, DbArtistItem, DbGenreItem, DbMetaItem, generate_default_db, generate_db, DatabaseObj, DbAlbumItem, DbArtistItem, DbGenreItem, DbMetaItem,
DbMusicItem, DEFAULT_SQLITE_FILEPATH, DbMusicItem, DEFAULT_SQLITE_FILEPATH,
}; };
} }

View file

@ -0,0 +1,78 @@
use std::fmt::{Debug, Display, Error, Formatter};
use super::MpsOp;
use super::RuntimeError;
/// Mps operation where clones of it emulate the Display behaviour without cloning the data
#[derive(Debug)]
pub enum PseudoOp {
Real(Box<dyn MpsOp>),
Fake(String)
}
impl PseudoOp {
pub fn try_real(&mut self) -> Result<&mut Box<dyn MpsOp>, RuntimeError> {
match self {
Self::Real(op) => Ok(op),
Self::Fake(_) => Err(RuntimeError {
line: 0,
op: self.clone(),
msg: "PseudoOp::Fake is not a real MpsOp".into(),
})
}
}
pub fn unwrap_real(self) -> Result<Box<dyn MpsOp>, RuntimeError> {
match self {
Self::Real(op) => {
let result = Ok(op);
result
},
Self::Fake(_) => Err(RuntimeError {
line: 0,
op: self.clone(),
msg: "PseudoOp::Fake is not a real MpsOp".into(),
})
}
}
#[inline]
pub fn is_real(&self) -> bool {
match self {
Self::Real(_) => true,
_ => false,
}
}
#[inline]
pub fn is_fake(&self) -> bool {
match self {
Self::Fake(_) => true,
_ => false,
}
}
}
impl Clone for PseudoOp {
fn clone(&self) -> Self {
match self {
Self::Real(op) => Self::Fake(format!("{}", op)),
Self::Fake(s) => Self::Fake(s.clone())
}
}
}
impl Display for PseudoOp {
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
match self {
Self::Real(op) => write!(f, "{}", op),
Self::Fake(s) => write!(f, "{}", s)
}
}
}
impl std::convert::From<Box<dyn MpsOp>> for PseudoOp {
fn from(item: Box<dyn MpsOp>) -> Self {
Self::Real(item)
}
}

View file

@ -0,0 +1,55 @@
use std::collections::VecDeque;
use crate::lang::SyntaxError;
use crate::lang::utility::{check_token_raw, assert_token_raw};
use crate::tokens::MpsToken;
/// Convenient parser for repeated patterns of tokens
pub struct RepeatedTokens<
X: 'static,
F1: FnMut(&mut VecDeque<MpsToken>) -> Result<Option<X>, SyntaxError>,
F2: FnMut(&mut VecDeque<MpsToken>) -> Result<bool, SyntaxError> >
{
pattern_ingest: F1,
separator_ingest: F2,
}
impl<
X: 'static,
F1: FnMut(&mut VecDeque<MpsToken>) -> Result<Option<X>, SyntaxError>,
F2: FnMut(&mut VecDeque<MpsToken>) -> Result<bool, SyntaxError> >
RepeatedTokens<X, F1, F2>
{
pub fn ingest_all(&mut self, tokens: &mut VecDeque<MpsToken>) -> Result<Vec<X>, SyntaxError> {
let mut result = Vec::<X>::new();
match (self.pattern_ingest)(tokens)? {
Some(x) => result.push(x),
None => return Ok(result),
}
while (self.separator_ingest)(tokens)? {
match (self.pattern_ingest)(tokens)? {
Some(x) => result.push(x),
None => break
}
}
Ok(result)
}
}
pub fn repeated_tokens<X, F1: FnMut(&mut VecDeque<MpsToken>) -> Result<Option<X>, SyntaxError>>(
ingestor: F1,
separator: MpsToken,
) -> RepeatedTokens<X, F1, impl FnMut(&mut VecDeque<MpsToken>) -> Result<bool, SyntaxError>> {
RepeatedTokens {
pattern_ingest: ingestor,
separator_ingest: move |tokens| {
if tokens.len() > 0 && check_token_raw(separator.clone(), &tokens[0]) {
assert_token_raw(separator.clone(), tokens)?;
Ok(true)
} else {
Ok(false)
}
},
//parsed: Vec::new(),
}
}

View file

@ -0,0 +1,98 @@
//! Basic types for MPS
use std::fmt::{Debug, Display, Error, Formatter};
#[derive(Debug, Clone)]
pub enum MpsTypePrimitive {
String(String),
Int(i64),
UInt(u64),
Float(f64),
Bool(bool),
}
impl MpsTypePrimitive {
#[inline]
pub fn compare(&self, other: &Self) -> Result<i8, String> {
let result = match self {
Self::String(s1) => {
match other {
Self::String(s2) => Some(map_ordering(s1.cmp(s2))),
_ => None,
}
},
Self::Int(i1) => {
match other {
Self::Int(i2) => Some(map_ordering(i1.cmp(i2))),
Self::UInt(i2) => Some(map_ordering((*i1 as i128).cmp(&(*i2 as i128)))),
Self::Float(i2) => Some(
map_ordering((*i1 as f64).partial_cmp(&(*i2 as f64)).unwrap_or(std::cmp::Ordering::Less))
),
_ => None,
}
},
Self::UInt(u1) => {
match other {
Self::UInt(u2) => Some(map_ordering(u1.cmp(u2))),
Self::Int(u2) => Some(map_ordering((*u1 as i128).cmp(&(*u2 as i128)))),
Self::Float(u2) => Some(
map_ordering((*u1 as f64).partial_cmp(&(*u2 as f64)).unwrap_or(std::cmp::Ordering::Less))
),
_ => None,
}
},
Self::Float(f1) => {
match other {
Self::Float(f2) => Some(map_ordering(f1.partial_cmp(f2).unwrap_or(std::cmp::Ordering::Less))),
Self::Int(f2) => Some(
map_ordering(f1.partial_cmp(&(*f2 as f64)).unwrap_or(std::cmp::Ordering::Less))
),
Self::UInt(f2) => Some(
map_ordering(f1.partial_cmp(&(*f2 as f64)).unwrap_or(std::cmp::Ordering::Less))
),
_ => None
}
},
Self::Bool(b1) => {
match other {
Self::Bool(b2) => {
if *b2 == *b1 {
Some(0)
} else if *b1{
Some(1)
} else {
Some(-1)
}
},
_ => None
}
}
};
match result {
Some(x) => Ok(x),
None => Err(format!("Cannot compare {} to {}: incompatible types", self, other))
}
}
}
impl Display for MpsTypePrimitive {
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
match self {
Self::String(s) => write!(f, "(String) {}", s),
Self::Int(i) => write!(f, "(Int) {}", *i),
Self::UInt(u) => write!(f, "(UInt) {}", *u),
Self::Float(f_) => write!(f, "(Float) {}", *f_),
Self::Bool(b) => write!(f, "(Bool) {}", *b),
}
}
}
#[inline]
fn map_ordering(ordering: std::cmp::Ordering) -> i8 {
match ordering {
std::cmp::Ordering::Less => -1,
std::cmp::Ordering::Equal => 0,
std::cmp::Ordering::Greater => 1,
}
}

View file

@ -2,18 +2,19 @@ use std::collections::VecDeque;
#[cfg(feature = "music_library")] #[cfg(feature = "music_library")]
use std::path::PathBuf; use std::path::PathBuf;
use crate::tokens::MpsToken;
use super::SyntaxError; use super::SyntaxError;
use crate::tokens::MpsToken;
use super::MpsTypePrimitive;
pub fn assert_token<T, F: FnOnce(MpsToken) -> Option<T>>( pub fn assert_token<T, F: FnOnce(MpsToken) -> Option<T>>(
caster: F, caster: F,
token: MpsToken, token: MpsToken,
tokens: &mut VecDeque<MpsToken> tokens: &mut VecDeque<MpsToken>,
) -> Result<T, SyntaxError> { ) -> Result<T, SyntaxError> {
if let Some(out) = caster(tokens.pop_front().unwrap()) { if let Some(out) = caster(tokens.pop_front().unwrap()) {
Ok(out) Ok(out)
} else { } else {
Err(SyntaxError{ Err(SyntaxError {
line: 0, line: 0,
token: token, token: token,
}) })
@ -22,7 +23,7 @@ pub fn assert_token<T, F: FnOnce(MpsToken) -> Option<T>>(
pub fn assert_token_raw( pub fn assert_token_raw(
token: MpsToken, token: MpsToken,
tokens: &mut VecDeque<MpsToken> tokens: &mut VecDeque<MpsToken>,
) -> Result<MpsToken, SyntaxError> { ) -> Result<MpsToken, SyntaxError> {
let result = tokens.pop_front().unwrap(); let result = tokens.pop_front().unwrap();
if std::mem::discriminant(&token) == std::mem::discriminant(&result) { if std::mem::discriminant(&token) == std::mem::discriminant(&result) {
@ -35,7 +36,85 @@ pub fn assert_token_raw(
} }
} }
#[cfg(feature = "music_library")] pub fn check_token_raw(
pub fn music_folder() -> PathBuf { token: MpsToken,
dirs::home_dir().unwrap_or_else(|| PathBuf::from("./")).join("Music") token_target: &MpsToken,
) -> bool {
std::mem::discriminant(&token) == std::mem::discriminant(token_target)
} }
pub fn assert_name(name: &str, tokens: &mut VecDeque<MpsToken>) -> Result<String, SyntaxError> {
match tokens.pop_front().unwrap() {
MpsToken::Name(n) => {
if n == name {
Ok(n)
} else {
Err(
SyntaxError {
line: 0,
token: MpsToken::Name(name.to_owned()),
}
)
}
},
_token => Err(SyntaxError {
line: 0,
token: MpsToken::Name(name.to_owned()),
})
}
}
pub fn check_name(name: &str, token: &MpsToken) -> bool {
match token {
MpsToken::Name(n) => n == name,
_ => false
}
}
pub fn check_is_type(token: &MpsToken) -> bool {
match token {
MpsToken::Literal(_) => true,
MpsToken::Name(s) =>
s.parse::<f64>().is_ok()
|| s.parse::<i64>().is_ok()
|| s.parse::<u64>().is_ok()
|| s == "false"
|| s == "true",
_ => false
}
}
pub fn assert_type(tokens: &mut VecDeque<MpsToken>) -> Result<MpsTypePrimitive, SyntaxError> {
match tokens.pop_front().unwrap() {
MpsToken::Literal(s) => Ok(MpsTypePrimitive::String(s)),
MpsToken::Name(s) => {
if let Ok(f) = s.parse::<f64>() {
Ok(MpsTypePrimitive::Float(f))
} else if let Ok(i) = s.parse::<i64>() {
Ok(MpsTypePrimitive::Int(i))
} else if let Ok(u) = s.parse::<u64>() {
Ok(MpsTypePrimitive::UInt(u))
} else if s == "false" {
Ok(MpsTypePrimitive::Bool(false))
} else if s == "true" {
Ok(MpsTypePrimitive::Bool(true))
} else {
Err(SyntaxError {
line: 0,
token: MpsToken::Name("Float | UInt | Int | Bool".into()),
})
}
},
_token => Err(SyntaxError {
line: 0,
token: MpsToken::Name("Float | UInt | Int | Bool | \"String\"".into()),
})
}
}
pub fn music_folder() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("./"))
.join("Music")
}

View file

@ -6,15 +6,15 @@ use crate::MpsContext;
use crate::MpsMusicItem; use crate::MpsMusicItem;
use crate::tokens::MpsToken; use crate::tokens::MpsToken;
use super::{RuntimeError, SyntaxError}; use crate::lang::{RuntimeError, SyntaxError};
use super::{MpsOp, SimpleMpsOpFactory, MpsOpFactory, BoxedMpsOpFactory}; use crate::lang::{MpsOp, SimpleMpsOpFactory, MpsOpFactory, BoxedMpsOpFactory};
use super::MpsLanguageDictionary; use crate::lang::MpsLanguageDictionary;
use super::utility::assert_token; use crate::lang::utility::assert_token;
#[derive(Debug, Clone)] #[derive(Debug)]
pub struct CommentStatement { pub struct CommentStatement {
comment: String, comment: String,
context: Option<MpsContext> context: Option<MpsContext>,
} }
impl CommentStatement { impl CommentStatement {
@ -35,6 +35,15 @@ impl Display for CommentStatement {
} }
} }
impl std::clone::Clone for CommentStatement {
fn clone(&self) -> Self {
Self {
comment: self.comment.clone(),
context: None,
}
}
}
impl Iterator for CommentStatement { impl Iterator for CommentStatement {
type Item = Result<MpsMusicItem, RuntimeError>; type Item = Result<MpsMusicItem, RuntimeError>;

View file

@ -0,0 +1,48 @@
use std::collections::VecDeque;
use std::fmt::{Debug, Display, Error, Formatter};
use crate::MpsContext;
use crate::MpsMusicItem;
use crate::tokens::MpsToken;
use crate::lang::{MpsFilterPredicate, MpsFilterFactory, MpsFilterStatementFactory};
use crate::lang::{SyntaxError, RuntimeError};
use crate::lang::MpsLanguageDictionary;
use crate::processing::OpGetter;
#[derive(Debug, Clone)]
pub struct EmptyFilter;
impl Display for EmptyFilter {
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
write!(f, "[empty]")
}
}
impl MpsFilterPredicate for EmptyFilter {
fn matches(&mut self, _item: &MpsMusicItem, _ctx: &mut MpsContext, _op: &mut OpGetter) -> Result<bool, RuntimeError> {
Ok(true)
}
}
pub struct EmptyFilterFactory;
impl MpsFilterFactory<EmptyFilter> for EmptyFilterFactory {
fn is_filter(&self, tokens: &VecDeque<&MpsToken>) -> bool {
tokens.len() == 0
}
fn build_filter(
&self,
_tokens: &mut VecDeque<MpsToken>,
_dict: &MpsLanguageDictionary,
) -> Result<EmptyFilter, SyntaxError> {
Ok(EmptyFilter)
}
}
pub type EmptyFilterStatementFactory = MpsFilterStatementFactory<EmptyFilter, EmptyFilterFactory>;
#[inline(always)]
pub fn empty_filter() -> EmptyFilterStatementFactory {
EmptyFilterStatementFactory::new(EmptyFilterFactory)
}

View file

@ -0,0 +1,136 @@
use std::collections::VecDeque;
use std::fmt::{Debug, Display, Error, Formatter};
use crate::MpsContext;
use crate::MpsMusicItem;
use crate::tokens::MpsToken;
use crate::lang::{MpsFilterPredicate, MpsFilterFactory, MpsFilterStatementFactory};
use crate::lang::{SyntaxError, RuntimeError};
use crate::lang::MpsLanguageDictionary;
use crate::lang::MpsTypePrimitive;
use crate::lang::utility::{assert_token, assert_type, check_is_type};
use super::utility::{item_to_primitive_lut, assert_comparison_operator};
use crate::processing::OpGetter;
use crate::processing::general::MpsType;
#[derive(Debug, Clone)]
enum VariableOrValue {
Variable(String),
Value(MpsTypePrimitive),
}
#[derive(Debug, Clone)]
pub struct FieldFilter {
field_name: String,
val: VariableOrValue,
comparison: [i8; 2]
}
impl Display for FieldFilter {
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
// TODO display other comparison operators correctly
match &self.val {
VariableOrValue::Variable(name) => write!(f, "{} == {}", self.field_name, name),
VariableOrValue::Value(t) => write!(f, "{} == {}", self.field_name, t),
}
}
}
impl MpsFilterPredicate for FieldFilter {
fn matches(&mut self, item: &MpsMusicItem, ctx: &mut MpsContext, op: &mut OpGetter) -> Result<bool, RuntimeError> {
let music_item_lut = item_to_primitive_lut(item.to_owned());
let variable = match &self.val {
VariableOrValue::Variable(name) => match ctx.variables.get(&name, op)? {
MpsType::Primitive(t) => Ok(t),
_ => Err(RuntimeError {
line: 0,
op: op(),
msg: format!("Variable {} is not comparable", name),
})
},
VariableOrValue::Value(val) => Ok(val)
}?;
if let Some(field) = music_item_lut.get(&self.field_name) {
let compare = field.compare(variable)
.map_err(|e| RuntimeError {
line: 0,
op: op(),
msg: e,
})?;
let mut is_match = false;
for comparator in self.comparison {
if comparator == compare {
is_match = true;
break;
}
}
Ok(is_match)
} else {
Err(RuntimeError {
line: 0,
op: op(),
msg: format!("Field {} does not exist", &self.field_name),
})
}
}
}
pub struct FieldFilterFactory;
impl MpsFilterFactory<FieldFilter> for FieldFilterFactory {
fn is_filter(&self, tokens: &VecDeque<&MpsToken>) -> bool {
let tokens_len = tokens.len();
(tokens_len == 3 // field > variable OR field < variable
&& tokens[0].is_name()
&& (tokens[1].is_open_angle_bracket() || tokens[1].is_close_angle_bracket())
&& (tokens[2].is_name() || check_is_type(&tokens[2]))
)
|| (tokens_len == 4 // field >= variable OR field <= variable
&& tokens[0].is_name()
&& (tokens[1].is_open_angle_bracket() || tokens[1].is_close_angle_bracket() || tokens[1].is_equals())
&& tokens[2].is_equals()
&& (tokens[3].is_name() || check_is_type(&tokens[3]))
)
}
fn build_filter(
&self,
tokens: &mut VecDeque<MpsToken>,
_dict: &MpsLanguageDictionary,
) -> Result<FieldFilter, SyntaxError> {
let field = assert_token(|t| match t {
MpsToken::Name(n) => Some(n),
_ => None
}, MpsToken::Name("field_name".into()), tokens)?;
let compare_operator = assert_comparison_operator(tokens)?;
if check_is_type(&tokens[0]) {
let value = VariableOrValue::Value(assert_type(tokens)?);
Ok(FieldFilter{
field_name: field,
val: value,
comparison: compare_operator,
})
} else {
let variable = VariableOrValue::Variable(
assert_token(|t| match t {
MpsToken::Name(n) => Some(n),
_ => None
}, MpsToken::Name("variable_name".into()), tokens)?
);
Ok(FieldFilter{
field_name: field,
val: variable,
comparison: compare_operator,
})
}
}
}
pub type FieldFilterStatementFactory = MpsFilterStatementFactory<FieldFilter, FieldFilterFactory>;
#[inline(always)]
pub fn field_filter() -> FieldFilterStatementFactory {
FieldFilterStatementFactory::new(FieldFilterFactory)
}

View file

@ -0,0 +1,6 @@
mod empty_filter;
mod field_filter;
pub(crate) mod utility;
pub use empty_filter::{EmptyFilter, EmptyFilterFactory, EmptyFilterStatementFactory, empty_filter};
pub use field_filter::{FieldFilter, FieldFilterFactory, FieldFilterStatementFactory, field_filter};

View file

@ -0,0 +1,57 @@
use std::collections::VecDeque;
use std::collections::HashMap;
use crate::MpsMusicItem;
use crate::lang::SyntaxError;
use crate::lang::MpsTypePrimitive;
use crate::lang::utility::assert_token_raw;
use crate::tokens::MpsToken;
pub fn item_to_primitive_lut(item: MpsMusicItem) -> HashMap<String, MpsTypePrimitive> {
let mut result = HashMap::new();
result.insert("title".into(), MpsTypePrimitive::String(item.title));
result.insert("artist".into(), MpsTypePrimitive::String(item.artist.unwrap_or("".to_owned())));
result.insert("album".into(), MpsTypePrimitive::String(item.album.unwrap_or("".to_owned())));
result.insert("filename".into(), MpsTypePrimitive::String(item.filename));
result.insert("genre".into(), MpsTypePrimitive::String(item.genre.unwrap_or("".to_owned())));
result.insert("track".into(), MpsTypePrimitive::UInt(item.track.unwrap_or(0)));
result.insert("year".into(), MpsTypePrimitive::UInt(item.year.unwrap_or(0)));
result
}
pub fn assert_comparison_operator(tokens: &mut VecDeque<MpsToken>) -> Result<[i8; 2], SyntaxError> {
let token1 = tokens.pop_front().unwrap();
match token1 {
MpsToken::Equals => {
if tokens.len() != 0 && tokens[0].is_equals() { // tokens: ==
assert_token_raw(MpsToken::Equals, tokens)?;
Ok([0, 0])
} else {
Err(SyntaxError {
line: 0,
token: MpsToken::Equals,
})
}
},
MpsToken::OpenAngleBracket => {
if tokens.len() != 0 && tokens[0].is_equals() { // tokens: <=
assert_token_raw(MpsToken::Equals, tokens)?;
Ok([0, -1])
} else { // token: <
Ok([-1, -1])
}
},
MpsToken::CloseAngleBracket => {
if tokens.len() != 0 && tokens[0].is_equals() { // tokens: >=
assert_token_raw(MpsToken::Equals, tokens)?;
Ok([0, 1])
} else { // token: >
Ok([1, 1])
}
},
_ => Err(SyntaxError {
line: 0,
token: MpsToken::Equals, // TODO this can be < > or =
})
}
}

View file

@ -0,0 +1,14 @@
mod comment;
mod repeat;
mod sql_init;
mod sql_query;
mod sql_simple_query;
mod variable_assign;
pub use sql_query::{SqlStatement, SqlStatementFactory};
pub use sql_simple_query::{SimpleSqlStatement, SimpleSqlStatementFactory};
pub use comment::{CommentStatement, CommentStatementFactory};
pub use repeat::{RepeatStatement, RepeatStatementFactory};
pub use variable_assign::{AssignStatement, AssignStatementFactory};
pub use sql_init::{SqlInitStatement, SqlInitStatementFactory};
pub mod filters;

View file

@ -0,0 +1,169 @@
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 crate::lang::{RuntimeError, SyntaxError};
use crate::lang::{MpsOp, PseudoOp, MpsOpFactory, BoxedMpsOpFactory};
use crate::lang::MpsLanguageDictionary;
use crate::lang::utility::{assert_name, assert_token_raw, assert_token, check_name};
#[derive(Debug)]
pub struct RepeatStatement {
inner_statement: PseudoOp,
inner_done: bool,
context: Option<MpsContext>,
cache: Vec<MpsMusicItem>,
cache_position: usize,
repetitions: usize,
loop_forever: bool,
}
impl Display for RepeatStatement {
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
write!(f, "repeat({})", self.inner_statement)
}
}
impl std::clone::Clone for RepeatStatement {
fn clone(&self) -> Self {
Self {
inner_statement: self.inner_statement.clone(),
inner_done: self.inner_done,
context: None,
cache: self.cache.clone(),
cache_position: self.cache_position,
repetitions: self.repetitions,
loop_forever: self.loop_forever,
}
}
}
impl Iterator for RepeatStatement {
type Item = Result<MpsMusicItem, RuntimeError>;
fn next(&mut self) -> Option<Self::Item> {
if !self.inner_done {
let real_op = match self.inner_statement.try_real() {
Err(e) => return Some(Err(e)),
Ok(real) => real
};
if self.context.is_some() {
let ctx = self.context.take().unwrap();
real_op.enter(ctx);
}
let inner_item = real_op.next();
match inner_item {
Some(x) => {
return match x {
Ok(music) => {
self.cache.push(music.clone());
Some(Ok(music))
},
Err(e) => Some(Err(e))
}
},
None => {
self.inner_done = true;
self.context = Some(real_op.escape());
},
}
}
// inner is done
if self.repetitions == 0 && !self.loop_forever {
None
} else {
if self.cache.len() == 0 {
if self.loop_forever {
Some(Err(RuntimeError {
line: 0,
op: (Box::new(self.clone()) as Box<dyn MpsOp>).into(),
msg: "Cannot repeat nothing".into()
}))
} else {
None
}
} else {
let music_item = self.cache[self.cache_position].clone();
self.cache_position += 1;
if self.cache_position == self.cache.len() {
if self.repetitions != 0 { self.repetitions -= 1; }
self.cache_position = 0;
}
Some(Ok(music_item))
}
}
}
}
impl MpsOp for RepeatStatement {
fn enter(&mut self, ctx: MpsContext) {
self.context = Some(ctx)
}
fn escape(&mut self) -> MpsContext {
if self.context.is_some() {
self.context.take().unwrap()
} else {
self.inner_statement.try_real().unwrap().escape()
}
}
}
pub struct RepeatStatementFactory;
impl MpsOpFactory<RepeatStatement> for RepeatStatementFactory {
fn is_op(&self, tokens: &VecDeque<MpsToken>) -> bool {
tokens.len() >= 3
&& check_name("repeat", &tokens[0])
&& tokens[1].is_open_bracket()
}
fn build_op(
&self,
tokens: &mut VecDeque<MpsToken>,
dict: &MpsLanguageDictionary,
) -> Result<RepeatStatement, SyntaxError> {
// repeat(query) or repeat(query, repetitions)
assert_name("repeat", tokens)?;
assert_token_raw(MpsToken::OpenBracket, tokens)?;
let inner_statement = dict.try_build_statement(tokens)?;
let mut count: Option<usize> = None;
if tokens[0].is_close_bracket() { // no repititions
assert_token_raw(MpsToken::CloseBracket, tokens)?;
} else if tokens[0].is_comma() { // repetitions specified
assert_token_raw(MpsToken::Comma, tokens)?;
count = Some(assert_token(|t| match t {
MpsToken::Name(n) => n.parse::<usize>().map(|d| d - 1).ok(),
_ => None
}, MpsToken::Name("usize".into()), tokens)?);
assert_token_raw(MpsToken::CloseBracket, tokens)?;
}
Ok(RepeatStatement {
inner_statement: inner_statement.into(),
inner_done: false,
context: None,
cache: Vec::new(),
cache_position: 0,
repetitions: count.unwrap_or(0),
loop_forever: count.is_none()
})
}
}
impl BoxedMpsOpFactory for RepeatStatementFactory {
fn build_op_boxed(
&self,
tokens: &mut VecDeque<MpsToken>,
dict: &MpsLanguageDictionary,
) -> Result<Box<dyn MpsOp>, SyntaxError> {
self.build_box(tokens, dict)
}
fn is_op_boxed(&self, tokens: &VecDeque<MpsToken>) -> bool {
self.is_op(tokens)
}
}

View file

@ -0,0 +1,115 @@
use std::collections::VecDeque;
use std::fmt::{Debug, Display, Error, Formatter};
use std::iter::Iterator;
use std::collections::HashMap;
use crate::MpsContext;
use crate::MpsMusicItem;
use crate::tokens::MpsToken;
use crate::lang::{RuntimeError, SyntaxError};
use crate::lang::{MpsOp, SimpleMpsOpFactory, MpsOpFactory, BoxedMpsOpFactory};
use crate::lang::MpsLanguageDictionary;
use crate::lang::utility::{assert_token_raw, check_name, assert_name, assert_token};
use crate::lang::repeated_tokens;
#[derive(Debug)]
pub struct SqlInitStatement {
context: Option<MpsContext>,
params: HashMap<String, String>,
}
impl Display for SqlInitStatement {
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
write!(f, "sql_init(")?;
for (key, val) in self.params.iter() {
write!(f, "{} = {},", key, val)?;
}
write!(f, ")")
}
}
impl std::clone::Clone for SqlInitStatement {
fn clone(&self) -> Self {
Self {
context: None,
params: HashMap::new(),
}
}
}
impl Iterator for SqlInitStatement {
type Item = Result<MpsMusicItem, RuntimeError>;
fn next(&mut self) -> Option<Self::Item> {
let pseudo_clone = self.clone();
// execute
match self.context.as_mut().unwrap().database.init_with_params(&self.params,
&mut move || (Box::new(pseudo_clone.clone()) as Box<dyn MpsOp>).into()) {
Ok(_) => None,
Err(e) => Some(Err(e))
}
}
}
impl MpsOp for SqlInitStatement {
fn enter(&mut self, ctx: MpsContext) {
self.context = Some(ctx)
}
fn escape(&mut self) -> MpsContext {
self.context.take().unwrap()
}
}
pub struct SqlInitStatementFactory;
impl SimpleMpsOpFactory<SqlInitStatement> for SqlInitStatementFactory {
fn is_op_simple(&self, tokens: &VecDeque<MpsToken>) -> bool {
tokens.len() >= 3
&& check_name("sql_init", &tokens[0])
&& tokens[1].is_open_bracket()
}
fn build_op_simple(
&self,
tokens: &mut VecDeque<MpsToken>,
) -> Result<SqlInitStatement, SyntaxError> {
assert_name("sql_init", tokens)?;
assert_token_raw(MpsToken::OpenBracket, tokens)?;
let ingest = |tokens2: &mut VecDeque<MpsToken>| {
if tokens2.len() < 3 {return Ok(None);} // nothing wrong, nothing left to ingest
let param_name = assert_token(|t| match t {
MpsToken::Name(s) => Some(s),
_ => None,
}, MpsToken::Name("param".into()), tokens2)?;
assert_token_raw(MpsToken::Equals, tokens2)?;
let param_val = assert_token(|t| match t {
MpsToken::Name(s) => Some(s),
MpsToken::Literal(s) => Some(s),
_ => None,
}, MpsToken::Name("value".into()), tokens2)?;
Ok(Some((param_name, param_val))) // successfully ingested one phrase
};
let params = repeated_tokens(ingest, MpsToken::Comma).ingest_all(tokens)?;
assert_token_raw(MpsToken::CloseBracket, tokens)?;
Ok(SqlInitStatement {
context: None,
params: HashMap::from_iter(params),
})
}
}
impl BoxedMpsOpFactory for SqlInitStatementFactory {
fn build_op_boxed(
&self,
tokens: &mut VecDeque<MpsToken>,
dict: &MpsLanguageDictionary,
) -> Result<Box<dyn MpsOp>, SyntaxError> {
self.build_box(tokens, dict)
}
fn is_op_boxed(&self, tokens: &VecDeque<MpsToken>) -> bool {
self.is_op(tokens)
}
}

View file

@ -2,9 +2,9 @@ use std::collections::VecDeque;
use std::fmt::{Debug, Display, Error, Formatter}; use std::fmt::{Debug, Display, Error, Formatter};
use std::iter::Iterator; use std::iter::Iterator;
use super::utility::{assert_token, assert_token_raw}; use crate::lang::utility::{assert_token, assert_token_raw, check_name, assert_name};
use super::{BoxedMpsOpFactory, MpsLanguageDictionary, MpsOp, MpsOpFactory}; use crate::lang::{BoxedMpsOpFactory, MpsLanguageDictionary, MpsOp, MpsOpFactory};
use super::{RuntimeError, SyntaxError}; use crate::lang::{RuntimeError, SyntaxError};
use crate::tokens::MpsToken; use crate::tokens::MpsToken;
use crate::MpsContext; use crate::MpsContext;
use crate::MpsMusicItem; use crate::MpsMusicItem;
@ -35,7 +35,7 @@ impl SqlStatement {
Ok(item) => Some(Ok(item.clone())), Ok(item) => Some(Ok(item.clone())),
Err(e) => Some(Err(RuntimeError { Err(e) => Some(Err(RuntimeError {
line: e.line, line: e.line,
op: Box::new(self.clone()), op: (Box::new(self.clone()) as Box<dyn MpsOp>).into(),
msg: e.msg.clone(), msg: e.msg.clone(),
})), })),
} }
@ -43,7 +43,7 @@ impl SqlStatement {
} else { } else {
Some(Err(RuntimeError { Some(Err(RuntimeError {
line: 0, line: 0,
op: Box::new(self.clone()), op: (Box::new(self.clone()) as Box<dyn MpsOp>).into(),
msg: format!("Context error: rows is None").into(), msg: format!("Context error: rows is None").into(),
})) }))
} }
@ -84,7 +84,7 @@ impl Iterator for SqlStatement {
// query has not been executed yet // query has not been executed yet
match ctx match ctx
.database .database
.raw(&self.query, &mut move || Box::new(self_clone.clone())) .raw(&self.query, &mut move || (Box::new(self_clone.clone()) as Box<dyn MpsOp>).into())
{ {
Err(e) => return Some(Err(e)), Err(e) => return Some(Err(e)),
Ok(rows) => { Ok(rows) => {
@ -108,7 +108,7 @@ 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.len() > 3
&& tokens[0].is_sql() && check_name("sql", &tokens[0])
&& tokens[1].is_open_bracket() && tokens[1].is_open_bracket()
&& tokens[2].is_literal() && tokens[2].is_literal()
&& tokens[3].is_close_bracket() && tokens[3].is_close_bracket()
@ -121,7 +121,8 @@ impl MpsOpFactory<SqlStatement> for SqlStatementFactory {
_dict: &MpsLanguageDictionary, _dict: &MpsLanguageDictionary,
) -> Result<SqlStatement, SyntaxError> { ) -> Result<SqlStatement, SyntaxError> {
// sql ( `some query` ) // sql ( `some query` )
assert_token_raw(MpsToken::Sql, tokens)?; assert_name("sql", tokens)?;
//assert_token_raw(MpsToken::Sql, tokens)?;
assert_token_raw(MpsToken::OpenBracket, tokens)?; assert_token_raw(MpsToken::OpenBracket, tokens)?;
let literal = assert_token( let literal = assert_token(
|t| match t { |t| match t {

View file

@ -2,9 +2,9 @@ use std::collections::VecDeque;
use std::fmt::{Debug, Display, Error, Formatter}; use std::fmt::{Debug, Display, Error, Formatter};
use std::iter::Iterator; use std::iter::Iterator;
use super::utility::{assert_token, assert_token_raw}; use crate::lang::utility::{assert_token, assert_token_raw};
use super::{BoxedMpsOpFactory, MpsLanguageDictionary, MpsOp, MpsOpFactory}; use crate::lang::{BoxedMpsOpFactory, MpsLanguageDictionary, MpsOp, MpsOpFactory};
use super::{RuntimeError, SyntaxError}; use crate::lang::{RuntimeError, SyntaxError};
use crate::tokens::MpsToken; use crate::tokens::MpsToken;
use crate::MpsContext; use crate::MpsContext;
use crate::MpsMusicItem; use crate::MpsMusicItem;
@ -83,7 +83,7 @@ impl SimpleSqlStatement {
Ok(item) => Some(Ok(item.clone())), Ok(item) => Some(Ok(item.clone())),
Err(e) => Some(Err(RuntimeError { Err(e) => Some(Err(RuntimeError {
line: e.line, line: e.line,
op: Box::new(self.clone()), op: (Box::new(self.clone()) as Box<dyn MpsOp>).into(),
msg: e.msg.clone(), msg: e.msg.clone(),
})), })),
} }
@ -91,7 +91,7 @@ impl SimpleSqlStatement {
} else { } else {
Some(Err(RuntimeError { Some(Err(RuntimeError {
line: 0, line: 0,
op: Box::new(self.clone()), op: (Box::new(self.clone()) as Box<dyn MpsOp>).into(),
msg: format!("Context error: rows is None").into(), msg: format!("Context error: rows is None").into(),
})) }))
} }
@ -134,16 +134,16 @@ impl Iterator for SimpleSqlStatement {
let query_result = match self.mode { let query_result = match self.mode {
QueryMode::Artist => ctx QueryMode::Artist => ctx
.database .database
.artist_like(&self.query, &mut move || Box::new(self_clone.clone())), .artist_like(&self.query, &mut move || (Box::new(self_clone.clone()) as Box<dyn MpsOp>).into()),
QueryMode::Album => ctx QueryMode::Album => ctx
.database .database
.album_like(&self.query, &mut move || Box::new(self_clone.clone())), .album_like(&self.query, &mut move || (Box::new(self_clone.clone()) as Box<dyn MpsOp>).into()),
QueryMode::Song => ctx QueryMode::Song => ctx
.database .database
.song_like(&self.query, &mut move || Box::new(self_clone.clone())), .song_like(&self.query, &mut move || (Box::new(self_clone.clone()) as Box<dyn MpsOp>).into()),
QueryMode::Genre => ctx QueryMode::Genre => ctx
.database .database
.genre_like(&self.query, &mut move || Box::new(self_clone.clone())), .genre_like(&self.query, &mut move || (Box::new(self_clone.clone()) as Box<dyn MpsOp>).into()),
}; };
match query_result { match query_result {
Err(e) => return Some(Err(e)), Err(e) => return Some(Err(e)),

View file

@ -0,0 +1,198 @@
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 crate::lang::{RuntimeError, SyntaxError};
use crate::lang::{MpsOp, PseudoOp, MpsOpFactory, BoxedMpsOpFactory, MpsTypePrimitive};
use crate::lang::MpsLanguageDictionary;
use crate::lang::utility::{assert_token_raw, assert_token, check_is_type, assert_type};
use crate::processing::general::MpsType;
#[derive(Debug)]
pub struct AssignStatement {
variable_name: String,
inner_statement: Option<PseudoOp>,
assign_type: Option<MpsTypePrimitive>,
context: Option<MpsContext>,
is_declaration: bool,
is_simple: bool,
}
impl Display for AssignStatement {
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
if let Some(inner_statement) = &self.inner_statement {
write!(f, "{} = {}", self.variable_name, inner_statement)
} else {
write!(f, "{} = ???", self.variable_name)
}
}
}
impl std::clone::Clone for AssignStatement {
fn clone(&self) -> Self {
Self {
variable_name: self.variable_name.clone(),
inner_statement: self.inner_statement.clone(),
assign_type: self.assign_type.clone(),
context: None,
is_declaration: self.is_declaration,
is_simple: self.is_simple,
}
}
}
impl Iterator for AssignStatement {
type Item = Result<MpsMusicItem, RuntimeError>;
fn next(&mut self) -> Option<Self::Item> {
if let Some(inner_statement) = &mut self.inner_statement {
if inner_statement.is_fake() {
Some(Err(RuntimeError {
line: 0,
op: (Box::new(self.clone()) as Box<dyn MpsOp>).into(),
msg: format!("Variable {} already assigned, cannot redo assignment", self.variable_name),
}))
} else {
let mut inner = inner_statement.clone();
std::mem::swap(inner_statement, &mut inner);
let real = match inner.unwrap_real() {
Ok(real) => real,
Err(e) => return Some(Err(e)),
};
let pseudo_clone = self.clone();
let result;
if self.is_declaration {
result = self.context.as_mut().unwrap()
.variables.declare(
&self.variable_name,
MpsType::Op(real),
&mut move ||(Box::new(pseudo_clone.clone()) as Box<dyn MpsOp>).into()
);
} else {
result = self.context.as_mut().unwrap()
.variables.assign(
&self.variable_name,
MpsType::Op(real),
&mut move ||(Box::new(pseudo_clone.clone()) as Box<dyn MpsOp>).into()
);
}
match result {
Ok(_) => None,
Err(e) => Some(Err(e))
}
}
} else if !self.is_simple {
panic!("Assignee statement for {} is None but assignment is not simple type", self.variable_name)
/*Some(Err(RuntimeError {
line: 0,
op: (Box::new(self.clone()) as Box<dyn MpsOp>).into(),
msg: format!("(BUG) Assignee statement for {} is None but assignment is not simple type", self.variable_name),
}))*/
} else {
let assign_type = self.assign_type.clone().unwrap();
let pseudo_clone = self.clone();
let result;
if self.is_declaration {
result = self.context.as_mut().unwrap()
.variables.declare(
&self.variable_name,
MpsType::Primitive(assign_type),
&mut move ||(Box::new(pseudo_clone.clone()) as Box<dyn MpsOp>).into()
);
} else {
result = self.context.as_mut().unwrap()
.variables.assign(
&self.variable_name,
MpsType::Primitive(assign_type),
&mut move ||(Box::new(pseudo_clone.clone()) as Box<dyn MpsOp>).into()
);
}
match result {
Ok(_) => None,
Err(e) => Some(Err(e))
}
}
}
}
impl MpsOp for AssignStatement {
fn enter(&mut self, ctx: MpsContext) {
self.context = Some(ctx)
}
fn escape(&mut self) -> MpsContext {
self.context.take().unwrap()
}
}
pub struct AssignStatementFactory;
impl MpsOpFactory<AssignStatement> for AssignStatementFactory {
fn is_op(&self, tokens: &VecDeque<MpsToken>) -> bool {
(tokens.len() >= 3
&&tokens[0].is_name() // can be any (valid) variable name
&& tokens[1].is_equals())
|| (tokens.len() >= 4
&& tokens[0].is_let()
&& tokens[1].is_name() // any name
&& tokens[2].is_equals())
}
fn build_op(
&self,
tokens: &mut VecDeque<MpsToken>,
dict: &MpsLanguageDictionary,
) -> Result<AssignStatement, SyntaxError> {
// [let] variable_name = inner_statement
let is_decl = tokens[0].is_let();
if is_decl { // variable declarations start with let, assignments do not
assert_token_raw(MpsToken::Let, tokens)?;
}
let name = assert_token(|t| match t {
MpsToken::Name(s) => Some(s),
_ => None
}, MpsToken::Name("variable_name".into()), tokens)?;
assert_token_raw(MpsToken::Equals, tokens)?;
let is_simple_assign = check_is_type(&tokens[0]);
if is_simple_assign {
let simple_type = assert_type(tokens)?;
Ok(AssignStatement {
variable_name: name,
inner_statement: None,
assign_type: Some(simple_type),
context: None,
is_declaration: is_decl,
is_simple: true,
})
} else {
let inner_statement = dict.try_build_statement(tokens)?;
Ok(AssignStatement {
variable_name: name,
inner_statement: Some(inner_statement.into()),
assign_type: None,
context: None,
is_declaration: is_decl,
is_simple: false,
})
}
}
}
impl BoxedMpsOpFactory for AssignStatementFactory {
fn build_op_boxed(
&self,
tokens: &mut VecDeque<MpsToken>,
dict: &MpsLanguageDictionary,
) -> Result<Box<dyn MpsOp>, SyntaxError> {
self.build_box(tokens, dict)
}
fn is_op_boxed(&self, tokens: &VecDeque<MpsToken>) -> bool {
self.is_op(tokens)
}
}

View file

@ -1,9 +1,39 @@
use std::path::Path; use std::path::Path;
use crate::lang::db::*;
use super::MpsLibrary; use super::MpsLibrary;
pub fn build_library<P: AsRef<Path>>(path: P) -> std::io::Result<MpsLibrary> { pub fn build_library_from_files<P: AsRef<Path>>(path: P, lib: &mut MpsLibrary) -> std::io::Result<()> {
let mut result = MpsLibrary::new(); //let mut result = MpsLibrary::new();
result.read_path(path, 10)?; lib.read_path(path, 10)?;
Ok(result) Ok(())
}
pub fn build_library_from_sqlite(conn: &rusqlite::Connection, lib: &mut MpsLibrary) -> rusqlite::Result<()> {
// build songs
for song in conn.prepare("SELECT * from songs")?
.query_map([], DbMusicItem::map_row)? {
lib.add_song(song?);
}
// build metadata
for meta in conn.prepare("SELECT * from metadata")?
.query_map([], DbMetaItem::map_row)? {
lib.add_metadata(meta?);
}
// build artists
for artist in conn.prepare("SELECT * from artists")?
.query_map([], DbArtistItem::map_row)? {
lib.add_artist(artist?);
}
// build albums
for album in conn.prepare("SELECT * from albums")?
.query_map([], DbAlbumItem::map_row)? {
lib.add_album(album?);
}
// build genres
for genre in conn.prepare("SELECT * from genres")?
.query_map([], DbGenreItem::map_row)? {
lib.add_genre(genre?);
}
Ok(())
} }

View file

@ -1,5 +1,6 @@
use std::collections::HashMap; use std::collections::{HashMap, HashSet};
use std::path::Path; use std::path::{Path, PathBuf};
use std::str::FromStr;
use symphonia::core::io::MediaSourceStream; use symphonia::core::io::MediaSourceStream;
use symphonia::core::probe::Hint; use symphonia::core::probe::Hint;
@ -14,6 +15,8 @@ pub struct MpsLibrary {
artists: HashMap<String, DbArtistItem>, artists: HashMap<String, DbArtistItem>,
albums: HashMap<String, DbAlbumItem>, albums: HashMap<String, DbAlbumItem>,
genres: HashMap<String, DbGenreItem>, genres: HashMap<String, DbGenreItem>,
files: HashSet<PathBuf>,
dirty: bool,
} }
impl MpsLibrary { impl MpsLibrary {
@ -24,6 +27,8 @@ impl MpsLibrary {
artists: HashMap::new(), artists: HashMap::new(),
albums: HashMap::new(), albums: HashMap::new(),
genres: HashMap::new(), genres: HashMap::new(),
files: HashSet::new(),
dirty: false,
} }
} }
@ -31,28 +36,80 @@ impl MpsLibrary {
self.songs.len() self.songs.len()
} }
pub fn clear_modified(&mut self) {
self.dirty = false;
}
pub fn is_modified(&self) -> bool {
self.dirty
}
#[inline(always)]
fn modify(&mut self) {
self.dirty = true;
}
#[inline]
pub fn contains_path<P: AsRef<Path>>(&self, path: P) -> bool {
self.files.contains(&path.as_ref().to_path_buf())
}
pub fn all_songs<'a>(&'a self) -> Vec<&'a DbMusicItem> { pub fn all_songs<'a>(&'a self) -> Vec<&'a DbMusicItem> {
self.songs.values().collect() self.songs.values().collect()
} }
#[inline]
pub fn add_song(&mut self, song: DbMusicItem) {
self.modify();
if let Ok(path) = PathBuf::from_str(&song.filename) {
self.files.insert(path);
}
self.songs.insert(song.song_id, song);
}
pub fn all_metadata<'a>(&'a self) -> Vec<&'a DbMetaItem> { pub fn all_metadata<'a>(&'a self) -> Vec<&'a DbMetaItem> {
self.metadata.values().collect() self.metadata.values().collect()
} }
#[inline]
pub fn add_metadata(&mut self, meta: DbMetaItem) {
self.modify();
self.metadata.insert(meta.meta_id, meta);
}
pub fn all_artists<'a>(&'a self) -> Vec<&'a DbArtistItem> { pub fn all_artists<'a>(&'a self) -> Vec<&'a DbArtistItem> {
self.artists.values().collect() self.artists.values().collect()
} }
#[inline]
pub fn add_artist(&mut self, artist: DbArtistItem) {
self.modify();
self.artists.insert(Self::sanitise_key(&artist.name), artist);
}
pub fn all_albums<'a>(&'a self) -> Vec<&'a DbAlbumItem> { pub fn all_albums<'a>(&'a self) -> Vec<&'a DbAlbumItem> {
self.albums.values().collect() self.albums.values().collect()
} }
#[inline]
pub fn add_album(&mut self, album: DbAlbumItem) {
self.modify();
self.albums.insert(Self::sanitise_key(&album.title), album);
}
pub fn all_genres<'a>(&'a self) -> Vec<&'a DbGenreItem> { pub fn all_genres<'a>(&'a self) -> Vec<&'a DbGenreItem> {
self.genres.values().collect() self.genres.values().collect()
} }
#[inline]
pub fn add_genre(&mut self, genre: DbGenreItem) {
self.modify();
self.genres.insert(Self::sanitise_key(&genre.title), genre);
}
pub fn read_path<P: AsRef<Path>>(&mut self, path: P, depth: usize) -> std::io::Result<()> { pub fn read_path<P: AsRef<Path>>(&mut self, path: P, depth: usize) -> std::io::Result<()> {
let path = path.as_ref(); let path = path.as_ref();
if self.contains_path(path) { return Ok(()); } // skip existing entries
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)?;
@ -104,27 +161,24 @@ impl MpsLibrary {
} // probably not a valid song, let's skip it } // 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.add_metadata(tags.meta(meta_id)); // definitely necessary
// genre has no links to others, so find that first // genre has no links to others, so find that first
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 self.add_genre(genre.clone());
.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 self.add_artist(artist.clone());
.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 self.add_artist(album_artist.clone());
.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);
@ -132,14 +186,12 @@ 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 self.add_album(album.clone());
.insert(Self::sanitise_key(&album.title), album.clone()); self.add_metadata(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( self.add_song(
song_id,
tags.song( tags.song(
song_id, song_id,
artist.artist_id, artist.artist_id,

View file

@ -2,5 +2,5 @@ mod build_library;
mod library; mod library;
mod tag; mod tag;
pub use build_library::build_library; pub use build_library::{build_library_from_files, build_library_from_sqlite};
pub use library::MpsLibrary; pub use library::MpsLibrary;

View file

@ -45,7 +45,7 @@ impl Tags {
self.filename self.filename
.file_name() .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(&format!(".{}", extension), "", 1)))
.unwrap_or("Unknown Title".into()) .unwrap_or("Unknown Title".into())
}; };
DbMusicItem { DbMusicItem {

View file

@ -1,9 +1,14 @@
use super::lang::db::{DatabaseObj, DbMusicItem}; use super::lang::db::{DatabaseObj, DbMusicItem, DbArtistItem, DbAlbumItem, DbMetaItem, DbGenreItem};
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct MpsMusicItem { pub struct MpsMusicItem {
pub title: String, pub title: String,
pub artist: Option<String>,
pub album: Option<String>,
pub filename: String, pub filename: String,
pub genre: Option<String>,
pub track: Option<u64>,
pub year: Option<u64>,
} }
impl MpsMusicItem { impl MpsMusicItem {
@ -11,7 +16,30 @@ impl MpsMusicItem {
let item = DbMusicItem::map_row(row)?; let item = DbMusicItem::map_row(row)?;
Ok(Self { Ok(Self {
title: item.title, title: item.title,
artist: None,
album: None,
filename: item.filename, filename: item.filename,
genre: None,
track: None,
year: None,
}) })
} }
pub fn merge(
music: DbMusicItem,
artist: DbArtistItem,
album: DbAlbumItem,
meta: DbMetaItem,
genre: DbGenreItem
) -> Self {
Self {
title: music.title,
artist: Some(artist.name),
album: Some(album.title),
filename: music.filename,
genre: Some(genre.title),
track: Some(meta.track),
year: Some(meta.date),
}
}
} }

View file

@ -1,5 +1,12 @@
mod sql; mod sql;
mod variables;
pub type OpGetter = dyn FnMut() -> crate::lang::PseudoOp;
pub mod database { pub mod database {
pub use super::sql::{MpsDatabaseQuerier, MpsSQLiteExecutor, QueryOp, QueryResult}; pub use super::sql::{MpsDatabaseQuerier, MpsSQLiteExecutor, QueryResult};
}
pub mod general {
pub use super::variables::{MpsVariableStorer, MpsOpStorage, MpsType};
} }

View file

@ -1,22 +1,36 @@
use core::fmt::Debug; use core::fmt::Debug;
use std::collections::{HashMap, HashSet};
use crate::lang::db::*; use crate::lang::db::*;
use crate::lang::{MpsOp, RuntimeError}; use crate::lang::RuntimeError;
use crate::MpsMusicItem; use crate::MpsMusicItem;
pub type QueryResult = Result<Vec<Result<MpsMusicItem, RuntimeError>>, RuntimeError>; use super::OpGetter as QueryOp;
pub type QueryOp = dyn FnMut() -> Box<dyn MpsOp>;
pub type QueryResult = Result<Vec<Result<MpsMusicItem, RuntimeError>>, RuntimeError>;
/// SQL querying functionality, loosely de-coupled from any specific SQL dialect (excluding raw call)
pub trait MpsDatabaseQuerier: Debug { pub trait MpsDatabaseQuerier: Debug {
/// raw SQL call, assumed (but not guaranteed) to retrieved music items
fn raw(&mut self, query: &str, op: &mut QueryOp) -> QueryResult; fn raw(&mut self, query: &str, op: &mut QueryOp) -> QueryResult;
/// get music, searching by artist name like `query`
fn artist_like(&mut self, query: &str, op: &mut QueryOp) -> QueryResult; fn artist_like(&mut self, query: &str, op: &mut QueryOp) -> QueryResult;
/// get music, searching by album title like `query`
fn album_like(&mut self, query: &str, op: &mut QueryOp) -> QueryResult; fn album_like(&mut self, query: &str, op: &mut QueryOp) -> QueryResult;
/// get music, searching by song title like `query`
fn song_like(&mut self, query: &str, op: &mut QueryOp) -> QueryResult; fn song_like(&mut self, query: &str, op: &mut QueryOp) -> QueryResult;
/// get music, searching by genre title like `query`
fn genre_like(&mut self, query: &str, op: &mut QueryOp) -> QueryResult; fn genre_like(&mut self, query: &str, op: &mut QueryOp) -> QueryResult;
/// connect to the SQL database with (optional) settings such as:
/// `"folder" = "path"` - path to root music directory
/// `"database" = "uri"` - connection URI for database (for SQLite this is just a filepath)
/// `"generate" = "true"|"yes"|"false"|"no"` - whether to populate the database using the music directory
fn init_with_params(&mut self, params: &HashMap<String, String>, op: &mut QueryOp) -> Result<(), RuntimeError>;
} }
#[derive(Default, Debug)] #[derive(Default, Debug)]
@ -135,6 +149,133 @@ impl MpsDatabaseQuerier for MpsSQLiteExecutor {
WHERE genres.title like ? ORDER BY songs.album, metadata.track"; WHERE genres.title like ? ORDER BY songs.album, metadata.track";
self.music_query_single_param(query_stmt, param, op) self.music_query_single_param(query_stmt, param, op)
} }
fn init_with_params(&mut self, params: &HashMap<String, String>, op: &mut QueryOp) -> Result<(), RuntimeError> {
// must be executed before connection is created
if self.sqlite_connection.is_some() {
Err(RuntimeError {
line: 0,
op: op(),
msg: "Cannot init SQLite connection: Already connected".into(),
})
} else {
// process params
// init connection
let mut keys: HashSet<&String> = params.keys().collect();
let mut settings = SqliteSettings::default();
for (key, val) in params.iter() {
let mut match_found = false;
match key as &str {
"folder" | "dir" => {
match_found = true;
settings.music_path = Some(val.clone());
},
"database" | "db" => {
match_found = true;
settings.db_path = Some(val.clone());
},
"generate" | "gen" => {
match_found = true;
settings.auto_generate = match val as &str {
"true" | "yes" => Ok(true),
"false" | "no" => Ok(false),
x => Err(RuntimeError{
line: 0,
op: op(),
msg: format!("Unrecognised right hand side of param \"{}\" = \"{}\"", key, x),
})
}?;
}
_ => {}
}
if match_found {
keys.remove(key);
}
}
if !keys.is_empty() {
// build error msg
let mut concat_keys = "".to_string();
let mut first = true;
for key in keys.drain() {
if first {
first = false;
concat_keys += key;
} else {
concat_keys += &format!("{}, ", key);
}
}
return Err(RuntimeError {
line: 0,
op: op(),
msg: format!("Unrecognised sql init parameter(s): {}", concat_keys)
})
}
self.sqlite_connection = Some(
settings.try_into()
.map_err(|e|
RuntimeError {
line: 0,
op: op(),
msg: format!("SQL connection error: {}", e)
}
)?
);
Ok(())
}
}
}
struct SqliteSettings {
music_path: Option<String>,
db_path: Option<String>,
auto_generate: bool,
}
impl std::default::Default for SqliteSettings {
fn default() -> Self {
SqliteSettings {
music_path: None,
db_path: None,
auto_generate: true
}
}
}
impl std::convert::TryInto<rusqlite::Connection> for SqliteSettings {
type Error = rusqlite::Error;
fn try_into(self) -> Result<rusqlite::Connection, Self::Error> {
let music_path = self.music_path
.and_then(
|p| Some(std::path::PathBuf::from(p))
).unwrap_or_else(
|| crate::lang::utility::music_folder()
);
let sqlite_path = self.db_path.unwrap_or_else(|| crate::lang::db::DEFAULT_SQLITE_FILEPATH.to_string());
crate::lang::db::generate_db(music_path, sqlite_path, self.auto_generate)
}
}
#[inline(always)]
fn build_mps_item(conn: &mut rusqlite::Connection, item: DbMusicItem) -> rusqlite::Result<MpsMusicItem> {
// query artist
let mut stmt = conn
.prepare_cached("SELECT * from artists WHERE artist_id = ?")?;
let artist = stmt.query_row([item.artist], DbArtistItem::map_row)?;
// query album
let mut stmt = conn
.prepare_cached("SELECT * from albums WHERE album_id = ?")?;
let album = stmt.query_row([item.album], DbAlbumItem::map_row)?;
// query metadata
let mut stmt = conn
.prepare_cached("SELECT * from metadata WHERE meta_id = ?")?;
let meta = stmt.query_row([item.metadata], DbMetaItem::map_row)?;
// query genre
let mut stmt = conn
.prepare_cached("SELECT * from genres WHERE genre_id = ?")?;
let genre = stmt.query_row([item.genre], DbGenreItem::map_row)?;
Ok(MpsMusicItem::merge(item, artist, album, meta, genre))
} }
#[inline] #[inline]
@ -142,13 +283,22 @@ fn perform_query(
conn: &mut rusqlite::Connection, conn: &mut rusqlite::Connection,
query: &str, query: &str,
) -> Result<Vec<rusqlite::Result<MpsMusicItem>>, String> { ) -> Result<Vec<rusqlite::Result<MpsMusicItem>>, String> {
let mut stmt = conn let collection: Vec<rusqlite::Result<DbMusicItem>>;
.prepare(query) {
.map_err(|e| format!("SQLite query error: {}", e))?; let mut stmt = conn
let iter = stmt .prepare(query)
.query_map([], MpsMusicItem::map_row) .map_err(|e| format!("SQLite query error: {}", e))?;
.map_err(|e| format!("SQLite item mapping error: {}", e))?; collection = stmt
Ok(iter.collect()) .query_map([], DbMusicItem::map_row)
.map_err(|e| format!("SQLite item mapping error: {}", e))?
.collect();
}
let iter2 = collection.into_iter()
.map(|item| match item {
Ok(item) => build_mps_item(conn, item),
Err(e) => Err(e)
});
Ok(iter2.collect())
} }
#[inline] #[inline]
@ -157,11 +307,20 @@ fn perform_single_param_query(
query: &str, query: &str,
param: &str, param: &str,
) -> Result<Vec<rusqlite::Result<MpsMusicItem>>, String> { ) -> Result<Vec<rusqlite::Result<MpsMusicItem>>, String> {
let mut stmt = conn let collection: Vec<rusqlite::Result<DbMusicItem>>;
.prepare_cached(query) {
.map_err(|e| format!("SQLite query error: {}", e))?; let mut stmt = conn
let iter = stmt .prepare_cached(query)
.query_map([param], MpsMusicItem::map_row) .map_err(|e| format!("SQLite query error: {}", e))?;
.map_err(|e| format!("SQLite item mapping error: {}", e))?; collection = stmt
Ok(iter.collect()) .query_map([param], DbMusicItem::map_row)
.map_err(|e| format!("SQLite item mapping error: {}", e))?
.collect();
}
let iter2 = collection.into_iter()
.map(|item| match item {
Ok(item) => build_mps_item(conn, item),
Err(e) => Err(e)
});
Ok(iter2.collect())
} }

View file

@ -0,0 +1,103 @@
use std::fmt::{Debug, Display, Error, Formatter};
use std::collections::HashMap;
use crate::lang::RuntimeError;
use crate::lang::MpsOp;
use crate::lang::MpsTypePrimitive;
use super::OpGetter;
#[derive(Debug)]
pub enum MpsType {
Op(Box<dyn MpsOp>),
Primitive(MpsTypePrimitive)
}
impl Display for MpsType {
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
match self {
Self::Op(op) => write!(f, "Op({})", op),
Self::Primitive(p) => write!(f, "{}", p)
}
}
}
pub trait MpsVariableStorer: Debug {
fn get(&self, name: &str, op: &mut OpGetter) -> Result<&MpsType, RuntimeError>;
fn get_mut(&mut self, name: &str, op: &mut OpGetter) -> Result<&mut MpsType, RuntimeError>;
fn assign(&mut self, name: &str, value: MpsType, op: &mut OpGetter) -> Result<(), RuntimeError>;
fn declare(&mut self, name: &str, value: MpsType, op: &mut OpGetter) -> Result<(), RuntimeError>;
fn remove(&mut self, name: &str, op: &mut OpGetter) -> Result<MpsType, RuntimeError>;
}
#[derive(Default, Debug)]
pub struct MpsOpStorage {
storage: HashMap<String, MpsType>,
}
impl MpsVariableStorer for MpsOpStorage {
fn get(&self, key: &str, op: &mut OpGetter) -> Result<&MpsType, RuntimeError> {
match self.storage.get(key) {
Some(item) => Ok(item),
None => Err(RuntimeError {
line: 0,
op: op(),
msg: format!("Variable {} not found", key),
})
}
}
fn get_mut(&mut self, key: &str, op: &mut OpGetter) -> Result<&mut MpsType, RuntimeError> {
match self.storage.get_mut(key) {
Some(item) => Ok(item),
None => Err(RuntimeError {
line: 0,
op: op(),
msg: format!("Variable {} not found", key),
})
}
}
fn assign(&mut self, key: &str, item: MpsType, op: &mut OpGetter) -> Result<(), RuntimeError> {
if !self.storage.contains_key(key) {
Err(RuntimeError {
line: 0,
op: op(),
msg: format!("Cannot assign to non-existent variable {}", key),
})
} else {
self.storage.insert(key.to_string(), item);
Ok(())
}
}
fn declare(&mut self, key: &str, item: MpsType, op: &mut OpGetter) -> Result<(), RuntimeError> {
if self.storage.contains_key(key) {
Err(RuntimeError {
line: 0,
op: op(),
msg: format!("Cannot overwrite existing variable {}", key),
})
} else {
self.storage.insert(key.to_string(), item);
Ok(())
}
}
fn remove(&mut self, key: &str, op: &mut OpGetter) -> Result<MpsType, RuntimeError> {
if self.storage.contains_key(key) {
Ok(self.storage.remove(key).unwrap())
} else {
Err(RuntimeError {
line: 0,
op: op(),
msg: format!("Cannot remove non-existing variable {}", key),
})
}
}
}

View file

@ -1,8 +1,8 @@
use std::fmt::{Debug, Display, Error, Formatter}; use std::fmt::{Debug, Display, Error, Formatter};
#[derive(Debug, Eq, PartialEq)] #[derive(Debug, Eq, PartialEq, Clone)]
pub enum MpsToken { pub enum MpsToken {
Sql, //Sql,
OpenBracket, OpenBracket,
CloseBracket, CloseBracket,
Comma, Comma,
@ -10,20 +10,30 @@ pub enum MpsToken {
Name(String), Name(String),
//Octothorpe, //Octothorpe,
Comment(String), Comment(String),
Equals,
Let,
OpenAngleBracket,
CloseAngleBracket,
Dot,
} }
impl MpsToken { impl MpsToken {
pub fn parse_from_string(s: String) -> Result<Self, String> { pub fn parse_from_string(s: String) -> Result<Self, String> {
match &s as &str { match &s as &str {
"sql" => Ok(Self::Sql), //"sql" => Ok(Self::Sql),
"(" => Ok(Self::OpenBracket), "(" => Ok(Self::OpenBracket),
")" => Ok(Self::CloseBracket), ")" => Ok(Self::CloseBracket),
"," => Ok(Self::Comma), "," => Ok(Self::Comma),
//"#" => Ok(Self::Octothorpe), //"#" => Ok(Self::Octothorpe),
"=" => Ok(Self::Equals),
"let" => Ok(Self::Let),
"<" => Ok(Self::OpenAngleBracket),
">" => Ok(Self::CloseAngleBracket),
"." => Ok(Self::Dot),
_ => { _ => {
// name validation // name validation
let mut ok = true; let mut ok = true;
for invalid_c in ["-", "+", ",", " ", "/", "\n", "\r", "!", "?"] { for invalid_c in ["-", "+", ",", " ", "/", "\n", "\r", "!", "?", "=", "."] {
if s.contains(invalid_c) { if s.contains(invalid_c) {
ok = false; ok = false;
break; break;
@ -38,12 +48,12 @@ impl MpsToken {
} }
} }
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 {
@ -59,6 +69,13 @@ impl MpsToken {
} }
} }
pub fn is_comma(&self) -> bool {
match self {
Self::Comma => true,
_ => false,
}
}
pub fn is_literal(&self) -> bool { pub fn is_literal(&self) -> bool {
match self { match self {
Self::Literal(_) => true, Self::Literal(_) => true,
@ -86,19 +103,59 @@ impl MpsToken {
_ => false, _ => false,
} }
} }
pub fn is_equals(&self) -> bool {
match self {
Self::Equals => true,
_ => false,
}
}
pub fn is_let(&self) -> bool {
match self {
Self::Let => true,
_ => false,
}
}
pub fn is_open_angle_bracket(&self) -> bool {
match self {
Self::OpenAngleBracket => true,
_ => false,
}
}
pub fn is_close_angle_bracket(&self) -> bool {
match self {
Self::CloseAngleBracket => true,
_ => false,
}
}
pub fn is_dot(&self) -> bool {
match self {
Self::Dot => true,
_ => false,
}
}
} }
impl Display for MpsToken { impl Display for MpsToken {
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
match self { match self {
Self::Sql => write!(f, "sql"), //Self::Sql => write!(f, "sql"),
Self::OpenBracket => write!(f, "("), Self::OpenBracket => write!(f, "("),
Self::CloseBracket => write!(f, ")"), Self::CloseBracket => write!(f, ")"),
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::Octothorpe => write!(f, "#"),
Self::Comment(s) => write!(f, "//{}", s), Self::Comment(s) => write!(f, "{}", s),
Self::Equals => write!(f, "="),
Self::Let => write!(f, "let"),
Self::OpenAngleBracket => write!(f, "<"),
Self::CloseAngleBracket => write!(f, ">"),
Self::Dot => write!(f, "."),
} }
} }
} }

View file

@ -234,6 +234,7 @@ impl ReaderStateMachine {
| Self::EndToken {} | Self::EndToken {}
| Self::EndComment {} | Self::EndComment {}
| Self::EndStatement {} | Self::EndStatement {}
| Self::EndOfFile {}
| Self::Invalid {..} => match input_char { | Self::Invalid {..} => match input_char {
'\\' => Self::Escaped { inside: '_' }, '\\' => Self::Escaped { inside: '_' },
'/' => Self::Slash { out: input }, '/' => Self::Slash { out: input },
@ -243,7 +244,7 @@ impl ReaderStateMachine {
' ' => Self::EndToken {}, ' ' => Self::EndToken {},
'\n' | '\r' | ';' => Self::EndStatement {}, '\n' | '\r' | ';' => Self::EndStatement {},
'\0' => Self::EndOfFile {}, '\0' => Self::EndOfFile {},
'(' | ')' | ',' => Self::SingleCharToken { out: input }, '(' | ')' | ',' | '=' | '<' | '>' | '.' => Self::SingleCharToken { out: input },
_ => Self::Regular { out: input }, _ => Self::Regular { out: input },
}, },
Self::Escaped { inside } => match inside { Self::Escaped { inside } => match inside {
@ -278,7 +279,7 @@ impl ReaderStateMachine {
'\n' | '\r' | '\0' => Self::EndComment {}, '\n' | '\r' | '\0' => Self::EndComment {},
_ => Self::Comment { out: input }, _ => Self::Comment { out: input },
}, },
Self::EndOfFile {} => Self::EndOfFile {}, //Self::EndOfFile {} => Self::EndOfFile {}, // For REPL, the end of the file is not necessarily the end forever
} }
} }

View file

@ -8,7 +8,7 @@ use std::io::Cursor;
fn parse_line() -> Result<(), ParseError> { fn parse_line() -> Result<(), ParseError> {
let cursor = Cursor::new("sql(`SELECT * FROM songs;`)"); let cursor = Cursor::new("sql(`SELECT * FROM songs;`)");
let correct_tokens: Vec<MpsToken> = vec![ let correct_tokens: Vec<MpsToken> = vec![
MpsToken::Sql, MpsToken::Name("sql".into()),
MpsToken::OpenBracket, MpsToken::OpenBracket,
MpsToken::Literal("SELECT * FROM songs;".into()), MpsToken::Literal("SELECT * FROM songs;".into()),
MpsToken::CloseBracket, MpsToken::CloseBracket,
@ -27,14 +27,16 @@ fn parse_line() -> Result<(), ParseError> {
// validity tests // validity tests
assert_eq!(buf.len(), correct_tokens.len()); assert_eq!(buf.len(), correct_tokens.len());
for i in 0..buf.len() { for i in 0..buf.len() {
assert_eq!(buf[i], correct_tokens[i]); assert_eq!(buf[i], correct_tokens[i], "Tokens at position {} do not match ()", i);
} }
tokenizer.read_line(&mut buf)?; // this should immediately return tokenizer.read_line(&mut buf)?; // this should immediately return
Ok(()) Ok(())
} }
fn execute_single_line(line: &str, should_be_emtpy: bool) -> Result<(), Box<dyn MpsLanguageError>> { #[inline(always)]
fn execute_single_line(line: &str, should_be_emtpy: bool, should_complete: bool) -> Result<(), Box<dyn MpsLanguageError>> {
println!("--- Executing MPS code: '{}' ---", line);
let cursor = Cursor::new(line); let cursor = Cursor::new(line);
let tokenizer = MpsTokenizer::new(cursor); let tokenizer = MpsTokenizer::new(cursor);
@ -45,34 +47,67 @@ fn execute_single_line(line: &str, should_be_emtpy: bool) -> Result<(), Box<dyn
if let Ok(item) = result { if let Ok(item) = result {
count += 1; count += 1;
if count > 100 { if count > 100 {
continue; if should_complete {
continue;
} else {
println!("Got 100 items, stopping to avoid infinite loop");
break;
}
} // no need to spam the rest of the songs } // 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 { if should_be_emtpy {
assert_eq!(count, 0); assert_eq!(count, 0, "{} music items found while iterating over line which should be None", count);
} else { } else {
assert_ne!(count, 0); // database is populated println!("Got {} items, execution complete (no songs were harmed in the making of this test)", count);
assert_ne!(count, 0, "0 music items found while iterating over line which should have Some results"); // assumption: database is populated
} }
Ok(()) Ok(())
} }
#[test] #[test]
fn execute_sql_line() -> Result<(), Box<dyn MpsLanguageError>> { fn execute_sql_line() -> Result<(), Box<dyn MpsLanguageError>> {
execute_single_line("sql(`SELECT * FROM songs ORDER BY artist;`)", false) execute_single_line("sql(`SELECT * FROM songs ORDER BY artist;`)", false, true)
} }
#[test] #[test]
fn execute_simple_sql_line() -> Result<(), Box<dyn MpsLanguageError>> { fn execute_simple_sql_line() -> Result<(), Box<dyn MpsLanguageError>> {
execute_single_line("song(`lov`)", false) execute_single_line("song(`lov`)", false, true)
} }
#[test] #[test]
fn execute_comment_line() -> Result<(), Box<dyn MpsLanguageError>> { fn execute_comment_line() -> Result<(), Box<dyn MpsLanguageError>> {
execute_single_line("// this is a comment", true)?; execute_single_line("// this is a comment", true, true)?;
execute_single_line("# this is a special comment", true) execute_single_line("# this is a special comment", true, true)
}
#[test]
fn execute_repeat_line() -> Result<(), Box<dyn MpsLanguageError>> {
execute_single_line("repeat(song(`Christmas in L.A.`))", false, false)?;
execute_single_line("repeat(song(`Christmas in L.A.`), 4)", false, true)
}
#[test]
fn execute_sql_init_line() -> Result<(), Box<dyn MpsLanguageError>> {
execute_single_line("sql_init(generate = no, folder = `/home/ngnius/Music`)", true, true)
}
#[test]
fn execute_assign_line() -> Result<(), Box<dyn MpsLanguageError>> {
execute_single_line("let some_var = repeat(song(`Christmas in L.A.`))", true, true)?;
execute_single_line("let some_var2 = 1234", true, true)
}
#[test]
fn execute_emptyfilter_line() -> Result<(), Box<dyn MpsLanguageError>> {
execute_single_line("song(`lov`).()", false, true)
}
#[test]
fn execute_fieldfilter_line() -> Result<(), Box<dyn MpsLanguageError>> {
execute_single_line("song(`lov`).(year >= 2020)", false, true)
} }