Implement embedded javascript script functions

This commit is contained in:
Tobie Morgan Hitchcock 2022-05-20 22:16:25 +01:00
parent 5425d0b550
commit a78df680d2
9 changed files with 659 additions and 54 deletions

164
Cargo.lock generated
View file

@ -8,6 +8,17 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "ahash"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
dependencies = [
"getrandom 0.2.6",
"once_cell",
"version_check",
]
[[package]]
name = "aho-corasick"
version = "0.7.18"
@ -251,6 +262,70 @@ dependencies = [
"generic-array 0.14.5",
]
[[package]]
name = "boa_engine"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ad69bff25e81f00e3938f24494cfaf628806bcb21c67da454d432f43431f8c8"
dependencies = [
"bitflags",
"boa_gc",
"boa_interner",
"boa_profiler",
"boa_unicode",
"chrono",
"dyn-clone",
"fast-float",
"gc",
"indexmap",
"num-bigint",
"num-integer",
"num-traits",
"once_cell",
"rand 0.8.5",
"regress",
"rustc-hash",
"ryu-js",
"serde",
"serde_json",
"tap",
"unicode-normalization",
]
[[package]]
name = "boa_gc"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a502c6d959fbbe7a8bf5ac51f0d32aa24594bda40e89cc8e71a3def9d24f4711"
dependencies = [
"gc",
]
[[package]]
name = "boa_interner"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e6d756c9183252a8ba3f434552766288f031a6aad6a7629fb84ddf49a7095f7"
dependencies = [
"gc",
"string-interner",
]
[[package]]
name = "boa_profiler"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be6644386d5b0c20bf502b7214f5c3ba4664e2c0db20b13a834d37cdd2120500"
[[package]]
name = "boa_unicode"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eddf0958b0fc991b4d626aa28ca2d838b0e2160a716388aeae70c6d48684bc6b"
dependencies = [
"unicode-general-category",
]
[[package]]
name = "boringssl-src"
version = "0.2.0"
@ -525,6 +600,12 @@ dependencies = [
"urlencoding",
]
[[package]]
name = "dyn-clone"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21e50f3adc76d6a43f5ed73b698a87d0760ca74617f60f7c3b879003536fdd28"
[[package]]
name = "echodb"
version = "0.3.0"
@ -569,6 +650,12 @@ dependencies = [
"rand 0.7.3",
]
[[package]]
name = "fast-float"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95765f67b4b18863968b4a1bd5bb576f732b29a4a28c7cd84c09fa3e2875f33c"
[[package]]
name = "fastrand"
version = "1.7.0"
@ -770,6 +857,27 @@ dependencies = [
"thread_local",
]
[[package]]
name = "gc"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3edaac0f5832202ebc99520cb77c932248010c4645d20be1dc62d6579f5b3752"
dependencies = [
"gc_derive",
]
[[package]]
name = "gc_derive"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60df8444f094ff7885631d80e78eb7d88c3c2361a98daaabb06256e4500db941"
dependencies = [
"proc-macro2",
"quote",
"syn",
"synstructure",
]
[[package]]
name = "generic-array"
version = "0.12.4"
@ -946,6 +1054,9 @@ name = "hashbrown"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
dependencies = [
"ahash",
]
[[package]]
name = "headers"
@ -1439,6 +1550,7 @@ dependencies = [
"autocfg",
"num-integer",
"num-traits",
"serde",
]
[[package]]
@ -1998,6 +2110,15 @@ version = "0.6.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
[[package]]
name = "regress"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a92ff21fe8026ce3f2627faaf43606f0b67b014dbc9ccf027181a804f75d92e"
dependencies = [
"memchr",
]
[[package]]
name = "remove_dir_all"
version = "0.5.3"
@ -2138,6 +2259,12 @@ version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f"
[[package]]
name = "ryu-js"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6518fc26bced4d53678a22d6e423e9d8716377def84545fe328236e3af070e7f"
[[package]]
name = "safemem"
version = "0.3.3"
@ -2400,6 +2527,17 @@ dependencies = [
"thiserror",
]
[[package]]
name = "string-interner"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e2531d8525b29b514d25e275a43581320d587b86db302b9a7e464bac579648"
dependencies = [
"cfg-if",
"hashbrown",
"serde",
]
[[package]]
name = "strsim"
version = "0.10.0"
@ -2449,11 +2587,13 @@ dependencies = [
"async-executor",
"async-recursion",
"bigdecimal",
"boa_engine",
"chrono",
"dmp",
"echodb",
"futures 0.3.21",
"fuzzy-matcher",
"gc",
"geo",
"indxdb",
"lexical-sort",
@ -2503,6 +2643,24 @@ dependencies = [
"unicode-xid",
]
[[package]]
name = "synstructure"
version = "0.12.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f"
dependencies = [
"proc-macro2",
"quote",
"syn",
"unicode-xid",
]
[[package]]
name = "tap"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "tempfile"
version = "3.3.0"
@ -2899,6 +3057,12 @@ version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
[[package]]
name = "unicode-general-category"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1218098468b8085b19a2824104c70d976491d247ce194bbd9dc77181150cdfd6"
[[package]]
name = "unicode-normalization"
version = "0.1.19"

View file

@ -6,17 +6,19 @@ version = "0.1.0"
authors = ["Tobie Morgan Hitchcock <tobie@surrealdb.com>"]
[features]
default = ["parallel", "kv-tikv", "kv-echodb", "kv-yokudb"]
default = ["parallel", "kv-tikv", "kv-echodb", "kv-yokudb", "scripting"]
parallel = ["executor"]
kv-tikv = ["tikv"]
kv-echodb = ["echodb"]
kv-indxdb = ["indxdb"]
kv-yokudb = []
scripting = ["boa", "gc"]
[dependencies]
argon2 = "0.4.0"
async-recursion = "1.0.0"
bigdecimal = { version = "0.3.0", features = ["serde", "string-only"] }
boa = { version = "0.14.0", package = "boa_engine", optional = true }
channel = { version = "1.6.1", package = "async-channel" }
chrono = { version = "0.4.19", features = ["serde"] }
derive = { version = "0.1.2", package = "surrealdb-derive" }
@ -25,6 +27,7 @@ echodb = { version = "0.3.0", optional = true }
executor = { version = "1.4.1", package = "async-executor", optional = true }
futures = "0.3.21"
fuzzy-matcher = "0.3.7"
gc = { version = "0.4.1", optional = true }
geo = { version = "0.20.1", features = ["use-serde"] }
indxdb = { version = "0.2.0", optional = true }
lexical-sort = "0.3.1"

View file

@ -1,3 +1,4 @@
use crate::err::Error;
use std::fmt;
use std::io;
@ -16,6 +17,15 @@ impl fmt::Display for Reason {
}
}
impl From<Reason> for Error {
fn from(reason: Reason) -> Self {
match reason {
Reason::Timedout => Error::QueryTimedout,
Reason::Canceled => Error::QueryCancelled,
}
}
}
impl From<Reason> for io::Error {
fn from(reason: Reason) -> Self {
let kind = match reason {

View file

@ -1,10 +1,291 @@
#![cfg(feature = "scripting")]
use crate::ctx::Context;
use crate::err::Error;
use crate::sql::script::Script;
use crate::sql::array::Array;
use crate::sql::datetime::Datetime;
use crate::sql::duration::Duration;
use crate::sql::number::Number;
use crate::sql::object::Object;
use crate::sql::thing::Thing;
use crate::sql::value::Value;
use bigdecimal::ToPrimitive;
use boa::builtins::date::Date;
use boa::class::Class;
use boa::class::ClassBuilder;
use boa::object::JsArray;
use boa::object::JsObject;
use boa::object::ObjectData;
use boa::object::ObjectKind;
use boa::property::Attribute;
use boa::Context as Boa;
use boa::JsResult;
use boa::JsString;
use boa::JsValue;
use chrono::Datelike;
use chrono::Timelike;
use gc::Finalize;
use gc::Trace;
pub fn run(_ctx: &Context, _expr: Script) -> Result<Value, Error> {
Err(Error::InvalidScript {
message: String::from("Embedded functions are not yet supported."),
})
#[derive(Debug, Trace, Finalize)]
pub struct JsDuration {
value: String,
}
impl Class for JsDuration {
const NAME: &'static str = "Duration";
const LENGTH: usize = 1;
fn constructor(_this: &JsValue, args: &[JsValue], ctx: &mut Boa) -> JsResult<Self> {
Ok(JsDuration {
value: args.get(0).cloned().unwrap_or_default().to_string(ctx)?.to_string(),
})
}
fn init(class: &mut ClassBuilder) -> JsResult<()> {
class.method("value", 0, |this, _, _| {
if let Some(v) = this.as_object() {
if let Some(v) = v.downcast_ref::<JsDuration>() {
return Ok(v.value.clone().into());
}
}
Ok(JsValue::Undefined)
});
class.method("toString", 0, |this, _, _| {
if let Some(v) = this.as_object() {
if let Some(v) = v.downcast_ref::<JsDuration>() {
return Ok(v.value.clone().into());
}
}
Ok(JsValue::Undefined)
});
Ok(())
}
}
#[derive(Debug, Trace, Finalize)]
pub struct JsRecord {
tb: String,
id: String,
}
impl Class for JsRecord {
const NAME: &'static str = "Record";
const LENGTH: usize = 2;
fn constructor(_this: &JsValue, args: &[JsValue], ctx: &mut Boa) -> JsResult<Self> {
Ok(JsRecord {
tb: args.get(0).cloned().unwrap_or_default().to_string(ctx)?.to_string(),
id: args.get(1).cloned().unwrap_or_default().to_string(ctx)?.to_string(),
})
}
fn init(class: &mut ClassBuilder) -> JsResult<()> {
class.method("tb", 0, |this, _, _| {
if let Some(v) = this.as_object() {
if let Some(v) = v.downcast_ref::<JsRecord>() {
return Ok(v.tb.clone().into());
}
}
Ok(JsValue::Undefined)
});
class.method("id", 0, |this, _, _| {
if let Some(v) = this.as_object() {
if let Some(v) = v.downcast_ref::<JsRecord>() {
return Ok(v.id.clone().into());
}
}
Ok(JsValue::Undefined)
});
class.method("toString", 0, |this, _, _| {
if let Some(v) = this.as_object() {
if let Some(v) = v.downcast_ref::<JsRecord>() {
return Ok(format!("{}:{}", v.tb, v.id).into());
}
}
Ok(JsValue::Undefined)
});
Ok(())
}
}
pub fn run(ctx: &Context, doc: Option<&Value>, src: &str) -> Result<Value, Error> {
let _ = ctx.check()?;
// Create an execution context
let mut ctx = Boa::default();
// Retrieve the current document
let obj = doc.map_or(JsValue::Undefined, JsValue::from);
// Create the main function structure
let src = format!("(function() {{ {} }}).call(document)", src);
// Register the current document as a global object
ctx.register_global_property("document", obj, Attribute::default());
// Register the JsDuration type as a global class
ctx.register_global_class::<JsDuration>().unwrap();
// Register the JsRecord type as a global class
ctx.register_global_class::<JsRecord>().unwrap();
// Attempt to execute the script
match ctx.eval(src.as_bytes()) {
// The script executed successfully
Ok(ref v) => Ok(v.into()),
// There was an error running the script
Err(e) => Err(Error::InvalidScript {
message: e.display().to_string(),
}),
}
}
impl From<&Datetime> for Date {
fn from(v: &Datetime) -> Self {
let mut obj = Self::default();
obj.set_components(
true,
Some(v.year() as f64),
Some(v.month0() as f64),
Some(v.day() as f64),
Some(v.hour() as f64),
Some(v.minute() as f64),
Some(v.second() as f64),
Some((v.nanosecond() / 1_000_000) as f64),
);
obj
}
}
impl From<&Value> for JsValue {
fn from(v: &Value) -> Self {
match v {
Value::Null => JsValue::Null,
Value::Void => JsValue::Undefined,
Value::None => JsValue::Undefined,
Value::True => JsValue::Boolean(true),
Value::False => JsValue::Boolean(false),
Value::Strand(v) => JsValue::String(v.as_str().into()),
Value::Number(Number::Int(v)) => JsValue::Integer(*v as i32),
Value::Number(Number::Float(v)) => JsValue::Rational(*v as f64),
Value::Number(Number::Decimal(v)) => match v.is_integer() {
true => JsValue::BigInt(v.to_i64().unwrap_or_default().into()),
false => JsValue::Rational(v.to_f64().unwrap_or_default()),
},
Value::Datetime(v) => JsValue::from(JsObject::from_proto_and_data(
Boa::default().intrinsics().constructors().object().prototype(),
ObjectData::date(v.into()),
)),
Value::Duration(v) => JsValue::from(JsObject::from_proto_and_data(
Boa::default().intrinsics().constructors().object().prototype(),
ObjectData::native_object(Box::new(JsDuration {
value: v.to_string(),
})),
)),
Value::Thing(v) => JsValue::from(JsObject::from_proto_and_data(
Boa::default().intrinsics().constructors().object().prototype(),
ObjectData::native_object(Box::new(JsRecord {
tb: v.tb.to_string(),
id: v.id.to_string(),
})),
)),
Value::Array(v) => {
let ctx = &mut Boa::default();
let arr = JsArray::new(ctx);
for v in v.iter() {
arr.push(JsValue::from(v), ctx).unwrap();
}
JsValue::from(arr)
}
Value::Object(v) => {
let ctx = &mut Boa::default();
let obj = JsObject::default();
for (k, v) in v.iter() {
let k = JsString::from(k.as_str());
let v = JsValue::from(v);
obj.set(k, v, true, ctx).unwrap();
}
JsValue::from(obj)
}
_ => JsValue::Null,
}
}
}
impl From<&JsValue> for Value {
fn from(v: &JsValue) -> Self {
match v {
JsValue::Null => Value::Null,
JsValue::Undefined => Value::None,
JsValue::Boolean(v) => Value::from(*v),
JsValue::String(v) => Value::from(v.as_str()),
JsValue::Integer(v) => Value::from(Number::Int(*v as i64)),
JsValue::Rational(v) => Value::from(Number::Float(*v as f64)),
JsValue::BigInt(v) => Value::from(Number::from(v.clone().to_string())),
JsValue::Object(v) => {
// Check to see if this object is a duration
if v.is::<JsDuration>() {
if let Some(v) = v.downcast_ref::<JsDuration>() {
let v = v.value.clone();
return Duration::from(v).into();
}
}
// Check to see if this object is a record
if v.is::<JsRecord>() {
if let Some(v) = v.downcast_ref::<JsRecord>() {
let v = (v.tb.clone(), v.id.clone());
return Thing::from(v).into();
}
}
// Check to see if this object is a date
if let Some(v) = v.borrow().as_date() {
if let Some(v) = v.to_utc() {
return Datetime::from(v).into();
}
}
// Get a borrowed reference to the object
let o = v.borrow();
// Check to see if this is a normal type
match o.kind() {
// This object is a Javascript Array
ObjectKind::Array => {
let mut x = Array::default();
let ctx = &mut Boa::default();
let len = v.get("length", ctx).unwrap().to_u32(ctx).unwrap();
for i in 0..len {
let v = o.properties().get(&i.into()).unwrap();
if let Some(v) = v.value() {
let v = Value::from(v);
x.push(v);
}
}
x.into()
}
// This object is a Javascript Object
ObjectKind::Ordinary => {
let mut x = Object::default();
for (k, v) in o.properties().iter() {
if let Some(v) = v.value() {
let k = k.to_string();
let v = Value::from(v);
x.insert(k, v);
}
}
x.into()
}
// This object is a Javascript Map
ObjectKind::Map(v) => {
let mut x = Object::default();
for (k, v) in v.iter() {
let k = Value::from(k).as_string();
let v = Value::from(v);
x.insert(k, v);
}
x.into()
}
// This object is a Javascript Set
ObjectKind::Set(v) => {
let mut x = Array::default();
for v in v.iter() {
let v = Value::from(v);
x.push(v);
}
x.into()
}
_ => Value::Null,
}
}
_ => Value::Null,
}
}
}

View file

@ -27,6 +27,15 @@ impl From<time::Duration> for Duration {
}
}
impl From<String> for Duration {
fn from(s: String) -> Self {
match duration(s.as_ref()) {
Ok((_, v)) => v,
Err(_) => Duration::default(),
}
}
}
impl<'a> From<&'a str> for Duration {
fn from(s: &str) -> Self {
match duration(s) {

View file

@ -5,7 +5,6 @@ use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error<I> {
ScriptError(String),
ParserError(I),
}

View file

@ -11,6 +11,7 @@ use crate::sql::value::{single, value, Value};
use nom::branch::alt;
use nom::bytes::complete::tag;
use nom::character::complete::char;
use nom::combinator::opt;
use nom::multi::separated_list0;
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
@ -110,27 +111,32 @@ impl Function {
doc: Option<&Value>,
) -> Result<Value, Error> {
match self {
Function::Future(ref e) => match opt.futures {
Function::Cast(s, e) => {
let a = e.compute(ctx, opt, txn, doc).await?;
fnc::cast::run(ctx, s, a)
}
Function::Normal(s, e) => {
let mut a: Vec<Value> = Vec::with_capacity(e.len());
for v in e {
a.push(v.compute(ctx, opt, txn, doc).await?);
}
fnc::run(ctx, s, a).await
}
Function::Future(e) => match opt.futures {
true => {
let a = e.compute(ctx, opt, txn, doc).await?;
fnc::future::run(ctx, a)
}
false => Ok(self.to_owned().into()),
},
Function::Script(ref s) => {
let a = s.to_owned();
fnc::script::run(ctx, a)
}
Function::Cast(ref s, ref e) => {
let a = e.compute(ctx, opt, txn, doc).await?;
fnc::cast::run(ctx, s, a)
}
Function::Normal(ref s, ref e) => {
let mut a: Vec<Value> = Vec::with_capacity(e.len());
for v in e {
a.push(v.compute(ctx, opt, txn, doc).await?);
}
fnc::run(ctx, s, a).await
#[allow(unused_variables)]
Function::Script(s) => {
#[cfg(feature = "scripting")]
return fnc::script::run(ctx, doc, s);
#[cfg(not(feature = "scripting"))]
return Err(Error::InvalidScript {
message: String::from("Embedded functions are not enabled."),
});
}
}
}
@ -140,7 +146,7 @@ impl fmt::Display for Function {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Function::Future(ref e) => write!(f, "fn::future -> {{ {} }}", e),
Function::Script(ref s) => write!(f, "fn::script -> {{ {} }}", s),
Function::Script(ref s) => write!(f, "fn::script -> {{{}}}", s),
Function::Cast(ref s, ref e) => write!(f, "<{}> {}", s, e),
Function::Normal(ref s, ref e) => write!(
f,
@ -153,7 +159,21 @@ impl fmt::Display for Function {
}
pub fn function(i: &str) -> IResult<&str, Function> {
alt((casts, embed, future, normal))(i)
alt((scripts, future, casts, normal))(i)
}
fn scripts(i: &str) -> IResult<&str, Function> {
let (i, _) = tag("fn::script")(i)?;
let (i, _) = mightbespace(i)?;
let (i, _) = char('-')(i)?;
let (i, _) = char('>')(i)?;
let (i, _) = mightbespace(i)?;
let (i, _) = opt(tag("function()"))(i)?;
let (i, _) = mightbespace(i)?;
let (i, _) = char('{')(i)?;
let (i, v) = script(i)?;
let (i, _) = char('}')(i)?;
Ok((i, Function::Script(v)))
}
fn future(i: &str) -> IResult<&str, Function> {
@ -170,20 +190,6 @@ fn future(i: &str) -> IResult<&str, Function> {
Ok((i, Function::Future(v)))
}
fn embed(i: &str) -> IResult<&str, Function> {
let (i, _) = tag("fn::script")(i)?;
let (i, _) = mightbespace(i)?;
let (i, _) = char('-')(i)?;
let (i, _) = char('>')(i)?;
let (i, _) = mightbespace(i)?;
let (i, _) = char('{')(i)?;
let (i, _) = mightbespace(i)?;
let (i, v) = script(i)?;
let (i, _) = mightbespace(i)?;
let (i, _) = char('}')(i)?;
Ok((i, Function::Script(v)))
}
fn casts(i: &str) -> IResult<&str, Function> {
let (i, _) = char('<')(i)?;
let (i, s) = function_casts(i)?;
@ -486,4 +492,22 @@ mod tests {
assert_eq!("fn::future -> { 1.2345 + 5.4321 }", format!("{}", out));
assert_eq!(out, Function::Future(Value::from(Expression::parse("1.2345 + 5.4321"))));
}
#[test]
fn function_script_expression() {
let sql = "fn::script -> { return this.tags.filter(t => { return t.length > 3; }); }";
let res = function(sql);
assert!(res.is_ok());
let out = res.unwrap().1;
assert_eq!(
"fn::script -> { return this.tags.filter(t => { return t.length > 3; }); }",
format!("{}", out)
);
assert_eq!(
out,
Function::Script(Script::parse(
" return this.tags.filter(t => { return t.length > 3; }); "
))
);
}
}

View file

@ -1,6 +1,5 @@
use crate::err::Error;
use crate::sql::error::Error::ParserError;
use crate::sql::error::Error::ScriptError;
use crate::sql::query::{query, Query};
use crate::sql::value::{json as value, Value};
use nom::Err;
@ -20,9 +19,6 @@ pub fn parse(input: &str) -> Result<Query, Error> {
sql: s.to_string(),
})
}
ScriptError(e) => Err(Error::InvalidScript {
message: e,
}),
},
Err(Err::Failure(e)) => match e {
ParserError(e) => {
@ -33,9 +29,6 @@ pub fn parse(input: &str) -> Result<Query, Error> {
sql: s.to_string(),
})
}
ScriptError(e) => Err(Error::InvalidScript {
message: e,
}),
},
_ => unreachable!(),
},
@ -56,9 +49,6 @@ pub fn json(input: &str) -> Result<Value, Error> {
sql: s.to_string(),
})
}
ScriptError(e) => Err(Error::InvalidScript {
message: e,
}),
},
Err(Err::Failure(e)) => match e {
ParserError(e) => {
@ -69,9 +59,6 @@ pub fn json(input: &str) -> Result<Value, Error> {
sql: s.to_string(),
})
}
ScriptError(e) => Err(Error::InvalidScript {
message: e,
}),
},
_ => unreachable!(),
},

View file

@ -1,12 +1,43 @@
use crate::sql::error::IResult;
use nom::branch::alt;
use nom::bytes::complete::escaped;
use nom::bytes::complete::is_not;
use nom::bytes::complete::tag;
use nom::character::complete::one_of;
use nom::combinator::recognize;
use nom::multi::many1;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::ops::Deref;
use std::str;
const SINGLE: &str = r#"'"#;
const SINGLE_ESC: &str = r#"\'"#;
const DOUBLE: &str = r#"""#;
const DOUBLE_ESC: &str = r#"\""#;
const BACKTICK: &str = r#"`"#;
const BACKTICK_ESC: &str = r#"\`"#;
const OBJECT_BEG: &str = "{";
const OBJECT_END: &str = "}";
#[derive(Clone, Debug, Default, Eq, PartialEq, PartialOrd, Serialize, Deserialize)]
pub struct Script(pub String);
impl From<String> for Script {
fn from(s: String) -> Self {
Script(s)
}
}
impl<'a> From<&'a str> for Script {
fn from(s: &str) -> Self {
Script(String::from(s))
}
}
impl Deref for Script {
type Target = String;
fn deref(&self) -> &Self::Target {
@ -16,10 +47,107 @@ impl Deref for Script {
impl fmt::Display for Script {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "\"{}\"", self.0)
write!(f, "{}", self.0)
}
}
pub fn script(_: &str) -> IResult<&str, Script> {
unimplemented!()
pub fn script(i: &str) -> IResult<&str, Script> {
let (i, v) = recognize(script_raw)(i)?;
Ok((i, Script(String::from(v))))
}
pub fn script_raw(i: &str) -> IResult<&str, &str> {
recognize(many1(alt((char_any, char_object, string_single, string_double, string_backtick))))(i)
}
fn char_any(i: &str) -> IResult<&str, &str> {
is_not("{}'`\"")(i)
}
fn char_object(i: &str) -> IResult<&str, &str> {
let (i, _) = tag(OBJECT_BEG)(i)?;
let (i, v) = script_raw(i)?;
let (i, _) = tag(OBJECT_END)(i)?;
Ok((i, v))
}
fn string_single(i: &str) -> IResult<&str, &str> {
let (i, _) = tag(SINGLE)(i)?;
let (i, v) = alt((escaped(is_not(SINGLE_ESC), '\\', one_of(SINGLE)), tag("")))(i)?;
let (i, _) = tag(SINGLE)(i)?;
Ok((i, v))
}
fn string_double(i: &str) -> IResult<&str, &str> {
let (i, _) = tag(DOUBLE)(i)?;
let (i, v) = alt((escaped(is_not(DOUBLE_ESC), '\\', one_of(DOUBLE)), tag("")))(i)?;
let (i, _) = tag(DOUBLE)(i)?;
Ok((i, v))
}
fn string_backtick(i: &str) -> IResult<&str, &str> {
let (i, _) = tag(BACKTICK)(i)?;
let (i, v) = alt((escaped(is_not(BACKTICK_ESC), '\\', one_of(BACKTICK)), tag("")))(i)?;
let (i, _) = tag(BACKTICK)(i)?;
Ok((i, v))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn script_basic() {
let sql = "return true;";
let res = script(sql);
assert!(res.is_ok());
let out = res.unwrap().1;
assert_eq!("return true;", format!("{}", out));
assert_eq!(out, Script::from("return true;"));
}
#[test]
fn script_object() {
let sql = "return { test: true, something: { other: true } };";
let res = script(sql);
assert!(res.is_ok());
let out = res.unwrap().1;
assert_eq!("return { test: true, something: { other: true } };", format!("{}", out));
assert_eq!(out, Script::from("return { test: true, something: { other: true } };"));
}
#[test]
fn script_closure() {
let sql = "return this.values.map(v => `This value is ${Number(v * 3)}`);";
let res = script(sql);
assert!(res.is_ok());
let out = res.unwrap().1;
assert_eq!(
"return this.values.map(v => `This value is ${Number(v * 3)}`);",
format!("{}", out)
);
assert_eq!(
out,
Script::from("return this.values.map(v => `This value is ${Number(v * 3)}`);")
);
}
#[test]
fn script_complex() {
let sql = r#"return { test: true, some: { object: "some text with uneven {{{ {} \" brackets", else: false } };"#;
let res = script(sql);
assert!(res.is_ok());
let out = res.unwrap().1;
assert_eq!(
r#"return { test: true, some: { object: "some text with uneven {{{ {} \" brackets", else: false } };"#,
format!("{}", out)
);
assert_eq!(
out,
Script::from(
r#"return { test: true, some: { object: "some text with uneven {{{ {} \" brackets", else: false } };"#
)
);
}
}