Skip to content

Commit 0f13f8a

Browse files
authored
feat: codegen settings (jdx#2640)
Fixes #2633
1 parent 4e19405 commit 0f13f8a

File tree

23 files changed

+1348
-569
lines changed

23 files changed

+1348
-569
lines changed

.github/workflows/release-plz.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ jobs:
3737
with: { toolchain: nightly, components: rustfmt }
3838
- uses: actions-rust-lang/setup-rust-toolchain@v1
3939
- run: mkdir -p "$HOME/bin" && echo "$HOME/bin" >> "$GITHUB_PATH"
40+
- run: npm i
4041
- run: cargo build --all-features && cp target/debug/mise "$HOME"/bin
4142
- uses: actions/cache/restore@v4
4243
with:

.markdownlintignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ registry/
22
target/
33
CHANGELOG.md
44
docs/node_modules/
5+
node_modules/

.mise.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ jq = "latest"
2222
"npm:prettier" = "3"
2323
direnv = "latest"
2424
actionlint = "latest"
25+
"pipx:toml-sort" = "latest"
2526
#python = { version = "latest", virtualenv = "{{env.HOME}}/.cache/venv" }
2627
#ruby = "3.1"
2728

.mise/tasks/lint-fix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ shfmt -w -i 2 -ci -bn "${scripts_dirs[@]}"
1212
prettier -w $(git ls-files '*.yml' '*.yaml')
1313
markdownlint --fix .
1414
actionlint
15+
toml-sort -i settings.toml --spaces-indent-inline-array 4
1516

1617
cat >rustfmt.toml <<EOF
1718
unstable_features = true

.mise/tasks/lint/settings

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
toml-sort --check settings.toml --spaces-indent-inline-array 4

.mise/tasks/render/settings

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#!/usr/bin/env node
2+
3+
const fs = require('fs');
4+
const toml = require('toml');
5+
const child_process = require('child_process');
6+
const Handlebars = require('handlebars');
7+
8+
const doc = toml.parse(fs.readFileSync('settings.toml', 'utf-8'));
9+
const settings = {};
10+
11+
function buildElement(key, props) {
12+
let type = props.type;
13+
if (type.startsWith('Option<')) {
14+
type = type.slice(7, -1);
15+
}
16+
type = type.replaceAll('PathBuf', 'String');
17+
if (type === 'bool') {
18+
type = 'boolean';
19+
} else if (type === "String" || type === "PathBuf") {
20+
type = 'string';
21+
} else if (type === "usize" || type === "u64") {
22+
type = 'number';
23+
} else if (type === "BTreeSet<String>" || type === "HashSet<String>" || type === "Vec<String>") {
24+
type = 'string[]';
25+
} else {
26+
throw new Error(`Unknown type: ${type}`);
27+
}
28+
if (!props.description) {
29+
console.error(`Missing description for ${key}`);
30+
process.exit(1);
31+
}
32+
const ele = {
33+
default: props.default,
34+
description: props.description,
35+
deprecated: props.deprecated,
36+
type,
37+
};
38+
if (props.enum) {
39+
ele.enum = props.enum.map((e) => e[0]);
40+
}
41+
if (type === 'string[]') {
42+
ele.type = 'array';
43+
ele.items = {
44+
type: 'string',
45+
};
46+
}
47+
return ele;
48+
}
49+
50+
for (const key in doc) {
51+
const props = doc[key];
52+
if (props.type) {
53+
settings[key] = buildElement(key, props);
54+
} else {
55+
for (const subkey in props) {
56+
settings[key] = settings[key] || {
57+
additionalProperties: false,
58+
description: props.description,
59+
properties: {},
60+
};
61+
settings[key].properties[subkey] = buildElement(`${key}.${subkey}`, props[subkey]);
62+
}
63+
}
64+
}
65+
66+
const schema_tmpl = Handlebars.compile(fs.readFileSync('schema/mise.json.hbs', 'utf-8'));
67+
fs.writeFileSync('schema/mise.json.tmp', schema_tmpl({
68+
settings_json: new Handlebars.SafeString(JSON.stringify(settings, null, 2)),
69+
}));
70+
71+
child_process.execSync('jq . < schema/mise.json.tmp > schema/mise.json');
72+
fs.unlinkSync('schema/mise.json.tmp');

Cargo.toml

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,16 @@ license = "MIT"
1212
keywords = ["mise"]
1313
categories = ["command-line-utilities"]
1414
include = [
15-
"src/**/*.rs",
16-
"src/plugins/core/assets/**",
17-
"src/assets/**",
18-
"/completions/*",
1915
"/Cargo.lock",
2016
"/LICENSE",
2117
"/README.md",
2218
"/build.rs",
19+
"/completions/*",
20+
"/settings.toml",
2321
"/zipsign.pub",
22+
"src/**/*.rs",
23+
"src/assets/**",
24+
"src/plugins/core/assets/**",
2425
]
2526
rust-version = "1.76.0"
2627
build = "build.rs"
@@ -69,7 +70,7 @@ heck = "0.5"
6970
home = "0.5.9"
7071
humantime = "2"
7172
indenter = "0.3.3"
72-
indexmap = { version = "2.2.6", features = ["serde"] }
73+
indexmap = { version = "2", features = ["serde"] }
7374
indicatif = { version = "0.17.8", features = ["default", "improved_unicode"] }
7475
indoc = "2.0.5"
7576
itertools = "0.13"
@@ -111,8 +112,8 @@ tokio = { version = "1.37.0", features = [
111112
"rt",
112113
"time",
113114
] }
114-
toml = { version = "0.8.12", features = ["parse"] }
115-
toml_edit = { version = "0.22.12", features = ["parse"] }
115+
toml = { version = "0.8", features = ["parse"] }
116+
toml_edit = { version = "0.22", features = ["parse"] }
116117
url = "2.5.0"
117118
usage-lib = { version = "0.3", features = ["clap"] }
118119
versions = { version = "6.2.0", features = ["serde"] }
@@ -141,6 +142,9 @@ sevenz-rust = "0.6"
141142
[build-dependencies]
142143
built = { version = "0.7", features = ["chrono", "git2"] }
143144
cfg_aliases = "0.2"
145+
heck = "0.5"
146+
toml = "0.8"
147+
indexmap = "2"
144148

145149
[dev-dependencies]
146150
assert_cmd = "2.0.14"

build.rs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,96 @@
1+
use heck::ToUpperCamelCase;
2+
use indexmap::IndexMap;
3+
use std::path::Path;
4+
use std::{env, fs};
5+
16
fn main() {
27
cfg_aliases::cfg_aliases! {
38
vfox: { any(feature = "vfox", target_os = "windows") },
49
asdf: { any(feature = "asdf", not(target_os = "windows")) },
510
}
611
built::write_built_file().expect("Failed to acquire build-time information");
12+
13+
codegen_settings();
14+
}
15+
16+
fn codegen_settings() {
17+
let out_dir = env::var_os("OUT_DIR").unwrap();
18+
let dest_path = Path::new(&out_dir).join("settings.rs");
19+
let mut lines = vec![r#"
20+
#[derive(Config, Default, Debug, Clone, Serialize)]
21+
#[config(partial_attr(derive(Clone, Serialize, Default)))]
22+
pub struct Settings {"#
23+
.to_string()];
24+
25+
let settings: toml::Table = fs::read_to_string("settings.toml")
26+
.unwrap()
27+
.parse()
28+
.unwrap();
29+
let props_to_code = |key: &str, props: &toml::Value| {
30+
let mut lines = vec![];
31+
let props = props.as_table().unwrap();
32+
if let Some(description) = props.get("description") {
33+
lines.push(format!(" /// {}", description.as_str().unwrap()));
34+
}
35+
if let Some(type_) = props.get("type") {
36+
let mut opts = IndexMap::new();
37+
if let Some(env) = props.get("env") {
38+
opts.insert("env".to_string(), env.to_string());
39+
}
40+
if let Some(default) = props.get("default") {
41+
opts.insert("default".to_string(), default.to_string());
42+
} else if type_.as_str().unwrap() == "bool" {
43+
opts.insert("default".to_string(), "false".to_string());
44+
}
45+
if let Some(parse_env) = props.get("parse_env") {
46+
opts.insert(
47+
"parse_env".to_string(),
48+
parse_env.as_str().unwrap().to_string(),
49+
);
50+
}
51+
dbg!(&opts);
52+
lines.push(format!(
53+
" #[config({})]",
54+
opts.iter()
55+
.map(|(k, v)| format!("{k} = {v}"))
56+
.collect::<Vec<_>>()
57+
.join(", ")
58+
));
59+
lines.push(format!(" pub {}: {},", key, type_.as_str().unwrap()));
60+
} else {
61+
lines.push(" #[config(nested)]".to_string());
62+
lines.push(format!(
63+
" pub {}: Settings{},",
64+
key,
65+
key.to_upper_camel_case()
66+
));
67+
}
68+
lines.join("\n")
69+
};
70+
for (key, props) in &settings {
71+
lines.push(props_to_code(key, props));
72+
}
73+
lines.push("}".to_string());
74+
75+
let nested_settings = settings
76+
.iter()
77+
.filter(|(_, v)| !v.as_table().unwrap().contains_key("type"))
78+
.collect::<Vec<_>>();
79+
for (child, props) in nested_settings {
80+
lines.push(format!(
81+
r#"#[derive(Config, Default, Debug, Clone, Serialize)]
82+
#[config(partial_attr(derive(Clone, Serialize, Default)))]
83+
#[config(partial_attr(serde(deny_unknown_fields)))]
84+
pub struct Settings{name} {{
85+
"#,
86+
name = child.to_upper_camel_case()
87+
));
88+
89+
for (key, props) in props.as_table().unwrap() {
90+
lines.push(props_to_code(key, props));
91+
}
92+
lines.push("}".to_string());
93+
}
94+
95+
fs::write(&dest_path, lines.join("\n")).unwrap();
796
}

docs/.vitepress/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export default defineConfig({
3030
{text: 'IDE Integration', link: '/ide-integration'},
3131
{text: 'Paranoid', link: '/paranoid'},
3232
{text: 'Registry', link: '/registry'},
33+
{text: 'Settings', link: '/settings'},
3334
{text: 'Plugins', link: '/plugins'},
3435
{text: 'Coming from rtx', link: '/rtx'},
3536
{text: 'Team', link: '/team'},

0 commit comments

Comments
 (0)