v0.1.3-pre.009
This commit is contained in:
309
crates/ksp-config-lib/src/composite.rs
Normal file
309
crates/ksp-config-lib/src/composite.rs
Normal file
@@ -0,0 +1,309 @@
|
||||
// file: crates/ksp-config-lib/src/composite.rs
|
||||
// version: 1
|
||||
|
||||
/// One resolved document component selected by a composite profile.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ResolvedCompositeComponent {
|
||||
component_id: String,
|
||||
resolved: crate::ResolvedConfigProfile,
|
||||
}
|
||||
|
||||
impl ResolvedCompositeComponent {
|
||||
/// Returns the component identifier unique inside the selected composite profile.
|
||||
#[must_use]
|
||||
pub fn component_id(&self) -> &str {
|
||||
return self.component_id.as_str();
|
||||
}
|
||||
|
||||
/// Returns the referenced standard document after global/profile resolution.
|
||||
#[must_use]
|
||||
pub const fn resolved(&self) -> &crate::ResolvedConfigProfile {
|
||||
return &self.resolved;
|
||||
}
|
||||
}
|
||||
|
||||
/// Validated composite document resolved to one composite profile and all of its referenced standard documents.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ResolvedConfigComposite {
|
||||
file_id: crate::ConfigFileId,
|
||||
path: std::path::PathBuf,
|
||||
profile_id: String,
|
||||
selection_source: crate::ConfigProfileSelectionSource,
|
||||
components: std::collections::BTreeMap<String, ResolvedCompositeComponent>,
|
||||
}
|
||||
|
||||
impl ResolvedConfigComposite {
|
||||
/// Returns the logical composite file identifier.
|
||||
#[must_use]
|
||||
pub fn file_id(&self) -> &crate::ConfigFileId {
|
||||
return &self.file_id;
|
||||
}
|
||||
|
||||
/// Returns the physical path of the validated composite source.
|
||||
#[must_use]
|
||||
pub fn path(&self) -> &std::path::Path {
|
||||
return self.path.as_path();
|
||||
}
|
||||
|
||||
/// Returns the selected composite profile identifier.
|
||||
#[must_use]
|
||||
pub fn profile_id(&self) -> &str {
|
||||
return self.profile_id.as_str();
|
||||
}
|
||||
|
||||
/// Returns whether the composite profile came from `default_profile` or an explicit caller selection.
|
||||
#[must_use]
|
||||
pub const fn selection_source(&self) -> crate::ConfigProfileSelectionSource {
|
||||
return self.selection_source;
|
||||
}
|
||||
|
||||
/// Returns all resolved components keyed by their composite-local `component_id`.
|
||||
#[must_use]
|
||||
pub const fn components(&self) -> &std::collections::BTreeMap<String, ResolvedCompositeComponent> {
|
||||
return &self.components;
|
||||
}
|
||||
|
||||
/// Returns one resolved component by its composite-local identifier.
|
||||
#[must_use]
|
||||
pub fn component(&self, component_id: &str) -> std::option::Option<&ResolvedCompositeComponent> {
|
||||
return self.components.get(component_id);
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::ConfigDocumentEngine {
|
||||
/// Loads, validates and resolves one registered composite Config document.
|
||||
///
|
||||
/// Composite descriptors are introduced only when a concrete consumer exists. The generic contract and schema are available from `0.1.3-pre.009` onward.
|
||||
/// Passing `None` selects the composite's `default_profile`; passing `Some(profile_id)` selects a composite profile explicitly.
|
||||
pub fn load_resolved_composite(
|
||||
&self,
|
||||
file_id: &crate::ConfigFileId,
|
||||
requested_profile: std::option::Option<&str>,
|
||||
) -> ksp_core_lib::Result<ResolvedConfigComposite> {
|
||||
let document = self.load_validated_document(file_id);
|
||||
let document = match document {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if !document.file_id().as_str().starts_with("cfg.composite.") {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_COMPOSITE_REFERENCE_INVALID, "requested Config document is not a composite")
|
||||
.with_context("file_id", document.file_id().as_str()),
|
||||
);
|
||||
}
|
||||
return resolve_composite_document(self, &document, requested_profile);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct CompositeDocumentSource {
|
||||
format_version: u32,
|
||||
default_profile: String,
|
||||
profiles: std::vec::Vec<CompositeProfileSource>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct CompositeProfileSource {
|
||||
profile_id: String,
|
||||
documents: std::vec::Vec<CompositeDocumentReferenceSource>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct CompositeDocumentReferenceSource {
|
||||
component_id: String,
|
||||
file_id: String,
|
||||
profile_id: std::option::Option<String>,
|
||||
}
|
||||
|
||||
pub(crate) fn validate_composite_document_contract(engine: &crate::ConfigDocumentEngine, document: &crate::ConfigJsonDocument) -> ksp_core_lib::Result<()> {
|
||||
if !document.file_id().as_str().starts_with("cfg.composite.") {
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
let source = parse_composite_source(document);
|
||||
let source = match source {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if source.format_version != 1 {
|
||||
return std::result::Result::Err(composite_semantic_error(document, "unsupported composite format_version"));
|
||||
}
|
||||
if source.default_profile.trim().is_empty() {
|
||||
return std::result::Result::Err(composite_semantic_error(document, "composite default_profile must not be empty"));
|
||||
}
|
||||
for (profile_index, profile) in source.profiles.iter().enumerate() {
|
||||
if profile.profile_id.trim().is_empty() {
|
||||
return std::result::Result::Err(
|
||||
composite_semantic_error(document, "composite profile_id must not be empty").with_context("profile_index", profile_index.to_string()),
|
||||
);
|
||||
}
|
||||
let mut component_ids = std::collections::BTreeSet::<String>::new();
|
||||
for (component_index, reference) in profile.documents.iter().enumerate() {
|
||||
if component_ids.contains(reference.component_id.as_str()) {
|
||||
return std::result::Result::Err(
|
||||
composite_semantic_error(document, "component_id values must be unique inside one composite profile")
|
||||
.with_context("profile_id", profile.profile_id.as_str())
|
||||
.with_context("component_index", component_index.to_string())
|
||||
.with_context("component_id", reference.component_id.as_str()),
|
||||
);
|
||||
}
|
||||
component_ids.insert(reference.component_id.clone());
|
||||
let validation = validate_reference(engine, document, profile, reference, component_index);
|
||||
if let std::result::Result::Err(error) = validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn resolve_composite_document(
|
||||
engine: &crate::ConfigDocumentEngine,
|
||||
document: &crate::ConfigJsonDocument,
|
||||
requested_profile: std::option::Option<&str>,
|
||||
) -> ksp_core_lib::Result<ResolvedConfigComposite> {
|
||||
let source = parse_composite_source(document);
|
||||
let source = match source {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let (selected_profile_id, selection_source) = match requested_profile {
|
||||
std::option::Option::Some(value) => (value, crate::ConfigProfileSelectionSource::Explicit),
|
||||
std::option::Option::None => (source.default_profile.as_str(), crate::ConfigProfileSelectionSource::DefaultProfile),
|
||||
};
|
||||
let mut selected: std::option::Option<&CompositeProfileSource> = std::option::Option::None;
|
||||
for profile in &source.profiles {
|
||||
if profile.profile_id == selected_profile_id {
|
||||
selected = std::option::Option::Some(profile);
|
||||
break;
|
||||
}
|
||||
}
|
||||
let selected = match selected {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_PROFILE_NOT_FOUND, "requested composite Config profile does not exist")
|
||||
.with_context("file_id", document.file_id().as_str())
|
||||
.with_context("path", document.path().to_string_lossy().into_owned())
|
||||
.with_context("profile_id", selected_profile_id),
|
||||
);
|
||||
},
|
||||
};
|
||||
let mut components = std::collections::BTreeMap::<String, ResolvedCompositeComponent>::new();
|
||||
for reference in &selected.documents {
|
||||
let file_id = crate::ConfigFileId::new(reference.file_id.as_str());
|
||||
let file_id = match file_id {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let resolved = crate::profile::load_resolved_profile_with_source(
|
||||
engine,
|
||||
&file_id,
|
||||
reference.profile_id.as_deref(),
|
||||
crate::ConfigProfileSelectionSource::Composite,
|
||||
);
|
||||
let resolved = match resolved {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let component = ResolvedCompositeComponent { component_id: reference.component_id.clone(), resolved };
|
||||
components.insert(reference.component_id.clone(), component);
|
||||
}
|
||||
return std::result::Result::Ok(ResolvedConfigComposite {
|
||||
file_id: document.file_id().clone(),
|
||||
path: document.path().to_path_buf(),
|
||||
profile_id: selected_profile_id.to_owned(),
|
||||
selection_source,
|
||||
components,
|
||||
});
|
||||
}
|
||||
|
||||
fn validate_reference(
|
||||
engine: &crate::ConfigDocumentEngine,
|
||||
document: &crate::ConfigJsonDocument,
|
||||
profile: &CompositeProfileSource,
|
||||
reference: &CompositeDocumentReferenceSource,
|
||||
component_index: usize,
|
||||
) -> ksp_core_lib::Result<()> {
|
||||
if reference.component_id.trim().is_empty() {
|
||||
return std::result::Result::Err(composite_reference_error(document, profile, reference, component_index, "component_id must not be empty"));
|
||||
}
|
||||
let file_id = crate::ConfigFileId::new(reference.file_id.as_str());
|
||||
let file_id = match file_id {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return std::result::Result::Err(composite_reference_error(document, profile, reference, component_index, "referenced file_id is invalid"));
|
||||
},
|
||||
};
|
||||
if !file_id.as_str().starts_with("cfg.std.") {
|
||||
return std::result::Result::Err(composite_reference_error(
|
||||
document,
|
||||
profile,
|
||||
reference,
|
||||
component_index,
|
||||
"composite references must target standard Config document file_ids",
|
||||
));
|
||||
}
|
||||
let descriptor = engine.registry().descriptor(&file_id);
|
||||
let descriptor = match descriptor {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return std::result::Result::Err(composite_reference_error(document, profile, reference, component_index, "referenced file_id is not registered"));
|
||||
},
|
||||
};
|
||||
if descriptor.kind() != crate::ConfigFileKind::Config {
|
||||
return std::result::Result::Err(composite_reference_error(
|
||||
document,
|
||||
profile,
|
||||
reference,
|
||||
component_index,
|
||||
"referenced file_id is not a Config document",
|
||||
));
|
||||
}
|
||||
let resolved =
|
||||
crate::profile::load_resolved_profile_with_source(engine, &file_id, reference.profile_id.as_deref(), crate::ConfigProfileSelectionSource::Composite);
|
||||
if let std::result::Result::Err(error) = resolved {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn parse_composite_source(document: &crate::ConfigJsonDocument) -> ksp_core_lib::Result<CompositeDocumentSource> {
|
||||
let source = serde_json::from_value::<CompositeDocumentSource>(document.value().clone());
|
||||
return match source {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(
|
||||
composite_semantic_error(document, "schema-valid composite document cannot be decoded into the KSP source contract").with_source(error),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
fn composite_semantic_error(document: &crate::ConfigJsonDocument, reason: &'static str) -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_DOCUMENT_SEMANTIC_INVALID, "Config composite violates KSP semantic invariants")
|
||||
.with_context("file_id", document.file_id().as_str())
|
||||
.with_context("path", document.path().to_string_lossy().into_owned())
|
||||
.with_context("reason", reason);
|
||||
}
|
||||
|
||||
fn composite_reference_error(
|
||||
document: &crate::ConfigJsonDocument,
|
||||
profile: &CompositeProfileSource,
|
||||
reference: &CompositeDocumentReferenceSource,
|
||||
component_index: usize,
|
||||
reason: &'static str,
|
||||
) -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_COMPOSITE_REFERENCE_INVALID, "Config composite contains an invalid document reference")
|
||||
.with_context("file_id", document.file_id().as_str())
|
||||
.with_context("path", document.path().to_string_lossy().into_owned())
|
||||
.with_context("profile_id", profile.profile_id.as_str())
|
||||
.with_context("component_index", component_index.to_string())
|
||||
.with_context("component_id", reference.component_id.as_str())
|
||||
.with_context("referenced_file_id", reference.file_id.as_str())
|
||||
.with_context("reason", reason);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/composite.rs"]
|
||||
mod tests;
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/src/document.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
/// A Config-managed JSON document that has passed syntax, schema and current semantic validation.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -97,7 +97,7 @@ impl ConfigDocumentEngine {
|
||||
std::result::Result::Ok(()) => {},
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
let semantic_validation = validate_document_semantics(&document);
|
||||
let semantic_validation = validate_document_semantics(self, &document);
|
||||
return match semantic_validation {
|
||||
std::result::Result::Ok(()) => std::result::Result::Ok(document),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
@@ -207,11 +207,15 @@ fn validate_instance(document: &ConfigJsonDocument, schema: &ConfigJsonDocument)
|
||||
};
|
||||
}
|
||||
|
||||
fn validate_document_semantics(document: &ConfigJsonDocument) -> ksp_core_lib::Result<()> {
|
||||
fn validate_document_semantics(engine: &ConfigDocumentEngine, document: &ConfigJsonDocument) -> ksp_core_lib::Result<()> {
|
||||
let profile_validation = crate::profile::validate_document_profile_contract(document);
|
||||
if let std::result::Result::Err(error) = profile_validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let composite_validation = crate::composite::validate_composite_document_contract(engine, document);
|
||||
if let std::result::Result::Err(error) = composite_validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
if document.file_id().as_str() != crate::FILE_ID_STD_LOGGING {
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/src/error.rs
|
||||
// version: 4
|
||||
// version: 5
|
||||
|
||||
/// Error code used when a Config bootstrap argument is missing its value.
|
||||
pub const ERROR_CODE_BOOTSTRAP_ARGUMENT_MISSING_VALUE: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "bootstrap_argument_missing_value");
|
||||
@@ -36,3 +36,6 @@ pub const ERROR_CODE_DOCUMENT_SEMANTIC_INVALID: ksp_core_lib::ErrorCode = ksp_co
|
||||
|
||||
/// Error code used when an explicitly requested Config profile does not exist in a validated document.
|
||||
pub const ERROR_CODE_PROFILE_NOT_FOUND: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "profile_not_found");
|
||||
|
||||
/// Error code used when a composite document references an invalid, unknown, or unsupported Config document.
|
||||
pub const ERROR_CODE_COMPOSITE_REFERENCE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "composite_reference_invalid");
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
// file: crates/ksp-config-lib/src/lib.rs
|
||||
// version: 4
|
||||
// version: 5
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
//! KSP-owned application configuration facade.
|
||||
//!
|
||||
//! `0.1.3-pre.008` owns bootstrap roots, the logical file registry, generic JSON/JSON Schema loading and standard-document profile resolution. The standard
|
||||
//! Logging document is the first registered runtime document. Composite resolution, environment substitution, sensitivity and persistence remain in later bounded
|
||||
//! prereleases.
|
||||
//! `0.1.3-pre.009` owns bootstrap roots, the logical file registry, generic JSON/JSON Schema loading, standard-document profile resolution and generic composite
|
||||
//! resolution by stable `file_id`. The standard Logging document remains the first registered runtime document. Environment substitution, sensitivity and persistence
|
||||
//! remain in later bounded prereleases.
|
||||
|
||||
mod bootstrap;
|
||||
mod composite;
|
||||
mod document;
|
||||
mod error;
|
||||
mod profile;
|
||||
@@ -26,6 +27,10 @@ pub use self::bootstrap::ConfigBootstrapOptions;
|
||||
pub use self::bootstrap::DEFAULT_CFG_PATH;
|
||||
/// Default root containing KSP JSON schemas.
|
||||
pub use self::bootstrap::DEFAULT_SCHEMA_PATH;
|
||||
/// One resolved document component selected by a composite profile.
|
||||
pub use self::composite::ResolvedCompositeComponent;
|
||||
/// Validated composite document resolved to one profile and its referenced standard documents.
|
||||
pub use self::composite::ResolvedConfigComposite;
|
||||
/// Generic JSON/JSON Schema engine owned by Config.
|
||||
pub use self::document::ConfigDocumentEngine;
|
||||
/// A Config-managed JSON document after syntax, schema and current semantic validation.
|
||||
@@ -34,6 +39,8 @@ pub use self::document::ConfigJsonDocument;
|
||||
pub use self::error::ERROR_CODE_BOOTSTRAP_ARGUMENT_MISSING_VALUE;
|
||||
/// Error code used when a Config bootstrap path is empty, inaccessible, or resolves to an existing non-directory path.
|
||||
pub use self::error::ERROR_CODE_BOOTSTRAP_INVALID_PATH;
|
||||
/// Error code used when a composite document contains an invalid or unsupported document reference.
|
||||
pub use self::error::ERROR_CODE_COMPOSITE_REFERENCE_INVALID;
|
||||
/// Error code used when a schema-valid Config document violates KSP semantic invariants.
|
||||
pub use self::error::ERROR_CODE_DOCUMENT_SEMANTIC_INVALID;
|
||||
/// Error code used when the same logical Config file identifier is registered more than once.
|
||||
@@ -70,10 +77,14 @@ pub use self::registry::ConfigFileId;
|
||||
pub use self::registry::ConfigFileKind;
|
||||
/// Registry of KSP-known logical Config files and their replaceable physical filenames.
|
||||
pub use self::registry::ConfigFileRegistry;
|
||||
/// Default physical filename for the generic composite JSON Schema document.
|
||||
pub use self::registry::DEFAULT_COMPOSITE_SCHEMA_FILENAME;
|
||||
/// Default physical filename for the standard Logging configuration document.
|
||||
pub use self::registry::DEFAULT_STD_LOGGING_FILENAME;
|
||||
/// Default physical filename for the standard Logging JSON Schema document.
|
||||
pub use self::registry::DEFAULT_STD_LOGGING_SCHEMA_FILENAME;
|
||||
/// Logical file identifier for the generic composite JSON Schema document.
|
||||
pub use self::registry::FILE_ID_SCHEMA_COMPOSITE;
|
||||
/// Logical file identifier for the standard Logging JSON Schema document.
|
||||
pub use self::registry::FILE_ID_SCHEMA_STD_LOGGING;
|
||||
/// Logical file identifier for the standard Logging configuration document.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/src/profile.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
/// Origin of one top-level value in a resolved standard Config profile.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
@@ -17,6 +17,8 @@ pub enum ConfigProfileSelectionSource {
|
||||
DefaultProfile,
|
||||
/// A caller explicitly requested the profile by `profile_id`.
|
||||
Explicit,
|
||||
/// A composite document selected the referenced standard document profile.
|
||||
Composite,
|
||||
}
|
||||
|
||||
/// Validated standard document resolved to one profile while retaining global/profile provenance.
|
||||
@@ -86,7 +88,7 @@ impl crate::ConfigDocumentEngine {
|
||||
/// Loads, validates and resolves one standard Config document to its default or explicitly requested profile.
|
||||
///
|
||||
/// Passing `None` selects the autonomous `default_profile` declared by the document. Passing `Some(profile_id)` selects that profile explicitly.
|
||||
/// Environment interpolation and composite overrides are intentionally not applied by this prerelease.
|
||||
/// Environment interpolation is intentionally not applied by this prerelease.
|
||||
pub fn load_resolved_profile(
|
||||
&self,
|
||||
file_id: &crate::ConfigFileId,
|
||||
@@ -97,10 +99,32 @@ impl crate::ConfigDocumentEngine {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return resolve_document_profile(&document, requested_profile);
|
||||
let source = match requested_profile {
|
||||
std::option::Option::Some(_) => ConfigProfileSelectionSource::Explicit,
|
||||
std::option::Option::None => ConfigProfileSelectionSource::DefaultProfile,
|
||||
};
|
||||
return resolve_document_profile(&document, requested_profile, source);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn load_resolved_profile_with_source(
|
||||
engine: &crate::ConfigDocumentEngine,
|
||||
file_id: &crate::ConfigFileId,
|
||||
requested_profile: std::option::Option<&str>,
|
||||
explicit_source: ConfigProfileSelectionSource,
|
||||
) -> ksp_core_lib::Result<ResolvedConfigProfile> {
|
||||
let document = engine.load_validated_document(file_id);
|
||||
let document = match document {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let source = match requested_profile {
|
||||
std::option::Option::Some(_) => explicit_source,
|
||||
std::option::Option::None => ConfigProfileSelectionSource::DefaultProfile,
|
||||
};
|
||||
return resolve_document_profile(&document, requested_profile, source);
|
||||
}
|
||||
|
||||
pub(crate) fn validate_document_profile_contract(document: &crate::ConfigJsonDocument) -> ksp_core_lib::Result<()> {
|
||||
let root = match document.value().as_object() {
|
||||
std::option::Option::Some(value) => value,
|
||||
@@ -158,7 +182,11 @@ pub(crate) fn validate_document_profile_contract(document: &crate::ConfigJsonDoc
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn resolve_document_profile(document: &crate::ConfigJsonDocument, requested_profile: std::option::Option<&str>) -> ksp_core_lib::Result<ResolvedConfigProfile> {
|
||||
fn resolve_document_profile(
|
||||
document: &crate::ConfigJsonDocument,
|
||||
requested_profile: std::option::Option<&str>,
|
||||
explicit_source: ConfigProfileSelectionSource,
|
||||
) -> ksp_core_lib::Result<ResolvedConfigProfile> {
|
||||
let root = match document.value().as_object() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(profile_semantic_error(document, "profile resolution requires an object document")),
|
||||
@@ -168,7 +196,7 @@ fn resolve_document_profile(document: &crate::ConfigJsonDocument, requested_prof
|
||||
std::option::Option::None => return std::result::Result::Err(profile_semantic_error(document, "profile resolution requires default_profile")),
|
||||
};
|
||||
let (selected_profile, selection_source) = match requested_profile {
|
||||
std::option::Option::Some(value) => (value, ConfigProfileSelectionSource::Explicit),
|
||||
std::option::Option::Some(value) => (value, explicit_source),
|
||||
std::option::Option::None => (default_profile, ConfigProfileSelectionSource::DefaultProfile),
|
||||
};
|
||||
let profiles = match root.get("profiles").and_then(serde_json::Value::as_array) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/src/registry.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
/// Bootstrap argument used to replace a known Config filename mapping.
|
||||
pub const ARG_FILE_MAP: &str = "--filemap";
|
||||
@@ -7,10 +7,14 @@ pub const ARG_FILE_MAP: &str = "--filemap";
|
||||
pub const FILE_ID_STD_LOGGING: &str = "cfg.std.logging";
|
||||
/// Logical file identifier for the standard Logging JSON Schema document.
|
||||
pub const FILE_ID_SCHEMA_STD_LOGGING: &str = "schema.std.logging";
|
||||
/// Logical file identifier for the generic composite JSON Schema document.
|
||||
pub const FILE_ID_SCHEMA_COMPOSITE: &str = "schema.composite";
|
||||
/// Default physical filename for the standard Logging configuration document.
|
||||
pub const DEFAULT_STD_LOGGING_FILENAME: &str = "std.logging.json";
|
||||
/// Default physical filename for the standard Logging JSON Schema document.
|
||||
pub const DEFAULT_STD_LOGGING_SCHEMA_FILENAME: &str = "std.logging.schema.json";
|
||||
/// Default physical filename for the generic composite JSON Schema document.
|
||||
pub const DEFAULT_COMPOSITE_SCHEMA_FILENAME: &str = "composite.schema.json";
|
||||
|
||||
/// Stable logical identifier for a Config-managed file.
|
||||
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||
@@ -77,7 +81,7 @@ impl ConfigFileDescriptor {
|
||||
return self.schema_file_id.as_ref();
|
||||
}
|
||||
|
||||
fn new(
|
||||
pub(crate) fn new(
|
||||
file_id: &'static str,
|
||||
kind: ConfigFileKind,
|
||||
filename: &'static str,
|
||||
@@ -131,7 +135,13 @@ impl ConfigFileRegistry {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return build_registry([logging, logging_schema]);
|
||||
let composite_schema =
|
||||
ConfigFileDescriptor::new(FILE_ID_SCHEMA_COMPOSITE, ConfigFileKind::Schema, DEFAULT_COMPOSITE_SCHEMA_FILENAME, std::option::Option::None);
|
||||
let composite_schema = match composite_schema {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return build_registry([logging, logging_schema, composite_schema]);
|
||||
}
|
||||
|
||||
/// Creates the default registry and applies repeatable `--filemap=<file_id>=<filename>` overrides from raw process arguments.
|
||||
@@ -208,7 +218,7 @@ impl ConfigFileRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
fn build_registry<const N: usize>(descriptors: [ConfigFileDescriptor; N]) -> ksp_core_lib::Result<ConfigFileRegistry> {
|
||||
pub(crate) fn build_registry<const N: usize>(descriptors: [ConfigFileDescriptor; N]) -> ksp_core_lib::Result<ConfigFileRegistry> {
|
||||
let mut registry = ConfigFileRegistry { descriptors: std::collections::BTreeMap::new() };
|
||||
for descriptor in descriptors {
|
||||
let file_id = descriptor.file_id.clone();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// file: crates/ksp-config-lib/tests/public_api.rs
|
||||
// version: 5
|
||||
// version: 6
|
||||
|
||||
//! Integration tests for the public `ksp-config-lib` bootstrap, file registry, validated JSON document and profile-resolution contracts.
|
||||
//! Integration tests for the public `ksp-config-lib` bootstrap, file registry, validated JSON document, profile-resolution and composite contracts.
|
||||
|
||||
#[test]
|
||||
fn bootstrap_contract_is_available_from_crate_root() {
|
||||
@@ -116,3 +116,20 @@ fn resolved_profile_contract_is_available_from_crate_root() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn composite_schema_and_provenance_contracts_are_available_from_crate_root() {
|
||||
let registry = ksp_config_lib::ConfigFileRegistry::defaults();
|
||||
let schema_id = ksp_config_lib::ConfigFileId::new(ksp_config_lib::FILE_ID_SCHEMA_COMPOSITE);
|
||||
assert!(registry.is_ok(), "public registry should remain constructible: {registry:?}");
|
||||
assert!(schema_id.is_ok(), "public composite schema file_id should be valid: {schema_id:?}");
|
||||
if let (std::result::Result::Ok(registry), std::result::Result::Ok(schema_id)) = (registry, schema_id) {
|
||||
let descriptor = registry.descriptor(&schema_id);
|
||||
assert!(descriptor.is_ok(), "public composite schema descriptor should exist: {descriptor:?}");
|
||||
if let std::result::Result::Ok(descriptor) = descriptor {
|
||||
assert_eq!(descriptor.filename(), std::path::Path::new(ksp_config_lib::DEFAULT_COMPOSITE_SCHEMA_FILENAME));
|
||||
assert_eq!(descriptor.kind(), ksp_config_lib::ConfigFileKind::Schema);
|
||||
}
|
||||
}
|
||||
assert_ne!(ksp_config_lib::ConfigProfileSelectionSource::Composite, ksp_config_lib::ConfigProfileSelectionSource::Explicit);
|
||||
}
|
||||
|
||||
247
crates/ksp-config-lib/unit_tests/composite.rs
Normal file
247
crates/ksp-config-lib/unit_tests/composite.rs
Normal file
@@ -0,0 +1,247 @@
|
||||
// file: crates/ksp-config-lib/unit_tests/composite.rs
|
||||
// version: 1
|
||||
|
||||
const TEST_COMPOSITE_FILE_ID: &str = "cfg.composite.test";
|
||||
const TEST_COMPOSITE_FILENAME: &str = "examples/composite.example.json";
|
||||
|
||||
#[test]
|
||||
fn committed_composite_example_resolves_default_document_profile() {
|
||||
let engine = committed_engine();
|
||||
let file_id = crate::ConfigFileId::new(TEST_COMPOSITE_FILE_ID);
|
||||
assert!(engine.is_ok(), "composite test engine should be constructible: {engine:?}");
|
||||
assert!(file_id.is_ok(), "composite test file_id should be valid: {file_id:?}");
|
||||
if let (std::result::Result::Ok(engine), std::result::Result::Ok(file_id)) = (engine, file_id) {
|
||||
let resolved = engine.load_resolved_composite(&file_id, std::option::Option::None);
|
||||
assert!(resolved.is_ok(), "committed composite example should resolve: {resolved:?}");
|
||||
if let std::result::Result::Ok(resolved) = resolved {
|
||||
assert_eq!(resolved.profile_id(), "local_default");
|
||||
assert_eq!(resolved.selection_source(), crate::ConfigProfileSelectionSource::DefaultProfile);
|
||||
let logging = resolved.component("logging");
|
||||
assert!(logging.is_some(), "logging component should be resolved");
|
||||
if let std::option::Option::Some(logging) = logging {
|
||||
assert_eq!(logging.resolved().file_id().as_str(), crate::FILE_ID_STD_LOGGING);
|
||||
assert_eq!(logging.resolved().profile_id(), "local_dev");
|
||||
assert_eq!(logging.resolved().selection_source(), crate::ConfigProfileSelectionSource::DefaultProfile);
|
||||
assert_eq!(logging.resolved().origin("logs_directory"), std::option::Option::Some(crate::ConfigValueOrigin::Global));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn composite_profile_override_marks_referenced_profile_selection_as_composite() {
|
||||
let engine = committed_engine();
|
||||
let file_id = crate::ConfigFileId::new(TEST_COMPOSITE_FILE_ID);
|
||||
assert!(engine.is_ok(), "composite test engine should be constructible: {engine:?}");
|
||||
assert!(file_id.is_ok(), "composite test file_id should be valid: {file_id:?}");
|
||||
if let (std::result::Result::Ok(engine), std::result::Result::Ok(file_id)) = (engine, file_id) {
|
||||
let resolved = engine.load_resolved_composite(&file_id, std::option::Option::Some("local_explicit"));
|
||||
assert!(resolved.is_ok(), "explicit composite profile should resolve: {resolved:?}");
|
||||
if let std::result::Result::Ok(resolved) = resolved {
|
||||
assert_eq!(resolved.selection_source(), crate::ConfigProfileSelectionSource::Explicit);
|
||||
let logging = resolved.component("logging");
|
||||
assert!(logging.is_some(), "logging component should be resolved");
|
||||
if let std::option::Option::Some(logging) = logging {
|
||||
assert_eq!(logging.resolved().profile_id(), "local_dev");
|
||||
assert_eq!(logging.resolved().selection_source(), crate::ConfigProfileSelectionSource::Composite);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_composite_profile_has_profile_not_found_error() {
|
||||
let engine = committed_engine();
|
||||
let file_id = crate::ConfigFileId::new(TEST_COMPOSITE_FILE_ID);
|
||||
assert!(engine.is_ok(), "composite test engine should be constructible: {engine:?}");
|
||||
assert!(file_id.is_ok(), "composite test file_id should be valid: {file_id:?}");
|
||||
if let (std::result::Result::Ok(engine), std::result::Result::Ok(file_id)) = (engine, file_id) {
|
||||
let result = engine.load_resolved_composite(&file_id, std::option::Option::Some("missing"));
|
||||
assert!(result.is_err(), "unknown composite profile must be rejected: {result:?}");
|
||||
if let std::result::Result::Err(error) = result {
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_PROFILE_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_referenced_file_id_is_rejected_during_composite_validation() {
|
||||
let fixture = fixture_roots("unknown-reference");
|
||||
let composite = r#"{
|
||||
"format_version": 1,
|
||||
"default_profile": "test",
|
||||
"profiles": [{
|
||||
"profile_id": "test",
|
||||
"documents": [{"component_id": "missing", "file_id": "cfg.std.missing"}]
|
||||
}]
|
||||
}"#;
|
||||
let prepared = prepare_fixture(&fixture, composite);
|
||||
assert!(prepared.is_ok(), "composite fixture should be writable: {prepared:?}");
|
||||
if prepared.is_ok() {
|
||||
let engine = fixture_engine(&fixture);
|
||||
let file_id = crate::ConfigFileId::new(TEST_COMPOSITE_FILE_ID);
|
||||
assert!(engine.is_ok(), "fixture engine should be constructible: {engine:?}");
|
||||
if let (std::result::Result::Ok(engine), std::result::Result::Ok(file_id)) = (engine, file_id) {
|
||||
let result = engine.load_validated_document(&file_id);
|
||||
assert!(result.is_err(), "unknown composite reference must be rejected: {result:?}");
|
||||
if let std::result::Result::Err(error) = result {
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_COMPOSITE_REFERENCE_INVALID);
|
||||
}
|
||||
}
|
||||
}
|
||||
cleanup_fixture(&fixture);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_component_ids_are_rejected_inside_one_composite_profile() {
|
||||
let fixture = fixture_roots("duplicate-component");
|
||||
let composite = r#"{
|
||||
"format_version": 1,
|
||||
"default_profile": "test",
|
||||
"profiles": [{
|
||||
"profile_id": "test",
|
||||
"documents": [
|
||||
{"component_id": "logging", "file_id": "cfg.std.logging"},
|
||||
{"component_id": "logging", "file_id": "cfg.std.logging"}
|
||||
]
|
||||
}]
|
||||
}"#;
|
||||
let prepared = prepare_fixture(&fixture, composite);
|
||||
assert!(prepared.is_ok(), "composite fixture should be writable: {prepared:?}");
|
||||
if prepared.is_ok() {
|
||||
let engine = fixture_engine(&fixture);
|
||||
let file_id = crate::ConfigFileId::new(TEST_COMPOSITE_FILE_ID);
|
||||
assert!(engine.is_ok(), "fixture engine should be constructible: {engine:?}");
|
||||
if let (std::result::Result::Ok(engine), std::result::Result::Ok(file_id)) = (engine, file_id) {
|
||||
let result = engine.load_validated_document(&file_id);
|
||||
assert!(result.is_err(), "duplicate component_id must be rejected: {result:?}");
|
||||
if let std::result::Result::Err(error) = result {
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_DOCUMENT_SEMANTIC_INVALID);
|
||||
}
|
||||
}
|
||||
}
|
||||
cleanup_fixture(&fixture);
|
||||
}
|
||||
|
||||
fn committed_engine() -> ksp_core_lib::Result<crate::ConfigDocumentEngine> {
|
||||
let workspace = workspace_root();
|
||||
let bootstrap = crate::ConfigBootstrapOptions::from_paths(workspace.join("config"), workspace.join("config/schemas"));
|
||||
let bootstrap = match bootstrap {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let registry = test_registry(TEST_COMPOSITE_FILENAME);
|
||||
return match registry {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(crate::ConfigDocumentEngine::new(bootstrap, value)),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
|
||||
fn fixture_engine(fixture: &FixtureRoots) -> ksp_core_lib::Result<crate::ConfigDocumentEngine> {
|
||||
let bootstrap = crate::ConfigBootstrapOptions::from_paths(fixture.config.as_path(), fixture.schemas.as_path());
|
||||
let bootstrap = match bootstrap {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let registry = test_registry("composite.test.json");
|
||||
return match registry {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(crate::ConfigDocumentEngine::new(bootstrap, value)),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
|
||||
fn test_registry(composite_filename: &'static str) -> ksp_core_lib::Result<crate::ConfigFileRegistry> {
|
||||
let logging = crate::registry::ConfigFileDescriptor::new(
|
||||
crate::FILE_ID_STD_LOGGING,
|
||||
crate::ConfigFileKind::Config,
|
||||
crate::DEFAULT_STD_LOGGING_FILENAME,
|
||||
std::option::Option::Some(crate::FILE_ID_SCHEMA_STD_LOGGING),
|
||||
);
|
||||
let logging = match logging {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let logging_schema = crate::registry::ConfigFileDescriptor::new(
|
||||
crate::FILE_ID_SCHEMA_STD_LOGGING,
|
||||
crate::ConfigFileKind::Schema,
|
||||
crate::DEFAULT_STD_LOGGING_SCHEMA_FILENAME,
|
||||
std::option::Option::None,
|
||||
);
|
||||
let logging_schema = match logging_schema {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let composite_schema = crate::registry::ConfigFileDescriptor::new(
|
||||
crate::FILE_ID_SCHEMA_COMPOSITE,
|
||||
crate::ConfigFileKind::Schema,
|
||||
crate::DEFAULT_COMPOSITE_SCHEMA_FILENAME,
|
||||
std::option::Option::None,
|
||||
);
|
||||
let composite_schema = match composite_schema {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let composite = crate::registry::ConfigFileDescriptor::new(
|
||||
TEST_COMPOSITE_FILE_ID,
|
||||
crate::ConfigFileKind::Config,
|
||||
composite_filename,
|
||||
std::option::Option::Some(crate::FILE_ID_SCHEMA_COMPOSITE),
|
||||
);
|
||||
let composite = match composite {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return crate::registry::build_registry([logging, logging_schema, composite_schema, composite]);
|
||||
}
|
||||
|
||||
fn prepare_fixture(fixture: &FixtureRoots, composite: &str) -> std::io::Result<()> {
|
||||
cleanup_fixture(fixture);
|
||||
let config = std::fs::create_dir_all(fixture.config.as_path());
|
||||
if let std::result::Result::Err(error) = config {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let schemas = std::fs::create_dir_all(fixture.schemas.as_path());
|
||||
if let std::result::Result::Err(error) = schemas {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let workspace = workspace_root();
|
||||
let logging = std::fs::copy(workspace.join("config/std.logging.json"), fixture.config.join(crate::DEFAULT_STD_LOGGING_FILENAME));
|
||||
if let std::result::Result::Err(error) = logging {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let logging_schema =
|
||||
std::fs::copy(workspace.join("config/schemas/std.logging.schema.json"), fixture.schemas.join(crate::DEFAULT_STD_LOGGING_SCHEMA_FILENAME));
|
||||
if let std::result::Result::Err(error) = logging_schema {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let composite_schema =
|
||||
std::fs::copy(workspace.join("config/schemas/composite.schema.json"), fixture.schemas.join(crate::DEFAULT_COMPOSITE_SCHEMA_FILENAME));
|
||||
if let std::result::Result::Err(error) = composite_schema {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
return std::fs::write(fixture.config.join("composite.test.json"), composite);
|
||||
}
|
||||
|
||||
fn workspace_root() -> std::path::PathBuf {
|
||||
return std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
}
|
||||
|
||||
struct FixtureRoots {
|
||||
root: std::path::PathBuf,
|
||||
config: std::path::PathBuf,
|
||||
schemas: std::path::PathBuf,
|
||||
}
|
||||
|
||||
fn fixture_roots(name: &str) -> FixtureRoots {
|
||||
let root = std::env::temp_dir().join(format!("ksp-config-composite-{name}-{}", std::process::id()));
|
||||
return FixtureRoots { config: root.join("config"), schemas: root.join("schemas"), root };
|
||||
}
|
||||
|
||||
fn cleanup_fixture(fixture: &FixtureRoots) {
|
||||
let removal = std::fs::remove_dir_all(fixture.root.as_path());
|
||||
if let std::result::Result::Err(error) = removal {
|
||||
if error.kind() != std::io::ErrorKind::NotFound {
|
||||
eprintln!("unable to cleanup Config composite fixture {}: {error}", fixture.root.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/unit_tests/registry.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
#[test]
|
||||
fn defaults_register_logging_document_and_schema_with_distinct_roots() {
|
||||
@@ -195,3 +195,26 @@ fn absolute_fixture_path() -> std::path::PathBuf {
|
||||
path.push("ksp-config-lib-absolute-mapping.json");
|
||||
return path;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn defaults_register_generic_composite_schema_without_runtime_composite() {
|
||||
let registry = super::ConfigFileRegistry::defaults();
|
||||
let schema_id = super::ConfigFileId::new(super::FILE_ID_SCHEMA_COMPOSITE);
|
||||
let runtime_id = super::ConfigFileId::new("cfg.composite.ksp-app-wallet-desk");
|
||||
assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
|
||||
assert!(schema_id.is_ok(), "composite schema file_id should be valid: {schema_id:?}");
|
||||
assert!(runtime_id.is_ok(), "future composite runtime file_id syntax should be valid: {runtime_id:?}");
|
||||
if let (std::result::Result::Ok(registry), std::result::Result::Ok(schema_id), std::result::Result::Ok(runtime_id)) = (registry, schema_id, runtime_id) {
|
||||
let schema = registry.descriptor(&schema_id);
|
||||
let runtime = registry.descriptor(&runtime_id);
|
||||
assert!(schema.is_ok(), "generic composite schema should be registered: {schema:?}");
|
||||
assert!(runtime.is_err(), "no fictitious runtime composite should be registered");
|
||||
if let std::result::Result::Ok(schema) = schema {
|
||||
assert_eq!(schema.kind(), super::ConfigFileKind::Schema);
|
||||
assert_eq!(schema.filename(), std::path::Path::new(super::DEFAULT_COMPOSITE_SCHEMA_FILENAME));
|
||||
}
|
||||
if let std::result::Result::Err(error) = runtime {
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_FILE_ID_UNKNOWN);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user