#[cfg(test)]
mod fixture;
mod resolve;
#[cfg(test)]
mod tests;
use std::hash::{DefaultHasher, Hash as _, Hasher as _};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex, PoisonError};
use arc_swap::ArcSwap;
use promptforge_core::parser::Prompt;
use crate::config::Config;
use crate::error::{CatalogError, FaultKind};
use crate::generation::Generation;
use crate::retrieval::Retrieval;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum OnBroken {
Reject,
Retain,
}
#[derive(Debug, Clone)]
pub(crate) struct Entry {
name: String,
description: String,
path: PathBuf,
state: EntryState,
}
#[derive(Debug, Clone)]
enum EntryState {
Healthy {
source: String,
#[cfg_attr(not(test), allow(dead_code))]
prompt: Box<Prompt>,
},
Broken { kind: FaultKind, detail: String },
}
impl Entry {
pub(crate) fn healthy(path: PathBuf, source: String, prompt: Prompt) -> Entry {
Entry {
name: prompt.frontmatter().name().to_owned(),
description: prompt.frontmatter().description().to_owned(),
path,
state: EntryState::Healthy {
source,
prompt: Box::new(prompt),
},
}
}
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) fn broken(name: String, path: PathBuf, problem: impl Into<String>) -> Entry {
Entry::broken_as(name, path, FaultKind::Unparsable, problem)
}
pub(crate) fn broken_as(
name: String,
path: PathBuf,
kind: FaultKind,
problem: impl Into<String>,
) -> Entry {
Entry {
name,
description: String::new(),
path,
state: EntryState::Broken {
kind,
detail: problem.into(),
},
}
}
#[must_use]
pub(crate) fn name(&self) -> &str {
&self.name
}
#[must_use]
pub(crate) fn description(&self) -> &str {
&self.description
}
#[must_use]
pub(crate) fn path(&self) -> &Path {
&self.path
}
#[must_use]
pub(crate) fn source(&self) -> Option<&str> {
match &self.state {
EntryState::Healthy { source, .. } => Some(source),
EntryState::Broken { .. } => None,
}
}
#[cfg_attr(not(test), allow(dead_code))]
#[must_use]
pub(crate) fn prompt(&self) -> Option<&Prompt> {
match &self.state {
EntryState::Healthy { prompt, .. } => Some(prompt.as_ref()),
EntryState::Broken { .. } => None,
}
}
#[must_use]
pub(crate) fn problem(&self) -> Option<&str> {
match &self.state {
EntryState::Broken { detail, .. } => Some(detail),
EntryState::Healthy { .. } => None,
}
}
#[must_use]
pub(crate) fn problem_kind(&self) -> Option<FaultKind> {
match &self.state {
EntryState::Broken { kind, .. } => Some(*kind),
EntryState::Healthy { .. } => None,
}
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Catalog {
entries: Vec<Entry>,
#[cfg(test)]
by_name: std::collections::HashMap<String, usize>,
}
impl Catalog {
pub fn resolve(config: &Config, on_broken: OnBroken) -> Result<Catalog, CatalogError> {
resolve::resolve(config, on_broken)
}
pub(crate) fn new(mut entries: Vec<Entry>) -> Catalog {
entries.sort_by(|a, b| {
a.name
.cmp(&b.name)
.then_with(|| a.problem().is_some().cmp(&b.problem().is_some()))
.then_with(|| a.path.cmp(&b.path))
});
#[cfg(test)]
let by_name = {
let mut map = std::collections::HashMap::with_capacity(entries.len());
for (index, entry) in entries.iter().enumerate() {
map.entry(entry.name.clone()).or_insert(index);
}
map
};
Catalog {
entries,
#[cfg(test)]
by_name,
}
}
#[must_use]
pub(crate) fn entries(&self) -> &[Entry] {
&self.entries
}
#[must_use]
pub(crate) fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub(crate) fn is_empty(&self) -> bool {
self.entries.is_empty()
}
#[cfg(test)]
#[must_use]
pub(crate) fn find(&self, name: &str) -> Option<&Entry> {
self.by_name.get(name).map(|&index| &self.entries[index])
}
#[must_use]
pub(crate) fn hash(&self) -> u64 {
let mut hasher = DefaultHasher::new();
for entry in &self.entries {
entry.name.hash(&mut hasher);
entry.description.hash(&mut hasher);
}
hasher.finish()
}
}
#[derive(Debug)]
#[non_exhaustive]
pub struct CatalogHandle {
current: ArcSwap<Generation>,
coordinator: Publisher,
}
#[derive(Debug, Default)]
struct Publisher {
tickets: AtomicU64,
published: Mutex<u64>,
}
impl CatalogHandle {
#[must_use]
pub fn new(catalog: Catalog) -> CatalogHandle {
CatalogHandle::with_retrieval(catalog, Retrieval::idle())
}
#[must_use]
pub(crate) fn with_retrieval(catalog: Catalog, retrieval: Retrieval) -> CatalogHandle {
CatalogHandle {
current: ArcSwap::from_pointee(Generation::new(catalog, retrieval)),
coordinator: Publisher::default(),
}
}
#[must_use]
pub(crate) fn load(&self) -> Arc<Generation> {
self.current.load_full()
}
pub(crate) fn store(&self, generation: Generation) {
self.current.store(Arc::new(generation));
}
pub(crate) fn claim(&self) -> u64 {
self.coordinator
.tickets
.fetch_add(1, Ordering::Relaxed)
.wrapping_add(1)
}
pub(crate) fn publish(&self, cancel: &AtomicBool, ticket: u64, generation: Generation) -> bool {
let mut published = self
.coordinator
.published
.lock()
.unwrap_or_else(PoisonError::into_inner);
if cancel.load(Ordering::SeqCst) {
return false;
}
if ticket <= *published {
return false;
}
self.store(generation);
*published = ticket;
true
}
}