use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct UclDocument {
pub structure: HashMap<String, Vec<String>>,
pub blocks: Vec<BlockDef>,
pub commands: Vec<Command>,
}
impl UclDocument {
pub fn new() -> Self {
Self {
structure: HashMap::new(),
blocks: Vec::new(),
commands: Vec::new(),
}
}
}
impl Default for UclDocument {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BlockDef {
pub content_type: ContentType,
pub id: String,
pub properties: HashMap<String, Value>,
pub content: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ContentType {
Text,
Table,
Code,
Math,
Media,
Json,
Binary,
Composite,
}
impl ContentType {
pub fn parse_content_type(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"text" => Some(Self::Text),
"table" => Some(Self::Table),
"code" => Some(Self::Code),
"math" => Some(Self::Math),
"media" => Some(Self::Media),
"json" => Some(Self::Json),
"binary" => Some(Self::Binary),
"composite" => Some(Self::Composite),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Command {
Edit(EditCommand),
Move(MoveCommand),
Append(AppendCommand),
Delete(DeleteCommand),
Prune(PruneCommand),
Fold(FoldCommand),
Link(LinkCommand),
Unlink(UnlinkCommand),
Snapshot(SnapshotCommand),
Transaction(TransactionCommand),
Atomic(Vec<Command>),
WriteSection(WriteSectionCommand),
Goto(GotoCommand),
Back(BackCommand),
Expand(ExpandCommand),
Follow(FollowCommand),
Path(PathFindCommand),
Search(SearchCommand),
Find(FindCommand),
View(ViewCommand),
Context(ContextCommand),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EditCommand {
pub block_id: String,
pub path: Path,
pub operator: Operator,
pub value: Value,
pub condition: Option<Condition>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MoveCommand {
pub block_id: String,
pub target: MoveTarget,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum MoveTarget {
ToParent {
parent_id: String,
index: Option<usize>,
},
Before {
sibling_id: String,
},
After {
sibling_id: String,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AppendCommand {
pub parent_id: String,
pub content_type: ContentType,
pub properties: HashMap<String, Value>,
pub content: String,
pub index: Option<usize>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DeleteCommand {
pub block_id: Option<String>,
pub cascade: bool,
pub preserve_children: bool,
pub condition: Option<Condition>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PruneCommand {
pub target: PruneTarget,
pub dry_run: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum PruneTarget {
Unreachable,
Where(Condition),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FoldCommand {
pub block_id: String,
pub depth: Option<usize>,
pub max_tokens: Option<usize>,
pub preserve_tags: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LinkCommand {
pub source_id: String,
pub edge_type: String,
pub target_id: String,
pub metadata: HashMap<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct UnlinkCommand {
pub source_id: String,
pub edge_type: String,
pub target_id: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum SnapshotCommand {
Create {
name: String,
description: Option<String>,
},
Restore {
name: String,
},
List,
Delete {
name: String,
},
Diff {
name1: String,
name2: String,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum TransactionCommand {
Begin { name: Option<String> },
Commit { name: Option<String> },
Rollback { name: Option<String> },
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct WriteSectionCommand {
pub section_id: String,
pub markdown: String,
pub base_heading_level: Option<usize>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GotoCommand {
pub block_id: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BackCommand {
pub steps: usize,
}
impl Default for BackCommand {
fn default() -> Self {
Self { steps: 1 }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[derive(Default)]
pub enum ExpandDirection {
#[default]
Down,
Up,
Both,
Semantic,
}
impl ExpandDirection {
pub fn parse(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"down" => Some(Self::Down),
"up" => Some(Self::Up),
"both" => Some(Self::Both),
"semantic" => Some(Self::Semantic),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[derive(Default)]
pub enum ViewMode {
#[default]
Full,
Preview { length: usize },
Metadata,
IdsOnly,
}
impl ViewMode {
pub fn parse(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"full" => Some(Self::Full),
"preview" => Some(Self::Preview { length: 100 }),
"metadata" => Some(Self::Metadata),
"ids" | "idsonly" | "ids_only" => Some(Self::IdsOnly),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct TraversalFilterCriteria {
pub include_roles: Vec<String>,
pub exclude_roles: Vec<String>,
pub include_tags: Vec<String>,
pub exclude_tags: Vec<String>,
pub content_pattern: Option<String>,
pub edge_types: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ExpandCommand {
pub block_id: String,
pub direction: ExpandDirection,
pub depth: usize,
pub mode: Option<ViewMode>,
pub filter: Option<TraversalFilterCriteria>,
}
impl Default for ExpandCommand {
fn default() -> Self {
Self {
block_id: String::new(),
direction: ExpandDirection::Down,
depth: 1,
mode: None,
filter: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FollowCommand {
pub source_id: String,
pub edge_types: Vec<String>,
pub target_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PathFindCommand {
pub from_id: String,
pub to_id: String,
pub max_length: Option<usize>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SearchCommand {
pub query: String,
pub limit: Option<usize>,
pub min_similarity: Option<f32>,
pub filter: Option<TraversalFilterCriteria>,
}
impl Default for SearchCommand {
fn default() -> Self {
Self {
query: String::new(),
limit: Some(10),
min_similarity: None,
filter: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct FindCommand {
pub role: Option<String>,
pub tag: Option<String>,
pub label: Option<String>,
pub pattern: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ViewTarget {
Block(String),
Neighborhood,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ViewCommand {
pub target: ViewTarget,
pub mode: ViewMode,
pub depth: Option<usize>,
}
impl Default for ViewCommand {
fn default() -> Self {
Self {
target: ViewTarget::Neighborhood,
mode: ViewMode::Full,
depth: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ContextCommand {
Add(ContextAddCommand),
Remove { block_id: String },
Clear,
Expand(ContextExpandCommand),
Compress { method: CompressionMethod },
Prune(ContextPruneCommand),
Render { format: Option<RenderFormat> },
Stats,
Focus { block_id: Option<String> },
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ContextAddTarget {
Block(String),
Results,
Children { parent_id: String },
Path { from_id: String, to_id: String },
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ContextAddCommand {
pub target: ContextAddTarget,
pub reason: Option<String>,
pub relevance: Option<f32>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ContextExpandCommand {
pub direction: ExpandDirection,
pub depth: Option<usize>,
pub token_budget: Option<usize>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ContextPruneCommand {
pub min_relevance: Option<f32>,
pub max_age_secs: Option<u64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum CompressionMethod {
Truncate,
Summarize,
StructureOnly,
}
impl CompressionMethod {
pub fn parse(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"truncate" => Some(Self::Truncate),
"summarize" => Some(Self::Summarize),
"structure_only" | "structureonly" | "structure" => Some(Self::StructureOnly),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[derive(Default)]
pub enum RenderFormat {
#[default]
Default,
ShortIds,
Markdown,
}
impl RenderFormat {
pub fn parse(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"default" => Some(Self::Default),
"short_ids" | "shortids" | "short" => Some(Self::ShortIds),
"markdown" | "md" => Some(Self::Markdown),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Path {
pub segments: Vec<PathSegment>,
}
impl Path {
pub fn new(segments: Vec<PathSegment>) -> Self {
Self { segments }
}
pub fn simple(name: &str) -> Self {
Self {
segments: vec![PathSegment::Property(name.to_string())],
}
}
}
impl std::fmt::Display for Path {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
self.segments
.iter()
.map(|s| match s {
PathSegment::Property(p) => p.clone(),
PathSegment::Index(i) => format!("[{}]", i),
PathSegment::Slice { start, end } => match (start, end) {
(Some(s), Some(e)) => format!("[{}:{}]", s, e),
(Some(s), None) => format!("[{}:]", s),
(None, Some(e)) => format!("[:{}]", e),
(None, None) => "[:]".to_string(),
},
PathSegment::JsonPath(p) => format!("${}", p),
})
.collect::<Vec<_>>()
.join(".")
)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum PathSegment {
Property(String),
Index(i64),
Slice {
start: Option<i64>,
end: Option<i64>,
},
JsonPath(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Operator {
Set, Append, Remove, Increment, Decrement, }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Value {
Null,
Bool(bool),
Number(f64),
String(String),
Array(Vec<Value>),
Object(HashMap<String, Value>),
BlockRef(String),
}
impl Value {
pub fn to_json(&self) -> serde_json::Value {
match self {
Value::Null => serde_json::Value::Null,
Value::Bool(b) => serde_json::Value::Bool(*b),
Value::Number(n) => serde_json::json!(*n),
Value::String(s) => serde_json::Value::String(s.clone()),
Value::Array(arr) => {
serde_json::Value::Array(arr.iter().map(|v| v.to_json()).collect())
}
Value::Object(obj) => {
let map: serde_json::Map<String, serde_json::Value> =
obj.iter().map(|(k, v)| (k.clone(), v.to_json())).collect();
serde_json::Value::Object(map)
}
Value::BlockRef(id) => serde_json::json!({"$ref": id}),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Condition {
Comparison {
path: Path,
op: ComparisonOp,
value: Value,
},
Contains {
path: Path,
value: Value,
},
StartsWith {
path: Path,
prefix: String,
},
EndsWith {
path: Path,
suffix: String,
},
Matches {
path: Path,
regex: String,
},
Exists {
path: Path,
},
IsNull {
path: Path,
},
And(Box<Condition>, Box<Condition>),
Or(Box<Condition>, Box<Condition>),
Not(Box<Condition>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ComparisonOp {
Eq, Ne, Gt, Ge, Lt, Le, }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_path_simple() {
let path = Path::simple("content.text");
assert_eq!(path.segments.len(), 1);
}
#[test]
fn test_value_to_json() {
let value = Value::Object(
[("key".to_string(), Value::String("value".to_string()))]
.into_iter()
.collect(),
);
let json = value.to_json();
assert_eq!(json["key"], "value");
}
}