///|
/// Type constraint for a runtime configuration key.
pub(all) enum ConfigKind {
Text
Integer
Boolean
ModeValue
ByteSequence
} derive(Debug, Eq)
///|
/// A configuration value represented without a dynamic runtime dependency.
pub(all) enum ConfigValue {
TextValue(String)
IntegerValue(Int)
BooleanValue(Bool)
ModeValue(Mode)
ByteSequenceValue(Array[Byte])
} derive(Debug)
///|
pub fn config_kind_name(kind : ConfigKind) -> String {
match kind {
Text => "text"
Integer => "integer"
Boolean => "boolean"
ModeValue => "mode"
ByteSequence => "bytes"
}
}
///|
pub fn ConfigValue::kind(self : ConfigValue) -> ConfigKind {
match self {
TextValue(_) => Text
IntegerValue(_) => Integer
BooleanValue(_) => Boolean
ModeValue(_) => ModeValue
ByteSequenceValue(_) => ByteSequence
}
}
///|
pub fn ConfigValue::as_text(self : ConfigValue) -> Result[String, ModbusError] {
match self {
TextValue(value) => Ok(value)
_ => Err(InvalidData)
}
}
///|
pub fn ConfigValue::as_integer(self : ConfigValue) -> Result[Int, ModbusError] {
match self {
IntegerValue(value) => Ok(value)
_ => Err(InvalidData)
}
}
///|
pub fn ConfigValue::as_boolean(self : ConfigValue) -> Result[Bool, ModbusError] {
match self {
BooleanValue(value) => Ok(value)
_ => Err(InvalidData)
}
}
///|
pub fn ConfigValue::as_mode(self : ConfigValue) -> Result[Mode, ModbusError] {
match self {
ModeValue(value) => Ok(value)
_ => Err(InvalidData)
}
}
///|
pub fn ConfigValue::as_bytes(
self : ConfigValue,
) -> Result[Array[Byte], ModbusError] {
match self {
ByteSequenceValue(value) => Ok(copy_bytes(value))
_ => Err(InvalidData)
}
}
///|
pub fn ConfigValue::display(
self : ConfigValue,
redact? : Bool = false,
) -> String {
if redact {
return ""
}
match self {
TextValue(value) => value
IntegerValue(value) => value.to_string()
BooleanValue(value) => if value { "true" } else { "false" }
ModeValue(value) => mode_name(value)
ByteSequenceValue(value) => bytes_to_hex_string(value)
}
}
///|
/// A schema entry describing a single configurable field.
pub(all) struct ConfigSpec {
key : String
kind : ConfigKind
required : Bool
default_value : ConfigValue?
min_integer : Int?
max_integer : Int?
min_length : Int
max_length : Int
secret : Bool
description : String
}
///|
pub fn ConfigSpec::new(
key : String,
kind : ConfigKind,
required? : Bool = false,
default_value? : ConfigValue,
min_integer? : Int,
max_integer? : Int,
min_length? : Int = 0,
max_length? : Int = 4096,
secret? : Bool = false,
description? : String = "",
) -> Result[ConfigSpec, ModbusError] {
if key == "" || min_length < 0 || max_length < min_length {
return Err(InvalidData)
}
if min_integer is Some(low) && max_integer is Some(high) && low > high {
return Err(InvalidData)
}
if default_value is Some(value) && value.kind() != kind {
return Err(InvalidData)
}
Ok({
key,
kind,
required,
default_value,
min_integer,
max_integer,
min_length,
max_length,
secret,
description,
})
}
///|
pub fn ConfigSpec::key(self : ConfigSpec) -> String {
self.key
}
///|
pub fn ConfigSpec::kind(self : ConfigSpec) -> ConfigKind {
self.kind
}
///|
pub fn ConfigSpec::required(self : ConfigSpec) -> Bool {
self.required
}
///|
pub fn ConfigSpec::default_value(self : ConfigSpec) -> ConfigValue? {
self.default_value
}
///|
pub fn ConfigSpec::secret(self : ConfigSpec) -> Bool {
self.secret
}
///|
pub fn ConfigSpec::description(self : ConfigSpec) -> String {
self.description
}
///|
pub fn ConfigSpec::validate(
self : ConfigSpec,
value : ConfigValue,
) -> Result[Unit, ModbusError] {
if value.kind() != self.kind {
return Err(InvalidData)
}
match value {
IntegerValue(number) => {
if self.min_integer is Some(low) && number < low {
return Err(InvalidData)
}
if self.max_integer is Some(high) && number > high {
return Err(InvalidData)
}
}
TextValue(text) =>
if text.length() < self.min_length || text.length() > self.max_length {
return Err(InvalidLength)
}
ByteSequenceValue(bytes) =>
if bytes.length() < self.min_length || bytes.length() > self.max_length {
return Err(InvalidLength)
}
BooleanValue(_) | ModeValue(_) => ()
}
Ok(())
}
///|
pub fn ConfigSpec::summary(self : ConfigSpec) -> String {
self.key +
" type=" +
config_kind_name(self.kind) +
" required=" +
(if self.required { "true" } else { "false" })
}
///|
/// A collection of configuration specifications.
pub(all) struct ConfigSchema {
specs : Array[ConfigSpec]
max_specs : Int
}
///|
pub fn ConfigSchema::new(
max_specs? : Int = 256,
) -> Result[ConfigSchema, ModbusError] {
if max_specs < 1 {
Err(CapacityExceeded)
} else {
Ok({ specs: [], max_specs })
}
}
///|
pub fn ConfigSchema::add(
self : ConfigSchema,
spec : ConfigSpec,
) -> Result[Unit, ModbusError] {
if self.specs.length() >= self.max_specs {
return Err(CapacityExceeded)
}
for current in self.specs {
if current.key() == spec.key() {
return Err(InvalidData)
}
}
self.specs.push(spec)
Ok(())
}
///|
pub fn ConfigSchema::length(self : ConfigSchema) -> Int {
self.specs.length()
}
///|
pub fn ConfigSchema::find(self : ConfigSchema, key : String) -> ConfigSpec? {
for spec in self.specs {
if spec.key() == key {
return Some(spec)
}
}
None
}
///|
pub fn ConfigSchema::get(
self : ConfigSchema,
index : Int,
) -> Result[ConfigSpec, ModbusError] {
if index < 0 || index >= self.specs.length() {
Err(InvalidAddress)
} else {
Ok(self.specs[index])
}
}
///|
pub fn ConfigSchema::snapshot(self : ConfigSchema) -> Array[ConfigSpec] {
let result : Array[ConfigSpec] = []
for spec in self.specs {
result.push(spec)
}
result
}
///|
pub fn ConfigSchema::validate_values(
self : ConfigSchema,
values : Array[ConfigEntry],
) -> Result[Unit, ModbusError] {
for spec in self.specs {
let mut found = false
for entry in values {
if entry.key() == spec.key() {
found = true
match spec.validate(entry.value()) {
Ok(_) => ()
Err(error) => return Err(error)
}
}
}
if spec.required() && !found && spec.default_value() is None {
return Err(InvalidData)
}
}
for entry in values {
if self.find(entry.key()) is None {
return Err(InvalidData)
}
}
Ok(())
}
///|
/// A key/value pair in a runtime configuration snapshot.
pub(all) struct ConfigEntry {
key : String
value : ConfigValue
source : String
revision : Int
}
///|
pub fn ConfigEntry::new(
key : String,
value : ConfigValue,
source? : String = "runtime",
revision? : Int = 0,
) -> Result[ConfigEntry, ModbusError] {
if key == "" || revision < 0 {
Err(InvalidData)
} else {
Ok({ key, value, source, revision })
}
}
///|
pub fn ConfigEntry::key(self : ConfigEntry) -> String {
self.key
}
///|
pub fn ConfigEntry::value(self : ConfigEntry) -> ConfigValue {
self.value
}
///|
pub fn ConfigEntry::source(self : ConfigEntry) -> String {
self.source
}
///|
pub fn ConfigEntry::revision(self : ConfigEntry) -> Int {
self.revision
}
///|
pub fn ConfigEntry::display(
self : ConfigEntry,
redact? : Bool = false,
) -> String {
self.key + "=" + self.value.display(redact~) + " source=" + self.source
}
///|
/// Mutable configuration store with atomic-style patch application.
pub(all) struct RuntimeConfig {
schema : ConfigSchema
entries : Array[ConfigEntry]
mut revision : Int
}
///|
pub fn RuntimeConfig::new(schema : ConfigSchema) -> RuntimeConfig {
{ schema, entries: [], revision: 0 }
}
///|
pub fn RuntimeConfig::revision(self : RuntimeConfig) -> Int {
self.revision
}
///|
pub fn RuntimeConfig::schema(self : RuntimeConfig) -> ConfigSchema {
self.schema
}
///|
pub fn RuntimeConfig::length(self : RuntimeConfig) -> Int {
self.entries.length()
}
///|
pub fn RuntimeConfig::get(self : RuntimeConfig, key : String) -> ConfigValue? {
for entry in self.entries {
if entry.key() == key {
return Some(entry.value())
}
}
match self.schema.find(key) {
Some(spec) => spec.default_value()
None => None
}
}
///|
pub fn RuntimeConfig::entry(self : RuntimeConfig, key : String) -> ConfigEntry? {
for entry in self.entries {
if entry.key() == key {
return Some(entry)
}
}
None
}
///|
pub fn RuntimeConfig::set(
self : RuntimeConfig,
key : String,
value : ConfigValue,
source? : String = "runtime",
) -> Result[Unit, ModbusError] {
let spec = match self.schema.find(key) {
Some(value) => value
None => return Err(InvalidData)
}
match spec.validate(value) {
Err(error) => return Err(error)
Ok(_) => ()
}
let entry = ConfigEntry::new(key, value, source~, revision=self.revision + 1).unwrap()
for index, current in self.entries {
if current.key() == key {
self.entries[index] = entry
self.revision += 1
return Ok(())
}
}
self.entries.push(entry)
self.revision += 1
Ok(())
}
///|
pub fn RuntimeConfig::remove(
self : RuntimeConfig,
key : String,
) -> Result[Unit, ModbusError] {
for index, entry in self.entries {
if entry.key() == key {
ignore(self.entries.remove(index))
self.revision += 1
return Ok(())
}
}
Err(InvalidData)
}
///|
pub fn RuntimeConfig::validate(
self : RuntimeConfig,
) -> Result[Unit, ModbusError] {
self.schema.validate_values(self.entries)
}
///|
pub fn RuntimeConfig::snapshot(self : RuntimeConfig) -> Array[ConfigEntry] {
let result : Array[ConfigEntry] = []
for entry in self.entries {
result.push(entry)
}
result
}
///|
pub fn RuntimeConfig::export_lines(
self : RuntimeConfig,
redact? : Bool = true,
) -> Array[String] {
let result : Array[String] = []
for entry in self.entries {
result.push(entry.display(redact~))
}
result
}
///|
/// A change applied to a runtime configuration.
pub(all) enum ConfigOperation {
SetValue(String, ConfigValue)
RemoveValue(String)
} derive(Debug)
///|
pub fn RuntimeConfig::apply(
self : RuntimeConfig,
operations : Array[ConfigOperation],
source? : String = "patch",
) -> Result[Int, ModbusError] {
let before = self.revision
for operation in operations {
match operation {
SetValue(key, value) =>
match self.set(key, value, source~) {
Ok(_) => ()
Err(error) => return Err(error)
}
RemoveValue(key) =>
match self.remove(key) {
Ok(_) => ()
Err(error) => return Err(error)
}
}
}
Ok(self.revision - before)
}
///|
pub fn config_values_equal(left : ConfigValue, right : ConfigValue) -> Bool {
match (left, right) {
(TextValue(a), TextValue(b)) => a == b
(IntegerValue(a), IntegerValue(b)) => a == b
(BooleanValue(a), BooleanValue(b)) => a == b
(ModeValue(a), ModeValue(b)) => a == b
(ByteSequenceValue(a), ByteSequenceValue(b)) => a == b
_ => false
}
}
///|
pub fn config_diff(
before : Array[ConfigEntry],
after : Array[ConfigEntry],
) -> Array[ConfigOperation] {
let result : Array[ConfigOperation] = []
for old in before {
let mut found = false
for current in after {
if current.key() == old.key() {
found = true
if !config_values_equal(old.value(), current.value()) {
result.push(SetValue(current.key(), current.value()))
}
}
}
if !found {
result.push(RemoveValue(old.key()))
}
}
for current in after {
let mut found = false
for old in before {
if old.key() == current.key() {
found = true
}
}
if !found {
result.push(SetValue(current.key(), current.value()))
}
}
result
}
///|
pub fn config_schema_summary(schema : ConfigSchema) -> String {
let mut required = 0
for spec in schema.snapshot() {
if spec.required() {
required += 1
}
}
"specs=" + schema.length().to_string() + " required=" + required.to_string()
}