use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Dtmi(pub String);
impl Dtmi {
pub fn validate(&self) -> Result<(), DtdlValidationError> {
let s = &self.0;
if !s.starts_with("dtmi:") {
return Err(DtdlValidationError::InvalidDtmi {
dtmi: s.clone(),
reason: "must start with 'dtmi:'",
});
}
let semicolon_pos = s
.rfind(';')
.ok_or_else(|| DtdlValidationError::InvalidDtmi {
dtmi: s.clone(),
reason: "missing version number after ';'",
})?;
let version_str = &s[semicolon_pos + 1..];
version_str
.parse::<u32>()
.map_err(|_| DtdlValidationError::InvalidDtmi {
dtmi: s.clone(),
reason: "version must be a non-negative integer",
})?;
let path = &s[5..semicolon_pos];
if path.is_empty() {
return Err(DtdlValidationError::InvalidDtmi {
dtmi: s.clone(),
reason: "path must not be empty",
});
}
for segment in path.split(':') {
if segment.is_empty()
|| !segment
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_')
{
return Err(DtdlValidationError::InvalidDtmi {
dtmi: s.clone(),
reason: "path segments must be non-empty alphanumeric/underscore",
});
}
}
Ok(())
}
pub fn version(&self) -> Option<u32> {
self.0
.rfind(';')
.and_then(|pos| self.0[pos + 1..].parse().ok())
}
pub fn path(&self) -> Option<&str> {
let s = &self.0;
let start = s.strip_prefix("dtmi:")?;
let semi = start.rfind(';')?;
Some(&start[..semi])
}
}
impl std::fmt::Display for Dtmi {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
pub fn primary_type(value: &serde_json::Value) -> Option<&str> {
match value {
serde_json::Value::String(s) => Some(s.as_str()),
serde_json::Value::Array(arr) => arr.first().and_then(|v| v.as_str()),
_ => None,
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DtdlSchema(pub String);
impl DtdlSchema {
pub fn to_xsd_uri(&self) -> &'static str {
match self.0.to_lowercase().as_str() {
"boolean" => "http://www.w3.org/2001/XMLSchema#boolean",
"date" => "http://www.w3.org/2001/XMLSchema#date",
"datetime" => "http://www.w3.org/2001/XMLSchema#dateTime",
"double" => "http://www.w3.org/2001/XMLSchema#double",
"duration" => "http://www.w3.org/2001/XMLSchema#duration",
"float" => "http://www.w3.org/2001/XMLSchema#float",
"integer" => "http://www.w3.org/2001/XMLSchema#integer",
"long" => "http://www.w3.org/2001/XMLSchema#long",
"string" => "http://www.w3.org/2001/XMLSchema#string",
"time" => "http://www.w3.org/2001/XMLSchema#time",
_ => "http://www.w3.org/2001/XMLSchema#anyType",
}
}
}
#[derive(Debug, Clone)]
pub struct DtdlTelemetryElement {
pub element_type: serde_json::Value,
pub id: Option<Dtmi>,
pub name: String,
pub schema: DtdlSchema,
pub unit: Option<String>,
pub description: Option<String>,
pub comment: Option<String>,
}
#[derive(Debug, Clone)]
pub struct DtdlPropertyElement {
pub element_type: serde_json::Value,
pub id: Option<Dtmi>,
pub name: String,
pub schema: DtdlSchema,
pub writable: Option<bool>,
pub unit: Option<String>,
pub description: Option<String>,
}
#[derive(Debug, Clone)]
pub struct DtdlCommandElement {
pub element_type: serde_json::Value,
pub name: String,
pub description: Option<String>,
}
#[derive(Debug, Clone)]
pub struct DtdlComponentElement {
pub element_type: serde_json::Value,
pub name: String,
pub schema: Dtmi,
}
#[derive(Debug, Clone)]
pub struct DtdlRelationshipElement {
pub element_type: serde_json::Value,
pub name: String,
pub target: Option<Dtmi>,
pub description: Option<String>,
}
#[derive(Debug, Clone)]
pub enum DtdlContent {
Telemetry(DtdlTelemetryElement),
Property(DtdlPropertyElement),
Command(DtdlCommandElement),
Component(DtdlComponentElement),
Relationship(DtdlRelationshipElement),
}
#[derive(Debug, Clone)]
pub struct DtdlInterface {
pub context: serde_json::Value,
pub element_type: serde_json::Value,
pub id: Dtmi,
pub display_name: Option<serde_json::Value>,
pub description: Option<String>,
pub comment: Option<String>,
pub contents: Option<Vec<DtdlContent>>,
pub schemas: Option<Vec<serde_json::Value>>,
pub extends: Option<serde_json::Value>,
}
#[derive(Debug, Error)]
pub enum DtdlValidationError {
#[error("invalid DTMI '{dtmi}': {reason}")]
InvalidDtmi { dtmi: String, reason: &'static str },
#[error("missing required field: {field}")]
MissingField { field: &'static str },
#[error("unsupported DTDL version '{version}'; only v3 is fully supported")]
UnsupportedVersion { version: String },
#[error("schema mismatch: {0}")]
SchemaMismatch(String),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dtmi_valid_simple() {
let d = Dtmi("dtmi:example:Thermostat;1".into());
assert!(d.validate().is_ok());
}
#[test]
fn dtmi_valid_multi_segment() {
let d = Dtmi("dtmi:com:example:devices:Sensor;42".into());
assert!(d.validate().is_ok());
}
#[test]
fn dtmi_version_extracted() {
let d = Dtmi("dtmi:example:Foo;7".into());
assert_eq!(d.version(), Some(7));
}
#[test]
fn dtmi_path_extracted() {
let d = Dtmi("dtmi:example:Foo;1".into());
assert_eq!(d.path(), Some("example:Foo"));
}
#[test]
fn dtmi_missing_prefix_fails() {
let d = Dtmi("http://example.org/Foo;1".into());
assert!(d.validate().is_err());
}
#[test]
fn dtmi_missing_version_fails() {
let d = Dtmi("dtmi:example:Foo".into());
assert!(d.validate().is_err());
}
#[test]
fn dtmi_non_integer_version_fails() {
let d = Dtmi("dtmi:example:Foo;abc".into());
assert!(d.validate().is_err());
}
#[test]
fn dtmi_empty_segment_fails() {
let d = Dtmi("dtmi::Foo;1".into());
assert!(d.validate().is_err());
}
#[test]
fn dtmi_special_chars_in_segment_fail() {
let d = Dtmi("dtmi:example:Foo-Bar;1".into());
assert!(d.validate().is_err());
}
#[test]
fn dtmi_empty_path_fails() {
let d = Dtmi("dtmi:;1".into());
assert!(d.validate().is_err());
}
#[test]
fn dtdl_schema_to_xsd_double() {
let s = DtdlSchema("double".into());
assert_eq!(s.to_xsd_uri(), "http://www.w3.org/2001/XMLSchema#double");
}
#[test]
fn dtdl_schema_to_xsd_string() {
let s = DtdlSchema("string".into());
assert_eq!(s.to_xsd_uri(), "http://www.w3.org/2001/XMLSchema#string");
}
#[test]
fn dtdl_schema_unknown_is_anytype() {
let s = DtdlSchema("complexEnumRef".into());
assert_eq!(s.to_xsd_uri(), "http://www.w3.org/2001/XMLSchema#anyType");
}
#[test]
fn primary_type_string() {
let v = serde_json::json!("Telemetry");
assert_eq!(primary_type(&v), Some("Telemetry"));
}
#[test]
fn primary_type_array() {
let v = serde_json::json!(["Telemetry", "Temperature"]);
assert_eq!(primary_type(&v), Some("Telemetry"));
}
#[test]
fn primary_type_null_returns_none() {
let v = serde_json::Value::Null;
assert_eq!(primary_type(&v), None);
}
}