Skip to main content

Crate apollo_configuration

Crate apollo_configuration 

Source
Expand description

A typed, friendly configuration system for Apollo products.

§The #[configuration] attribute

apollo-configuration’s #[configuration] attribute provides:

  • Parsing of YAML files with friendly error reporting
  • JSON schema generation
  • Validation of user input according to that JSON schema
  • Defaulting of field values
  • Expansion of environment variables
use apollo_configuration::configuration;
use apollo_configuration::parse_yaml;

#[configuration]
struct CsrfConfig {
    #[config(default = true)]
    enabled: bool,
    #[config(default = default_required_headers())]
    required_headers: Vec<String>,
}

fn default_required_headers() -> Vec<String> {
    vec![
        "x-apollo-operation-name".to_string(),
        "apollo-require-preflight".to_string(),
    ]
}

§JSON Schema Generation

Use export_json_schema to generate a JSON schema for a Configuration struct.

§YAML Parsing

Use parse_yaml to parse and validate YAML text into a Configuration struct.

§Validation

Configuration structs are validated using JSON Schema, and each field is validated according to its own validation rules. It’s common for configuration structures to require “cross-cutting” validation: for example, disallowing two features that cannot be used together. To do things like this, you can add an additional custom validation function using the #[configuration(validate)] attribute.

use apollo_configuration::configuration;
use apollo_configuration::ErrorCollector;

#[configuration]
struct HomepageConfig {
    enabled: bool,
    graph_ref: Option<String>,
}

#[configuration]
struct ApolloSandboxConfig {
    enabled: bool,
}

#[configuration(validate = validate_config)]
struct Config {
    homepage: HomepageConfig,
    sandbox: ApolloSandboxConfig,
}

fn validate_config(config: &Config, mut errors: ErrorCollector<'_>) {
    if config.homepage.enabled && config.sandbox.enabled {
        errors.nest("sandbox")
            .report_simple("sandbox can only be enabled if homepage is disabled");
    }
}

§The #[config] attribute

The #[config] attribute can be used on the structure’s fields to change how fields are parsed, validated, and displayed.

§Optional/Required fields

All fields are assumed to be optional by default. The type’s Default impl is used when a field is absent during deserialization. If a field is required, mark it with #[config(required)]. If a configuration structure contains any required fields, it will not implement Default.

If a field’s type does not have a Default impl, you can provide a default value:

#[configuration]
struct CacheConfig {
    #[config(default = NonZeroUsize::new(500).unwrap())]
    cache_size: NonZeroUsize,
}

§Field validation

apollo-configuration supports schema-level validation and fully custom validation.

Schema-level validation works by using one of the below sub-attributes inside the #[config] attribute:

  • range(min = 0, max = 1024) - Restrict the value of an integer field to the given range.
  • length(min = 0, max = 256) - Restrict the length of a string or array field to the given range.
  • length(exact = 10) - Require a string or array field to have the given length exactly.
  • pattern(r"^\d+$") - Require a string field to match the given regular expression.

Schema-level validation is also reflected in the generated JSON schema.

Custom validation works by specifying a validation function in the #[config(validate)] attribute:

use apollo_configuration::configuration;
use apollo_configuration::ErrorCollector;

#[configuration]
struct Config {
    #[config(validate = validate_div_by_3)]
    magic_number: usize,
}

fn validate_div_by_3(magic_number: &usize, mut errors: ErrorCollector<'_>) {
    if !magic_number.is_multiple_of(3) {
        errors.report_simple("magic number must be divisible by 3");
    }
}

Custom validation is not reflected in the JSON schema. Generally, schema-level validation should be preferred. However, custom validation supports rich diagnostics using ErrorCollector::report, so it can still be a good choice if schema-level validation gives poor errors.

Custom validation can be disabled using the #[config(skip_validate)] attribute. This can be necessary when using external types that do not implement Validate:

use apollo_configuration::configuration;

#[configuration]
struct Config {
    #[config(skip_validate, default = tinystr::TinyAsciiStr::try_from_utf8(b"unnamed").unwrap())]
    #[schemars(with = "String")]
    name: tinystr::TinyAsciiStr<16>,
}

§Expansion

Your users can write ${env.VAR_NAME} syntax inside YAML values to insert the value of an environment variable into the configuration. This can be used to include more dynamic values or secrets without having to write them into the configuration file.

It is an error to reference an environment variable that is not defined. The bash-style defaulting syntax can be used to handle optional environment variables: ${env.VAR_NAME:-default}.

If your configuration structure contains secrets, API keys, passwords, or sensitive URLs, remember to use a redaction helper such as apollo-redaction.

§Types for use as configuration values

Primitive Rust types and several ecosystem types are supported as configuration values. The types module provides wrappers for common external types that can be used in configuration.

Available newtypes:

To enable these types add the appropriate feature to the apollo-configuration dependency: duration, grpc, http, regex, url

All newtypes support wrapping with apollo_redaction::Redacted for sensitive values like API keys or passwords:

use apollo_configuration::configuration;
use apollo_redaction::Redacted;

#[configuration]
struct ApiConfig {
    #[config(required)]
    api_key: Redacted<String>,
}

let config: ApiConfig = apollo_configuration::parse_yaml(
    "api_key: secret-key-123",
    &Default::default(),
).unwrap();

// The value is accessible but protected from accidental logging
assert_eq!(config.api_key.unredact(), "secret-key-123");
assert_eq!(config.api_key.to_string(), "[REDACTED]");

See the apollo-redaction crate for details.

§Other types

To use a type as a configuration value, it must implement these traits:

Where it makes sense, types used in configuration values should also implement Clone, Default, and serde::Serialize.

§Enum configuration

Enums can use the #[configuration] attribute. This is useful for configuration options where the user selects between different modes or backends.

use apollo_configuration::configuration;

#[configuration]
enum Endpoint {
    Http { url: String, timeout: u64 },
    Grpc { host: String, port: u16 },
}

§Default variant

Mark a variant with #[config(default)] to make the enum implement Default. When parsing from YAML, omitted enum fields will use this default:

use apollo_configuration::configuration;
use apollo_configuration::parse_yaml;

#[configuration]
#[derive(PartialEq, Eq)]
enum LogLevel {
    Debug,
    #[config(default)]
    Info,
    Warn,
    Error,
}

#[configuration]
#[derive(PartialEq, Eq)]
struct Config {
    log_level: LogLevel,
}

let config: Config = parse_yaml(r#"log_level: debug"#, &Default::default()).unwrap();
assert_eq!(config.log_level, LogLevel::Debug);

let config: Config = parse_yaml(r#"{}"#, &Default::default()).unwrap();
assert_eq!(config.log_level, LogLevel::Info);

§Unit variant fields

Unit variant enums can be parsed directly from string values in YAML. The #[configuration] attribute applies #[serde(rename_all = "snake_case")] automatically:

use apollo_configuration::configuration;

#[configuration]
#[derive(PartialEq, Eq)]
enum EnabledMode {
    Measure,
    #[config(default)]
    Enforce,
}

#[configuration]
struct DemandControlConfig {
    enabled: bool,
    mode: EnabledMode,
}

let config: DemandControlConfig = apollo_configuration::parse_yaml(r#"
    enabled: true
    mode: measure
"#, &Default::default()).unwrap();
assert_eq!(config.mode, EnabledMode::Measure);

§Struct variant fields

Struct variant fields support the same #[config(...)] attributes as struct fields:

use apollo_configuration::configuration;

#[configuration]
enum Endpoint {
    #[config(default)]
    Http {
        #[config(default = "http://localhost".to_string())]
        url: String,
        #[config(default = 30)]
        timeout: u64,
    },
    Grpc {
        #[config(required)]
        host: String,
        #[config(default = 9090)]
        port: u16,
    },
}

§Tuple variant fields

Tuple variants wrap a single inner type. The #[config(...)] attributes apply to the inner type:

use apollo_configuration::configuration;

#[configuration]
struct HttpConfig {
    #[config(default = "http://localhost".to_string())]
    url: String,
}

#[configuration]
enum Endpoint {
    #[config(default)]
    Http(HttpConfig),
    Custom(String),
}

Modules§

expansion
Traits and types to customize ${} expansion functionality.
types
Type support for configuration values.

Structs§

ErrorCollector
Error collector for custom validation rules.
ExpandError
Overridable
ParseYamlOptions
Options for parsing configuration from YAML.
ValidationError
A JSON schema validation error.
ValidationErrors
A list of JSON schema validation errors.

Enums§

ConfigError
Top-level error type.
YamlToJsonError
Errors that can occur when transforming the YAML input to a JSON-compatible value.

Traits§

Configuration
Marker trait for configuration structs. See the #[configuration] proc macro for documentation.
Validate
Validation trait for configuration values.

Functions§

export_json_schema
Return a JSON schema for the given configuration structure.
parse_yaml
validate
Validate a programmatically created value.

Attribute Macros§

configuration
Turn a Rust structure or enum into a configuration structure.