1use std::collections::{BTreeMap, BTreeSet};
6use std::path::PathBuf;
7use std::str::{self, FromStr};
8use std::sync::LazyLock;
9
10use regex::Regex;
11use thiserror::Error;
12
13use serde::{Deserialize, Serialize};
14
15use crate::model::app_manifest::ReadManifest;
16use crate::model::names::{ApplicationName, ModuleName, RelativePath, vocabulary};
17
18vocabulary!(
19 #[derive(PartialOrd, Ord, Hash)]
23 pub enum AppRole: "app role" {
24 Plugin => "plugin",
25 Bundled => "bundled",
26 }
27);
28
29#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
31pub struct PluginFile {
32 pub app: ApplicationName,
33 pub path: RelativePath,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct SourceFile {
38 pub path: RelativePath,
39 pub bytes: Vec<u8>,
40}
41
42#[derive(Debug, Clone)]
43pub struct PluginAppSources {
44 pub root: PathBuf,
45 pub manifest: ReadManifest,
46 pub manifest_path: RelativePath,
47 pub files: Vec<SourceFile>,
48}
49
50#[derive(Debug, Clone, Default)]
52pub struct PluginSources {
53 pub apps: Vec<PluginAppSources>,
54 pub host_provided: BTreeSet<ApplicationName>,
55}
56
57#[derive(Debug, Clone)]
58pub struct PluginApp {
59 root: PathBuf,
60 manifest: ReadManifest,
61 manifest_path: RelativePath,
62 files: Vec<SourceFile>,
63 role: AppRole,
64}
65
66impl PluginApp {
67 #[must_use]
68 pub fn root(&self) -> &PathBuf {
69 &self.root
70 }
71
72 #[must_use]
73 pub fn manifest(&self) -> &ReadManifest {
74 &self.manifest
75 }
76
77 #[must_use]
78 pub fn manifest_path(&self) -> &RelativePath {
79 &self.manifest_path
80 }
81
82 #[must_use]
83 pub fn files(&self) -> &[SourceFile] {
84 &self.files
85 }
86
87 #[must_use]
88 pub fn role(&self) -> AppRole {
89 self.role
90 }
91}
92
93#[derive(Debug, Clone, PartialEq, Eq, Error)]
94pub enum PluginSetError {
95 #[error("app {app} is found twice: {} and {}", first.display(), second.display())]
96 DuplicateApp {
97 app: ApplicationName,
98 first: PathBuf,
99 second: PathBuf,
100 },
101 #[error("module {module} is defined twice: {}/{} and {}/{}", first.app, first.path, second.app, second.path)]
102 DuplicateModule {
103 module: ModuleName,
104 first: PluginFile,
105 second: PluginFile,
106 },
107}
108
109#[derive(Debug, Clone)]
111pub struct PluginSet {
112 apps: BTreeMap<ApplicationName, PluginApp>,
113 host_provided: BTreeSet<ApplicationName>,
114}
115
116static MODULE_ATTRIBUTE: LazyLock<Regex> = LazyLock::new(|| {
117 Regex::new(r"(?m)^\s*-\s*module\s*\(\s*([A-Za-z0-9_@.']+)\s*\)").expect("regex")
118});
119
120#[must_use]
122pub fn declared_module(file: &SourceFile) -> Option<ModuleName> {
123 if !file.path.as_str().ends_with(".erl") {
124 return None;
125 }
126 let text = str::from_utf8(&file.bytes).ok()?;
127 let name = MODULE_ATTRIBUTE.captures(text)?.get(1)?.as_str();
128 ModuleName::from_str(name).ok()
129}
130
131impl PluginSet {
132 pub fn new(sources: PluginSources) -> Result<Self, PluginSetError> {
133 let mut apps: BTreeMap<ApplicationName, PluginApp> = BTreeMap::new();
134 let mut modules: BTreeMap<ModuleName, PluginFile> = BTreeMap::new();
135 for app in sources.apps {
136 let name = app.manifest.name().clone();
137 if let Some(first) = apps.get(&name) {
138 return Err(PluginSetError::DuplicateApp {
139 app: name,
140 first: first.root.clone(),
141 second: app.root,
142 });
143 }
144 for file in &app.files {
145 let Some(module) = declared_module(file) else {
146 continue;
147 };
148 let here = PluginFile {
149 app: name.clone(),
150 path: file.path.clone(),
151 };
152 if let Some(first) = modules.get(&module) {
153 return Err(PluginSetError::DuplicateModule {
154 module,
155 first: first.clone(),
156 second: here,
157 });
158 }
159 modules.insert(module, here);
160 }
161 apps.insert(
162 name,
163 PluginApp {
164 root: app.root,
165 manifest: app.manifest,
166 manifest_path: app.manifest_path,
167 files: app.files,
168 role: AppRole::Plugin,
169 },
170 );
171 }
172 let required: BTreeSet<ApplicationName> = apps
173 .values()
174 .flat_map(|app| app.manifest.requires().iter().cloned())
175 .collect();
176 for (name, app) in &mut apps {
177 if required.contains(name) {
178 app.role = AppRole::Bundled;
179 }
180 }
181 Ok(Self {
182 apps,
183 host_provided: sources.host_provided,
184 })
185 }
186
187 #[must_use]
188 pub fn apps(&self) -> &BTreeMap<ApplicationName, PluginApp> {
189 &self.apps
190 }
191
192 #[must_use]
193 pub fn app(&self, name: &ApplicationName) -> Option<&PluginApp> {
194 self.apps.get(name)
195 }
196
197 #[must_use]
198 pub fn contains_app(&self, name: &ApplicationName) -> bool {
199 self.apps.contains_key(name)
200 }
201
202 #[must_use]
203 pub fn host_provided(&self) -> &BTreeSet<ApplicationName> {
204 &self.host_provided
205 }
206
207 pub fn files(&self) -> impl Iterator<Item = (PluginFile, &SourceFile)> {
209 self.apps.iter().flat_map(|(app, plugin)| {
210 plugin.files.iter().map(move |file| {
211 (
212 PluginFile {
213 app: app.clone(),
214 path: file.path.clone(),
215 },
216 file,
217 )
218 })
219 })
220 }
221}