use std::cell::RefCell;
use std::path::PathBuf;
use crate::domain::model::entry_locator::EntryLocator;
use crate::domain::model::entry_origin::EntryOrigin;
use crate::domain::model::issue::companion::{
CompanionContent, CompanionIdentifier, IssueCompanions,
};
use crate::domain::model::issue::test_fixtures::ir;
use crate::domain::model::issue::Issue;
use crate::domain::model::record_ref::IssueRef;
use crate::domain::usecases::issue::{IndexSampler, IssueIdGenerator, IssueRepository};
#[derive(Default)]
pub struct CycleIndexSampler {
next: usize,
}
impl IndexSampler for CycleIndexSampler {
fn next_index(&mut self, len: usize) -> usize {
let i = self.next % len;
self.next = self.next.wrapping_add(1);
i
}
}
pub struct SequentialIssueIdGenerator {
next: std::cell::Cell<u64>,
}
impl SequentialIssueIdGenerator {
pub fn starting_at(n: u64) -> Self {
Self {
next: std::cell::Cell::new(n),
}
}
}
impl Default for SequentialIssueIdGenerator {
fn default() -> Self {
Self::starting_at(1)
}
}
impl IssueIdGenerator for SequentialIssueIdGenerator {
fn next_id(&self) -> anyhow::Result<IssueRef> {
let n = self.next.get();
self.next.set(n + 1);
Ok(ir(n))
}
}
struct FakeEntry {
location: EntryLocator,
result: Result<Issue, String>,
}
pub struct FakeIssueRepository {
entries: RefCell<Vec<FakeEntry>>,
saves: RefCell<Vec<Issue>>,
id_prefix: Option<String>,
}
impl Default for FakeIssueRepository {
fn default() -> Self {
Self {
entries: RefCell::new(Vec::new()),
saves: RefCell::new(Vec::new()),
id_prefix: None,
}
}
}
impl FakeIssueRepository {
pub fn new() -> Self {
Self::default()
}
pub fn with_issues<I: IntoIterator<Item = Issue>>(issues: I) -> Self {
let mut me = Self::new();
for issue in issues {
me.push_issue(issue);
}
me
}
pub fn with_pairs<I: IntoIterator<Item = (PathBuf, Issue)>>(pairs: I) -> Self {
let me = Self::new();
for (path, mut issue) in pairs {
let location = EntryLocator::new(path.display().to_string());
issue.location = location.clone();
me.entries.borrow_mut().push(FakeEntry {
location,
result: Ok(issue),
});
}
me
}
pub fn with_id_prefix(mut self, prefix: &str) -> Self {
self.id_prefix = Some(prefix.to_string());
self
}
pub fn push_issue(&mut self, mut issue: Issue) {
let location =
EntryLocator::new(format!("docs/issues/{}-issue/index.md", issue.id.suffix()));
issue.location = location.clone();
self.entries.borrow_mut().push(FakeEntry {
location,
result: Ok(issue),
});
}
pub fn push_union_issue(&mut self, mut issue: Issue, source: &str) {
let location = EntryLocator::new(format!("{source}/{}-issue/index.md", issue.id.suffix()));
issue.origin = EntryOrigin::Union {
name: source.to_string(),
};
issue.location = location.clone();
self.entries.borrow_mut().push(FakeEntry {
location,
result: Ok(issue),
});
}
pub fn push_broken(&mut self, location: &str, reason: &str) {
self.entries.borrow_mut().push(FakeEntry {
location: EntryLocator::new(location),
result: Err(reason.to_string()),
});
}
pub fn saves(&self) -> Vec<Issue> {
self.saves.borrow().clone()
}
pub fn last_saved(&self) -> Option<Issue> {
self.saves.borrow().last().cloned()
}
pub fn saved_for(&self, id: &IssueRef) -> Option<Issue> {
self.saves
.borrow()
.iter()
.rev()
.find(|i| &i.id == id)
.cloned()
}
pub fn save_count(&self) -> usize {
self.saves.borrow().len()
}
}
impl IssueRepository for FakeIssueRepository {
fn save(&self, issue: &Issue) -> anyhow::Result<()> {
self.saves.borrow_mut().push(issue.clone());
let mut entries = self.entries.borrow_mut();
let location =
EntryLocator::new(format!("docs/issues/{}-issue/index.md", issue.id.suffix()));
let mut stored = issue.clone();
if let Some(existing) = entries
.iter_mut()
.find(|e| e.result.as_ref().map(|i| i.id == issue.id).unwrap_or(false))
{
stored.location = existing.location.clone();
existing.result = Ok(stored);
} else {
stored.location = location.clone();
entries.push(FakeEntry {
location,
result: Ok(stored),
});
}
Ok(())
}
fn list(&self) -> anyhow::Result<crate::domain::model::issue::IssueCollection> {
Ok(self
.entries
.borrow()
.iter()
.filter_map(|e| e.result.as_ref().ok().cloned())
.collect())
}
fn find_by_id(&self, id: &IssueRef) -> anyhow::Result<Option<Issue>> {
Ok(self
.entries
.borrow()
.iter()
.filter_map(|e| e.result.as_ref().ok())
.find(|i| &i.id == id)
.cloned())
}
fn issue_companions(&self, _id: &IssueRef) -> anyhow::Result<IssueCompanions> {
Ok(IssueCompanions::new())
}
fn read_companion(
&self,
_id: &IssueRef,
_identifier: &CompanionIdentifier,
) -> anyhow::Result<Option<CompanionContent>> {
Ok(None)
}
fn configured_id_prefix(&self) -> Option<&str> {
self.id_prefix.as_deref()
}
}
impl crate::domain::usecases::entry_defect_scanner::EntryDefectScanner for FakeIssueRepository {
fn scan(&self) -> anyhow::Result<Vec<crate::domain::model::malformed_entry::MalformedEntry>> {
use crate::domain::model::load_defect::LoadDefect;
use crate::domain::model::malformed_entry::MalformedEntry;
Ok(self
.entries
.borrow()
.iter()
.filter_map(|e| {
e.result.as_ref().err().map(|reason| MalformedEntry {
location: e.location.clone(),
origin: EntryOrigin::Local,
defect: LoadDefect::InvalidFrontmatter {
reason: reason.clone(),
},
})
})
.collect())
}
}
pub mod contract {
use super::*;
use crate::domain::model::issue::test_fixtures::{make_issue, st};
fn an_issue(n: u64) -> Issue {
make_issue(n, "Title", st("open"))
}
pub fn empty_list_yields_no_issues<R: IssueRepository>(repo: R) {
assert!(repo.list().unwrap().is_empty());
}
pub fn find_by_id_unknown_returns_none<R: IssueRepository>(repo: R) {
assert!(repo.find_by_id(&ir(404)).unwrap().is_none());
}
pub fn saved_issue_is_findable_by_id<R: IssueRepository>(repo: R) {
let issue = an_issue(1);
repo.save(&issue).unwrap();
let found = repo.find_by_id(&issue.id).unwrap().expect("expected Some");
assert_eq!(found.id, issue.id);
assert_eq!(found.title, issue.title);
}
pub fn saved_issue_appears_in_list<R: IssueRepository>(repo: R) {
let issue = an_issue(1);
repo.save(&issue).unwrap();
let list = repo.list().unwrap();
assert!(list.iter().any(|i| i.id == issue.id));
}
pub fn saving_twice_keeps_latest<R: IssueRepository>(repo: R) {
let mut issue = an_issue(1);
repo.save(&issue).unwrap();
issue.title = crate::domain::model::title::Title::new("Updated").unwrap();
repo.save(&issue).unwrap();
let found = repo.find_by_id(&issue.id).unwrap().unwrap();
assert_eq!(found.title.as_str(), "Updated");
}
pub fn configured_id_prefix_defaults_to_none<R: IssueRepository>(repo: R) {
assert!(repo.configured_id_prefix().is_none());
}
}
pub fn check_issue_repository_contract<R, F>(make: F)
where
R: IssueRepository,
F: Fn() -> R,
{
contract::empty_list_yields_no_issues(make());
contract::find_by_id_unknown_returns_none(make());
contract::saved_issue_is_findable_by_id(make());
contract::saved_issue_appears_in_list(make());
contract::saving_twice_keeps_latest(make());
}
#[cfg(test)]
mod fake_contract {
use super::*;
#[test]
fn fake_issue_repository_satisfies_the_contract() {
check_issue_repository_contract(FakeIssueRepository::new);
}
#[test]
fn fake_issue_repository_default_has_no_id_prefix() {
contract::configured_id_prefix_defaults_to_none(FakeIssueRepository::new());
}
}