// file: crates/ksp-config-lib/src/bootstrap.rs // version: 1 /// Default root containing KSP runtime configuration documents. pub const DEFAULT_CFG_PATH: &str = "config"; /// Default root containing KSP JSON schemas. pub const DEFAULT_SCHEMA_PATH: &str = "config/schemas"; /// Bootstrap argument used to replace the configuration document root. pub const ARG_CFG_PATH: &str = "--cfgpath"; /// Bootstrap argument used to replace the schema root. pub const ARG_SCHEMA_PATH: &str = "--schemapath"; /// Non-recursive bootstrap options required before Config can resolve any managed document. #[derive(Clone, Debug, Eq, PartialEq)] pub struct ConfigBootstrapOptions { cfg_path: std::path::PathBuf, schema_path: std::path::PathBuf, } impl ConfigBootstrapOptions { /// Creates bootstrap options using the KSP hardcoded configuration and schema roots. pub fn defaults() -> ksp_core_lib::Result { return Self::from_paths(crate::DEFAULT_CFG_PATH, crate::DEFAULT_SCHEMA_PATH); } /// Creates bootstrap options from explicit programmatic configuration and schema roots. pub fn from_paths( cfg_path: impl std::convert::Into, schema_path: impl std::convert::Into, ) -> ksp_core_lib::Result { let cfg_path = validate_bootstrap_path(crate::ARG_CFG_PATH, cfg_path.into()); let cfg_path = match cfg_path { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let schema_path = validate_bootstrap_path(crate::ARG_SCHEMA_PATH, schema_path.into()); return match schema_path { std::result::Result::Ok(value) => std::result::Result::Ok(Self { cfg_path, schema_path: value }), std::result::Result::Err(error) => std::result::Result::Err(error), }; } /// Parses the KSP-owned bootstrap path arguments from a raw process argument slice. /// /// Both `--cfgpath=value` / `--cfgpath value` and `--schemapath=value` / `--schemapath value` are accepted. Unrelated arguments are ignored so an /// application can pass its complete argument vector. When the same bootstrap path is specified more than once, the last explicit value wins. pub fn from_args(args: &[std::ffi::OsString]) -> ksp_core_lib::Result { let mut options = Self::defaults_unchecked(); let mut index: usize = 0; while index < args.len() { let argument = &args[index]; if argument.as_os_str() == std::ffi::OsStr::new(crate::ARG_CFG_PATH) { let parsed = parse_separate_path_argument(args, index, crate::ARG_CFG_PATH); match parsed { std::result::Result::Ok((path, next_index)) => { options.cfg_path = path; index = next_index; }, std::result::Result::Err(error) => return std::result::Result::Err(error), } } else if argument.as_os_str() == std::ffi::OsStr::new(crate::ARG_SCHEMA_PATH) { let parsed = parse_separate_path_argument(args, index, crate::ARG_SCHEMA_PATH); match parsed { std::result::Result::Ok((path, next_index)) => { options.schema_path = path; index = next_index; }, std::result::Result::Err(error) => return std::result::Result::Err(error), } } else { let inline = parse_inline_path_argument(argument); match inline { std::option::Option::Some((kind, path)) => match kind { BootstrapPathKind::Config => options.cfg_path = path, BootstrapPathKind::Schema => options.schema_path = path, }, std::option::Option::None => {}, } } index += 1; } return Self::from_paths(options.cfg_path, options.schema_path); } /// Returns the root used for managed runtime configuration documents. #[must_use] pub fn cfg_path(&self) -> &std::path::Path { return self.cfg_path.as_path(); } /// Returns the root used for managed JSON schemas. #[must_use] pub fn schema_path(&self) -> &std::path::Path { return self.schema_path.as_path(); } /// Replaces the configuration document root after applying bootstrap path validation. pub fn with_cfg_path(self, path: impl std::convert::Into) -> ksp_core_lib::Result { let validated = validate_bootstrap_path(crate::ARG_CFG_PATH, path.into()); return match validated { std::result::Result::Ok(cfg_path) => std::result::Result::Ok(Self { cfg_path, schema_path: self.schema_path }), std::result::Result::Err(error) => std::result::Result::Err(error), }; } /// Replaces the schema root after applying bootstrap path validation. pub fn with_schema_path(self, path: impl std::convert::Into) -> ksp_core_lib::Result { let validated = validate_bootstrap_path(crate::ARG_SCHEMA_PATH, path.into()); return match validated { std::result::Result::Ok(schema_path) => std::result::Result::Ok(Self { cfg_path: self.cfg_path, schema_path }), std::result::Result::Err(error) => std::result::Result::Err(error), }; } fn defaults_unchecked() -> Self { return Self { cfg_path: std::path::PathBuf::from(crate::DEFAULT_CFG_PATH), schema_path: std::path::PathBuf::from(crate::DEFAULT_SCHEMA_PATH), }; } } #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum BootstrapPathKind { Config, Schema, } fn parse_inline_path_argument(argument: &std::ffi::OsStr) -> std::option::Option<(BootstrapPathKind, std::path::PathBuf)> { let text = match argument.to_str() { std::option::Option::Some(value) => value, std::option::Option::None => return std::option::Option::None, }; let cfg_prefix = "--cfgpath="; let schema_prefix = "--schemapath="; if let std::option::Option::Some(value) = text.strip_prefix(cfg_prefix) { return std::option::Option::Some((BootstrapPathKind::Config, std::path::PathBuf::from(value))); } if let std::option::Option::Some(value) = text.strip_prefix(schema_prefix) { return std::option::Option::Some((BootstrapPathKind::Schema, std::path::PathBuf::from(value))); } return std::option::Option::None; } fn parse_separate_path_argument(args: &[std::ffi::OsString], index: usize, argument_name: &'static str) -> ksp_core_lib::Result<(std::path::PathBuf, usize)> { let value_index = index + 1; if value_index >= args.len() { return std::result::Result::Err(missing_argument_value_error(argument_name)); } let value = &args[value_index]; let option_like = match value.to_str() { std::option::Option::Some(text) => text.starts_with("--"), std::option::Option::None => false, }; if option_like { return std::result::Result::Err(missing_argument_value_error(argument_name)); } return std::result::Result::Ok((std::path::PathBuf::from(value.as_os_str()), value_index)); } fn validate_bootstrap_path(argument_name: &'static str, path: std::path::PathBuf) -> ksp_core_lib::Result { if path.as_os_str().is_empty() { return std::result::Result::Err(invalid_path_error(argument_name, &path, "path is empty")); } let metadata = std::fs::metadata(path.as_path()); return match metadata { std::result::Result::Ok(value) => { if value.is_dir() { std::result::Result::Ok(path) } else { std::result::Result::Err(invalid_path_error(argument_name, &path, "existing path is not a directory")) } }, std::result::Result::Err(error) => { if error.kind() == std::io::ErrorKind::NotFound { std::result::Result::Ok(path) } else { std::result::Result::Err(invalid_path_source_error(argument_name, &path, error)) } }, }; } fn missing_argument_value_error(argument_name: &'static str) -> ksp_core_lib::Error { return ksp_core_lib::Error::new(crate::ERROR_CODE_BOOTSTRAP_ARGUMENT_MISSING_VALUE, "Config bootstrap argument requires a path value") .with_context("argument", argument_name); } fn invalid_path_error(argument_name: &'static str, path: &std::path::Path, reason: &'static str) -> ksp_core_lib::Error { return ksp_core_lib::Error::new(crate::ERROR_CODE_BOOTSTRAP_INVALID_PATH, "Config bootstrap path is invalid") .with_context("argument", argument_name) .with_context("path", path.to_string_lossy().into_owned()) .with_context("reason", reason); } fn invalid_path_source_error(argument_name: &'static str, path: &std::path::Path, source: std::io::Error) -> ksp_core_lib::Error { return ksp_core_lib::Error::new(crate::ERROR_CODE_BOOTSTRAP_INVALID_PATH, "Config bootstrap path cannot be inspected") .with_context("argument", argument_name) .with_context("path", path.to_string_lossy().into_owned()) .with_source(source); } #[cfg(test)] #[path = "../unit_tests/bootstrap.rs"] mod tests;