Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor CLI #93

Merged
merged 8 commits into from
Jun 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
# will default to the old resolver.
resolver = "2"

# Members of the workspace, just Hipcheck and our custom task runner.
members = ["hipcheck", "xtask"]
# Members of the workspace.
members = ["hipcheck", "hipcheck-macros", "xtask"]

# Make sure Hipcheck is run with `cargo run`.
#
Expand Down
13 changes: 13 additions & 0 deletions hipcheck-macros/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "hipcheck-macros"
version = "0.1.0"
edition = "2021"
license = "Apache-2.0"

[lib]
proc-macro = true

[dependencies]
proc-macro2 = "1.0.84"
quote = "1.0.36"
syn = "2.0.66"
12 changes: 12 additions & 0 deletions hipcheck-macros/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@

# `hipcheck-macros`

`hipcheck-macros` is a helper crate for [Hipcheck] which provides procedural
macros. It's not intended for use by anyone else, and generally involves
private APIs.

## License

`hipcheck-macros` is Apache-2.0 licensed.

[Hipcheck]: https://github.com/mitre/hipcheck
21 changes: 21 additions & 0 deletions hipcheck-macros/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// SPDX-License-Identifier: Apache-2.0

// Use the `README.md` as the crate docs.
#![doc = include_str!("../README.md")]

mod update;

use proc_macro::TokenStream;
use syn::{parse_macro_input, DeriveInput, Error};

/// Derive an implementation of the `Update` trait for the type.
#[proc_macro_derive(Update)]
pub fn derive_update(input: TokenStream) -> TokenStream {
// Parse the input.
let input = parse_macro_input!(input as DeriveInput);

// Generate the token stream.
update::derive_update(input)
.unwrap_or_else(Error::into_compile_error)
.into()
}
110 changes: 110 additions & 0 deletions hipcheck-macros/src/update.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// SPDX-License-Identifier: Apache-2.0

use proc_macro2::TokenStream;
use quote::quote;
use std::ops::Not as _;
use syn::{
spanned::Spanned, Data, DataStruct, DeriveInput, Error, Field, Fields, FieldsNamed, Ident,
Result,
};

/// Convenience macro for producing new `syn::Error`s.
macro_rules! err {
( $span:expr, $msg:literal ) => {
Err(Error::new($span, $msg))
};
}

/// A `proc_macro2`-flavor implementation of the derive macro.
pub fn derive_update(input: DeriveInput) -> Result<TokenStream> {
let ident = &input.ident;
let fields = extract_field_names(&input)?;

Ok(quote! {
impl crate::cli::Update for #ident {
fn update(&mut self, other: &Self) {
#( self.#fields.update(&other.#fields ); )*
}
}
})
}

/// Extract field names from derive input.
fn extract_field_names(input: &DeriveInput) -> Result<Vec<Ident>> {
let strukt = extract_struct(input)?;
let fields = extract_named_fields(strukt)?;
let names = extract_field_names_from_fields(&fields[..])?;
Ok(names)
}

/// Validate the input type has no generic parameters.
fn validate_no_generics(input: &DeriveInput) -> Result<()> {
// We don't support generic types.
let generics = input.generics.params.iter().collect::<Vec<_>>();
if generics.is_empty().not() {
return err!(
input.span(),
"#[derive(Update)] does not support generic types"
);
}

Ok(())
}

/// Extract a struct from the input.
fn extract_struct(input: &DeriveInput) -> Result<&DataStruct> {
validate_no_generics(input)?;

match &input.data {
Data::Struct(struct_data) => Ok(struct_data),
Data::Enum(_) => err!(input.span(), "#[derive(Update)] does not support enums"),
Data::Union(_) => err!(input.span(), "#[derive(Update)] does not support unions"),
}
}

/// Extract named fields from a struct.
fn extract_named_fields(input: &DataStruct) -> Result<Vec<&Field>> {
match &input.fields {
Fields::Named(fields) => Ok(collect_named_fields(fields)),
field @ Fields::Unnamed(_) => err!(
field.span(),
"#[derive(Update)] does not support unnamed fields"
),
field @ Fields::Unit => err!(
field.span(),
"#[derive(Update)] does not support unit structs"
),
}
}

/// Validate all fields are named.
fn validate_named_fields(fields: &[&Field]) -> Result<()> {
for field in fields {
if field.ident.is_none() {
return err!(
field.span(),
"#[derive(Update)] does not support unnamed fields"
);
}
}

Ok(())
}

/// Collect named fields into a convenient `Vec`.
fn collect_named_fields(fields: &FieldsNamed) -> Vec<&Field> {
fields.named.iter().collect::<Vec<_>>()
}

/// Extract field names from a bunch of named fields.
fn extract_field_names_from_fields(fields: &[&Field]) -> Result<Vec<Ident>> {
// SAFETY: We confirm the `ident` is present, so the `unwrap` is fine.
validate_named_fields(fields)?;

let names = fields
.iter()
.map(|field| field.ident.as_ref().unwrap().to_owned())
.collect();

Ok(names)
}
4 changes: 2 additions & 2 deletions hipcheck/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ path = "src/main.rs"
content_inspector = "0.2.4"
dotenv = "0.15.0"
chrono = { version = "0.4.19", features = ["serde"] }
clap = { version = "4.5.4", default-features = false, features = ["string", "std", "derive"] }
clap-verbosity-flag = "2.2.0"
clap = { version = "4.5.4", features = ["derive"] }
dirs = "5.0.1"
duct = "0.13.5"
env_logger = { version = "0.11.3" }
graphql_client = "0.14.0"
hipcheck-macros = { path = "../hipcheck-macros" }
lazy_static = "1.4.0"
libc = "0.2.155"
log = "0.4.16"
Expand Down
Loading