From 27cf1562ad799da7e10c441452ca07fc095ba7cb Mon Sep 17 00:00:00 2001 From: "E. Rivas" <8037031-er433@users.noreply.gitlab.com> Date: Wed, 1 Nov 2023 18:58:27 +0100 Subject: [PATCH] Api: URL Pattern API implementation --- Cargo.lock | 55 ++++ jstz_api/Cargo.toml | 1 + jstz_api/src/lib.rs | 1 + jstz_api/src/urlpattern/mod.rs | 557 +++++++++++++++++++++++++++++++++ jstz_cli/src/repl.rs | 3 +- jstz_core/src/value.rs | 3 +- packages/jstz-types/index.d.ts | 50 +++ 7 files changed, 668 insertions(+), 2 deletions(-) create mode 100644 jstz_api/src/urlpattern/mod.rs diff --git a/Cargo.lock b/Cargo.lock index f3f072056..5e5be0510 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1631,6 +1631,7 @@ dependencies = [ "serde_json", "tezos-smart-rollup", "url", + "urlpattern", ] [[package]] @@ -3400,6 +3401,47 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + [[package]] name = "unicode-bidi" version = "0.3.13" @@ -3456,6 +3498,19 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "urlpattern" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9bd5ff03aea02fa45b13a7980151fe45009af1980ba69f651ec367121a31609" +dependencies = [ + "derive_more", + "regex", + "serde", + "unic-ucd-ident", + "url", +] + [[package]] name = "utf16_iter" version = "1.0.4" diff --git a/jstz_api/Cargo.toml b/jstz_api/Cargo.toml index 30a6c6531..481f23945 100644 --- a/jstz_api/Cargo.toml +++ b/jstz_api/Cargo.toml @@ -19,3 +19,4 @@ serde = "1.0.188" serde_json = "1.0.107" tezos-smart-rollup = "0.2.1" url = "2.4.1" +urlpattern = "0.2.0" diff --git a/jstz_api/src/lib.rs b/jstz_api/src/lib.rs index 102ec97c2..1a759defa 100644 --- a/jstz_api/src/lib.rs +++ b/jstz_api/src/lib.rs @@ -4,6 +4,7 @@ mod kv; pub mod http; mod text_encoder; pub mod url; +pub mod urlpattern; pub use console::ConsoleApi; pub use kv::KvApi; pub use text_encoder::TextEncoderApi; diff --git a/jstz_api/src/urlpattern/mod.rs b/jstz_api/src/urlpattern/mod.rs new file mode 100644 index 000000000..70f4792c3 --- /dev/null +++ b/jstz_api/src/urlpattern/mod.rs @@ -0,0 +1,557 @@ +//! `jstz`'s implementation of JavaScript's `URLPattern` Web API. +//! +//! More information: +//! - [MDN documentation][mdn] +//! - [WHATWG `URLPattern` specification][spec] +//! +//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/URLPattern +//! [spec]: https://urlpattern.spec.whatwg.org/ + +use boa_engine::{ + js_string, + object::{builtins::JsArray, Object}, + property::Attribute, + value::TryFromJs, + Context, JsArgs, JsError, JsNativeError, JsObject, JsResult, JsString, JsValue, + NativeFunction, +}; +use boa_gc::{custom_trace, Finalize, GcRefMut, Trace}; +use jstz_core::{ + accessor, + native::{ + register_global_class, Accessor, ClassBuilder, JsNativeObject, + JsNativeObjectToString, NativeClass, + }, + value::IntoJs, +}; +use urlpattern::quirks::StringOrInit as InnerStringOrQuirksInit; +use urlpattern::quirks::UrlPatternInit as InnerUrlPatternQuirksInit; +use urlpattern::UrlPattern as InnerUrlPattern; +use urlpattern::UrlPatternComponentResult as InnerUrlPatternComponentResult; +use urlpattern::UrlPatternResult as InnerUrlPatternResult; + +pub struct UrlPatternInput(InnerStringOrQuirksInit); +#[derive(Default)] +pub struct UrlPatternInit(InnerUrlPatternQuirksInit); + +pub struct UrlPatternComponentResult(InnerUrlPatternComponentResult); +pub struct UrlPatternResult { + // It should be UrlPatternInit instead of UrlPatternInput + // according to Deno types? + pub(crate) inputs: Vec, + pub(crate) url_pattern_result: InnerUrlPatternResult, +} + +#[derive(Finalize)] +pub struct UrlPattern { + pub(crate) url_pattern: InnerUrlPattern, +} + +unsafe impl Trace for UrlPattern { + custom_trace!(_this, {}); +} + +impl JsNativeObjectToString for UrlPattern { + fn to_string( + this: &JsNativeObject, + context: &mut Context<'_>, + ) -> JsResult { + let s = format!("{:?}", this.deref().url_pattern); + Ok(s.into_js(context)) + } +} + +impl UrlPattern { + // We do not support options (ignoreCase), as it is not supported in Deno + // nor in `urlpattern` crate. There is an open PR for supporting it in + // `urlpattern`, and we could use it when it gets merged. + pub fn new( + _this: &JsNativeObject, + input: UrlPatternInput, + base_url: Option, + _context: &mut Context<'_>, + ) -> JsResult { + let UrlPatternInput(stringorinit) = input; + let urlpatterninit = urlpattern::quirks::process_construct_pattern_input( + stringorinit, + base_url.as_deref(), + ) + .map_err(|_| { + JsError::from_native( + JsNativeError::typ().with_message("Failed to build UrlPatternInit"), + ) + })?; + let url_pattern = InnerUrlPattern::parse(urlpatterninit).map_err(|_| { + JsError::from_native( + JsNativeError::typ().with_message("Failed to parse UrlPatternInit"), + ) + })?; + + Ok(Self { url_pattern }) + } + + pub fn protocol(&self) -> String { + String::from(self.url_pattern.protocol()) + } + + pub fn username(&self) -> String { + String::from(self.url_pattern.username()) + } + + pub fn password(&self) -> String { + String::from(self.url_pattern.password()) + } + + pub fn hostname(&self) -> String { + String::from(self.url_pattern.hostname()) + } + + pub fn port(&self) -> String { + String::from(self.url_pattern.port()) + } + + pub fn pathname(&self) -> String { + String::from(self.url_pattern.pathname()) + } + + pub fn search(&self) -> String { + String::from(self.url_pattern.search()) + } + + pub fn hash(&self) -> String { + String::from(self.url_pattern.hash()) + } + + pub fn test( + &self, + input: UrlPatternInput, + base_url: Option, + ) -> JsResult { + let UrlPatternInput(string_or_init) = input; + let (url_pattern_match_input, _) = + urlpattern::quirks::process_match_input(string_or_init, base_url.as_deref()) + .unwrap() + .unwrap(); + + self.url_pattern.test(url_pattern_match_input).map_err(|_| { + JsNativeError::typ() + .with_message("Failed to run `test` on `UrlPattern`") + .into() + }) + } + + pub fn exec( + &self, + input: UrlPatternInput, + base_url: Option, + ) -> JsResult> { + let UrlPatternInput(string_or_init) = input; + let (url_pattern_match_input, (string_or_init, base_url)) = + urlpattern::quirks::process_match_input(string_or_init, base_url.as_deref()) + .unwrap() + .unwrap(); + let mut inputs: Vec = Vec::new(); + inputs.push(UrlPatternInput(string_or_init)); + if let Some(base_url) = base_url { + inputs.push(UrlPatternInput(InnerStringOrQuirksInit::String(base_url))); + } + self.url_pattern + .exec(url_pattern_match_input) + .map(|op| { + op.map(|url_pattern_result| UrlPatternResult { + inputs, + url_pattern_result, + }) + }) + .map_err(|_| { + JsNativeError::typ() + .with_message("Failed to run `exec` on `UrlPattern`") + .into() + }) + } +} + +pub struct UrlPatternClass; + +impl UrlPattern { + fn try_from_js<'a>(value: &'a JsValue) -> JsResult> { + value + .as_object() + .and_then(|obj| obj.downcast_mut::()) + .ok_or_else(|| { + JsNativeError::typ() + .with_message( + "Failed to convert js value into rust type `UrlPattern`", + ) + .into() + }) + } +} + +impl UrlPatternClass { + fn hash(context: &mut Context<'_>) -> Accessor { + accessor!( + context, + UrlPattern, + "hash", + get:((url, context) => Ok(url.hash().into_js(context))) + ) + } + + fn hostname(context: &mut Context<'_>) -> Accessor { + accessor!( + context, + UrlPattern, + "hostname", + get:((url, context) => Ok(url.hostname().into_js(context))) + ) + } + + fn password(context: &mut Context<'_>) -> Accessor { + accessor!( + context, + UrlPattern, + "password", + get:((url, context) => Ok(url.password().into_js(context))) + ) + } + + fn pathname(context: &mut Context<'_>) -> Accessor { + accessor!( + context, + UrlPattern, + "pathname", + get:((url, context) => Ok(url.pathname().into_js(context))) + ) + } + + fn port(context: &mut Context<'_>) -> Accessor { + accessor!( + context, + UrlPattern, + "port", + get:((url, context) => Ok(url.port().into_js(context))) + ) + } + + fn protocol(context: &mut Context<'_>) -> Accessor { + accessor!( + context, + UrlPattern, + "protocol", + get:((url, context) => Ok(url.protocol().into_js(context))) + ) + } + + fn search(context: &mut Context<'_>) -> Accessor { + accessor!( + context, + UrlPattern, + "search", + get:((url, context) => Ok(url.search().into_js(context))) + ) + } + + fn username(context: &mut Context<'_>) -> Accessor { + accessor!( + context, + UrlPattern, + "username", + get:((url, context) => Ok(url.username().into_js(context))) + ) + } + + fn test( + this: &JsValue, + args: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { + let url_pattern = UrlPattern::try_from_js(this)?; + let input: UrlPatternInput = args.get(0).unwrap().try_js_into(context)?; + let base_url: Option = args.get_or_undefined(1).try_js_into(context).ok(); + Ok(url_pattern.test(input, base_url)?.into_js(context)) + } + + fn exec( + this: &JsValue, + args: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { + let url_pattern = UrlPattern::try_from_js(this)?; + let input: UrlPatternInput = args.get(0).unwrap().try_js_into(context)?; + let base_url: Option = args.get_or_undefined(1).try_js_into(context).ok(); + url_pattern + .exec(input, base_url)? + .map_or(Ok(JsValue::Null), |e| Ok(e.into_js(context))) + } +} + +impl TryFromJs for UrlPatternInit { + fn try_from_js(value: &JsValue, context: &mut Context<'_>) -> JsResult { + if value.is_undefined() { + return Ok(UrlPatternInit::default()); + } + + let obj = value.as_object().ok_or_else(|| { + JsError::from_native(JsNativeError::typ().with_message("Expected `JsObject`")) + })?; + + macro_rules! get_optional_property { + ($obj:ident, $field:literal, $context:ident) => { + if $obj.has_property(js_string!($field), $context)? { + $obj.get(js_string!($field), $context)? + .try_js_into($context)? + } else { + None + } + }; + } + + let url_pattern_init = urlpattern::quirks::UrlPatternInit { + protocol: get_optional_property!(obj, "protocol", context), + username: get_optional_property!(obj, "username", context), + password: get_optional_property!(obj, "password", context), + hostname: get_optional_property!(obj, "hostname", context), + port: get_optional_property!(obj, "port", context), + pathname: get_optional_property!(obj, "pathname", context), + search: get_optional_property!(obj, "search", context), + hash: get_optional_property!(obj, "hash", context), + base_url: get_optional_property!(obj, "base_url", context), + }; + + Ok(Self(url_pattern_init)) + } +} + +impl TryFromJs for UrlPatternInput { + fn try_from_js(value: &JsValue, context: &mut Context<'_>) -> JsResult { + if let Some(string) = value.as_string() { + return Ok(Self(InnerStringOrQuirksInit::String( + string.to_std_string_escaped(), + ))); + }; + + let UrlPatternInit(init) = UrlPatternInit::try_from_js(value, context)?; + Ok(Self(InnerStringOrQuirksInit::Init(init))) + } +} + +impl IntoJs for UrlPatternInput { + fn into_js(self, context: &mut Context<'_>) -> JsValue { + let UrlPatternInput(string_or_init) = self; + match string_or_init { + InnerStringOrQuirksInit::Init(init) => UrlPatternInit(init).into_js(context), + InnerStringOrQuirksInit::String(string) => JsString::from(string).into(), + } + } +} + +impl IntoJs for UrlPatternComponentResult { + fn into_js(self, context: &mut Context<'_>) -> JsValue { + let UrlPatternComponentResult(url_pattern_component_result) = self; + let input = url_pattern_component_result.input; + let groups: Vec<(String, String)> = + url_pattern_component_result.groups.into_iter().collect(); + let obj = JsObject::with_object_proto(context.intrinsics()); + let _ = obj.create_data_property( + JsString::from("input"), + JsValue::String(JsString::from(input)), + context, + ); + let group_obj = JsObject::with_object_proto(context.intrinsics()); + for (key, value) in groups.iter() { + let value = JsValue::String(JsString::from(value.clone())); + let _ = group_obj.create_data_property( + JsString::from(key.clone()), + value, + context, + ); + } + let _ = obj.create_data_property(JsString::from("groups"), group_obj, context); + obj.into() + } +} + +impl IntoJs for UrlPatternInit { + fn into_js(self, context: &mut Context<'_>) -> JsValue { + let obj = JsObject::with_object_proto(context.intrinsics()); + let UrlPatternInit(init) = self; + + macro_rules! create_data_properties_if_some { + ($obj:ident, $init:ident, $field:ident, $context:ident) => { + let property_name = stringify!($field); + if let Some(s) = $init.$field { + let _ = $obj.create_data_property( + JsString::from(property_name), + JsString::from(s), + $context, + ); + } + }; + } + + create_data_properties_if_some!(obj, init, protocol, context); + create_data_properties_if_some!(obj, init, username, context); + create_data_properties_if_some!(obj, init, password, context); + create_data_properties_if_some!(obj, init, hostname, context); + create_data_properties_if_some!(obj, init, port, context); + create_data_properties_if_some!(obj, init, pathname, context); + create_data_properties_if_some!(obj, init, search, context); + create_data_properties_if_some!(obj, init, hash, context); + + obj.into() + } +} + +impl IntoJs for UrlPatternResult { + fn into_js(self, context: &mut Context<'_>) -> JsValue { + let UrlPatternResult { + url_pattern_result, + inputs, + } = self; + let obj = JsObject::with_object_proto(context.intrinsics()); + + macro_rules! create_data_property { + ($obj:ident, $inner:ident, $field:ident, $context:ident) => { + let property_name = stringify!($field); + let $field = UrlPatternComponentResult($inner.$field).into_js($context); + let _ = $obj.create_data_property( + JsString::from(property_name), + $field, + $context, + ); + }; + } + + create_data_property!(obj, url_pattern_result, protocol, context); + create_data_property!(obj, url_pattern_result, username, context); + create_data_property!(obj, url_pattern_result, password, context); + create_data_property!(obj, url_pattern_result, hostname, context); + create_data_property!(obj, url_pattern_result, port, context); + create_data_property!(obj, url_pattern_result, pathname, context); + create_data_property!(obj, url_pattern_result, search, context); + create_data_property!(obj, url_pattern_result, hash, context); + + let inputs: JsValue = { + let array: JsArray = JsArray::new(context); + for input in inputs.into_iter() { + let _ = array.push(input.into_js(context), context); + } + array.into() + }; + let _ = obj.create_data_property(JsString::from("inputs"), inputs, context); + + obj.into() + } +} + +impl NativeClass for UrlPatternClass { + type Instance = UrlPattern; + + const NAME: &'static str = "URLPattern"; + + fn constructor( + this: &JsNativeObject, + args: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { + let input: UrlPatternInput = args.get_or_undefined(0).try_js_into(context)?; + let base_url: Option = args.get_or_undefined(1).try_js_into(context)?; + + UrlPattern::new(this, input, base_url, context) + } + + fn init(class: &mut ClassBuilder<'_, '_>) -> JsResult<()> { + let hash = UrlPatternClass::hash(class.context()); + let hostname = UrlPatternClass::hostname(class.context()); + let password = UrlPatternClass::password(class.context()); + let pathname = UrlPatternClass::pathname(class.context()); + let port = UrlPatternClass::port(class.context()); + let protocol = UrlPatternClass::protocol(class.context()); + let search = UrlPatternClass::search(class.context()); + let username = UrlPatternClass::username(class.context()); + + class + .accessor(js_string!("hash"), hash, Attribute::all()) + .accessor(js_string!("hostname"), hostname, Attribute::all()) + .accessor(js_string!("password"), password, Attribute::all()) + .accessor(js_string!("pathname"), pathname, Attribute::all()) + .accessor(js_string!("port"), port, Attribute::all()) + .accessor(js_string!("protocol"), protocol, Attribute::all()) + .accessor(js_string!("search"), search, Attribute::all()) + .accessor(js_string!("username"), username, Attribute::all()) + .method( + js_string!("test"), + 0, + NativeFunction::from_fn_ptr(UrlPatternClass::test), + ) + .method( + js_string!("exec"), + 0, + NativeFunction::from_fn_ptr(UrlPatternClass::exec), + ); + Ok(()) + } +} + +pub struct UrlPatternApi; + +impl jstz_core::Api for UrlPatternApi { + fn init(self, context: &mut Context<'_>) { + register_global_class::(context) + .expect("The `URLPattern` class shouldn't exist yet") + } +} + +/* + +Some tests from Deno: + +(function () { + const pattern = new URLPattern("https://deno.land/foo/:bar"); + console.log(pattern.protocol == "https"); + console.log(pattern.protocol == "https"); + console.log(pattern.hostname == "deno.land"); + console.log(pattern.pathname == "/foo/:bar"); + + console.log(pattern.test("https://deno.land/foo/x")); + console.log(!pattern.test("https://deno.com/foo/x")); + match = pattern.exec("https://deno.land/foo/x"); + console.log(match); + console.log(match.pathname.input == "/foo/x"); + // false, but also false in Deno/Chrome + console.log(match.pathname.groups == { bar: "x" }); + +})(); + +(function () { + const pattern = new URLPattern("/foo/:bar", "https://deno.land"); + console.log(pattern.protocol == "https"); + console.log(pattern.hostname == "deno.land"); + console.log(pattern.pathname == "/foo/:bar"); + + console.log(pattern.test("https://deno.land/foo/x")); + console.log(!pattern.test("https://deno.com/foo/x")); + const match = pattern.exec("https://deno.land/foo/x"); + console.log(match); + console.log(match.pathname.input == "/foo/x"); + // false, but also false in Deno/Chrome + console.log(match.pathname.groups == { bar: "x" }); +})(); + +(function () { + const pattern = new URLPattern({ + pathname: "/foo/:bar", + }); + console.log(pattern.protocol == "*"); + console.log(pattern.hostname == "*"); + console.log(pattern.pathname == "/foo/:bar"); + + console.log(pattern.test("https://deno.land/foo/x")); + console.log(pattern.test("https://deno.com/foo/x")); + console.log(!pattern.test("https://deno.com/bar/x")); + + console.log(pattern.test({ pathname: "/foo/x" })); +})(); + +*/ diff --git a/jstz_cli/src/repl.rs b/jstz_cli/src/repl.rs index 82a661cc6..c7443ba78 100644 --- a/jstz_cli/src/repl.rs +++ b/jstz_cli/src/repl.rs @@ -1,6 +1,6 @@ use anyhow::Result; use boa_engine::{js_string, JsResult, JsValue, Source}; -use jstz_api::{http::HttpApi, url::UrlApi, ConsoleApi, KvApi, TextEncoderApi}; +use jstz_api::{http::HttpApi, url::UrlApi, urlpattern::UrlPatternApi, ConsoleApi, KvApi, TextEncoderApi}; use jstz_core::host::HostRuntime; use jstz_core::{ host_defined, @@ -45,6 +45,7 @@ pub fn exec(self_address: Option, cfg: &Config) -> Result<()> { ); realm_clone.register_api(TextEncoderApi, rt.context()); realm_clone.register_api(UrlApi, rt.context()); + realm_clone.register_api(UrlPatternApi, rt.context()); realm_clone.register_api(HttpApi, rt.context()); realm_clone.register_api( LedgerApi { diff --git a/jstz_core/src/value.rs b/jstz_core/src/value.rs index 23fcb6e29..1b33fba5a 100644 --- a/jstz_core/src/value.rs +++ b/jstz_core/src/value.rs @@ -68,7 +68,8 @@ impl_into_js_from_into!( u32, u64, u8, - usize + usize, + bool ); impl IntoJs for String { diff --git a/packages/jstz-types/index.d.ts b/packages/jstz-types/index.d.ts index ff0767b9f..00c698d7f 100644 --- a/packages/jstz-types/index.d.ts +++ b/packages/jstz-types/index.d.ts @@ -40,6 +40,56 @@ declare var URL: { canParse(url: string, base?: string): boolean; }; + +declare interface URLPatternInit { + protocol?: string; + username?: string; + password?: string; + hostname?: string; + port?: string; + pathname?: string; + search?: string; + hash?: string; + baseURL?: string; +} + +declare type URLPatternInput = string | URLPatternInit; + +declare interface URLPatternComponentResult { + input: string; + groups: Record; +} + +declare interface URLPatternResult { + inputs: [URLPatternInit] | [URLPatternInit, string]; + protocol: URLPatternComponentResult; + username: URLPatternComponentResult; + password: URLPatternComponentResult; + hostname: URLPatternComponentResult; + port: URLPatternComponentResult; + pathname: URLPatternComponentResult; + search: URLPatternComponentResult; + hash: URLPatternComponentResult; +} + +declare interface URLPattern { + test(input: URLPatternInput, baseURL?: string): boolean; + exec(input: URLPatternInput, baseURL?: string): URLPatternResult | null; + readonly hash: string; + readonly hostname: string; + readonly password: string; + readonly pathname: string; + readonly port: string; + readonly protocol: string; + readonly search: string; + readonly username: string; +} + +declare var URLPattern: { + readonly prototype: URLPattern; + new (input: URLPatternInput, baseURL?: string): URLPattern; +}; + declare type BufferSource = ArrayBufferView | ArrayBuffer; declare type BodyInit = string | BufferSource;