0.5.1-pre.006
This commit is contained in:
@@ -1,8 +1,17 @@
|
||||
<!-- file: ks-config/CHANGELOG.md -->
|
||||
<!-- version: 15 -->
|
||||
<!-- version: 16 -->
|
||||
|
||||
# CHANGELOG — ks-config
|
||||
|
||||
## `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` ;
|
||||
- extrait les endpoints HTTP/WebSocket vers `config/transport.config.json` et les listeners vers `config/listeners.config.json`, chacun avec schéma et exemple dédiés sous `config/schemas/` / `config/` ;
|
||||
- conserve `AppConfig`/`ProfileConfig` comme contrat runtime résolu transitoire, reconstruit depuis la composition et les documents partagés ;
|
||||
- ajoute les APIs typées de chargement, validation et sélection de profils de composition, transport et listeners ;
|
||||
- ajoute un test d’API externe garantissant que composition, transport et listeners restent accessibles uniquement via les exports crate-root ;
|
||||
- réserve le split store/data, wallet, execution et configuration spécifique des binaires à `pre.007`, avant les surfaces publiques sûres de `pre.008`.
|
||||
|
||||
## `0.5.1-pre.005`
|
||||
|
||||
- retire définitivement le bloc `logging` de `ProfileConfig` et du schéma général ;
|
||||
|
||||
@@ -1,43 +1,59 @@
|
||||
<!-- file: ks-config/README.md -->
|
||||
<!-- version: 8 -->
|
||||
<!-- version: 9 -->
|
||||
|
||||
# ks-config
|
||||
|
||||
`ks-config` définit le contrat de configuration générale typé du workspace, son schéma JSON embarqué et les fonctions de chargement, résolution d’environnement, validation et sérialisation.
|
||||
`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.
|
||||
|
||||
## Responsabilités
|
||||
|
||||
- exposer `AppConfig` et les sections de configuration générale ;
|
||||
- valider le JSON général contre `config/schemas/app.config.schema.json` ;
|
||||
- appliquer les invariants métier après désérialisation ;
|
||||
- charger `.env`, ou le fichier sélectionné par `KS_ENV_FILE`, depuis la racine du workspace ;
|
||||
- résoudre les placeholders namespacés `${KS_*}` / `${KB_*}` et leurs fallbacks ;
|
||||
- sélectionner le profil applicatif actif ;
|
||||
- exporter les types généraux nécessaires au frontend avec `ts-rs`.
|
||||
- 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 ;
|
||||
- reconstruire temporairement `AppConfig/ProfileConfig` pour les consommateurs existants ;
|
||||
- rester indépendant de la liste des applications et workers futurs.
|
||||
|
||||
## Hors périmètre
|
||||
## Composition
|
||||
|
||||
`ks-config` ne possède plus le contrat logging. `LoggingConfig`, les routes, filtres, profils logging et `config/schemas/logging.config.schema.json` appartiennent à `ks-logging`.
|
||||
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`.
|
||||
|
||||
La crate n’initialise ni PostgreSQL, ni les transports et ne manipule aucun secret de wallet. Elle fournit uniquement la configuration générale validée à ces consommateurs.
|
||||
Une composition choisit :
|
||||
|
||||
## Surface publique
|
||||
- les documents partagés à charger ;
|
||||
- le profil logging ;
|
||||
- le profil transport ;
|
||||
- le profil listeners ;
|
||||
- temporairement les sections encore non séparées.
|
||||
|
||||
Les principales fonctions sont `read_config_json_file_with_environment`, `parse_config_json`, `validate_config`, `validate_config_json_schema`, `active_profile` et les sérialiseurs JSON.
|
||||
Un futur worker pourra donc ajouter son propre `<binary>.default.config.json` sans créer une branche spécifique dans `ks-config`.
|
||||
|
||||
Les types publics couvrent les profils applicatifs, endpoints, listeners, base de données, wallet, exécution et démonstration.
|
||||
## Documents spécialisés actuels
|
||||
|
||||
## Relations
|
||||
- `config/transport.config.json` / `config/schemas/transport.config.schema.json` ;
|
||||
- `config/listeners.config.json` / `config/schemas/listeners.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.
|
||||
|
||||
- dépend de `ks-core` pour les erreurs structurées ;
|
||||
- fournit les profils généraux à `ks-store`, `ks-onchain-transport`, `ks-pipeline`, `ks-wallet` et aux applications ;
|
||||
- fournit à `ks-logging` les helpers génériques de chargement `.env` et de résolution des placeholders ;
|
||||
- embarque [`../config/schemas/app.config.schema.json`](../config/schemas/app.config.schema.json) ;
|
||||
- utilise [`../config/app.config.json`](../config/app.config.json) comme configuration générale chargée par défaut et [`../config/example.app.config.json`](../config/example.app.config.json) comme exemple minimal conforme.
|
||||
Les anciens `config/app.config.json` et `config/example.app.config.json` ne sont plus des fichiers runtime.
|
||||
|
||||
## Statut
|
||||
## API publique principale
|
||||
|
||||
Le split config/logging est effectif : un profil applicatif ne transporte plus de bloc logging et la sélection logging est indépendante. La prerelease suivante sépare les représentations source/runtime/public/diagnostic et ferme l’exposition des secrets résolus.
|
||||
- `read_composed_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` ;
|
||||
- `load_workspace_environment`, `resolve_environment_placeholders` ;
|
||||
- `AppConfig`, `ProfileConfig` pendant la phase de compatibilité.
|
||||
|
||||
## Frontières
|
||||
|
||||
`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.
|
||||
|
||||
Les sections database/data, wallet, execution et demo restent transitoires dans la composition jusqu'au split suivant.
|
||||
|
||||
## Documents
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<!-- file: ks-config/TODO.md -->
|
||||
<!-- version: 7 -->
|
||||
<!-- version: 8 -->
|
||||
|
||||
# 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.
|
||||
|
||||
@@ -1,143 +1,118 @@
|
||||
<!-- file: ks-config/USAGE.md -->
|
||||
<!-- version: 7 -->
|
||||
<!-- version: 8 -->
|
||||
|
||||
# Utilisation de ks-config
|
||||
|
||||
## Chargement recommandé
|
||||
## 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` ne doit pas connaître la liste des futurs workers ou applications.
|
||||
|
||||
## Chargement d'une composition
|
||||
|
||||
```rust
|
||||
let config_result = ks_config::read_config_json_file_with_environment(
|
||||
std::path::Path::new("config/app.config.json"),
|
||||
let composed = match ks_config::read_composed_app_config_with_environment(
|
||||
std::path::Path::new("config/kb-app-demo-desktop.default.config.json"),
|
||||
std::path::Path::new("."),
|
||||
);
|
||||
let config = match config_result {
|
||||
) {
|
||||
Ok(value) => value,
|
||||
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),
|
||||
};
|
||||
```
|
||||
|
||||
Cette API charge l’environnement du workspace, résout les placeholders puis applique successivement le schéma JSON général, la désérialisation typée et les invariants métier.
|
||||
Le chargeur :
|
||||
|
||||
Le fichier chargé par défaut par le desktop est `config/app.config.json`. `KS_CONFIG_PATH` permet de remplacer explicitement ce chemin.
|
||||
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.
|
||||
|
||||
## Chargement de l’environnement
|
||||
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.
|
||||
|
||||
## Configuration transport indépendante
|
||||
|
||||
```rust
|
||||
let transport = match ks_config::read_transport_json_file_with_environment(
|
||||
std::path::Path::new("config/transport.config.json"),
|
||||
std::path::Path::new("."),
|
||||
) {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let profile = match ks_config::transport_profile(&transport, "mainnet_research") {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
```
|
||||
|
||||
Le document expose `TransportConfigDocument` et `TransportProfileConfig` avec les endpoints HTTP/WebSocket et leurs rôles.
|
||||
|
||||
## Configuration listeners indépendante
|
||||
|
||||
```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),
|
||||
};
|
||||
```
|
||||
|
||||
`resolved_listener_config` convertit ce profil spécialisé vers le contrat runtime `ListenerConfig` utilisé pendant la phase de transition.
|
||||
|
||||
## Chargement de l'environnement
|
||||
|
||||
```rust
|
||||
let report = match ks_config::load_workspace_environment(std::path::Path::new(".")) {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
|
||||
if let Some(path) = report.loaded_path {
|
||||
println!("environment file: {}", path.display());
|
||||
}
|
||||
```
|
||||
|
||||
`EnvironmentLoadReport` indique le fichier chargé. 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.
|
||||
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.
|
||||
|
||||
Les contrats d’environnement utilisent `KS_*` pour Khadhroony Solana et `KB_*` pour les besoins réellement propres au Bot.
|
||||
|
||||
## Résolution explicite des placeholders
|
||||
## Résolution des placeholders
|
||||
|
||||
```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"));
|
||||
```
|
||||
|
||||
Cette API retourne encore une chaîne ordinaire pouvant contenir des secrets résolus. Elle reste strictement backend jusqu’à l’introduction de la représentation sensible de `0.5.1-pre.006`.
|
||||
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.
|
||||
|
||||
## Validation et parsing
|
||||
## Contrat runtime résolu
|
||||
|
||||
```rust
|
||||
if let Err(error) = ks_config::validate_config_json_schema(raw_json) {
|
||||
return Err(error);
|
||||
}
|
||||
let config = match ks_config::parse_config_json(raw_json) {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
if let Err(error) = ks_config::validate_config(&config) {
|
||||
return Err(error);
|
||||
}
|
||||
```
|
||||
`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.
|
||||
|
||||
`parse_config_json` effectue déjà les deux validations ; les appels séparés servent aux outils de diagnostic.
|
||||
|
||||
## Schéma embarqué
|
||||
|
||||
```rust
|
||||
let schema_text = ks_config::config_json_schema_text();
|
||||
let schema_value = match ks_config::config_json_schema_value() {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
|
||||
println!("embedded app schema bytes={}", schema_text.len());
|
||||
assert!(schema_value.is_object());
|
||||
```
|
||||
|
||||
Le schéma actif est [`../config/schemas/app.config.schema.json`](../config/schemas/app.config.schema.json). Les fichiers [`../config/app.config.json`](../config/app.config.json) et [`../config/example.app.config.json`](../config/example.app.config.json) doivent tous deux être conformes.
|
||||
|
||||
## Sélection du profil actif
|
||||
|
||||
```rust
|
||||
let profile = match ks_config::active_profile(&config) {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
|
||||
println!("active profile: {}", profile.name);
|
||||
println!("http endpoints: {}", profile.solana.http_endpoints.len());
|
||||
println!("websocket endpoints: {}", profile.solana.ws_endpoints.len());
|
||||
```
|
||||
|
||||
Le profil applicatif ne contient plus de configuration logging. Le document logging et son `active_profile` sont chargés par `ks-logging`.
|
||||
|
||||
## Sérialisation
|
||||
|
||||
```rust
|
||||
let pretty = match ks_config::serialize_config_json_pretty(&config) {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
|
||||
let write_result = std::fs::write("config/generated.app.config.json", pretty);
|
||||
if let Err(error) = write_result {
|
||||
return Err(ks_core::Error::new(
|
||||
"config_write_failed",
|
||||
format!("cannot write generated configuration: {error}"),
|
||||
));
|
||||
}
|
||||
```
|
||||
|
||||
La configuration est validée avant sérialisation.
|
||||
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.
|
||||
|
||||
## Types publics importants
|
||||
|
||||
- `AppConfig`, `ProfileConfig`, `AppSectionConfig` ;
|
||||
- `DatabaseConfig`, `PostgresConfig`, `SqliteConfig`, `DataConfig` ;
|
||||
- `SolanaConfig`, `HttpEndpointConfig`, `WsEndpointConfig`, `EndpointRoleConfig` ;
|
||||
- `CompositionConfigDocument`, `CompositionProfileConfig`, `CompositionConfigSources` ;
|
||||
- `TransportConfigDocument`, `TransportProfileConfig` ;
|
||||
- `ListenersConfigDocument`, `ListenersProfileConfig` ;
|
||||
- `AppConfig`, `ProfileConfig` comme contrat runtime de transition ;
|
||||
- `HttpEndpointConfig`, `WsEndpointConfig`, `EndpointRoleConfig` ;
|
||||
- `ListenerConfig` et ses variantes ;
|
||||
- `WalletConfig`, `ExecutionConfig`, `DemoConfig`.
|
||||
- `DatabaseConfig`, `WalletConfig`, `ExecutionConfig` jusqu'à leur split dédié.
|
||||
|
||||
Les types logging ne font plus partie de `ks-config`.
|
||||
Les types logging appartiennent à `ks-logging`.
|
||||
|
||||
## Erreurs et invariants
|
||||
## Prochaine frontière
|
||||
|
||||
Les erreurs utilisent `ks_core::Error` avec un code stable. Les validations couvrent notamment l’unicité des profils, l’existence du profil actif, les URLs, les rôles d’endpoints, les limites d’exécution et les contraintes wallet.
|
||||
|
||||
## Tests instructifs
|
||||
|
||||
Les tests vérifient séparément `app.config.json` et `example.app.config.json`, le roundtrip du document général et les invariants `parser_rejects_*` / `schema_rejects_*`.
|
||||
|
||||
## Limites
|
||||
|
||||
- format JSON uniquement ;
|
||||
- aucune connexion externe n’est ouverte par la crate ;
|
||||
- la politique de camouflage des valeurs résolues appartient à `0.5.1-pre.006`.
|
||||
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.
|
||||
|
||||
373
ks-config/src/composition.rs
Normal file
373
ks-config/src/composition.rs
Normal file
@@ -0,0 +1,373 @@
|
||||
// file: ks-config/src/composition.rs
|
||||
// version: 1
|
||||
|
||||
//! Binary composition configuration and resolution into the shared runtime profile contract.
|
||||
|
||||
const COMPOSITION_JSON_SCHEMA: &str =
|
||||
include_str!("../../config/schemas/composition.config.schema.json");
|
||||
|
||||
/// Root composition document used by a binary to select shared configuration profiles.
|
||||
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize)]
|
||||
pub struct CompositionConfigDocument {
|
||||
/// Active binary composition profile name.
|
||||
pub active_profile: std::string::String,
|
||||
/// Shared configuration documents referenced by this composition.
|
||||
pub sources: CompositionConfigSources,
|
||||
/// Named binary composition profiles.
|
||||
pub profiles: std::vec::Vec<CompositionProfileConfig>,
|
||||
}
|
||||
|
||||
/// Paths of shared configuration documents consumed by one binary composition.
|
||||
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize)]
|
||||
pub struct CompositionConfigSources {
|
||||
/// Logging configuration document path relative to the workspace root unless absolute.
|
||||
pub logging: std::string::String,
|
||||
/// Transport configuration document path relative to the workspace root unless absolute.
|
||||
pub transport: std::string::String,
|
||||
/// Listener configuration document path relative to the workspace root unless absolute.
|
||||
pub listeners: std::string::String,
|
||||
}
|
||||
|
||||
/// Named composition profile referencing shared configuration profiles.
|
||||
#[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.
|
||||
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.
|
||||
pub demo: crate::DemoConfig,
|
||||
}
|
||||
|
||||
/// Returns the embedded binary composition JSON Schema text.
|
||||
pub fn composition_json_schema_text() -> &'static str {
|
||||
return COMPOSITION_JSON_SCHEMA;
|
||||
}
|
||||
|
||||
/// 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),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(ks_core::Error::new(
|
||||
"composition_config_schema_parse_failed",
|
||||
error.to_string(),
|
||||
)),
|
||||
};
|
||||
}
|
||||
|
||||
/// 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::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::Err(error) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"composition_config_json_parse_failed",
|
||||
error.to_string(),
|
||||
));
|
||||
},
|
||||
};
|
||||
let validator = match jsonschema::validator_for(&schema) {
|
||||
std::result::Result::Ok(validator) => validator,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"composition_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(
|
||||
"composition_config_schema_validation_failed",
|
||||
error.to_string(),
|
||||
)),
|
||||
};
|
||||
}
|
||||
|
||||
/// Parses and validates one binary composition document.
|
||||
pub fn parse_composition_json(raw_json: &str) -> ks_core::Result<CompositionConfigDocument> {
|
||||
match validate_composition_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::<CompositionConfigDocument>(raw_json) {
|
||||
std::result::Result::Ok(document) => document,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"composition_config_json_decode_failed",
|
||||
error.to_string(),
|
||||
));
|
||||
},
|
||||
};
|
||||
return match validate_composition_document(&document) {
|
||||
std::result::Result::Ok(()) => std::result::Result::Ok(document),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
|
||||
/// Reads and parses one binary composition document from a filesystem path.
|
||||
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::Err(error) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"composition_config_file_read_failed",
|
||||
error.to_string(),
|
||||
));
|
||||
},
|
||||
};
|
||||
return parse_composition_json(&raw_json);
|
||||
}
|
||||
|
||||
/// Loads workspace environment values, resolves placeholders and parses one binary composition document.
|
||||
pub fn read_composition_json_file_with_environment(
|
||||
path: &std::path::Path,
|
||||
workspace_root: &std::path::Path,
|
||||
) -> ks_core::Result<CompositionConfigDocument> {
|
||||
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(content) => content,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"composition_config_file_read_failed",
|
||||
error.to_string(),
|
||||
));
|
||||
},
|
||||
};
|
||||
let resolved = crate::resolve_environment_placeholders(&raw_json);
|
||||
return parse_composition_json(&resolved);
|
||||
}
|
||||
|
||||
/// Returns one named binary composition profile.
|
||||
pub fn composition_profile<'a>(
|
||||
document: &'a CompositionConfigDocument,
|
||||
profile_name: &str,
|
||||
) -> ks_core::Result<&'a CompositionProfileConfig> {
|
||||
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(
|
||||
"composition_profile_not_found",
|
||||
profile_name.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
/// Returns the active binary composition profile.
|
||||
pub fn active_composition_profile(
|
||||
document: &CompositionConfigDocument,
|
||||
) -> ks_core::Result<&CompositionProfileConfig> {
|
||||
return composition_profile(document, document.active_profile.as_str());
|
||||
}
|
||||
|
||||
/// Resolves a source path relative to the workspace root unless already absolute.
|
||||
pub fn resolve_composition_source_path(
|
||||
workspace_root: &std::path::Path,
|
||||
configured_path: &str,
|
||||
) -> std::path::PathBuf {
|
||||
let path = std::path::PathBuf::from(configured_path);
|
||||
if path.is_absolute() {
|
||||
return path;
|
||||
}
|
||||
return workspace_root.join(path);
|
||||
}
|
||||
|
||||
/// Resolves all shared transport/listener selections into the existing runtime application contract.
|
||||
pub fn compose_app_config(
|
||||
document: &CompositionConfigDocument,
|
||||
transport: &crate::TransportConfigDocument,
|
||||
listeners: &crate::ListenersConfigDocument,
|
||||
) -> 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),
|
||||
};
|
||||
profiles.push(crate::ProfileConfig {
|
||||
name: profile.name.clone(),
|
||||
app: profile.app.clone(),
|
||||
database: profile.database.clone(),
|
||||
data: profile.data.clone(),
|
||||
solana: crate::SolanaConfig {
|
||||
http_endpoints: transport_profile.http_endpoints.clone(),
|
||||
ws_endpoints: transport_profile.ws_endpoints.clone(),
|
||||
listeners: crate::resolved_listener_config(listeners_profile),
|
||||
},
|
||||
wallet: profile.wallet.clone(),
|
||||
execution: profile.execution.clone(),
|
||||
demo: profile.demo.clone(),
|
||||
});
|
||||
}
|
||||
let config = crate::AppConfig {
|
||||
active_profile: document.active_profile.clone(),
|
||||
profiles,
|
||||
};
|
||||
return match crate::validate_config(&config) {
|
||||
std::result::Result::Ok(()) => std::result::Result::Ok(config),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
|
||||
/// Reads a binary composition and its referenced transport/listener documents into the resolved 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> {
|
||||
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 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 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 app_config = match compose_app_config(&composition, &transport, &listeners) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(crate::ComposedAppConfig {
|
||||
composition,
|
||||
app_config,
|
||||
transport_path,
|
||||
listeners_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,
|
||||
}
|
||||
|
||||
/// 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") {
|
||||
std::result::Result::Ok(()) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
for source in [
|
||||
&document.sources.logging,
|
||||
&document.sources.transport,
|
||||
&document.sources.listeners,
|
||||
] {
|
||||
match crate::require_non_empty(source, "composition.sources") {
|
||||
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(
|
||||
"composition_profiles_empty",
|
||||
"at least one composition 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, "composition.profile.name") {
|
||||
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),
|
||||
}
|
||||
}
|
||||
if !names.insert(profile.name.clone()) {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"composition_profile_name_duplicate",
|
||||
profile.name.clone(),
|
||||
));
|
||||
}
|
||||
}
|
||||
return match active_composition_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_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() {
|
||||
assert!(super::parse_composition_json(DEFAULT_COMPOSITION).is_ok());
|
||||
assert!(super::parse_composition_json(EXAMPLE_COMPOSITION).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_composition_resolves_shared_transport_and_listener_profiles() {
|
||||
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) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("default transport must parse: {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());
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,79 @@
|
||||
// file: ks-config/src/lib.rs
|
||||
// version: 6
|
||||
// version: 7
|
||||
|
||||
//! Khadhroony Solana application configuration contract and loading helpers.
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
mod composition;
|
||||
mod environment;
|
||||
mod listeners;
|
||||
mod settings;
|
||||
mod transport;
|
||||
|
||||
/// Exposes the resolved application configuration plus its composition source.
|
||||
pub use self::composition::ComposedAppConfig;
|
||||
/// Exposes the binary composition document type.
|
||||
pub use self::composition::CompositionConfigDocument;
|
||||
/// Exposes binary composition source paths.
|
||||
pub use self::composition::CompositionConfigSources;
|
||||
/// Exposes one named binary composition profile.
|
||||
pub use self::composition::CompositionProfileConfig;
|
||||
/// Exposes the active binary composition profile resolver.
|
||||
pub use self::composition::active_composition_profile;
|
||||
/// Exposes the resolved runtime composition builder.
|
||||
pub use self::composition::compose_app_config;
|
||||
/// Exposes the binary composition JSON Schema text.
|
||||
pub use self::composition::composition_json_schema_text;
|
||||
/// Exposes the binary composition JSON Schema value parser.
|
||||
pub use self::composition::composition_json_schema_value;
|
||||
/// Exposes one named binary composition profile resolver.
|
||||
pub use self::composition::composition_profile;
|
||||
/// Exposes binary composition parsing from JSON.
|
||||
pub use self::composition::parse_composition_json;
|
||||
/// Exposes composed application loading with referenced shared documents.
|
||||
pub use self::composition::read_composed_app_config_with_environment;
|
||||
/// Exposes binary composition loading from a filesystem path.
|
||||
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 composition source path resolution.
|
||||
pub use self::composition::resolve_composition_source_path;
|
||||
/// Exposes typed binary composition validation.
|
||||
pub use self::composition::validate_composition_document;
|
||||
/// Exposes binary composition JSON Schema validation.
|
||||
pub use self::composition::validate_composition_json_schema;
|
||||
/// Exposes the environment loading report.
|
||||
pub use self::environment::EnvironmentLoadReport;
|
||||
/// Exposes workspace environment-file loading.
|
||||
pub use self::environment::load_workspace_environment;
|
||||
/// Exposes environment placeholder resolution.
|
||||
pub use self::environment::resolve_environment_placeholders;
|
||||
/// 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 listener JSON Schema text.
|
||||
pub use self::listeners::listeners_json_schema_text;
|
||||
/// Exposes the listener JSON Schema value parser.
|
||||
pub use self::listeners::listeners_json_schema_value;
|
||||
/// Exposes one named listener profile resolver.
|
||||
pub use self::listeners::listeners_profile;
|
||||
/// Exposes listener configuration parsing from JSON.
|
||||
pub use self::listeners::parse_listeners_json;
|
||||
/// Exposes listener configuration loading from a filesystem path.
|
||||
pub use self::listeners::read_listeners_json_file;
|
||||
/// Exposes listener configuration loading with workspace environment resolution.
|
||||
pub use self::listeners::read_listeners_json_file_with_environment;
|
||||
/// Exposes conversion from a listener profile to the resolved runtime contract.
|
||||
pub use self::listeners::resolved_listener_config;
|
||||
/// Exposes typed listener document validation.
|
||||
pub use self::listeners::validate_listeners_document;
|
||||
/// Exposes listener JSON Schema validation.
|
||||
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.
|
||||
@@ -53,21 +112,52 @@ pub use self::settings::WalletConfig;
|
||||
pub use self::settings::WsEndpointConfig;
|
||||
/// Exposes the active profile resolver.
|
||||
pub use self::settings::active_profile;
|
||||
/// Exposes the embedded JSON Schema text.
|
||||
/// Exposes the embedded JSON Schema text for the resolved runtime contract.
|
||||
pub use self::settings::config_json_schema_text;
|
||||
/// Exposes the embedded JSON Schema value parser.
|
||||
/// Exposes the embedded JSON Schema value parser for the resolved runtime contract.
|
||||
pub use self::settings::config_json_schema_value;
|
||||
/// Exposes the configuration parser from a JSON string.
|
||||
/// Exposes resolved runtime configuration parsing from a JSON string.
|
||||
pub use self::settings::parse_config_json;
|
||||
/// Exposes the configuration loader from a filesystem path.
|
||||
/// Exposes resolved runtime configuration loading from a filesystem path.
|
||||
pub use self::settings::read_config_json_file;
|
||||
/// Exposes configuration loading with workspace environment resolution.
|
||||
/// Exposes resolved runtime configuration loading with workspace environment resolution.
|
||||
pub use self::settings::read_config_json_file_with_environment;
|
||||
/// Exposes the compact JSON serializer for configuration values.
|
||||
/// Exposes the compact JSON serializer for resolved runtime configuration values.
|
||||
pub use self::settings::serialize_config_json;
|
||||
/// Exposes the pretty JSON serializer for configuration values.
|
||||
/// Exposes the pretty JSON serializer for resolved runtime configuration values.
|
||||
pub use self::settings::serialize_config_json_pretty;
|
||||
/// Exposes the typed configuration validator.
|
||||
/// Exposes the typed resolved runtime configuration validator.
|
||||
pub use self::settings::validate_config;
|
||||
/// Exposes the JSON Schema validator for raw JSON configuration.
|
||||
/// Exposes the JSON Schema validator for raw resolved runtime configuration JSON.
|
||||
pub use self::settings::validate_config_json_schema;
|
||||
/// Exposes the independent transport configuration document.
|
||||
pub use self::transport::TransportConfigDocument;
|
||||
/// Exposes one named transport profile.
|
||||
pub use self::transport::TransportProfileConfig;
|
||||
/// Exposes the active transport profile resolver.
|
||||
pub use self::transport::active_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.
|
||||
pub use self::transport::transport_json_schema_text;
|
||||
/// Exposes the transport JSON Schema value parser.
|
||||
pub use self::transport::transport_json_schema_value;
|
||||
/// Exposes one named transport profile resolver.
|
||||
pub use self::transport::transport_profile;
|
||||
/// Exposes typed transport document validation.
|
||||
pub use self::transport::validate_transport_document;
|
||||
/// Exposes transport JSON Schema validation.
|
||||
pub use self::transport::validate_transport_json_schema;
|
||||
|
||||
/// Internal non-empty string validation shared by configuration documents.
|
||||
pub(crate) use self::settings::require_non_empty;
|
||||
/// Internal typed HTTP endpoint validation shared by resolved and transport documents.
|
||||
pub(crate) use self::settings::validate_http_endpoint;
|
||||
/// Internal typed listener validation shared by resolved and listener documents.
|
||||
pub(crate) use self::settings::validate_listeners;
|
||||
/// Internal typed WebSocket endpoint validation shared by resolved and transport documents.
|
||||
pub(crate) use self::settings::validate_ws_endpoint;
|
||||
|
||||
224
ks-config/src/listeners.rs
Normal file
224
ks-config/src/listeners.rs
Normal file
@@ -0,0 +1,224 @@
|
||||
// file: ks-config/src/listeners.rs
|
||||
// version: 1
|
||||
|
||||
//! Independent Solana listener configuration document loading and validation.
|
||||
|
||||
const LISTENERS_JSON_SCHEMA: &str =
|
||||
include_str!("../../config/schemas/listeners.config.schema.json");
|
||||
|
||||
/// 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,
|
||||
/// Named listener profiles available in this document.
|
||||
pub profiles: std::vec::Vec<ListenersProfileConfig>,
|
||||
}
|
||||
|
||||
/// Named listener profile.
|
||||
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize)]
|
||||
pub struct ListenersProfileConfig {
|
||||
/// Profile code.
|
||||
pub name: std::string::String,
|
||||
/// Enables listener creation.
|
||||
pub enabled: bool,
|
||||
/// Default commitment used by listener subscriptions.
|
||||
pub default_commitment: std::string::String,
|
||||
/// Log listeners filtered by program mentions.
|
||||
pub log_listeners: std::vec::Vec<crate::LogListenerConfig>,
|
||||
/// Program account listeners.
|
||||
pub program_listeners: std::vec::Vec<crate::ProgramListenerConfig>,
|
||||
/// Account listeners.
|
||||
pub account_listeners: std::vec::Vec<crate::AccountListenerConfig>,
|
||||
}
|
||||
|
||||
/// Returns the embedded listener JSON Schema text.
|
||||
pub fn listeners_json_schema_text() -> &'static str {
|
||||
return LISTENERS_JSON_SCHEMA;
|
||||
}
|
||||
|
||||
/// Parses the embedded listener JSON Schema into a JSON value.
|
||||
pub fn listeners_json_schema_value() -> ks_core::Result<serde_json::Value> {
|
||||
let schema_result = serde_json::from_str::<serde_json::Value>(LISTENERS_JSON_SCHEMA);
|
||||
return match schema_result {
|
||||
std::result::Result::Ok(schema) => std::result::Result::Ok(schema),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(ks_core::Error::new(
|
||||
"listeners_config_schema_parse_failed",
|
||||
error.to_string(),
|
||||
)),
|
||||
};
|
||||
}
|
||||
|
||||
/// Validates raw listener JSON against the embedded listener schema.
|
||||
pub fn validate_listeners_json_schema(raw_json: &str) -> ks_core::Result<()> {
|
||||
let schema = match listeners_json_schema_value() {
|
||||
std::result::Result::Ok(schema) => schema,
|
||||
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::Err(error) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"listeners_config_json_parse_failed",
|
||||
error.to_string(),
|
||||
));
|
||||
},
|
||||
};
|
||||
let validator = match jsonschema::validator_for(&schema) {
|
||||
std::result::Result::Ok(validator) => validator,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"listeners_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(
|
||||
"listeners_config_schema_validation_failed",
|
||||
error.to_string(),
|
||||
)),
|
||||
};
|
||||
}
|
||||
|
||||
/// Parses and validates one independent listener configuration document.
|
||||
pub fn parse_listeners_json(raw_json: &str) -> ks_core::Result<ListenersConfigDocument> {
|
||||
match validate_listeners_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::<ListenersConfigDocument>(raw_json) {
|
||||
std::result::Result::Ok(document) => document,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"listeners_config_json_decode_failed",
|
||||
error.to_string(),
|
||||
));
|
||||
},
|
||||
};
|
||||
return match validate_listeners_document(&document) {
|
||||
std::result::Result::Ok(()) => std::result::Result::Ok(document),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
|
||||
/// Reads and parses one listener configuration document from a filesystem path.
|
||||
pub fn read_listeners_json_file(
|
||||
path: &std::path::Path,
|
||||
) -> ks_core::Result<ListenersConfigDocument> {
|
||||
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(
|
||||
"listeners_config_file_read_failed",
|
||||
error.to_string(),
|
||||
));
|
||||
},
|
||||
};
|
||||
return parse_listeners_json(&raw_json);
|
||||
}
|
||||
|
||||
/// Loads workspace environment values, resolves placeholders and parses listener configuration.
|
||||
pub fn read_listeners_json_file_with_environment(
|
||||
path: &std::path::Path,
|
||||
workspace_root: &std::path::Path,
|
||||
) -> ks_core::Result<ListenersConfigDocument> {
|
||||
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(content) => content,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"listeners_config_file_read_failed",
|
||||
error.to_string(),
|
||||
));
|
||||
},
|
||||
};
|
||||
let resolved = crate::resolve_environment_placeholders(&raw_json);
|
||||
return parse_listeners_json(&resolved);
|
||||
}
|
||||
|
||||
/// Returns one named listener profile.
|
||||
pub fn listeners_profile<'a>(
|
||||
document: &'a ListenersConfigDocument,
|
||||
profile_name: &str,
|
||||
) -> ks_core::Result<&'a ListenersProfileConfig> {
|
||||
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(
|
||||
"listeners_profile_not_found",
|
||||
profile_name.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
/// Returns the active listener profile declared by the document.
|
||||
pub fn active_listeners_profile(
|
||||
document: &ListenersConfigDocument,
|
||||
) -> ks_core::Result<&ListenersProfileConfig> {
|
||||
return listeners_profile(document, document.active_profile.as_str());
|
||||
}
|
||||
|
||||
/// Converts one named listener profile into the resolved runtime listener contract.
|
||||
pub fn resolved_listener_config(profile: &ListenersProfileConfig) -> crate::ListenerConfig {
|
||||
return crate::ListenerConfig {
|
||||
enabled: profile.enabled,
|
||||
default_commitment: profile.default_commitment.clone(),
|
||||
log_listeners: profile.log_listeners.clone(),
|
||||
program_listeners: profile.program_listeners.clone(),
|
||||
account_listeners: profile.account_listeners.clone(),
|
||||
};
|
||||
}
|
||||
|
||||
/// 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") {
|
||||
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(
|
||||
"listeners_config_profiles_empty",
|
||||
"at least one listener 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, "listeners.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(
|
||||
"listeners_profile_name_duplicate",
|
||||
profile.name.clone(),
|
||||
));
|
||||
}
|
||||
let resolved = resolved_listener_config(profile);
|
||||
match crate::validate_listeners(&resolved) {
|
||||
std::result::Result::Ok(()) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
}
|
||||
return match active_listeners_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_LISTENERS: &str = include_str!("../../config/listeners.config.json");
|
||||
const EXAMPLE_LISTENERS: &str = include_str!("../../config/example.listeners.config.json");
|
||||
|
||||
#[test]
|
||||
fn default_and_example_listener_configs_validate() {
|
||||
assert!(super::parse_listeners_json(DEFAULT_LISTENERS).is_ok());
|
||||
assert!(super::parse_listeners_json(EXAMPLE_LISTENERS).is_ok());
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,24 @@
|
||||
// file: ks-config/src/settings.rs
|
||||
// version: 20
|
||||
// version: 21
|
||||
|
||||
//! Typed configuration models shared by applications and workers.
|
||||
//! Resolved runtime configuration models retained during the `0.5.1` source-document split.
|
||||
|
||||
use ts_rs::TS; // rust-rules: derive-import
|
||||
|
||||
const CONFIG_JSON_SCHEMA: &str = include_str!("../../config/schemas/app.config.schema.json");
|
||||
const CONFIG_JSON_SCHEMA: &str =
|
||||
include_str!("../../config/schemas/resolved.app.config.schema.json");
|
||||
|
||||
/// Root configuration containing every named profile.
|
||||
/// Resolved runtime configuration containing every composed named profile.
|
||||
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize, TS)]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ks_config/settings/AppConfig.ts")]
|
||||
pub struct AppConfig {
|
||||
/// Active profile name.
|
||||
pub active_profile: std::string::String,
|
||||
/// Named profiles available in this configuration file.
|
||||
/// Named profiles reconstructed from the active binary composition and shared documents.
|
||||
pub profiles: std::vec::Vec<ProfileConfig>,
|
||||
}
|
||||
|
||||
/// Configuration profile selected by the root active profile name.
|
||||
/// Resolved runtime profile selected by the root active profile name.
|
||||
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize, TS)]
|
||||
#[ts(
|
||||
export,
|
||||
@@ -382,12 +383,12 @@ pub struct DemoConfig {
|
||||
pub trading_demo_enabled: bool,
|
||||
}
|
||||
|
||||
/// Returns the embedded JSON Schema text used for configuration validation.
|
||||
/// Returns the embedded JSON Schema text used for resolved runtime contract validation.
|
||||
pub fn config_json_schema_text() -> &'static str {
|
||||
return CONFIG_JSON_SCHEMA;
|
||||
}
|
||||
|
||||
/// Parses the embedded JSON Schema into a JSON value.
|
||||
/// Parses the embedded resolved runtime JSON Schema into a JSON value.
|
||||
pub fn config_json_schema_value() -> ks_core::Result<serde_json::Value> {
|
||||
let schema_result = serde_json::from_str::<serde_json::Value>(CONFIG_JSON_SCHEMA);
|
||||
return match schema_result {
|
||||
@@ -399,7 +400,7 @@ pub fn config_json_schema_value() -> ks_core::Result<serde_json::Value> {
|
||||
};
|
||||
}
|
||||
|
||||
/// Validates a raw JSON configuration string against the embedded JSON Schema.
|
||||
/// Validates a raw resolved runtime JSON string against the embedded compatibility schema.
|
||||
pub fn validate_config_json_schema(raw_json: &str) -> ks_core::Result<()> {
|
||||
let schema = match config_json_schema_value() {
|
||||
std::result::Result::Ok(schema) => schema,
|
||||
@@ -433,7 +434,7 @@ pub fn validate_config_json_schema(raw_json: &str) -> ks_core::Result<()> {
|
||||
};
|
||||
}
|
||||
|
||||
/// Parses the application configuration from a JSON string and validates it.
|
||||
/// Parses the resolved runtime application contract from a JSON string and validates it.
|
||||
pub fn parse_config_json(raw_json: &str) -> ks_core::Result<AppConfig> {
|
||||
match validate_config_json_schema(raw_json) {
|
||||
std::result::Result::Ok(()) => (),
|
||||
@@ -454,7 +455,7 @@ pub fn parse_config_json(raw_json: &str) -> ks_core::Result<AppConfig> {
|
||||
};
|
||||
}
|
||||
|
||||
/// Reads and parses the application configuration from a filesystem path.
|
||||
/// Reads and parses a resolved runtime application snapshot from a filesystem path.
|
||||
pub fn read_config_json_file(path: &std::path::Path) -> ks_core::Result<AppConfig> {
|
||||
let raw_json = match std::fs::read_to_string(path) {
|
||||
std::result::Result::Ok(content) => content,
|
||||
@@ -468,7 +469,7 @@ pub fn read_config_json_file(path: &std::path::Path) -> ks_core::Result<AppConfi
|
||||
return parse_config_json(&raw_json);
|
||||
}
|
||||
|
||||
/// Loads the workspace environment, resolves placeholders and parses one configuration file.
|
||||
/// Loads the workspace environment, resolves placeholders and parses one resolved runtime snapshot.
|
||||
pub fn read_config_json_file_with_environment(
|
||||
path: &std::path::Path,
|
||||
workspace_root: &std::path::Path,
|
||||
@@ -700,7 +701,7 @@ fn validate_solana(config: &SolanaConfig) -> ks_core::Result<()> {
|
||||
return validate_listeners(&config.listeners);
|
||||
}
|
||||
|
||||
fn validate_http_endpoint(config: &HttpEndpointConfig) -> ks_core::Result<()> {
|
||||
pub(crate) fn validate_http_endpoint(config: &HttpEndpointConfig) -> ks_core::Result<()> {
|
||||
match validate_endpoint_common(&config.name, &config.provider, &config.cluster, &config.roles) {
|
||||
std::result::Result::Ok(()) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
@@ -731,7 +732,7 @@ fn validate_http_endpoint(config: &HttpEndpointConfig) -> ks_core::Result<()> {
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn validate_ws_endpoint(config: &WsEndpointConfig) -> ks_core::Result<()> {
|
||||
pub(crate) fn validate_ws_endpoint(config: &WsEndpointConfig) -> ks_core::Result<()> {
|
||||
match validate_endpoint_common(&config.name, &config.provider, &config.cluster, &config.roles) {
|
||||
std::result::Result::Ok(()) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
@@ -826,7 +827,7 @@ fn validate_endpoint_role(config: &EndpointRoleConfig) -> ks_core::Result<()> {
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn validate_listeners(config: &ListenerConfig) -> ks_core::Result<()> {
|
||||
pub(crate) fn validate_listeners(config: &ListenerConfig) -> ks_core::Result<()> {
|
||||
match require_non_empty(&config.default_commitment, "solana.listeners.default_commitment") {
|
||||
std::result::Result::Ok(()) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
@@ -1015,7 +1016,7 @@ fn validate_wallet_execution_pair(
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn require_non_empty(value: &str, field_name: &str) -> ks_core::Result<()> {
|
||||
pub(crate) fn require_non_empty(value: &str, field_name: &str) -> ks_core::Result<()> {
|
||||
if value.trim().is_empty() {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"config_field_empty",
|
||||
@@ -1045,14 +1046,18 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_CONFIG: &str = include_str!("../../config/app.config.json");
|
||||
const EXAMPLE_CONFIG: &str = include_str!("../../config/example.app.config.json");
|
||||
const DEFAULT_CONFIG: &str =
|
||||
include_str!("../../test-fixtures/config/resolved.app.config.json");
|
||||
const EXAMPLE_CONFIG: &str =
|
||||
include_str!("../../test-fixtures/config/example.resolved.app.config.json");
|
||||
|
||||
fn parse_default_value() -> serde_json::Value {
|
||||
let result = serde_json::from_str::<serde_json::Value>(DEFAULT_CONFIG);
|
||||
match result {
|
||||
std::result::Result::Ok(value) => return value,
|
||||
std::result::Result::Err(error) => panic!("default app config must be valid JSON: {error}"),
|
||||
std::result::Result::Err(error) => {
|
||||
panic!("default app config must be valid JSON: {error}")
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
240
ks-config/src/transport.rs
Normal file
240
ks-config/src/transport.rs
Normal file
@@ -0,0 +1,240 @@
|
||||
// file: ks-config/src/transport.rs
|
||||
// version: 1
|
||||
|
||||
//! 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.
|
||||
#[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,
|
||||
/// Named transport profiles available in this document.
|
||||
pub profiles: std::vec::Vec<TransportProfileConfig>,
|
||||
}
|
||||
|
||||
/// Named Solana HTTP and WebSocket endpoint profile.
|
||||
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize)]
|
||||
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.
|
||||
pub ws_endpoints: std::vec::Vec<crate::WsEndpointConfig>,
|
||||
}
|
||||
|
||||
/// Returns the embedded transport JSON Schema text.
|
||||
pub fn transport_json_schema_text() -> &'static str {
|
||||
return TRANSPORT_JSON_SCHEMA;
|
||||
}
|
||||
|
||||
/// 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),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(ks_core::Error::new(
|
||||
"transport_config_schema_parse_failed",
|
||||
error.to_string(),
|
||||
)),
|
||||
};
|
||||
}
|
||||
|
||||
/// 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::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::Err(error) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"transport_config_json_parse_failed",
|
||||
error.to_string(),
|
||||
));
|
||||
},
|
||||
};
|
||||
let validator = match jsonschema::validator_for(&schema) {
|
||||
std::result::Result::Ok(validator) => validator,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"transport_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(
|
||||
"transport_config_schema_validation_failed",
|
||||
error.to_string(),
|
||||
)),
|
||||
};
|
||||
}
|
||||
|
||||
/// Parses and validates one independent transport configuration document.
|
||||
pub fn parse_transport_json(raw_json: &str) -> ks_core::Result<TransportConfigDocument> {
|
||||
match validate_transport_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::<TransportConfigDocument>(raw_json) {
|
||||
std::result::Result::Ok(document) => document,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"transport_config_json_decode_failed",
|
||||
error.to_string(),
|
||||
));
|
||||
},
|
||||
};
|
||||
return match validate_transport_document(&document) {
|
||||
std::result::Result::Ok(()) => std::result::Result::Ok(document),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub fn read_transport_json_file_with_environment(
|
||||
path: &std::path::Path,
|
||||
workspace_root: &std::path::Path,
|
||||
) -> ks_core::Result<TransportConfigDocument> {
|
||||
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(content) => content,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"transport_config_file_read_failed",
|
||||
error.to_string(),
|
||||
));
|
||||
},
|
||||
};
|
||||
let resolved = crate::resolve_environment_placeholders(&raw_json);
|
||||
return parse_transport_json(&resolved);
|
||||
}
|
||||
|
||||
/// Returns one named transport profile.
|
||||
pub fn transport_profile<'a>(
|
||||
document: &'a TransportConfigDocument,
|
||||
profile_name: &str,
|
||||
) -> ks_core::Result<&'a TransportProfileConfig> {
|
||||
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(
|
||||
"transport_profile_not_found",
|
||||
profile_name.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
/// 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());
|
||||
}
|
||||
|
||||
/// 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") {
|
||||
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(
|
||||
"transport_config_profiles_empty",
|
||||
"at least one transport 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, "transport.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(
|
||||
"transport_profile_name_duplicate",
|
||||
profile.name.clone(),
|
||||
));
|
||||
}
|
||||
if profile.http_endpoints.is_empty() || profile.ws_endpoints.is_empty() {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"transport_profile_endpoints_empty",
|
||||
profile.name.clone(),
|
||||
));
|
||||
}
|
||||
for endpoint in &profile.http_endpoints {
|
||||
match crate::validate_http_endpoint(endpoint) {
|
||||
std::result::Result::Ok(()) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
}
|
||||
for endpoint in &profile.ws_endpoints {
|
||||
match crate::validate_ws_endpoint(endpoint) {
|
||||
std::result::Result::Ok(()) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
return match active_transport_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_TRANSPORT: &str = include_str!("../../config/transport.config.json");
|
||||
const EXAMPLE_TRANSPORT: &str = include_str!("../../config/example.transport.config.json");
|
||||
|
||||
#[test]
|
||||
fn default_and_example_transport_configs_validate() {
|
||||
assert!(super::parse_transport_json(DEFAULT_TRANSPORT).is_ok());
|
||||
assert!(super::parse_transport_json(EXAMPLE_TRANSPORT).is_ok());
|
||||
}
|
||||
|
||||
#[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 {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
panic!("default transport config must parse: {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());
|
||||
}
|
||||
}
|
||||
32
ks-config/tests/external_composition_api.rs
Normal file
32
ks-config/tests/external_composition_api.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
// file: ks-config/tests/external_composition_api.rs
|
||||
// version: 1
|
||||
|
||||
//! External API coverage for binary composition and shared configuration documents.
|
||||
|
||||
#[test]
|
||||
fn external_consumer_resolves_composition_through_crate_root_exports() {
|
||||
let composition = match ks_config::parse_composition_json(include_str!(
|
||||
"../../config/example.kb-app-demo-desktop.default.config.json"
|
||||
)) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("composition must parse: {error}"),
|
||||
};
|
||||
let transport = match ks_config::parse_transport_json(include_str!(
|
||||
"../../config/example.transport.config.json"
|
||||
)) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("transport must parse: {error}"),
|
||||
};
|
||||
let listeners = match ks_config::parse_listeners_json(include_str!(
|
||||
"../../config/example.listeners.config.json"
|
||||
)) {
|
||||
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) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("composition must resolve: {error}"),
|
||||
};
|
||||
assert_eq!(resolved.active_profile, composition.active_profile);
|
||||
assert_eq!(resolved.profiles.len(), composition.profiles.len());
|
||||
}
|
||||
Reference in New Issue
Block a user