kevy_index/
view_sidecar.rs1use crate::value::IndexValue;
6use std::fmt::Write as _;
7use crate::view::{Leaf, Tree, ViewMode, ViewSpec};
8
9#[derive(Debug, Clone, Default)]
12pub struct ViewCatalog {
13 specs: Vec<ViewSpec>,
14}
15
16pub const MAX_VIEWS: usize = 64;
18
19impl ViewCatalog {
20 pub fn new() -> Self {
22 Self::default()
23 }
24
25 pub fn create(&mut self, spec: ViewSpec) -> Result<(), &'static str> {
27 spec.validate()?;
28 if self.specs.len() >= MAX_VIEWS {
29 return Err("ERR view limit reached (64)");
30 }
31 if self.specs.iter().any(|s| s.name == spec.name) {
32 return Err("ERR view already exists");
33 }
34 self.specs.push(spec);
35 Ok(())
36 }
37
38 pub fn drop_view(&mut self, name: &[u8]) -> bool {
40 let n = self.specs.len();
41 self.specs.retain(|s| s.name != name);
42 self.specs.len() != n
43 }
44
45 pub fn get(&self, name: &[u8]) -> Option<&ViewSpec> {
47 self.specs.iter().find(|s| s.name == name)
48 }
49
50 pub fn iter(&self) -> impl Iterator<Item = &ViewSpec> {
52 self.specs.iter()
53 }
54
55 pub fn len(&self) -> usize {
57 self.specs.len()
58 }
59
60 pub fn is_empty(&self) -> bool {
62 self.specs.is_empty()
63 }
64
65 pub fn to_sidecar(&self) -> String {
67 let mut out = String::from("kevy-view-catalog v1\n");
68 for s in &self.specs {
69 out.push_str(&s.to_line());
70 out.push('\n');
71 }
72 out
73 }
74
75 pub fn from_sidecar(text: &str) -> Option<ViewCatalog> {
77 let mut lines = text.lines();
78 if lines.next()? != "kevy-view-catalog v1" {
79 return None;
80 }
81 let mut c = ViewCatalog::new();
82 for line in lines {
83 if line.is_empty() {
84 continue;
85 }
86 c.create(ViewSpec::from_line(line)?).ok()?;
87 }
88 Some(c)
89 }
90}
91
92fn esc(b: &[u8]) -> String {
93 let mut out = String::with_capacity(b.len());
94 for &c in b {
95 if c == b' ' || c == b'\t' || c == b'\n' || c == b'%' || c == b'(' || c == b')' || !(33..127).contains(&c) {
96 let _ = write!(out, "%{c:02X}");
97 } else {
98 out.push(c as char);
99 }
100 }
101 if out.is_empty() { "%".into() } else { out }
102}
103
104fn unesc(s: &str) -> Option<Vec<u8>> {
105 if s == "%" {
106 return Some(Vec::new());
107 }
108 let mut out = Vec::with_capacity(s.len());
109 let b = s.as_bytes();
110 let mut i = 0;
111 while i < b.len() {
112 if b[i] == b'%' {
113 out.push(u8::from_str_radix(s.get(i + 1..i + 3)?, 16).ok()?);
114 i += 3;
115 } else {
116 out.push(b[i]);
117 i += 1;
118 }
119 }
120 Some(out)
121}
122
123fn val_ser(v: &IndexValue) -> String {
124 match v {
125 IndexValue::I64(i) => format!("i{i}"),
126 IndexValue::F64(f) => format!("f{}", f.to_bits()),
127 IndexValue::Str(s) => format!("s{}", esc(s)),
128 }
129}
130
131fn val_de(s: &str) -> Option<IndexValue> {
132 let (tag, rest) = s.split_at(1);
133 match tag {
134 "i" => rest.parse().ok().map(IndexValue::I64),
135 "f" => rest.parse::<u64>().ok().map(|b| IndexValue::F64(f64::from_bits(b))),
136 "s" => unesc(rest).map(IndexValue::Str),
137 _ => None,
138 }
139}
140
141fn tree_ser(t: &Tree, out: &mut String) {
142 match t {
143 Tree::Leaf(l) => {
144 let _ = write!(out, "(L {} {} {})", esc(&l.index), val_ser(&l.min), val_ser(&l.max));
145 }
146 Tree::And(a, b) | Tree::Or(a, b) | Tree::Diff(a, b) => {
147 let op = match t {
148 Tree::And(..) => "A",
149 Tree::Or(..) => "O",
150 _ => "D",
151 };
152 let _ = write!(out, "({op} ");
153 tree_ser(a, out);
154 out.push(' ');
155 tree_ser(b, out);
156 out.push(')');
157 }
158 }
159}
160
161fn tree_de(toks: &[&str], pos: &mut usize) -> Option<Tree> {
162 let t = toks.get(*pos)?;
163 *pos += 1;
164 match *t {
165 "(L" => {
166 let idx = unesc(toks.get(*pos)?)?;
167 let min = val_de(toks.get(*pos + 1)?)?;
168 let max = val_de(toks.get(*pos + 2)?.trim_end_matches(')'))?;
169 *pos += 3;
170 Some(Tree::Leaf(Leaf { index: idx, min, max }))
171 }
172 "(A" | "(O" | "(D" => {
173 let a = tree_de(toks, pos)?;
174 let b = tree_de(toks, pos)?;
175 let tree = match *t {
176 "(A" => Tree::And(Box::new(a), Box::new(b)),
177 "(O" => Tree::Or(Box::new(a), Box::new(b)),
178 _ => Tree::Diff(Box::new(a), Box::new(b)),
179 };
180 Some(tree)
181 }
182 _ => None,
183 }
184}
185
186impl ViewSpec {
187 pub fn to_line(&self) -> String {
189 let (mode, k) = match self.mode {
190 ViewMode::Virtual => ("v", 0),
191 ViewMode::Materialized { top_k } => ("m", top_k),
192 };
193 let via = self.via.as_deref().map_or_else(|| "-".into(), esc);
194 let mut tree = String::new();
195 tree_ser(&self.tree, &mut tree);
196 format!(
197 "{} {} {} {} {} {} {}",
198 esc(&self.name),
199 esc(&self.order_by),
200 u8::from(self.desc),
201 mode,
202 k,
203 via,
204 tree
205 )
206 }
207
208 pub fn from_line(line: &str) -> Option<ViewSpec> {
210 let toks: Vec<&str> = line.split(' ').collect();
211 if toks.len() < 7 {
212 return None;
213 }
214 let mode = match toks[3] {
215 "v" => ViewMode::Virtual,
216 "m" => ViewMode::Materialized { top_k: toks[4].parse().ok()? },
217 _ => return None,
218 };
219 let via = if toks[5] == "-" { None } else { Some(unesc(toks[5])?) };
220 let mut pos = 6;
221 let tree = tree_de_root(&toks, &mut pos)?;
224 Some(ViewSpec {
225 name: unesc(toks[0])?,
226 order_by: unesc(toks[1])?,
227 desc: toks[2] == "1",
228 mode,
229 via,
230 tree,
231 })
232 }
233}
234
235fn tree_de_root(toks: &[&str], pos: &mut usize) -> Option<Tree> {
236 tree_de(toks, pos)
241}