///|
/// A validation rule for one configuration path.
pub enum Rule {
Required(String)
EnumValue(String, Array[String])
IntRange(String, Int?, Int?)
Deprecated(String, String)
Requires(String, String)
Conflicts(String, String)
SecretLike(String)
} derive(@debug.Debug, Eq)
///|
pub fn required(path : String) -> Rule {
Required(path)
}
///|
pub fn enum_value(path : String, allowed : Array[String]) -> Rule {
EnumValue(path, allowed)
}
///|
pub fn int_range(path : String, min : Int?, max : Int?) -> Rule {
IntRange(path, min, max)
}
///|
pub fn deprecated(path : String, replacement : String) -> Rule {
Deprecated(path, replacement)
}
///|
pub fn requires(path : String, required_path : String) -> Rule {
Requires(path, required_path)
}
///|
pub fn conflicts(path : String, other_path : String) -> Rule {
Conflicts(path, other_path)
}
///|
pub fn secret_like(path : String) -> Rule {
SecretLike(path)
}
///|
/// Applies validation rules to an already-merged view.
pub fn validate(view : ConfigView, rules : Array[Rule]) -> Array[Diagnostic] {
let diagnostics : Array[Diagnostic] = []
for rule in rules {
match rule {
Required(path) => validate_required(view, diagnostics, path)
EnumValue(path, allowed) =>
validate_enum(view, diagnostics, path, allowed)
IntRange(path, min, max) =>
validate_int_range(view, diagnostics, path, min, max)
Deprecated(path, replacement) =>
validate_deprecated(view, diagnostics, path, replacement)
Requires(path, required_path) =>
validate_requires(view, diagnostics, path, required_path)
Conflicts(path, other_path) =>
validate_conflicts(view, diagnostics, path, other_path)
SecretLike(path) => validate_secret_like(view, diagnostics, path)
}
}
diagnostics
}
///|
fn validate_required(
view : ConfigView,
diagnostics : Array[Diagnostic],
path : String,
) -> Unit {
match view.get_path(path) {
None =>
diagnostics.push(
error(
"required",
"missing required configuration key '" + path + "'",
hint=Some(
"add a non-empty value to a base or environment-specific config layer",
),
),
)
Some(value) =>
if value.trim() == "" {
diagnostics.push(
error(
"required",
"required configuration key '" + path + "' is empty",
hint=Some("set a non-empty value in the effective config layer"),
),
)
}
}
}
///|
fn validate_enum(
view : ConfigView,
diagnostics : Array[Diagnostic],
path : String,
allowed : Array[String],
) -> Unit {
match view.get_path(path) {
None => ()
Some(value) =>
if !contains_string_value(allowed, value) {
diagnostics.push(
error(
"enum",
"key '" + path + "' has unsupported value '" + value + "'",
hint=Some("allowed values: " + allowed.join(", ")),
),
)
}
}
}
///|
fn validate_int_range(
view : ConfigView,
diagnostics : Array[Diagnostic],
path : String,
min : Int?,
max : Int?,
) -> Unit {
match view.get_path(path) {
None => ()
Some(raw) => {
match view.get_int(path) {
None =>
diagnostics.push(
error("int", "key '" + path + "' must be an integer"),
)
Some(value) => {
match min {
Some(min_value) =>
if value < min_value {
diagnostics.push(
error(
"range",
"key '" +
path +
"' is below minimum " +
min_value.to_string(),
),
)
}
None => ()
}
match max {
Some(max_value) =>
if value > max_value {
diagnostics.push(
error(
"range",
"key '" +
path +
"' is above maximum " +
max_value.to_string(),
),
)
}
None => ()
}
}
}
ignore(raw)
}
}
}
///|
fn validate_deprecated(
view : ConfigView,
diagnostics : Array[Diagnostic],
path : String,
replacement : String,
) -> Unit {
if view.get_path(path) != None {
diagnostics.push(
warning(
"deprecated",
"key '" + path + "' is deprecated",
hint=Some("use '" + replacement + "' instead"),
),
)
}
}
///|
fn validate_requires(
view : ConfigView,
diagnostics : Array[Diagnostic],
path : String,
required_path : String,
) -> Unit {
if view.get_path(path) != None && view.get_path(required_path) == None {
diagnostics.push(
error("requires", "key '" + path + "' requires '" + required_path + "'"),
)
}
}
///|
fn validate_conflicts(
view : ConfigView,
diagnostics : Array[Diagnostic],
path : String,
other_path : String,
) -> Unit {
if view.get_path(path) != None && view.get_path(other_path) != None {
diagnostics.push(
error(
"conflicts",
"key '" + path + "' conflicts with '" + other_path + "'",
),
)
}
}
///|
fn validate_secret_like(
view : ConfigView,
diagnostics : Array[Diagnostic],
path : String,
) -> Unit {
match view.get_path(path) {
None => ()
Some(value) =>
if value.length() == 0 {
diagnostics.push(
error("secret-empty", "secret-like key '" + path + "' is empty"),
)
} else if value.length() < 8 {
diagnostics.push(
warning(
"secret-short",
"secret-like key '" + path + "' looks too short",
),
)
}
}
}
///|
fn contains_string_value(values : Array[String], target : String) -> Bool {
for value in values {
if value == target {
return true
}
}
false
}
///|
/// Returns true if any diagnostic is an error.
pub fn has_errors(diagnostics : Array[Diagnostic]) -> Bool {
!diagnostics_ok(diagnostics)
}