v0.2.6-pre.003

This commit is contained in:
2026-08-20 17:08:32 +02:00
parent 1a57cdeef0
commit f9f9ac79b6
40 changed files with 1735 additions and 122 deletions

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-config-lib/README.md -->
<!-- version: 5 -->
<!-- version: 6 -->
# ksp-config-lib
@@ -33,18 +33,21 @@ La crate centralise les documents JSON, leurs schemas, les profils et compositio
Le registre par défaut connaît :
```text
cfg.std.logging -> config/std.logging.json
cfg.std.transport -> config/std.transport.json
schema.std.logging -> config/schemas/std.logging.schema.json
schema.std.transport -> config/schemas/std.transport.schema.json
schema.composite -> config/schemas/composite.schema.json
cfg.composite.ksp-app-wallet-desk -> config/composite.ksp-app-wallet-desk.json
cfg.std.logging -> config/std.logging.json
cfg.std.transport -> config/std.transport.json
cfg.std.wallet -> config/std.wallet.json
schema.composite -> config/schemas/composite.schema.json
schema.std.logging -> config/schemas/std.logging.schema.json
schema.std.transport -> config/schemas/std.transport.schema.json
schema.std.wallet -> config/schemas/std.wallet.schema.json
```
`ConfigFileRegistry::descriptors()` expose ces descripteurs en lecture seule et dans un ordre déterministe par `file_id`. Une application de management peut ainsi découvrir les fichiers connus sans maintenir une liste parallèle ni dépendre de leurs filenames physiques.
`ConfigManagement::read_source()` permet d'inspecter le texte brut d'un document Config enregistré même lorsque ce document est invalide. `save_source_candidate()` complète cette frontière : le candidat brut est parsé, validé contre son schema et les invariants sémantiques KSP, puis persisté atomiquement uniquement après validation complète. Le `file_id` doit appartenir au registre et désigner un document Config ; aucun path arbitraire n'est accepté.
`config/examples/composite.example.json` démontre le format composite sans créer de composite runtime fictif.
`config/examples/composite.example.json` conserve lexemple générique. `config/composite.ksp-app-wallet-desk.json` est le premier composite runtime concret : il sélectionne Logging, Transport et Wallet par `file_id`, sans dépendre de leurs filenames physiques.
Le fichier local d'environnement est :
@@ -62,9 +65,9 @@ Les autres crates et applications KSP ne doivent pas :
- parser ou écrire directement `.env` ;
- ouvrir directement les documents Config connus par leur filename physique ;
- réimplémenter la sélection de profils, les compositions ou les placeholders ;
- reconstruire elles-mêmes la configuration Logging ou Transport depuis le JSON.
- reconstruire elles-mêmes la configuration Logging, Transport ou Wallet depuis le JSON.
`ksp-config-lib` dépend de `ksp-core-lib` pour `Error`/`Result`, de `ksp-logging-lib` pour les événements Config utiles et le contrat `LoggingSettings`, et de `ksp-onchain-transport-lib` pour construire le contrat runtime Transport dans la direction Config -> Transport.
`ksp-config-lib` dépend de `ksp-core-lib` pour `Error`/`Result`, de `ksp-logging-lib` pour les événements Config utiles et le contrat `LoggingSettings`, et de `ksp-onchain-transport-lib` pour construire le contrat runtime Transport dans la direction Config -> Transport. Le document Wallet reste un contrat de chemins/profils Config et nintroduit aucune dépendance Config -> `ksp-wallet-lib`.
La dépendance inverse est interdite : `ksp-core-lib`, `ksp-logging-lib` et `ksp-onchain-transport-lib` ne dépendent pas de Config.
@@ -78,7 +81,7 @@ Un secret reste accessible au runtime ou au management lorsqu'un consumer autori
Les méthodes `reveal_*` constituent un opt-in explicite au réel. L'authentification/autorisation de l'utilisateur humain appartient à l'application appelante et les valeurs retournées par ces méthodes ne doivent jamais être journalisées.
Le document Logging refuse les valeurs de sensibilité `Secret` dans sa configuration effective. Le document Transport les accepte pour les URLs endpoint : la valeur réelle est transmise au runtime légitime, tandis que la projection sûre et les `Debug` restent redacted.
Le document Logging refuse les valeurs de sensibilité `Secret` dans sa configuration effective. Le document Transport les accepte pour les URLs endpoint : la valeur réelle est transmise au runtime légitime, tandis que la projection sûre et les `Debug` restent redacted. `std.wallet` refuse également toute sensibilité `Secret` pour `wallets_directory`/`wallets_subdirectory`; les passwords Wallet restent un autre flux Config et ne sont jamais stockés dans ce JSON.
## Documentation
@@ -87,4 +90,6 @@ Le document Logging refuse les valeurs de sensibilité `Secret` dans sa configur
- [`../../docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md`](../../docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md) — plan historique détaillé de la fondation Config ;
- [`../../config/std.logging.json`](../../config/std.logging.json) — document standard Logging ;
- [`../../config/std.transport.json`](../../config/std.transport.json) — document standard HTTP Transport ;
- [`../../config/std.wallet.json`](../../config/std.wallet.json) — racine Wallet globale et sous-répertoire optionnel par profil ;
- [`../../config/composite.ksp-app-wallet-desk.json`](../../config/composite.ksp-app-wallet-desk.json) — composition Logging/Transport/Wallet de Wallet Desk ;
- [`../../.env.example`](../../.env.example) — inventaire versionné des variables d'environnement runtime.

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-config-lib/TODO.md -->
<!-- version: 4 -->
<!-- version: 5 -->
# TODO ksp-config-lib
@@ -35,6 +35,14 @@ Ces points ne nécessitent pas de duplication de logique dans `ksp-config-lib`;
`0.2.1-pre.006` introduit le premier nouveau domaine standard depuis Logging : `std.transport.json`, son schema, son exemple, son enregistrement et l'adapter Config -> `HttpTransportSettings`. Cette extension confirme que les nouveaux domaines restent ajoutés à la demande d'un consumer réel, sans transformer Config en propriétaire du runtime Transport.
## Extension `0.2.6` — Wallet Desk
`0.2.6-pre.003` ajoute le domaine standard Wallet au moment où son premier consumer réel en a besoin : `cfg.std.wallet`, `schema.std.wallet`, `ResolvedWalletConfig` et le composite concret `cfg.composite.ksp-app-wallet-desk`. `wallets_directory` reste global ; `wallets_subdirectory` est optionnel par profil et ne peut pas sortir de la racine par syntaxe de path. Ladapter Config résout les paths et leur provenance mais ne crée pas de répertoire et ne dépend pas de `ksp-wallet-lib`.
Le même delta ajoute les entrées `resolve_*_config_profile` nécessaires pour mapper un profil Logging/Wallet déjà sélectionné par un composite sans perdre sa provenance `Composite`. La préparation filesystem du répertoire effectif appartient à `ksp-app-wallet-desk`.
Les futurs passwords `KSP_SECRET_WALLET_PASS_*` restent hors du JSON `std.wallet` et seront introduits dans la tranche dunlock dédiée ; Config restera leur propriétaire process/`.env`.
## Futur, uniquement au besoin
Les capacités suivantes sont différées jusqu'à l'apparition de composants réels :

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-config-lib/USAGE.md -->
<!-- version: 5 -->
<!-- version: 6 -->
# Utilisation de ksp-config-lib
@@ -28,8 +28,10 @@ Les arguments compris par Config sont :
```text
--cfgpath=/path/to/config
--schemapath=/path/to/schemas
--filemap=cfg.composite.ksp-app-wallet-desk=my-wallet-desk.json
--filemap=cfg.std.logging=my-logging.json
--filemap=cfg.std.transport=my-transport.json
--filemap=cfg.std.wallet=my-wallet.json
```
`cfgpath` et `schemapath` ne sont jamais lus depuis JSON, `.env` ou une variable KSP : cette règle évite un bootstrap récursif.
@@ -140,6 +142,26 @@ Les scalaires `*_ms` restent des valeurs Config et sont convertis en `std::time:
La dépendance reste unidirectionnelle : Config connaît le contrat Transport pour le construire ; Transport ne connaît ni Config, ni `.env`, ni les variables KSP.
### 4.2 Résoudre le répertoire Wallet depuis Config
`std.wallet` conserve une racine globale et laisse un profil ajouter un sous-répertoire relatif :
```rust
let wallet = match engine.load_resolved_wallet_config(std::option::Option::None, &environment) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let root = wallet.wallets_directory();
let profile_subdirectory = wallet.wallets_subdirectory();
let effective = wallet.effective_wallets_directory();
let _ = (root, profile_subdirectory, effective);
```
`wallets_directory` accepte `${KSP_WALLETS_DIRECTORY:-wallets}` et une valeur relative est ancrée au current working directory du processus. `wallets_subdirectory` naccepte que des composants relatifs normaux : chemin absolu, `.` et `..` sont rejetés. Config résout/valide le chemin mais **ne crée pas** les répertoires et nénumère aucun `.kspwallet`; ces responsabilités appartiennent au consumer applicatif.
Les chemins Wallet refusent toute valeur `KSP_SECRET_*`. Les futurs `KSP_SECRET_WALLET_PASS_*` constituent un flux de secrets distinct et ne sont pas des champs de `std.wallet.json`.
## 5. Profils et composites
Pour un document standard profilé :
@@ -152,6 +174,19 @@ let profile = engine.load_resolved_profile(&file_id, std::option::Option::None);
Un composite référence les documents par `file_id`, jamais par filename. `load_resolved_composite(...)` conserve chaque `ResolvedConfigProfile` composant et sa provenance plutôt que d'aplatir plusieurs domaines dans une map ambiguë.
Lorsquun adapter runtime consomme un profil déjà choisi par un composite, utiliser lentrée dédiée afin de préserver `ConfigProfileSelectionSource::Composite` :
```rust
let component = match composite.component("wallet") {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(/* erreur applicative */),
};
let wallet = engine.resolve_wallet_config_profile(component.resolved(), &environment);
```
La même forme existe pour Logging via `resolve_logging_config_profile`. Le composite concret `cfg.composite.ksp-app-wallet-desk` référence actuellement `logging`, `transport` et `wallet`; Wallet Desk valide ces trois frontières au bootstrap.
## 6. Management de `std.logging.json`
Une application de management construit la façade à partir d'un moteur :

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/document.rs
// version: 6
// version: 7
/// A Config-managed JSON document that has passed syntax, schema and current semantic validation.
#[derive(Clone, Debug, PartialEq)]
@@ -266,6 +266,10 @@ fn validate_document_semantics(engine: &ConfigDocumentEngine, document: &ConfigJ
if let std::result::Result::Err(error) = composite_validation {
return std::result::Result::Err(error);
}
let wallet_validation = crate::validate_wallet_document_contract(document);
if let std::result::Result::Err(error) = wallet_validation {
return std::result::Result::Err(error);
}
if document.file_id().as_str() != crate::FILE_ID_STD_LOGGING {
return std::result::Result::Ok(());
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/lib.rs
// version: 14
// version: 15
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -9,8 +9,8 @@
//!
//! The `0.1.3` surface owns bootstrap roots, the logical file registry, JSON/JSON Schema validation, standard-document profiles, generic composites and
//! KSP/KSPB environment resolution through process + `.env` + fallback precedence. Resolved values preserve real/safe representations, sensitivity and
//! provenance. Standard Logging and HTTP Transport documents map explicitly to their runtime settings contracts, while the management surface provides
//! typed Logging mutation, safe environment reports, explicit privileged reveal calls and atomic JSON/`.env` persistence.
//! provenance. Standard Logging, HTTP Transport and Wallet documents map explicitly to their runtime consumer contracts, while the management surface
//! provides typed Logging mutation, safe environment reports, explicit privileged reveal calls and atomic JSON/`.env` persistence.
mod bootstrap;
mod composite;
@@ -25,6 +25,7 @@ mod profile;
mod registry;
mod sensitivity;
mod transport;
mod wallet;
/// Bootstrap argument used to replace the configuration document root.
pub use self::bootstrap::ARG_CFG_PATH;
@@ -138,6 +139,8 @@ 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 Wallet Desk composite configuration document.
pub use self::registry::DEFAULT_COMPOSITE_KSP_APP_WALLET_DESK_FILENAME;
/// 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.
@@ -148,16 +151,26 @@ pub use self::registry::DEFAULT_STD_LOGGING_SCHEMA_FILENAME;
pub use self::registry::DEFAULT_STD_TRANSPORT_FILENAME;
/// Default physical filename for the standard HTTP Transport JSON Schema document.
pub use self::registry::DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME;
/// Default physical filename for the standard Wallet configuration document.
pub use self::registry::DEFAULT_STD_WALLET_FILENAME;
/// Default physical filename for the standard Wallet JSON Schema document.
pub use self::registry::DEFAULT_STD_WALLET_SCHEMA_FILENAME;
/// Logical file identifier for the Wallet Desk composite configuration document.
pub use self::registry::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK;
/// 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 HTTP Transport JSON Schema document.
pub use self::registry::FILE_ID_SCHEMA_STD_TRANSPORT;
/// Logical file identifier for the standard Wallet JSON Schema document.
pub use self::registry::FILE_ID_SCHEMA_STD_WALLET;
/// Logical file identifier for the standard Logging configuration document.
pub use self::registry::FILE_ID_STD_LOGGING;
/// Logical file identifier for the standard HTTP Transport configuration document.
pub use self::registry::FILE_ID_STD_TRANSPORT;
/// Logical file identifier for the standard Wallet configuration document.
pub use self::registry::FILE_ID_STD_WALLET;
/// Sensitivity assigned to one Config value after environment resolution.
pub use self::sensitivity::ConfigSensitivity;
/// Provenance segment participating in one resolved Config value.
@@ -170,6 +183,8 @@ pub use self::sensitivity::ResolvedConfigJson;
pub use self::sensitivity::ResolvedConfigText;
/// Effective standard HTTP Transport configuration mapped to `ksp_onchain_transport_lib::HttpTransportSettings`.
pub use self::transport::ResolvedTransportConfig;
/// Effective standard Wallet configuration resolved to validated filesystem roots.
pub use self::wallet::ResolvedWalletConfig;
/// Validates composite document contract.
pub(crate) use self::composite::validate_composite_document_contract;
@@ -189,3 +204,5 @@ pub(crate) use self::profile::load_resolved_profile_with_source;
pub(crate) use self::profile::validate_document_profile_contract;
/// Executes the crate-internal build registry operation for the owning module.
pub(crate) use self::registry::build_registry;
/// Validates standard Wallet source path invariants before environment resolution.
pub(crate) use self::wallet::validate_wallet_document_contract;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/logging.rs
// version: 1
// version: 2
/// Effective standard Logging configuration resolved from Config and mapped to the Logging runtime contract.
#[derive(Clone, Eq, PartialEq)]
@@ -103,6 +103,24 @@ impl crate::ConfigDocumentEngine {
};
return resolve_logging_profile(&profile, environment);
}
/// Maps an already resolved standard Logging profile to the runtime Logging adapter while preserving its selection provenance.
///
/// This entry point is intended for profiles selected by a composite. The profile must reference `cfg.std.logging`.
pub fn resolve_logging_config_profile(
&self,
profile: &crate::ResolvedConfigProfile,
environment: &crate::ConfigEnvironment,
) -> ksp_core_lib::Result<ResolvedLoggingConfig> {
if profile.file_id().as_str() != crate::FILE_ID_STD_LOGGING {
return std::result::Result::Err(effective_error(profile, "resolved Config profile does not reference the standard Logging document"));
}
let descriptor = self.registry().descriptor(profile.file_id());
if let std::result::Result::Err(error) = descriptor {
return std::result::Result::Err(error);
}
return resolve_logging_profile(profile, environment);
}
}
#[derive(serde::Deserialize)]

View File

@@ -1,8 +1,10 @@
// file: crates/ksp-config-lib/src/registry.rs
// version: 7
// version: 8
/// Bootstrap argument used to replace a known Config filename mapping.
pub const ARG_FILE_MAP: &str = "--filemap";
/// Default physical filename for the Wallet Desk composite configuration document.
pub const DEFAULT_COMPOSITE_KSP_APP_WALLET_DESK_FILENAME: &str = "composite.ksp-app-wallet-desk.json";
/// Default physical filename for the generic composite JSON Schema document.
pub const DEFAULT_COMPOSITE_SCHEMA_FILENAME: &str = "composite.schema.json";
/// Default physical filename for the standard Logging configuration document.
@@ -13,16 +15,26 @@ pub const DEFAULT_STD_LOGGING_SCHEMA_FILENAME: &str = "std.logging.schema.json";
pub const DEFAULT_STD_TRANSPORT_FILENAME: &str = "std.transport.json";
/// Default physical filename for the standard HTTP Transport JSON Schema document.
pub const DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME: &str = "std.transport.schema.json";
/// Default physical filename for the standard Wallet configuration document.
pub const DEFAULT_STD_WALLET_FILENAME: &str = "std.wallet.json";
/// Default physical filename for the standard Wallet JSON Schema document.
pub const DEFAULT_STD_WALLET_SCHEMA_FILENAME: &str = "std.wallet.schema.json";
/// Logical file identifier for the Wallet Desk composite configuration document.
pub const FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK: &str = "cfg.composite.ksp-app-wallet-desk";
/// Logical file identifier for the generic composite JSON Schema document.
pub const FILE_ID_SCHEMA_COMPOSITE: &str = "schema.composite";
/// 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 standard HTTP Transport JSON Schema document.
pub const FILE_ID_SCHEMA_STD_TRANSPORT: &str = "schema.std.transport";
/// Logical file identifier for the standard Wallet JSON Schema document.
pub const FILE_ID_SCHEMA_STD_WALLET: &str = "schema.std.wallet";
/// Logical file identifier for the standard Logging configuration document.
pub const FILE_ID_STD_LOGGING: &str = "cfg.std.logging";
/// Logical file identifier for the standard HTTP Transport configuration document.
pub const FILE_ID_STD_TRANSPORT: &str = "cfg.std.transport";
/// Logical file identifier for the standard Wallet configuration document.
pub const FILE_ID_STD_WALLET: &str = "cfg.std.wallet";
/// Stable logical identifier for a Config-managed file.
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
@@ -128,6 +140,22 @@ pub struct ConfigFileRegistry {
impl ConfigFileRegistry {
/// Creates the registry containing the KSP default file mappings known in the current release.
pub fn defaults() -> ksp_core_lib::Result<Self> {
let composite = ConfigFileDescriptor::new(
FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK,
ConfigFileKind::Config,
DEFAULT_COMPOSITE_KSP_APP_WALLET_DESK_FILENAME,
std::option::Option::Some(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),
};
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),
};
let logging = ConfigFileDescriptor::new(
FILE_ID_STD_LOGGING,
ConfigFileKind::Config,
@@ -160,13 +188,23 @@ impl ConfigFileRegistry {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
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 {
let wallet = ConfigFileDescriptor::new(
FILE_ID_STD_WALLET,
ConfigFileKind::Config,
DEFAULT_STD_WALLET_FILENAME,
std::option::Option::Some(FILE_ID_SCHEMA_STD_WALLET),
);
let wallet = match wallet {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return crate::build_registry([logging, logging_schema, transport, transport_schema, composite_schema]);
let wallet_schema =
ConfigFileDescriptor::new(FILE_ID_SCHEMA_STD_WALLET, ConfigFileKind::Schema, DEFAULT_STD_WALLET_SCHEMA_FILENAME, std::option::Option::None);
let wallet_schema = match wallet_schema {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return crate::build_registry([composite, composite_schema, logging, logging_schema, transport, transport_schema, wallet, wallet_schema]);
}
/// Creates the default registry and applies repeatable `--filemap=<file_id>=<filename>` overrides from raw process arguments.

View File

@@ -0,0 +1,290 @@
// file: crates/ksp-config-lib/src/wallet.rs
// version: 1
/// Effective standard Wallet configuration resolved from Config.
#[derive(Clone, Eq, PartialEq)]
pub struct ResolvedWalletConfig {
effective: crate::ResolvedConfigJson,
effective_wallets_directory: std::path::PathBuf,
file_id: crate::ConfigFileId,
profile_id: String,
selection_source: crate::ConfigProfileSelectionSource,
source_path: std::path::PathBuf,
wallets_directory: std::path::PathBuf,
wallets_subdirectory: std::option::Option<std::path::PathBuf>,
}
impl ResolvedWalletConfig {
/// Returns the detailed environment-resolved effective Config view.
#[must_use]
pub const fn effective(&self) -> &crate::ResolvedConfigJson {
return &self.effective;
}
/// Returns the effective Wallet directory after applying the optional profile subdirectory.
#[must_use]
pub fn effective_wallets_directory(&self) -> &std::path::Path {
return self.effective_wallets_directory.as_path();
}
/// Returns the logical Config document identifier used by this runtime configuration.
#[must_use]
pub const fn file_id(&self) -> &crate::ConfigFileId {
return &self.file_id;
}
/// Returns the selected standard Wallet profile identifier.
#[must_use]
pub fn profile_id(&self) -> &str {
return self.profile_id.as_str();
}
/// Returns the source that selected the standard Wallet profile.
#[must_use]
pub const fn selection_source(&self) -> crate::ConfigProfileSelectionSource {
return self.selection_source;
}
/// Returns the physical source Config document path.
#[must_use]
pub fn source_path(&self) -> &std::path::Path {
return self.source_path.as_path();
}
/// Returns the resolved global Wallet root directory.
#[must_use]
pub fn wallets_directory(&self) -> &std::path::Path {
return self.wallets_directory.as_path();
}
/// Returns the validated optional relative Wallet subdirectory selected by the profile.
#[must_use]
pub fn wallets_subdirectory(&self) -> std::option::Option<&std::path::Path> {
return self.wallets_subdirectory.as_deref();
}
}
impl std::fmt::Debug for ResolvedWalletConfig {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter
.debug_struct("ResolvedWalletConfig")
.field("effective", &self.effective)
.field("effective_wallets_directory", &self.effective_wallets_directory)
.field("file_id", &self.file_id)
.field("profile_id", &self.profile_id)
.field("selection_source", &self.selection_source)
.field("source_path", &self.source_path)
.field("wallets_directory", &self.wallets_directory)
.field("wallets_subdirectory", &self.wallets_subdirectory)
.finish();
}
}
impl crate::ConfigDocumentEngine {
/// Loads the standard Wallet document, selects a profile, resolves environment placeholders and validates the effective directory contract.
///
/// `requested_profile = None` uses the document `default_profile`; `Some(profile_id)` requests an explicit profile. `wallets_directory` may be absolute
/// or relative to the process current working directory. `wallets_subdirectory` is always relative and may contain nested normal path components only.
pub fn load_resolved_wallet_config(
&self,
requested_profile: std::option::Option<&str>,
environment: &crate::ConfigEnvironment,
) -> ksp_core_lib::Result<ResolvedWalletConfig> {
let file_id = crate::ConfigFileId::new(crate::FILE_ID_STD_WALLET);
let file_id = match file_id {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let profile = self.load_resolved_profile(&file_id, requested_profile);
let profile = match profile {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return resolve_wallet_profile(&profile, environment);
}
/// Maps an already resolved standard Wallet profile to the runtime Wallet Config adapter.
///
/// This entry point is intended for profiles selected by a composite and preserves their original selection source instead of reclassifying the profile as
/// an explicit selection. The profile must reference `cfg.std.wallet`.
pub fn resolve_wallet_config_profile(
&self,
profile: &crate::ResolvedConfigProfile,
environment: &crate::ConfigEnvironment,
) -> ksp_core_lib::Result<ResolvedWalletConfig> {
if profile.file_id().as_str() != crate::FILE_ID_STD_WALLET {
return std::result::Result::Err(effective_error(profile, "resolved Config profile does not reference the standard Wallet document"));
}
let descriptor = self.registry().descriptor(profile.file_id());
if let std::result::Result::Err(error) = descriptor {
return std::result::Result::Err(error);
}
return resolve_wallet_profile(profile, environment);
}
}
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct EffectiveWalletSource {
format_version: u32,
profile_id: String,
wallets_directory: String,
wallets_subdirectory: std::option::Option<String>,
}
/// Validates standard Wallet source path invariants that are meaningful before environment resolution.
pub(crate) fn validate_wallet_document_contract(document: &crate::ConfigJsonDocument) -> ksp_core_lib::Result<()> {
if document.file_id().as_str() != crate::FILE_ID_STD_WALLET {
return std::result::Result::Ok(());
}
let profiles = document.value().get("profiles").and_then(serde_json::Value::as_array);
let profiles = match profiles {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Ok(()),
};
for (profile_index, profile) in profiles.iter().enumerate() {
let subdirectory = profile.get("wallets_subdirectory").and_then(serde_json::Value::as_str);
if let std::option::Option::Some(value) = subdirectory {
let validation = validate_relative_subdirectory(value);
if let std::result::Result::Err(_) = validation {
return std::result::Result::Err(
wallet_document_error(document, "wallets_subdirectory must contain only relative normal path components")
.with_context("profile_index", profile_index.to_string()),
);
}
}
}
return std::result::Result::Ok(());
}
fn resolve_wallet_profile(profile: &crate::ResolvedConfigProfile, environment: &crate::ConfigEnvironment) -> ksp_core_lib::Result<ResolvedWalletConfig> {
let effective = profile.resolve_effective_environment_detailed(environment);
let effective = match effective {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if effective.sensitivity().is_secret() {
return std::result::Result::Err(effective_error(profile, "standard Wallet configuration must not consume Secret environment values"));
}
let source = serde_json::from_value::<EffectiveWalletSource>(effective.value().clone());
let source = match source {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(
effective_error(profile, "effective Wallet Config cannot be decoded into the runtime adapter contract").with_source(error),
);
},
};
if source.format_version != 1 {
return std::result::Result::Err(effective_error(profile, "effective Wallet format_version is unsupported"));
}
if source.profile_id != profile.profile_id() {
return std::result::Result::Err(effective_error(profile, "effective Wallet profile_id does not match the selected profile"));
}
let wallets_directory = resolve_wallets_directory(source.wallets_directory.as_str(), profile);
let wallets_directory = match wallets_directory {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let wallets_subdirectory = resolve_wallets_subdirectory(source.wallets_subdirectory.as_deref(), profile);
let wallets_subdirectory = match wallets_subdirectory {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let effective_wallets_directory = match &wallets_subdirectory {
std::option::Option::Some(value) => wallets_directory.join(value),
std::option::Option::None => wallets_directory.clone(),
};
return std::result::Result::Ok(ResolvedWalletConfig {
effective,
effective_wallets_directory,
file_id: profile.file_id().clone(),
profile_id: profile.profile_id().to_owned(),
selection_source: profile.selection_source(),
source_path: profile.path().to_path_buf(),
wallets_directory,
wallets_subdirectory,
});
}
fn resolve_wallets_directory(value: &str, profile: &crate::ResolvedConfigProfile) -> ksp_core_lib::Result<std::path::PathBuf> {
if value.trim().is_empty() {
return std::result::Result::Err(effective_field_error(profile, "wallets_directory", "effective Wallet wallets_directory must not be empty"));
}
let path = std::path::PathBuf::from(value);
if path.is_absolute() {
return std::result::Result::Ok(path);
}
let current_directory = std::env::current_dir();
return match current_directory {
std::result::Result::Ok(current_directory) => std::result::Result::Ok(current_directory.join(path)),
std::result::Result::Err(error) => std::result::Result::Err(
effective_field_error(profile, "wallets_directory", "process current working directory cannot be resolved").with_source(error),
),
};
}
fn resolve_wallets_subdirectory(
value: std::option::Option<&str>,
profile: &crate::ResolvedConfigProfile,
) -> ksp_core_lib::Result<std::option::Option<std::path::PathBuf>> {
let value = match value {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Ok(std::option::Option::None),
};
let validation = validate_relative_subdirectory(value);
if let std::result::Result::Err(()) = validation {
return std::result::Result::Err(effective_field_error(
profile,
"wallets_subdirectory",
"effective Wallet wallets_subdirectory must contain only relative normal path components",
));
}
return std::result::Result::Ok(std::option::Option::Some(std::path::PathBuf::from(value)));
}
fn validate_relative_subdirectory(value: &str) -> std::result::Result<(), ()> {
if value.trim().is_empty() {
return std::result::Result::Err(());
}
let path = std::path::Path::new(value);
if path.is_absolute() {
return std::result::Result::Err(());
}
let mut component_count: usize = 0;
for component in path.components() {
match component {
std::path::Component::Normal(_) => component_count += 1,
std::path::Component::CurDir | std::path::Component::ParentDir | std::path::Component::RootDir | std::path::Component::Prefix(_) => {
return std::result::Result::Err(());
},
}
}
if component_count == 0 {
return std::result::Result::Err(());
}
return std::result::Result::Ok(());
}
fn wallet_document_error(document: &crate::ConfigJsonDocument, reason: &'static str) -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_DOCUMENT_SEMANTIC_INVALID, "standard Wallet Config 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 effective_field_error(profile: &crate::ResolvedConfigProfile, field: &'static str, reason: &'static str) -> ksp_core_lib::Error {
return effective_error(profile, reason).with_context("field", field);
}
fn effective_error(profile: &crate::ResolvedConfigProfile, reason: &'static str) -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_EFFECTIVE_CONFIG_INVALID, "effective Config cannot be mapped to the requested runtime contract")
.with_context("file_id", profile.file_id().as_str())
.with_context("path", profile.path().to_string_lossy().into_owned())
.with_context("profile_id", profile.profile_id())
.with_context("reason", reason);
}
#[cfg(test)]
#[path = "../unit_tests/wallet.rs"]
mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/tests/ownership.rs
// version: 3
// version: 4
//! Workspace ownership audits for KSP application configuration boundaries.
@@ -241,11 +241,14 @@ fn workspace_crates_do_not_hardcode_config_managed_physical_files() {
};
for token in [
"\".env\"",
"\"composite.ksp-app-wallet-desk.json\"",
"\"composite.schema.json\"",
"\"std.logging.json\"",
"\"std.logging.schema.json\"",
"\"std.transport.json\"",
"\"std.transport.schema.json\"",
"\"composite.schema.json\"",
"\"std.wallet.json\"",
"\"std.wallet.schema.json\"",
] {
assert!(!source.contains(token), "{} hardcodes Config-managed physical resource {token}; use ksp-config-lib contracts", rust_file.display());
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/tests/public_api.rs
// version: 17
// version: 19
//! Integration tests for the public `ksp-config-lib` bootstrap, registry, JSON/profile/composite, environment-resolution, sensitivity,
//! Logging/Transport adapters and management contracts.
@@ -80,18 +80,19 @@ fn registry_descriptor_inventory_is_available_from_crate_root() {
assert!(registry.is_ok(), "public registry should remain constructible: {registry:?}");
if let std::result::Result::Ok(registry) = registry {
let descriptors: std::vec::Vec<&ksp_config_lib::ConfigFileDescriptor> = registry.descriptors().collect();
assert_eq!(descriptors.len(), 5);
assert_eq!(descriptors[0].file_id().as_str(), ksp_config_lib::FILE_ID_STD_LOGGING);
assert_eq!(descriptors[0].kind(), ksp_config_lib::ConfigFileKind::Config);
assert_eq!(descriptors[1].file_id().as_str(), ksp_config_lib::FILE_ID_STD_TRANSPORT);
assert_eq!(descriptors[1].kind(), ksp_config_lib::ConfigFileKind::Config);
assert_eq!(descriptors[2].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_COMPOSITE);
assert_eq!(descriptors[3].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_LOGGING);
assert_eq!(descriptors[4].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_TRANSPORT);
let schema_file_id = descriptors[1].schema_file_id();
assert!(schema_file_id.is_some(), "public Transport descriptor should preserve schema association");
assert_eq!(descriptors.len(), 8);
assert_eq!(descriptors[0].file_id().as_str(), ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK);
assert_eq!(descriptors[1].file_id().as_str(), ksp_config_lib::FILE_ID_STD_LOGGING);
assert_eq!(descriptors[2].file_id().as_str(), ksp_config_lib::FILE_ID_STD_TRANSPORT);
assert_eq!(descriptors[3].file_id().as_str(), ksp_config_lib::FILE_ID_STD_WALLET);
assert_eq!(descriptors[4].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_COMPOSITE);
assert_eq!(descriptors[5].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_LOGGING);
assert_eq!(descriptors[6].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_TRANSPORT);
assert_eq!(descriptors[7].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_WALLET);
let schema_file_id = descriptors[3].schema_file_id();
assert!(schema_file_id.is_some(), "public Wallet descriptor should preserve schema association");
if let std::option::Option::Some(schema_file_id) = schema_file_id {
assert_eq!(schema_file_id.as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_TRANSPORT);
assert_eq!(schema_file_id.as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_WALLET);
}
}
}
@@ -198,7 +199,8 @@ fn sensitivity_and_safe_resolution_contracts_are_available_from_crate_root() {
#[test]
fn logging_adapter_contract_is_available_from_crate_root() {
let adapter = ksp_config_lib::ConfigDocumentEngine::load_resolved_logging_config;
let _ = adapter;
let composite_adapter = ksp_config_lib::ConfigDocumentEngine::resolve_logging_config_profile;
let _ = (adapter, composite_adapter);
assert_eq!(ksp_config_lib::ERROR_CODE_EFFECTIVE_CONFIG_INVALID.domain(), "config");
assert_eq!(ksp_config_lib::ERROR_CODE_EFFECTIVE_CONFIG_INVALID.code(), "effective_config_invalid");
assert!(std::mem::size_of::<ksp_config_lib::ResolvedLoggingConfig>() > 0);
@@ -254,3 +256,16 @@ fn transport_adapter_contract_is_available_from_crate_root() {
assert_eq!(ksp_config_lib::DEFAULT_STD_TRANSPORT_FILENAME, "std.transport.json");
assert_eq!(ksp_config_lib::DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME, "std.transport.schema.json");
}
#[test]
fn wallet_adapter_contract_is_available_from_crate_root() {
let _loader = ksp_config_lib::ConfigDocumentEngine::load_resolved_wallet_config;
let _composite_loader = ksp_config_lib::ConfigDocumentEngine::resolve_wallet_config_profile;
assert!(std::mem::size_of::<ksp_config_lib::ResolvedWalletConfig>() > 0);
assert_eq!(ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK, "cfg.composite.ksp-app-wallet-desk");
assert_eq!(ksp_config_lib::FILE_ID_STD_WALLET, "cfg.std.wallet");
assert_eq!(ksp_config_lib::FILE_ID_SCHEMA_STD_WALLET, "schema.std.wallet");
assert_eq!(ksp_config_lib::DEFAULT_COMPOSITE_KSP_APP_WALLET_DESK_FILENAME, "composite.ksp-app-wallet-desk.json");
assert_eq!(ksp_config_lib::DEFAULT_STD_WALLET_FILENAME, "std.wallet.json");
assert_eq!(ksp_config_lib::DEFAULT_STD_WALLET_SCHEMA_FILENAME, "std.wallet.schema.json");
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/unit_tests/registry.rs
// version: 6
// version: 7
#[test]
fn descriptors_expose_complete_registry_in_deterministic_file_id_order() {
@@ -7,29 +7,21 @@ fn descriptors_expose_complete_registry_in_deterministic_file_id_order() {
assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
if let std::result::Result::Ok(registry) = registry {
let descriptors: std::vec::Vec<&crate::ConfigFileDescriptor> = registry.descriptors().collect();
assert_eq!(descriptors.len(), 5);
assert_eq!(descriptors[0].file_id().as_str(), crate::FILE_ID_STD_LOGGING);
assert_eq!(descriptors[0].kind(), crate::ConfigFileKind::Config);
assert_eq!(descriptors[0].filename(), std::path::Path::new(crate::DEFAULT_STD_LOGGING_FILENAME));
let logging_schema_file_id = descriptors[0].schema_file_id();
assert!(logging_schema_file_id.is_some(), "logging descriptor should expose its validation schema");
if let std::option::Option::Some(schema_file_id) = logging_schema_file_id {
assert_eq!(schema_file_id.as_str(), crate::FILE_ID_SCHEMA_STD_LOGGING);
}
assert_eq!(descriptors[1].file_id().as_str(), crate::FILE_ID_STD_TRANSPORT);
assert_eq!(descriptors[1].kind(), crate::ConfigFileKind::Config);
assert_eq!(descriptors[1].filename(), std::path::Path::new(crate::DEFAULT_STD_TRANSPORT_FILENAME));
let transport_schema_file_id = descriptors[1].schema_file_id();
assert!(transport_schema_file_id.is_some(), "transport descriptor should expose its validation schema");
if let std::option::Option::Some(schema_file_id) = transport_schema_file_id {
assert_eq!(schema_file_id.as_str(), crate::FILE_ID_SCHEMA_STD_TRANSPORT);
}
assert_eq!(descriptors[2].file_id().as_str(), crate::FILE_ID_SCHEMA_COMPOSITE);
assert_eq!(descriptors[2].kind(), crate::ConfigFileKind::Schema);
assert_eq!(descriptors[3].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_LOGGING);
assert_eq!(descriptors[3].kind(), crate::ConfigFileKind::Schema);
assert_eq!(descriptors[4].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_TRANSPORT);
assert_eq!(descriptors[4].kind(), crate::ConfigFileKind::Schema);
assert_eq!(descriptors.len(), 8);
assert_eq!(descriptors[0].file_id().as_str(), crate::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK);
assert_eq!(descriptors[0].filename(), std::path::Path::new(crate::DEFAULT_COMPOSITE_KSP_APP_WALLET_DESK_FILENAME));
assert_eq!(descriptors[0].schema_file_id().map(crate::ConfigFileId::as_str), std::option::Option::Some(crate::FILE_ID_SCHEMA_COMPOSITE));
assert_eq!(descriptors[1].file_id().as_str(), crate::FILE_ID_STD_LOGGING);
assert_eq!(descriptors[2].file_id().as_str(), crate::FILE_ID_STD_TRANSPORT);
assert_eq!(descriptors[3].file_id().as_str(), crate::FILE_ID_STD_WALLET);
assert_eq!(descriptors[3].filename(), std::path::Path::new(crate::DEFAULT_STD_WALLET_FILENAME));
assert_eq!(descriptors[3].schema_file_id().map(crate::ConfigFileId::as_str), std::option::Option::Some(crate::FILE_ID_SCHEMA_STD_WALLET));
assert_eq!(descriptors[4].file_id().as_str(), crate::FILE_ID_SCHEMA_COMPOSITE);
assert_eq!(descriptors[5].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_LOGGING);
assert_eq!(descriptors[6].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_TRANSPORT);
assert_eq!(descriptors[7].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_WALLET);
assert!(descriptors[0..4].iter().all(|descriptor| return descriptor.kind() == crate::ConfigFileKind::Config));
assert!(descriptors[4..8].iter().all(|descriptor| return descriptor.kind() == crate::ConfigFileKind::Schema));
}
}
@@ -118,6 +110,39 @@ fn defaults_register_transport_document_and_schema_with_distinct_roots() {
}
}
#[test]
fn defaults_register_wallet_document_schema_and_wallet_desk_composite() {
let registry = crate::ConfigFileRegistry::defaults();
assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
if let std::result::Result::Ok(registry) = registry {
let wallet_id = crate::ConfigFileId::new(crate::FILE_ID_STD_WALLET);
let wallet_schema_id = crate::ConfigFileId::new(crate::FILE_ID_SCHEMA_STD_WALLET);
let composite_id = crate::ConfigFileId::new(crate::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK);
assert!(wallet_id.is_ok());
assert!(wallet_schema_id.is_ok());
assert!(composite_id.is_ok());
if let (std::result::Result::Ok(wallet_id), std::result::Result::Ok(wallet_schema_id), std::result::Result::Ok(composite_id)) =
(wallet_id, wallet_schema_id, composite_id)
{
let wallet = registry.descriptor(&wallet_id);
let wallet_schema = registry.descriptor(&wallet_schema_id);
let composite = registry.descriptor(&composite_id);
assert!(wallet.is_ok());
assert!(wallet_schema.is_ok());
assert!(composite.is_ok());
if let (std::result::Result::Ok(wallet), std::result::Result::Ok(wallet_schema), std::result::Result::Ok(composite)) =
(wallet, wallet_schema, composite)
{
assert_eq!(wallet.filename(), std::path::Path::new(crate::DEFAULT_STD_WALLET_FILENAME));
assert_eq!(wallet.schema_file_id(), std::option::Option::Some(&wallet_schema_id));
assert_eq!(wallet_schema.filename(), std::path::Path::new(crate::DEFAULT_STD_WALLET_SCHEMA_FILENAME));
assert_eq!(composite.filename(), std::path::Path::new(crate::DEFAULT_COMPOSITE_KSP_APP_WALLET_DESK_FILENAME));
assert_eq!(composite.schema_file_id().map(crate::ConfigFileId::as_str), std::option::Option::Some(crate::FILE_ID_SCHEMA_COMPOSITE));
}
}
}
}
#[test]
fn resolve_path_uses_descriptor_kind_to_select_bootstrap_root() {
let registry = crate::ConfigFileRegistry::defaults();

View File

@@ -0,0 +1,200 @@
// file: crates/ksp-config-lib/unit_tests/wallet.rs
// version: 1
#[test]
fn default_wallet_config_resolves_global_root_without_subdirectory() {
let engine = workspace_engine();
assert!(engine.is_ok(), "workspace engine should be constructible: {engine:?}");
if let std::result::Result::Ok(engine) = engine {
let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
let resolved = engine.load_resolved_wallet_config(std::option::Option::Some("default"), &environment);
assert!(resolved.is_ok(), "default Wallet profile should resolve: {resolved:?}");
if let std::result::Result::Ok(resolved) = resolved {
assert_eq!(resolved.file_id().as_str(), crate::FILE_ID_STD_WALLET);
assert_eq!(resolved.profile_id(), "default");
assert!(resolved.wallets_subdirectory().is_none());
assert!(resolved.wallets_directory().is_absolute());
assert_eq!(resolved.effective_wallets_directory(), resolved.wallets_directory());
}
}
}
#[test]
fn wallet_profile_appends_relative_subdirectory() {
let engine = workspace_engine();
assert!(engine.is_ok(), "workspace engine should be constructible: {engine:?}");
if let std::result::Result::Ok(engine) = engine {
let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
let resolved = engine.load_resolved_wallet_config(std::option::Option::Some("tests"), &environment);
assert!(resolved.is_ok(), "tests Wallet profile should resolve: {resolved:?}");
if let std::result::Result::Ok(resolved) = resolved {
assert_eq!(resolved.wallets_subdirectory(), std::option::Option::Some(std::path::Path::new("tests")));
let expected = resolved.wallets_directory().join("tests");
assert_eq!(resolved.effective_wallets_directory(), expected.as_path());
}
}
}
#[test]
fn wallet_directory_environment_override_uses_config_precedence() {
let engine = workspace_engine();
assert!(engine.is_ok(), "workspace engine should be constructible: {engine:?}");
if let std::result::Result::Ok(engine) = engine {
let mut process = std::collections::BTreeMap::<String, String>::new();
process.insert("KSP_WALLETS_DIRECTORY".to_owned(), "process-wallets".to_owned());
let mut dotenv = std::collections::BTreeMap::<String, String>::new();
dotenv.insert("KSP_WALLETS_DIRECTORY".to_owned(), "dotenv-wallets".to_owned());
let environment = crate::ConfigEnvironment::from_maps(process, dotenv);
let resolved = engine.load_resolved_wallet_config(std::option::Option::Some("default"), &environment);
assert!(resolved.is_ok(), "Wallet environment override should resolve: {resolved:?}");
if let std::result::Result::Ok(resolved) = resolved {
assert!(resolved.wallets_directory().ends_with("process-wallets"));
assert!(resolved.effective().provenance_at("/wallets_directory").is_some());
}
}
}
#[test]
fn unsafe_wallet_subdirectories_are_rejected() {
for (name, value) in [("absolute", "/escape"), ("parent", "../escape"), ("nested-parent", "tests/../escape"), ("current", "./tests")] {
let fixture = fixture_roots(name);
let prepared = prepare_wallet_fixture(&fixture, value);
assert!(prepared.is_ok(), "wallet fixture should be writable: {prepared:?}");
if prepared.is_ok() {
let engine = fixture_engine(&fixture);
let file_id = crate::ConfigFileId::new(crate::FILE_ID_STD_WALLET);
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(), "unsafe wallets_subdirectory must be rejected: {name} {result:?}");
if let std::result::Result::Err(error) = result {
assert_eq!(error.code(), crate::ERROR_CODE_DOCUMENT_SEMANTIC_INVALID);
}
}
}
cleanup_fixture(&fixture);
}
}
#[test]
fn composite_selected_wallet_profile_preserves_composite_provenance() {
let engine = workspace_engine();
assert!(engine.is_ok(), "workspace engine should be constructible: {engine:?}");
if let std::result::Result::Ok(engine) = engine {
let composite_file_id = crate::ConfigFileId::new(crate::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK);
assert!(composite_file_id.is_ok(), "Wallet Desk composite file_id should be valid: {composite_file_id:?}");
if let std::result::Result::Ok(composite_file_id) = composite_file_id {
let composite = engine.load_resolved_composite(&composite_file_id, std::option::Option::None);
assert!(composite.is_ok(), "Wallet Desk composite should resolve: {composite:?}");
if let std::result::Result::Ok(composite) = composite {
let component = composite.component("wallet");
assert!(component.is_some(), "Wallet Desk composite should expose wallet component");
if let std::option::Option::Some(component) = component {
let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
let resolved = engine.resolve_wallet_config_profile(component.resolved(), &environment);
assert!(resolved.is_ok(), "composite Wallet profile should map through the runtime adapter: {resolved:?}");
if let std::result::Result::Ok(resolved) = resolved {
assert_eq!(resolved.profile_id(), "default");
assert_eq!(resolved.selection_source(), crate::ConfigProfileSelectionSource::Composite);
}
}
}
}
}
}
#[test]
fn secret_wallet_directory_placeholder_is_rejected_by_effective_adapter() {
let fixture = fixture_roots("secret-root");
let prepared = prepare_wallet_root_fixture(&fixture, "${KSP_SECRET_WALLET_DIRECTORY}", std::option::Option::None);
assert!(prepared.is_ok(), "wallet fixture should be writable: {prepared:?}");
if prepared.is_ok() {
let engine = fixture_engine(&fixture);
assert!(engine.is_ok(), "fixture engine should be constructible: {engine:?}");
if let std::result::Result::Ok(engine) = engine {
let mut process = std::collections::BTreeMap::<String, String>::new();
process.insert("KSP_SECRET_WALLET_DIRECTORY".to_owned(), "hidden-wallets".to_owned());
let environment = crate::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new());
let result = engine.load_resolved_wallet_config(std::option::Option::Some("default"), &environment);
assert!(result.is_err(), "Wallet path Config must reject Secret environment sensitivity: {result:?}");
if let std::result::Result::Err(error) = result {
assert_eq!(error.code(), crate::ERROR_CODE_EFFECTIVE_CONFIG_INVALID);
}
}
}
cleanup_fixture(&fixture);
}
fn workspace_engine() -> ksp_core_lib::Result<crate::ConfigDocumentEngine> {
let root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
let bootstrap = crate::ConfigBootstrapOptions::from_paths(root.join("config"), root.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 = crate::ConfigFileRegistry::defaults();
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 = crate::ConfigFileRegistry::defaults();
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 prepare_wallet_fixture(fixture: &FixtureRoots, subdirectory: &str) -> std::io::Result<()> {
return prepare_wallet_root_fixture(fixture, "wallets", std::option::Option::Some(subdirectory));
}
fn prepare_wallet_root_fixture(fixture: &FixtureRoots, root: &str, subdirectory: std::option::Option<&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 = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
let schema = std::fs::copy(workspace.join("config/schemas/std.wallet.schema.json"), fixture.schemas.join(crate::DEFAULT_STD_WALLET_SCHEMA_FILENAME));
if let std::result::Result::Err(error) = schema {
return std::result::Result::Err(error);
}
let profile = match subdirectory {
std::option::Option::Some(value) => format!(r#"{{"profile_id":"default","wallets_subdirectory":"{value}"}}"#),
std::option::Option::None => "{\"profile_id\":\"default\"}".to_owned(),
};
let source = format!(r#"{{"format_version":1,"wallets_directory":"{root}","default_profile":"default","profiles":[{profile}]}}"#);
return std::fs::write(fixture.config.join(crate::DEFAULT_STD_WALLET_FILENAME), source);
}
fn fixture_roots(name: &str) -> FixtureRoots {
let root = std::env::temp_dir().join(format!("ksp-config-wallet-{name}-{}", std::process::id()));
return FixtureRoots { config: root.join("config"), root: root.clone(), schemas: root.join("schemas") };
}
fn cleanup_fixture(fixture: &FixtureRoots) {
let removal = std::fs::remove_dir_all(fixture.root.as_path());
if let std::result::Result::Err(error) = removal
&& error.kind() != std::io::ErrorKind::NotFound
{
eprintln!("unable to cleanup Config Wallet fixture {}: {error}", fixture.root.display());
}
}
struct FixtureRoots {
config: std::path::PathBuf,
root: std::path::PathBuf,
schemas: std::path::PathBuf,
}