use indexmap::IndexMap;
use std::fmt;
#[derive(Debug, Clone, PartialEq)]
pub struct DxDocument {
pub context: IndexMap<String, DxLlmValue>,
pub refs: IndexMap<String, String>,
pub sections: IndexMap<char, DxSection>,
pub section_names: IndexMap<char, String>,
pub entry_order: Vec<EntryRef>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum EntryRef {
Context(String),
Section(char),
}
impl DxDocument {
#[must_use]
pub fn new() -> Self {
Self {
context: IndexMap::new(),
refs: IndexMap::new(),
sections: IndexMap::new(),
section_names: IndexMap::new(),
entry_order: Vec::new(),
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.context.is_empty() && self.refs.is_empty() && self.sections.is_empty()
}
#[must_use]
pub fn get_path(&self, path: &str) -> Option<&DxLlmValue> {
if let Some(value) = self.context.get(path) {
return Some(value);
}
let (root, nested_path) = path.split_once('.')?;
let mut current = self.context.get(root)?;
for segment in nested_path.split('.') {
match current {
DxLlmValue::Obj(fields) => {
current = fields.get(segment)?;
}
_ => return None,
}
}
Some(current)
}
#[must_use]
pub fn section_by_name(&self, name: &str) -> Option<&DxSection> {
self.section_names
.iter()
.find_map(|(section_id, section_name)| {
(section_name == name)
.then(|| self.sections.get(section_id))
.flatten()
})
}
}
impl Default for DxDocument {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct DxSection {
pub schema: Vec<String>,
pub rows: Vec<Vec<DxLlmValue>>,
}
impl DxSection {
#[must_use]
pub fn new(schema: Vec<String>) -> Self {
Self {
schema,
rows: Vec::new(),
}
}
pub fn add_row(&mut self, row: Vec<DxLlmValue>) -> Result<(), String> {
if row.len() != self.schema.len() {
return Err(format!(
"Row length {} doesn't match schema length {}",
row.len(),
self.schema.len()
));
}
self.rows.push(row);
Ok(())
}
#[must_use]
pub fn row_count(&self) -> usize {
self.rows.len()
}
#[must_use]
pub fn column_index(&self, name: &str) -> Option<usize> {
self.schema.iter().position(|column| column == name)
}
#[must_use]
pub fn column_values(&self, name: &str) -> Option<Vec<&DxLlmValue>> {
let column_index = self.column_index(name)?;
Some(
self.rows
.iter()
.filter_map(|row| row.get(column_index))
.collect(),
)
}
#[must_use]
pub fn value_by_key(
&self,
key_column: &str,
key: &str,
value_column: &str,
) -> Option<&DxLlmValue> {
let key_index = self.column_index(key_column)?;
let value_index = self.column_index(value_column)?;
self.rows.iter().find_map(|row| {
let row_key = row.get(key_index)?.to_string();
(row_key == key).then(|| row.get(value_index)).flatten()
})
}
#[must_use]
pub fn column_count(&self) -> usize {
self.schema.len()
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum DxLlmValue {
Str(String),
Num(f64),
Bool(bool),
Null,
Arr(Vec<DxLlmValue>),
Obj(IndexMap<String, DxLlmValue>),
Ref(String),
}
impl DxLlmValue {
#[must_use]
pub fn is_null(&self) -> bool {
matches!(self, DxLlmValue::Null)
}
#[must_use]
pub fn type_name(&self) -> &'static str {
match self {
DxLlmValue::Str(_) => "string",
DxLlmValue::Num(_) => "number",
DxLlmValue::Bool(_) => "bool",
DxLlmValue::Null => "null",
DxLlmValue::Arr(_) => "array",
DxLlmValue::Obj(_) => "object",
DxLlmValue::Ref(_) => "ref",
}
}
#[must_use]
pub fn as_str(&self) -> Option<&str> {
match self {
DxLlmValue::Str(s) => Some(s),
_ => None,
}
}
#[must_use]
pub fn as_num(&self) -> Option<f64> {
match self {
DxLlmValue::Num(n) => Some(*n),
_ => None,
}
}
#[must_use]
pub fn as_bool(&self) -> Option<bool> {
match self {
DxLlmValue::Bool(b) => Some(*b),
_ => None,
}
}
#[must_use]
pub fn as_arr(&self) -> Option<&Vec<DxLlmValue>> {
match self {
DxLlmValue::Arr(arr) => Some(arr),
_ => None,
}
}
#[must_use]
pub fn as_obj(&self) -> Option<&IndexMap<String, DxLlmValue>> {
match self {
DxLlmValue::Obj(obj) => Some(obj),
_ => None,
}
}
#[must_use]
pub fn as_ref(&self) -> Option<&str> {
match self {
DxLlmValue::Ref(key) => Some(key),
_ => None,
}
}
}
impl fmt::Display for DxLlmValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DxLlmValue::Str(s) => write!(f, "{}", s),
DxLlmValue::Num(n) => {
if n.fract() == 0.0 {
write!(f, "{}", *n as i64)
} else {
write!(f, "{}", n)
}
}
DxLlmValue::Bool(b) => write!(f, "{}", if *b { "true" } else { "false" }),
DxLlmValue::Null => write!(f, "null"),
DxLlmValue::Arr(arr) => {
write!(f, "[")?;
for (i, v) in arr.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{}", v)?;
}
write!(f, "]")
}
DxLlmValue::Obj(obj) => {
write!(f, "[")?;
for (i, (k, v)) in obj.iter().enumerate() {
if i > 0 {
write!(f, ",")?;
}
write!(f, "{}={}", k, v)?;
}
write!(f, "]")
}
DxLlmValue::Ref(key) => write!(f, "^{}", key),
}
}
}
impl From<&str> for DxLlmValue {
fn from(s: &str) -> Self {
DxLlmValue::Str(s.to_string())
}
}
impl From<String> for DxLlmValue {
fn from(s: String) -> Self {
DxLlmValue::Str(s)
}
}
impl From<f64> for DxLlmValue {
fn from(n: f64) -> Self {
DxLlmValue::Num(n)
}
}
impl From<i64> for DxLlmValue {
fn from(n: i64) -> Self {
DxLlmValue::Num(n as f64)
}
}
impl From<bool> for DxLlmValue {
fn from(b: bool) -> Self {
DxLlmValue::Bool(b)
}
}
const fn _assert_send<T: Send>() {}
const fn _assert_sync<T: Sync>() {}
const _: () = _assert_send::<DxDocument>();
const _: () = _assert_sync::<DxDocument>();
const _: () = _assert_send::<DxSection>();
const _: () = _assert_sync::<DxSection>();
const _: () = _assert_send::<DxLlmValue>();
const _: () = _assert_sync::<DxLlmValue>();
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dx_document_new() {
let doc = DxDocument::new();
assert!(doc.is_empty());
assert!(doc.context.is_empty());
assert!(doc.refs.is_empty());
assert!(doc.sections.is_empty());
}
#[test]
fn test_dx_section_add_row() {
let mut section = DxSection::new(vec!["id".to_string(), "name".to_string()]);
let result = section.add_row(vec![
DxLlmValue::Num(1.0),
DxLlmValue::Str("Test".to_string()),
]);
assert!(result.is_ok());
assert_eq!(section.row_count(), 1);
let result = section.add_row(vec![DxLlmValue::Num(2.0)]);
assert!(result.is_err());
}
#[test]
fn test_dx_document_canonical_path_and_named_section_access() {
let mut doc = DxDocument::new();
let mut project = IndexMap::new();
project.insert(
"name".to_string(),
DxLlmValue::Str("dx-devtools".to_string()),
);
project.insert("kind".to_string(), DxLlmValue::Str("www-app".to_string()));
doc.context
.insert("project".to_string(), DxLlmValue::Obj(project));
let mut tools = DxSection::new(vec![
"name".to_string(),
"command".to_string(),
"enabled".to_string(),
"output".to_string(),
]);
tools
.add_row(vec![
DxLlmValue::Str("style".to_string()),
DxLlmValue::Str("dx style build".to_string()),
DxLlmValue::Bool(true),
DxLlmValue::Str("styles/app.generated.css".to_string()),
])
.unwrap();
doc.sections.insert('t', tools);
doc.section_names.insert('t', "tools".to_string());
assert_eq!(
doc.get_path("project.name").unwrap().as_str(),
Some("dx-devtools")
);
let tools = doc.section_by_name("tools").unwrap();
assert_eq!(tools.column_index("command"), Some(1));
assert_eq!(
tools
.value_by_key("name", "style", "output")
.unwrap()
.as_str(),
Some("styles/app.generated.css")
);
}
#[test]
fn test_dx_llm_value_type_name() {
assert_eq!(DxLlmValue::Str("test".to_string()).type_name(), "string");
assert_eq!(DxLlmValue::Num(42.0).type_name(), "number");
assert_eq!(DxLlmValue::Bool(true).type_name(), "bool");
assert_eq!(DxLlmValue::Null.type_name(), "null");
assert_eq!(DxLlmValue::Arr(vec![]).type_name(), "array");
assert_eq!(DxLlmValue::Obj(IndexMap::new()).type_name(), "object");
assert_eq!(DxLlmValue::Ref("key".to_string()).type_name(), "ref");
}
#[test]
#[allow(clippy::approx_constant)] fn test_dx_llm_value_display() {
assert_eq!(format!("{}", DxLlmValue::Str("hello".to_string())), "hello");
assert_eq!(format!("{}", DxLlmValue::Num(42.0)), "42");
assert_eq!(format!("{}", DxLlmValue::Num(3.14)), "3.14");
assert_eq!(format!("{}", DxLlmValue::Bool(true)), "true");
assert_eq!(format!("{}", DxLlmValue::Bool(false)), "false");
assert_eq!(format!("{}", DxLlmValue::Null), "null");
assert_eq!(format!("{}", DxLlmValue::Ref("A".to_string())), "^A");
}
#[test]
fn test_dx_llm_value_obj() {
let mut fields = IndexMap::new();
fields.insert("host".to_string(), DxLlmValue::Str("localhost".to_string()));
fields.insert("port".to_string(), DxLlmValue::Num(8080.0));
let obj = DxLlmValue::Obj(fields);
assert_eq!(obj.type_name(), "object");
assert!(obj.as_obj().is_some());
let obj_map = obj.as_obj().unwrap();
assert_eq!(obj_map.len(), 2);
assert_eq!(obj_map.get("host").unwrap().as_str(), Some("localhost"));
assert_eq!(obj_map.get("port").unwrap().as_num(), Some(8080.0));
}
}