490 lines
26 KiB
Markdown
490 lines
26 KiB
Markdown
<!-- file: crates/ksp-config-lib/USAGE.md -->
|
||
<!-- version: 18 -->
|
||
|
||
# Utilisation de ksp-config-lib
|
||
|
||
## 1. Bootstrap et moteur documentaire
|
||
|
||
Config doit interpréter ses propres arguments de bootstrap avant toute lecture de document :
|
||
|
||
```rust
|
||
let args: std::vec::Vec<std::ffi::OsString> = std::env::args_os().collect();
|
||
|
||
let bootstrap = match ksp_config_lib::ConfigBootstrapOptions::from_args(args.as_slice()) {
|
||
std::result::Result::Ok(value) => value,
|
||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||
};
|
||
|
||
let registry = match ksp_config_lib::ConfigFileRegistry::from_args(args.as_slice()) {
|
||
std::result::Result::Ok(value) => value,
|
||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||
};
|
||
|
||
let engine = ksp_config_lib::ConfigDocumentEngine::new(bootstrap, registry);
|
||
```
|
||
|
||
Les arguments compris par Config sont :
|
||
|
||
```text
|
||
--cfgpath=/path/to/config
|
||
--schemapath=/path/to/schemas
|
||
--filemap=cfg.composite.ksp-app-raw-transaction-ingest-desk=my-raw-ingest-desk.json
|
||
--filemap=cfg.composite.ksp-app-solprices-desk=my-solprices-desk.json
|
||
--filemap=cfg.composite.ksp-app-wallet-desk=my-wallet-desk.json
|
||
--filemap=cfg.std.logging=my-logging.json
|
||
--filemap=cfg.std.offchain_transport=my-offchain-transport.json
|
||
--filemap=cfg.std.store=my-store.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.
|
||
|
||
### 1.1 Inventorier les fichiers enregistrés
|
||
|
||
Le registre expose une vue read-only déterministe des descripteurs connus :
|
||
|
||
```rust
|
||
for descriptor in registry.descriptors() {
|
||
let file_id = descriptor.file_id().as_str();
|
||
let kind = descriptor.kind();
|
||
let filename = descriptor.filename();
|
||
let schema_file_id = descriptor.schema_file_id();
|
||
let _ = (file_id, kind, filename, schema_file_id);
|
||
}
|
||
```
|
||
|
||
L'ordre est celui des `file_id`. La vue reflète les éventuels overrides `--filemap` déjà appliqués tout en conservant le kind et l'association de schema. Elle permet notamment à une application de management de construire sa liste de documents/schemas sans dupliquer le registre dans sa propre couche.
|
||
|
||
## 2. Charger et valider un document connu
|
||
|
||
Les consumers utilisent un `file_id` logique :
|
||
|
||
```rust
|
||
let file_id = match ksp_config_lib::ConfigFileId::new(ksp_config_lib::FILE_ID_STD_LOGGING) {
|
||
std::result::Result::Ok(value) => value,
|
||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||
};
|
||
|
||
let document = engine.load_validated_document(&file_id);
|
||
```
|
||
|
||
Le moteur résout le path physique via le registre, charge le schema associé, valide le schema lui-même, valide l'instance puis applique les invariants sémantiques KSP.
|
||
|
||
## 3. Environnement effectif
|
||
|
||
`ConfigEnvironment::load()` capture les variables process KSP/KSPB et lit `./.env` :
|
||
|
||
```rust
|
||
let environment = match ksp_config_lib::ConfigEnvironment::load() {
|
||
std::result::Result::Ok(value) => value,
|
||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||
};
|
||
```
|
||
|
||
La priorité est :
|
||
|
||
```text
|
||
process environment > .env > fallback > missing
|
||
```
|
||
|
||
Une chaîne vide explicitement présente est une valeur définie ; elle ne provoque pas l'utilisation du fallback.
|
||
|
||
Exemples de placeholders :
|
||
|
||
```text
|
||
${KSP_LOGS_DIRECTORY}
|
||
${KSP_LOGS_DIRECTORY:-logs}
|
||
```
|
||
|
||
Pour conserver la sensibilité et la provenance, préférer les variantes détaillées :
|
||
|
||
```rust
|
||
let resolved = environment.resolve_text_detailed("${KSP_SECRET_EXAMPLE}");
|
||
```
|
||
|
||
`ResolvedConfigText` / `ResolvedConfigJson` séparent valeur réelle et valeur sûre. Une représentation `Debug` ne doit pas révéler le réel d'un secret.
|
||
|
||
## 4. Construire Logging depuis Config
|
||
|
||
Le chemin normal consiste à charger le profil Logging, résoudre l'environnement puis construire directement le contrat Logging public :
|
||
|
||
```rust
|
||
let resolved = match engine.load_resolved_logging_config(std::option::Option::None, &environment) {
|
||
std::result::Result::Ok(value) => value,
|
||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||
};
|
||
|
||
let settings = resolved.into_settings();
|
||
let initialized = ksp_logging_lib::initialize(&settings);
|
||
let mut logging_guard = match initialized {
|
||
std::result::Result::Ok(value) => value,
|
||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||
};
|
||
```
|
||
|
||
L'application/service possède `logging_guard`. Config ne conserve pas de singleton Logging.
|
||
|
||
Un `logs_directory` relatif est ancré sur le current working directory du processus. Un path absolu est conservé. Une valeur explicite invalide produit une erreur effective : elle ne retombe pas silencieusement sur le fallback du placeholder.
|
||
|
||
Les `files[].path` restent relatifs sous le root Logging, y compris après interpolation.
|
||
|
||
### 4.1 Construire le Transport HTTP + WebSocket + Yellowstone gRPC depuis Config
|
||
|
||
Config possède l'adapter du document `std.transport` vers les contrats runtime de `ksp-onchain-transport-lib` :
|
||
|
||
```rust
|
||
let transport = match engine.load_resolved_transport_config(std::option::Option::None, &environment) {
|
||
std::result::Result::Ok(value) => value,
|
||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||
};
|
||
|
||
let http_settings = transport.http_settings();
|
||
let ws_settings = transport.ws_settings();
|
||
let grpc_settings = transport.grpc_settings();
|
||
let _ = (http_settings, ws_settings, grpc_settings);
|
||
```
|
||
|
||
`std.transport` V3 conserve intégralement les shapes V1/V2, ajoute `grpc_defaults` au niveau global et permet `profiles[].grpc_endpoints[]`. Les profils V3 peuvent rester HTTP + WebSocket seulement : l'absence de `grpc_endpoints` mappe vers `None` et n'invente aucun endpoint. La lecture V1 HTTP-only et V2 HTTP + WebSocket reste stricte et backward-compatible.
|
||
|
||
Un endpoint gRPC V3 sépare explicitement :
|
||
|
||
```text
|
||
provider = description de l'opérateur/exécution, par exemple publicnode
|
||
protocol = solana_yellowstone
|
||
metadata = metadata non secrète
|
||
secret_metadata = metadata dont la valeur doit avoir une provenance KSP_SECRET_*/KSPB_SECRET_*
|
||
```
|
||
|
||
`protocol` n'est pas un nouveau `WsProtocolKind` et ne transforme pas PublicNode en protocole. Config résout les placeholders, vérifie la classe de sensibilité des metadata puis construit `YellowstoneGrpcTransportSettings`. Transport ne lit jamais l'environnement. Les valeurs de `secret_metadata` sont disponibles au runtime mais redacted dans `safe_value` et dans les représentations `Debug`.
|
||
|
||
Les profils versionnés `publicnode_mainnet` et `publicnode_testnet` représentent leurs endpoints Yellowstone sous la forme URL TLS requise par Transport et conservent le `x-token` dans `secret_metadata`. Ils restent des sources Transport network-scoped et ne définissent jamais l’identité du réseau applicatif.
|
||
|
||
L'accesseur historique `into_transport_settings()` conserve volontairement son tuple `(HTTP, Option<WS>)`. Un consumer ayant besoin des trois backends utilise `into_all_transport_settings()` ou les accesseurs séparés afin de ne pas casser silencieusement les consumers V2.
|
||
|
||
Les protocoles WebSocket restent `kind = "solana_standard"` et `kind = "helius_laserstream"`. Chaque endpoint peut en plus déclarer `capabilities`, liste de descriptors `WsSubscriptionKind` (`account`, `block`, `logs`, `program`, `root`, `signature`, `slot`, `slots_updates`, `vote`, `helius_transaction`). L'absence de cette liste reste backward-compatible mais ne constitue aucune preuve de support ; une liste présente est mappée explicitement vers Transport puis validée contre le protocole. Config ne déduit jamais une capability depuis `provider`.
|
||
|
||
Les profils Solana standard publics versionnés déclarent `account/block/logs/program/root/signature/slot`. `block` reste une capability explicite, instable et validator/provider-gated : Config ne l’infère jamais du protocol kind ni du provider, mais les endpoints publics committés l’annoncent désormais explicitement pour rendre la route Standard Block composable. Le profil `testnet_public` fournit le socle HTTP + WS standard Testnet sans credential ; `publicnode_testnet` reste une source Yellowstone optionnelle séparée. Les profils `helius_devnet` et `helius_mainnet` déclarent les sept familles Helius déjà supportées par la façade (`account/logs/program/root/signature/slot/slots_updates`) plus `helius_transaction`, sans `block` ni `vote`. Helius LaserStream WebSocket conserve ses URLs Config-owned et sa clé `KSP_SECRET_HELIUS_API_KEY`; cette surface est indépendante de Yellowstone gRPC.
|
||
|
||
Les scalaires `*_ms` restent des valeurs Config et sont convertis en `std::time::Duration` par l'adapter. La dépendance reste unidirectionnelle : Config connaît les contrats Transport pour les 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` n’accepte 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`.
|
||
|
||
### 4.3 Construire le service Off-chain Transport depuis Config
|
||
|
||
Le document `cfg.std.offchain_transport` possède actuellement le domaine `market_price`. Config résout les placeholders, vérifie la provenance des API keys et de la paire DexScreener, puis construit directement `MarketPriceService` :
|
||
|
||
```rust
|
||
let offchain = match engine.load_resolved_offchain_transport_config(
|
||
std::option::Option::None,
|
||
&environment,
|
||
) {
|
||
std::result::Result::Ok(value) => value,
|
||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||
};
|
||
|
||
for entry in offchain.service().registry().entries() {
|
||
println!(
|
||
"{} {:?}",
|
||
entry.descriptor().display_name(),
|
||
entry.state().availability(),
|
||
);
|
||
}
|
||
```
|
||
|
||
Le profil versionné `public_keyless` ne requiert aucun secret. Le profil `all_free` utilise les credentials `KSP_SECRET_*` et la paire `KSP_PUBLIC_DEXSCREENER_SOL_USD_PAIR_ADDRESS` inventoriés dans `.env.example`. Une API key littérale ou issue d'une provenance non secrète est refusée par l'adapter effectif ; une paire DexScreener issue d'une provenance Secret est également refusée.
|
||
|
||
Config ne permet pas de fournir `base_url`, `endpoint_url`, `rate_limit` ou `requests_per` aux branches provider. Les origines et cadences sûres restent possédées par `ksp-offchain-transport-lib`. La direction de dépendance reste donc :
|
||
|
||
```text
|
||
ksp-config-lib -> ksp-offchain-transport-lib
|
||
```
|
||
|
||
Off-chain Transport ne lit ni `.env`, ni `KSP_*`, ni les documents Config. `ksp-app-solprices-desk` reçoit le service déjà composé puis utilise uniquement la surface provider-neutral `registry()`, `refresh`, `refresh_many` et `refresh_all`.
|
||
|
||
### 4.4 Construire le Store depuis Config
|
||
|
||
`cfg.std.store` définit des targets nommés. Chaque target sélectionne exactement un réseau logique, un backend et une URI PostgreSQL distincte. Config résout les secrets puis construit le contrat backend-neutral `ksp_store_lib::StoreSettings` sans activer la feature PostgreSQL du consumer :
|
||
|
||
```rust
|
||
let store_config = match engine.load_resolved_store_config(
|
||
std::option::Option::Some("devnet"),
|
||
&environment,
|
||
) {
|
||
std::result::Result::Ok(value) => value,
|
||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||
};
|
||
|
||
let target_id = store_config.target_id();
|
||
let network = store_config.settings().network();
|
||
let _ = (target_id, network);
|
||
|
||
let store_settings = store_config.into_settings();
|
||
let store = match ksp_store_lib::Store::open(store_settings).await {
|
||
std::result::Result::Ok(value) => value,
|
||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||
};
|
||
let _health = store.health().await;
|
||
let closed = store.close().await;
|
||
if let std::result::Result::Err(error) = closed {
|
||
return std::result::Result::Err(error);
|
||
}
|
||
```
|
||
|
||
Targets committed :
|
||
|
||
```text
|
||
devnet -> network devnet -> KSP_SECRET_STORE_DEVNET_POSTGRES_URI
|
||
mainnet -> network mainnet -> KSP_SECRET_STORE_MAINNET_POSTGRES_URI
|
||
testnet -> network testnet -> KSP_SECRET_STORE_TESTNET_POSTGRES_URI
|
||
```
|
||
|
||
`default_profile = "devnet"` choisit un seul target. La sélection d'un autre target se fait par le `profile_id` explicite ; `ksp-store-lib` ne multiplexe pas plusieurs bases ou réseaux dans une même instance.
|
||
|
||
Chaque `connection_uri` doit provenir d'un placeholder `KSP_SECRET_*`/`KSPB_SECRET_*`. Une URI littérale ou issue d'une variable non secrète est rejetée par l'adapter effectif. La valeur réelle est transmise au runtime Store, mais `ResolvedStoreConfig`, `StoreSettings` et les projections sûres ne l'affichent pas.
|
||
|
||
La direction de dépendance reste :
|
||
|
||
```text
|
||
ksp-config-lib -> ksp-store-lib (default-features = false)
|
||
ksp-store-lib -X-> ksp-config-lib
|
||
ksp-store-postgres-lib -X-> ksp-config-lib
|
||
```
|
||
|
||
## 5. Profils et composites
|
||
|
||
Pour un document standard profilé :
|
||
|
||
```rust
|
||
let profile = engine.load_resolved_profile(&file_id, std::option::Option::None);
|
||
```
|
||
|
||
`None` utilise le `default_profile`; `Some("profile_id")` impose un profil explicite.
|
||
|
||
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ë.
|
||
|
||
Lorsqu’un adapter runtime consomme un profil déjà choisi par un composite, utiliser l’entré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 `cfg.composite.ksp-app-raw-transaction-ingest-desk` référence exactement `logging`, `transport` et `store`; ses profils couplent explicitement un profil Transport et un profil Store du même réseau logique (`devnet`, `mainnet` ou `testnet`) afin que l'application n'assemble jamais arbitrairement deux réseaux. Le composite `cfg.composite.ksp-app-solprices-desk` référence `logging` et `offchain_transport`. Le composite `cfg.composite.ksp-app-wallet-desk` référence `logging`, `offchain_transport`, `transport` et `wallet`; chaque application valide ses frontières de composition au bootstrap.
|
||
|
||
## 6. Management de `std.logging.json`
|
||
|
||
Une application de management construit la façade à partir d'un moteur :
|
||
|
||
```rust
|
||
let management = ksp_config_lib::ConfigManagement::new(engine);
|
||
|
||
let document = match management.load_logging_document() {
|
||
std::result::Result::Ok(value) => value,
|
||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||
};
|
||
```
|
||
|
||
Le type `LoggingConfigDocument` et ses sous-structures exposent des setters/mutators typés. Après modification, la sauvegarde :
|
||
|
||
```rust
|
||
let saved = management.save_logging_document(&document);
|
||
```
|
||
|
||
valide le candidat complet avant toute substitution du fichier. Un candidat invalide ne remplace pas la source existante.
|
||
|
||
`read_source(file_id)` reste disponible pour une UI de réparation : il peut lire le texte brut d'un document enregistré même lorsque son JSON ou son schema est invalide. Il n'ouvre pas un path arbitraire.
|
||
|
||
## 7. Management de `.env`
|
||
|
||
Les rapports ordinaires sont sûrs :
|
||
|
||
```rust
|
||
let report = management.environment_report();
|
||
```
|
||
|
||
Ils distinguent notamment valeur souhaitée `.env`, valeur effective, source et shadowing process sans exposer un secret réel.
|
||
|
||
L'accès au réel est volontairement explicite :
|
||
|
||
```rust
|
||
let effective = management.reveal_effective_environment_value("KSP_SECRET_EXAMPLE");
|
||
let persisted = management.reveal_dotenv_value("KSP_SECRET_EXAMPLE");
|
||
```
|
||
|
||
Une application doit contrôler l'autorisation de l'utilisateur avant ces appels et ne jamais journaliser les valeurs retournées.
|
||
|
||
Les mutations persistantes utilisent :
|
||
|
||
```rust
|
||
let changed = management.set_dotenv_value("KSP_LOGS_DIRECTORY", "logs");
|
||
let removed = management.remove_dotenv_value("KSP_LOGS_DIRECTORY");
|
||
```
|
||
|
||
Elles n'altèrent jamais l'environnement hérité du processus. Une valeur process peut donc masquer une modification `.env`; `ConfigEnvironmentChangeReport` distingue `source_changed`, `effective_changed`, `shadowed_by_process_environment` et `reload_required`.
|
||
|
||
Sur Unix, un nouveau `.env` est créé avec des permissions privées `0600`; les permissions existantes sont préservées lors des remplacements atomiques.
|
||
|
||
Le composite `cfg.composite.ksp-app-raw-transaction-ingest-desk` expose des profils applicatifs `devnet`, `mainnet` et `testnet`. Un même profil peut référencer `transport`, `transport.helius` et `transport.yellowstone` tant que tous les profils Transport résolus appartiennent au même réseau que Store. Le provider identifie une source de capability, jamais un réseau logique.
|
||
|
||
## 8. `.env.example`
|
||
|
||
`/.env.example` est l'inventaire versionné. `/.env` reste local et ignoré.
|
||
|
||
Toute nouvelle variable runtime concrète `KSP_*` / `KSPB_*` introduite dans le code ou les documents Config doit être ajoutée à `.env.example` avec un commentaire expliquant son usage. Les audits `ksp-config-lib/tests/ownership.rs` font échouer `cargo test` lorsqu'une clé concrète est oubliée.
|
||
|
||
## 9. Frontière Tauri
|
||
|
||
Une application Tauri doit appeler les APIs ci-dessus via ses commandes/DTO applicatifs. Elle ne lit ni JSON ni `.env` directement et ne résout jamais elle-même les placeholders.
|
||
|
||
Les valeurs `Secret` ne doivent pas être incluses par défaut dans les DTO publics. Une action UI explicitement autorisée peut appeler une méthode `reveal_*` et transporter le résultat par un DTO spécifique, sans log ni diagnostic contenant la valeur réelle.
|
||
|
||
## 10. Index de la surface publique
|
||
|
||
Ce guide reste volontairement indépendant des numéros de release. Les contrats publics sont regroupés ci-dessous par usage ; les constantes de noms/erreurs accompagnent les mêmes familles et ne constituent pas des workflows séparés.
|
||
|
||
| Famille publique | Contrats principaux | Exemple |
|
||
|------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------|
|
||
| Bootstrap | `ConfigBootstrapOptions`, `ARG_CFG_PATH`, `ARG_SCHEMA_PATH`, `DEFAULT_CFG_PATH`, `DEFAULT_SCHEMA_PATH` | §1 |
|
||
| Registre logique | `ConfigFileRegistry`, `ConfigFileId`, `ConfigFileDescriptor`, `ConfigFileKind`, `ARG_FILE_MAP`, constantes `FILE_ID_*` / `DEFAULT_*_FILENAME` | §1–2 |
|
||
| Documents | `ConfigDocumentEngine`, `ConfigJsonDocument` | §2 |
|
||
| Profils | `ResolvedConfigProfile`, `ConfigProfileSelectionSource`, `ConfigValueOrigin` | §5 |
|
||
| Composites | `ResolvedConfigComposite`, `ResolvedCompositeComponent` | §5 |
|
||
| Environnement | `ConfigEnvironment`, `ConfigEnvironmentSource`, `ConfigEnvironmentValue`, `DEFAULT_DOTENV_PATH`, `DEFAULT_DOTENV_EXAMPLE_PATH` | §3, §7–8 |
|
||
| Sensibilité/provenance | `ConfigSensitivity`, `ConfigValueProvenance`, `ResolvedConfigText`, `ResolvedConfigJson`, `REDACTED_CONFIG_VALUE` | §3 |
|
||
| Logging effectif | `ResolvedLoggingConfig` | §4 |
|
||
| Transport effectif | `ResolvedTransportConfig` | §4.1 |
|
||
| Management | `ConfigManagement`, `ConfigManagedSource`, `ConfigDocumentChangeReport`, `ConfigEnvironmentReport`, `ConfigEnvironmentChangeReport` | §6–7 |
|
||
| Source Logging typée | `LoggingConfigDocument`, `LoggingProfileConfig`, `LoggingConsoleConfig`, `LoggingFileConfig`, `LoggingOutputFilterConfig`, `LoggingTargetFilterConfig` | §6 et exemple ci-dessous |
|
||
| Erreurs Config | constantes `ERROR_CODE_*` réexportées par la crate | exemple ci-dessous |
|
||
|
||
### 10.1 Modifier une configuration Logging typée
|
||
|
||
Les getters permettent d'inspecter la source ; les setters et vues `*_mut()` permettent de construire un candidat avant validation/persistence :
|
||
|
||
```rust
|
||
let mut document = match management.load_logging_document() {
|
||
std::result::Result::Ok(value) => value,
|
||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||
};
|
||
|
||
document.set_logs_directory("${KSP_LOGS_DIRECTORY:-logs}");
|
||
|
||
if let std::option::Option::Some(profile) = document.profiles_mut().first_mut() {
|
||
profile.set_default_filter("debug");
|
||
profile.console_mut().set_enabled(true);
|
||
profile.console_mut().filter_mut().set_level("info");
|
||
profile.console_mut().filter_mut().domains_mut().push("config".to_owned());
|
||
}
|
||
|
||
let saved = match management.save_logging_document(&document) {
|
||
std::result::Result::Ok(value) => value,
|
||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||
};
|
||
|
||
if saved.source_changed() && saved.reload_required() {
|
||
// The application decides when/how to reload the affected runtime consumer.
|
||
}
|
||
```
|
||
|
||
La construction depuis zéro utilise les constructeurs publics `LoggingConfigDocument::new`, `LoggingProfileConfig::new`, `LoggingConsoleConfig::new`, `LoggingFileConfig::new`, `LoggingOutputFilterConfig::new` et `LoggingTargetFilterConfig::new`. Les mêmes contraintes schema/sémantiques sont appliquées au moment de `save_logging_document()`.
|
||
|
||
### 10.2 Inspecter et réparer un source enregistré sans contourner Config
|
||
|
||
```rust
|
||
let source = match management.read_source(&file_id) {
|
||
std::result::Result::Ok(value) => value,
|
||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||
};
|
||
|
||
let logical_id = source.file_id();
|
||
let managed_path = source.path();
|
||
let raw_content = source.content();
|
||
```
|
||
|
||
Cette lecture est notamment destinée à une UI de réparation lorsque le document n'est plus validable. Elle n'autorise pas la lecture d'un chemin arbitraire.
|
||
|
||
Après édition du texte brut, le candidat est soumis à Config :
|
||
|
||
```rust
|
||
let saved = match management.save_source_candidate(&file_id, edited_source.as_str()) {
|
||
std::result::Result::Ok(value) => value,
|
||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||
};
|
||
|
||
if saved.source_changed() && saved.reload_required() {
|
||
// Reload the affected Config consumer through the application lifecycle.
|
||
}
|
||
```
|
||
|
||
`save_source_candidate()` :
|
||
|
||
- accepte uniquement un `file_id` enregistré de kind `Config` ;
|
||
- parse le candidat comme JSON ;
|
||
- valide le schema enregistré et les invariants sémantiques KSP ;
|
||
- ne remplace aucune donnée lorsque l'une de ces validations échoue ;
|
||
- persiste atomiquement le texte brut validé sans reformattage implicite ;
|
||
- retourne `ConfigDocumentChangeReport` pour distinguer un changement réel d'un candidat identique.
|
||
|
||
Le path reste résolu exclusivement par `ConfigFileRegistry`; l'appelant ne fournit jamais de path physique.
|
||
|
||
### 10.3 Exploiter les rapports `.env`
|
||
|
||
```rust
|
||
let reports = match management.environment_report() {
|
||
std::result::Result::Ok(value) => value,
|
||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||
};
|
||
|
||
for report in reports {
|
||
let name = report.variable_name();
|
||
let sensitivity = report.sensitivity();
|
||
let desired = report.desired_safe_value();
|
||
let effective = report.effective_safe_value();
|
||
let source = report.effective_source();
|
||
let shadowed = report.shadowed_by_process_environment();
|
||
let _ = (name, sensitivity, desired, effective, source, shadowed);
|
||
}
|
||
```
|
||
|
||
Après une mutation, `ConfigEnvironmentChangeReport` expose `source_changed()`, `effective_changed()`, `shadowed_by_process_environment()` et `reload_required()`.
|
||
|
||
### 10.4 Distinguer un code d'erreur Config
|
||
|
||
Les codes publics permettent à une UI/service de brancher sa logique sans parser le texte du message :
|
||
|
||
```rust
|
||
let loaded = engine.load_validated_document(&file_id);
|
||
|
||
if let std::result::Result::Err(error) = loaded {
|
||
if error.code() == ksp_config_lib::ERROR_CODE_SCHEMA_VALIDATION_FAILED {
|
||
// Present a schema-specific diagnostic path to the caller.
|
||
}
|
||
return std::result::Result::Err(error);
|
||
}
|
||
```
|
||
|
||
Le message/context d'erreur reste destiné au diagnostic ; l'identité machine-readable passe par `ErrorCode`.
|