Skip to content
File

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

rust250 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::Number;
7use jsg::ToJS;
8use jsg_macros::jsg_method;
9use jsg_macros::jsg_oneof;
10use jsg_macros::jsg_resource;
11 
12#[jsg_oneof]
13#[derive(Debug, Clone, PartialEq)]
14enum StringOrNumber {
15 String(String),
16 Number(Number),
17}
18 
19#[jsg_oneof]
20#[derive(Debug, Clone, PartialEq)]
21enum NumberOrString {
22 Number(Number),
23 String(String),
24}
25 
26#[jsg_oneof]
27#[derive(Debug, Clone, PartialEq)]
28enum StringOrBool {
29 String(String),
30 Bool(bool),
31}
32 
33#[jsg_oneof]
34#[derive(Debug, Clone, PartialEq)]
35enum ThreeTypes {
36 String(String),
37 Number(Number),
38 Bool(bool),
39}
40 
41#[jsg_resource]
42struct EnumTestResource;
43 
44#[jsg_resource]
45#[expect(clippy::unnecessary_wraps)]
46impl EnumTestResource {
47 #[jsg_method]
48 pub fn string_or_number(&self, value: StringOrNumber) -> Result<String, jsg::Error> {
49 match value {
50 StringOrNumber::String(s) => Ok(format!("string:{s}")),
51 StringOrNumber::Number(n) => Ok(format!("number:{n}")),
52 }
53 }
54 
55 #[jsg_method]
56 pub fn string_or_number_ref(&self, value: &StringOrNumber) -> Result<String, jsg::Error> {
57 match value {
58 StringOrNumber::String(s) => Ok(format!("ref_string:{s}")),
59 StringOrNumber::Number(n) => Ok(format!("ref_number:{n}")),
60 }
61 }
62 
63 #[jsg_method]
64 pub fn string_or_bool(&self, value: StringOrBool) -> Result<String, jsg::Error> {
65 match value {
66 StringOrBool::String(s) => Ok(format!("string:{s}")),
67 StringOrBool::Bool(b) => Ok(format!("bool:{b}")),
68 }
69 }
70 
71 #[jsg_method]
72 pub fn three_types(&self, value: ThreeTypes) -> Result<String, jsg::Error> {
73 match value {
74 ThreeTypes::String(s) => Ok(format!("string:{s}")),
75 ThreeTypes::Number(n) => Ok(format!("number:{n}")),
76 ThreeTypes::Bool(b) => Ok(format!("bool:{b}")),
77 }
78 }
79 
80 #[jsg_method]
81 pub fn number_or_string(&self, value: NumberOrString) -> Result<String, jsg::Error> {
82 match value {
83 NumberOrString::Number(n) => Ok(format!("number:{n}")),
84 NumberOrString::String(s) => Ok(format!("string:{s}")),
85 }
86 }
87}
88 
89#[test]
90fn jsg_oneof_derives_debug_and_clone() {
91 let s = StringOrNumber::String("test".to_owned());
92 let cloned = s.clone();
93 assert_eq!(s, cloned);
94 
95 let debug_str = format!("{s:?}");
96 assert!(debug_str.contains("String"));
97 assert!(debug_str.contains("test"));
98}
99 
100#[test]
101fn jsg_oneof_string_or_number_accepts_string() {
102 let harness = crate::Harness::new();
103 harness.run_in_context(|lock, ctx| {
104 let resource = jsg::Rc::new(EnumTestResource);
105 let wrapped = resource.to_js(lock);
106 ctx.set_global("resource", wrapped);
107 
108 let result: String = ctx.eval(lock, "resource.stringOrNumber('hello')").unwrap();
109 assert_eq!(result, "string:hello");
110 Ok(())
111 });
112}
113 
114#[test]
115fn jsg_oneof_string_or_number_accepts_number() {
116 let harness = crate::Harness::new();
117 harness.run_in_context(|lock, ctx| {
118 let resource = jsg::Rc::new(EnumTestResource);
119 let wrapped = resource.to_js(lock);
120 ctx.set_global("resource", wrapped);
121 
122 let result: String = ctx.eval(lock, "resource.stringOrNumber(42)").unwrap();
123 assert_eq!(result, "number:42");
124 Ok(())
125 });
126}
127 
128#[test]
129fn jsg_oneof_string_or_number_rejects_boolean() {
130 let harness = crate::Harness::new();
131 harness.run_in_context(|lock, ctx| {
132 let resource = jsg::Rc::new(EnumTestResource);
133 let wrapped = resource.to_js(lock);
134 ctx.set_global("resource", wrapped);
135 
136 let err = ctx
137 .eval::<String>(lock, "resource.stringOrNumber(true)")
138 .unwrap_err()
139 .unwrap_jsg_err(lock);
140 assert_eq!(err.name, ExceptionType::TypeError);
141 assert!(err.message.contains("string"));
142 assert!(err.message.contains("number"));
143 Ok(())
144 });
145}
146 
147#[test]
148fn jsg_oneof_string_or_bool_accepts_both() {
149 let harness = crate::Harness::new();
150 harness.run_in_context(|lock, ctx| {
151 let resource = jsg::Rc::new(EnumTestResource);
152 let wrapped = resource.to_js(lock);
153 ctx.set_global("resource", wrapped);
154 
155 let result: String = ctx.eval(lock, "resource.stringOrBool('test')").unwrap();
156 assert_eq!(result, "string:test");
157 
158 let result: String = ctx.eval(lock, "resource.stringOrBool(true)").unwrap();
159 assert_eq!(result, "bool:true");
160 Ok(())
161 });
162}
163 
164#[test]
165fn jsg_oneof_three_types_accepts_all() {
166 let harness = crate::Harness::new();
167 harness.run_in_context(|lock, ctx| {
168 let resource = jsg::Rc::new(EnumTestResource);
169 let wrapped = resource.to_js(lock);
170 ctx.set_global("resource", wrapped);
171 
172 let result: String = ctx.eval(lock, "resource.threeTypes('hello')").unwrap();
173 assert_eq!(result, "string:hello");
174 
175 let result: String = ctx.eval(lock, "resource.threeTypes(123.5)").unwrap();
176 assert_eq!(result, "number:123.5");
177 
178 let result: String = ctx.eval(lock, "resource.threeTypes(false)").unwrap();
179 assert_eq!(result, "bool:false");
180 Ok(())
181 });
182}
183 
184#[test]
185fn jsg_oneof_three_types_rejects_null_and_undefined() {
186 let harness = crate::Harness::new();
187 harness.run_in_context(|lock, ctx| {
188 let resource = jsg::Rc::new(EnumTestResource);
189 let wrapped = resource.to_js(lock);
190 ctx.set_global("resource", wrapped);
191 
192 let err = ctx
193 .eval::<String>(lock, "resource.threeTypes(null)")
194 .unwrap_err()
195 .unwrap_jsg_err(lock);
196 assert_eq!(err.name, ExceptionType::TypeError);
197 
198 let err = ctx
199 .eval::<String>(lock, "resource.threeTypes(undefined)")
200 .unwrap_err()
201 .unwrap_jsg_err(lock);
202 assert_eq!(err.name, ExceptionType::TypeError);
203 Ok(())
204 });
205}
206 
207#[test]
208fn jsg_oneof_variant_order_matches_declaration() {
209 let harness = crate::Harness::new();
210 harness.run_in_context(|lock, ctx| {
211 let resource = jsg::Rc::new(EnumTestResource);
212 let wrapped = resource.to_js(lock);
213 ctx.set_global("resource", wrapped);
214 
215 // StringOrNumber has String first, Number second
216 let result: String = ctx.eval(lock, "resource.stringOrNumber('42')").unwrap();
217 assert_eq!(result, "string:42");
218 
219 let result: String = ctx.eval(lock, "resource.stringOrNumber(42)").unwrap();
220 assert_eq!(result, "number:42");
221 
222 // NumberOrString has Number first, String second
223 let result: String = ctx.eval(lock, "resource.numberOrString(42)").unwrap();
224 assert_eq!(result, "number:42");
225 
226 let result: String = ctx.eval(lock, "resource.numberOrString('42')").unwrap();
227 assert_eq!(result, "string:42");
228 Ok(())
229 });
230}
231 
232#[test]
233fn jsg_oneof_reference_parameter() {
234 let harness = crate::Harness::new();
235 harness.run_in_context(|lock, ctx| {
236 let resource = jsg::Rc::new(EnumTestResource);
237 let wrapped = resource.to_js(lock);
238 ctx.set_global("resource", wrapped);
239 
240 let result: String = ctx
241 .eval(lock, "resource.stringOrNumberRef('hello')")
242 .unwrap();
243 assert_eq!(result, "ref_string:hello");
244 
245 let result: String = ctx.eval(lock, "resource.stringOrNumberRef(42)").unwrap();
246 assert_eq!(result, "ref_number:42");
247 Ok(())
248 });
249}