From b924b7dc5f566d2dfa12c9dfb90ee9721bb8aa49 Mon Sep 17 00:00:00 2001 From: Samir Talwar Date: Fri, 23 Feb 2024 21:46:31 +0100 Subject: [PATCH] Add an `InitializationContext`. The initializion context is a value of a type specified by the connector, which can be injected into the entrypoint. This allows users of the connector functions to provide a little more control over how the connector configures itself. --- rust-connector-sdk/src/connector.rs | 49 ++++++++--------- rust-connector-sdk/src/connector/example.rs | 3 ++ rust-connector-sdk/src/default_main.rs | 59 ++++++++++++++++----- 3 files changed, 71 insertions(+), 40 deletions(-) diff --git a/rust-connector-sdk/src/connector.rs b/rust-connector-sdk/src/connector.rs index 457c1ddf..26d51487 100644 --- a/rust-connector-sdk/src/connector.rs +++ b/rust-connector-sdk/src/connector.rs @@ -221,33 +221,27 @@ pub enum MutationError { /// Connectors using this library should implement this trait. /// +/// It provides methods which implement the standard endpoints defined by the specification: +/// capabilities, schema, query, mutation, query/explain, and mutation/explain. /// -/// It provides methods which implement the standard endpoints -/// defined by the specification: capabilities, schema, query, mutation, -/// query/explain, and mutation/explain. -/// -/// In addition, it introduces names for types to manage -/// state and configuration (if any), and provides any necessary context -/// for observability purposes (metrics, logging and tracing). +/// In addition, it introduces names for types to manage state and configuration (if any), and +/// provides any necessary context for observability purposes (metrics, logging and tracing). /// /// ## Configuration /// -/// Connectors encapsulate data sources, and likely require configuration -/// (connection strings, web service tokens, etc.). The NDC specification -/// does not discuss this sort of configuration, because it is an -/// implementation detail of a specific connector, but it is useful to -/// adopt a convention here for simplified configuration management. +/// Connectors encapsulate data sources, and likely require configuration (connection strings, web +/// service tokens, etc.). The NDC specification does not discuss this sort of configuration, +/// because it is an implementation detail of a specific connector, but it is useful to adopt a +/// convention here for simplified configuration management. /// -/// Configuration is specified as JSON, validated, and stored in a binary -/// format. +/// Configuration is input as a directory, which needs to be processed by the connector. The format +/// of the files in the directory is connector-specific. /// -/// This trait defines two types for managing configuration: +/// In addition, the caller can provide a [`Connector::InitializationContext`] value to help +/// prepare the configuration. This might, for example, provide connector-specific secrets. /// -/// - [`Connector::RawConfiguration`] defines the type of unvalidated, raw -/// configuration. -/// - [`Connector::Configuration`] defines the type of validated -/// configuration. Ideally, invalid configuration should not be representable -/// in this form. +/// Once parsed, the configuration should be represented by the [`Connector::Configuration`] type, +/// which is then accessible on request. /// /// ## State /// @@ -255,20 +249,22 @@ pub enum MutationError { /// /// - [`Connector::State`] defines the type of any unserializable runtime state. /// -/// State is distinguished from configuration in that it is not provided directly by -/// the user, and would not ordinarily be serializable. For example, a connection string -/// would be configuration, but a connection pool object created from that -/// connection string would be state. +/// State is distinguished from configuration in that it is not provided directly by the user, and +/// is transient. For example, a connection string would be configuration, but a connection pool +/// object created from that connection string would be state. #[async_trait] pub trait Connector { - /// The type of validated configuration + /// Context used to initialize the server state + type InitializationContext: Sync + Send; + /// The connector configuration, parsed and validated type Configuration: Sync + Send; - /// The type of unserializable state + /// The transient state of the connector type State: Sync + Send; /// Validate the raw configuration provided by the user, /// returning a configuration error or a validated [`Connector::Configuration`]. async fn parse_configuration( + context: &Self::InitializationContext, configuration_dir: impl AsRef + Send, ) -> Result; @@ -280,6 +276,7 @@ pub trait Connector { /// In addition, this function should register any /// connector-specific metrics with the metrics registry. async fn try_init_state( + context: &Self::InitializationContext, configuration: &Self::Configuration, metrics: &mut prometheus::Registry, ) -> Result; diff --git a/rust-connector-sdk/src/connector/example.rs b/rust-connector-sdk/src/connector/example.rs index d55f23af..eae73ece 100644 --- a/rust-connector-sdk/src/connector/example.rs +++ b/rust-connector-sdk/src/connector/example.rs @@ -11,16 +11,19 @@ pub struct Example {} #[async_trait] impl Connector for Example { + type InitializationContext = (); type Configuration = (); type State = (); async fn parse_configuration( + _context: &Self::InitializationContext, _configuration_dir: impl AsRef + Send, ) -> Result { Ok(()) } async fn try_init_state( + _context: &Self::InitializationContext, _configuration: &Self::Configuration, _metrics: &mut prometheus::Registry, ) -> Result { diff --git a/rust-connector-sdk/src/default_main.rs b/rust-connector-sdk/src/default_main.rs index f3fccd7a..697ed050 100644 --- a/rust-connector-sdk/src/default_main.rs +++ b/rust-connector-sdk/src/default_main.rs @@ -145,10 +145,26 @@ where /// not described in the [NDC specification](http://hasura.github.io/ndc-spec/). /// Specifically: /// -/// - It reads configuration as JSON from a file specified on the command line, +/// - It reads configuration from a directory specified on the command line, /// - It reports traces to an OTLP collector specified on the command line, /// - Logs are written to stdout +/// +/// It provides the default initialization context to the connector configuration. pub async fn default_main() -> Result<(), Box> +where + C::InitializationContext: Default, + C::Configuration: Clone, + C::State: Clone, +{ + default_main_with::(C::InitializationContext::default()).await +} + +/// A default main function for a connector that takes an initialization context. +/// +/// This works just like [`default_main`], but allows you to use a non-default context. +pub async fn default_main_with( + context: C::InitializationContext, +) -> Result<(), Box> where C::Configuration: Clone, C::State: Clone, @@ -156,14 +172,15 @@ where let CliArgs { command } = CliArgs::parse(); match command { - Command::Serve(serve_command) => serve::(serve_command).await, - Command::Test(test_command) => test::(test_command).await, - Command::Replay(replay_command) => replay::(replay_command).await, + Command::Serve(serve_command) => serve::(&context, serve_command).await, + Command::Test(test_command) => test::(&context, test_command).await, + Command::Replay(replay_command) => replay::(&context, replay_command).await, Command::CheckHealth(check_health_command) => check_health(check_health_command).await, } } async fn serve( + context: &C::InitializationContext, serve_command: ServeCommand, ) -> Result<(), Box> where @@ -173,7 +190,7 @@ where init_tracing(&serve_command.service_name, &serve_command.otlp_endpoint) .expect("Unable to initialize tracing"); - let server_state = init_server_state::(serve_command.configuration).await; + let server_state = init_server_state::(context, serve_command.configuration).await; let router = create_router::( server_state.clone(), @@ -229,12 +246,15 @@ where /// Initialize the server state from the configuration file. pub async fn init_server_state( + context: &C::InitializationContext, config_directory: impl AsRef + Send, ) -> ServerState { - let configuration = C::parse_configuration(config_directory).await.unwrap(); + let configuration = C::parse_configuration(context, config_directory) + .await + .unwrap(); let mut metrics = Registry::new(); - let state = C::try_init_state(&configuration, &mut metrics) + let state = C::try_init_state(context, &configuration, &mut metrics) .await .unwrap(); @@ -510,13 +530,16 @@ impl ndc_test::Connector for ConnectorAdapter { } } -async fn test(command: TestCommand) -> Result<(), Box> { +async fn test( + context: &C::InitializationContext, + command: TestCommand, +) -> Result<(), Box> { let test_configuration = ndc_test::TestConfiguration { seed: command.seed, snapshots_dir: command.snapshots_dir, }; - let connector = make_connector_adapter::(command.configuration).await; + let connector = make_connector_adapter::(context, command.configuration).await; let results = ndc_test::test_connector(&test_configuration, &connector).await; if !results.failures.is_empty() { @@ -529,8 +552,11 @@ async fn test(command: TestCommand) -> Result<(), Box(command: ReplayCommand) -> Result<(), Box> { - let connector = make_connector_adapter::(command.configuration).await; +async fn replay( + context: &C::InitializationContext, + command: ReplayCommand, +) -> Result<(), Box> { + let connector = make_connector_adapter::(context, command.configuration).await; let results = ndc_test::test_snapshots_in_directory(&connector, command.snapshots_dir).await; if !results.failures.is_empty() { @@ -543,11 +569,16 @@ async fn replay(command: ReplayCommand) -> Result<(), Box(configuration_path: PathBuf) -> ConnectorAdapter { - let configuration = C::parse_configuration(configuration_path).await.unwrap(); +async fn make_connector_adapter( + context: &C::InitializationContext, + configuration_path: PathBuf, +) -> ConnectorAdapter { + let configuration = C::parse_configuration(context, configuration_path) + .await + .unwrap(); let mut metrics = Registry::new(); - let state = C::try_init_state(&configuration, &mut metrics) + let state = C::try_init_state(context, &configuration, &mut metrics) .await .unwrap();