0.5.1-pre.007
This commit is contained in:
@@ -1,8 +1,16 @@
|
||||
<!-- file: ks-config/CHANGELOG.md -->
|
||||
<!-- version: 16 -->
|
||||
<!-- version: 17 -->
|
||||
|
||||
# CHANGELOG — ks-config
|
||||
|
||||
## `0.5.1-pre.007`
|
||||
|
||||
- extrait store, wallet et execution vers `config/store.config.json`, `config/wallet.config.json` et `config/execution.config.json`, chacun avec schéma, exemple et `default_profile` ;
|
||||
- place `wallets_directory` hors profils wallet et déplace les autorisations `*_send_enabled` dans la politique execution ;
|
||||
- remplace `app.auto_reconnect_default` par des classes de defaults WebSocket nommées dans le transport, avec overrides propres à chaque endpoint ;
|
||||
- permet la composition directe des defaults partagés sans fichier applicatif et valide les références de profils d’une composition avant production du contrat runtime ;
|
||||
- supprime la nécessité d’une composition par défaut propre à `ks-pipeline-demo-scenarios`.
|
||||
|
||||
## `0.5.1-pre.006`
|
||||
|
||||
- remplace le document source monolithique `app.config.json` par des compositions propres aux binaires, dont `config/kb-app-demo-desktop.default.config.json` et `config/ks-pipeline-demo-scenarios.default.config.json` ;
|
||||
|
||||
@@ -1,49 +1,50 @@
|
||||
<!-- file: ks-config/README.md -->
|
||||
<!-- version: 9 -->
|
||||
<!-- version: 10 -->
|
||||
|
||||
# ks-config
|
||||
|
||||
`ks-config` fournit les contrats de configuration généralistes Khadhroony Solana, les chargeurs de documents spécialisés et la résolution des compositions utilisées par les binaires.
|
||||
`ks-config` fournit les contrats de configuration généralistes Khadhroony Solana, les chargeurs des documents spécialisés et la résolution des compositions facultatives utilisées par les binaires.
|
||||
|
||||
## Responsabilités
|
||||
|
||||
- charger l'environnement du workspace et résoudre les placeholders `${KS_*}` / `${KB_*}` ;
|
||||
- valider les documents transport, listeners et composition contre leurs schémas sous `config/schemas/` ;
|
||||
- fournir les types partagés d'endpoints, listeners et profils runtime ;
|
||||
- résoudre les références d'une composition vers des profils transport/listeners ;
|
||||
- valider les documents transport, listeners, store, wallet, execution et composition contre leurs schémas sous `config/schemas/` ;
|
||||
- fournir les types partagés d'endpoints, listeners, stockage, wallet, exécution et profils runtime ;
|
||||
- résoudre les defaults de chaque document ou les overrides d'une composition ;
|
||||
- refuser une référence de fichier ou de profil inexistante ;
|
||||
- reconstruire temporairement `AppConfig/ProfileConfig` pour les consommateurs existants ;
|
||||
- rester indépendant de la liste des applications et workers futurs.
|
||||
|
||||
## Composition
|
||||
## Defaults et composition
|
||||
|
||||
Le desktop possède `config/kb-app-demo-desktop.default.config.json`. Le CLI des scénarios possède `config/ks-pipeline-demo-scenarios.default.config.json`.
|
||||
Chaque document partagé possède un `default_profile`. Un consommateur qui accepte les defaults n'a pas besoin de composition dédiée.
|
||||
|
||||
Une composition choisit :
|
||||
Le desktop possède `config/kb-app-demo-desktop.default.config.json` parce qu'il sélectionne des combinaisons propres à l'application. `ks-pipeline-demo-scenarios` utilise directement les defaults partagés ; `KS_DEVNET_CONFIG_PATH` permet encore un override explicite lorsque nécessaire.
|
||||
|
||||
- les documents partagés à charger ;
|
||||
- le profil logging ;
|
||||
- le profil transport ;
|
||||
- le profil listeners ;
|
||||
- temporairement les sections encore non séparées.
|
||||
|
||||
Un futur worker pourra donc ajouter son propre `<binary>.default.config.json` sans créer une branche spécifique dans `ks-config`.
|
||||
Un futur worker pourra ajouter son propre `<binary>.default.config.json` uniquement s'il doit remplacer des fichiers/profils ou ajouter des paramètres propres au binaire.
|
||||
|
||||
## Documents spécialisés actuels
|
||||
|
||||
- `config/transport.config.json` / `config/schemas/transport.config.schema.json` ;
|
||||
- `config/listeners.config.json` / `config/schemas/listeners.config.schema.json` ;
|
||||
- `config/store.config.json` / `config/schemas/store.config.schema.json` ;
|
||||
- `config/wallet.config.json` / `config/schemas/wallet.config.schema.json` ;
|
||||
- `config/execution.config.json` / `config/schemas/execution.config.schema.json` ;
|
||||
- `config/logging.config.json` reste possédé par `ks-logging` ;
|
||||
- `config/schemas/resolved.app.config.schema.json` décrit uniquement le contrat runtime transitoire.
|
||||
|
||||
Les anciens `config/app.config.json` et `config/example.app.config.json` ne sont plus des fichiers runtime.
|
||||
Les valeurs globales telles que `logs_directory` et `wallets_directory` ne sont pas des propriétés de profils.
|
||||
|
||||
## API publique principale
|
||||
|
||||
- `read_composed_app_config_with_environment` ;
|
||||
- `read_default_shared_app_config_with_environment` ;
|
||||
- `parse_composition_json`, `composition_profile`, `active_composition_profile` ;
|
||||
- `read_transport_json_file_with_environment`, `transport_profile` ;
|
||||
- `read_listeners_json_file_with_environment`, `listeners_profile` ;
|
||||
- `compose_app_config` ;
|
||||
- `read_transport_json_file_with_environment`, `transport_profile`, `default_transport_profile` ;
|
||||
- `read_listeners_json_file_with_environment`, `listeners_profile`, `default_listeners_profile` ;
|
||||
- `read_store_json_file_with_environment`, `store_profile`, `default_store_profile` ;
|
||||
- `read_wallet_json_file_with_environment`, `wallet_profile`, `default_wallet_profile` ;
|
||||
- `read_execution_json_file_with_environment`, `execution_profile`, `default_execution_profile` ;
|
||||
- `load_workspace_environment`, `resolve_environment_placeholders` ;
|
||||
- `AppConfig`, `ProfileConfig` pendant la phase de compatibilité.
|
||||
|
||||
@@ -51,9 +52,9 @@ Les anciens `config/app.config.json` et `config/example.app.config.json` ne sont
|
||||
|
||||
`ks-logging` dépend de `ks-config` pour les helpers d'environnement, mais `ks-config` ne possède aucun type logging runtime.
|
||||
|
||||
`ks-onchain-transport` peut consommer directement `TransportProfileConfig`. Un worker n'a donc pas besoin d'un profil applicatif complet pour construire ses pools réseau.
|
||||
`ks-onchain-transport` peut consommer directement `TransportProfileConfig`.
|
||||
|
||||
Les sections database/data, wallet, execution et demo restent transitoires dans la composition jusqu'au split suivant.
|
||||
Les dérivations TS-RS des types de `ks-config` sont encore présentes pendant `pre.007`, mais elles ne sont pas considérées comme une frontière durable. L'audit montre que le frontend desktop n'importe pas directement ces bindings ; leur suppression ou réduction est prévue avec les DTO publics sûrs de la prochaine prerelease.
|
||||
|
||||
## Documents
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<!-- file: ks-config/TODO.md -->
|
||||
<!-- version: 8 -->
|
||||
<!-- version: 9 -->
|
||||
|
||||
# TODO — ks-config
|
||||
|
||||
## Série `0.5.x`
|
||||
|
||||
- [ ] `0.5.1` - sortir `database`, wallet, execution et la configuration spécifique au desktop vers leurs documents/propriétaires dédiés ; réaffecter `logs_directory` à logging et `wallets_directory` à wallet, puis réduire la composition aux références nécessaires.
|
||||
- [ ] `0.5.1` - appliquer à l’exécution les classes `KS_SECRET_*` / `KB_SECRET_*`, `KS_PUBLIC_*` / `KB_PUBLIC_*` et internes, avec propagation de sensibilité aux valeurs composées.
|
||||
- [ ] `0.5.1` - séparer les représentations source, runtime résolue, publique et diagnostic.
|
||||
- [ ] `0.5.1` - interdire qu’un secret résolu soit sérialisé, loggé, inclus dans une erreur ou transmis via Tauri.
|
||||
- [ ] `0.5.1` - supprimer l’exposition frontend de `AppConfig` et `ProfileConfig` résolus complets.
|
||||
- [ ] `0.5.1` - retirer de `ks-config` et réduire dans `ks-lib` les bindings TS-RS qui ne constituent pas un contrat générique indépendant de Tauri ; définir les wrappers publics nécessaires dans `kb-app-demo-desktop`.
|
||||
- [ ] Contrat - maintenir l’identité entre chaque schéma embarqué et son fichier sous `config/schemas/`.
|
||||
- [ ] Tests - ajouter les tests d’API externe et les canaris de non-divulgation.
|
||||
- [ ] Documentation - maintenir guides, exemples et `.env.example` avec l’implémentation correspondante.
|
||||
|
||||
@@ -1,15 +1,28 @@
|
||||
<!-- file: ks-config/USAGE.md -->
|
||||
<!-- version: 8 -->
|
||||
<!-- version: 9 -->
|
||||
|
||||
# Utilisation de ks-config
|
||||
|
||||
## Rôle de la crate
|
||||
|
||||
`ks-config` fournit les types, validations et chargeurs des configurations Khadhroony Solana partagées. Un binaire choisit son propre fichier de composition et combine les profils spécialisés dont il a besoin.
|
||||
`ks-config` fournit les types, validations et chargeurs des configurations Khadhroony Solana partagées. Un consommateur peut utiliser les defaults des documents spécialisés ou fournir une composition qui remplace seulement les sélections nécessaires.
|
||||
|
||||
`ks-config` ne doit pas connaître la liste des futurs workers ou applications.
|
||||
`ks-config` ne connaît pas la liste des futurs workers ou applications.
|
||||
|
||||
## Chargement d'une composition
|
||||
## Charger les defaults partagés
|
||||
|
||||
```rust
|
||||
let config = match ks_config::read_default_shared_app_config_with_environment(
|
||||
std::path::Path::new("."),
|
||||
) {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
```
|
||||
|
||||
Le chargeur combine les `default_profile` indépendants de transport, listeners, store, wallet et execution. Il ne suppose pas que ces profils portent tous le même nom.
|
||||
|
||||
## Charger une composition de binaire
|
||||
|
||||
```rust
|
||||
let composed = match ks_config::read_composed_app_config_with_environment(
|
||||
@@ -20,25 +33,22 @@ let composed = match ks_config::read_composed_app_config_with_environment(
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let config = composed.app_config;
|
||||
let profile = match ks_config::active_profile(&config) {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
```
|
||||
|
||||
Le chargeur :
|
||||
|
||||
1. charge l'environnement du workspace ;
|
||||
2. valide la composition ;
|
||||
3. résout les chemins `transport` et `listeners` déclarés par la composition ;
|
||||
4. charge et valide les deux documents spécialisés ;
|
||||
5. sélectionne les profils référencés ;
|
||||
6. reconstruit le contrat runtime `AppConfig/ProfileConfig` ;
|
||||
7. applique les invariants métier existants.
|
||||
3. résout les six chemins spécialisés ;
|
||||
4. charge et valide transport, listeners, store, wallet et execution ;
|
||||
5. vérifie l'existence du profil logging référencé sans créer de dépendance `ks-config -> ks-logging` ;
|
||||
6. applique les overrides ou les `default_profile` ;
|
||||
7. reconstruit le contrat runtime `AppConfig/ProfileConfig` ;
|
||||
8. applique les invariants croisés existants.
|
||||
|
||||
Le logging reste chargé par `ks-logging`, car son type runtime appartient à cette crate. La composition fournit néanmoins le chemin et le nom de profil à sélectionner.
|
||||
Le document logging est ensuite chargé par `ks-logging`, qui possède son type runtime.
|
||||
|
||||
## Configuration transport indépendante
|
||||
## Transport et defaults WebSocket
|
||||
|
||||
```rust
|
||||
let transport = match ks_config::read_transport_json_file_with_environment(
|
||||
@@ -48,31 +58,31 @@ let transport = match ks_config::read_transport_json_file_with_environment(
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let profile = match ks_config::transport_profile(&transport, "mainnet_research") {
|
||||
let profile = match ks_config::default_transport_profile(&transport) {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
```
|
||||
|
||||
Le document expose `TransportConfigDocument` et `TransportProfileConfig` avec les endpoints HTTP/WebSocket et leurs rôles.
|
||||
Les endpoints WebSocket source sélectionnent une classe `ws_endpoint_defaults`; les `overrides` propres à l'endpoint sont appliqués avant de produire `TransportProfileConfig`.
|
||||
|
||||
## Configuration listeners indépendante
|
||||
## Listeners
|
||||
|
||||
```rust
|
||||
let listeners = match ks_config::read_listeners_json_file_with_environment(
|
||||
std::path::Path::new("config/listeners.config.json"),
|
||||
std::path::Path::new("."),
|
||||
) {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let profile = match ks_config::listeners_profile(&listeners, "mainnet_research") {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
```
|
||||
`default_listeners_profile` ou `listeners_profile` sélectionnent un ensemble de subscriptions. `resolved_listener_config` le convertit vers le contrat runtime transitoire `ListenerConfig`.
|
||||
|
||||
`resolved_listener_config` convertit ce profil spécialisé vers le contrat runtime `ListenerConfig` utilisé pendant la phase de transition.
|
||||
## Store
|
||||
|
||||
`store.config.json` possède les paramètres de backend et les profils PostgreSQL/SQLite. Les helpers principaux sont `read_store_json_file_with_environment`, `default_store_profile` et `store_profile`.
|
||||
|
||||
## Wallet
|
||||
|
||||
`wallet.config.json` porte `wallets_directory` hors profils. Chaque profil définit un chemin relatif, le cluster, l'alias et la politique temporaire/persistante. `resolved_wallet_profile` construit le chemin runtime final.
|
||||
|
||||
Les autorisations de soumission ne font plus partie du wallet.
|
||||
|
||||
## Exécution
|
||||
|
||||
`execution.config.json` possède simulation, confirmation, limites et `*_send_enabled`. Les helpers sont `read_execution_json_file_with_environment`, `default_execution_profile` et `execution_profile`.
|
||||
|
||||
## Chargement de l'environnement
|
||||
|
||||
@@ -85,34 +95,19 @@ let report = match ks_config::load_workspace_environment(std::path::Path::new(".
|
||||
|
||||
Par défaut, `ks-config` sélectionne `.env`; `KS_ENV_FILE` permet d'en choisir un autre sans écraser les variables déjà présentes dans le processus.
|
||||
|
||||
## Résolution des placeholders
|
||||
Les valeurs globales peuvent être remplacées, notamment :
|
||||
|
||||
```rust
|
||||
let raw = r#"{"databaseUrl":"${KS_SECRET_POSTGRES_DEVNET_URL:-postgres://localhost/ks}"}"#;
|
||||
let resolved = ks_config::resolve_environment_placeholders(raw);
|
||||
assert!(resolved.contains("databaseUrl"));
|
||||
```text
|
||||
KS_LOGS_DIRECTORY
|
||||
KS_WALLETS_DIRECTORY
|
||||
```
|
||||
|
||||
La chaîne résolue peut encore contenir des secrets. Elle reste strictement backend jusqu'à l'introduction de la représentation sensible dédiée.
|
||||
|
||||
## Contrat runtime résolu
|
||||
|
||||
`AppConfig` et `ProfileConfig` restent provisoirement le contrat runtime commun afin de ne pas casser tous les consommateurs pendant les splits successifs. Ils ne correspondent plus à un fichier `config/app.config.json` chargé directement.
|
||||
`AppConfig` et `ProfileConfig` restent provisoirement le contrat runtime commun afin de ne pas casser tous les consommateurs pendant les splits successifs. Ils ne correspondent plus à un fichier source `app.config.json`.
|
||||
|
||||
Le schéma de ce contrat résolu est `config/schemas/resolved.app.config.schema.json`. Les fixtures correspondantes sont sous `test-fixtures/config/` et ne sont pas des configurations runtime.
|
||||
Le schéma de ce contrat est `config/schemas/resolved.app.config.schema.json`. Les fixtures correspondantes sont sous `test-fixtures/config/`.
|
||||
|
||||
## Types publics importants
|
||||
## Frontière TypeScript
|
||||
|
||||
- `CompositionConfigDocument`, `CompositionProfileConfig`, `CompositionConfigSources` ;
|
||||
- `TransportConfigDocument`, `TransportProfileConfig` ;
|
||||
- `ListenersConfigDocument`, `ListenersProfileConfig` ;
|
||||
- `AppConfig`, `ProfileConfig` comme contrat runtime de transition ;
|
||||
- `HttpEndpointConfig`, `WsEndpointConfig`, `EndpointRoleConfig` ;
|
||||
- `ListenerConfig` et ses variantes ;
|
||||
- `DatabaseConfig`, `WalletConfig`, `ExecutionConfig` jusqu'à leur split dédié.
|
||||
|
||||
Les types logging appartiennent à `ks-logging`.
|
||||
|
||||
## Prochaine frontière
|
||||
|
||||
Les sections store/data, wallet, execution et desktop seront sorties de la composition dans la prerelease suivante. Une fois cette décomposition terminée, les surfaces source/runtime/public/diagnostic et le camouflage des secrets pourront être définis sur des responsabilités stables.
|
||||
Les bindings TS-RS générés depuis `ks-config` ne sont pas importés directement par le frontend desktop actuel. Ils sont donc considérés comme transitoires jusqu'à la prochaine prerelease, qui introduira les DTO publics/diagnostiques et supprimera les exports TypeScript non justifiés des crates généralistes.
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
// file: ks-config/src/composition.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Binary composition configuration and resolution into the shared runtime profile contract.
|
||||
|
||||
const COMPOSITION_JSON_SCHEMA: &str =
|
||||
include_str!("../../config/schemas/composition.config.schema.json");
|
||||
const DEFAULT_LOGGING_CONFIG_PATH: &str = "config/logging.config.json";
|
||||
const DEFAULT_TRANSPORT_CONFIG_PATH: &str = "config/transport.config.json";
|
||||
const DEFAULT_LISTENERS_CONFIG_PATH: &str = "config/listeners.config.json";
|
||||
const DEFAULT_STORE_CONFIG_PATH: &str = "config/store.config.json";
|
||||
const DEFAULT_WALLET_CONFIG_PATH: &str = "config/wallet.config.json";
|
||||
const DEFAULT_EXECUTION_CONFIG_PATH: &str = "config/execution.config.json";
|
||||
|
||||
/// Root composition document used by a binary to select shared configuration profiles.
|
||||
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize)]
|
||||
@@ -26,33 +32,58 @@ pub struct CompositionConfigSources {
|
||||
pub transport: std::string::String,
|
||||
/// Listener configuration document path relative to the workspace root unless absolute.
|
||||
pub listeners: std::string::String,
|
||||
/// Store configuration document path relative to the workspace root unless absolute.
|
||||
pub store: std::string::String,
|
||||
/// Wallet configuration document path relative to the workspace root unless absolute.
|
||||
pub wallet: std::string::String,
|
||||
/// Execution configuration document path relative to the workspace root unless absolute.
|
||||
pub execution: std::string::String,
|
||||
}
|
||||
|
||||
/// Named composition profile referencing shared configuration profiles.
|
||||
/// Named composition profile with optional overrides of each shared document default.
|
||||
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize)]
|
||||
pub struct CompositionProfileConfig {
|
||||
/// Composition profile code.
|
||||
pub name: std::string::String,
|
||||
/// Application metadata retained until the remaining configuration split is completed.
|
||||
/// Application metadata retained until the application-specific split is completed.
|
||||
pub app: crate::AppSectionConfig,
|
||||
/// Logging profile selected from the referenced logging document.
|
||||
pub logging_profile: std::string::String,
|
||||
/// Transport profile selected from the referenced transport document.
|
||||
pub transport_profile: std::string::String,
|
||||
/// Listener profile selected from the referenced listener document.
|
||||
pub listeners_profile: std::string::String,
|
||||
/// Database configuration retained until the store split prerelease.
|
||||
pub database: crate::DatabaseConfig,
|
||||
/// Local data directories retained until their owning configuration files are split.
|
||||
pub data: crate::DataConfig,
|
||||
/// Wallet configuration retained until the wallet split prerelease.
|
||||
pub wallet: crate::WalletConfig,
|
||||
/// Execution policy retained until the execution split prerelease.
|
||||
pub execution: crate::ExecutionConfig,
|
||||
/// Desktop demo flags retained until the binary-specific split prerelease.
|
||||
/// Optional logging profile override.
|
||||
pub logging_profile: std::option::Option<std::string::String>,
|
||||
/// Optional transport profile override.
|
||||
pub transport_profile: std::option::Option<std::string::String>,
|
||||
/// Optional listener profile override.
|
||||
pub listeners_profile: std::option::Option<std::string::String>,
|
||||
/// Optional store profile override.
|
||||
pub store_profile: std::option::Option<std::string::String>,
|
||||
/// Optional wallet profile override.
|
||||
pub wallet_profile: std::option::Option<std::string::String>,
|
||||
/// Optional execution profile override.
|
||||
pub execution_profile: std::option::Option<std::string::String>,
|
||||
/// Desktop demo flags retained until the application-specific split is completed.
|
||||
pub demo: crate::DemoConfig,
|
||||
}
|
||||
|
||||
/// Resolved application configuration and the source documents needed by a binary.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ComposedAppConfig {
|
||||
/// Parsed binary composition document.
|
||||
pub composition: CompositionConfigDocument,
|
||||
/// Resolved runtime application configuration.
|
||||
pub app_config: crate::AppConfig,
|
||||
/// Resolved logging source path.
|
||||
pub logging_path: std::path::PathBuf,
|
||||
/// Resolved transport source path.
|
||||
pub transport_path: std::path::PathBuf,
|
||||
/// Resolved listener source path.
|
||||
pub listeners_path: std::path::PathBuf,
|
||||
/// Resolved store source path.
|
||||
pub store_path: std::path::PathBuf,
|
||||
/// Resolved wallet source path.
|
||||
pub wallet_path: std::path::PathBuf,
|
||||
/// Resolved execution source path.
|
||||
pub execution_path: std::path::PathBuf,
|
||||
}
|
||||
|
||||
/// Returns the embedded binary composition JSON Schema text.
|
||||
pub fn composition_json_schema_text() -> &'static str {
|
||||
return COMPOSITION_JSON_SCHEMA;
|
||||
@@ -60,9 +91,9 @@ pub fn composition_json_schema_text() -> &'static str {
|
||||
|
||||
/// Parses the embedded binary composition JSON Schema into a JSON value.
|
||||
pub fn composition_json_schema_value() -> ks_core::Result<serde_json::Value> {
|
||||
let schema_result = serde_json::from_str::<serde_json::Value>(COMPOSITION_JSON_SCHEMA);
|
||||
return match schema_result {
|
||||
std::result::Result::Ok(schema) => std::result::Result::Ok(schema),
|
||||
let result = serde_json::from_str::<serde_json::Value>(COMPOSITION_JSON_SCHEMA);
|
||||
return match result {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(ks_core::Error::new(
|
||||
"composition_config_schema_parse_failed",
|
||||
error.to_string(),
|
||||
@@ -73,11 +104,11 @@ pub fn composition_json_schema_value() -> ks_core::Result<serde_json::Value> {
|
||||
/// Validates raw binary composition JSON against its embedded schema.
|
||||
pub fn validate_composition_json_schema(raw_json: &str) -> ks_core::Result<()> {
|
||||
let schema = match composition_json_schema_value() {
|
||||
std::result::Result::Ok(schema) => schema,
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let instance = match serde_json::from_str::<serde_json::Value>(raw_json) {
|
||||
std::result::Result::Ok(instance) => instance,
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"composition_config_json_parse_failed",
|
||||
@@ -86,7 +117,7 @@ pub fn validate_composition_json_schema(raw_json: &str) -> ks_core::Result<()> {
|
||||
},
|
||||
};
|
||||
let validator = match jsonschema::validator_for(&schema) {
|
||||
std::result::Result::Ok(validator) => validator,
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"composition_config_schema_compile_failed",
|
||||
@@ -110,7 +141,7 @@ pub fn parse_composition_json(raw_json: &str) -> ks_core::Result<CompositionConf
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
let document = match serde_json::from_str::<CompositionConfigDocument>(raw_json) {
|
||||
std::result::Result::Ok(document) => document,
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"composition_config_json_decode_failed",
|
||||
@@ -129,7 +160,7 @@ pub fn read_composition_json_file(
|
||||
path: &std::path::Path,
|
||||
) -> ks_core::Result<CompositionConfigDocument> {
|
||||
let raw_json = match std::fs::read_to_string(path) {
|
||||
std::result::Result::Ok(content) => content,
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"composition_config_file_read_failed",
|
||||
@@ -150,7 +181,7 @@ pub fn read_composition_json_file_with_environment(
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
let raw_json = match std::fs::read_to_string(path) {
|
||||
std::result::Result::Ok(content) => content,
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"composition_config_file_read_failed",
|
||||
@@ -158,8 +189,7 @@ pub fn read_composition_json_file_with_environment(
|
||||
));
|
||||
},
|
||||
};
|
||||
let resolved = crate::resolve_environment_placeholders(&raw_json);
|
||||
return parse_composition_json(&resolved);
|
||||
return parse_composition_json(&crate::resolve_environment_placeholders(&raw_json));
|
||||
}
|
||||
|
||||
/// Returns one named binary composition profile.
|
||||
@@ -197,36 +227,79 @@ pub fn resolve_composition_source_path(
|
||||
return workspace_root.join(path);
|
||||
}
|
||||
|
||||
/// Resolves all shared transport/listener selections into the existing runtime application contract.
|
||||
/// Resolves one optional composition override against a shared document default profile.
|
||||
pub fn resolve_profile_selection<'a>(
|
||||
override_name: std::option::Option<&'a str>,
|
||||
default_profile: &'a str,
|
||||
) -> &'a str {
|
||||
return match override_name {
|
||||
std::option::Option::Some(value) if !value.trim().is_empty() => value,
|
||||
_ => default_profile,
|
||||
};
|
||||
}
|
||||
|
||||
/// Resolves all shared source selections into the runtime application contract.
|
||||
pub fn compose_app_config(
|
||||
document: &CompositionConfigDocument,
|
||||
transport: &crate::TransportConfigDocument,
|
||||
listeners: &crate::ListenersConfigDocument,
|
||||
store: &crate::StoreConfigDocument,
|
||||
wallet: &crate::WalletConfigDocument,
|
||||
execution: &crate::ExecutionConfigDocument,
|
||||
) -> ks_core::Result<crate::AppConfig> {
|
||||
let mut profiles = std::vec::Vec::<crate::ProfileConfig>::new();
|
||||
for profile in &document.profiles {
|
||||
let transport_profile =
|
||||
match crate::transport_profile(transport, profile.transport_profile.as_str()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let listeners_profile =
|
||||
match crate::listeners_profile(listeners, profile.listeners_profile.as_str()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let transport_name = resolve_profile_selection(
|
||||
profile.transport_profile.as_deref(),
|
||||
transport.default_profile.as_str(),
|
||||
);
|
||||
let listeners_name = resolve_profile_selection(
|
||||
profile.listeners_profile.as_deref(),
|
||||
listeners.default_profile.as_str(),
|
||||
);
|
||||
let store_name = resolve_profile_selection(
|
||||
profile.store_profile.as_deref(),
|
||||
store.default_profile.as_str(),
|
||||
);
|
||||
let wallet_name = resolve_profile_selection(
|
||||
profile.wallet_profile.as_deref(),
|
||||
wallet.default_profile.as_str(),
|
||||
);
|
||||
let execution_name = resolve_profile_selection(
|
||||
profile.execution_profile.as_deref(),
|
||||
execution.default_profile.as_str(),
|
||||
);
|
||||
let transport_profile = match crate::transport_profile(transport, transport_name) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let listeners_profile = match crate::listeners_profile(listeners, listeners_name) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let store_profile = match crate::store_profile(store, store_name) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let wallet_profile = match crate::resolved_wallet_profile(wallet, wallet_name) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let execution_profile = match crate::execution_profile(execution, execution_name) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
profiles.push(crate::ProfileConfig {
|
||||
name: profile.name.clone(),
|
||||
app: profile.app.clone(),
|
||||
database: profile.database.clone(),
|
||||
data: profile.data.clone(),
|
||||
database: store_profile.database.clone(),
|
||||
solana: crate::SolanaConfig {
|
||||
http_endpoints: transport_profile.http_endpoints.clone(),
|
||||
ws_endpoints: transport_profile.ws_endpoints.clone(),
|
||||
http_endpoints: transport_profile.http_endpoints,
|
||||
ws_endpoints: transport_profile.ws_endpoints,
|
||||
listeners: crate::resolved_listener_config(listeners_profile),
|
||||
},
|
||||
wallet: profile.wallet.clone(),
|
||||
execution: profile.execution.clone(),
|
||||
wallet: wallet_profile,
|
||||
execution: execution_profile.config.clone(),
|
||||
demo: profile.demo.clone(),
|
||||
});
|
||||
}
|
||||
@@ -240,20 +313,28 @@ pub fn compose_app_config(
|
||||
};
|
||||
}
|
||||
|
||||
/// Reads a binary composition and its referenced transport/listener documents into the resolved runtime contract.
|
||||
/// Reads a binary composition and all referenced shared documents into the runtime contract.
|
||||
pub fn read_composed_app_config_with_environment(
|
||||
composition_path: &std::path::Path,
|
||||
workspace_root: &std::path::Path,
|
||||
) -> ks_core::Result<crate::ComposedAppConfig> {
|
||||
) -> ks_core::Result<ComposedAppConfig> {
|
||||
let composition =
|
||||
match read_composition_json_file_with_environment(composition_path, workspace_root) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let logging_path =
|
||||
resolve_composition_source_path(workspace_root, composition.sources.logging.as_str());
|
||||
let transport_path =
|
||||
resolve_composition_source_path(workspace_root, composition.sources.transport.as_str());
|
||||
let listeners_path =
|
||||
resolve_composition_source_path(workspace_root, composition.sources.listeners.as_str());
|
||||
let store_path =
|
||||
resolve_composition_source_path(workspace_root, composition.sources.store.as_str());
|
||||
let wallet_path =
|
||||
resolve_composition_source_path(workspace_root, composition.sources.wallet.as_str());
|
||||
let execution_path =
|
||||
resolve_composition_source_path(workspace_root, composition.sources.execution.as_str());
|
||||
let transport =
|
||||
match crate::read_transport_json_file_with_environment(&transport_path, workspace_root) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
@@ -264,34 +345,120 @@ pub fn read_composed_app_config_with_environment(
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let app_config = match compose_app_config(&composition, &transport, &listeners) {
|
||||
let store = match crate::read_store_json_file_with_environment(&store_path, workspace_root) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(crate::ComposedAppConfig {
|
||||
let wallet = match crate::read_wallet_json_file_with_environment(&wallet_path, workspace_root) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let execution =
|
||||
match crate::read_execution_json_file_with_environment(&execution_path, workspace_root) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
match validate_logging_references_with_environment(&logging_path, workspace_root, &composition)
|
||||
{
|
||||
std::result::Result::Ok(()) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
let app_config =
|
||||
match compose_app_config(&composition, &transport, &listeners, &store, &wallet, &execution)
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(ComposedAppConfig {
|
||||
composition,
|
||||
app_config,
|
||||
logging_path,
|
||||
transport_path,
|
||||
listeners_path,
|
||||
store_path,
|
||||
wallet_path,
|
||||
execution_path,
|
||||
});
|
||||
}
|
||||
|
||||
/// Resolved application configuration and the source documents needed by a binary.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ComposedAppConfig {
|
||||
/// Parsed binary composition document.
|
||||
pub composition: CompositionConfigDocument,
|
||||
/// Resolved runtime application configuration.
|
||||
pub app_config: crate::AppConfig,
|
||||
/// Resolved transport source path.
|
||||
pub transport_path: std::path::PathBuf,
|
||||
/// Resolved listener source path.
|
||||
pub listeners_path: std::path::PathBuf,
|
||||
/// Loads canonical shared documents and composes their independent default profiles without an application composition file.
|
||||
pub fn read_default_shared_app_config_with_environment(
|
||||
workspace_root: &std::path::Path,
|
||||
) -> ks_core::Result<crate::AppConfig> {
|
||||
let transport_path = workspace_root.join(DEFAULT_TRANSPORT_CONFIG_PATH);
|
||||
let listeners_path = workspace_root.join(DEFAULT_LISTENERS_CONFIG_PATH);
|
||||
let store_path = workspace_root.join(DEFAULT_STORE_CONFIG_PATH);
|
||||
let wallet_path = workspace_root.join(DEFAULT_WALLET_CONFIG_PATH);
|
||||
let execution_path = workspace_root.join(DEFAULT_EXECUTION_CONFIG_PATH);
|
||||
let transport =
|
||||
match crate::read_transport_json_file_with_environment(&transport_path, workspace_root) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let listeners =
|
||||
match crate::read_listeners_json_file_with_environment(&listeners_path, workspace_root) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let store = match crate::read_store_json_file_with_environment(&store_path, workspace_root) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let wallet = match crate::read_wallet_json_file_with_environment(&wallet_path, workspace_root) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let execution =
|
||||
match crate::read_execution_json_file_with_environment(&execution_path, workspace_root) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let defaults = [
|
||||
transport.default_profile.as_str(),
|
||||
listeners.default_profile.as_str(),
|
||||
store.default_profile.as_str(),
|
||||
wallet.default_profile.as_str(),
|
||||
execution.default_profile.as_str(),
|
||||
];
|
||||
let profile_name = if defaults.iter().all(|value| return *value == defaults[0]) {
|
||||
defaults[0].to_string()
|
||||
} else {
|
||||
"shared_default".to_string()
|
||||
};
|
||||
let composition = CompositionConfigDocument {
|
||||
active_profile: profile_name.clone(),
|
||||
sources: CompositionConfigSources {
|
||||
logging: DEFAULT_LOGGING_CONFIG_PATH.to_string(),
|
||||
transport: DEFAULT_TRANSPORT_CONFIG_PATH.to_string(),
|
||||
listeners: DEFAULT_LISTENERS_CONFIG_PATH.to_string(),
|
||||
store: DEFAULT_STORE_CONFIG_PATH.to_string(),
|
||||
wallet: DEFAULT_WALLET_CONFIG_PATH.to_string(),
|
||||
execution: DEFAULT_EXECUTION_CONFIG_PATH.to_string(),
|
||||
},
|
||||
profiles: vec![CompositionProfileConfig {
|
||||
name: profile_name.clone(),
|
||||
app: crate::AppSectionConfig {
|
||||
name: "khadhroony-solana".to_string(),
|
||||
environment: profile_name,
|
||||
},
|
||||
logging_profile: std::option::Option::None,
|
||||
transport_profile: std::option::Option::None,
|
||||
listeners_profile: std::option::Option::None,
|
||||
store_profile: std::option::Option::None,
|
||||
wallet_profile: std::option::Option::None,
|
||||
execution_profile: std::option::Option::None,
|
||||
demo: crate::DemoConfig {
|
||||
live_demo_enabled: false,
|
||||
trading_demo_enabled: false,
|
||||
},
|
||||
}],
|
||||
};
|
||||
return compose_app_config(&composition, &transport, &listeners, &store, &wallet, &execution);
|
||||
}
|
||||
|
||||
/// Validates a typed binary composition document before referenced documents are loaded.
|
||||
pub fn validate_composition_document(document: &CompositionConfigDocument) -> ks_core::Result<()> {
|
||||
match crate::require_non_empty(&document.active_profile, "active_profile") {
|
||||
match crate::require_non_empty(&document.active_profile, "composition.active_profile") {
|
||||
std::result::Result::Ok(()) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
@@ -299,6 +466,9 @@ pub fn validate_composition_document(document: &CompositionConfigDocument) -> ks
|
||||
&document.sources.logging,
|
||||
&document.sources.transport,
|
||||
&document.sources.listeners,
|
||||
&document.sources.store,
|
||||
&document.sources.wallet,
|
||||
&document.sources.execution,
|
||||
] {
|
||||
match crate::require_non_empty(source, "composition.sources") {
|
||||
std::result::Result::Ok(()) => (),
|
||||
@@ -317,12 +487,19 @@ pub fn validate_composition_document(document: &CompositionConfigDocument) -> ks
|
||||
std::result::Result::Ok(()) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
for reference in
|
||||
[&profile.logging_profile, &profile.transport_profile, &profile.listeners_profile]
|
||||
{
|
||||
match crate::require_non_empty(reference, "composition.profile.reference") {
|
||||
std::result::Result::Ok(()) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
for reference in [
|
||||
profile.logging_profile.as_ref(),
|
||||
profile.transport_profile.as_ref(),
|
||||
profile.listeners_profile.as_ref(),
|
||||
profile.store_profile.as_ref(),
|
||||
profile.wallet_profile.as_ref(),
|
||||
profile.execution_profile.as_ref(),
|
||||
] {
|
||||
if let std::option::Option::Some(value) = reference
|
||||
&& let std::result::Result::Err(error) =
|
||||
crate::require_non_empty(value, "composition.profile.reference")
|
||||
{
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
}
|
||||
if !names.insert(profile.name.clone()) {
|
||||
@@ -338,14 +515,85 @@ pub fn validate_composition_document(document: &CompositionConfigDocument) -> ks
|
||||
};
|
||||
}
|
||||
|
||||
fn validate_logging_references_with_environment(
|
||||
path: &std::path::Path,
|
||||
workspace_root: &std::path::Path,
|
||||
composition: &CompositionConfigDocument,
|
||||
) -> ks_core::Result<()> {
|
||||
match crate::load_workspace_environment(workspace_root) {
|
||||
std::result::Result::Ok(_) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
let raw_json = match std::fs::read_to_string(path) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"composition_logging_file_read_failed",
|
||||
error.to_string(),
|
||||
));
|
||||
},
|
||||
};
|
||||
let resolved = crate::resolve_environment_placeholders(&raw_json);
|
||||
let value = match serde_json::from_str::<serde_json::Value>(&resolved) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"composition_logging_json_parse_failed",
|
||||
error.to_string(),
|
||||
));
|
||||
},
|
||||
};
|
||||
let default_profile = match value.get("default_profile").and_then(serde_json::Value::as_str) {
|
||||
std::option::Option::Some(value) if !value.trim().is_empty() => value,
|
||||
_ => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"composition_logging_default_profile_missing",
|
||||
path.display().to_string(),
|
||||
));
|
||||
},
|
||||
};
|
||||
let profiles = match value.get("profiles").and_then(serde_json::Value::as_array) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"composition_logging_profiles_missing",
|
||||
path.display().to_string(),
|
||||
));
|
||||
},
|
||||
};
|
||||
let mut names = std::collections::BTreeSet::<std::string::String>::new();
|
||||
for profile in profiles {
|
||||
if let std::option::Option::Some(name) =
|
||||
profile.get("name").and_then(serde_json::Value::as_str)
|
||||
{
|
||||
names.insert(name.to_string());
|
||||
}
|
||||
}
|
||||
if !names.contains(default_profile) {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"composition_logging_default_profile_not_found",
|
||||
default_profile.to_string(),
|
||||
));
|
||||
}
|
||||
for profile in &composition.profiles {
|
||||
let selected =
|
||||
resolve_profile_selection(profile.logging_profile.as_deref(), default_profile);
|
||||
if !names.contains(selected) {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"composition_logging_profile_not_found",
|
||||
selected.to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
const DEFAULT_COMPOSITION: &str =
|
||||
include_str!("../../config/kb-app-demo-desktop.default.config.json");
|
||||
const EXAMPLE_COMPOSITION: &str =
|
||||
include_str!("../../config/example.kb-app-demo-desktop.default.config.json");
|
||||
const DEFAULT_TRANSPORT: &str = include_str!("../../config/transport.config.json");
|
||||
const DEFAULT_LISTENERS: &str = include_str!("../../config/listeners.config.json");
|
||||
|
||||
#[test]
|
||||
fn default_and_example_compositions_validate() {
|
||||
@@ -354,20 +602,19 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_composition_resolves_shared_transport_and_listener_profiles() {
|
||||
fn local_devnet_composition_uses_shared_defaults() {
|
||||
let composition = match super::parse_composition_json(DEFAULT_COMPOSITION) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("default composition must parse: {error}"),
|
||||
};
|
||||
let transport = match crate::parse_transport_json(DEFAULT_TRANSPORT) {
|
||||
let profile = match super::composition_profile(&composition, "local_devnet") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("default transport must parse: {error}"),
|
||||
std::result::Result::Err(error) => {
|
||||
panic!("local Devnet composition must resolve: {error}")
|
||||
},
|
||||
};
|
||||
let listeners = match crate::parse_listeners_json(DEFAULT_LISTENERS) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("default listeners must parse: {error}"),
|
||||
};
|
||||
let resolved = super::compose_app_config(&composition, &transport, &listeners);
|
||||
assert!(resolved.is_ok());
|
||||
assert!(profile.logging_profile.is_none());
|
||||
assert!(profile.transport_profile.is_none());
|
||||
assert!(profile.store_profile.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
172
ks-config/src/execution.rs
Normal file
172
ks-config/src/execution.rs
Normal file
@@ -0,0 +1,172 @@
|
||||
// file: ks-config/src/execution.rs
|
||||
// version: 1
|
||||
|
||||
//! Independent execution policy configuration document loading and validation.
|
||||
|
||||
const EXECUTION_JSON_SCHEMA: &str =
|
||||
include_str!("../../config/schemas/execution.config.schema.json");
|
||||
|
||||
/// Root execution policy document with one default profile and named alternatives.
|
||||
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize)]
|
||||
pub struct ExecutionConfigDocument {
|
||||
/// Default execution profile used when a composition does not override it.
|
||||
pub default_profile: std::string::String,
|
||||
/// Named execution policy profiles.
|
||||
pub profiles: std::vec::Vec<ExecutionProfileConfig>,
|
||||
}
|
||||
|
||||
/// Named execution policy profile.
|
||||
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize)]
|
||||
pub struct ExecutionProfileConfig {
|
||||
/// Profile code.
|
||||
pub name: std::string::String,
|
||||
/// Runtime execution safety configuration.
|
||||
#[serde(flatten)]
|
||||
pub config: crate::ExecutionConfig,
|
||||
}
|
||||
|
||||
/// Returns the embedded execution JSON Schema text.
|
||||
pub fn execution_json_schema_text() -> &'static str {
|
||||
return EXECUTION_JSON_SCHEMA;
|
||||
}
|
||||
|
||||
/// Parses and validates one execution configuration document.
|
||||
pub fn parse_execution_json(raw_json: &str) -> ks_core::Result<ExecutionConfigDocument> {
|
||||
let schema = match serde_json::from_str::<serde_json::Value>(EXECUTION_JSON_SCHEMA) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"execution_config_schema_parse_failed",
|
||||
error.to_string(),
|
||||
));
|
||||
},
|
||||
};
|
||||
let instance = match serde_json::from_str::<serde_json::Value>(raw_json) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"execution_config_json_parse_failed",
|
||||
error.to_string(),
|
||||
));
|
||||
},
|
||||
};
|
||||
let validator = match jsonschema::validator_for(&schema) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"execution_config_schema_compile_failed",
|
||||
error.to_string(),
|
||||
));
|
||||
},
|
||||
};
|
||||
if let std::result::Result::Err(error) = validator.validate(&instance) {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"execution_config_schema_validation_failed",
|
||||
error.to_string(),
|
||||
));
|
||||
}
|
||||
let document = match serde_json::from_str::<ExecutionConfigDocument>(raw_json) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"execution_config_json_decode_failed",
|
||||
error.to_string(),
|
||||
));
|
||||
},
|
||||
};
|
||||
return match validate_execution_document(&document) {
|
||||
std::result::Result::Ok(()) => std::result::Result::Ok(document),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
|
||||
/// Loads environment placeholders and parses one execution configuration file.
|
||||
pub fn read_execution_json_file_with_environment(
|
||||
path: &std::path::Path,
|
||||
workspace_root: &std::path::Path,
|
||||
) -> ks_core::Result<ExecutionConfigDocument> {
|
||||
match crate::load_workspace_environment(workspace_root) {
|
||||
std::result::Result::Ok(_) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
let raw_json = match std::fs::read_to_string(path) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"execution_config_file_read_failed",
|
||||
error.to_string(),
|
||||
));
|
||||
},
|
||||
};
|
||||
return parse_execution_json(&crate::resolve_environment_placeholders(&raw_json));
|
||||
}
|
||||
|
||||
/// Returns one named execution profile.
|
||||
pub fn execution_profile<'a>(
|
||||
document: &'a ExecutionConfigDocument,
|
||||
profile_name: &str,
|
||||
) -> ks_core::Result<&'a ExecutionProfileConfig> {
|
||||
for profile in &document.profiles {
|
||||
if profile.name == profile_name {
|
||||
return std::result::Result::Ok(profile);
|
||||
}
|
||||
}
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"execution_profile_not_found",
|
||||
profile_name.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
/// Returns the default execution profile.
|
||||
pub fn default_execution_profile(
|
||||
document: &ExecutionConfigDocument,
|
||||
) -> ks_core::Result<&ExecutionProfileConfig> {
|
||||
return execution_profile(document, document.default_profile.as_str());
|
||||
}
|
||||
|
||||
/// Validates one typed execution configuration document.
|
||||
pub fn validate_execution_document(document: &ExecutionConfigDocument) -> ks_core::Result<()> {
|
||||
match crate::require_non_empty(&document.default_profile, "execution.default_profile") {
|
||||
std::result::Result::Ok(()) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
if document.profiles.is_empty() {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"execution_config_profiles_empty",
|
||||
"at least one execution profile is required",
|
||||
));
|
||||
}
|
||||
let mut names = std::collections::BTreeSet::<std::string::String>::new();
|
||||
for profile in &document.profiles {
|
||||
match crate::require_non_empty(&profile.name, "execution.profile.name") {
|
||||
std::result::Result::Ok(()) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
if !names.insert(profile.name.clone()) {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"execution_profile_name_duplicate",
|
||||
profile.name.clone(),
|
||||
));
|
||||
}
|
||||
match crate::validate_execution(&profile.config) {
|
||||
std::result::Result::Ok(()) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
}
|
||||
return match default_execution_profile(document) {
|
||||
std::result::Result::Ok(_) => std::result::Result::Ok(()),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
const DEFAULT_EXECUTION: &str = include_str!("../../config/execution.config.json");
|
||||
const EXAMPLE_EXECUTION: &str = include_str!("../../config/example.execution.config.json");
|
||||
|
||||
#[test]
|
||||
fn default_and_example_execution_configs_validate() {
|
||||
assert!(super::parse_execution_json(DEFAULT_EXECUTION).is_ok());
|
||||
assert!(super::parse_execution_json(EXAMPLE_EXECUTION).is_ok());
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,19 @@
|
||||
// file: ks-config/src/lib.rs
|
||||
// version: 7
|
||||
// version: 8
|
||||
|
||||
//! Khadhroony Solana application configuration contract and loading helpers.
|
||||
//! Khadhroony Solana shared configuration contracts and composition helpers.
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
mod composition;
|
||||
mod environment;
|
||||
mod execution;
|
||||
mod listeners;
|
||||
mod settings;
|
||||
mod store;
|
||||
mod transport;
|
||||
mod wallet;
|
||||
|
||||
/// Exposes the resolved application configuration plus its composition source.
|
||||
pub use self::composition::ComposedAppConfig;
|
||||
@@ -38,8 +41,12 @@ pub use self::composition::read_composed_app_config_with_environment;
|
||||
pub use self::composition::read_composition_json_file;
|
||||
/// Exposes binary composition loading with workspace environment resolution.
|
||||
pub use self::composition::read_composition_json_file_with_environment;
|
||||
/// Exposes canonical shared configuration loading without an application composition file.
|
||||
pub use self::composition::read_default_shared_app_config_with_environment;
|
||||
/// Exposes composition source path resolution.
|
||||
pub use self::composition::resolve_composition_source_path;
|
||||
/// Exposes composition profile fallback resolution.
|
||||
pub use self::composition::resolve_profile_selection;
|
||||
/// Exposes typed binary composition validation.
|
||||
pub use self::composition::validate_composition_document;
|
||||
/// Exposes binary composition JSON Schema validation.
|
||||
@@ -50,12 +57,28 @@ pub use self::environment::EnvironmentLoadReport;
|
||||
pub use self::environment::load_workspace_environment;
|
||||
/// Exposes environment placeholder resolution.
|
||||
pub use self::environment::resolve_environment_placeholders;
|
||||
/// Exposes the independent execution configuration document.
|
||||
pub use self::execution::ExecutionConfigDocument;
|
||||
/// Exposes one named execution profile.
|
||||
pub use self::execution::ExecutionProfileConfig;
|
||||
/// Exposes the default execution profile resolver.
|
||||
pub use self::execution::default_execution_profile;
|
||||
/// Exposes the execution JSON Schema text.
|
||||
pub use self::execution::execution_json_schema_text;
|
||||
/// Exposes one named execution profile resolver.
|
||||
pub use self::execution::execution_profile;
|
||||
/// Exposes execution configuration parsing from JSON.
|
||||
pub use self::execution::parse_execution_json;
|
||||
/// Exposes execution configuration loading with workspace environment resolution.
|
||||
pub use self::execution::read_execution_json_file_with_environment;
|
||||
/// Exposes typed execution document validation.
|
||||
pub use self::execution::validate_execution_document;
|
||||
/// Exposes the independent listener configuration document.
|
||||
pub use self::listeners::ListenersConfigDocument;
|
||||
/// Exposes one named listener profile.
|
||||
pub use self::listeners::ListenersProfileConfig;
|
||||
/// Exposes the active listener profile resolver.
|
||||
pub use self::listeners::active_listeners_profile;
|
||||
/// Exposes the default listener profile resolver.
|
||||
pub use self::listeners::default_listeners_profile;
|
||||
/// Exposes the listener JSON Schema text.
|
||||
pub use self::listeners::listeners_json_schema_text;
|
||||
/// Exposes the listener JSON Schema value parser.
|
||||
@@ -76,15 +99,13 @@ pub use self::listeners::validate_listeners_document;
|
||||
pub use self::listeners::validate_listeners_json_schema;
|
||||
/// Exposes the account listener configuration type.
|
||||
pub use self::settings::AccountListenerConfig;
|
||||
/// Exposes the root application configuration type.
|
||||
/// Exposes the root resolved runtime configuration type.
|
||||
pub use self::settings::AppConfig;
|
||||
/// Exposes the application section configuration type.
|
||||
/// Exposes the application metadata configuration type.
|
||||
pub use self::settings::AppSectionConfig;
|
||||
/// Exposes the local data configuration type.
|
||||
pub use self::settings::DataConfig;
|
||||
/// Exposes the database configuration type.
|
||||
pub use self::settings::DatabaseConfig;
|
||||
/// Exposes the demo application configuration type.
|
||||
/// Exposes the temporary demo application configuration type.
|
||||
pub use self::settings::DemoConfig;
|
||||
/// Exposes the endpoint role configuration type.
|
||||
pub use self::settings::EndpointRoleConfig;
|
||||
@@ -118,9 +139,9 @@ pub use self::settings::config_json_schema_text;
|
||||
pub use self::settings::config_json_schema_value;
|
||||
/// Exposes resolved runtime configuration parsing from a JSON string.
|
||||
pub use self::settings::parse_config_json;
|
||||
/// Exposes resolved runtime configuration loading from a filesystem path.
|
||||
/// Exposes resolved runtime snapshot loading from a filesystem path.
|
||||
pub use self::settings::read_config_json_file;
|
||||
/// Exposes resolved runtime configuration loading with workspace environment resolution.
|
||||
/// Exposes resolved runtime snapshot loading with workspace environment resolution.
|
||||
pub use self::settings::read_config_json_file_with_environment;
|
||||
/// Exposes the compact JSON serializer for resolved runtime configuration values.
|
||||
pub use self::settings::serialize_config_json;
|
||||
@@ -130,16 +151,42 @@ pub use self::settings::serialize_config_json_pretty;
|
||||
pub use self::settings::validate_config;
|
||||
/// Exposes the JSON Schema validator for raw resolved runtime configuration JSON.
|
||||
pub use self::settings::validate_config_json_schema;
|
||||
/// Exposes the independent store configuration document.
|
||||
pub use self::store::StoreConfigDocument;
|
||||
/// Exposes one named store profile.
|
||||
pub use self::store::StoreProfileConfig;
|
||||
/// Exposes the default store profile resolver.
|
||||
pub use self::store::default_store_profile;
|
||||
/// Exposes store configuration parsing from JSON.
|
||||
pub use self::store::parse_store_json;
|
||||
/// Exposes store configuration loading with workspace environment resolution.
|
||||
pub use self::store::read_store_json_file_with_environment;
|
||||
/// Exposes the store JSON Schema text.
|
||||
pub use self::store::store_json_schema_text;
|
||||
/// Exposes the store JSON Schema value parser.
|
||||
pub use self::store::store_json_schema_value;
|
||||
/// Exposes one named store profile resolver.
|
||||
pub use self::store::store_profile;
|
||||
/// Exposes typed store document validation.
|
||||
pub use self::store::validate_store_document;
|
||||
/// Exposes store JSON Schema validation.
|
||||
pub use self::store::validate_store_json_schema;
|
||||
/// Exposes the independent transport configuration document.
|
||||
pub use self::transport::TransportConfigDocument;
|
||||
/// Exposes one named transport profile.
|
||||
/// Exposes one fully resolved transport profile.
|
||||
pub use self::transport::TransportProfileConfig;
|
||||
/// Exposes the active transport profile resolver.
|
||||
pub use self::transport::active_transport_profile;
|
||||
/// Exposes one transport source profile.
|
||||
pub use self::transport::TransportProfileSourceConfig;
|
||||
/// Exposes WebSocket endpoint default values.
|
||||
pub use self::transport::WsEndpointDefaultsConfig;
|
||||
/// Exposes WebSocket endpoint-specific overrides.
|
||||
pub use self::transport::WsEndpointOverridesConfig;
|
||||
/// Exposes one source WebSocket endpoint before defaults are resolved.
|
||||
pub use self::transport::WsEndpointSourceConfig;
|
||||
/// Exposes the default transport profile resolver.
|
||||
pub use self::transport::default_transport_profile;
|
||||
/// Exposes transport configuration parsing from JSON.
|
||||
pub use self::transport::parse_transport_json;
|
||||
/// Exposes transport configuration loading from a filesystem path.
|
||||
pub use self::transport::read_transport_json_file;
|
||||
/// Exposes transport configuration loading with workspace environment resolution.
|
||||
pub use self::transport::read_transport_json_file_with_environment;
|
||||
/// Exposes the transport JSON Schema text.
|
||||
@@ -152,12 +199,36 @@ pub use self::transport::transport_profile;
|
||||
pub use self::transport::validate_transport_document;
|
||||
/// Exposes transport JSON Schema validation.
|
||||
pub use self::transport::validate_transport_json_schema;
|
||||
/// Exposes the independent wallet configuration document.
|
||||
pub use self::wallet::WalletConfigDocument;
|
||||
/// Exposes one named wallet source profile.
|
||||
pub use self::wallet::WalletProfileConfig;
|
||||
/// Exposes the default wallet source profile resolver.
|
||||
pub use self::wallet::default_wallet_profile;
|
||||
/// Exposes wallet configuration parsing from JSON.
|
||||
pub use self::wallet::parse_wallet_json;
|
||||
/// Exposes wallet configuration loading with workspace environment resolution.
|
||||
pub use self::wallet::read_wallet_json_file_with_environment;
|
||||
/// Exposes one resolved runtime wallet profile.
|
||||
pub use self::wallet::resolved_wallet_profile;
|
||||
/// Exposes typed wallet document validation.
|
||||
pub use self::wallet::validate_wallet_document;
|
||||
/// Exposes the wallet JSON Schema text.
|
||||
pub use self::wallet::wallet_json_schema_text;
|
||||
/// Exposes one named wallet source profile resolver.
|
||||
pub use self::wallet::wallet_profile;
|
||||
|
||||
/// Internal non-empty string validation shared by configuration documents.
|
||||
/// Keeps `require_non_empty` available to internal configuration modules.
|
||||
pub(crate) use self::settings::require_non_empty;
|
||||
/// Internal typed HTTP endpoint validation shared by resolved and transport documents.
|
||||
/// Keeps `validate_database` available to internal configuration modules.
|
||||
pub(crate) use self::settings::validate_database;
|
||||
/// Keeps `validate_execution` available to internal configuration modules.
|
||||
pub(crate) use self::settings::validate_execution;
|
||||
/// Keeps `validate_http_endpoint` available to internal configuration modules.
|
||||
pub(crate) use self::settings::validate_http_endpoint;
|
||||
/// Internal typed listener validation shared by resolved and listener documents.
|
||||
/// Keeps `validate_listeners` available to internal configuration modules.
|
||||
pub(crate) use self::settings::validate_listeners;
|
||||
/// Internal typed WebSocket endpoint validation shared by resolved and transport documents.
|
||||
/// Keeps `validate_wallet` available to internal configuration modules.
|
||||
pub(crate) use self::settings::validate_wallet;
|
||||
/// Keeps `validate_ws_endpoint` available to internal configuration modules.
|
||||
pub(crate) use self::settings::validate_ws_endpoint;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: ks-config/src/listeners.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Independent Solana listener configuration document loading and validation.
|
||||
|
||||
@@ -9,8 +9,8 @@ const LISTENERS_JSON_SCHEMA: &str =
|
||||
/// Root listener document containing independently selectable named profiles.
|
||||
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize)]
|
||||
pub struct ListenersConfigDocument {
|
||||
/// Active listener profile used by consumers that do not provide an explicit selection.
|
||||
pub active_profile: std::string::String,
|
||||
/// Default listener profile used by consumers that do not provide an explicit selection.
|
||||
pub default_profile: std::string::String,
|
||||
/// Named listener profiles available in this document.
|
||||
pub profiles: std::vec::Vec<ListenersProfileConfig>,
|
||||
}
|
||||
@@ -157,11 +157,11 @@ pub fn listeners_profile<'a>(
|
||||
));
|
||||
}
|
||||
|
||||
/// Returns the active listener profile declared by the document.
|
||||
pub fn active_listeners_profile(
|
||||
/// Returns the default listener profile declared by the document.
|
||||
pub fn default_listeners_profile(
|
||||
document: &ListenersConfigDocument,
|
||||
) -> ks_core::Result<&ListenersProfileConfig> {
|
||||
return listeners_profile(document, document.active_profile.as_str());
|
||||
return listeners_profile(document, document.default_profile.as_str());
|
||||
}
|
||||
|
||||
/// Converts one named listener profile into the resolved runtime listener contract.
|
||||
@@ -177,7 +177,7 @@ pub fn resolved_listener_config(profile: &ListenersProfileConfig) -> crate::List
|
||||
|
||||
/// Validates a typed listener configuration document.
|
||||
pub fn validate_listeners_document(document: &ListenersConfigDocument) -> ks_core::Result<()> {
|
||||
match crate::require_non_empty(&document.active_profile, "active_profile") {
|
||||
match crate::require_non_empty(&document.default_profile, "default_profile") {
|
||||
std::result::Result::Ok(()) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
@@ -205,7 +205,7 @@ pub fn validate_listeners_document(document: &ListenersConfigDocument) -> ks_cor
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
}
|
||||
return match active_listeners_profile(document) {
|
||||
return match default_listeners_profile(document) {
|
||||
std::result::Result::Ok(_) => std::result::Result::Ok(()),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: ks-config/src/settings.rs
|
||||
// version: 21
|
||||
// version: 22
|
||||
|
||||
//! Resolved runtime configuration models retained during the `0.5.1` source-document split.
|
||||
|
||||
@@ -31,8 +31,6 @@ pub struct ProfileConfig {
|
||||
pub app: AppSectionConfig,
|
||||
/// Database configuration.
|
||||
pub database: DatabaseConfig,
|
||||
/// Local data directories.
|
||||
pub data: DataConfig,
|
||||
/// Solana endpoint and listener configuration.
|
||||
pub solana: SolanaConfig,
|
||||
/// Wallet configuration.
|
||||
@@ -54,8 +52,6 @@ pub struct AppSectionConfig {
|
||||
pub name: std::string::String,
|
||||
/// Environment name.
|
||||
pub environment: std::string::String,
|
||||
/// Default auto reconnect flag used when a transport does not override it.
|
||||
pub auto_reconnect_default: bool,
|
||||
}
|
||||
|
||||
/// Database backend configuration.
|
||||
@@ -112,16 +108,6 @@ pub struct SqliteConfig {
|
||||
pub use_wal: bool,
|
||||
}
|
||||
|
||||
/// Local data directory configuration.
|
||||
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize, TS)]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ks_config/settings/DataConfig.ts")]
|
||||
pub struct DataConfig {
|
||||
/// Wallet directory path.
|
||||
pub wallets_directory: std::string::String,
|
||||
/// Logs directory path.
|
||||
pub logs_directory: std::string::String,
|
||||
}
|
||||
|
||||
/// Solana endpoints and listener configuration.
|
||||
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize, TS)]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ks_config/settings/SolanaConfig.ts")]
|
||||
@@ -317,14 +303,6 @@ pub struct WalletConfig {
|
||||
pub temporary_wallet_alias: std::string::String,
|
||||
/// Persists the managed temporary wallet between process restarts.
|
||||
pub temporary_wallet_persist: bool,
|
||||
/// Enables local validator transaction sending.
|
||||
pub localnet_send_enabled: bool,
|
||||
/// Enables devnet transaction sending.
|
||||
pub devnet_send_enabled: bool,
|
||||
/// Enables testnet transaction sending.
|
||||
pub testnet_send_enabled: bool,
|
||||
/// Enables mainnet transaction sending.
|
||||
pub mainnet_send_enabled: bool,
|
||||
}
|
||||
|
||||
/// Execution safety configuration.
|
||||
@@ -334,6 +312,14 @@ pub struct WalletConfig {
|
||||
export_to = "../frontend/ts/bindings/ks_config/settings/ExecutionConfig.ts"
|
||||
)]
|
||||
pub struct ExecutionConfig {
|
||||
/// Enables local validator transaction sending.
|
||||
pub localnet_send_enabled: bool,
|
||||
/// Enables devnet transaction sending.
|
||||
pub devnet_send_enabled: bool,
|
||||
/// Enables testnet transaction sending.
|
||||
pub testnet_send_enabled: bool,
|
||||
/// Enables mainnet transaction sending.
|
||||
pub mainnet_send_enabled: bool,
|
||||
/// Enables dry-run for newly prepared execution policies.
|
||||
pub dry_run_default: bool,
|
||||
/// Requires simulation before send.
|
||||
@@ -598,10 +584,6 @@ fn validate_profile(profile: &ProfileConfig) -> ks_core::Result<()> {
|
||||
std::result::Result::Ok(()) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
match validate_data(&profile.data) {
|
||||
std::result::Result::Ok(()) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
match validate_solana(&profile.solana) {
|
||||
std::result::Result::Ok(()) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
@@ -625,7 +607,7 @@ fn validate_app_section(config: &AppSectionConfig) -> ks_core::Result<()> {
|
||||
return require_non_empty(&config.environment, "app.environment");
|
||||
}
|
||||
|
||||
fn validate_database(config: &DatabaseConfig) -> ks_core::Result<()> {
|
||||
pub(crate) fn validate_database(config: &DatabaseConfig) -> ks_core::Result<()> {
|
||||
if config.backend != "postgres" && config.backend != "sqlite" {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"database_backend_invalid",
|
||||
@@ -651,14 +633,6 @@ fn validate_database(config: &DatabaseConfig) -> ks_core::Result<()> {
|
||||
return require_non_empty(&config.sqlite.path, "database.sqlite.path");
|
||||
}
|
||||
|
||||
fn validate_data(config: &DataConfig) -> ks_core::Result<()> {
|
||||
match require_non_empty(&config.wallets_directory, "data.wallets_directory") {
|
||||
std::result::Result::Ok(()) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
return require_non_empty(&config.logs_directory, "data.logs_directory");
|
||||
}
|
||||
|
||||
fn validate_solana(config: &SolanaConfig) -> ks_core::Result<()> {
|
||||
if config.http_endpoints.is_empty() {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
@@ -889,7 +863,7 @@ fn validate_account_listener(config: &AccountListenerConfig) -> ks_core::Result<
|
||||
return require_non_empty(&config.account_pubkey, "account_listener.account_pubkey");
|
||||
}
|
||||
|
||||
fn validate_wallet(config: &WalletConfig) -> ks_core::Result<()> {
|
||||
pub(crate) fn validate_wallet(config: &WalletConfig) -> ks_core::Result<()> {
|
||||
match require_non_empty(&config.wallet_dir, "wallet.wallet_dir") {
|
||||
std::result::Result::Ok(()) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
@@ -935,7 +909,7 @@ fn validate_wallet_alias(value: &str) -> ks_core::Result<()> {
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn validate_execution(config: &ExecutionConfig) -> ks_core::Result<()> {
|
||||
pub(crate) fn validate_execution(config: &ExecutionConfig) -> ks_core::Result<()> {
|
||||
let configured_spend = config
|
||||
.localnet_max_spend_lamports
|
||||
.saturating_add(config.devnet_max_spend_lamports)
|
||||
@@ -985,11 +959,15 @@ fn validate_wallet_execution_pair(
|
||||
execution: &ExecutionConfig,
|
||||
) -> ks_core::Result<()> {
|
||||
for (enabled, limit, cluster) in [
|
||||
(wallet.localnet_send_enabled, execution.localnet_max_spend_lamports, "localnet"),
|
||||
(wallet.devnet_send_enabled, execution.devnet_max_spend_lamports, "devnet"),
|
||||
(wallet.testnet_send_enabled, execution.testnet_max_spend_lamports, "testnet"),
|
||||
(
|
||||
wallet.mainnet_send_enabled,
|
||||
execution.localnet_send_enabled,
|
||||
execution.localnet_max_spend_lamports,
|
||||
"localnet",
|
||||
),
|
||||
(execution.devnet_send_enabled, execution.devnet_max_spend_lamports, "devnet"),
|
||||
(execution.testnet_send_enabled, execution.testnet_max_spend_lamports, "testnet"),
|
||||
(
|
||||
execution.mainnet_send_enabled,
|
||||
execution.mainnet_max_spend_lamports,
|
||||
"mainnet-beta",
|
||||
),
|
||||
@@ -1001,7 +979,7 @@ fn validate_wallet_execution_pair(
|
||||
));
|
||||
}
|
||||
}
|
||||
if wallet.mainnet_send_enabled && !execution.require_operator_confirmation {
|
||||
if execution.mainnet_send_enabled && !execution.require_operator_confirmation {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"execution_mainnet_send_without_confirmation",
|
||||
"mainnet sending requires operator confirmation",
|
||||
|
||||
186
ks-config/src/store.rs
Normal file
186
ks-config/src/store.rs
Normal file
@@ -0,0 +1,186 @@
|
||||
// file: ks-config/src/store.rs
|
||||
// version: 1
|
||||
|
||||
//! Independent store configuration document loading and validation.
|
||||
|
||||
const STORE_JSON_SCHEMA: &str = include_str!("../../config/schemas/store.config.schema.json");
|
||||
|
||||
/// Root store document with one default profile and named alternatives.
|
||||
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize)]
|
||||
pub struct StoreConfigDocument {
|
||||
/// Default store profile used when a composition does not override it.
|
||||
pub default_profile: std::string::String,
|
||||
/// Named store profiles.
|
||||
pub profiles: std::vec::Vec<StoreProfileConfig>,
|
||||
}
|
||||
|
||||
/// Named store profile.
|
||||
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize)]
|
||||
pub struct StoreProfileConfig {
|
||||
/// Profile code.
|
||||
pub name: std::string::String,
|
||||
/// Database backend configuration.
|
||||
pub database: crate::DatabaseConfig,
|
||||
}
|
||||
|
||||
/// Returns the embedded store JSON Schema text.
|
||||
pub fn store_json_schema_text() -> &'static str {
|
||||
return STORE_JSON_SCHEMA;
|
||||
}
|
||||
|
||||
/// Parses the embedded store JSON Schema into a JSON value.
|
||||
pub fn store_json_schema_value() -> ks_core::Result<serde_json::Value> {
|
||||
let result = serde_json::from_str::<serde_json::Value>(STORE_JSON_SCHEMA);
|
||||
return match result {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(ks_core::Error::new(
|
||||
"store_config_schema_parse_failed",
|
||||
error.to_string(),
|
||||
)),
|
||||
};
|
||||
}
|
||||
|
||||
/// Validates raw store JSON against the embedded schema.
|
||||
pub fn validate_store_json_schema(raw_json: &str) -> ks_core::Result<()> {
|
||||
let schema = match store_json_schema_value() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let instance = match serde_json::from_str::<serde_json::Value>(raw_json) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"store_config_json_parse_failed",
|
||||
error.to_string(),
|
||||
));
|
||||
},
|
||||
};
|
||||
let validator = match jsonschema::validator_for(&schema) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"store_config_schema_compile_failed",
|
||||
error.to_string(),
|
||||
));
|
||||
},
|
||||
};
|
||||
return match validator.validate(&instance) {
|
||||
std::result::Result::Ok(()) => std::result::Result::Ok(()),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(ks_core::Error::new(
|
||||
"store_config_schema_validation_failed",
|
||||
error.to_string(),
|
||||
)),
|
||||
};
|
||||
}
|
||||
|
||||
/// Parses and validates one store configuration document.
|
||||
pub fn parse_store_json(raw_json: &str) -> ks_core::Result<StoreConfigDocument> {
|
||||
match validate_store_json_schema(raw_json) {
|
||||
std::result::Result::Ok(()) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
let document = match serde_json::from_str::<StoreConfigDocument>(raw_json) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"store_config_json_decode_failed",
|
||||
error.to_string(),
|
||||
));
|
||||
},
|
||||
};
|
||||
return match validate_store_document(&document) {
|
||||
std::result::Result::Ok(()) => std::result::Result::Ok(document),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
|
||||
/// Loads environment placeholders and parses one store configuration file.
|
||||
pub fn read_store_json_file_with_environment(
|
||||
path: &std::path::Path,
|
||||
workspace_root: &std::path::Path,
|
||||
) -> ks_core::Result<StoreConfigDocument> {
|
||||
match crate::load_workspace_environment(workspace_root) {
|
||||
std::result::Result::Ok(_) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
let raw_json = match std::fs::read_to_string(path) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"store_config_file_read_failed",
|
||||
error.to_string(),
|
||||
));
|
||||
},
|
||||
};
|
||||
return parse_store_json(&crate::resolve_environment_placeholders(&raw_json));
|
||||
}
|
||||
|
||||
/// Returns one named store profile.
|
||||
pub fn store_profile<'a>(
|
||||
document: &'a StoreConfigDocument,
|
||||
profile_name: &str,
|
||||
) -> ks_core::Result<&'a StoreProfileConfig> {
|
||||
for profile in &document.profiles {
|
||||
if profile.name == profile_name {
|
||||
return std::result::Result::Ok(profile);
|
||||
}
|
||||
}
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"store_profile_not_found",
|
||||
profile_name.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
/// Returns the default store profile.
|
||||
pub fn default_store_profile(
|
||||
document: &StoreConfigDocument,
|
||||
) -> ks_core::Result<&StoreProfileConfig> {
|
||||
return store_profile(document, document.default_profile.as_str());
|
||||
}
|
||||
|
||||
/// Validates one typed store configuration document.
|
||||
pub fn validate_store_document(document: &StoreConfigDocument) -> ks_core::Result<()> {
|
||||
match crate::require_non_empty(&document.default_profile, "store.default_profile") {
|
||||
std::result::Result::Ok(()) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
if document.profiles.is_empty() {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"store_config_profiles_empty",
|
||||
"at least one store profile is required",
|
||||
));
|
||||
}
|
||||
let mut names = std::collections::BTreeSet::<std::string::String>::new();
|
||||
for profile in &document.profiles {
|
||||
match crate::require_non_empty(&profile.name, "store.profile.name") {
|
||||
std::result::Result::Ok(()) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
if !names.insert(profile.name.clone()) {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"store_profile_name_duplicate",
|
||||
profile.name.clone(),
|
||||
));
|
||||
}
|
||||
match crate::validate_database(&profile.database) {
|
||||
std::result::Result::Ok(()) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
}
|
||||
return match default_store_profile(document) {
|
||||
std::result::Result::Ok(_) => std::result::Result::Ok(()),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
const DEFAULT_STORE: &str = include_str!("../../config/store.config.json");
|
||||
const EXAMPLE_STORE: &str = include_str!("../../config/example.store.config.json");
|
||||
|
||||
#[test]
|
||||
fn default_and_example_store_configs_validate() {
|
||||
assert!(super::parse_store_json(DEFAULT_STORE).is_ok());
|
||||
assert!(super::parse_store_json(EXAMPLE_STORE).is_ok());
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,99 @@
|
||||
// file: ks-config/src/transport.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Independent Solana transport configuration document loading and validation.
|
||||
|
||||
const TRANSPORT_JSON_SCHEMA: &str =
|
||||
include_str!("../../config/schemas/transport.config.schema.json");
|
||||
|
||||
/// Root transport document containing independently selectable named profiles.
|
||||
/// Root transport document containing shared endpoint defaults and named profiles.
|
||||
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize)]
|
||||
pub struct TransportConfigDocument {
|
||||
/// Active transport profile used by consumers that do not provide an explicit selection.
|
||||
pub active_profile: std::string::String,
|
||||
/// Default transport profile used when a composition does not override it.
|
||||
pub default_profile: std::string::String,
|
||||
/// Named WebSocket endpoint defaults resolved before endpoint-specific overrides.
|
||||
pub ws_endpoint_defaults: std::vec::Vec<WsEndpointDefaultsConfig>,
|
||||
/// Named transport profiles available in this document.
|
||||
pub profiles: std::vec::Vec<TransportProfileConfig>,
|
||||
pub profiles: std::vec::Vec<TransportProfileSourceConfig>,
|
||||
}
|
||||
|
||||
/// Named Solana HTTP and WebSocket endpoint profile.
|
||||
/// Default values shared by one class of WebSocket endpoints.
|
||||
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize)]
|
||||
pub struct WsEndpointDefaultsConfig {
|
||||
/// Defaults code referenced by endpoints.
|
||||
pub name: std::string::String,
|
||||
/// Connection timeout in milliseconds.
|
||||
pub connect_timeout_ms: u64,
|
||||
/// Request timeout in milliseconds.
|
||||
pub request_timeout_ms: u64,
|
||||
/// Unsubscribe timeout in milliseconds.
|
||||
pub unsubscribe_timeout_ms: u64,
|
||||
/// Writer channel capacity.
|
||||
pub write_channel_capacity: u32,
|
||||
/// Event channel capacity.
|
||||
pub event_channel_capacity: u32,
|
||||
/// Default automatic reconnect policy.
|
||||
pub auto_reconnect: bool,
|
||||
}
|
||||
|
||||
/// Endpoint-specific overrides applied on top of named WebSocket defaults.
|
||||
#[derive(Clone, Debug, Default, serde::Deserialize, Eq, PartialEq, serde::Serialize)]
|
||||
pub struct WsEndpointOverridesConfig {
|
||||
/// Optional connection timeout override.
|
||||
pub connect_timeout_ms: std::option::Option<u64>,
|
||||
/// Optional request timeout override.
|
||||
pub request_timeout_ms: std::option::Option<u64>,
|
||||
/// Optional unsubscribe timeout override.
|
||||
pub unsubscribe_timeout_ms: std::option::Option<u64>,
|
||||
/// Optional writer channel capacity override.
|
||||
pub write_channel_capacity: std::option::Option<u32>,
|
||||
/// Optional event channel capacity override.
|
||||
pub event_channel_capacity: std::option::Option<u32>,
|
||||
/// Optional automatic reconnect override.
|
||||
pub auto_reconnect: std::option::Option<bool>,
|
||||
}
|
||||
|
||||
/// Source shape of one WebSocket endpoint before defaults are resolved.
|
||||
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize)]
|
||||
pub struct WsEndpointSourceConfig {
|
||||
/// Endpoint name.
|
||||
pub name: std::string::String,
|
||||
/// Named defaults to resolve before endpoint-specific overrides.
|
||||
pub defaults: std::string::String,
|
||||
/// Enables this endpoint.
|
||||
pub enabled: bool,
|
||||
/// Provider code.
|
||||
pub provider: std::string::String,
|
||||
/// Cluster code.
|
||||
pub cluster: std::string::String,
|
||||
/// Full endpoint URL.
|
||||
pub url: std::string::String,
|
||||
/// Optional endpoint-specific overrides.
|
||||
#[serde(default)]
|
||||
pub overrides: WsEndpointOverridesConfig,
|
||||
/// Role-specific limits for this URL.
|
||||
pub roles: std::vec::Vec<crate::EndpointRoleConfig>,
|
||||
}
|
||||
|
||||
/// Named Solana HTTP and WebSocket endpoint source profile.
|
||||
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize)]
|
||||
pub struct TransportProfileSourceConfig {
|
||||
/// Profile code.
|
||||
pub name: std::string::String,
|
||||
/// HTTP JSON-RPC endpoints.
|
||||
pub http_endpoints: std::vec::Vec<crate::HttpEndpointConfig>,
|
||||
/// Standard Solana WebSocket endpoints before shared defaults are resolved.
|
||||
pub ws_endpoints: std::vec::Vec<WsEndpointSourceConfig>,
|
||||
}
|
||||
|
||||
/// Fully resolved transport profile consumed by transport clients.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct TransportProfileConfig {
|
||||
/// Profile code.
|
||||
pub name: std::string::String,
|
||||
/// HTTP JSON-RPC endpoints.
|
||||
pub http_endpoints: std::vec::Vec<crate::HttpEndpointConfig>,
|
||||
/// Standard Solana WebSocket endpoints.
|
||||
/// Standard Solana WebSocket endpoints with defaults and overrides resolved.
|
||||
pub ws_endpoints: std::vec::Vec<crate::WsEndpointConfig>,
|
||||
}
|
||||
|
||||
@@ -33,9 +104,9 @@ pub fn transport_json_schema_text() -> &'static str {
|
||||
|
||||
/// Parses the embedded transport JSON Schema into a JSON value.
|
||||
pub fn transport_json_schema_value() -> ks_core::Result<serde_json::Value> {
|
||||
let schema_result = serde_json::from_str::<serde_json::Value>(TRANSPORT_JSON_SCHEMA);
|
||||
return match schema_result {
|
||||
std::result::Result::Ok(schema) => std::result::Result::Ok(schema),
|
||||
let result = serde_json::from_str::<serde_json::Value>(TRANSPORT_JSON_SCHEMA);
|
||||
return match result {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(ks_core::Error::new(
|
||||
"transport_config_schema_parse_failed",
|
||||
error.to_string(),
|
||||
@@ -46,11 +117,11 @@ pub fn transport_json_schema_value() -> ks_core::Result<serde_json::Value> {
|
||||
/// Validates raw transport JSON against the embedded transport schema.
|
||||
pub fn validate_transport_json_schema(raw_json: &str) -> ks_core::Result<()> {
|
||||
let schema = match transport_json_schema_value() {
|
||||
std::result::Result::Ok(schema) => schema,
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let instance = match serde_json::from_str::<serde_json::Value>(raw_json) {
|
||||
std::result::Result::Ok(instance) => instance,
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"transport_config_json_parse_failed",
|
||||
@@ -59,7 +130,7 @@ pub fn validate_transport_json_schema(raw_json: &str) -> ks_core::Result<()> {
|
||||
},
|
||||
};
|
||||
let validator = match jsonschema::validator_for(&schema) {
|
||||
std::result::Result::Ok(validator) => validator,
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"transport_config_schema_compile_failed",
|
||||
@@ -83,7 +154,7 @@ pub fn parse_transport_json(raw_json: &str) -> ks_core::Result<TransportConfigDo
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
let document = match serde_json::from_str::<TransportConfigDocument>(raw_json) {
|
||||
std::result::Result::Ok(document) => document,
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"transport_config_json_decode_failed",
|
||||
@@ -97,23 +168,7 @@ pub fn parse_transport_json(raw_json: &str) -> ks_core::Result<TransportConfigDo
|
||||
};
|
||||
}
|
||||
|
||||
/// Reads and parses one transport configuration document from a filesystem path.
|
||||
pub fn read_transport_json_file(
|
||||
path: &std::path::Path,
|
||||
) -> ks_core::Result<TransportConfigDocument> {
|
||||
let raw_json = match std::fs::read_to_string(path) {
|
||||
std::result::Result::Ok(content) => content,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"transport_config_file_read_failed",
|
||||
error.to_string(),
|
||||
));
|
||||
},
|
||||
};
|
||||
return parse_transport_json(&raw_json);
|
||||
}
|
||||
|
||||
/// Loads workspace environment values, resolves placeholders and parses transport configuration.
|
||||
/// Loads environment placeholders and parses one transport configuration file.
|
||||
pub fn read_transport_json_file_with_environment(
|
||||
path: &std::path::Path,
|
||||
workspace_root: &std::path::Path,
|
||||
@@ -123,7 +178,7 @@ pub fn read_transport_json_file_with_environment(
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
let raw_json = match std::fs::read_to_string(path) {
|
||||
std::result::Result::Ok(content) => content,
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"transport_config_file_read_failed",
|
||||
@@ -131,15 +186,97 @@ pub fn read_transport_json_file_with_environment(
|
||||
));
|
||||
},
|
||||
};
|
||||
let resolved = crate::resolve_environment_placeholders(&raw_json);
|
||||
return parse_transport_json(&resolved);
|
||||
return parse_transport_json(&crate::resolve_environment_placeholders(&raw_json));
|
||||
}
|
||||
|
||||
/// Returns one named transport profile.
|
||||
pub fn transport_profile<'a>(
|
||||
/// Resolves one named transport profile including WebSocket defaults and overrides.
|
||||
pub fn transport_profile(
|
||||
document: &TransportConfigDocument,
|
||||
profile_name: &str,
|
||||
) -> ks_core::Result<TransportProfileConfig> {
|
||||
let source = match source_transport_profile(document, profile_name) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mut ws_endpoints = std::vec::Vec::<crate::WsEndpointConfig>::new();
|
||||
for endpoint in &source.ws_endpoints {
|
||||
let defaults = match ws_endpoint_defaults(document, endpoint.defaults.as_str()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let resolved = crate::WsEndpointConfig {
|
||||
name: endpoint.name.clone(),
|
||||
enabled: endpoint.enabled,
|
||||
provider: endpoint.provider.clone(),
|
||||
cluster: endpoint.cluster.clone(),
|
||||
url: endpoint.url.clone(),
|
||||
connect_timeout_ms: if let std::option::Option::Some(value) =
|
||||
endpoint.overrides.connect_timeout_ms
|
||||
{
|
||||
value
|
||||
} else {
|
||||
defaults.connect_timeout_ms
|
||||
},
|
||||
request_timeout_ms: if let std::option::Option::Some(value) =
|
||||
endpoint.overrides.request_timeout_ms
|
||||
{
|
||||
value
|
||||
} else {
|
||||
defaults.request_timeout_ms
|
||||
},
|
||||
unsubscribe_timeout_ms: if let std::option::Option::Some(value) =
|
||||
endpoint.overrides.unsubscribe_timeout_ms
|
||||
{
|
||||
value
|
||||
} else {
|
||||
defaults.unsubscribe_timeout_ms
|
||||
},
|
||||
write_channel_capacity: if let std::option::Option::Some(value) =
|
||||
endpoint.overrides.write_channel_capacity
|
||||
{
|
||||
value
|
||||
} else {
|
||||
defaults.write_channel_capacity
|
||||
},
|
||||
event_channel_capacity: if let std::option::Option::Some(value) =
|
||||
endpoint.overrides.event_channel_capacity
|
||||
{
|
||||
value
|
||||
} else {
|
||||
defaults.event_channel_capacity
|
||||
},
|
||||
auto_reconnect: if let std::option::Option::Some(value) =
|
||||
endpoint.overrides.auto_reconnect
|
||||
{
|
||||
value
|
||||
} else {
|
||||
defaults.auto_reconnect
|
||||
},
|
||||
roles: endpoint.roles.clone(),
|
||||
};
|
||||
match crate::validate_ws_endpoint(&resolved) {
|
||||
std::result::Result::Ok(()) => ws_endpoints.push(resolved),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(TransportProfileConfig {
|
||||
name: source.name.clone(),
|
||||
http_endpoints: source.http_endpoints.clone(),
|
||||
ws_endpoints,
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns the default resolved transport profile.
|
||||
pub fn default_transport_profile(
|
||||
document: &TransportConfigDocument,
|
||||
) -> ks_core::Result<TransportProfileConfig> {
|
||||
return transport_profile(document, document.default_profile.as_str());
|
||||
}
|
||||
|
||||
fn source_transport_profile<'a>(
|
||||
document: &'a TransportConfigDocument,
|
||||
profile_name: &str,
|
||||
) -> ks_core::Result<&'a TransportProfileConfig> {
|
||||
) -> ks_core::Result<&'a TransportProfileSourceConfig> {
|
||||
for profile in &document.profiles {
|
||||
if profile.name == profile_name {
|
||||
return std::result::Result::Ok(profile);
|
||||
@@ -151,19 +288,57 @@ pub fn transport_profile<'a>(
|
||||
));
|
||||
}
|
||||
|
||||
/// Returns the active transport profile declared by the document.
|
||||
pub fn active_transport_profile(
|
||||
document: &TransportConfigDocument,
|
||||
) -> ks_core::Result<&TransportProfileConfig> {
|
||||
return transport_profile(document, document.active_profile.as_str());
|
||||
fn ws_endpoint_defaults<'a>(
|
||||
document: &'a TransportConfigDocument,
|
||||
defaults_name: &str,
|
||||
) -> ks_core::Result<&'a WsEndpointDefaultsConfig> {
|
||||
for defaults in &document.ws_endpoint_defaults {
|
||||
if defaults.name == defaults_name {
|
||||
return std::result::Result::Ok(defaults);
|
||||
}
|
||||
}
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"transport_ws_defaults_not_found",
|
||||
defaults_name.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
/// Validates a typed transport configuration document.
|
||||
pub fn validate_transport_document(document: &TransportConfigDocument) -> ks_core::Result<()> {
|
||||
match crate::require_non_empty(&document.active_profile, "active_profile") {
|
||||
match crate::require_non_empty(&document.default_profile, "transport.default_profile") {
|
||||
std::result::Result::Ok(()) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
if document.ws_endpoint_defaults.is_empty() {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"transport_ws_defaults_empty",
|
||||
"at least one WebSocket defaults profile is required",
|
||||
));
|
||||
}
|
||||
let mut defaults_names = std::collections::BTreeSet::<std::string::String>::new();
|
||||
for defaults in &document.ws_endpoint_defaults {
|
||||
match crate::require_non_empty(&defaults.name, "transport.ws_endpoint_defaults.name") {
|
||||
std::result::Result::Ok(()) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
if !defaults_names.insert(defaults.name.clone()) {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"transport_ws_defaults_duplicate",
|
||||
defaults.name.clone(),
|
||||
));
|
||||
}
|
||||
if defaults.connect_timeout_ms == 0
|
||||
|| defaults.request_timeout_ms == 0
|
||||
|| defaults.unsubscribe_timeout_ms == 0
|
||||
|| defaults.write_channel_capacity == 0
|
||||
|| defaults.event_channel_capacity == 0
|
||||
{
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"transport_ws_defaults_limit_zero",
|
||||
defaults.name.clone(),
|
||||
));
|
||||
}
|
||||
}
|
||||
if document.profiles.is_empty() {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"transport_config_profiles_empty",
|
||||
@@ -195,13 +370,21 @@ pub fn validate_transport_document(document: &TransportConfigDocument) -> ks_cor
|
||||
}
|
||||
}
|
||||
for endpoint in &profile.ws_endpoints {
|
||||
match crate::validate_ws_endpoint(endpoint) {
|
||||
match crate::require_non_empty(&endpoint.defaults, "transport.ws_endpoint.defaults") {
|
||||
std::result::Result::Ok(()) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
match ws_endpoint_defaults(document, endpoint.defaults.as_str()) {
|
||||
std::result::Result::Ok(_) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
}
|
||||
match transport_profile(document, profile.name.as_str()) {
|
||||
std::result::Result::Ok(_) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
}
|
||||
return match active_transport_profile(document) {
|
||||
return match default_transport_profile(document) {
|
||||
std::result::Result::Ok(_) => std::result::Result::Ok(()),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
@@ -219,22 +402,19 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schema_rejects_unsupported_transport_surface() {
|
||||
let value_result = serde_json::from_str::<serde_json::Value>(DEFAULT_TRANSPORT);
|
||||
let mut value = match value_result {
|
||||
fn websocket_defaults_are_resolved_before_endpoint_overrides() {
|
||||
let document = match super::parse_transport_json(DEFAULT_TRANSPORT) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("default transport must parse: {error}"),
|
||||
};
|
||||
let profile = match super::transport_profile(&document, "mainnet_research") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
panic!("default transport config must parse: {error}")
|
||||
panic!("mainnet research transport must resolve: {error}")
|
||||
},
|
||||
};
|
||||
value["profiles"][0]["advanced_streams"] = serde_json::json!([]);
|
||||
let raw_result = serde_json::to_string(&value);
|
||||
let raw = match raw_result {
|
||||
std::result::Result::Ok(raw) => raw,
|
||||
std::result::Result::Err(error) => {
|
||||
panic!("transport test value must serialize: {error}")
|
||||
},
|
||||
};
|
||||
assert!(super::validate_transport_json_schema(&raw).is_err());
|
||||
assert_eq!(profile.ws_endpoints[0].event_channel_capacity, 1024);
|
||||
assert_eq!(profile.ws_endpoints[1].event_channel_capacity, 4096);
|
||||
assert!(!profile.ws_endpoints[0].auto_reconnect);
|
||||
}
|
||||
}
|
||||
|
||||
216
ks-config/src/wallet.rs
Normal file
216
ks-config/src/wallet.rs
Normal file
@@ -0,0 +1,216 @@
|
||||
// file: ks-config/src/wallet.rs
|
||||
// version: 1
|
||||
|
||||
//! Independent wallet configuration document loading and validation.
|
||||
|
||||
const WALLET_JSON_SCHEMA: &str = include_str!("../../config/schemas/wallet.config.schema.json");
|
||||
|
||||
/// Root wallet document with global storage and named runtime profiles.
|
||||
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize)]
|
||||
pub struct WalletConfigDocument {
|
||||
/// Root directory for wallet storage, independent from profile selection.
|
||||
pub wallets_directory: std::string::String,
|
||||
/// Default wallet profile used when a composition does not override it.
|
||||
pub default_profile: std::string::String,
|
||||
/// Named wallet profiles.
|
||||
pub profiles: std::vec::Vec<WalletProfileConfig>,
|
||||
}
|
||||
|
||||
/// Named wallet profile whose directory is relative to the global wallet root unless absolute.
|
||||
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize)]
|
||||
pub struct WalletProfileConfig {
|
||||
/// Profile code.
|
||||
pub name: std::string::String,
|
||||
/// Wallet subdirectory or absolute directory.
|
||||
pub directory: std::string::String,
|
||||
/// Selected cluster name.
|
||||
pub cluster: std::string::String,
|
||||
/// Enables the managed temporary wallet for this profile.
|
||||
pub temporary_wallet_enabled: bool,
|
||||
/// Alias used for the managed temporary wallet.
|
||||
pub temporary_wallet_alias: std::string::String,
|
||||
/// Persists the managed temporary wallet between process restarts.
|
||||
pub temporary_wallet_persist: bool,
|
||||
}
|
||||
|
||||
/// Returns the embedded wallet JSON Schema text.
|
||||
pub fn wallet_json_schema_text() -> &'static str {
|
||||
return WALLET_JSON_SCHEMA;
|
||||
}
|
||||
|
||||
/// Parses and validates one wallet configuration document.
|
||||
pub fn parse_wallet_json(raw_json: &str) -> ks_core::Result<WalletConfigDocument> {
|
||||
let schema = match serde_json::from_str::<serde_json::Value>(WALLET_JSON_SCHEMA) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"wallet_config_schema_parse_failed",
|
||||
error.to_string(),
|
||||
));
|
||||
},
|
||||
};
|
||||
let instance = match serde_json::from_str::<serde_json::Value>(raw_json) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"wallet_config_json_parse_failed",
|
||||
error.to_string(),
|
||||
));
|
||||
},
|
||||
};
|
||||
let validator = match jsonschema::validator_for(&schema) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"wallet_config_schema_compile_failed",
|
||||
error.to_string(),
|
||||
));
|
||||
},
|
||||
};
|
||||
if let std::result::Result::Err(error) = validator.validate(&instance) {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"wallet_config_schema_validation_failed",
|
||||
error.to_string(),
|
||||
));
|
||||
}
|
||||
let document = match serde_json::from_str::<WalletConfigDocument>(raw_json) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"wallet_config_json_decode_failed",
|
||||
error.to_string(),
|
||||
));
|
||||
},
|
||||
};
|
||||
return match validate_wallet_document(&document) {
|
||||
std::result::Result::Ok(()) => std::result::Result::Ok(document),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
|
||||
/// Loads environment placeholders and parses one wallet configuration file.
|
||||
pub fn read_wallet_json_file_with_environment(
|
||||
path: &std::path::Path,
|
||||
workspace_root: &std::path::Path,
|
||||
) -> ks_core::Result<WalletConfigDocument> {
|
||||
match crate::load_workspace_environment(workspace_root) {
|
||||
std::result::Result::Ok(_) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
let raw_json = match std::fs::read_to_string(path) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"wallet_config_file_read_failed",
|
||||
error.to_string(),
|
||||
));
|
||||
},
|
||||
};
|
||||
return parse_wallet_json(&crate::resolve_environment_placeholders(&raw_json));
|
||||
}
|
||||
|
||||
/// Returns one named wallet source profile.
|
||||
pub fn wallet_profile<'a>(
|
||||
document: &'a WalletConfigDocument,
|
||||
profile_name: &str,
|
||||
) -> ks_core::Result<&'a WalletProfileConfig> {
|
||||
for profile in &document.profiles {
|
||||
if profile.name == profile_name {
|
||||
return std::result::Result::Ok(profile);
|
||||
}
|
||||
}
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"wallet_profile_not_found",
|
||||
profile_name.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
/// Returns the default wallet source profile.
|
||||
pub fn default_wallet_profile(
|
||||
document: &WalletConfigDocument,
|
||||
) -> ks_core::Result<&WalletProfileConfig> {
|
||||
return wallet_profile(document, document.default_profile.as_str());
|
||||
}
|
||||
|
||||
/// Resolves one source wallet profile into the runtime wallet contract.
|
||||
pub fn resolved_wallet_profile(
|
||||
document: &WalletConfigDocument,
|
||||
profile_name: &str,
|
||||
) -> ks_core::Result<crate::WalletConfig> {
|
||||
let profile = match wallet_profile(document, profile_name) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let configured = std::path::PathBuf::from(profile.directory.as_str());
|
||||
let wallet_dir = if configured.is_absolute() {
|
||||
configured
|
||||
} else {
|
||||
std::path::PathBuf::from(document.wallets_directory.as_str()).join(configured)
|
||||
};
|
||||
let resolved = crate::WalletConfig {
|
||||
wallet_dir: wallet_dir.display().to_string(),
|
||||
cluster: profile.cluster.clone(),
|
||||
temporary_wallet_enabled: profile.temporary_wallet_enabled,
|
||||
temporary_wallet_alias: profile.temporary_wallet_alias.clone(),
|
||||
temporary_wallet_persist: profile.temporary_wallet_persist,
|
||||
};
|
||||
return match crate::validate_wallet(&resolved) {
|
||||
std::result::Result::Ok(()) => std::result::Result::Ok(resolved),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
|
||||
/// Validates one typed wallet configuration document.
|
||||
pub fn validate_wallet_document(document: &WalletConfigDocument) -> ks_core::Result<()> {
|
||||
match crate::require_non_empty(&document.wallets_directory, "wallet.wallets_directory") {
|
||||
std::result::Result::Ok(()) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
match crate::require_non_empty(&document.default_profile, "wallet.default_profile") {
|
||||
std::result::Result::Ok(()) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
if document.profiles.is_empty() {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"wallet_config_profiles_empty",
|
||||
"at least one wallet profile is required",
|
||||
));
|
||||
}
|
||||
let mut names = std::collections::BTreeSet::<std::string::String>::new();
|
||||
for profile in &document.profiles {
|
||||
match crate::require_non_empty(&profile.name, "wallet.profile.name") {
|
||||
std::result::Result::Ok(()) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
match crate::require_non_empty(&profile.directory, "wallet.profile.directory") {
|
||||
std::result::Result::Ok(()) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
if !names.insert(profile.name.clone()) {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"wallet_profile_name_duplicate",
|
||||
profile.name.clone(),
|
||||
));
|
||||
}
|
||||
match resolved_wallet_profile(document, profile.name.as_str()) {
|
||||
std::result::Result::Ok(_) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
}
|
||||
return match default_wallet_profile(document) {
|
||||
std::result::Result::Ok(_) => std::result::Result::Ok(()),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
const DEFAULT_WALLET: &str = include_str!("../../config/wallet.config.json");
|
||||
const EXAMPLE_WALLET: &str = include_str!("../../config/example.wallet.config.json");
|
||||
|
||||
#[test]
|
||||
fn default_and_example_wallet_configs_validate() {
|
||||
assert!(super::parse_wallet_json(DEFAULT_WALLET).is_ok());
|
||||
assert!(super::parse_wallet_json(EXAMPLE_WALLET).is_ok());
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: ks-config/tests/external_composition_api.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! External API coverage for binary composition and shared configuration documents.
|
||||
|
||||
@@ -23,7 +23,31 @@ fn external_consumer_resolves_composition_through_crate_root_exports() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("listeners must parse: {error}"),
|
||||
};
|
||||
let resolved = match ks_config::compose_app_config(&composition, &transport, &listeners) {
|
||||
let store =
|
||||
match ks_config::parse_store_json(include_str!("../../config/example.store.config.json")) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("store must parse: {error}"),
|
||||
};
|
||||
let wallet =
|
||||
match ks_config::parse_wallet_json(include_str!("../../config/example.wallet.config.json"))
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("wallet must parse: {error}"),
|
||||
};
|
||||
let execution = match ks_config::parse_execution_json(include_str!(
|
||||
"../../config/example.execution.config.json"
|
||||
)) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("execution must parse: {error}"),
|
||||
};
|
||||
let resolved = match ks_config::compose_app_config(
|
||||
&composition,
|
||||
&transport,
|
||||
&listeners,
|
||||
&store,
|
||||
&wallet,
|
||||
&execution,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("composition must resolve: {error}"),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user