Skip to content
File

Blob: src/rust/jsg-test/tests/non_coercible.rs

rust172 lines
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 
5use jsg::ExceptionType;
6use jsg::NonCoercible;
7use jsg::Number;
8use jsg::ToJS;
9use jsg_macros::jsg_method;
10use jsg_macros::jsg_resource;
11 
12#[jsg_resource]
13struct MyResource;
14 
15#[jsg_resource]
16#[expect(clippy::unnecessary_wraps)]
17impl 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]
35fn 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]
41fn 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]
49fn 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]
55fn 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]
67fn 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]
78fn 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]
94fn 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]
102fn 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]
119fn 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]
127fn 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}