diff --git a/README.md b/README.md index 4083d09..b0d17ec 100644 --- a/README.md +++ b/README.md @@ -793,6 +793,8 @@ Here is a list of built-in functions available: | match | match regex | `match("AaAaAbbBBBb", "(?i)a+(?-i)b+")` | | replace | replace | `replace("AaAaAbbBBBb", "(?i)a+(?-i)b+", "xxx")` | | replace_all | replace all | `replace_all("AaAaAbbBBBb", "A", "b")` | +| jsonify | jsonify value | `jsonify(struct {a: 9})` | +| parse_json | parse a json string | `parse_json("""{"a": 9}""")` | #### Matching regexes diff --git a/adana-script-core/Cargo.toml b/adana-script-core/Cargo.toml index 0aae50c..26cae88 100644 --- a/adana-script-core/Cargo.toml +++ b/adana-script-core/Cargo.toml @@ -28,5 +28,6 @@ regex = { workspace = true, default-features = false, features = [ "unicode-script", "unicode-segment", ] } +serde_json.workspace = true [target.'cfg(not(target_arch = "wasm32"))'.dependencies] libloading.workspace = true diff --git a/adana-script-core/src/lib.rs b/adana-script-core/src/lib.rs index e9086f0..f8a1075 100644 --- a/adana-script-core/src/lib.rs +++ b/adana-script-core/src/lib.rs @@ -4,12 +4,12 @@ use std::collections::BTreeMap; use constants::{ BREAK, CAPITALIZE, CEIL, DROP, ELSE, EULER_NUMBER, FALSE, FLOOR, FOR, IF, IN, IS_ARRAY, IS_BOOL, IS_DOUBLE, IS_ERROR, IS_FUNCTION, IS_I8, IS_INT, - IS_MATCH, IS_STRUCT, IS_U8, MAKE_ERROR, MATCH, MULTILINE, NULL, PI, - REPLACE, REPLACE_ALL, REQUIRE, RETURN, ROUND, STRUCT, TAU, TO_BINARY, - TO_HEX, TO_LOWER, TO_UPPER, TRUE, WHILE, + IS_MATCH, IS_STRUCT, IS_U8, JSONIFY, MAKE_ERROR, MATCH, MULTILINE, NULL, + PARSE_JSON, PI, REPLACE, REPLACE_ALL, REQUIRE, RETURN, ROUND, STRUCT, TAU, + TO_BINARY, TO_HEX, TO_LOWER, TO_UPPER, TRUE, WHILE, }; +pub use primitive::Primitive; -use primitive::Primitive; use serde::{Deserialize, Serialize}; use strum::EnumCount; @@ -68,6 +68,8 @@ pub mod constants { pub const IS_FUNCTION: &str = "is_function"; pub const IS_ARRAY: &str = "is_array"; pub const IS_STRUCT: &str = "is_struct"; + pub const JSONIFY: &str = "jsonify"; + pub const PARSE_JSON: &str = "parse_json"; pub const MAKE_ERROR: &str = "make_err"; pub const ABS: &str = "abs"; pub const LENGTH: &str = "length"; @@ -236,6 +238,8 @@ pub enum BuiltInFunctionType { IsDouble, IsFunction, IsArray, + ParseJson, + Jsonify, } #[derive(Debug, Eq, PartialEq, Copy, Clone, Serialize, Deserialize)] @@ -332,6 +336,8 @@ impl BuiltInFunctionType { BuiltInFunctionType::IsFunction => IS_FUNCTION, BuiltInFunctionType::IsArray => IS_ARRAY, BuiltInFunctionType::MakeError => MAKE_ERROR, + BuiltInFunctionType::Jsonify => JSONIFY, + BuiltInFunctionType::ParseJson => PARSE_JSON, } } } @@ -402,6 +408,8 @@ pub const FORBIDDEN_VARIABLE_NAME: &[&str] = &[ IS_BOOL, IS_ARRAY, MAKE_ERROR, + JSONIFY, + PARSE_JSON, EVAL, TO_BOOL, SQRT, diff --git a/adana-script-core/src/primitive.rs b/adana-script-core/src/primitive/core_primitive.rs similarity index 99% rename from adana-script-core/src/primitive.rs rename to adana-script-core/src/primitive/core_primitive.rs index ea4f25c..3f23e3e 100644 --- a/adana-script-core/src/primitive.rs +++ b/adana-script-core/src/primitive/core_primitive.rs @@ -12,7 +12,7 @@ use std::{ sync::{Arc, RwLock}, }; -use super::{constants::NULL, Value}; +use crate::{constants::NULL, Value}; const MAX_U32_AS_I128: i128 = u32::MAX as i128; diff --git a/adana-script-core/src/primitive/json.rs b/adana-script-core/src/primitive/json.rs new file mode 100644 index 0000000..a8dcf04 --- /dev/null +++ b/adana-script-core/src/primitive/json.rs @@ -0,0 +1,84 @@ +use std::collections::BTreeMap; + +use anyhow::anyhow; +use serde_json::{json, Value}; + +use super::Primitive; + +pub trait Json { + fn from_json(s: &str) -> anyhow::Result + where + Self: Sized; + fn to_json(&self) -> anyhow::Result; +} + +fn json_to_primitive(value: Value) -> anyhow::Result { + match value { + Value::Null => Ok(Primitive::Null), + Value::Bool(b) => Ok(Primitive::Bool(b)), + Value::Number(number) => number + .as_u64() + .map(|n| Primitive::Int(n as i128)) + .or(number.as_i64().map(|n| Primitive::Int(n as i128))) + .or(number.as_f64().map(Primitive::Double)) + .ok_or(anyhow!("could not parse json number")), + Value::String(s) => Ok(Primitive::String(s)), + Value::Array(json_array) => { + let mut prim_array = Vec::with_capacity(json_array.len()); + for v in json_array { + prim_array.push(json_to_primitive(v)?); + } + Ok(Primitive::Array(prim_array)) + } + Value::Object(o) => { + let mut struct_array = BTreeMap::new(); + for (k, v) in o { + struct_array.insert(k, json_to_primitive(v)?); + } + Ok(Primitive::Struct(struct_array)) + } + } +} +fn primitive_to_value(p: &Primitive) -> anyhow::Result { + match p { + Primitive::Ref(r) => { + let r = + r.read().map_err(|e| anyhow!("could not acquire lock! {e}"))?; + primitive_to_value(&r) + } + Primitive::U8(u) => Ok(json!(u)), + Primitive::I8(u) => Ok(json!(u)), + Primitive::Int(u) => Ok(json!(u)), + Primitive::Double(u) => Ok(json!(u)), + Primitive::Bool(b) => Ok(json!(b)), + Primitive::Null => Ok(Value::Null), + Primitive::String(s) => Ok(Value::String(s.to_owned())), + Primitive::Array(prim_arr) => { + let mut json_arr = Vec::with_capacity(prim_arr.len()); + for p in prim_arr { + json_arr.push(primitive_to_value(p)?); + } + Ok(Value::Array(json_arr)) + } + Primitive::Struct(s) => { + let mut o = serde_json::Map::with_capacity(s.len()); + for (k, v) in s { + o.insert(k.to_string(), primitive_to_value(v)?); + } + Ok(Value::Object(o)) + } + v => Ok(json!(v.to_string())), + } +} + +impl Json for Primitive { + fn from_json(s: &str) -> anyhow::Result { + let value = serde_json::from_str(s)?; + json_to_primitive(value) + } + + fn to_json(&self) -> anyhow::Result { + let value = primitive_to_value(self)?; + serde_json::to_string_pretty(&value).map_err(|e| anyhow!("{e}")) + } +} diff --git a/adana-script-core/src/primitive/mod.rs b/adana-script-core/src/primitive/mod.rs new file mode 100644 index 0000000..4a7bd39 --- /dev/null +++ b/adana-script-core/src/primitive/mod.rs @@ -0,0 +1,5 @@ +mod core_primitive; +mod json; +pub use core_primitive::*; +pub use json::*; +// pub use json::*; diff --git a/adana-script/src/compute.rs b/adana-script/src/compute.rs index a5aa6ca..021bdf0 100644 --- a/adana-script/src/compute.rs +++ b/adana-script/src/compute.rs @@ -14,10 +14,10 @@ use super::{ast::to_ast, require_dynamic_lib::require_dynamic_lib}; use adana_script_core::{ primitive::{ Abs, Add, And, Array, BitShift, Cos, DisplayBinary, DisplayHex, Div, - Logarithm, Mul, Neg, Not, Or, Pow, Primitive, RefPrimitive, Rem, Round, - Sin, Sqrt, StringManipulation, Sub, Tan, ToBool, ToNumber, TypeOf, - TYPE_ARRAY, TYPE_BOOL, TYPE_DOUBLE, TYPE_ERROR, TYPE_FUNCTION, TYPE_I8, - TYPE_INT, TYPE_STRUCT, TYPE_U8, + Json, Logarithm, Mul, Neg, Not, Or, Pow, Primitive, RefPrimitive, Rem, + Round, Sin, Sqrt, StringManipulation, Sub, Tan, ToBool, ToNumber, + TypeOf, TYPE_ARRAY, TYPE_BOOL, TYPE_DOUBLE, TYPE_ERROR, TYPE_FUNCTION, + TYPE_I8, TYPE_INT, TYPE_STRUCT, TYPE_U8, }, BuiltInFunctionType, KeyAccess, Operator, TreeNodeValue, Value, }; @@ -1054,6 +1054,12 @@ fn compute_recur( adana_script_core::BuiltInFunctionType::MakeError => { Ok(Primitive::Error(v.to_string())) } + adana_script_core::BuiltInFunctionType::Jsonify => { + Ok(Primitive::String(v.to_json()?)) + } + adana_script_core::BuiltInFunctionType::ParseJson => { + Primitive::from_json(&v.to_string()) + } } } diff --git a/adana-script/src/parser.rs b/adana-script/src/parser.rs index a3cdac8..67f7716 100644 --- a/adana-script/src/parser.rs +++ b/adana-script/src/parser.rs @@ -454,6 +454,10 @@ fn parse_builtin_fn(s: &str) -> Res { parse_builtin(BuiltInFunctionType::Ceil), parse_builtin(BuiltInFunctionType::Floor), )), + alt(( + parse_builtin(BuiltInFunctionType::Jsonify), + parse_builtin(BuiltInFunctionType::ParseJson), + )), alt(( parse_builtin_many_args(BuiltInFunctionType::IsMatch), parse_builtin_many_args(BuiltInFunctionType::Match), @@ -742,6 +746,9 @@ fn parse_value(s: &str) -> Res { parse_variable, parse_constant, parse_null, + parse_drop, + parse_early_return, + parse_break, )), opt(comments), ), @@ -810,6 +817,7 @@ fn parse_simple_instruction(s: &str) -> Res { tag_no_space("="), alt(( all_consuming(parse_multidepth_access), + all_consuming(parse_builtin_fn), all_consuming(parse_fn_call), // TODO maybe with tuple() this giant mess can be simplified e.g // tuple(alt(parser1, parser2,...)) @@ -926,21 +934,21 @@ fn parse_early_return(s: &str) -> Res { pub fn parse_instructions(instructions: &str) -> Res> { let (instructions, _) = opt(comments)(instructions)?; + let instructions = instructions.trim(); if instructions.is_empty() { return Ok((instructions, vec![Value::NoOp])); } + terminated( many1(preceded( opt(comments), alt(( + all_consuming(parse_value), parse_foreach, parse_while_statement, parse_if_statement, - parse_break, - parse_early_return, parse_simple_instruction, - parse_drop, )), )), opt(comments), diff --git a/adana-script/src/tests/builtin.rs b/adana-script/src/tests/builtin.rs index f71a1d5..e8146d7 100644 --- a/adana-script/src/tests/builtin.rs +++ b/adana-script/src/tests/builtin.rs @@ -85,6 +85,105 @@ fn test_ceil() { let r = compute(r#"ceil(4.7)"#, &mut ctx, "N/A").unwrap(); assert_eq!(Primitive::Double(5.), r); } +#[test] +fn test_parse_json() { + let mut ctx = BTreeMap::new(); + let expr = r#"parse_json("""{"a": 9}""")"#; + let r = compute(expr, &mut ctx, "N/A").unwrap(); + assert_eq!( + r, + Primitive::Struct(BTreeMap::from([( + "a".to_string(), + Primitive::Int(9) + )])) + ); + let expr = r#" + struct { + firstName: "Nordine", + lastName: "Bittich", + age: 400, + notes: [1034,1032,3.18,"hello",3334], + skills: struct { + programming: "🔥", + sport: "⚽", + } + } + "#; + let expect = compute(expr, &mut ctx, "N/A").unwrap(); + let expr = r#" + s= parse_json("""{ + "age": 400, + "firstName": "Nordine", + "lastName": "Bittich", + "notes": [ + 1034, + 1032, + 3.18, + "hello", + 3334 + ], + "skills": { + "programming": "🔥", + "sport": "⚽" + } + }""") + "#; + let curr = compute(expr, &mut ctx, "N/A").unwrap(); + assert_eq!(curr, expect); +} +#[test] +fn test_jsonify() { + let mut ctx = BTreeMap::new(); + let expr = r#"jsonify(struct {a: 9})"#; + let r = compute(expr, &mut ctx, "N/A").unwrap(); + assert_eq!( + r, + Primitive::String( + r#"{ + "a": 9 +}"# + .to_string() + ) + ); + + let expr = r#" + s = struct { + firstName: "Nordine", + lastName: "Bittich", + age: 36, + notes: [1,2,3.18,"hello",4], + skills: struct { + programming: "🔥", + sport: "⚽", + } + } + jsonify(s) + "#; + let r = compute(expr, &mut ctx, "N/A").unwrap(); + assert_eq!( + Primitive::String( + r#"{ + "age": 36, + "firstName": "Nordine", + "lastName": "Bittich", + "notes": [ + 1, + 2, + 3.18, + "hello", + 4 + ], + "skills": { + "programming": "🔥", + "sport": "⚽" + } +}"# + .to_string() + ), + r + ); +} + #[test] fn test_round() { let mut ctx = BTreeMap::new();