diff --git a/mps-interpreter/src/interpretor.rs b/mps-interpreter/src/interpretor.rs index c1732c5..da20ee4 100644 --- a/mps-interpreter/src/interpretor.rs +++ b/mps-interpreter/src/interpretor.rs @@ -171,6 +171,7 @@ pub(crate) fn standard_vocab(vocabulary: &mut MpsLanguageDictionary) { .add(crate::lang::vocabulary::item_ops::ConstantItemOpFactory) .add(crate::lang::vocabulary::item_ops::VariableAssignItemOpFactory) .add(crate::lang::vocabulary::item_ops::FieldAssignItemOpFactory) + .add(crate::lang::vocabulary::item_ops::FileItemOpFactory) .add(crate::lang::vocabulary::item_ops::VariableDeclareItemOpFactory) .add(crate::lang::vocabulary::item_ops::InterpolateStringItemOpFactory) .add(crate::lang::vocabulary::item_ops::BranchItemOpFactory) diff --git a/mps-interpreter/src/lang/function.rs b/mps-interpreter/src/lang/function.rs index e356ad2..521b46c 100644 --- a/mps-interpreter/src/lang/function.rs +++ b/mps-interpreter/src/lang/function.rs @@ -66,7 +66,7 @@ impl + 'static> BoxedMpsOpFactory tokens, )?; assert_token_raw(MpsToken::OpenBracket, tokens)?; - let end_tokens = tokens.split_off(tokens.len()-1); + let end_tokens = tokens.split_off(tokens.len() - 1); let func = self.op_factory.build_function_params(name, tokens, dict)?; tokens.extend(end_tokens); assert_token_raw(MpsToken::CloseBracket, tokens)?; diff --git a/mps-interpreter/src/lang/vocabulary/item_ops/file.rs b/mps-interpreter/src/lang/vocabulary/item_ops/file.rs new file mode 100644 index 0000000..ceb8c25 --- /dev/null +++ b/mps-interpreter/src/lang/vocabulary/item_ops/file.rs @@ -0,0 +1,66 @@ +use core::ops::Deref; +use std::collections::VecDeque; +use std::fmt::{Debug, Display, Error, Formatter}; + +use crate::lang::utility::{assert_name, assert_token_raw, check_name}; +use crate::lang::{MpsItemBlockFactory, MpsItemOp, MpsItemOpFactory}; +use crate::lang::{MpsLanguageDictionary, MpsTypePrimitive}; +use crate::lang::{RuntimeMsg, SyntaxError}; +use crate::processing::general::MpsType; +use crate::tokens::MpsToken; +use crate::MpsContext; + +#[derive(Debug)] +pub struct FileItemOp { + inner: Box, +} + +impl Deref for FileItemOp { + type Target = dyn MpsItemOp; + fn deref(&self) -> &Self::Target { + self + } +} + +impl Display for FileItemOp { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + write!(f, "file({})", self.inner) + } +} + +impl MpsItemOp for FileItemOp { + fn execute(&self, context: &mut MpsContext) -> Result { + let inner_return = self.inner.execute(context)?; + if let MpsType::Primitive(MpsTypePrimitive::String(path)) = inner_return { + Ok(MpsType::Item(context.filesystem.single(&path, None)?)) + } else { + Err(RuntimeMsg(format!( + "Cannot use {} as filepath (should be String)", + inner_return + ))) + } + } +} + +pub struct FileItemOpFactory; + +impl MpsItemOpFactory for FileItemOpFactory { + fn is_item_op(&self, tokens: &VecDeque) -> bool { + !tokens.is_empty() && check_name("file", &tokens[0]) + } + + fn build_item_op( + &self, + tokens: &mut VecDeque, + factory: &MpsItemBlockFactory, + dict: &MpsLanguageDictionary, + ) -> Result { + assert_name("file", tokens)?; + assert_token_raw(MpsToken::OpenBracket, tokens)?; + let end_tokens = tokens.split_off(tokens.len() - 1); + let inner_op = factory.try_build_item_statement(tokens, dict)?; + tokens.extend(end_tokens); + assert_token_raw(MpsToken::CloseBracket, tokens)?; + Ok(FileItemOp { inner: inner_op }) + } +} diff --git a/mps-interpreter/src/lang/vocabulary/item_ops/mod.rs b/mps-interpreter/src/lang/vocabulary/item_ops/mod.rs index afc2942..d50dbc6 100644 --- a/mps-interpreter/src/lang/vocabulary/item_ops/mod.rs +++ b/mps-interpreter/src/lang/vocabulary/item_ops/mod.rs @@ -6,6 +6,7 @@ mod constant; mod constructor; mod empty; mod field_assign; +mod file; mod iter_op; mod logical_and; mod logical_or; @@ -26,6 +27,7 @@ pub use constant::{ConstantItemOp, ConstantItemOpFactory}; pub use constructor::{ConstructorItemOp, ConstructorItemOpFactory}; pub use empty::{EmptyItemOp, EmptyItemOpFactory}; pub use field_assign::{FieldAssignItemOp, FieldAssignItemOpFactory}; +pub use file::{FileItemOp, FileItemOpFactory}; pub use iter_op::{IterItemOp, IterItemOpFactory}; pub use logical_and::{AndItemOp, AndItemOpFactory}; pub use logical_or::{OrItemOp, OrItemOpFactory}; diff --git a/mps-interpreter/src/lang/vocabulary/item_ops/retrieve_variable.rs b/mps-interpreter/src/lang/vocabulary/item_ops/retrieve_variable.rs index bd165ac..6e13e7d 100644 --- a/mps-interpreter/src/lang/vocabulary/item_ops/retrieve_variable.rs +++ b/mps-interpreter/src/lang/vocabulary/item_ops/retrieve_variable.rs @@ -38,10 +38,13 @@ impl MpsItemOp for VariableRetrieveItemOp { let var = context.variables.get(&self.variable_name)?; if let Some(field_name) = &self.field_name { if let MpsType::Item(item) = var { - Ok(match item.field(field_name) { - Some(val) => MpsType::Primitive(val.clone()), - None => MpsType::empty(), - }) + match item.field(field_name) { + Some(val) => Ok(MpsType::Primitive(val.clone())), + None => Err(RuntimeMsg(format!( + "Cannot access field `{}` on variable `{}` (field does not exist)", + field_name, self.variable_name + ))), + } } else { Err(RuntimeMsg(format!( "Cannot access field `{}` on variable `{}` ({} is not Item)", diff --git a/mps-interpreter/src/lib.rs b/mps-interpreter/src/lib.rs index bc0b416..ef0a728 100644 --- a/mps-interpreter/src/lib.rs +++ b/mps-interpreter/src/lib.rs @@ -230,6 +230,10 @@ //! //! Format a value into a string. This behaves differently depending on the value's type: When the value is an Item, the item's corresponding field will replace all `{field}` instances in the format string. When the value is a primitive type (String, Int, Bool, etc.), the value's text equivalent will replace all `{}` instances in the format string. When the value is an iterable operation (Op), the operation's script equivalent will replace all `{}` instances in the format string. //! +//! ### file(filepath) -- e.g. `file("~/Music/Romantic Traffic.flac"),` +//! +//! Load a item from file, populating the item with the song's tags. +//! mod context; mod interpretor; diff --git a/mps-interpreter/src/processing/filesystem.rs b/mps-interpreter/src/processing/filesystem.rs index 7c007d9..2036981 100644 --- a/mps-interpreter/src/processing/filesystem.rs +++ b/mps-interpreter/src/processing/filesystem.rs @@ -81,7 +81,7 @@ impl FileIter { root: Option

, pattern: Option<&str>, recurse: bool, - ) -> Result { + ) -> Result { let root_path = match root { None => crate::lang::utility::music_folder(), Some(p) => p.as_ref().to_path_buf(), @@ -91,7 +91,7 @@ impl FileIter { vec.push( root_path .read_dir() - .map_err(|e| RuntimeMsg(format!("Directory read error: {}", e)))? + .map_err(|e| format!("Directory read error: {}", e))? .into(), ); vec @@ -99,15 +99,12 @@ impl FileIter { Vec::with_capacity(DEFAULT_VEC_CACHE_SIZE) }; let pattern_re = if let Some(pattern) = pattern { - Some( - Regex::new(pattern) - .map_err(|e| RuntimeMsg(format!("Regex compile error: {}", e)))?, - ) + Some(Regex::new(pattern).map_err(|e| format!("Regex compile error: {}", e))?) } else { None }; - let tags_re = Regex::new(DEFAULT_REGEX) - .map_err(|e| RuntimeMsg(format!("Regex compile error: {}", e)))?; + let tags_re = + Regex::new(DEFAULT_REGEX).map_err(|e| format!("Regex compile error: {}", e))?; Ok(Self { root: root_path, pattern: pattern_re, @@ -235,6 +232,24 @@ impl FileIter { item.set_field("filename", path_str.to_string().into()); } + fn only_once(&mut self) -> Result { + if self.root.is_file() { + self.is_complete = true; + match self.build_item(&self.root) { + Some(item) => Ok(item), + None => Err(format!( + "Failed to populate item from file `{}`", + self.root.display() + )), + } + } else { + Err(format!( + "Cannot populate item from non-file `{}`", + self.root.display() + )) + } + } + /*fn default_title(path: &Path) -> String { let extension = path.extension().and_then(|ext| ext.to_str()).unwrap_or(""); path.file_name() @@ -312,6 +327,8 @@ pub trait MpsFilesystemQuerier: Debug { recursive: bool, ) -> Result; + fn single(&mut self, path: &str, pattern: Option<&str>) -> Result; + fn expand(&self, folder: Option<&str>) -> Result, RuntimeMsg> { #[cfg(feature = "shellexpand")] match folder { @@ -338,6 +355,12 @@ impl MpsFilesystemQuerier for MpsFilesystemExecutor { recursive: bool, ) -> Result { let folder = self.expand(folder)?; - FileIter::new(folder, pattern, recursive) + FileIter::new(folder, pattern, recursive).map_err(RuntimeMsg) + } + + fn single(&mut self, path: &str, pattern: Option<&str>) -> Result { + let path = self.expand(Some(path))?; + let mut file_iter = FileIter::new(path, pattern, false).map_err(RuntimeMsg)?; + file_iter.only_once().map_err(RuntimeMsg) } } diff --git a/mps-interpreter/tests/single_line.rs b/mps-interpreter/tests/single_line.rs index 6395243..1828fd0 100644 --- a/mps-interpreter/tests/single_line.rs +++ b/mps-interpreter/tests/single_line.rs @@ -534,7 +534,6 @@ fn execute_retrieveitemop_line() -> Result<(), Box> { execute_single_line( "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).{ item.path = item.filename, - item.not_a_field, item.new_field = 42, item.title = item.path, }", @@ -750,3 +749,14 @@ fn execute_uniquefilter_line() -> Result<(), Box> { true, ) } + +#[test] +fn execute_fileitemop_line() -> Result<(), Box> { + execute_single_line( + "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).{ + item = file(item.filename), +}", + false, + true, + ) +} diff --git a/src/help.rs b/src/help.rs index 5918c0c..826a8b5 100644 --- a/src/help.rs +++ b/src/help.rs @@ -146,4 +146,7 @@ Comma-separated procedure steps will be executed sequentially (like a for loop i Constructor for a new item. Each function parameter defines a new field and it's value. ~`string_format` something -- e.g. ~`{filename}` item - Format a value into a string. This behaves differently depending on the value's type: When the value is an Item, the item's corresponding field will replace all `{field}` instances in the format string. When the value is a primitive type (String, Int, Bool, etc.), the value's text equivalent will replace all `{}` instances in the format string. When the value is an iterable operation (Op), the operation's script equivalent will replace all `{}` instances in the format string."; + Format a value into a string. This behaves differently depending on the value's type: When the value is an Item, the item's corresponding field will replace all `{field}` instances in the format string. When the value is a primitive type (String, Int, Bool, etc.), the value's text equivalent will replace all `{}` instances in the format string. When the value is an iterable operation (Op), the operation's script equivalent will replace all `{}` instances in the format string. + + file(filepath) -- e.g. file(`~/Music/Romantic Traffic.flac`) + Load a item from file, populating the item with the song's tags.";