use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq)]
pub enum FieldLookup {
Missing,
Present(RuntimeValue),
}
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(untagged)]
pub enum RuntimeValue {
Null,
Boolean(bool),
String(String),
Integer(i64),
Decimal(f64),
Binary(String),
Date(String),
Time(String),
DateTime(String),
Duration(String),
List(Vec<RuntimeValue>),
Missing(MissingValue),
Invalid(InvalidValue),
Map(BTreeMap<String, RuntimeValue>),
}
impl<'de> Deserialize<'de> for RuntimeValue {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
super::value_serde::deserialize_runtime_value(deserializer)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct MissingValue {
#[serde(rename = "$dtcs")]
pub marker: MissingMarker,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub enum MissingMarker {
#[default]
Missing,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InvalidValue {
#[serde(rename = "$dtcs")]
pub marker: InvalidMarker,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub enum InvalidMarker {
#[default]
Invalid,
}
impl RuntimeValue {
#[must_use]
pub fn missing() -> Self {
Self::Missing(MissingValue::default())
}
#[must_use]
pub fn invalid(reason: impl Into<String>) -> Self {
Self::Invalid(InvalidValue {
marker: InvalidMarker::Invalid,
reason: Some(reason.into()),
})
}
#[must_use]
pub fn is_null(&self) -> bool {
matches!(self, Self::Null)
}
#[must_use]
pub fn is_missing(&self) -> bool {
matches!(self, Self::Missing(_))
}
#[must_use]
pub fn is_invalid(&self) -> bool {
matches!(self, Self::Invalid(_))
}
#[must_use]
pub fn as_str(&self) -> Option<&str> {
match self {
Self::String(s)
| Self::Date(s)
| Self::Time(s)
| Self::DateTime(s)
| Self::Duration(s)
| Self::Binary(s) => Some(s.as_str()),
_ => None,
}
}
#[must_use]
pub fn as_integer(&self) -> Option<i64> {
match self {
Self::Integer(v) => Some(*v),
Self::Decimal(v) => Some(*v as i64),
Self::String(s) => s.parse().ok(),
_ => None,
}
}
#[must_use]
pub fn as_decimal(&self) -> Option<f64> {
match self {
Self::Decimal(v) => Some(*v),
Self::Integer(v) => Some(*v as f64),
Self::String(s) => s.parse().ok(),
_ => None,
}
}
#[must_use]
pub fn as_bool(&self) -> Option<bool> {
match self {
Self::Boolean(v) => Some(*v),
_ => None,
}
}
}
#[must_use]
pub fn lookup_field(row: &Row, field_name: &str) -> FieldLookup {
match row.get(field_name) {
None => FieldLookup::Missing,
Some(value) => FieldLookup::Present(value.clone()),
}
}
pub type Row = BTreeMap<String, RuntimeValue>;
pub type Dataset = Vec<Row>;
pub type RuntimeInputs = BTreeMap<String, Dataset>;
pub type RuntimeOutputs = BTreeMap<String, Dataset>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QualifiedField {
pub interface_id: String,
pub field_name: String,
}
#[must_use]
pub fn parse_qualified_field(target: &str) -> Option<QualifiedField> {
parse_qualified_field_with_interfaces(target, &[])
}
#[must_use]
pub fn parse_qualified_field_with_interfaces(
target: &str,
interface_ids: &[String],
) -> Option<QualifiedField> {
let target = target.trim();
if target.is_empty() || !target.contains('.') {
return None;
}
let mut best: Option<(&str, usize)> = None;
for id in interface_ids {
let prefix = format!("{id}.");
if target.starts_with(&prefix) {
let len = id.len();
if best.map_or(true, |(_, bl)| len > bl) {
best = Some((id.as_str(), len));
}
}
}
if let Some((id, len)) = best {
let field_name = &target[len + 1..];
if field_name.is_empty() {
return None;
}
return Some(QualifiedField {
interface_id: id.to_string(),
field_name: field_name.to_string(),
});
}
let (interface_id, field_name) = target.split_once('.')?;
if interface_id.is_empty() || field_name.is_empty() {
return None;
}
Some(QualifiedField {
interface_id: interface_id.into(),
field_name: field_name.into(),
})
}