diskann_record/load/context.rs
1/*
2 * Copyright (c) Microsoft Corporation.
3 * Licensed under the MIT license.
4 */
5
6//! Load-side context, object, array, and side-car reader.
7//!
8//! [`Context`] is the cheap, clonable handle threaded through every
9//! [`super::Load::load`] / [`super::Loadable::load`] impl. From a [`Context`], loaders
10//! ask:
11//!
12//! * [`Context::as_object`] / [`Object::field`] for nested records.
13//! * [`Context::as_array`] / [`Array::iter`] for sequences.
14//! * [`Context::as_str`] / [`Context::as_number`] / [`Context::as_bool`] / [`Context::is_null`] for scalars.
15//! * [`Object::read`] for side-car artifacts referenced by a
16//! [`save::Handle`](super::save::Handle).
17//!
18//! [`Reader`] implements [`std::io::Read`] and [`std::io::Seek`] over a side-car
19//! artifact, regardless of the provider's backing store.
20
21use std::io::BufReader;
22
23use crate::{
24 Number, Version,
25 load::{Loadable, Result, error},
26 save,
27};
28
29/// The backing store for a load operation.
30///
31/// A `LoadContext` supplies the root manifest [`save::Value`] ([`LoadContext::value`])
32/// and resolves side-car artifacts referenced by handles ([`LoadContext::read`]). The
33/// concrete implementations live under `crate::backend`: a disk-backed context (under
34/// the `disk` feature) and an in-memory context. Alternative implementations (e.g. a
35/// virtual filesystem) can be supplied for testing.
36///
37/// The generic [`load`](super::load) entry point is parameterized over this trait, and
38/// [`Context`] / [`Object`] / `Array` borrow it as an object-safe `&dyn LoadContext`
39/// so the load tree is agnostic to the concrete context type.
40pub(crate) trait LoadContext {
41 /// The root value of the manifest.
42 ///
43 /// # Errors
44 ///
45 /// Returns [`Error`](crate::load::Error) if the root value cannot be produced.
46 fn value(&self) -> Result<&save::Value<'_>>;
47
48 /// Open the side-car artifact identified by `key` for reading.
49 ///
50 /// # Errors
51 ///
52 /// Returns [`error::Kind::MissingFile`] if the file is not registered with this
53 /// context or if `key` escapes the manifest directory.
54 fn read(&self, key: &str) -> Result<Reader<'_>>;
55}
56
57/// The backend-specific half of a [`Reader`].
58///
59/// Each [`LoadContext`] implementation supplies its own `ReaderInner` (e.g. an in-memory
60/// cursor or an on-disk file). The blanket impl covers any type that is both
61/// [`std::io::Read`] and [`std::io::Seek`].
62pub(crate) trait ReaderInner: std::io::Read + std::io::Seek {}
63
64impl<T> ReaderInner for T where T: std::io::Read + std::io::Seek {}
65
66/// A borrowed reader over a side-car artifact.
67///
68/// Produced by [`Object::read`]. Implements [`std::io::Read`] and [`std::io::Seek`] over
69/// whatever backing store the `LoadContext` provides, so non-file-backed providers
70/// (like an in-memory byte buffer) can supply an arbitrary seekable reader.
71pub struct Reader<'a> {
72 io: BufReader<Box<dyn ReaderInner + 'a>>,
73}
74
75impl<'a> Reader<'a> {
76 /// Build a reader over an arbitrary borrowed [`ReaderInner`] source.
77 ///
78 /// Used by [`LoadContext`] implementations to expose a side-car artifact backed by a
79 /// file or an in-memory [`std::io::Cursor`].
80 pub(crate) fn new<T>(io: T) -> Self
81 where
82 T: ReaderInner + 'a,
83 {
84 Self {
85 io: BufReader::new(Box::new(io)),
86 }
87 }
88}
89
90impl std::io::Read for Reader<'_> {
91 // Required method
92 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
93 self.io.read(buf)
94 }
95
96 // Provided methods
97 fn read_vectored(&mut self, bufs: &mut [std::io::IoSliceMut<'_>]) -> std::io::Result<usize> {
98 self.io.read_vectored(bufs)
99 }
100 fn read_to_end(&mut self, buf: &mut Vec<u8>) -> std::io::Result<usize> {
101 self.io.read_to_end(buf)
102 }
103 fn read_to_string(&mut self, buf: &mut String) -> std::io::Result<usize> {
104 self.io.read_to_string(buf)
105 }
106 fn read_exact(&mut self, buf: &mut [u8]) -> std::io::Result<()> {
107 self.io.read_exact(buf)
108 }
109}
110
111impl std::io::Seek for Reader<'_> {
112 fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
113 self.io.seek(pos)
114 }
115 fn rewind(&mut self) -> std::io::Result<()> {
116 self.io.rewind()
117 }
118 fn stream_position(&mut self) -> std::io::Result<u64> {
119 self.io.stream_position()
120 }
121 fn seek_relative(&mut self, offset: i64) -> std::io::Result<()> {
122 self.io.seek_relative(offset)
123 }
124}
125
126///////////////////////
127// User facing types //
128///////////////////////
129
130/// A cheap, clonable handle threaded through every load impl.
131///
132/// Loaders use the `as_*` accessors to peek at the underlying [`save::Value`] kind
133/// (e.g. [`Context::as_object`], [`Context::as_array`], [`Context::as_str`]) and
134/// [`Context::load`] to recursively deserialize a nested value into a concrete type.
135#[derive(Clone)]
136pub struct Context<'a> {
137 inner: &'a dyn LoadContext,
138 value: &'a save::Value<'a>,
139}
140
141impl<'a> Context<'a> {
142 pub(super) fn new(inner: &'a dyn LoadContext, value: &'a save::Value<'a>) -> Self {
143 Self { inner, value }
144 }
145
146 fn context(&self) -> &'a dyn LoadContext {
147 self.inner
148 }
149
150 /// Recursively deserialize the underlying value into a `T`.
151 ///
152 /// Equivalent to calling `T::load(self.clone())`. Use this from inner loaders that
153 /// want to delegate to another [`Loadable`].
154 pub fn load<T>(&self) -> Result<T>
155 where
156 T: Loadable<'a>,
157 {
158 T::load(self.clone())
159 }
160
161 /// Returns `Some(Object)` if the value is a versioned object, else `None`.
162 pub fn as_object(&self) -> Option<Object<'a>> {
163 match self.value {
164 save::Value::Object(versioned) => {
165 let object = Object {
166 inner: self.inner,
167 record: versioned.record(),
168 version: versioned.version(),
169 };
170 Some(object)
171 }
172 _ => None,
173 }
174 }
175
176 /// Returns `Some(s)` if the value is a string, else `None`.
177 pub fn as_str(&self) -> Option<&'a str> {
178 match self.value {
179 save::Value::String(s) => Some(s),
180 _ => None,
181 }
182 }
183
184 /// Returns `Some(Array)` if the value is an array, else `None`.
185 pub fn as_array(&self) -> Option<Array<'a>> {
186 match self.value {
187 save::Value::Array(array) => Some(Array::new(self.context(), array)),
188 _ => None,
189 }
190 }
191
192 /// Returns `Some(Number)` if the value is numeric, else `None`.
193 ///
194 /// Use the conversion methods on [`Number`] (e.g. `as_u32`, `as_i64`) to narrow to
195 /// the target Rust type; out-of-range conversions return `None` and should be
196 /// surfaced as [`error::Kind::NumberOutOfRange`].
197 pub fn as_number(&self) -> Option<Number> {
198 match self.value {
199 save::Value::Number(number) => Some(*number),
200 _ => None,
201 }
202 }
203
204 /// Returns `Some(b)` if the value is a boolean, else `None`.
205 pub fn as_bool(&self) -> Option<bool> {
206 match self.value {
207 save::Value::Bool(value) => Some(*value),
208 _ => None,
209 }
210 }
211
212 /// Returns `true` if the value is null.
213 ///
214 /// Used by [`Loadable`] impls for [`Option<T>`] to detect the absent variant.
215 pub fn is_null(&self) -> bool {
216 matches!(self.value, save::Value::Null)
217 }
218
219 pub(crate) fn as_handle(&self) -> Option<&save::Handle> {
220 match self.value {
221 save::Value::Handle(handle) => Some(handle),
222 _ => None,
223 }
224 }
225}
226
227/// A versioned record reached through [`Context::as_object`].
228///
229/// `Object` is the entry point for record-based deserialization: it exposes the schema
230/// version via [`Object::version`], the user keys via [`Object::keys`], typed field
231/// extraction via [`Object::field`] (and the [`load_fields!`](crate::load_fields)
232/// macro), and side-car artifact access via [`Object::read`].
233pub struct Object<'a> {
234 inner: &'a dyn LoadContext,
235 record: &'a save::Record<'a>,
236 version: Version,
237}
238
239impl<'a> Object<'a> {
240 /// The schema [`Version`] recorded in the manifest for this object.
241 pub fn version(&self) -> Version {
242 self.version
243 }
244
245 /// Iterate over the user keys of this record. Reserved keys (`$version`,
246 /// `$handle`) are tracked separately and never appear here.
247 pub fn keys(&self) -> save::Keys<'_, 'a> {
248 self.record.keys()
249 }
250
251 /// Number of user keys in this record.
252 pub fn len(&self) -> usize {
253 self.record.len()
254 }
255
256 /// Whether this record has no user keys.
257 pub fn is_empty(&self) -> bool {
258 self.record.is_empty()
259 }
260
261 /// Return the sole user key of this record, used by enum loaders to dispatch
262 /// to a variant arm. Errors with a recoverable [`error::Kind::TypeMismatch`]
263 /// if the record has zero or multiple user keys (i.e. the wire shape does
264 /// not look like an enum).
265 pub fn single_key(&self) -> Result<&str> {
266 let mut keys = self.record.keys();
267 let Some(first) = keys.next() else {
268 return Err(error::Kind::TypeMismatch.into());
269 };
270 if keys.next().is_some() {
271 return Err(error::Kind::TypeMismatch.into());
272 }
273 Ok(first)
274 }
275
276 /// Descend into the raw [`Context`] for `key`, without imposing a type.
277 /// Useful for enum variants whose payload is itself an [`Object`], an array,
278 /// or any other [`save::Value`]. Returns [`error::Kind::MissingField`] when
279 /// the key is absent.
280 pub fn child(&self, key: &str) -> Result<Context<'a>> {
281 match self.record.get(key) {
282 Some(value) => Ok(Context::new(self.context(), value)),
283 None => Err(error::Kind::MissingField.into()),
284 }
285 }
286
287 /// Extract the value under `key` and deserialize it into a `T`.
288 ///
289 /// This is the typed counterpart to [`Object::child`] and the primitive used by the
290 /// [`load_fields!`](crate::load_fields) macro.
291 ///
292 /// # Errors
293 ///
294 /// Returns [`error::Kind::MissingField`] if the key is absent. Errors raised by
295 /// `T::load` (e.g. [`error::Kind::TypeMismatch`]) are propagated unchanged.
296 pub fn field<T>(&self, key: &str) -> Result<T>
297 where
298 T: Loadable<'a>,
299 {
300 match self.record.get(key) {
301 Some(value) => T::load(Context::new(self.context(), value)),
302 None => Err((error::Kind::MissingField).into()),
303 }
304 }
305
306 /// Open the side-car artifact identified by `handle` for reading.
307 ///
308 /// The handle must have been previously written through the matching
309 /// [`save::Context::write`](super::save::Context::write) call and embedded in this
310 /// record. Returns [`error::Kind::MissingFile`] if the file is not registered in the
311 /// manifest or if the handle attempts to escape the manifest directory.
312 pub fn read(&self, handle: &save::Handle) -> Result<Reader<'_>> {
313 self.inner.read(handle.as_str())
314 }
315
316 fn context(&self) -> &'a dyn LoadContext {
317 self.inner
318 }
319}
320
321/// A homogeneous sequence of values reached through [`Context::as_array`].
322///
323/// Backed by a borrowed `&[Value]`. Use [`Array::iter`] to walk the elements; each
324/// item is yielded as a [`Context`] that can be further deserialized via
325/// [`Context::load`].
326pub struct Array<'a> {
327 inner: &'a dyn LoadContext,
328 array: &'a [save::Value<'a>],
329}
330
331impl<'a> Array<'a> {
332 fn new(inner: &'a dyn LoadContext, array: &'a [save::Value<'a>]) -> Self {
333 Self { inner, array }
334 }
335
336 /// Number of elements in the array.
337 pub fn len(&self) -> usize {
338 self.array.len()
339 }
340
341 /// Returns `true` if the array is empty.
342 pub fn is_empty(&self) -> bool {
343 self.len() == 0
344 }
345
346 /// Iterate over the elements, each as a [`Context`] ready for further deserialization.
347 pub fn iter(&self) -> Iter<'a> {
348 Iter::new(self.context(), self.array.iter())
349 }
350
351 fn context(&self) -> &'a dyn LoadContext {
352 self.inner
353 }
354}
355
356/// Iterator returned by [`Array::iter`].
357pub struct Iter<'a> {
358 inner: &'a dyn LoadContext,
359 iter: std::slice::Iter<'a, save::Value<'a>>,
360}
361
362impl<'a> Iter<'a> {
363 fn new(inner: &'a dyn LoadContext, iter: std::slice::Iter<'a, save::Value<'a>>) -> Self {
364 Self { inner, iter }
365 }
366}
367
368impl<'a> Iterator for Iter<'a> {
369 type Item = Context<'a>;
370 fn next(&mut self) -> Option<Self::Item> {
371 self.iter
372 .next()
373 .map(|value| Context::new(self.inner, value))
374 }
375
376 fn size_hint(&self) -> (usize, Option<usize>) {
377 self.iter.size_hint()
378 }
379}
380
381impl ExactSizeIterator for Iter<'_> {}
382
383#[cfg(test)]
384mod tests {
385 use super::*;
386
387 /// Minimal [`LoadContext`] used to anchor a [`Context`] in tests; the `Object`
388 /// methods under test never touch the context's `value` / `read` operations.
389 struct TestContext;
390
391 impl LoadContext for TestContext {
392 fn value(&self) -> Result<&save::Value<'_>> {
393 unimplemented!("not exercised by Object accessor tests")
394 }
395
396 fn read(&self, _key: &str) -> Result<Reader<'_>> {
397 unimplemented!("not exercised by Object accessor tests")
398 }
399 }
400
401 #[test]
402 fn object_reports_populated_keys() {
403 let mut record = save::Record::empty();
404 record.insert("alpha", save::Value::Null).unwrap();
405 record.insert("beta", save::Value::Bool(true)).unwrap();
406 let value = record.into_value(Version::new(1, 0));
407
408 let inner = TestContext;
409 let object = Context::new(&inner, &value)
410 .as_object()
411 .expect("a versioned object value must yield an Object");
412
413 assert_eq!(object.len(), 2);
414 assert!(!object.is_empty());
415 let mut keys: Vec<&str> = object.keys().collect();
416 keys.sort_unstable();
417 assert_eq!(keys, ["alpha", "beta"]);
418 }
419
420 #[test]
421 fn object_reports_empty_record() {
422 let value = save::Record::empty().into_value(Version::new(1, 0));
423
424 let inner = TestContext;
425 let object = Context::new(&inner, &value)
426 .as_object()
427 .expect("a versioned object value must yield an Object");
428
429 assert_eq!(object.len(), 0);
430 assert!(object.is_empty());
431 assert_eq!(object.keys().count(), 0);
432 }
433}