diff --git a/Cargo.lock b/Cargo.lock index aec7aa0..c63af22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -143,6 +143,15 @@ dependencies = [ "cpufeatures 0.3.0", ] +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.20.2" @@ -309,6 +318,17 @@ dependencies = [ "cipher", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -324,6 +344,7 @@ dependencies = [ name = "el" version = "0.1.0" dependencies = [ + "base64", "blake3", "clap", "el-build", @@ -337,8 +358,11 @@ dependencies = [ "el-seal", "el-test", "el-types", + "hex", + "hmac", "reqwest", "serde_json", + "sha2", "thiserror 2.0.18", "tiny_http", "tokio", @@ -742,6 +766,21 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "http" version = "1.4.0" @@ -1616,6 +1655,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" diff --git a/Cargo.toml b/Cargo.toml index 237dd03..ed6320d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,3 +54,7 @@ reqwest = { version = "0.12", features = ["json", "blocking"] } uuid = { version = "1", features = ["v4"] } walkdir = "2" tiny_http = "0.12" +hmac = "0.12" +sha2 = "0.10" +hex = "0.4" +base64 = "0.22" diff --git a/bin/el/Cargo.toml b/bin/el/Cargo.toml index d91792f..31660a5 100644 --- a/bin/el/Cargo.toml +++ b/bin/el/Cargo.toml @@ -30,3 +30,7 @@ uuid = { workspace = true } walkdir = { workspace = true } tiny_http = { workspace = true } blake3 = { workspace = true } +hmac = { workspace = true } +sha2 = { workspace = true } +hex = { workspace = true } +base64 = { workspace = true } diff --git a/bin/el/src/main.rs b/bin/el/src/main.rs index 6afd5dd..5327b1d 100644 --- a/bin/el/src/main.rs +++ b/bin/el/src/main.rs @@ -1758,9 +1758,10 @@ fn dispatch_builtin( Value::Str(s) => s, _ => String::new(), }; + // Return empty string when env var is not set (so `env("X") == ""` works) let val = std::env::var(&key) .map(Value::Str) - .unwrap_or(Value::Nil); + .unwrap_or_else(|_| Value::Str(String::new())); stack.push(val); BuiltinResult::Handled } @@ -3167,7 +3168,7 @@ fn dispatch_builtin( BuiltinResult::Handled } "json_get_string" => { - // json_get_string(map_or_json_str, key) -> String + // json_get_string(map_or_struct_or_json_str, key) -> String let key = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new(), @@ -3181,6 +3182,13 @@ fn dispatch_builtin( other => other.to_string(), }) .unwrap_or_default(), + Value::Struct { fields, .. } => fields.iter() + .find(|(k, _)| k == &key) + .map(|(_, v)| match v { + Value::Str(s) => s.clone(), + other => other.to_string(), + }) + .unwrap_or_default(), Value::Str(json_str) => { serde_json::from_str::(json_str) .ok() @@ -3193,7 +3201,7 @@ fn dispatch_builtin( BuiltinResult::Handled } "json_get_int" => { - // json_get_int(map_or_json_str, key) -> Int + // json_get_int(map_or_struct_or_json_str, key) -> Int let key = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new(), @@ -3208,6 +3216,14 @@ fn dispatch_builtin( _ => 0, }) .unwrap_or(0), + Value::Struct { fields, .. } => fields.iter() + .find(|(k, _)| k == &key) + .map(|(_, v)| match v { + Value::Int(n) => *n, + Value::Str(s) => s.parse().unwrap_or(0), + _ => 0, + }) + .unwrap_or(0), Value::Str(json_str) => { serde_json::from_str::(json_str) .ok() @@ -3220,7 +3236,7 @@ fn dispatch_builtin( BuiltinResult::Handled } "json_get_array" => { - // json_get_array(map_or_json_str, key) -> [String] + // json_get_array(map_or_struct_or_json_str, key) -> [String] let key = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new(), @@ -3234,6 +3250,13 @@ fn dispatch_builtin( _ => vec![], }) .unwrap_or_default(), + Value::Struct { fields, .. } => fields.iter() + .find(|(k, _)| k == &key) + .map(|(_, v)| match v { + Value::List(items) => items.clone(), + _ => vec![], + }) + .unwrap_or_default(), Value::Str(json_str) => { serde_json::from_str::(json_str) .ok() diff --git a/crates/el-types/src/types.rs b/crates/el-types/src/types.rs index 9aeea85..fcdf921 100644 --- a/crates/el-types/src/types.rs +++ b/crates/el-types/src/types.rs @@ -117,6 +117,139 @@ impl TypeEnv { env.functions.insert("log".into(), void_fn_any.clone()); env.functions.insert("print_err".into(), void_fn_any); + // ── String builtins ─────────────────────────────────────────────────── + let str_fn = |params: Vec, ret: Type| Type::Fn { params, return_type: Box::new(ret) }; + let s = Type::String; + let i = Type::Int; + let b = Type::Bool; + let u = Type::Unknown; + + // String operations + for name in &["str_contains","str_starts_with","string_starts_with","string_contains","str_ends_with","str_eq","string_ends_with","string_index_of","str_index_of","str_last_index_of"] { + env.functions.insert(name.to_string(), str_fn(vec![s.clone(), s.clone()], b.clone())); + } + for name in &["str_to_lowercase","str_trim","string_trim","string_to_upper","str_upper","string_to_lower","str_lower","to_string","int_to_str","bool_to_str"] { + env.functions.insert(name.to_string(), str_fn(vec![u.clone()], s.clone())); + } + for name in &["str_len","string_len","list_len","array_length","array_len","map_len","json_array_len"] { + env.functions.insert(name.to_string(), str_fn(vec![u.clone()], i.clone())); + } + for name in &["str_replace","string_replace","string_concat","string_substring","str_slice"] { + env.functions.insert(name.to_string(), str_fn(vec![s.clone(), s.clone(), s.clone()], s.clone())); + } + env.functions.insert("str_split".into(), str_fn(vec![s.clone(), s.clone()], Type::Unknown)); + env.functions.insert("string_split".into(), str_fn(vec![s.clone(), s.clone()], Type::Unknown)); + env.functions.insert("string_split_last".into(), str_fn(vec![s.clone(), s.clone()], Type::Unknown)); + env.functions.insert("array_join".into(), str_fn(vec![u.clone(), s.clone()], s.clone())); + env.functions.insert("list_join".into(), str_fn(vec![u.clone(), s.clone()], s.clone())); + + // Parse / convert + for name in &["str_to_int","parse_int","int_parse"] { + env.functions.insert(name.to_string(), str_fn(vec![s.clone()], i.clone())); + } + for name in &["str_to_float","parse_float"] { + env.functions.insert(name.to_string(), str_fn(vec![s.clone()], Type::Float)); + } + + // JSON + env.functions.insert("json_get".into(), str_fn(vec![s.clone(), s.clone()], u.clone())); + env.functions.insert("json_set".into(), str_fn(vec![s.clone(), s.clone(), s.clone()], s.clone())); + env.functions.insert("json_keys".into(), str_fn(vec![s.clone()], Type::Unknown)); + env.functions.insert("json_stringify".into(), str_fn(vec![u.clone()], s.clone())); + env.functions.insert("json_parse".into(), str_fn(vec![s.clone()], u.clone())); + env.functions.insert("json_encode".into(), str_fn(vec![u.clone()], s.clone())); + env.functions.insert("json_decode".into(), str_fn(vec![s.clone()], u.clone())); + env.functions.insert("json_get_string".into(), str_fn(vec![u.clone(), s.clone()], s.clone())); + env.functions.insert("json_get_int".into(), str_fn(vec![u.clone(), s.clone()], i.clone())); + env.functions.insert("json_get_array".into(), str_fn(vec![u.clone(), s.clone()], Type::Unknown)); + env.functions.insert("json_array_get".into(), str_fn(vec![s.clone(), i.clone()], u.clone())); + env.functions.insert("json_array_push".into(), str_fn(vec![s.clone(), s.clone()], s.clone())); + env.functions.insert("json_array_len".into(), str_fn(vec![s.clone()], i.clone())); + + // Array + for name in &["array_push","array_pop","array_reverse","array_sort","array_first","array_last"] { + env.functions.insert(name.to_string(), str_fn(vec![u.clone()], u.clone())); + } + env.functions.insert("array_get".into(), str_fn(vec![u.clone(), i.clone()], u.clone())); + env.functions.insert("list_get".into(), str_fn(vec![u.clone(), i.clone()], u.clone())); + env.functions.insert("array_concat".into(), str_fn(vec![u.clone(), u.clone()], u.clone())); + env.functions.insert("array_contains".into(), str_fn(vec![u.clone(), u.clone()], b.clone())); + env.functions.insert("array_slice".into(), str_fn(vec![u.clone(), i.clone(), i.clone()], u.clone())); + env.functions.insert("array_zip".into(), str_fn(vec![u.clone(), u.clone()], u.clone())); + env.functions.insert("array_enumerate".into(), str_fn(vec![u.clone()], u.clone())); + + // Map + env.functions.insert("map_new".into(), str_fn(vec![], u.clone())); + for name in &["map_get","map_remove","map_contains","map_keys","map_values"] { + env.functions.insert(name.to_string(), str_fn(vec![u.clone(), s.clone()], u.clone())); + } + env.functions.insert("map_set".into(), str_fn(vec![u.clone(), s.clone(), u.clone()], u.clone())); + env.functions.insert("map_len".into(), str_fn(vec![u.clone()], i.clone())); + + // Filesystem + for name in &["fs_read"] { + env.functions.insert(name.to_string(), str_fn(vec![s.clone()], s.clone())); + } + for name in &["fs_write","fs_append","fs_exists","fs_mkdir","fs_remove","fs_is_dir"] { + env.functions.insert(name.to_string(), str_fn(vec![u.clone()], b.clone())); + } + env.functions.insert("fs_list".into(), str_fn(vec![s.clone()], Type::Unknown)); + env.functions.insert("fs_list_recursive".into(), str_fn(vec![s.clone()], Type::Unknown)); + env.functions.insert("path_join".into(), str_fn(vec![s.clone(), s.clone()], s.clone())); + env.functions.insert("path_parent".into(), str_fn(vec![s.clone()], s.clone())); + env.functions.insert("cwd".into(), str_fn(vec![], s.clone())); + + // Crypto / UUID + env.functions.insert("blake3_hash".into(), str_fn(vec![s.clone()], s.clone())); + env.functions.insert("uuid_new".into(), str_fn(vec![], s.clone())); + env.functions.insert("uuid_v4".into(), str_fn(vec![], s.clone())); + env.functions.insert("hmac_sha256".into(), str_fn(vec![s.clone(), s.clone()], s.clone())); + env.functions.insert("base64_url_encode".into(), str_fn(vec![s.clone()], s.clone())); + env.functions.insert("base64_url_decode".into(), str_fn(vec![s.clone()], s.clone())); + env.functions.insert("unix_timestamp".into(), str_fn(vec![], i.clone())); + env.functions.insert("now_millis".into(), str_fn(vec![], i.clone())); + + // HTTP + env.functions.insert("http_get".into(), str_fn(vec![s.clone()], s.clone())); + env.functions.insert("http_post".into(), str_fn(vec![s.clone(), s.clone()], s.clone())); + env.functions.insert("http_put".into(), str_fn(vec![s.clone(), s.clone()], s.clone())); + env.functions.insert("http_delete".into(), str_fn(vec![s.clone()], s.clone())); + env.functions.insert("http_patch".into(), str_fn(vec![s.clone(), s.clone()], s.clone())); + env.functions.insert("http_get_auth".into(), str_fn(vec![s.clone(), s.clone()], s.clone())); + env.functions.insert("http_put_auth".into(), str_fn(vec![s.clone(), s.clone(), s.clone()], s.clone())); + env.functions.insert("http_delete_auth".into(), str_fn(vec![s.clone(), s.clone()], s.clone())); + env.functions.insert("http_serve".into(), str_fn(vec![u.clone()], Type::Void)); + + // State + env.functions.insert("state_get".into(), str_fn(vec![s.clone()], s.clone())); + env.functions.insert("state_set".into(), str_fn(vec![s.clone(), s.clone()], b.clone())); + env.functions.insert("state_del".into(), str_fn(vec![s.clone()], b.clone())); + env.functions.insert("state_keys".into(), str_fn(vec![], Type::Unknown)); + + // System + env.functions.insert("env".into(), str_fn(vec![s.clone()], s.clone())); + env.functions.insert("args".into(), str_fn(vec![], Type::Unknown)); + env.functions.insert("exit".into(), str_fn(vec![i.clone()], Type::Void)); + env.functions.insert("sleep_ms".into(), str_fn(vec![i.clone()], Type::Void)); + env.functions.insert("timestamp".into(), str_fn(vec![], s.clone())); + + // Math + for name in &["math_abs","math_floor","math_ceil","math_round","math_sqrt"] { + env.functions.insert(name.to_string(), str_fn(vec![u.clone()], u.clone())); + } + env.functions.insert("math_max".into(), str_fn(vec![u.clone(), u.clone()], u.clone())); + env.functions.insert("math_min".into(), str_fn(vec![u.clone(), u.clone()], u.clone())); + env.functions.insert("math_pow".into(), str_fn(vec![u.clone(), u.clone()], Type::Float)); + + // Result / Optional + for name in &["result_ok","result_err","result_unwrap","result_unwrap_or","optional_some","optional_unwrap","optional_unwrap_or"] { + env.functions.insert(name.to_string(), str_fn(vec![u.clone()], u.clone())); + } + for name in &["result_is_ok","result_is_err","optional_is_some","optional_is_none"] { + env.functions.insert(name.to_string(), str_fn(vec![u.clone()], b.clone())); + } + env.functions.insert("optional_none".into(), str_fn(vec![], Type::Void)); + env }