Skip to content

Commit

Permalink
Merge pull request #41 from cbgbt/vend-plugins
Browse files Browse the repository at this point in the history
Move Settings Models to the Settings SDK
  • Loading branch information
cbgbt authored Jun 17, 2024
2 parents 9cb0286 + 43c22ac commit c5dfef4
Show file tree
Hide file tree
Showing 111 changed files with 8,309 additions and 3 deletions.
41 changes: 38 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,44 @@
resolver = "2"

members = [
"bottlerocket-settings-sdk",
"bottlerocket-template-helper",
"bottlerocket-defaults-helper",
# Settings Plugin SDK
"bottlerocket-settings-plugin",
"bottlerocket-settings-derive",
"bottlerocket-defaults-helper",

# Public API for Bottlerocket settings models
"bottlerocket-settings-models/settings-models",

# Useful libraries for constructing settings models
"bottlerocket-settings-models/model-derive",
"bottlerocket-settings-models/modeled-types",
"bottlerocket-settings-models/scalar",
"bottlerocket-settings-models/scalar-derive",
"bottlerocket-settings-models/string-impls-for",

# Settings extensions
# These will eventually live in the kit workspaces that own the related software
"bottlerocket-settings-models/settings-extensions/autoscaling",
"bottlerocket-settings-models/settings-extensions/aws",
"bottlerocket-settings-models/settings-extensions/bootstrap-containers",
"bottlerocket-settings-models/settings-extensions/cloudformation",
"bottlerocket-settings-models/settings-extensions/container-registry",
"bottlerocket-settings-models/settings-extensions/container-runtime",
"bottlerocket-settings-models/settings-extensions/dns",
"bottlerocket-settings-models/settings-extensions/ecs",
"bottlerocket-settings-models/settings-extensions/host-containers",
"bottlerocket-settings-models/settings-extensions/kernel",
"bottlerocket-settings-models/settings-extensions/metrics",
"bottlerocket-settings-models/settings-extensions/motd",
"bottlerocket-settings-models/settings-extensions/network",
"bottlerocket-settings-models/settings-extensions/ntp",
"bottlerocket-settings-models/settings-extensions/oci-defaults",
"bottlerocket-settings-models/settings-extensions/oci-hooks",
"bottlerocket-settings-models/settings-extensions/pki",
"bottlerocket-settings-models/settings-extensions/updates",

# Settings Extension SDK
# Currently unused in Bottlerocket
"bottlerocket-settings-sdk",
"bottlerocket-template-helper",
]
18 changes: 18 additions & 0 deletions bottlerocket-settings-models/model-derive/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "bottlerocket-model-derive"
version = "0.1.0"
authors = ["Tom Kirchner <[email protected]>"]
license = "Apache-2.0 OR MIT"
edition = "2021"
publish = false
# Don't rebuild crate just because of changes to README.
exclude = ["README.md"]

[lib]
path = "src/lib.rs"
proc-macro = true

[dependencies]
darling = "0.20"
quote = "1"
syn = { version = "2", default-features = false, features = ["full", "parsing", "printing", "proc-macro", "visit-mut"] }
44 changes: 44 additions & 0 deletions bottlerocket-settings-models/model-derive/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# model-derive

Current version: 0.1.0

## Overview

This module provides a attribute-style procedural macro, `model`, that makes sure a struct is
ready to be used as an API model.

The goal is to reduce cognitive overhead when reading models.
We do this by automatically specifying required attributes on structs and fields.

Several arguments are available to override default behavior; see below.

## Changes it makes

### Visibility

All types must be public, so `pub` is added.
Override this (at a per-struct or per-field level) by specifying your own visibility.

### Derives

All structs must serde-`Serializable` and -`Deserializable`, and comparable via `PartialEq`.
`Debug` is added for convenience.
`Default` can also be added by specifying the argument `impl_default = true`.

### Serde

Structs have a `#[serde(...)]` attribute added to deny unknown fields and rename fields to kebab-case.
The struct can be renamed (for ser/de purposes) by specifying the argument `rename = "bla"`.

Fields have a `#[serde(...)]` attribute added to skip `Option` fields that are `None`.
This is because we accept updates in the API that are structured the same way as the model, but we don't want to require users to specify fields they aren't changing.
This can be disabled by specifying the argument `add_option = false`.

### Option

Fields are all wrapped in `Option<...>`.
Similar to the `serde` attribute added to fields, this is because we don't want users to have to specify fields they aren't changing, and can be disabled the same way, by specifying `add_option = false`.

## Colophon

This text was generated from `README.tpl` using [cargo-readme](https://crates.io/crates/cargo-readme), and includes the rustdoc from `src/lib.rs`.
175 changes: 175 additions & 0 deletions bottlerocket-settings-models/model-derive/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
/*!
# Overview
This module provides a attribute-style procedural macro, `model`, that makes sure a struct is
ready to be used as an API model.
The goal is to reduce cognitive overhead when reading models.
We do this by automatically specifying required attributes on structs and fields.
Several arguments are available to override default behavior; see below.
# Changes it makes
## Visibility
All types must be public, so `pub` is added.
Override this (at a per-struct or per-field level) by specifying your own visibility.
## Derives
All structs must serde-`Serializable` and -`Deserializable`, and comparable via `PartialEq`.
`Debug` is added for convenience.
`Default` can also be added by specifying the argument `impl_default = true`.
## Serde
Structs have a `#[serde(...)]` attribute added to deny unknown fields and rename fields to kebab-case.
The struct can be renamed (for ser/de purposes) by specifying the argument `rename = "bla"`.
Fields have a `#[serde(...)]` attribute added to skip `Option` fields that are `None`.
This is because we accept updates in the API that are structured the same way as the model, but we don't want to require users to specify fields they aren't changing.
This can be disabled by specifying the argument `add_option = false`.
## Option
Fields are all wrapped in `Option<...>`.
Similar to the `serde` attribute added to fields, this is because we don't want users to have to specify fields they aren't changing, and can be disabled the same way, by specifying `add_option = false`.
*/

extern crate proc_macro;

use darling::{ast::NestedMeta, FromMeta};
use proc_macro::TokenStream;
use quote::ToTokens;
use syn::visit_mut::{self, VisitMut};
use syn::{parse_quote, Attribute, Field, ItemStruct, Visibility};

/// Define a `#[model]` attribute that can be placed on structs to be used in an API model.
/// Model requirements are automatically applied to the struct and its fields.
/// (The attribute must be placed on sub-structs; it can't be recursively applied to structs
/// referenced in the given struct.)
#[proc_macro_attribute]
pub fn model(args: TokenStream, input: TokenStream) -> TokenStream {
// Parse args
let args: ParsedArgs = ParsedArgs::from_list(
&NestedMeta::parse_meta_list(args.into())
.expect("Unable to parse arguments to `model` macro"),
)
.expect("Unable to parse arguments to `model` macro");
let mut helper = ModelHelper::from(args);

// Parse and modify source
let mut ast: ItemStruct =
syn::parse(input).expect("Unable to parse item `model` was placed on - is it a struct?");
helper.visit_item_struct_mut(&mut ast);
ast.into_token_stream().into()
}

/// Store any args given by the user inside `#[model(...)]`.
#[derive(Debug, Default, FromMeta)]
#[darling(default)]
struct ParsedArgs {
rename: Option<String>,
impl_default: Option<bool>,
add_option: Option<bool>,
}

/// Stores the user's requested options, plus any defaults for unspecified options.
#[derive(Debug)]
struct ModelHelper {
rename: Option<String>,
impl_default: bool,
add_option: bool,
}

/// Takes the user's requested options and sets default values for anything unspecified.
impl From<ParsedArgs> for ModelHelper {
fn from(args: ParsedArgs) -> Self {
// Add any default values
ModelHelper {
rename: args.rename,
impl_default: args.impl_default.unwrap_or(false),
add_option: args.add_option.unwrap_or(true),
}
}
}

/// VisitMut helps us modify the node types we want without digging through the huge token trees
/// need to represent them.
impl VisitMut for ModelHelper {
// Visit struct definitions.
fn visit_item_struct_mut(&mut self, node: &mut ItemStruct) {
if let Visibility::Inherited = node.vis {
node.vis = parse_quote!(pub)
}

// Add our serde attribute, if the user hasn't set one
if !is_attr_set("serde", &node.attrs) {
// Rename the struct, if the user requested
let attr = if let Some(ref rename_to) = self.rename {
parse_quote!(
#[serde(deny_unknown_fields, rename_all = "kebab-case", rename = #rename_to)]
)
} else {
parse_quote!(
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
)
};
node.attrs.push(attr);
}

// Add our derives, if the user hasn't set any
if !is_attr_set("derive", &node.attrs) {
// Derive Default, if the user requested
let attr = if self.impl_default {
parse_quote!(#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)])
} else {
parse_quote!(#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)])
};
// Rust 1.52 added a legacy_derive_helpers warning (soon to be an error) that yells if
// you use an attribute macro before the derive macro that introduces it. We should
// always put derive macros at the start of the list to avoid this.
node.attrs.insert(0, attr);
}

// Let the default implementation do its thing, recursively.
visit_mut::visit_item_struct_mut(self, node);
}

// Visit field definitions in structs.
fn visit_field_mut(&mut self, node: &mut Field) {
if let Visibility::Inherited = node.vis {
node.vis = parse_quote!(pub)
}

// Add our serde attribute, if the user hasn't set one
if self.add_option {
if !is_attr_set("serde", &node.attrs) {
node.attrs.push(parse_quote!(
#[serde(skip_serializing_if = "Option::is_none")]
));
}

// Wrap each field's type in `Option<...>`
let ty = &node.ty;
node.ty = parse_quote!(Option<#ty>);
}

// Let the default implementation do its thing, recursively.
visit_mut::visit_field_mut(self, node);
}
}

/// Checks whether an attribute named `attr_name` (e.g. "serde") is set in the given list of
/// `syn::Attribute`s.
fn is_attr_set(attr_name: &'static str, attrs: &[Attribute]) -> bool {
for attr in attrs {
if let Some(name) = attr.path().get_ident() {
if name == attr_name {
return true;
}
}
}
false
}
25 changes: 25 additions & 0 deletions bottlerocket-settings-models/modeled-types/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[package]
name = "bottlerocket-modeled-types"
version = "0.1.0"
authors = []
license = "Apache-2.0 OR MIT"
edition = "2021"
publish = false
# Don't rebuild crate just because of changes to README.
exclude = ["README.md"]

[dependencies]
base64 = "0.21"
indexmap = { version = "2", features = ["serde"] }
lazy_static = "1"
regex = "1"
bottlerocket-scalar = { path = "../scalar", version = "0.1" }
bottlerocket-scalar-derive = { path = "../scalar-derive", version = "0.1" }
semver = "1"
serde = "1"
serde_json = "1"
serde_plain = "1"
snafu = "0.8"
bottlerocket-string-impls-for = { path = "../string-impls-for", version = "0.1" }
url = "2"
x509-parser = "0.16"
11 changes: 11 additions & 0 deletions bottlerocket-settings-models/modeled-types/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# modeled-types

Current version: 0.1.0

This module contains data types that can be used in the model when special input/output
(ser/de) behavior is desired. For example, the ValidBase64 type can be used for a model field
when we don't even want to accept an API call with invalid base64 data.

## Colophon

This text was generated from `README.tpl` using [cargo-readme](https://crates.io/crates/cargo-readme), and includes the rustdoc from `src/lib.rs`.
Loading

0 comments on commit c5dfef4

Please sign in to comment.