Skip to main content

prax_schema/loader/
source.rs

1//! Source provenance tracking for multi-file schemas.
2
3use std::path::{Path, PathBuf};
4
5use serde::{Deserialize, Serialize};
6
7use crate::ast::Span;
8
9/// Opaque, dense identifier for a source file in a [`SourceMap`].
10#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Ord, PartialOrd, Serialize, Deserialize)]
11pub struct SourceId(pub u32);
12
13/// A (source file id, span) pair used in cross-file diagnostics.
14#[derive(Copy, Clone, Debug, PartialEq, Eq)]
15pub struct SourceLoc {
16    pub source: SourceId,
17    pub span: Span,
18}
19
20impl SourceLoc {
21    pub fn new(source: SourceId, span: Span) -> Self {
22        Self { source, span }
23    }
24}
25
26/// A single source file (path + content) loaded into the schema.
27#[derive(Debug, Clone)]
28pub struct SourceFile {
29    pub path: PathBuf,
30    pub content: String,
31}
32
33/// Map of [`SourceId`] -> [`SourceFile`].
34///
35/// Built incrementally during loading. Empty by default.
36#[derive(Debug, Clone, Default)]
37pub struct SourceMap {
38    files: Vec<SourceFile>,
39}
40
41impl SourceMap {
42    pub fn new() -> Self {
43        Self::default()
44    }
45
46    /// Insert a new source file and return its [`SourceId`].
47    pub fn insert(&mut self, path: impl Into<PathBuf>, content: impl Into<String>) -> SourceId {
48        let id = SourceId(self.files.len() as u32);
49        self.files.push(SourceFile {
50            path: path.into(),
51            content: content.into(),
52        });
53        id
54    }
55
56    pub fn get(&self, id: SourceId) -> Option<&SourceFile> {
57        self.files.get(id.0 as usize)
58    }
59
60    pub fn iter(&self) -> impl Iterator<Item = (SourceId, &SourceFile)> {
61        self.files
62            .iter()
63            .enumerate()
64            .map(|(i, f)| (SourceId(i as u32), f))
65    }
66
67    pub fn len(&self) -> usize {
68        self.files.len()
69    }
70
71    pub fn is_empty(&self) -> bool {
72        self.files.is_empty()
73    }
74
75    /// Convenience: path for a given id.
76    pub fn path_of(&self, id: SourceId) -> Option<&Path> {
77        self.get(id).map(|f| f.path.as_path())
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn insert_assigns_monotonic_ids() {
87        let mut map = SourceMap::new();
88        let a = map.insert("a.prax", "model A {}");
89        let b = map.insert("b.prax", "model B {}");
90        assert_eq!(a, SourceId(0));
91        assert_eq!(b, SourceId(1));
92        assert_eq!(map.len(), 2);
93    }
94
95    #[test]
96    fn get_returns_inserted_file() {
97        let mut map = SourceMap::new();
98        let id = map.insert("/tmp/x.prax", "content");
99        let f = map.get(id).unwrap();
100        assert_eq!(f.path.to_str().unwrap(), "/tmp/x.prax");
101        assert_eq!(f.content, "content");
102    }
103
104    #[test]
105    fn get_unknown_id_returns_none() {
106        let map = SourceMap::new();
107        assert!(map.get(SourceId(42)).is_none());
108    }
109}