File
Blob: src/rust/jsg-test/tests/non_coercible.rs
| 1 | // Copyright (c) 2026 Cloudflare, Inc. |
| 2 | // Licensed under the Apache 2.0 license found in the LICENSE file or at: |
| 3 | // https://opensource.org/licenses/Apache-2.0 |
| 4 | |
| 5 | use jsg::ExceptionType; |
| 6 | use jsg::NonCoercible; |
| 7 | use jsg::Number; |
| 8 | use jsg::ToJS; |
| 9 | use jsg_macros::jsg_method; |
| 10 | use jsg_macros::jsg_resource; |
| 11 | |
| 12 | #[jsg_resource] |
| 13 | struct MyResource; |
| 14 | |
| 15 | #[jsg_resource] |
| 16 | #[expect(clippy::unnecessary_wraps)] |
| 17 | impl MyResource { |
| 18 | #[jsg_method] |
| 19 | pub fn string(&self, nc: NonCoercible<String>) -> Result<String, jsg::Error> { |
| 20 | Ok(nc.as_ref().clone()) |
| 21 | } |
| 22 | |
| 23 | #[jsg_method] |
| 24 | pub fn boolean(&self, nc: NonCoercible<bool>) -> Result<bool, jsg::Error> { |
| 25 | Ok(*nc.as_ref()) |
| 26 | } |
| 27 | |
| 28 | #[jsg_method] |
| 29 | pub fn number(&self, nc: NonCoercible<Number>) -> Result<Number, jsg::Error> { |
| 30 | Ok(*nc.as_ref()) |
| 31 | } |
| 32 | } |
| 33 | |
| 34 | #[test] |
| 35 | fn non_coercible_string_new_and_as_ref() { |
| 36 | let nc: NonCoercible<String> = NonCoercible::new("hello".to_owned()); |
| 37 | assert_eq!(nc.as_ref(), "hello"); |
| 38 | } |
| 39 | |
| 40 | #[test] |
| 41 | fn non_coercible_bool_new_and_as_ref() { |
| 42 | let nc_true: NonCoercible<bool> = NonCoercible::new(true); |
| 43 | let nc_false: NonCoercible<bool> = NonCoercible::new(false); |
| 44 | assert!(*nc_true.as_ref()); |
| 45 | assert!(!*nc_false.as_ref()); |
| 46 | } |
| 47 | |
| 48 | #[test] |
| 49 | fn non_coercible_number_new_and_as_ref() { |
| 50 | let nc: NonCoercible<Number> = NonCoercible::new(Number::new(2.5)); |
| 51 | assert!((nc.as_ref().value() - 2.5).abs() < f64::EPSILON); |
| 52 | } |
| 53 | |
| 54 | #[test] |
| 55 | fn non_coercible_from_trait() { |
| 56 | let nc_string: NonCoercible<String> = "test".to_owned().into(); |
| 57 | assert_eq!(nc_string.as_ref(), "test"); |
| 58 | |
| 59 | let nc_bool: NonCoercible<bool> = true.into(); |
| 60 | assert!(*nc_bool.as_ref()); |
| 61 | |
| 62 | let nc_num: NonCoercible<Number> = Number::new(42.0).into(); |
| 63 | assert!((nc_num.as_ref().value() - 42.0).abs() < f64::EPSILON); |
| 64 | } |
| 65 | |
| 66 | #[test] |
| 67 | fn non_coercible_as_ref_trait() { |
| 68 | let nc: NonCoercible<String> = NonCoercible::new("as_ref test".to_owned()); |
| 69 | let s: &String = nc.as_ref(); |
| 70 | assert_eq!(s, "as_ref test"); |
| 71 | |
| 72 | let nc_bool: NonCoercible<bool> = NonCoercible::new(true); |
| 73 | let b: &bool = nc_bool.as_ref(); |
| 74 | assert!(*b); |
| 75 | } |
| 76 | |
| 77 | #[test] |
| 78 | fn non_coercible_deref_trait() { |
| 79 | let nc: NonCoercible<String> = NonCoercible::new("deref test".to_owned()); |
| 80 | // Deref allows us to call String methods directly |
| 81 | assert_eq!(nc.len(), 10); |
| 82 | assert!(nc.starts_with("deref")); |
| 83 | |
| 84 | let nc_bool: NonCoercible<bool> = NonCoercible::new(true); |
| 85 | // Deref to bool |
| 86 | assert!(*nc_bool); |
| 87 | |
| 88 | let nc_num: NonCoercible<Number> = NonCoercible::new(Number::new(2.5)); |
| 89 | // Deref to Number, then get value |
| 90 | assert!(nc_num.value() > 2.0); |
| 91 | } |
| 92 | |
| 93 | #[test] |
| 94 | fn non_coercible_clone() { |
| 95 | let nc1: NonCoercible<String> = NonCoercible::new("clone test".to_owned()); |
| 96 | let nc2 = nc1.clone(); |
| 97 | assert_eq!(nc1.as_ref(), nc2.as_ref()); |
| 98 | assert_eq!(*nc1, *nc2); |
| 99 | } |
| 100 | |
| 101 | #[test] |
| 102 | fn non_coercible_partial_eq() { |
| 103 | let nc1: NonCoercible<String> = NonCoercible::new("equal".to_owned()); |
| 104 | let nc2: NonCoercible<String> = NonCoercible::new("equal".to_owned()); |
| 105 | let nc3: NonCoercible<String> = NonCoercible::new("different".to_owned()); |
| 106 | |
| 107 | assert_eq!(nc1, nc2); |
| 108 | assert_ne!(nc1, nc3); |
| 109 | |
| 110 | let nc_bool1: NonCoercible<bool> = NonCoercible::new(true); |
| 111 | let nc_bool2: NonCoercible<bool> = NonCoercible::new(true); |
| 112 | let nc_bool3: NonCoercible<bool> = NonCoercible::new(false); |
| 113 | |
| 114 | assert_eq!(nc_bool1, nc_bool2); |
| 115 | assert_ne!(nc_bool1, nc_bool3); |
| 116 | } |
| 117 | |
| 118 | #[test] |
| 119 | fn non_coercible_debug() { |
| 120 | let nc: NonCoercible<String> = NonCoercible::new("debug test".to_owned()); |
| 121 | let debug_str = format!("{nc:?}"); |
| 122 | assert!(debug_str.contains("NonCoercible")); |
| 123 | assert!(debug_str.contains("debug test")); |
| 124 | } |
| 125 | |
| 126 | #[test] |
| 127 | fn non_coercible_methods_accept_correct_types_and_reject_incorrect_types() { |
| 128 | let harness = crate::Harness::new(); |
| 129 | harness.run_in_context(|lock, ctx| { |
| 130 | let resource = jsg::Rc::new(MyResource); |
| 131 | let wrapped = resource.to_js(lock); |
| 132 | ctx.set_global("resource", wrapped); |
| 133 | |
| 134 | // String method accepts string |
| 135 | let result: String = ctx.eval(lock, "resource.string('hello')").unwrap(); |
| 136 | assert_eq!(result, "hello"); |
| 137 | |
| 138 | // String method rejects number |
| 139 | let err = ctx |
| 140 | .eval::<String>(lock, "resource.string(123)") |
| 141 | .unwrap_err() |
| 142 | .unwrap_jsg_err(lock); |
| 143 | assert_eq!(err.name, ExceptionType::TypeError); |
| 144 | assert!(err.message.contains("string")); |
| 145 | |
| 146 | // Boolean method accepts boolean |
| 147 | let result: bool = ctx.eval(lock, "resource.boolean(true)").unwrap(); |
| 148 | assert!(result); |
| 149 | |
| 150 | // Boolean method rejects string |
| 151 | let err = ctx |
| 152 | .eval::<bool>(lock, "resource.boolean('true')") |
| 153 | .unwrap_err() |
| 154 | .unwrap_jsg_err(lock); |
| 155 | assert_eq!(err.name, ExceptionType::TypeError); |
| 156 | assert!(err.message.contains("bool")); |
| 157 | |
| 158 | // Number method accepts number |
| 159 | let result: Number = ctx.eval(lock, "resource.number(42.5)").unwrap(); |
| 160 | assert!((result.value() - 42.5).abs() < f64::EPSILON); |
| 161 | |
| 162 | // Number method rejects string |
| 163 | let err = ctx |
| 164 | .eval::<Number>(lock, "resource.number('42')") |
| 165 | .unwrap_err() |
| 166 | .unwrap_jsg_err(lock); |
| 167 | assert_eq!(err.name, ExceptionType::TypeError); |
| 168 | assert!(err.message.contains("number")); |
| 169 | Ok(()) |
| 170 | }); |
| 171 | } |