Skip to content

Commit

Permalink
chore(config): leverage proc macros (#9111)
Browse files Browse the repository at this point in the history
### Description

This PR leverages
[derive_setters](https://crates.io/crates/derive_setters) and
[merge](https://docs.rs/merge/latest/merge/index.html) to reduce the
amount of changes required when adding a field to the configuration
options.

The manual setup is error prone as there are 4 places that need to be
updated and forgetting the final place will result in code compiling,
but the field never getting layered properly.

### Testing Instructions

Existing unit & integration tests, but also looking what the proc macros
are generating (outputs shown are produced using `rust-analyzer`'s
[Expand
macro](https://rust-analyzer.github.io/manual.html#expand-macro-recursively)
feature:

Output of `#[derive(Setters)]`
```
impl TurborepoConfigBuilder {
    pub fn with_api_url(mut self, value: Option<String>) -> Self {
        self.override_config.api_url = value;
        self
    }
...
```
Which exactly matches what `create_builder` would produce:
```
pub fn with_api_url(mut self, value: Option<String>) -> Self {
    self.override_config.api_url = value;
    self
}
```

`#derive(Merge)` produces
```
impl ::merge::Merge for ConfigurationOptions {
    fn merge(&mut self, other: Self) {
        ::merge::Merge::merge(&mut self.api_url, other.api_url);
...
```
and `Merge` is defined for `Option` as :
```
impl<T> Merge for Option<T> {
    fn merge(&mut self, mut other: Self) {
        if !self.is_some() {
            *self = other.take();
        }
    }
}
```
[source](https://docs.rs/merge/latest/src/merge/lib.rs.html#139-145)

So `self.api_url` will only be set to `other.api_url` if there isn't a
value present which is the behavior we want since we merge configs from
highest to lowest precedence.

---------

Co-authored-by: Nicholas Yang <nicholas.yang@vercel.com>
  • Loading branch information
chris-olszewski and NicholasLYang authored Sep 10, 2024
1 parent 2adeac9 commit bd2bffa
Show file tree
Hide file tree
Showing 4 changed files with 50 additions and 93 deletions.
36 changes: 36 additions & 0 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ console = "0.15.5"
console-subscriber = "0.1.8"
crossbeam-channel = "0.5.8"
dashmap = "5.4.0"
derive_setters = "0.1.6"
dialoguer = "0.10.3"
dunce = "1.0.3"
either = "1.9.0"
Expand All @@ -108,6 +109,7 @@ indicatif = "0.17.3"
indoc = "2.0.0"
itertools = "0.10.5"
lazy_static = "1.4.0"
merge = "0.1.0"
mime = "0.3.16"
notify = "6.1.1"
once_cell = "1.17.1"
Expand Down
2 changes: 2 additions & 0 deletions crates/turborepo-lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ const_format = "0.2.30"
convert_case = "0.6.0"
crossterm = "0.26"
ctrlc = { version = "3.4.0", features = ["termination"] }
derive_setters = { workspace = true }
dialoguer = { workspace = true, features = ["fuzzy-select"] }
dirs-next = "2.0.0"
dunce = { workspace = true }
Expand All @@ -70,6 +71,7 @@ itertools = { workspace = true }
jsonc-parser = { version = "0.21.0" }
lazy_static = { workspace = true }
libc = "0.2.140"
merge = { workspace = true }
miette = { workspace = true, features = ["fancy"] }
nix = "0.26.2"
notify = { workspace = true }
Expand Down
103 changes: 10 additions & 93 deletions crates/turborepo-lib/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ use std::{collections::HashMap, ffi::OsString, io};

use camino::{Utf8Path, Utf8PathBuf};
use convert_case::{Case, Casing};
use derive_setters::Setters;
use env::{EnvVars, OverrideEnvVars};
use file::{AuthFile, ConfigFile};
use merge::Merge;
use miette::{Diagnostic, NamedSource, SourceSpan};
use serde::Deserialize;
use struct_iterable::Iterable;
Expand Down Expand Up @@ -173,15 +175,6 @@ pub enum Error {
},
}

macro_rules! create_builder {
($func_name:ident, $property_name:ident, $type:ty) => {
pub fn $func_name(mut self, value: $type) -> Self {
self.override_config.$property_name = value;
self
}
};
}

const DEFAULT_API_URL: &str = "https://vercel.com/api";
const DEFAULT_LOGIN_URL: &str = "https://vercel.com";
const DEFAULT_TIMEOUT: u64 = 30;
Expand All @@ -190,8 +183,13 @@ const DEFAULT_UPLOAD_TIMEOUT: u64 = 60;
// We intentionally don't derive Serialize so that different parts
// of the code that want to display the config can tune how they
// want to display and what fields they want to include.
#[derive(Deserialize, Default, Debug, PartialEq, Eq, Clone, Iterable)]
#[derive(Deserialize, Default, Debug, PartialEq, Eq, Clone, Iterable, Merge, Setters)]
#[serde(rename_all = "camelCase")]
// Generate setters for the builder type that set these values on its override_config field
#[setters(
prefix = "with_",
generate_delegates(ty = "TurborepoConfigBuilder", field = "override_config")
)]
pub struct ConfigurationOptions {
#[serde(alias = "apiurl")]
#[serde(alias = "ApiUrl")]
Expand Down Expand Up @@ -336,44 +334,6 @@ impl ConfigurationOptions {
}
}

macro_rules! create_set_if_empty {
($func_name:ident, $property_name:ident, $type:ty) => {
fn $func_name(&mut self, value: &mut Option<$type>) {
if self.$property_name.is_none() {
if let Some(value) = value.take() {
self.$property_name = Some(value);
}
}
}
};
}

// Private setters used only for construction
impl ConfigurationOptions {
create_set_if_empty!(set_api_url, api_url, String);
create_set_if_empty!(set_login_url, login_url, String);
create_set_if_empty!(set_team_slug, team_slug, String);
create_set_if_empty!(set_team_id, team_id, String);
create_set_if_empty!(set_token, token, String);
create_set_if_empty!(set_signature, signature, bool);
create_set_if_empty!(set_enabled, enabled, bool);
create_set_if_empty!(set_preflight, preflight, bool);
create_set_if_empty!(set_timeout, timeout, u64);
create_set_if_empty!(set_ui, ui, UIMode);
create_set_if_empty!(set_allow_no_package_manager, allow_no_package_manager, bool);
create_set_if_empty!(set_daemon, daemon, bool);
create_set_if_empty!(set_env_mode, env_mode, EnvMode);
create_set_if_empty!(set_cache_dir, cache_dir, Utf8PathBuf);
create_set_if_empty!(set_scm_base, scm_base, String);
create_set_if_empty!(set_scm_head, scm_head, String);
create_set_if_empty!(set_spaces_id, spaces_id, String);
create_set_if_empty!(
set_root_turbo_json_path,
root_turbo_json_path,
AbsoluteSystemPathBuf
);
}

// Maps Some("") to None to emulate how Go handles empty strings
fn non_empty_str(s: Option<&str>) -> Option<&str> {
s.filter(|s| !s.is_empty())
Expand Down Expand Up @@ -428,30 +388,6 @@ impl TurborepoConfigBuilder {
.unwrap_or_else(get_lowercased_env_vars)
}

create_builder!(with_api_url, api_url, Option<String>);
create_builder!(with_login_url, login_url, Option<String>);
create_builder!(with_team_slug, team_slug, Option<String>);
create_builder!(with_team_id, team_id, Option<String>);
create_builder!(with_token, token, Option<String>);
create_builder!(with_signature, signature, Option<bool>);
create_builder!(with_enabled, enabled, Option<bool>);
create_builder!(with_preflight, preflight, Option<bool>);
create_builder!(with_timeout, timeout, Option<u64>);
create_builder!(with_ui, ui, Option<UIMode>);
create_builder!(
with_allow_no_package_manager,
allow_no_package_manager,
Option<bool>
);
create_builder!(with_daemon, daemon, Option<bool>);
create_builder!(with_env_mode, env_mode, Option<EnvMode>);
create_builder!(with_cache_dir, cache_dir, Option<Utf8PathBuf>);
create_builder!(
with_root_turbo_json_path,
root_turbo_json_path,
Option<AbsoluteSystemPathBuf>
);

pub fn build(&self) -> Result<ConfigurationOptions, Error> {
// Priority, from least significant to most significant:
// - shared configuration (turbo.json)
Expand Down Expand Up @@ -483,27 +419,8 @@ impl TurborepoConfigBuilder {
let config = sources.into_iter().try_fold(
ConfigurationOptions::default(),
|mut acc, current_source| {
let mut current_source_config = current_source.get_configuration_options(&acc)?;
acc.set_api_url(&mut current_source_config.api_url);
acc.set_login_url(&mut current_source_config.login_url);
acc.set_team_slug(&mut current_source_config.team_slug);
acc.set_team_id(&mut current_source_config.team_id);
acc.set_token(&mut current_source_config.token);
acc.set_signature(&mut current_source_config.signature);
acc.set_enabled(&mut current_source_config.enabled);
acc.set_preflight(&mut current_source_config.preflight);
acc.set_timeout(&mut current_source_config.timeout);
acc.set_spaces_id(&mut current_source_config.spaces_id);
acc.set_ui(&mut current_source_config.ui);
acc.set_allow_no_package_manager(
&mut current_source_config.allow_no_package_manager,
);
acc.set_daemon(&mut current_source_config.daemon);
acc.set_env_mode(&mut current_source_config.env_mode);
acc.set_scm_base(&mut current_source_config.scm_base);
acc.set_scm_head(&mut current_source_config.scm_head);
acc.set_cache_dir(&mut current_source_config.cache_dir);
acc.set_root_turbo_json_path(&mut current_source_config.root_turbo_json_path);
let current_source_config = current_source.get_configuration_options(&acc)?;
acc.merge(current_source_config);
Ok(acc)
},
);
Expand Down

0 comments on commit bd2bffa

Please sign in to comment.