use crate::executor::{ExecutionStatus, Executor, ExecutorError};
use crossbeam::channel::{bounded, Receiver, Sender};
use fuse_rust::{FuseProperty, Fuseable};
use serde::Deserialize;
use std::{fmt, fs, path::PathBuf, thread};
pub struct Script {
pub metadata: Metadata,
pub path: PathBuf,
source: String,
channel: Option<ExecutorChannel>,
}
#[derive(Debug)]
enum ExecutorJob {
Request((String, Option<String>)),
Responce(Result<ExecutionStatus, ExecutorError>),
Kill,
}
struct ExecutorChannel {
sender: Sender<ExecutorJob>,
receiver: Receiver<ExecutorJob>,
}
#[derive(Debug)]
pub enum ParseScriptError {
NoMetadata,
InvalidMetadata(serde_jsonrc::error::Error),
FailedToRead(std::io::Error),
}
impl fmt::Display for ParseScriptError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ParseScriptError::NoMetadata => write!(f, "no metadata"),
ParseScriptError::InvalidMetadata(e) => write!(f, "invalid metadata: {}", e),
ParseScriptError::FailedToRead(e) => write!(f, "failed to read script: {}", e),
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct Metadata {
pub api: u32,
pub name: String,
pub description: String,
pub author: Option<String>,
pub icon: String,
pub tags: Option<String>,
}
impl Fuseable for Metadata {
fn properties(&self) -> Vec<fuse_rust::FuseProperty> {
return vec![
FuseProperty {
value: "name".to_string(),
weight: 1.0,
},
];
}
fn lookup(&self, key: &str) -> Option<&str> {
match key {
"name" => Some(&self.name),
_ => None,
}
}
}
impl Fuseable for &Script {
fn properties(&self) -> Vec<FuseProperty> {
self.metadata.properties()
}
fn lookup(&self, key: &str) -> Option<&str> {
self.metadata.lookup(key)
}
}
impl Script {
pub fn from_file(path: PathBuf) -> Result<Self, ParseScriptError> {
match fs::read_to_string(path.clone()) {
Ok(source) => Script::from_source(source, path),
Err(e) => Err(ParseScriptError::FailedToRead(e)),
}
}
pub fn from_source(source: String, path: PathBuf) -> Result<Self, ParseScriptError> {
let start = source.find("/**").ok_or(ParseScriptError::NoMetadata)?;
let end = source.find("**/").ok_or(ParseScriptError::NoMetadata)?;
let mut metadata: Metadata = serde_jsonrc::from_str(&source[start + 3..end])
.map_err(ParseScriptError::InvalidMetadata)?;
metadata.icon = metadata.icon.to_lowercase();
Ok(Script {
metadata,
source,
channel: None,
path,
})
}
fn init_executor_thread(&mut self) {
assert!(self.channel.is_none());
let (sender, receiver) = bounded(0);
{
let t_name = self.metadata.name.clone();
let t_source = self.source.clone();
let (t_sender, t_receiver) = (sender.clone(), receiver.clone());
thread::spawn(move || {
info!("thread spawned for {}", t_name);
let mut executor = None;
debug!("executor created");
loop {
match t_receiver.recv().unwrap() {
ExecutorJob::Request((full_text, selection)) => {
if executor.is_none() {
let exe = Executor::new(&t_source);
if let Err(ref err) = exe {
warn!("failed to create executor");
t_sender.send(ExecutorJob::Responce(Err(err.clone()))).unwrap();
}
executor = exe.ok();
}
if let Some(executor) = executor.as_mut() {
info!(
"request received, full_text: {} bytes, selection: {} bytes",
full_text.len(),
selection.as_ref().map(|s| s.len()).unwrap_or(0),
);
let result = executor.execute(&full_text, selection.as_deref());
t_sender.send(ExecutorJob::Responce(result)).unwrap(); }
}
ExecutorJob::Responce(_) => {
warn!("executor thread received a responce on channel");
}
ExecutorJob::Kill => {
info!("killing thread for {}", t_name);
return;
}
}
}
});
};
self.channel = Some(ExecutorChannel { sender, receiver });
}
pub fn kill_thread(&mut self) {
if let Some(channel) = &self.channel {
channel.sender.send(ExecutorJob::Kill).unwrap(); }
self.channel = None;
}
pub fn execute(
&mut self,
full_text: &str,
selection: Option<&str>,
) -> Result<ExecutionStatus, ExecutorError> {
if self.channel.is_none() {
self.init_executor_thread();
}
assert!(self.channel.is_some());
let channel = self.channel.as_ref().expect("channel is none");
channel
.sender
.send(ExecutorJob::Request((
full_text.to_owned(),
selection.map(|s| s.to_owned()),
)))
.expect("channel is disconnected");
let result = channel
.receiver
.recv()
.expect("receive channel is empty and disconnected");
if let ExecutorJob::Responce(status) = result {
return status;
}
panic!(
"expected a responce on channel, but got a request: {:?}",
result
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{executor::TextReplacement, script::ParseScriptError};
use std::borrow::Cow;
#[test]
fn test_retain_execution_context() {
let mut script = Script::from_source(
"
/**
{
\"api\":1,
\"name\":\"Counter\",
\"description\":\"Counts up\",
\"author\":\"Zoey\",
\"icon\":\"html\",
\"tags\":\"count\"
}
**/
let number = 0;
function main(state) {
number += 1;
state.text = number;
}"
.to_string(),
PathBuf::new(),
)
.unwrap();
for i in 1..10 {
let status = script.execute("", None);
assert!(status.is_ok());
assert_eq!(
TextReplacement::Full(i.to_string()),
status.unwrap().into_replacement()
);
}
}
#[test]
fn test_is_selection() {
let mut script = Script::from_source(
r#"
/**
{
"api": 1,
"name": "Test",
"description": "Test script",
"author": "Zoey",
"icon": "html",
"tags": "test"
}
**/
let number = 0;
function main(state) {
state.fullText = state.isSelection;
}"#
.to_string(),
PathBuf::new(),
)
.unwrap();
let status = script.execute("", None);
assert!(status.is_ok());
assert_eq!(
TextReplacement::Full("false".to_string()),
status.unwrap().into_replacement()
);
let status = script.execute("foo", Some("fo"));
assert!(status.is_ok());
assert_eq!(
TextReplacement::Full("true".to_string()),
status.unwrap().into_replacement()
);
}
#[test]
fn test_builtin_scripts() {
use rust_embed::RustEmbed;
#[derive(RustEmbed)]
#[folder = "submodules/Boop/Boop/Boop/scripts/"]
struct Scripts;
for file in Scripts::iter() {
println!("testing {}", file);
let source: Cow<'static, [u8]> = Scripts::get(&file).unwrap();
let script_source = String::from_utf8(source.to_vec()).unwrap();
let full_text = match file.as_ref() {
"MinifyJSON.js" => "{\n\n\"foo\":\n\"bar\"}",
"SumAll.js" => "100\n9.00\n230\n2.09",
_ => "foobar ♈ ♉ ♊ ♋ ♌ ♍ ♎ ♏ ♐ ♑ ♒ ♓ 😁 😝 😋 😄",
};
match Script::from_source(script_source, PathBuf::new()) {
Ok(mut script) => {
script.execute(full_text, None).unwrap();
}
Err(e) => match e {
ParseScriptError::NoMetadata => {
assert!(file.starts_with("lib/")); }
e => panic!(e),
},
}
}
}
#[test]
fn test_extra_scripts() {
use rust_embed::RustEmbed;
#[derive(RustEmbed)]
#[folder = "submodules/Boop/Scripts/"]
struct Scripts;
for file in Scripts::iter() {
if !file.ends_with(".js") {
continue; }
println!("testing {}", file);
let source: Cow<'static, [u8]> = Scripts::get(&file).unwrap();
let script_source = String::from_utf8(source.to_vec()).unwrap();
match Script::from_source(script_source, PathBuf::new()) {
Ok(mut script) => {
script
.execute(
"foobar ♈ ♉ ♊ ♋ ♌ ♍ ♎ ♏ ♐ ♑ ♒ ♓ 😁 😝 😋 😄",
None,
)
.unwrap();
}
Err(e) => panic!(e),
}
}
}
}