v0.1.3-pre.004
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-logging-lib/src/lib.rs
|
||||
// version: 4
|
||||
// version: 5
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
@@ -7,9 +7,9 @@
|
||||
//! KSP-owned logging and tracing facade.
|
||||
//!
|
||||
//! This crate owns the KSP runtime logging contract. Behavioral KSP crates emit events and spans through this facade rather than depending directly on the
|
||||
//! `tracing` stack. `0.1.2-pre.005` owns the single global subscriber, KSP takeover filtering, hot reload, non-blocking console/file outputs, rolling file
|
||||
//! appenders, ANSI stripping, dropped-line counters and the worker guards required to flush active queues. The integration surface is hardened by
|
||||
//! deterministic saturation, concurrent reload and ownership audits before final release validation.
|
||||
//! `tracing` stack. The crate owns the single global subscriber, KSP takeover filtering, hot reload and non-blocking outputs. `0.1.3-pre.004` extends
|
||||
//! the public settings contract with explicit console properties, multiple file descriptors, per-output level/target/domain filters and selectable formats;
|
||||
//! runtime activation of the newly represented multi-output routing is completed separately so unsupported capabilities are never ignored silently.
|
||||
|
||||
mod error;
|
||||
mod macros;
|
||||
@@ -44,8 +44,12 @@ pub use self::settings::FileRotation;
|
||||
pub use self::settings::FileSettings;
|
||||
/// Runtime filter level used by KSP logging settings.
|
||||
pub use self::settings::LogFilterLevel;
|
||||
/// Output format requested for one Logging sink.
|
||||
pub use self::settings::LogFormat;
|
||||
/// Complete runtime settings consumed by Logging initialization and reload.
|
||||
pub use self::settings::LoggingSettings;
|
||||
/// Per-output routing filter applied in addition to the global KSP takeover policy.
|
||||
pub use self::settings::OutputFilter;
|
||||
/// Lifecycle events emitted for spans by the formatted subscriber.
|
||||
pub use self::settings::SpanEvents;
|
||||
/// Per-target filter override owned by Logging.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-logging-lib/src/runtime.rs
|
||||
// version: 8
|
||||
// version: 9
|
||||
|
||||
use tracing_subscriber::Layer; // rust-rules: trait-import
|
||||
use tracing_subscriber::layer::SubscriberExt; // rust-rules: trait-import
|
||||
@@ -165,17 +165,27 @@ fn prepare_runtime(settings: &crate::LoggingSettings) -> ksp_core_lib::Result<Pr
|
||||
if let std::option::Option::Some(error) = validation_error {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
if settings.console().is_none() && settings.file().is_none() {
|
||||
let runtime_validation = validate_current_runtime_capabilities(settings);
|
||||
if let std::result::Result::Err(error) = runtime_validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let enabled_console = settings.console().filter(|console| -> bool {
|
||||
return console.enabled();
|
||||
});
|
||||
let enabled_file = settings.files().iter().find(|file| -> bool {
|
||||
return file.enabled();
|
||||
});
|
||||
if enabled_console.is_none() && enabled_file.is_none() {
|
||||
return std::result::Result::Ok(PreparedRuntime { layers: RuntimeLayers::new(), outputs: RuntimeOutputs::default() });
|
||||
}
|
||||
let prepared_file = match settings.file() {
|
||||
let prepared_file = match enabled_file {
|
||||
std::option::Option::Some(file) => match build_file_output(file, settings) {
|
||||
std::result::Result::Ok(output) => std::option::Option::Some(output),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
},
|
||||
std::option::Option::None => std::option::Option::None,
|
||||
};
|
||||
let prepared_console = settings.console().map(|console| -> PreparedOutput {
|
||||
let prepared_console = enabled_console.map(|console| -> PreparedOutput {
|
||||
return build_console_output(console, settings);
|
||||
});
|
||||
let mut output_layers = RuntimeLayers::new();
|
||||
@@ -193,6 +203,37 @@ fn prepare_runtime(settings: &crate::LoggingSettings) -> ksp_core_lib::Result<Pr
|
||||
return std::result::Result::Ok(PreparedRuntime { layers, outputs });
|
||||
}
|
||||
|
||||
fn validate_current_runtime_capabilities(settings: &crate::LoggingSettings) -> ksp_core_lib::Result<()> {
|
||||
let enabled_file_count = settings
|
||||
.files()
|
||||
.iter()
|
||||
.filter(|file| -> bool {
|
||||
return file.enabled();
|
||||
})
|
||||
.count();
|
||||
if enabled_file_count > 1 {
|
||||
return runtime_capability_error("multiple enabled file outputs require the multi-sink runtime");
|
||||
}
|
||||
if let std::option::Option::Some(console) = settings.console()
|
||||
&& console.enabled()
|
||||
&& (console.ansi() || console.format() != crate::LogFormat::Human || !console.filter().is_unrestricted())
|
||||
{
|
||||
return runtime_capability_error("console ANSI, selectable formats and per-output routing require the multi-sink runtime");
|
||||
}
|
||||
for file in settings.files() {
|
||||
if file.enabled() && (file.format() != crate::LogFormat::Human || !file.filter().is_unrestricted()) {
|
||||
return runtime_capability_error("file formats and per-output routing require the multi-sink runtime");
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn runtime_capability_error(message: &str) -> ksp_core_lib::Result<()> {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, message).with_context("runtime_contract", "single-output-compatibility"),
|
||||
);
|
||||
}
|
||||
|
||||
fn build_console_output(console: &crate::ConsoleSettings, settings: &crate::LoggingSettings) -> PreparedOutput {
|
||||
return match console.output() {
|
||||
crate::ConsoleOutput::Stdout => build_non_blocking_output(std::io::stdout(), "ksp-logging-console", settings, true),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-logging-lib/src/settings.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
/// Runtime filter level used by KSP logging settings.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
@@ -18,7 +18,7 @@ pub enum LogFilterLevel {
|
||||
Trace,
|
||||
}
|
||||
|
||||
/// Per-target filter override owned by Logging.
|
||||
/// Per-target filter override owned by Logging for the global KSP takeover policy.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct TargetFilter {
|
||||
target_prefix: std::string::String,
|
||||
@@ -56,6 +56,74 @@ pub enum SpanEvents {
|
||||
Full,
|
||||
}
|
||||
|
||||
/// Output format requested for one Logging sink.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum LogFormat {
|
||||
/// Standard human-readable formatter with metadata.
|
||||
Human,
|
||||
/// Compact human-readable formatter.
|
||||
Compact,
|
||||
/// Expanded pretty human-readable formatter.
|
||||
Pretty,
|
||||
/// Structured JSON formatter.
|
||||
Json,
|
||||
}
|
||||
|
||||
/// Per-output routing filter applied in addition to the global KSP takeover policy.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct OutputFilter {
|
||||
level: crate::LogFilterLevel,
|
||||
targets: std::vec::Vec<std::string::String>,
|
||||
domains: std::vec::Vec<std::string::String>,
|
||||
}
|
||||
|
||||
impl OutputFilter {
|
||||
/// Creates an explicit per-output filter.
|
||||
///
|
||||
/// `targets` contains KSP target prefixes or the single wildcard `*`. `domains` contains domain prefixes or the single wildcard `*`.
|
||||
#[must_use]
|
||||
pub fn new(level: crate::LogFilterLevel, targets: std::vec::Vec<std::string::String>, domains: std::vec::Vec<std::string::String>) -> Self {
|
||||
return Self { level, targets, domains };
|
||||
}
|
||||
|
||||
/// Creates an unrestricted routing filter that does not further constrain the global KSP takeover policy.
|
||||
#[must_use]
|
||||
pub fn unrestricted() -> Self {
|
||||
return Self::new(crate::LogFilterLevel::Trace, std::vec!["*".to_string()], std::vec!["*".to_string()]);
|
||||
}
|
||||
|
||||
/// Returns the maximum verbosity accepted by this output.
|
||||
#[must_use]
|
||||
pub const fn level(&self) -> crate::LogFilterLevel {
|
||||
return self.level;
|
||||
}
|
||||
|
||||
/// Returns target selectors in declaration order.
|
||||
#[must_use]
|
||||
pub fn targets(&self) -> &[std::string::String] {
|
||||
return self.targets.as_slice();
|
||||
}
|
||||
|
||||
/// Returns domain selectors in declaration order.
|
||||
#[must_use]
|
||||
pub fn domains(&self) -> &[std::string::String] {
|
||||
return self.domains.as_slice();
|
||||
}
|
||||
|
||||
/// Returns whether this filter leaves routing entirely to the global takeover policy.
|
||||
pub(crate) fn is_unrestricted(&self) -> bool {
|
||||
let targets_all = match self.targets.as_slice() {
|
||||
[selector] => selector == "*",
|
||||
_ => false,
|
||||
};
|
||||
let domains_all = match self.domains.as_slice() {
|
||||
[selector] => selector == "*",
|
||||
_ => false,
|
||||
};
|
||||
return self.level == crate::LogFilterLevel::Trace && targets_all && domains_all;
|
||||
}
|
||||
}
|
||||
|
||||
/// Console stream selected for human-readable logs.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum ConsoleOutput {
|
||||
@@ -66,22 +134,38 @@ pub enum ConsoleOutput {
|
||||
}
|
||||
|
||||
/// Runtime settings for the optional console output.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ConsoleSettings {
|
||||
enabled: bool,
|
||||
output: crate::ConsoleOutput,
|
||||
ansi: bool,
|
||||
format: crate::LogFormat,
|
||||
filter: crate::OutputFilter,
|
||||
}
|
||||
|
||||
impl ConsoleSettings {
|
||||
/// Creates console settings targeting standard output.
|
||||
/// Creates explicit console settings.
|
||||
#[must_use]
|
||||
pub const fn stdout() -> Self {
|
||||
return Self { output: crate::ConsoleOutput::Stdout };
|
||||
pub fn new(enabled: bool, output: crate::ConsoleOutput, ansi: bool, format: crate::LogFormat, filter: crate::OutputFilter) -> Self {
|
||||
return Self { enabled, output, ansi, format, filter };
|
||||
}
|
||||
|
||||
/// Creates console settings targeting standard error.
|
||||
/// Creates enabled standard-output settings compatible with the `0.1.2` runtime behavior.
|
||||
#[must_use]
|
||||
pub const fn stderr() -> Self {
|
||||
return Self { output: crate::ConsoleOutput::Stderr };
|
||||
pub fn stdout() -> Self {
|
||||
return Self::new(true, crate::ConsoleOutput::Stdout, false, crate::LogFormat::Human, crate::OutputFilter::unrestricted());
|
||||
}
|
||||
|
||||
/// Creates enabled standard-error settings compatible with the `0.1.2` runtime behavior.
|
||||
#[must_use]
|
||||
pub fn stderr() -> Self {
|
||||
return Self::new(true, crate::ConsoleOutput::Stderr, false, crate::LogFormat::Human, crate::OutputFilter::unrestricted());
|
||||
}
|
||||
|
||||
/// Returns whether this console output is enabled.
|
||||
#[must_use]
|
||||
pub const fn enabled(&self) -> bool {
|
||||
return self.enabled;
|
||||
}
|
||||
|
||||
/// Returns the selected console stream.
|
||||
@@ -89,9 +173,27 @@ impl ConsoleSettings {
|
||||
pub const fn output(&self) -> crate::ConsoleOutput {
|
||||
return self.output;
|
||||
}
|
||||
|
||||
/// Returns whether ANSI formatting is requested for the console output.
|
||||
#[must_use]
|
||||
pub const fn ansi(&self) -> bool {
|
||||
return self.ansi;
|
||||
}
|
||||
|
||||
/// Returns the requested console format.
|
||||
#[must_use]
|
||||
pub const fn format(&self) -> crate::LogFormat {
|
||||
return self.format;
|
||||
}
|
||||
|
||||
/// Returns the routing filter associated with the console output.
|
||||
#[must_use]
|
||||
pub const fn filter(&self) -> &crate::OutputFilter {
|
||||
return &self.filter;
|
||||
}
|
||||
}
|
||||
|
||||
/// Rotation cadence for the optional file output.
|
||||
/// Rotation cadence for a file output.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum FileRotation {
|
||||
/// Keeps a single non-rotating file.
|
||||
@@ -102,23 +204,62 @@ pub enum FileRotation {
|
||||
Daily,
|
||||
}
|
||||
|
||||
/// Runtime settings for the optional file output.
|
||||
/// Runtime settings for one file output.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct FileSettings {
|
||||
output_id: std::string::String,
|
||||
enabled: bool,
|
||||
directory: std::path::PathBuf,
|
||||
file_name_prefix: std::string::String,
|
||||
rotation: crate::FileRotation,
|
||||
format: crate::LogFormat,
|
||||
ansi: bool,
|
||||
filter: crate::OutputFilter,
|
||||
}
|
||||
|
||||
impl FileSettings {
|
||||
/// Creates file output settings.
|
||||
/// Creates explicit file output settings.
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
output_id: impl std::convert::Into<std::string::String>,
|
||||
enabled: bool,
|
||||
directory: impl std::convert::Into<std::path::PathBuf>,
|
||||
file_name_prefix: impl std::convert::Into<std::string::String>,
|
||||
rotation: crate::FileRotation,
|
||||
format: crate::LogFormat,
|
||||
filter: crate::OutputFilter,
|
||||
) -> Self {
|
||||
return Self { directory: directory.into(), file_name_prefix: file_name_prefix.into(), rotation };
|
||||
return Self {
|
||||
output_id: output_id.into(),
|
||||
enabled,
|
||||
directory: directory.into(),
|
||||
file_name_prefix: file_name_prefix.into(),
|
||||
rotation,
|
||||
format,
|
||||
ansi: false,
|
||||
filter,
|
||||
};
|
||||
}
|
||||
|
||||
/// Sets whether ANSI formatting is requested and returns the updated settings.
|
||||
///
|
||||
/// Persistent file outputs are required to keep this value `false`; [`crate::LoggingSettings::validate`] rejects `true`.
|
||||
#[must_use]
|
||||
pub fn with_ansi(mut self, ansi: bool) -> Self {
|
||||
self.ansi = ansi;
|
||||
return self;
|
||||
}
|
||||
|
||||
/// Returns the stable output identifier used for diagnostics and reload accounting.
|
||||
#[must_use]
|
||||
pub fn output_id(&self) -> &str {
|
||||
return self.output_id.as_str();
|
||||
}
|
||||
|
||||
/// Returns whether this file output is enabled.
|
||||
#[must_use]
|
||||
pub const fn enabled(&self) -> bool {
|
||||
return self.enabled;
|
||||
}
|
||||
|
||||
/// Returns the directory containing log files.
|
||||
@@ -138,6 +279,24 @@ impl FileSettings {
|
||||
pub const fn rotation(&self) -> crate::FileRotation {
|
||||
return self.rotation;
|
||||
}
|
||||
|
||||
/// Returns the requested file format.
|
||||
#[must_use]
|
||||
pub const fn format(&self) -> crate::LogFormat {
|
||||
return self.format;
|
||||
}
|
||||
|
||||
/// Returns whether ANSI formatting was requested for this file output.
|
||||
#[must_use]
|
||||
pub const fn ansi(&self) -> bool {
|
||||
return self.ansi;
|
||||
}
|
||||
|
||||
/// Returns the routing filter associated with this file output.
|
||||
#[must_use]
|
||||
pub const fn filter(&self) -> &crate::OutputFilter {
|
||||
return &self.filter;
|
||||
}
|
||||
}
|
||||
|
||||
/// Complete runtime settings consumed by `ksp-logging-lib` initialization and reload.
|
||||
@@ -147,29 +306,29 @@ pub struct LoggingSettings {
|
||||
target_filters: std::vec::Vec<crate::TargetFilter>,
|
||||
span_events: crate::SpanEvents,
|
||||
console: std::option::Option<crate::ConsoleSettings>,
|
||||
file: std::option::Option<crate::FileSettings>,
|
||||
files: std::vec::Vec<crate::FileSettings>,
|
||||
}
|
||||
|
||||
impl LoggingSettings {
|
||||
/// Creates explicit Logging settings without any target override.
|
||||
/// Creates explicit Logging settings without any global target override.
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
default_filter: crate::LogFilterLevel,
|
||||
span_events: crate::SpanEvents,
|
||||
console: std::option::Option<crate::ConsoleSettings>,
|
||||
file: std::option::Option<crate::FileSettings>,
|
||||
files: std::vec::Vec<crate::FileSettings>,
|
||||
) -> Self {
|
||||
return Self { default_filter, target_filters: std::vec::Vec::new(), span_events, console, file };
|
||||
return Self { default_filter, target_filters: std::vec::Vec::new(), span_events, console, files };
|
||||
}
|
||||
|
||||
/// Adds one target-prefix override and returns the updated settings.
|
||||
/// Adds one target-prefix override to the global KSP takeover policy and returns the updated settings.
|
||||
#[must_use]
|
||||
pub fn with_target_filter(mut self, target_filter: crate::TargetFilter) -> Self {
|
||||
self.target_filters.push(target_filter);
|
||||
return self;
|
||||
}
|
||||
|
||||
/// Returns the default level applied to KSP-owned targets.
|
||||
/// Returns the default level applied to KSP-owned targets by the global takeover policy.
|
||||
#[must_use]
|
||||
pub const fn default_filter(&self) -> crate::LogFilterLevel {
|
||||
return self.default_filter;
|
||||
@@ -187,16 +346,16 @@ impl LoggingSettings {
|
||||
return self.span_events;
|
||||
}
|
||||
|
||||
/// Returns console settings when console output is enabled.
|
||||
/// Returns console settings when the console output is declared.
|
||||
#[must_use]
|
||||
pub fn console(&self) -> std::option::Option<&crate::ConsoleSettings> {
|
||||
return self.console.as_ref();
|
||||
}
|
||||
|
||||
/// Returns file settings when file output is enabled.
|
||||
/// Returns all declared file outputs in declaration order.
|
||||
#[must_use]
|
||||
pub fn file(&self) -> std::option::Option<&crate::FileSettings> {
|
||||
return self.file.as_ref();
|
||||
pub fn files(&self) -> &[crate::FileSettings] {
|
||||
return self.files.as_slice();
|
||||
}
|
||||
|
||||
/// Validates backend-independent invariants of the runtime settings.
|
||||
@@ -216,18 +375,142 @@ impl LoggingSettings {
|
||||
);
|
||||
}
|
||||
}
|
||||
if let std::option::Option::Some(file) = self.file.as_ref()
|
||||
&& file.file_name_prefix().is_empty()
|
||||
{
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "file name prefix must not be empty")
|
||||
.with_context("field", "file.file_name_prefix"),
|
||||
);
|
||||
if let std::option::Option::Some(console) = self.console.as_ref() {
|
||||
let validation_result = validate_output_filter(console.filter(), "console.filter");
|
||||
if let std::result::Result::Err(error) = validation_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
}
|
||||
for (index, file) in self.files.iter().enumerate() {
|
||||
let output_id_validation = validate_output_id(file.output_id(), index);
|
||||
if let std::result::Result::Err(error) = output_id_validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
if file.directory().as_os_str().is_empty() {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "file output directory must not be empty")
|
||||
.with_context("field", format!("files[{index}].directory"))
|
||||
.with_context("output_id", file.output_id()),
|
||||
);
|
||||
}
|
||||
if file.file_name_prefix().is_empty() {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "file name prefix must not be empty")
|
||||
.with_context("field", format!("files[{index}].file_name_prefix"))
|
||||
.with_context("output_id", file.output_id()),
|
||||
);
|
||||
}
|
||||
if file.ansi() {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "ANSI sequences are not allowed in persistent file outputs")
|
||||
.with_context("field", format!("files[{index}].ansi"))
|
||||
.with_context("output_id", file.output_id()),
|
||||
);
|
||||
}
|
||||
let filter_validation = validate_output_filter(file.filter(), format!("files[{index}].filter").as_str());
|
||||
if let std::result::Result::Err(error) = filter_validation {
|
||||
return std::result::Result::Err(error.with_context("output_id", file.output_id()));
|
||||
}
|
||||
for previous in &self.files[..index] {
|
||||
if previous.output_id() == file.output_id() {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "file output identifiers must be unique")
|
||||
.with_context("field", format!("files[{index}].output_id"))
|
||||
.with_context("output_id", file.output_id()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_output_id(output_id: &str, index: usize) -> ksp_core_lib::Result<()> {
|
||||
let mut previous_was_separator = true;
|
||||
if output_id.is_empty() {
|
||||
return invalid_output_id(output_id, index);
|
||||
}
|
||||
for byte in output_id.bytes() {
|
||||
if byte == b'.' {
|
||||
if previous_was_separator {
|
||||
return invalid_output_id(output_id, index);
|
||||
}
|
||||
previous_was_separator = true;
|
||||
continue;
|
||||
}
|
||||
if !(byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_' || byte == b'-') {
|
||||
return invalid_output_id(output_id, index);
|
||||
}
|
||||
previous_was_separator = false;
|
||||
}
|
||||
if previous_was_separator {
|
||||
return invalid_output_id(output_id, index);
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn invalid_output_id(output_id: &str, index: usize) -> ksp_core_lib::Result<()> {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "file output identifier is invalid")
|
||||
.with_context("field", format!("files[{index}].output_id"))
|
||||
.with_context("output_id", output_id),
|
||||
);
|
||||
}
|
||||
|
||||
fn validate_output_filter(filter: &crate::OutputFilter, field: &str) -> ksp_core_lib::Result<()> {
|
||||
let target_validation = validate_selectors(filter.targets(), true, format!("{field}.targets").as_str());
|
||||
if let std::result::Result::Err(error) = target_validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let domain_validation = validate_selectors(filter.domains(), false, format!("{field}.domains").as_str());
|
||||
if let std::result::Result::Err(error) = domain_validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn validate_selectors(selectors: &[std::string::String], target_dimension: bool, field: &str) -> ksp_core_lib::Result<()> {
|
||||
if selectors.is_empty() {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "output selector list must not be empty").with_context("field", field),
|
||||
);
|
||||
}
|
||||
if selectors.len() > 1
|
||||
&& selectors.iter().any(|selector| -> bool {
|
||||
return selector == "*";
|
||||
})
|
||||
{
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "wildcard selector must be used alone").with_context("field", field),
|
||||
);
|
||||
}
|
||||
for (index, selector) in selectors.iter().enumerate() {
|
||||
if selector.trim().is_empty() {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "output selector must not be empty")
|
||||
.with_context("field", format!("{field}[{index}]")),
|
||||
);
|
||||
}
|
||||
if target_dimension && selector != "*" && !selector.starts_with("ksp-") {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "output target selector must identify a KSP-owned target")
|
||||
.with_context("field", format!("{field}[{index}]"))
|
||||
.with_context("selector", selector),
|
||||
);
|
||||
}
|
||||
for previous in &selectors[..index] {
|
||||
if previous == selector {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "output selectors must be unique")
|
||||
.with_context("field", format!("{field}[{index}]"))
|
||||
.with_context("selector", selector),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/settings.rs"]
|
||||
mod tests;
|
||||
|
||||
Reference in New Issue
Block a user