use crate::options::CacheMode;
use crate::policy::{DefaultPolicy, ResolutionPolicy};
use anyhow::Result;
use globset::{Glob, GlobSet, GlobSetBuilder};
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::io::{BufReader, Write};
use std::path::{Path, PathBuf};
use std::time::Duration;
const DEFAULT_INCLUDE_PATTERNS: &[&str] = &["*.ttl", "*.xml", "*.n3"];
const DEFAULT_REMOTE_CACHE_TTL: Duration = Duration::from_secs(60 * 60 * 24);
fn default_remote_cache_ttl_secs() -> u64 {
DEFAULT_REMOTE_CACHE_TTL.as_secs()
}
fn cache_mode_ser<S>(mode: &CacheMode, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_bool(mode.is_enabled())
}
fn cache_mode_de<'de, D>(deserializer: D) -> Result<CacheMode, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = bool::deserialize(deserializer)?;
Ok(CacheMode::from(value))
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct Config {
pub root: PathBuf,
#[serde(default)]
pub locations: Vec<PathBuf>,
#[serde(default)]
pub external_graph_store: Option<String>,
#[serde(default)]
includes: Vec<String>,
#[serde(default)]
excludes: Vec<String>,
#[serde(default)]
include_ontologies: Vec<String>,
#[serde(default)]
exclude_ontologies: Vec<String>,
pub require_ontology_names: bool,
pub strict: bool,
pub offline: bool,
pub resolution_policy: String,
#[serde(
default,
serialize_with = "cache_mode_ser",
deserialize_with = "cache_mode_de"
)]
pub use_cached_ontologies: CacheMode,
#[serde(default = "default_remote_cache_ttl_secs")]
pub remote_cache_ttl_secs: u64,
pub temporary: bool,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ConfigOverrides {
pub locations: Option<Vec<PathBuf>>,
pub includes: Option<Vec<String>>,
pub excludes: Option<Vec<String>>,
pub include_ontologies: Option<Vec<String>>,
pub exclude_ontologies: Option<Vec<String>>,
pub require_ontology_names: Option<bool>,
pub strict: Option<bool>,
pub offline: Option<bool>,
pub resolution_policy: Option<String>,
pub use_cached_ontologies: Option<CacheMode>,
pub remote_cache_ttl_secs: Option<u64>,
}
impl Config {
pub fn builder() -> ConfigBuilder {
ConfigBuilder::new()
}
pub(crate) fn build_globsets(&self) -> Result<(GlobSet, GlobSet)> {
fn contains_meta(pat: &str) -> bool {
pat.chars()
.any(|c| matches!(c, '*' | '?' | '[' | ']' | '{' | '}' | '!'))
}
fn expand_patterns(patterns: &[String]) -> Result<GlobSet> {
let mut builder = GlobSetBuilder::new();
for pat in patterns {
let trimmed = pat.trim_end_matches('/');
builder.add(Glob::new(trimmed)?);
if !contains_meta(trimmed) {
builder.add(Glob::new(&format!("{}/**", trimmed))?);
}
}
Ok(builder.build()?)
}
let includes = expand_patterns(&self.includes)?;
let excludes = expand_patterns(&self.excludes)?;
Ok((includes, excludes))
}
pub(crate) fn includes_is_empty(&self) -> bool {
self.includes.is_empty()
}
pub(crate) fn build_ontology_regexes(&self) -> Result<(Vec<Regex>, Vec<Regex>)> {
let inc = self
.include_ontologies
.iter()
.map(|p| Regex::new(p))
.collect::<Result<Vec<_>, _>>()?;
let exc = self
.exclude_ontologies
.iter()
.map(|p| Regex::new(p))
.collect::<Result<Vec<_>, _>>()?;
Ok((inc, exc))
}
pub fn apply_overrides(&mut self, overrides: &ConfigOverrides) -> Result<bool> {
let mut candidate = self.clone();
let mut changed = false;
macro_rules! replace {
($field:ident) => {
if let Some(value) = &overrides.$field {
if &candidate.$field != value {
candidate.$field = value.clone();
changed = true;
}
}
};
}
replace!(locations);
replace!(includes);
replace!(excludes);
replace!(include_ontologies);
replace!(exclude_ontologies);
replace!(require_ontology_names);
replace!(strict);
replace!(offline);
replace!(resolution_policy);
replace!(use_cached_ontologies);
replace!(remote_cache_ttl_secs);
candidate.build_globsets()?;
candidate.build_ontology_regexes()?;
if crate::policy::policy_from_name(&candidate.resolution_policy).is_none() {
return Err(anyhow::anyhow!(
"Unknown resolution policy: {}",
candidate.resolution_policy
));
}
*self = candidate;
Ok(changed)
}
pub fn default(root: PathBuf) -> Result<Self> {
Config::builder().root(root).offline(true).build()
}
pub fn temporary(root: PathBuf) -> Result<Self> {
Config::builder().root(root).temporary(true).build()
}
pub fn new_with_default_matches(root: PathBuf) -> Result<Self> {
Config::builder().root(root).build()
}
pub fn is_included(&self, path: &Path) -> bool {
let rel = path.strip_prefix(&self.root).unwrap_or(path).to_path_buf();
let (include_set, exclude_set) = match self.build_globsets() {
Ok(sets) => sets,
Err(err) => {
log::warn!("Invalid include/exclude pattern: {err}");
return true;
}
};
if exclude_set.is_match(&rel) {
return false;
}
if self.includes.is_empty() {
return true;
}
include_set.is_match(&rel)
}
pub fn save_to_file(&self, file: &Path) -> Result<()> {
let config_str = serde_json::to_string_pretty(&self)?;
let mut file = std::fs::File::create(file)?;
file.write_all(config_str.as_bytes())?;
Ok(())
}
pub fn from_file(file: &Path) -> Result<Self> {
let file = std::fs::File::open(file)?;
let reader = BufReader::new(file);
let config: Config = serde_json::from_reader(reader)?;
Ok(config)
}
pub fn print(&self) {
println!("Configuration:");
println!(" Root: {}", self.root.display());
if let Some(store) = &self.external_graph_store {
println!(" External Graph Store: {store}");
}
if !self.locations.is_empty() {
println!(" Locations:");
for loc in &self.locations {
println!(" - {}", loc.display());
}
}
println!(" Include Patterns:");
for pat in &self.includes {
println!(" - {pat}");
}
if !self.excludes.is_empty() {
println!(" Exclude Patterns:");
for pat in &self.excludes {
println!(" - {pat}");
}
}
if !self.include_ontologies.is_empty() {
println!(" Include Ontology Regexes:");
for pat in &self.include_ontologies {
println!(" - {pat}");
}
}
if !self.exclude_ontologies.is_empty() {
println!(" Exclude Ontology Regexes:");
for pat in &self.exclude_ontologies {
println!(" - {pat}");
}
}
println!(" Require Ontology Names: {}", self.require_ontology_names);
println!(" Strict: {}", self.strict);
println!(" Offline: {}", self.offline);
println!(
" Use Cached Ontologies: {}",
self.use_cached_ontologies.is_enabled()
);
println!(" Remote Cache TTL (secs): {}", self.remote_cache_ttl_secs);
println!(" Resolution Policy: {}", self.resolution_policy);
println!(" Temporary: {}", self.temporary);
}
}
pub struct ConfigBuilder {
root: Option<PathBuf>,
locations: Option<Vec<PathBuf>>,
external_graph_store: Option<Option<String>>,
includes: Option<Vec<String>>,
excludes: Option<Vec<String>>,
include_ontologies: Option<Vec<String>>,
exclude_ontologies: Option<Vec<String>>,
require_ontology_names: Option<bool>,
strict: Option<bool>,
offline: Option<bool>,
resolution_policy: Option<String>,
temporary: Option<bool>,
use_cached_ontologies: Option<CacheMode>,
remote_cache_ttl_secs: Option<u64>,
}
impl ConfigBuilder {
pub fn new() -> Self {
Self {
root: None,
locations: None,
external_graph_store: None,
includes: None,
excludes: None,
include_ontologies: None,
exclude_ontologies: None,
require_ontology_names: None,
strict: None,
offline: None,
resolution_policy: None,
temporary: None,
use_cached_ontologies: None,
remote_cache_ttl_secs: None,
}
}
pub fn root(mut self, root: PathBuf) -> Self {
self.root = Some(root);
self
}
pub fn locations(mut self, locations: Vec<PathBuf>) -> Self {
self.locations = Some(locations);
self
}
pub fn external_graph_store<S: Into<String>>(mut self, store: Option<S>) -> Self {
self.external_graph_store = Some(store.map(|s| s.into()));
self
}
pub fn includes<I>(mut self, includes: I) -> Self
where
I: IntoIterator,
I::Item: AsRef<str>,
{
self.includes = Some(
includes
.into_iter()
.map(|s| s.as_ref().to_string())
.collect(),
);
self
}
pub fn excludes<I>(mut self, excludes: I) -> Self
where
I: IntoIterator,
I::Item: AsRef<str>,
{
self.excludes = Some(
excludes
.into_iter()
.map(|s| s.as_ref().to_string())
.collect(),
);
self
}
pub fn include_ontologies<I>(mut self, patterns: I) -> Self
where
I: IntoIterator,
I::Item: AsRef<str>,
{
self.include_ontologies = Some(
patterns
.into_iter()
.map(|s| s.as_ref().to_string())
.collect(),
);
self
}
pub fn exclude_ontologies<I>(mut self, patterns: I) -> Self
where
I: IntoIterator,
I::Item: AsRef<str>,
{
self.exclude_ontologies = Some(
patterns
.into_iter()
.map(|s| s.as_ref().to_string())
.collect(),
);
self
}
pub fn require_ontology_names(mut self, require: bool) -> Self {
self.require_ontology_names = Some(require);
self
}
pub fn strict(mut self, strict: bool) -> Self {
self.strict = Some(strict);
self
}
pub fn offline(mut self, offline: bool) -> Self {
self.offline = Some(offline);
self
}
pub fn use_cached_ontologies(mut self, mode: CacheMode) -> Self {
self.use_cached_ontologies = Some(mode);
self
}
pub fn remote_cache_ttl_secs(mut self, ttl_secs: u64) -> Self {
self.remote_cache_ttl_secs = Some(ttl_secs);
self
}
pub fn resolution_policy(mut self, policy: String) -> Self {
self.resolution_policy = Some(policy);
self
}
pub fn temporary(mut self, temporary: bool) -> Self {
self.temporary = Some(temporary);
self
}
pub fn build(self) -> Result<Config> {
let root = self
.root
.ok_or_else(|| anyhow::anyhow!("Config 'root' is required"))?;
let locations = self.locations.unwrap_or_default();
let includes_str = self.includes.unwrap_or_else(|| {
DEFAULT_INCLUDE_PATTERNS
.iter()
.map(|s| s.to_string())
.collect()
});
let excludes_str = self.excludes.unwrap_or_default();
let include_ontologies = self.include_ontologies.unwrap_or_default();
let exclude_ontologies = self.exclude_ontologies.unwrap_or_default();
Ok(Config {
root,
locations,
external_graph_store: self.external_graph_store.unwrap_or(None),
includes: includes_str,
excludes: excludes_str,
include_ontologies,
exclude_ontologies,
require_ontology_names: self.require_ontology_names.unwrap_or(false),
strict: self.strict.unwrap_or(false),
offline: self.offline.unwrap_or(false),
resolution_policy: self
.resolution_policy
.unwrap_or_else(|| DefaultPolicy.policy_name().to_string()),
use_cached_ontologies: self.use_cached_ontologies.unwrap_or_default(),
remote_cache_ttl_secs: self
.remote_cache_ttl_secs
.unwrap_or_else(default_remote_cache_ttl_secs),
temporary: self.temporary.unwrap_or(false),
})
}
}
impl Default for ConfigBuilder {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum HowCreated {
New,
SameConfig,
RecreatedDifferentConfig,
RecreatedFlag,
}
impl std::fmt::Display for HowCreated {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
HowCreated::New => write!(f, "New Environment"),
HowCreated::SameConfig => write!(f, "Same Config. Reusing existing environment."),
HowCreated::RecreatedDifferentConfig => {
write!(f, "Recreated environment due to different config")
}
HowCreated::RecreatedFlag => write!(f, "Recreated environment due to 'recreate' flag"),
}
}
}