use std::collections::{BTreeMap, BTreeSet};
use std::path::PathBuf;
use std::str::{self, FromStr};
use std::sync::LazyLock;
use regex::Regex;
use thiserror::Error;
use serde::{Deserialize, Serialize};
use crate::model::app_manifest::ReadManifest;
use crate::model::names::{ApplicationName, ModuleName, RelativePath, vocabulary};
vocabulary!(
#[derive(PartialOrd, Ord, Hash)]
pub enum AppRole: "app role" {
Plugin => "plugin",
Bundled => "bundled",
}
);
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct PluginFile {
pub app: ApplicationName,
pub path: RelativePath,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SourceFile {
pub path: RelativePath,
pub bytes: Vec<u8>,
}
#[derive(Debug, Clone)]
pub struct PluginAppSources {
pub root: PathBuf,
pub manifest: ReadManifest,
pub manifest_path: RelativePath,
pub files: Vec<SourceFile>,
}
#[derive(Debug, Clone, Default)]
pub struct PluginSources {
pub apps: Vec<PluginAppSources>,
pub host_provided: BTreeSet<ApplicationName>,
}
#[derive(Debug, Clone)]
pub struct PluginApp {
root: PathBuf,
manifest: ReadManifest,
manifest_path: RelativePath,
files: Vec<SourceFile>,
role: AppRole,
}
impl PluginApp {
#[must_use]
pub fn root(&self) -> &PathBuf {
&self.root
}
#[must_use]
pub fn manifest(&self) -> &ReadManifest {
&self.manifest
}
#[must_use]
pub fn manifest_path(&self) -> &RelativePath {
&self.manifest_path
}
#[must_use]
pub fn files(&self) -> &[SourceFile] {
&self.files
}
#[must_use]
pub fn role(&self) -> AppRole {
self.role
}
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum PluginSetError {
#[error("app {app} is found twice: {} and {}", first.display(), second.display())]
DuplicateApp {
app: ApplicationName,
first: PathBuf,
second: PathBuf,
},
#[error("module {module} is defined twice: {}/{} and {}/{}", first.app, first.path, second.app, second.path)]
DuplicateModule {
module: ModuleName,
first: PluginFile,
second: PluginFile,
},
}
#[derive(Debug, Clone)]
pub struct PluginSet {
apps: BTreeMap<ApplicationName, PluginApp>,
host_provided: BTreeSet<ApplicationName>,
}
static MODULE_ATTRIBUTE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?m)^\s*-\s*module\s*\(\s*([A-Za-z0-9_@.']+)\s*\)").expect("regex")
});
#[must_use]
pub fn declared_module(file: &SourceFile) -> Option<ModuleName> {
if !file.path.as_str().ends_with(".erl") {
return None;
}
let text = str::from_utf8(&file.bytes).ok()?;
let name = MODULE_ATTRIBUTE.captures(text)?.get(1)?.as_str();
ModuleName::from_str(name).ok()
}
impl PluginSet {
pub fn new(sources: PluginSources) -> Result<Self, PluginSetError> {
let mut apps: BTreeMap<ApplicationName, PluginApp> = BTreeMap::new();
let mut modules: BTreeMap<ModuleName, PluginFile> = BTreeMap::new();
for app in sources.apps {
let name = app.manifest.name().clone();
if let Some(first) = apps.get(&name) {
return Err(PluginSetError::DuplicateApp {
app: name,
first: first.root.clone(),
second: app.root,
});
}
for file in &app.files {
let Some(module) = declared_module(file) else {
continue;
};
let here = PluginFile {
app: name.clone(),
path: file.path.clone(),
};
if let Some(first) = modules.get(&module) {
return Err(PluginSetError::DuplicateModule {
module,
first: first.clone(),
second: here,
});
}
modules.insert(module, here);
}
apps.insert(
name,
PluginApp {
root: app.root,
manifest: app.manifest,
manifest_path: app.manifest_path,
files: app.files,
role: AppRole::Plugin,
},
);
}
let required: BTreeSet<ApplicationName> = apps
.values()
.flat_map(|app| app.manifest.requires().iter().cloned())
.collect();
for (name, app) in &mut apps {
if required.contains(name) {
app.role = AppRole::Bundled;
}
}
Ok(Self {
apps,
host_provided: sources.host_provided,
})
}
#[must_use]
pub fn apps(&self) -> &BTreeMap<ApplicationName, PluginApp> {
&self.apps
}
#[must_use]
pub fn app(&self, name: &ApplicationName) -> Option<&PluginApp> {
self.apps.get(name)
}
#[must_use]
pub fn contains_app(&self, name: &ApplicationName) -> bool {
self.apps.contains_key(name)
}
#[must_use]
pub fn host_provided(&self) -> &BTreeSet<ApplicationName> {
&self.host_provided
}
pub fn files(&self) -> impl Iterator<Item = (PluginFile, &SourceFile)> {
self.apps.iter().flat_map(|(app, plugin)| {
plugin.files.iter().map(move |file| {
(
PluginFile {
app: app.clone(),
path: file.path.clone(),
},
file,
)
})
})
}
}