0.5.1-pre.006

This commit is contained in:
2026-08-10 02:44:24 +02:00
parent b6a286a4df
commit 34a670eaec
72 changed files with 5589 additions and 1932 deletions

View 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());
}
}

View File

@@ -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
View 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());
}
}

View File

@@ -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
View 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());
}
}