Logo

Autoschematic

GitHub
Cluster Login

Resource Bodies and Connector::filter(addr)

Implementing the Resource trait defines how the connector stores resource files on-disk, for example, as RON or YAML. The body should contain user-editable state only, and not include information already carried by the address.

The S3 bucket resource is a clean example:

#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct S3Bucket {
    pub policy: Option<ron::Value>,
    pub public_access_block: Option<PublicAccessBlock>,
    pub acl: Option<Acl>,
    pub tags: Tags,
}

Notice that the bucket name and bucket region are not in the resource body. Those already live in the implementation of ResourceAddress, S3ResourceAddress, encoded as aws/s3/{region}/buckets/{name}.ron.

Resource::to_bytes() and Resource::from_bytes() should try to encode/decode the resource to/from its on-disk format. This sounds complex, but in practice it's just using Serialize/Deserialize with whatever format you want. See the GithubConnector for an example. Notice that from_bytes() also gets the address, to determine by decoding it what type to deserialize:

use autoschematic_core::util::RON;

impl Resource for GitHubResource {
    fn to_bytes(&self) -> Result<Vec<u8>, anyhow::Error> {
        let pretty_config = autoschematic_core::util::PrettyConfig::default().struct_names(true);
        match self {
            GitHubResource::Repository(repo) => Ok(RON.to_string_pretty(&repo, pretty_config)?.into()),
            GitHubResource::BranchProtection(protection) => Ok(RON.to_string_pretty(&protection, pretty_config)?.into()),
        }
    }

    fn from_bytes(addr: &impl ResourceAddress, s: &[u8]) -> Result<Self, anyhow::Error> {
        // try to decode the path as a GithubResourceAddress...
        let addr = GitHubResourceAddress::from_path(&addr.to_path_buf())?;
        let s = std::str::from_utf8(s)?;

        // ...and match on its variant to decode it!
        match addr {
            GitHubResourceAddress::Repository { .. } => Ok(GitHubResource::Repository(RON.from_str(s)?)),
            GitHubResourceAddress::BranchProtection { .. } => Ok(GitHubResource::BranchProtection(RON.from_str(s)?)),
            _ => Err(invalid_addr(&addr)),
        }
    }
}

Connector::filter(addr): declaring the address space to the host

Connector::filter(addr) is used to inform the host of which files "belong" to the connector. If a connector returns FilterResponse::Resource for a given address, it indicates to the host that it should exclusively use this file as the state for some particular resource at that address. In other words, this address decoding logic is the mechanism by which connectors describe an hierarchy of nested objects.

So, the S3Connector might return:

aws/s3/us-east-1/buckets/office.ron => FilterResponse::Resource aws/s3/eu-west-2/buckets/backup.ron => FilterResponse::Resource some_random_file.txt => FilterResponse::None aws/vpc/eu-west-2/vpcs/main.ron => FilterResponse::None

Note that addresses never include the prefix here; connectors run with their working directory at the root of the git repo, but they are informed of their prefix at new() and should save it if they need it (for loading configs etc). In other words, if the full path is "./${prefix}/${addr}", connectors are only passed "./${addr}" in operations like filter, get, plan, etc. Connector-specific config is separate. If a path is a connector config, filter() should return FilterResponse::Config so upstream caches of filter() can be invalidated when it changes:

async fn filter(&self, addr: &Path) -> Result<FilterResponse, anyhow::Error> {
    if let Ok(addr) = GitHubResourceAddress::from_path(addr) {
        match addr {
            GitHubResourceAddress::Config => Ok(FilterResponse::Config),
            _ => Ok(FilterResponse::Resource),
        }
    } else {
        Ok(FilterResponse::none())
    }
}

If the API you're writing a connector for already has a declarative interface of some sort, you may be able to use it as your native representation in Autoschematic. The Kubernetes connector serializes Kubernetes objects directly and stores them as .yaml files, and the Snowflake connector directly fetches and applies Snowflake's Declarative Data Language (DDL) for manangin schemas, tables, and other SQL objects. Connector ops for those connectors are

Otherwise, most connectors currently use RON, the Rusty Object Notation as their on-disk format. It's particularly well suited for modelling Rust structs & enums, and Autoschematic along with the language server and vscode extension support RON even further with syntax highlighting, autoformatting, docs-on-hover, and even syntax & type error highlighting for internal and connector-supported files.

Reference files:

Next: Read Methods (get, list, and subpaths)