xidl_parser/hir/
include.rs1use crate::parser::IncludeResolver;
2use std::collections::BTreeMap;
3use std::fs;
4use std::path::{Path, PathBuf};
5
6pub fn split_version_selector(path: &str) -> (&str, Option<&str>) {
11 let Some(index) = path.rfind('@').filter(|index| *index > 0) else {
12 return (path, None);
13 };
14 let (target, selector) = path.split_at(index);
15 let selector = selector.trim_start_matches('@');
16 if selector.is_empty() {
17 return (path, None);
18 }
19 (target, Some(selector))
20}
21
22pub struct FsIncludeResolver;
24
25impl IncludeResolver for FsIncludeResolver {
26 fn resolve(
27 &mut self,
28 parent_path: Option<&str>,
29 path: &str,
30 ) -> crate::error::ParserResult<(String, String)> {
31 let parent = parent_path.map(Path::new).unwrap_or_else(|| Path::new("."));
32 let base = parent.parent().unwrap_or_else(|| Path::new("."));
33 let resolved_path = normalize_path(&base.join(path));
34 read_include(&resolved_path)
35 }
36}
37
38pub struct MountIncludeResolver {
45 mounts: BTreeMap<String, PathBuf>,
46}
47
48impl MountIncludeResolver {
49 pub fn new(mounts: BTreeMap<String, PathBuf>) -> Self {
51 Self { mounts }
52 }
53
54 fn mount_for(&self, path: &str) -> Option<(&str, &Path)> {
56 self.mounts
57 .iter()
58 .filter(|(name, _)| {
59 path.strip_prefix(name.as_str())
60 .is_some_and(|rest| rest.is_empty() || rest.starts_with('/'))
61 })
62 .max_by_key(|(name, _)| name.len())
63 .map(|(name, root)| (name.as_str(), root.as_path()))
64 }
65}
66
67impl IncludeResolver for MountIncludeResolver {
68 fn resolve(
69 &mut self,
70 parent_path: Option<&str>,
71 path: &str,
72 ) -> crate::error::ParserResult<(String, String)> {
73 let Some((name, root)) = self.mount_for(path) else {
74 return FsIncludeResolver.resolve(parent_path, path);
75 };
76 let rest = path[name.len()..].trim_start_matches('/');
77 let resolved_path = normalize_path(&root.join(rest));
78 if !resolved_path.is_file() {
79 return Err(crate::error::ParseError::Message(format!(
80 "dependency include '{path}' does not exist: the '{name}' mount at '{}' \
81 resolves it to '{}'",
82 root.display(),
83 resolved_path.display()
84 )));
85 }
86 read_include(&resolved_path).map_err(|err| {
87 crate::error::ParseError::Message(format!(
88 "dependency include '{path}' could not be read through the '{name}' mount \
89 at '{}': {err}",
90 root.display()
91 ))
92 })
93 }
94}
95
96fn read_include(resolved_path: &Path) -> crate::error::ParserResult<(String, String)> {
98 if !resolved_path.is_file() {
99 return Err(crate::error::ParseError::Message(format!(
100 "include path '{}' does not exist",
101 resolved_path.display()
102 )));
103 }
104
105 let content = fs::read_to_string(resolved_path).map_err(|err| {
106 crate::error::ParseError::Message(format!(
107 "failed to read include '{}': {err}",
108 resolved_path.display()
109 ))
110 })?;
111
112 Ok((resolved_path.display().to_string(), content))
113}
114
115pub(crate) fn normalize_path(path: &Path) -> PathBuf {
116 let path = if path.is_absolute() {
117 path.to_path_buf()
118 } else {
119 std::env::current_dir()
120 .unwrap_or_else(|_| PathBuf::from("."))
121 .join(path)
122 };
123 fs::canonicalize(&path).unwrap_or(path)
124}