///| 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 {
if view.get_path(path) == None {
diagnostics.push(error("required", "missing required configuration key '" + path + "'", hint=Some("add the key to a base or environment-specific 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)
}