diff --git a/.gitignore b/.gitignore index b83d222..2369c4b 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ +/pkg/ /target/ diff --git a/Cargo.lock b/Cargo.lock index 43bf4a6..a34679b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "antlr4rust" version = "0.3.0-beta3" @@ -73,6 +88,15 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "cc" +version = "1.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3a42d84bb6b69d3a8b3eaacf0d88f179e1929695e1ad012b6cf64d9caaa5fd2" +dependencies = [ + "shlex", +] + [[package]] name = "cel" version = "0.11.0" @@ -90,13 +114,24 @@ dependencies = [ ] [[package]] -name = "cel_js_example" -version = "0.1.0" +name = "cel-rust-wasm" +version = "0.2.0" dependencies = [ "cel", - "serde_json", + "chrono", + "console_error_panic_hook", + "serde-wasm-bindgen", + "wasm-bindgen", + "web-sys", + "wee_alloc", ] +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + [[package]] name = "cfg-if" version = "1.0.1" @@ -109,15 +144,54 @@ version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", + "windows-link", ] [[package]] -name = "itoa" -version = "1.0.15" +name = "console_error_panic_hook" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if 1.0.1", + "wasm-bindgen", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] [[package]] name = "js-sys" @@ -163,6 +237,12 @@ version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +[[package]] +name = "memory_units" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3" + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -219,7 +299,7 @@ version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ - "cfg-if", + "cfg-if 1.0.1", "libc", "redox_syscall", "smallvec", @@ -294,12 +374,6 @@ version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - [[package]] name = "scopeguard" version = "1.2.0" @@ -315,6 +389,17 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "serde_derive" version = "1.0.219" @@ -327,16 +412,10 @@ dependencies = [ ] [[package]] -name = "serde_json" -version = "1.0.142" +name = "shlex" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", -] +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "smallvec" @@ -403,7 +482,7 @@ version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ - "cfg-if", + "cfg-if 1.0.1", "once_cell", "rustversion", "wasm-bindgen-macro", @@ -455,6 +534,109 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wee_alloc" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "memory_units", + "winapi", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-targets" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index 71aa4b8..2efd557 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,13 +1,38 @@ [package] -description = "Example of running cel-rust interpreter in browser" +name = "cel-rust-wasm" +version = "0.2.0" edition = "2024" -name = "cel_js_example" -publish = false -version = "0.1.0" - -[dependencies] -cel = "0.11.0" -serde_json = "1.0" [lib] -crate-type = ["cdylib", "rlib"] +crate-type = ["cdylib"] + +[dependencies] +wasm-bindgen = "0.2" +cel = "0.11" +serde-wasm-bindgen = "0.6" +chrono = { version = "0.4", features = ["serde"] } + +# The `console_error_panic_hook` crate provides better debugging of panics by +# logging them with `console.error`. This is great for development, but requires +# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for +# code size when deploying. +console_error_panic_hook = { version = "0.1.6", optional = true } + +# `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size +# compared to the default allocator's ~10K. It is slower than the default +# allocator, however. +wee_alloc = { version = "0.4.5", optional = true } + +[dependencies.web-sys] +version = "0.3" +features = [ + "console", +] + +[features] +default = ["console_error_panic_hook"] + +[profile.release] +# Tell `rustc` to optimize for small code size. +opt-level = "s" +lto = true diff --git a/README b/README new file mode 100644 index 0000000..c83aa4b --- /dev/null +++ b/README @@ -0,0 +1,18 @@ +To build and use this: + +1. Install wasm-pack (if you haven't already): + ```bash + curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh + ``` + +2. Build the WASM module: + ```bash + wasm-pack build --target web --out-dir pkg + ``` + +3. Serve the HTML file using a local HTTP server (required for ES modules): + ```bash + python3 -m http.server 8000 + # or + npx serve . + ``` diff --git a/cel_js_example.wasm b/cel_js_example.wasm deleted file mode 120000 index 260f674..0000000 --- a/cel_js_example.wasm +++ /dev/null @@ -1 +0,0 @@ -target/wasm32-unknown-unknown/release/cel_js_example.wasm \ No newline at end of file diff --git a/index.html b/index.html index 4239346..76d793d 100644 --- a/index.html +++ b/index.html @@ -1,160 +1,320 @@ - - - + + - CEL evaluator - - - - - - -

CEL evaluator

+ + +

CEL-Rust WASM-bindgen Demo

- Using cel-rust compiled to WebAssembly. - Change the CEL expression or JSON context to see it update in real-time. + This demo uses wasm-bindgen to provide a clean JavaScript API + for CEL evaluation.

-
- - -
- -
- - -
+
+ + +
+ +
+ + +
- - + +
- - \ No newline at end of file +
+ +
Click "Evaluate" to see results...
+
+ +
+

Examples (click to try):

+ +
+

Basic String Operations

+ Expression: 'Hello ' + name + '!' + Context: {"name": "World"} +
+ +
+

Math Operations

+ Expression: (price * quantity) * (1 + tax) + Context: {"price": 10.50, "quantity": 3, "tax": 0.08} +
+ +
+

List Operations

+ Expression: scores.filter(s, s > 90).size() + ' high scores' + Context: {"scores": [85, 92, 78, 96, 88]} +
+ +
+

Conditional Logic

+ Expression: age >= 18 ? (age >= 65 ? 'senior' : 'adult') : + 'minor' + Context: {"age": 25} +
+ +
+

Map/Object Access

+ Expression: user.profile.country == 'US' && + user.profile.verified + Context: {"user": {"profile": {"country": "US", "verified": + true}}} +
+
+ + + + + + diff --git a/src/lib.rs b/src/lib.rs index 5508135..1fbece4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,109 +1,231 @@ -use std::collections::HashMap; -use std::mem; -use std::slice; -use std::str; - use cel::{Context, Program, Value}; -use serde_json; +use serde_wasm_bindgen; +use std::collections::HashMap; +use wasm_bindgen::prelude::*; -#[unsafe(no_mangle)] -pub extern "C" fn allocation(n: usize) -> *mut u8 { - mem::ManuallyDrop::new(Vec::with_capacity(n)).as_mut_ptr() +// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global +// allocator. +#[cfg(feature = "wee_alloc")] +#[global_allocator] +static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_namespace = console)] + fn log(s: &str); } -#[unsafe(no_mangle)] -pub unsafe extern "C" fn evaluate(s: *const u8) -> *mut u8 { - unsafe { - let length = u32::from_le_bytes(*(s as *const [u8; 4])) as usize; - let input = slice::from_raw_parts(s.offset(4), length); - let output = evaluate_buffers(input); - mem::ManuallyDrop::new(output).as_mut_ptr() +macro_rules! console_log { + ($($t:tt)*) => (log(&format_args!($($t)*).to_string())) +} + +#[wasm_bindgen] +pub struct CelResult { + success: bool, + result: String, + error: Option, +} + +#[wasm_bindgen] +impl CelResult { + #[wasm_bindgen(getter)] + pub fn success(&self) -> bool { + self.success + } + + #[wasm_bindgen(getter)] + pub fn result(&self) -> String { + self.result.clone() + } + + #[wasm_bindgen(getter)] + pub fn error(&self) -> Option { + self.error.clone() } } -fn evaluate_buffers(input: &[u8]) -> Vec { - let contents = str::from_utf8(input).unwrap(); - let result = evaluate_cel_with_context(contents); - let success = result.is_ok(); - let message = result.unwrap_or_else(|e| e); - let len = message.len(); - let mut buffer = Vec::with_capacity(len + 8); - buffer.push(if success { 1 } else { 0 }); - buffer.extend(vec![0; 3]); - buffer.extend_from_slice(&(len as u32).to_le_bytes()); - buffer.extend_from_slice(message.as_bytes()); - buffer -} +/// Convert a JavaScript value to a CEL Value recursively +fn js_value_to_cel_value(js_val: &JsValue) -> Result { + if js_val.is_null() || js_val.is_undefined() { + Ok(Value::Null) + } else if js_val.is_bigint() { + // Handle BigInt + match js_val.as_f64() { + Some(n) => Ok(Value::Int(n as i64)), + None => Err("Failed to convert BigInt to number".to_string()), + } + } else if let Some(b) = js_val.as_bool() { + Ok(Value::Bool(b)) + } else if let Some(n) = js_val.as_f64() { + // Check if it's an integer + if n.fract() == 0.0 && n >= i64::MIN as f64 && n <= i64::MAX as f64 { + Ok(Value::Int(n as i64)) + } else { + Ok(Value::Float(n)) + } + } else if let Some(s) = js_val.as_string() { + Ok(Value::String(s.into())) + } else if js_val.is_array() { + // Handle arrays + let array = js_sys::Array::from(js_val); + let mut cel_vec = Vec::new(); -fn json_to_cel_value(json_value: &serde_json::Value) -> Value { - match json_value { - serde_json::Value::Null => Value::Null, - serde_json::Value::Bool(b) => Value::Bool(*b), - serde_json::Value::Number(n) => { - if let Some(i) = n.as_i64() { - Value::Int(i) - } else if let Some(f) = n.as_f64() { - Value::Float(f) - } else { - Value::Null - } + for i in 0..array.length() { + let item = array.get(i); + let cel_item = js_value_to_cel_value(&item)?; + cel_vec.push(cel_item); } - serde_json::Value::String(s) => Value::String(s.clone().into()), - serde_json::Value::Array(arr) => { - let cel_vec: Vec = arr.iter().map(json_to_cel_value).collect(); - Value::List(cel_vec.into()) - } - serde_json::Value::Object(obj) => { - let mut cel_map = HashMap::new(); - for (key, value) in obj.iter() { - cel_map.insert(key.clone(), json_to_cel_value(value)); - } - Value::Map(cel_map.into()) - } - } -} -fn evaluate_cel_with_context(content: &str) -> Result { - // Split the input to get expression and context - let parts: Vec<&str> = content.split("\n---CONTEXT---\n").collect(); - let expression = parts[0].trim(); - let context_json = if parts.len() > 1 { - parts[1].trim() + Ok(Value::List(cel_vec.into())) + } else if js_val.is_object() { + // Handle objects + let obj = js_sys::Object::from(js_val.clone()); + let entries = js_sys::Object::entries(&obj); + let mut cel_map = HashMap::new(); + + for i in 0..entries.length() { + let entry = js_sys::Array::from(&entries.get(i)); + let key = entry + .get(0) + .as_string() + .ok_or_else(|| "Object key must be a string".to_string())?; + let value = entry.get(1); + let cel_value = js_value_to_cel_value(&value)?; + cel_map.insert(key, cel_value); + } + + Ok(Value::Map(cel_map.into())) } else { - "{}" - }; + Err(format!("Unsupported JavaScript type: {:?}", js_val)) + } +} - // Parse the CEL expression - let program = match Program::compile(expression) { - Ok(prog) => prog, - Err(e) => return Err(format!("Compilation error: {}", e)), - }; - - // Create context and add JSON values - let mut context = Context::default(); - - // Parse JSON context if provided and not empty - if !context_json.is_empty() && context_json != "{}" { - match serde_json::from_str::(context_json) { - Ok(json_value) => { - if let serde_json::Value::Object(obj) = json_value { - for (key, value) in obj.iter() { - let cel_value = json_to_cel_value(value); - context - .add_variable(key, cel_value) - .map_err(|e| format!("Context error: {}", e))?; - } - } else { - return Err("JSON context must be an object".to_string()); - } +/// Convert a CEL Value back to a JavaScript value +fn cel_value_to_js_value(cel_val: &Value) -> Result { + match cel_val { + Value::Null => Ok(JsValue::NULL), + Value::Bool(b) => Ok(JsValue::from_bool(*b)), + Value::Int(i) => Ok(JsValue::from_f64(*i as f64)), + Value::UInt(u) => Ok(JsValue::from_f64(*u as f64)), + Value::Float(f) => Ok(JsValue::from_f64(*f)), + Value::String(s) => Ok(JsValue::from_str(s)), + Value::Bytes(b) => { + // Convert bytes to Uint8Array + let array = js_sys::Uint8Array::new_with_length(b.len() as u32); + for (i, byte) in b.iter().enumerate() { + array.set_index(i as u32, *byte); } - Err(e) => return Err(format!("JSON parsing error: {}", e)), + Ok(array.into()) + } + Value::List(list) => { + let array = js_sys::Array::new(); + for item in list.iter() { + let js_item = cel_value_to_js_value(item)?; + array.push(&js_item); + } + Ok(array.into()) + } + Value::Map(map) => { + let obj = js_sys::Object::new(); + for (key, value) in map.iter() { + let js_value = cel_value_to_js_value(value)?; + js_sys::Reflect::set(&obj, &JsValue::from_str(key), &js_value) + .map_err(|_| "Failed to set object property".to_string())?; + } + Ok(obj.into()) + } + Value::Duration(d) => { + // Convert duration to string representation + Ok(JsValue::from_str(&format!("{}s", d.as_secs_f64()))) + } + Value::Timestamp(ts) => { + // Convert timestamp to ISO string + let datetime = chrono::DateTime::from_timestamp(ts.as_secs() as i64, ts.subsec_nanos()) + .ok_or_else(|| "Invalid timestamp".to_string())?; + Ok(JsValue::from_str(&datetime.to_rfc3339())) + } + _ => Err(format!( + "Unsupported CEL type for conversion: {:?}", + cel_val + )), + } +} + +/// Evaluate a CEL expression with the given context +#[wasm_bindgen] +pub fn evaluate_cel(expression: &str, context: &JsValue) -> CelResult { + let result = evaluate_cel_internal(expression, context); + + match result { + Ok(value) => CelResult { + success: true, + result: format!("{:?}", value), + error: None, + }, + Err(error) => CelResult { + success: false, + result: String::new(), + error: Some(error), + }, + } +} + +/// Evaluate a CEL expression and return the result as a JavaScript value +#[wasm_bindgen] +pub fn evaluate_cel_js(expression: &str, context: &JsValue) -> Result { + match evaluate_cel_internal(expression, context) { + Ok(cel_value) => match cel_value_to_js_value(&cel_value) { + Ok(js_val) => Ok(js_val), + Err(e) => Err(JsValue::from_str(&format!("Conversion error: {}", e))), + }, + Err(e) => Err(JsValue::from_str(&e)), + } +} + +fn evaluate_cel_internal(expression: &str, context: &JsValue) -> Result { + // Compile the CEL expression + let program = Program::compile(expression).map_err(|e| format!("Compilation error: {}", e))?; + + // Create context + let mut cel_context = Context::default(); + + // Add context variables if provided + if !context.is_undefined() && !context.is_null() { + if context.is_object() { + let obj = js_sys::Object::from(context.clone()); + let entries = js_sys::Object::entries(&obj); + + for i in 0..entries.length() { + let entry = js_sys::Array::from(&entries.get(i)); + let key = entry + .get(0) + .as_string() + .ok_or_else(|| "Context key must be a string".to_string())?; + let value = entry.get(1); + + let cel_value = js_value_to_cel_value(&value) + .map_err(|e| format!("Failed to convert context value '{}': {}", key, e))?; + + cel_context + .add_variable(&key, cel_value) + .map_err(|e| format!("Failed to add context variable '{}': {}", key, e))?; + } + } else { + return Err("Context must be an object".to_string()); } } // Execute the program - match program.execute(&context) { - Ok(value) => Ok(format!("{:?}", value)), - Err(e) => Err(format!("Execution error: {}", e)), - } + program + .execute(&cel_context) + .map_err(|e| format!("Execution error: {}", e)) +} + +/// Initialize the WASM module (optional, for debugging) +#[wasm_bindgen(start)] +pub fn main() { + #[cfg(feature = "console_error_panic_hook")] + console_error_panic_hook::set_once(); + + console_log!("CEL-Rust WASM module initialized!"); }