use cranpose_core::{compositionLocalOfWithPolicy, CompositionLocal, CompositionLocalProvider};
use cranpose_macros::composable;
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Clone, Debug, PartialEq)]
pub enum LaunchArgValue {
Bool(bool),
Int(i32),
Long(i64),
Float(f32),
Text(String),
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct LaunchArgs {
entries: Vec<(Box<str>, LaunchArgValue)>,
debuggable: bool,
}
pub type LaunchArgsRef = Rc<LaunchArgs>;
impl LaunchArgs {
pub fn new(
entries: impl IntoIterator<Item = (String, LaunchArgValue)>,
debuggable: bool,
) -> Self {
let mut collected: Vec<(Box<str>, LaunchArgValue)> = Vec::new();
for (name, value) in entries {
if name.is_empty() || collected.iter().any(|(known, _)| **known == *name) {
continue;
}
collected.push((name.into_boxed_str(), value));
}
Self {
entries: collected,
debuggable,
}
}
pub fn is_debuggable(&self) -> bool {
self.debuggable
}
pub fn contains(&self, name: &str) -> bool {
self.value(name).is_some()
}
pub fn names(&self) -> impl Iterator<Item = &str> {
self.entries.iter().map(|(name, _)| &**name)
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn value(&self, name: &str) -> Option<&LaunchArgValue> {
self.entries
.iter()
.find(|(known, _)| &**known == name)
.map(|(_, value)| value)
}
pub fn boolean(&self, name: &str) -> Option<bool> {
match self.value(name)? {
LaunchArgValue::Bool(value) => Some(*value),
LaunchArgValue::Text(text) => parse_boolean(text),
_ => None,
}
}
pub fn int(&self, name: &str) -> Option<i32> {
match self.value(name)? {
LaunchArgValue::Int(value) => Some(*value),
LaunchArgValue::Long(value) => i32::try_from(*value).ok(),
LaunchArgValue::Text(text) => text.trim().parse().ok(),
_ => None,
}
}
pub fn long(&self, name: &str) -> Option<i64> {
match self.value(name)? {
LaunchArgValue::Long(value) => Some(*value),
LaunchArgValue::Int(value) => Some(i64::from(*value)),
LaunchArgValue::Text(text) => text.trim().parse().ok(),
_ => None,
}
}
pub fn float(&self, name: &str) -> Option<f32> {
match self.value(name)? {
LaunchArgValue::Float(value) => Some(*value),
LaunchArgValue::Int(value) => Some(*value as f32),
LaunchArgValue::Long(value) => Some(*value as f32),
LaunchArgValue::Text(text) => text.trim().parse().ok(),
_ => None,
}
}
pub fn string(&self, name: &str) -> Option<&str> {
match self.value(name)? {
LaunchArgValue::Text(text) => Some(text),
_ => None,
}
}
}
fn parse_boolean(text: &str) -> Option<bool> {
match text.trim().to_ascii_lowercase().as_str() {
"true" | "1" | "yes" | "on" => Some(true),
"false" | "0" | "no" | "off" => Some(false),
_ => None,
}
}
thread_local! {
static PLATFORM_LAUNCH_ARGS: RefCell<Option<LaunchArgsRef>> = const { RefCell::new(None) };
static DEFAULT_LAUNCH_ARGS: RefCell<Option<LaunchArgsRef>> = const { RefCell::new(None) };
}
pub fn set_platform_launch_args(args: LaunchArgsRef) {
PLATFORM_LAUNCH_ARGS.with(|cell| *cell.borrow_mut() = Some(args));
}
pub fn clear_platform_launch_args() {
PLATFORM_LAUNCH_ARGS.with(|cell| *cell.borrow_mut() = None);
}
pub fn launch_args() -> LaunchArgsRef {
if let Some(args) = PLATFORM_LAUNCH_ARGS.with(|cell| cell.borrow().clone()) {
return args;
}
DEFAULT_LAUNCH_ARGS.with(|cell| {
let mut cached = cell.borrow_mut();
cached
.get_or_insert_with(|| Rc::new(default_launch_args()))
.clone()
})
}
pub fn is_debuggable() -> bool {
launch_args().is_debuggable()
}
fn default_launch_args() -> LaunchArgs {
#[cfg(not(target_arch = "wasm32"))]
{
launch_args_from_command_line(std::env::args().skip(1), cfg!(debug_assertions))
}
#[cfg(target_arch = "wasm32")]
{
LaunchArgs::new(std::iter::empty(), cfg!(debug_assertions))
}
}
pub fn launch_args_from_command_line(
tokens: impl IntoIterator<Item = String>,
debuggable: bool,
) -> LaunchArgs {
let mut entries = Vec::new();
for token in tokens {
if token == "--" {
break;
}
let Some(option) = token.strip_prefix("--") else {
continue;
};
match option.split_once('=') {
Some((name, value)) => {
entries.push((name.to_string(), LaunchArgValue::Text(value.to_string())))
}
None => entries.push((option.to_string(), LaunchArgValue::Bool(true))),
}
}
LaunchArgs::new(entries, debuggable)
}
pub fn local_launch_args() -> CompositionLocal<LaunchArgsRef> {
thread_local! {
static LOCAL_LAUNCH_ARGS: RefCell<Option<CompositionLocal<LaunchArgsRef>>> = const { RefCell::new(None) };
}
LOCAL_LAUNCH_ARGS.with(|cell| {
let mut local = cell.borrow_mut();
local
.get_or_insert_with(|| compositionLocalOfWithPolicy(launch_args, Rc::ptr_eq))
.clone()
})
}
#[allow(non_snake_case)]
#[composable]
pub fn ProvideLaunchArgs(args: LaunchArgsRef, content: impl FnOnce()) {
let local = local_launch_args();
CompositionLocalProvider(vec![local.provides(args)], move || {
content();
});
}
#[allow(non_snake_case)]
#[composable]
pub fn isDebuggable() -> bool {
local_launch_args().current().is_debuggable()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::run_test_composition;
use std::cell::RefCell as StdRefCell;
fn args(entries: &[(&str, LaunchArgValue)]) -> LaunchArgs {
LaunchArgs::new(
entries
.iter()
.map(|(name, value)| ((*name).to_string(), value.clone())),
false,
)
}
fn command_line(tokens: &[&str]) -> LaunchArgs {
launch_args_from_command_line(tokens.iter().map(|token| (*token).to_string()), false)
}
#[test]
fn typed_extras_read_back_in_the_type_they_arrived_in() {
let args = args(&[
("ob_autoplay", LaunchArgValue::Bool(true)),
("ob_level", LaunchArgValue::Int(7)),
("ob_seed", LaunchArgValue::Long(9_000_000_000)),
("ob_time_scale", LaunchArgValue::Float(0.5)),
("ob_screen", LaunchArgValue::Text("lobby".to_string())),
]);
assert_eq!(args.boolean("ob_autoplay"), Some(true));
assert_eq!(args.int("ob_level"), Some(7));
assert_eq!(args.long("ob_seed"), Some(9_000_000_000));
assert_eq!(args.float("ob_time_scale"), Some(0.5));
assert_eq!(args.string("ob_screen"), Some("lobby"));
}
#[test]
fn a_missing_argument_reads_as_none_for_every_type() {
let args = args(&[]);
assert_eq!(args.boolean("absent"), None);
assert_eq!(args.int("absent"), None);
assert_eq!(args.long("absent"), None);
assert_eq!(args.float("absent"), None);
assert_eq!(args.string("absent"), None);
assert!(!args.contains("absent"));
assert!(args.is_empty());
}
#[test]
fn text_arguments_parse_into_the_requested_number_type() {
let args = args(&[
("level", LaunchArgValue::Text("7".to_string())),
("seed", LaunchArgValue::Text("9000000000".to_string())),
("scale", LaunchArgValue::Text("0.25".to_string())),
("flag", LaunchArgValue::Text("ON".to_string())),
]);
assert_eq!(args.int("level"), Some(7));
assert_eq!(args.long("seed"), Some(9_000_000_000));
assert_eq!(args.float("scale"), Some(0.25));
assert_eq!(args.boolean("flag"), Some(true));
assert_eq!(args.int("seed"), None, "a long that does not fit an i32");
assert_eq!(args.boolean("level"), None, "numbers are not truthy");
}
#[test]
fn integer_arguments_widen_but_do_not_become_text() {
let args = args(&[("level", LaunchArgValue::Int(7))]);
assert_eq!(args.long("level"), Some(7));
assert_eq!(args.float("level"), Some(7.0));
assert_eq!(args.string("level"), None);
}
#[test]
fn the_command_line_maps_flags_and_assignments_to_arguments() {
let args = command_line(&[
"--ob_debug",
"--ob_level=7",
"positional",
"--ob_screen=lobby",
]);
assert_eq!(args.boolean("ob_debug"), Some(true));
assert_eq!(args.int("ob_level"), Some(7));
assert_eq!(args.string("ob_screen"), Some("lobby"));
assert_eq!(
args.len(),
3,
"positional arguments are not launch arguments"
);
}
#[test]
fn the_command_line_stops_at_a_bare_double_dash() {
let args = command_line(&["--before", "--", "--after"]);
assert!(args.contains("before"));
assert!(!args.contains("after"));
}
#[test]
fn the_first_value_wins_when_a_name_repeats() {
let args = args(&[
("level", LaunchArgValue::Int(1)),
("level", LaunchArgValue::Int(2)),
]);
assert_eq!(args.int("level"), Some(1));
assert_eq!(args.len(), 1);
}
#[test]
fn the_installed_platform_snapshot_takes_precedence() {
clear_platform_launch_args();
set_platform_launch_args(Rc::new(args(&[(
"ob_autoplay",
LaunchArgValue::Bool(true),
)])));
assert_eq!(launch_args().boolean("ob_autoplay"), Some(true));
clear_platform_launch_args();
assert_eq!(launch_args().boolean("ob_autoplay"), None);
}
#[test]
fn debuggable_is_reported_by_the_snapshot() {
clear_platform_launch_args();
set_platform_launch_args(Rc::new(LaunchArgs::new(std::iter::empty(), true)));
assert!(is_debuggable());
set_platform_launch_args(Rc::new(LaunchArgs::new(std::iter::empty(), false)));
assert!(!is_debuggable());
clear_platform_launch_args();
}
#[test]
fn provide_launch_args_reaches_composition() {
let captured = Rc::new(StdRefCell::new(None));
{
let captured = Rc::clone(&captured);
run_test_composition(move || {
let captured = Rc::clone(&captured);
let provided = Rc::new(LaunchArgs::new(
[("ob_level".to_string(), LaunchArgValue::Int(3))],
true,
));
ProvideLaunchArgs(provided, move || {
*captured.borrow_mut() = Some((
local_launch_args().current().int("ob_level"),
isDebuggable(),
));
});
});
}
assert_eq!(*captured.borrow(), Some((Some(3), true)));
}
}