#[derive(Clone, Copy)]
enum Kind {
Integer { min: u64, max: u64 },
Float { min: f64, max: f64 },
Boolean,
Choice(&'static [&'static str]),
AbsolutePath,
Locale,
List,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Store {
Toml,
Bootstrap,
LocaleFile,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Scopes {
Both,
GlobalOnly,
}
struct KeySpec {
section: &'static str,
field: &'static str,
kind: Kind,
store: Store,
scopes: Scopes,
default: fn(&AtomwriteConfig) -> serde_json::Value,
}
impl KeySpec {
fn name(&self) -> String {
if self.section.is_empty() {
self.field.to_string()
} else {
format!("{}.{}", self.section, self.field)
}
}
}
include!("config_help.inc.rs");
const INT_MAX: u64 = i64::MAX as u64;
const UNIT: Kind = Kind::Float { min: 0.0, max: 1.0 };
const POSITIVE: Kind = Kind::Integer {
min: 1,
max: INT_MAX,
};
const fn between(min: u64, max: u64) -> Kind {
Kind::Integer { min, max }
}
const fn toml_key(
section: &'static str,
field: &'static str,
kind: Kind,
scopes: Scopes,
default: fn(&AtomwriteConfig) -> serde_json::Value,
) -> KeySpec {
KeySpec {
section,
field,
kind,
store: Store::Toml,
scopes,
default,
}
}
static KEYS: &[KeySpec] = &[
toml_key(
"defaults",
"line_ending",
Kind::Choice(&["auto", "lf", "cr-lf", "cr"]),
Scopes::Both,
|c| serde_json::json!(c.defaults.line_ending),
),
toml_key("defaults", "backup", Kind::Boolean, Scopes::Both, |c| {
serde_json::json!(c.defaults.backup)
}),
toml_key("delete", "backup", Kind::Boolean, Scopes::Both, |c| {
serde_json::json!(c.delete.backup)
}),
toml_key(
"defaults",
"retention",
between(0, u8::MAX as u64),
Scopes::Both,
|c| serde_json::json!(c.defaults.retention),
),
toml_key("defaults", "max_filesize", POSITIVE, Scopes::Both, |c| {
serde_json::json!(c.defaults.max_filesize)
}),
toml_key(
"fuzzy",
"mode",
Kind::Choice(&["auto", "aggressive", "off"]),
Scopes::Both,
|c| serde_json::json!(c.fuzzy.mode),
),
toml_key("fuzzy", "threshold", UNIT, Scopes::Both, |c| {
serde_json::json!(c.fuzzy.threshold)
}),
toml_key("fuzzy", "threshold_aggressive", UNIT, Scopes::Both, |c| {
serde_json::json!(c.fuzzy.threshold_aggressive)
}),
toml_key("fuzzy", "threshold_context", UNIT, Scopes::Both, |c| {
serde_json::json!(c.fuzzy.threshold_context)
}),
toml_key("fuzzy", "threshold_jw", UNIT, Scopes::Both, |c| {
serde_json::json!(c.fuzzy.threshold_jw)
}),
toml_key("fuzzy", "soft_floor_delta", UNIT, Scopes::Both, |c| {
serde_json::json!(c.fuzzy.soft_floor_delta)
}),
toml_key("fuzzy", "max_content_bytes", POSITIVE, Scopes::Both, |c| {
serde_json::json!(c.fuzzy.max_content_bytes)
}),
toml_key(
"apply",
"max_ellipsis_lines",
POSITIVE,
Scopes::Both,
|c| serde_json::json!(c.apply.max_ellipsis_lines),
),
toml_key(
"write",
"confirm_large_bytes",
POSITIVE,
Scopes::Both,
|c| serde_json::json!(c.write.confirm_large_bytes),
),
toml_key(
"write",
"shrink_block_percent",
between(
crate::constants::SHRINK_BLOCK_PERCENT_MIN as u64,
crate::constants::SHRINK_BLOCK_PERCENT_MAX as u64,
),
Scopes::Both,
|c| serde_json::json!(c.write.shrink_block_percent),
),
toml_key(
"write",
"auto_rotate_max_age_secs",
POSITIVE,
Scopes::Both,
|c| serde_json::json!(c.write.auto_rotate_max_age_secs),
),
toml_key(
"write",
"content_risk_patterns",
Kind::List,
Scopes::Both,
|c| {
let configured = &c.write.content_risk_patterns;
if configured.is_empty() {
serde_json::json!(crate::constants::WRITE_CONTENT_RISK_PATTERNS)
} else {
serde_json::json!(configured)
}
},
),
toml_key("watch", "idle_exit_ms", between(0, INT_MAX), Scopes::Both, |c| {
serde_json::json!(c.watch.idle_exit_ms)
}),
toml_key(
"watch",
"debounce_ms",
between(crate::constants::WATCH_DEBOUNCE_FLOOR_MS, INT_MAX),
Scopes::Both,
|c| serde_json::json!(c.watch.debounce_ms),
),
toml_key("log", "file", Kind::Boolean, Scopes::Both, |c| {
serde_json::json!(c.log.file)
}),
toml_key(
"log",
"rotation",
Kind::Choice(&["daily", "hourly"]),
Scopes::Both,
|c| serde_json::json!(c.log.rotation),
),
KeySpec {
section: "storage",
field: "home",
kind: Kind::AbsolutePath,
store: Store::Bootstrap,
scopes: Scopes::GlobalOnly,
default: |c| serde_json::json!(c.storage.home),
},
KeySpec {
section: "",
field: "locale",
kind: Kind::Locale,
store: Store::LocaleFile,
scopes: Scopes::GlobalOnly,
default: |_| serde_json::Value::Null,
},
toml_key(
"concurrency",
"max_threads",
between(
crate::constants::CONCURRENCY_MIN_THREADS,
crate::constants::CONCURRENCY_MAX_THREADS_CEILING,
),
Scopes::Both,
|c| serde_json::json!(c.concurrency.max_threads),
),
toml_key(
"concurrency",
"ram_fraction_percent",
between(
crate::constants::RAM_FRACTION_PERCENT_MIN,
crate::constants::PERCENT_MAX,
),
Scopes::Both,
|c| serde_json::json!(c.concurrency.ram_fraction_percent),
),
toml_key("search", "context", between(0, INT_MAX), Scopes::Both, |c| {
serde_json::json!(c.search.context)
}),
toml_key(
"search",
"smart_case",
Kind::Boolean,
Scopes::Both,
|c| serde_json::json!(c.search.smart_case),
),
toml_key("search", "max_filesize", POSITIVE, Scopes::Both, |c| {
serde_json::json!(c.search.max_filesize)
}),
toml_key("search", "max_columns", POSITIVE, Scopes::Both, |c| {
serde_json::json!(c.search.max_columns)
}),
toml_key(
"search",
"max_inflight_bytes",
between(
crate::constants::SEARCH_MIN_INFLIGHT_BYTES,
crate::constants::SEARCH_MAX_INFLIGHT_CEILING_BYTES,
),
Scopes::Both,
|c| serde_json::json!(c.search.max_inflight_bytes),
),
toml_key(
"search",
"format",
Kind::Choice(&["ndjson", "text"]),
Scopes::Both,
|c| serde_json::json!(c.search.format),
),
toml_key(
"read",
"max_output_bytes",
POSITIVE,
Scopes::Both,
|c| serde_json::json!(c.read.max_output_bytes),
),
toml_key("read", "number", Kind::Boolean, Scopes::Both, |c| {
serde_json::json!(c.read.number)
}),
toml_key(
"output",
"profile",
Kind::Choice(&["full", "lean"]),
Scopes::Both,
|c| serde_json::json!(c.output.profile),
),
toml_key(
"summary",
"errors_max_entries",
between(
crate::constants::SUMMARY_ERRORS_MIN_ENTRIES as u64,
crate::constants::SUMMARY_ERRORS_CEILING as u64,
),
Scopes::Both,
|c| serde_json::json!(c.summary.errors_max_entries),
),
toml_key(
"lock",
"acquire_timeout_ms",
between(
crate::constants::LOCK_MIN_ACQUIRE_TIMEOUT_MS,
crate::constants::LOCK_MAX_ACQUIRE_TIMEOUT_MS,
),
Scopes::Both,
|c| serde_json::json!(c.lock.acquire_timeout_ms),
),
toml_key(
"retry",
"base_ms",
between(
crate::constants::RETRY_MIN_MS,
crate::constants::RETRY_MAX_WAIT_CEILING_MS,
),
Scopes::Both,
|_| serde_json::json!(crate::constants::LOCK_RETRY_BASE_MS),
),
toml_key(
"retry",
"max_ms",
between(
crate::constants::RETRY_MIN_MS,
crate::constants::RETRY_MAX_WAIT_CEILING_MS,
),
Scopes::Both,
|_| serde_json::json!(crate::constants::LOCK_RETRY_CAP_MS),
),
toml_key(
"retry",
"max_elapsed_ms",
between(
crate::constants::RETRY_MIN_MS,
crate::constants::RETRY_MAX_ELAPSED_CEILING_MS,
),
Scopes::Both,
|_| serde_json::json!(crate::constants::PERSIST_RETRY_MAX_ELAPSED_MS),
),
toml_key(
"extract",
"max_sample_rows",
between(0, crate::constants::EXTRACT_MAX_SAMPLE_ROWS_CEILING),
Scopes::Both,
|c| serde_json::json!(c.extract.max_sample_rows),
),
toml_key(
"extract",
"max_text_chars",
between(1, crate::constants::EXTRACT_MAX_TEXT_CHARS_CEILING),
Scopes::Both,
|c| serde_json::json!(c.extract.max_text_chars),
),
toml_key(
"extract",
"max_sheets",
between(1, crate::constants::EXTRACT_MAX_PARTS_CEILING),
Scopes::Both,
|c| serde_json::json!(c.extract.max_sheets),
),
toml_key(
"extract",
"max_slides",
between(1, crate::constants::EXTRACT_MAX_PARTS_CEILING),
Scopes::Both,
|c| serde_json::json!(c.extract.max_slides),
),
];
fn find_key(key: &str) -> Result<&'static KeySpec, AtomwriteError> {
KEYS.iter()
.find(|spec| spec.name() == key)
.ok_or_else(|| AtomwriteError::InvalidInput {
reason: format!(
"unknown config key '{key}'; valid keys: {}",
KEYS.iter()
.map(KeySpec::name)
.collect::<Vec<_>>()
.join(", ")
),
})
}
struct Parsed {
toml_text: String,
json: serde_json::Value,
}
fn out_of_range(key: &str, raw: &str, range: &str) -> AtomwriteError {
AtomwriteError::InvalidInput {
reason: format!(
"config key '{key}' = {raw} {} {range}",
crate::constants::CONFIG_OUT_OF_RANGE_REASON_MARKER
),
}
}
fn bad_value(key: &str, expected: &str, raw: &str) -> AtomwriteError {
AtomwriteError::InvalidInput {
reason: format!("config key '{key}' expects {expected}, got '{raw}'"),
}
}
impl Kind {
fn label(&self) -> &'static str {
match self {
Self::Integer { .. } => "integer",
Self::Float { .. } => "float",
Self::Boolean => "boolean",
Self::Choice(_) | Self::AbsolutePath | Self::Locale => "string",
Self::List => "list",
}
}
fn parse(&self, key: &str, raw: &str) -> Result<Parsed, AtomwriteError> {
let raw = raw.trim();
match *self {
Self::Integer { min, max } => {
let n: i128 = raw
.parse()
.map_err(|_| bad_value(key, "a whole number", raw))?;
let n = u64::try_from(n)
.ok()
.filter(|n| (min..=max).contains(n))
.ok_or_else(|| out_of_range(key, raw, &format!("{min}..={max}")))?;
Ok(Parsed {
toml_text: n.to_string(),
json: serde_json::json!(n),
})
}
Self::Float { min, max } => {
let n: f64 = raw
.parse()
.ok()
.filter(|n: &f64| n.is_finite())
.ok_or_else(|| bad_value(key, "a finite number", raw))?;
if !(min..=max).contains(&n) {
return Err(out_of_range(key, raw, &format!("{min}..={max}")));
}
Ok(Parsed {
toml_text: format!("{n:?}"),
json: serde_json::json!(n),
})
}
Self::Boolean => match raw {
"true" | "false" => Ok(Parsed {
toml_text: raw.to_string(),
json: serde_json::json!(raw == "true"),
}),
_ => Err(bad_value(key, "true or false", raw)),
},
Self::Choice(words) => {
if words.contains(&raw) {
Ok(Parsed {
toml_text: raw.to_string(),
json: serde_json::json!(raw),
})
} else {
Err(bad_value(key, &format!("one of {}", words.join(", ")), raw))
}
}
Self::AbsolutePath => {
if Path::new(raw).is_absolute() {
Ok(Parsed {
toml_text: raw.to_string(),
json: serde_json::json!(raw),
})
} else {
Err(bad_value(key, "an absolute path", raw))
}
}
Self::Locale => crate::locale::Language::from_tag(raw)
.map(|language| Parsed {
toml_text: language.as_str().to_string(),
json: serde_json::json!(language.as_str()),
})
.ok_or_else(|| bad_value(key, "a supported locale (en, pt-BR)", raw)),
Self::List => parse_list(key, raw),
}
}
}
include!("config_list_parse.inc.rs");