1use crate::{KeyValuePair, WasiNnGraph};
9#[cfg(feature = "clap")]
10use clap::builder::{StringValueParser, TypedValueParser, ValueParserFactory};
11#[cfg(feature = "clap")]
12use clap::error::{Error, ErrorKind};
13#[cfg(feature = "serde")]
14use serde::de::{self, Visitor};
15use std::fmt;
16use std::num::NonZeroU32;
17use std::path::PathBuf;
18use std::str::FromStr;
19use std::time::Duration;
20use wasmtime::error::Context;
21use wasmtime::{Result, bail, format_err};
22
23const IGNORED_NUMBER_CHARS: [char; 1] = ['_'];
25
26#[macro_export]
27macro_rules! wasmtime_option_group {
28 (
29 #[env = $env:tt]
30 $(#[$attr:meta])*
31 pub struct $opts:ident {
32 $(
33 $(#[doc = $doc:tt])*
34 $(#[doc($doc_attr:meta)])?
35 $(#[serde($serde_attr:meta)])*
36 pub $opt:ident: $container:ident<$payload:ty>,
37 )+
38
39 $(
40 #[prefixed = $prefix:tt]
41 $(#[serde($serde_attr2:meta)])*
42 $(#[doc = $prefixed_doc:tt])*
43 $(#[doc($prefixed_doc_attr:meta)])?
44 pub $prefixed:ident: Vec<(String, Option<String>)>,
45 )?
46 }
47 enum $option:ident {
48 ...
49 }
50 ) => {
51 #[derive(Default, Debug, PartialEq, Clone)]
52 #[cfg_attr(feature = "serde", derive(serde_derive::Deserialize, serde_derive::Serialize))]
53 #[cfg_attr(feature = "serde", serde(rename_all = "kebab-case", deny_unknown_fields))]
54 $(#[$attr])*
55 pub struct $opts {
56 $(
57 $(#[cfg_attr(feature = "serde", serde($serde_attr))])*
58 $(#[doc($doc_attr)])?
59 pub $opt: $container<$payload>,
60 )+
61 $(
62 $(#[cfg_attr(feature = "serde", serde($serde_attr2))])*
63 pub $prefixed: Vec<(String, Option<String>)>,
64 )?
65 }
66
67 #[derive(Clone, PartialEq)]
68 #[expect(non_camel_case_types, reason = "macro-generated code")]
69 enum $option {
70 $(
71 $opt($payload),
72 )+
73 $(
74 $prefixed(String, Option<String>),
75 )?
76 }
77
78 impl $crate::opt::WasmtimeOption for $option {
79 const ENV_PREFIX: &'static str = concat!("WASMTIME_", $env);
80 const OPTIONS: &'static [$crate::opt::OptionDesc<$option>] = &[
81 $(
82 $crate::opt::OptionDesc {
83 name: $crate::opt::OptName::Name(stringify!($opt)),
84 parse: |_, s| {
85 Ok($option::$opt(
86 $crate::opt::WasmtimeOptionValue::parse(s)?
87 ))
88 },
89 val_help: <$payload as $crate::opt::WasmtimeOptionValue>::VAL_HELP,
90 docs: concat!($($doc, "\n",)*),
91 },
92 )+
93 $(
94 $crate::opt::OptionDesc {
95 name: $crate::opt::OptName::Prefix($prefix),
96 parse: |name, val| {
97 Ok($option::$prefixed(
98 name.to_string(),
99 val.map(|v| v.to_string()),
100 ))
101 },
102 val_help: "[=val]",
103 docs: concat!($($prefixed_doc, "\n",)*),
104 },
105 )?
106 ];
107 }
108
109 impl core::fmt::Display for $option {
110 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
111 match self {
112 $(
113 $option::$opt(val) => {
114 write!(f, "{}=", stringify!($opt).replace('_', "-"))?;
115 $crate::opt::WasmtimeOptionValue::display(val, f)
116 }
117 )+
118 $(
119 $option::$prefixed(key, val) => {
120 write!(f, "{}-{key}", stringify!($prefixed))?;
121 if let Some(val) = val {
122 write!(f, "={val}")?;
123 }
124 Ok(())
125 }
126 )?
127 }
128 }
129 }
130
131 impl $opts {
132 fn configure_with(&mut self, opts: &[$crate::opt::CommaSeparated<$option>]) -> Result<()> {
133 let env_opts = <$option as $crate::opt::WasmtimeOption>::parse_env()?;
134 for opt in env_opts.iter().chain(opts.iter().flat_map(|o| o.0.iter())) {
135 self.configure(opt);
136 }
137 Ok(())
138 }
139
140 fn configure(&mut self, opt: &$option) {
141 match opt {
142 $(
143 $option::$opt(val) => {
144 $crate::opt::OptionContainer::push(&mut self.$opt, val.clone());
145 }
146 )+
147 $(
148 $option::$prefixed(key, val) => self.$prefixed.push((key.clone(), val.clone())),
149 )?
150 }
151 }
152
153 fn to_options(&self) -> Vec<$option> {
154 let mut ret = Vec::new();
155 $(
156 for item in $crate::opt::OptionContainer::get(&self.$opt) {
157 ret.push($option::$opt(item.clone()));
158 }
159 )+
160 $(
161 for (key,val) in self.$prefixed.iter() {
162 ret.push($option::$prefixed(key.clone(), val.clone()));
163 }
164 )?
165 ret
166 }
167 }
168 };
169}
170
171#[derive(Clone, Debug, PartialEq)]
173pub struct CommaSeparated<T>(pub Vec<T>);
174
175#[cfg(feature = "clap")]
176impl<T> ValueParserFactory for CommaSeparated<T>
177where
178 T: WasmtimeOption,
179{
180 type Parser = CommaSeparatedParser<T>;
181
182 fn value_parser() -> CommaSeparatedParser<T> {
183 CommaSeparatedParser(std::marker::PhantomData)
184 }
185}
186
187#[derive(Clone)]
188#[cfg(feature = "clap")]
189pub struct CommaSeparatedParser<T>(std::marker::PhantomData<T>);
190
191#[cfg(feature = "clap")]
192impl<T> TypedValueParser for CommaSeparatedParser<T>
193where
194 T: WasmtimeOption,
195{
196 type Value = CommaSeparated<T>;
197
198 fn parse_ref(
199 &self,
200 cmd: &clap::Command,
201 arg: Option<&clap::Arg>,
202 value: &std::ffi::OsStr,
203 ) -> Result<Self::Value, Error> {
204 let val = StringValueParser::new().parse_ref(cmd, arg, value)?;
205
206 let options = T::OPTIONS;
207 let arg = arg.expect("should always have an argument");
208 let arg_long = arg.get_long().expect("should have a long name specified");
209 let arg_short = arg.get_short().expect("should have a short name specified");
210
211 if val == "help" {
214 let mut max = 0;
215 for d in options {
216 max = max.max(d.name.display_string().len() + d.val_help.len());
217 }
218 println!("Available {arg_long} options:\n");
219 for d in options {
220 print!(
221 " -{arg_short} {:>1$}",
222 d.name.display_string(),
223 max - d.val_help.len()
224 );
225 print!("{}", d.val_help);
226 print!(" --");
227 if val == "help" {
228 for line in d.docs.lines().map(|s| s.trim()) {
229 if line.is_empty() {
230 break;
231 }
232 print!(" {line}");
233 }
234 println!();
235 } else {
236 println!();
237 for line in d.docs.lines().map(|s| s.trim()) {
238 let line = line.trim();
239 println!(" {line}");
240 }
241 }
242 }
243 println!("\npass `-{arg_short} help-long` to see longer-form explanations");
244 std::process::exit(0);
245 }
246 if val == "help-long" {
247 println!("Available {arg_long} options:\n");
248 for d in options {
249 println!(
250 " -{arg_short} {}{} --",
251 d.name.display_string(),
252 d.val_help
253 );
254 println!();
255 for line in d.docs.lines().map(|s| s.trim()) {
256 let line = line.trim();
257 println!(" {line}");
258 }
259 }
260 std::process::exit(0);
261 }
262
263 T::parse_csv(&val).map(CommaSeparated).map_err(|e| {
264 Error::raw(
265 ErrorKind::InvalidValue,
266 format!("failed to parse -{arg_short} / --{arg_long} option: {e:?}\n"),
267 )
268 })
269 }
270}
271
272pub trait WasmtimeOption: Sized + Send + Sync + Clone + 'static {
275 const OPTIONS: &'static [OptionDesc<Self>];
276 const ENV_PREFIX: &'static str;
277
278 fn parse_env() -> Result<Vec<Self>> {
284 let mut ret = Vec::new();
285 if let Some(val) = std::env::var_os(Self::ENV_PREFIX) {
286 let val = match val.to_str() {
287 Some(s) => s,
288 None => bail!("env var `{}` is not valid UTF-8", Self::ENV_PREFIX),
289 };
290 ret.extend(
291 Self::parse_csv(&val)
292 .with_context(|| format!("failed to parse env var `{}`", Self::ENV_PREFIX))?,
293 );
294 }
295
296 for option in Self::OPTIONS {
297 let key = match &option.name {
298 OptName::Name(s) => format!("{}_{}", Self::ENV_PREFIX, s.to_ascii_uppercase()),
299 OptName::Prefix(_) => continue,
300 };
301 let val = match std::env::var_os(&key) {
302 Some(val) => val,
303 None => continue,
304 };
305 let val = match val.to_str() {
306 Some(s) => s,
307 None => bail!("env var `{key}` is not valid UTF-8"),
308 };
309 ret.push(
310 (option.parse)(&key, Some(val))
311 .with_context(|| format!("failed to parse env var `{key}`"))?,
312 );
313 }
314 Ok(ret)
315 }
316
317 fn parse_csv(val: &str) -> Result<Vec<Self>> {
319 let mut result = Vec::new();
320 for val in val.split(',') {
321 let mut iter = val.splitn(2, '=');
323 let key = iter.next().unwrap();
324 let key_val = iter.next();
325
326 let option = Self::OPTIONS
328 .iter()
329 .filter_map(|d| match d.name {
330 OptName::Name(s) => {
331 let s = s.replace('_', "-");
332 if s == key { Some((d, s)) } else { None }
333 }
334 OptName::Prefix(s) => {
335 let name = key.strip_prefix(s)?.strip_prefix("-")?;
336 Some((d, name.to_string()))
337 }
338 })
339 .next();
340
341 let (desc, key) = match option {
342 Some(pair) => pair,
343 None => bail!("unknown option: {key}\n"),
344 };
345
346 result.push(
347 (desc.parse)(&key, key_val)
348 .with_context(|| format!("failed to parse option `{val}`"))?,
349 );
350 }
351 Ok(result)
352 }
353}
354
355pub struct OptionDesc<T> {
356 pub name: OptName,
357 pub docs: &'static str,
358 pub parse: fn(&str, Option<&str>) -> Result<T>,
359 pub val_help: &'static str,
360}
361
362pub enum OptName {
363 Name(&'static str),
366
367 Prefix(&'static str),
369}
370
371impl OptName {
372 #[cfg(feature = "clap")]
373 fn display_string(&self) -> String {
374 match self {
375 OptName::Name(s) => s.replace('_', "-"),
376 OptName::Prefix(s) => format!("{s}-<KEY>"),
377 }
378 }
379}
380
381pub trait WasmtimeOptionValue: Sized {
384 const VAL_HELP: &'static str;
386
387 fn parse(val: Option<&str>) -> Result<Self>;
389
390 fn display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result;
392}
393
394impl WasmtimeOptionValue for String {
395 const VAL_HELP: &'static str = "=val";
396 fn parse(val: Option<&str>) -> Result<Self> {
397 match val {
398 Some(val) => Ok(val.to_string()),
399 None => bail!("value must be specified with `key=val` syntax"),
400 }
401 }
402
403 fn display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
404 f.write_str(self)
405 }
406}
407
408impl WasmtimeOptionValue for PathBuf {
409 const VAL_HELP: &'static str = "=path";
410 fn parse(val: Option<&str>) -> Result<Self> {
411 match val {
412 Some(val) => Ok(PathBuf::from_str(val)?),
413 None => bail!("value must be specified with key=val syntax"),
414 }
415 }
416
417 fn display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
418 write!(f, "{self:?}")
419 }
420}
421
422impl WasmtimeOptionValue for u32 {
423 const VAL_HELP: &'static str = "=N";
424 fn parse(val: Option<&str>) -> Result<Self> {
425 let val = String::parse(val)?.replace(IGNORED_NUMBER_CHARS, "");
426 match val.strip_prefix("0x") {
427 Some(hex) => Ok(u32::from_str_radix(hex, 16)?),
428 None => Ok(val.parse()?),
429 }
430 }
431
432 fn display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
433 write!(f, "{self}")
434 }
435}
436
437impl WasmtimeOptionValue for NonZeroU32 {
438 const VAL_HELP: &'static str = "=N";
439
440 fn parse(val: Option<&str>) -> Result<Self> {
441 let n = <u32 as WasmtimeOptionValue>::parse(val)?;
442 NonZeroU32::new(n).ok_or_else(|| format_err!("value must be non-zero"))
443 }
444
445 fn display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
446 write!(f, "{self}")
447 }
448}
449
450impl WasmtimeOptionValue for u64 {
451 const VAL_HELP: &'static str = "=N";
452 fn parse(val: Option<&str>) -> Result<Self> {
453 let val = String::parse(val)?.replace(IGNORED_NUMBER_CHARS, "");
454 match val.strip_prefix("0x") {
455 Some(hex) => Ok(u64::from_str_radix(hex, 16)?),
456 None => Ok(val.parse()?),
457 }
458 }
459
460 fn display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
461 write!(f, "{self}")
462 }
463}
464
465impl WasmtimeOptionValue for usize {
466 const VAL_HELP: &'static str = "=N";
467 fn parse(val: Option<&str>) -> Result<Self> {
468 let val = String::parse(val)?.replace(IGNORED_NUMBER_CHARS, "");
469 match val.strip_prefix("0x") {
470 Some(hex) => Ok(usize::from_str_radix(hex, 16)?),
471 None => Ok(val.parse()?),
472 }
473 }
474
475 fn display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
476 write!(f, "{self}")
477 }
478}
479
480impl WasmtimeOptionValue for bool {
481 const VAL_HELP: &'static str = "[=y|n]";
482 fn parse(val: Option<&str>) -> Result<Self> {
483 match val {
484 None | Some("y") | Some("yes") | Some("true") => Ok(true),
485 Some("n") | Some("no") | Some("false") => Ok(false),
486 Some(s) => bail!("unknown boolean flag `{s}`, only yes,no,<nothing> accepted"),
487 }
488 }
489
490 fn display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
491 if *self {
492 f.write_str("y")
493 } else {
494 f.write_str("n")
495 }
496 }
497}
498
499impl WasmtimeOptionValue for Duration {
500 const VAL_HELP: &'static str = "=N|Ns|Nms|..";
501 fn parse(val: Option<&str>) -> Result<Duration> {
502 let s = String::parse(val)?;
503 if let Ok(val) = s.parse() {
505 return Ok(Duration::from_secs(val));
506 }
507
508 if let Some(num) = s.strip_suffix("s") {
509 if let Ok(val) = num.parse() {
510 return Ok(Duration::from_secs(val));
511 }
512 }
513 if let Some(num) = s.strip_suffix("ms") {
514 if let Ok(val) = num.parse() {
515 return Ok(Duration::from_millis(val));
516 }
517 }
518 if let Some(num) = s.strip_suffix("us").or(s.strip_suffix("μs")) {
519 if let Ok(val) = num.parse() {
520 return Ok(Duration::from_micros(val));
521 }
522 }
523 if let Some(num) = s.strip_suffix("ns") {
524 if let Ok(val) = num.parse() {
525 return Ok(Duration::from_nanos(val));
526 }
527 }
528
529 bail!("failed to parse duration: {s}")
530 }
531
532 fn display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
533 let subsec = self.subsec_nanos();
534 if subsec == 0 {
535 write!(f, "{}s", self.as_secs())
536 } else if subsec % 1_000 == 0 {
537 write!(f, "{}μs", self.as_micros())
538 } else if subsec % 1_000_000 == 0 {
539 write!(f, "{}ms", self.as_millis())
540 } else {
541 write!(f, "{}ns", self.as_nanos())
542 }
543 }
544}
545
546impl WasmtimeOptionValue for wasmtime::OptLevel {
547 const VAL_HELP: &'static str = "=0|1|2|s";
548 fn parse(val: Option<&str>) -> Result<Self> {
549 match String::parse(val)?.as_str() {
550 "0" => Ok(wasmtime::OptLevel::None),
551 "1" => Ok(wasmtime::OptLevel::Speed),
552 "2" => Ok(wasmtime::OptLevel::Speed),
553 "s" => Ok(wasmtime::OptLevel::SpeedAndSize),
554 other => bail!("unknown optimization level `{other}`, only 0,1,2,s accepted"),
555 }
556 }
557
558 fn display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
559 match *self {
560 wasmtime::OptLevel::None => f.write_str("0"),
561 wasmtime::OptLevel::Speed => f.write_str("2"),
562 wasmtime::OptLevel::SpeedAndSize => f.write_str("s"),
563 _ => unreachable!(),
564 }
565 }
566}
567
568impl WasmtimeOptionValue for wasmtime::RegallocAlgorithm {
569 const VAL_HELP: &'static str = "=backtracking|single-pass";
570 fn parse(val: Option<&str>) -> Result<Self> {
571 match String::parse(val)?.as_str() {
572 "backtracking" => Ok(wasmtime::RegallocAlgorithm::Backtracking),
573 "single-pass" => Ok(wasmtime::RegallocAlgorithm::SinglePass),
574 other => {
575 bail!("unknown regalloc algorithm`{other}`, only backtracking,single-pass accepted")
576 }
577 }
578 }
579
580 fn display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
581 match *self {
582 wasmtime::RegallocAlgorithm::Backtracking => f.write_str("backtracking"),
583 wasmtime::RegallocAlgorithm::SinglePass => f.write_str("single-pass"),
584 _ => unreachable!(),
585 }
586 }
587}
588
589impl WasmtimeOptionValue for wasmtime::Strategy {
590 const VAL_HELP: &'static str = "=winch|cranelift";
591 fn parse(val: Option<&str>) -> Result<Self> {
592 match String::parse(val)?.as_str() {
593 "cranelift" => Ok(wasmtime::Strategy::Cranelift),
594 "winch" => Ok(wasmtime::Strategy::Winch),
595 other => bail!("unknown compiler `{other}` only `cranelift` and `winch` accepted",),
596 }
597 }
598
599 fn display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
600 match *self {
601 wasmtime::Strategy::Cranelift => f.write_str("cranelift"),
602 wasmtime::Strategy::Winch => f.write_str("winch"),
603 _ => unreachable!(),
604 }
605 }
606}
607
608impl WasmtimeOptionValue for wasmtime::Collector {
609 const VAL_HELP: &'static str = "=drc|null|copying";
610 fn parse(val: Option<&str>) -> Result<Self> {
611 match String::parse(val)?.as_str() {
612 "drc" => Ok(wasmtime::Collector::DeferredReferenceCounting),
613 "null" => Ok(wasmtime::Collector::Null),
614 "copying" => Ok(wasmtime::Collector::Copying),
615 other => {
616 bail!("unknown collector `{other}` only `drc`, `null`, and `copying` accepted",)
617 }
618 }
619 }
620
621 fn display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
622 match *self {
623 wasmtime::Collector::DeferredReferenceCounting => f.write_str("drc"),
624 wasmtime::Collector::Null => f.write_str("null"),
625 wasmtime::Collector::Copying => f.write_str("copying"),
626 _ => unreachable!(),
627 }
628 }
629}
630
631impl WasmtimeOptionValue for wasmtime::Enabled {
632 const VAL_HELP: &'static str = "[=y|n|auto]";
633 fn parse(val: Option<&str>) -> Result<Self> {
634 match val {
635 None | Some("y") | Some("yes") | Some("true") => Ok(wasmtime::Enabled::Yes),
636 Some("n") | Some("no") | Some("false") => Ok(wasmtime::Enabled::No),
637 Some("auto") => Ok(wasmtime::Enabled::Auto),
638 Some(s) => bail!("unknown flag `{s}`, only yes,no,auto,<nothing> accepted"),
639 }
640 }
641
642 fn display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
643 match *self {
644 wasmtime::Enabled::Yes => f.write_str("y"),
645 wasmtime::Enabled::No => f.write_str("n"),
646 wasmtime::Enabled::Auto => f.write_str("auto"),
647 }
648 }
649}
650
651impl WasmtimeOptionValue for wasmtime::Inlining {
652 const VAL_HELP: &'static str = "[=y|n|gc|inter-module|intrinsics]";
653 fn parse(val: Option<&str>) -> Result<Self> {
654 match val {
655 None => Ok(wasmtime::Inlining::Yes),
656 Some(val) => val.parse(),
657 }
658 }
659
660 fn display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
661 write!(f, "{self}")
662 }
663}
664
665impl WasmtimeOptionValue for WasiNnGraph {
666 const VAL_HELP: &'static str = "=<format>::<dir>";
667 fn parse(val: Option<&str>) -> Result<Self> {
668 let val = String::parse(val)?;
669 let mut parts = val.splitn(2, "::");
670 Ok(WasiNnGraph {
671 format: parts.next().unwrap().to_string(),
672 dir: match parts.next() {
673 Some(part) => part.into(),
674 None => bail!("graph does not contain `::` separator for directory"),
675 },
676 })
677 }
678
679 fn display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
680 write!(f, "{}::{}", self.format, self.dir)
681 }
682}
683
684impl WasmtimeOptionValue for KeyValuePair {
685 const VAL_HELP: &'static str = "=<name>=<val>";
686 fn parse(val: Option<&str>) -> Result<Self> {
687 let val = String::parse(val)?;
688 let mut parts = val.splitn(2, "=");
689 Ok(KeyValuePair {
690 key: parts.next().unwrap().to_string(),
691 value: match parts.next() {
692 Some(part) => part.into(),
693 None => "".to_string(),
694 },
695 })
696 }
697
698 fn display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
699 f.write_str(&self.key)?;
700 if !self.value.is_empty() {
701 f.write_str("=")?;
702 f.write_str(&self.value)?;
703 }
704 Ok(())
705 }
706}
707
708pub trait OptionContainer<T> {
709 fn push(&mut self, val: T);
710 fn get<'a>(&'a self) -> impl Iterator<Item = &'a T>
711 where
712 T: 'a;
713}
714
715impl<T> OptionContainer<T> for Option<T> {
716 fn push(&mut self, val: T) {
717 *self = Some(val);
718 }
719 fn get<'a>(&'a self) -> impl Iterator<Item = &'a T>
720 where
721 T: 'a,
722 {
723 self.iter()
724 }
725}
726
727impl<T> OptionContainer<T> for Vec<T> {
728 fn push(&mut self, val: T) {
729 Vec::push(self, val);
730 }
731 fn get<'a>(&'a self) -> impl Iterator<Item = &'a T>
732 where
733 T: 'a,
734 {
735 self.iter()
736 }
737}
738
739#[cfg(feature = "serde")]
744struct ToStringVisitor {}
745
746#[cfg(feature = "serde")]
747impl<'de> Visitor<'de> for ToStringVisitor {
748 type Value = String;
749
750 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
751 write!(formatter, "&str, u64, or i64")
752 }
753
754 fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
755 where
756 E: de::Error,
757 {
758 Ok(s.to_owned())
759 }
760
761 fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
762 where
763 E: de::Error,
764 {
765 Ok(v.to_string())
766 }
767
768 fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
769 where
770 E: de::Error,
771 {
772 Ok(v.to_string())
773 }
774}
775
776#[cfg(feature = "serde")]
778pub(crate) fn deserialize_cli_parse_wrapper<'de, D, T>(
779 deserializer: D,
780) -> Result<Option<T>, D::Error>
781where
782 T: WasmtimeOptionValue,
783 D: serde::Deserializer<'de>,
784{
785 let to_string_visitor = ToStringVisitor {};
786 let str = deserializer.deserialize_any(to_string_visitor)?;
787
788 T::parse(Some(&str))
789 .map(Some)
790 .map_err(serde::de::Error::custom)
791}
792
793#[cfg(feature = "serde")]
794pub(crate) fn serialize_cli_parse_wrapper<S, T>(val: &Option<T>, ser: S) -> Result<S::Ok, S::Error>
795where
796 T: WasmtimeOptionValue,
797 S: serde::Serializer,
798{
799 match val {
800 Some(val) => ser.serialize_some(&fmt::from_fn(|f| val.display(f)).to_string()),
801 None => ser.serialize_none(),
802 }
803}
804
805#[cfg(test)]
806mod tests {
807 use super::WasmtimeOptionValue;
808
809 #[test]
810 fn numbers_with_underscores() {
811 assert!(<u32 as WasmtimeOptionValue>::parse(Some("123")).is_ok_and(|v| v == 123));
812 assert!(<u32 as WasmtimeOptionValue>::parse(Some("1_2_3")).is_ok_and(|v| v == 123));
813 }
814}