Initial commit
This commit is contained in:
36
src/application/application.rs
Normal file
36
src/application/application.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use crate::application::services::books::Books;
|
||||
use crate::application::config::Config;
|
||||
use crate::domain::book::Book;
|
||||
use crate::domain::repository::{BookFilter, Repository};
|
||||
|
||||
pub struct Application<R: Repository<Book, BookFilter>> {
|
||||
pub books: Books<R>,
|
||||
cfg: Config,
|
||||
}
|
||||
|
||||
impl<R: Repository<Book, BookFilter> +'static> Application<R> {
|
||||
pub fn new(repo: R) -> Self {
|
||||
let cfg = Config::new();
|
||||
|
||||
Application {
|
||||
books: Books::new(repo, (&cfg.books_dir).into(), "https://foo.bar/".to_string()),
|
||||
|
||||
cfg
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(&mut self) -> Result<(), String>{
|
||||
self.books.add_books_from_path();
|
||||
|
||||
if self.cfg.watcher {
|
||||
return match self.books.watch_dir() {
|
||||
Ok(_) => {Ok(())}
|
||||
Err(e) => {
|
||||
Err(format!("Error start watching books: {}", e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
15
src/application/config.rs
Normal file
15
src/application/config.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
use envman::EnvMan;
|
||||
|
||||
#[derive(EnvMan)]
|
||||
pub struct Config {
|
||||
#[envman(default = "./")]
|
||||
pub books_dir: String,
|
||||
#[envman(default = "true")]
|
||||
pub watcher: bool,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn new() -> Self {
|
||||
Config::load_from_env().expect("Failed to load configuration")
|
||||
}
|
||||
}
|
||||
75
src/application/loaders/fs.rs
Normal file
75
src/application/loaders/fs.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
use crate::application::parsers;
|
||||
use crate::domain::book::Book;
|
||||
use std::collections::VecDeque;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub struct Loader {
|
||||
root: PathBuf,
|
||||
}
|
||||
|
||||
pub struct LoaderIter {
|
||||
queue: VecDeque<PathBuf>,
|
||||
}
|
||||
|
||||
impl Loader {
|
||||
pub fn new(root: PathBuf) -> Self {
|
||||
Loader { root }
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for Loader {
|
||||
type Item = Book;
|
||||
type IntoIter = LoaderIter;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
let mut queue = VecDeque::new();
|
||||
queue.push_back(self.root);
|
||||
LoaderIter { queue }
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for LoaderIter {
|
||||
type Item = Book;
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
while let Some(path) = self.queue.pop_front() {
|
||||
match path.is_dir() {
|
||||
true => {
|
||||
if let Ok(entries) = fs::read_dir(&path) {
|
||||
for entry in entries.flatten() {
|
||||
self.queue.push_back(entry.path());
|
||||
}
|
||||
}
|
||||
}
|
||||
false => {
|
||||
let book = Self::parse_path(&path);
|
||||
if book.is_some() {
|
||||
return book;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl LoaderIter {
|
||||
fn parse_path(path: &PathBuf) -> Option<Book> {
|
||||
match parsers::parse(&path) {
|
||||
Ok(book) => return Some(book),
|
||||
Err(err) => {
|
||||
match err {
|
||||
parsers::Error::ParseError(err) => {
|
||||
println!("Failed to load book at {}: {:?}", path.display(), err);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
81
src/application/loaders/inotify.rs
Normal file
81
src/application/loaders/inotify.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
use crate::application::parsers;
|
||||
use crate::domain::book::Book;
|
||||
use inotify::{Event, Inotify, WatchMask};
|
||||
use std::collections::VecDeque;
|
||||
use std::ffi::OsStr;
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
|
||||
const BUFFER_SIZE: usize = 4096;
|
||||
|
||||
pub struct Loader {
|
||||
inotify: Inotify,
|
||||
}
|
||||
|
||||
pub struct LoaderIter<'a> {
|
||||
inotify: &'a mut Inotify,
|
||||
buf: Vec<u8>,
|
||||
queue: VecDeque<Book>,
|
||||
}
|
||||
|
||||
impl Loader {
|
||||
pub fn new(root: PathBuf) -> io::Result<Self> {
|
||||
let mask = WatchMask::MODIFY | WatchMask::CREATE;
|
||||
let inotify = Inotify::init()?;
|
||||
|
||||
inotify.watches().add(&root, mask)?;
|
||||
Ok(Loader { inotify })
|
||||
}
|
||||
|
||||
pub fn iter(&mut self) -> LoaderIter<'_> {
|
||||
LoaderIter {
|
||||
inotify: &mut self.inotify,
|
||||
buf: vec![0u8; BUFFER_SIZE],
|
||||
queue: VecDeque::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for LoaderIter<'a> {
|
||||
type Item = Book;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if let Some(book) = self.queue.pop_front() {
|
||||
return Some(book);
|
||||
}
|
||||
|
||||
match self.inotify.read_events_blocking(&mut self.buf) {
|
||||
Ok(events) => {
|
||||
for ev in events {
|
||||
println!("{:?}", ev);
|
||||
if let Some(book) = Self::process_event(ev) {
|
||||
println!("{}", book);
|
||||
self.queue.push_back(book);
|
||||
}
|
||||
}
|
||||
self.queue.pop_front()
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("inotify error: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> LoaderIter<'a> {
|
||||
fn process_event(event: Event<&OsStr>) -> Option<Book> {
|
||||
let path = PathBuf::from(event.name?);
|
||||
if !path.exists() {
|
||||
return None;
|
||||
}
|
||||
|
||||
match parsers::parse(&path) {
|
||||
Ok(book) => Some(book),
|
||||
Err(err) => {
|
||||
eprintln!("Failed to parse book from {:?}: {:?}", path, err);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
src/application/loaders/mod.rs
Normal file
2
src/application/loaders/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod fs;
|
||||
pub mod inotify;
|
||||
5
src/application/mod.rs
Normal file
5
src/application/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod services;
|
||||
mod loaders;
|
||||
mod parsers;
|
||||
mod config;
|
||||
pub mod application;
|
||||
16
src/application/parsers/mod.rs
Normal file
16
src/application/parsers/mod.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use crate::domain::book::Book;
|
||||
use std::path::PathBuf;
|
||||
|
||||
mod rs;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
NotSupported,
|
||||
ParseError(String),
|
||||
}
|
||||
pub fn parse(path: &PathBuf) -> Result<Book, Error> {
|
||||
match path.extension().and_then(|s| s.to_str()) {
|
||||
Some("rs") => rs::parse(path).map_err(Error::ParseError),
|
||||
Some(_) | None => Err(Error::NotSupported),
|
||||
}
|
||||
}
|
||||
15
src/application/parsers/rs.rs
Normal file
15
src/application/parsers/rs.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
use crate::domain::author::Author;
|
||||
use crate::domain::book::Book;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub fn parse(path: &PathBuf) -> Result<Book, String> {
|
||||
let mut book = Book::new();
|
||||
|
||||
book.title = path.to_string_lossy().to_string();
|
||||
|
||||
let mut author = Author::new();
|
||||
author.first_name = path.extension().unwrap().to_string_lossy().to_string();
|
||||
book.author.push(author);
|
||||
|
||||
return Ok(book);
|
||||
}
|
||||
87
src/application/services/books.rs
Normal file
87
src/application/services/books.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
use crate::application::loaders::fs;
|
||||
use crate::application::loaders::inotify;
|
||||
use crate::domain::book::Book;
|
||||
use crate::domain::feed::{BooksFeed, Entry};
|
||||
use crate::domain::repository::{BookFilter, Repository};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::{io, thread};
|
||||
use url::Url;
|
||||
|
||||
const AUTHOR_URL_PREFIX: &str = "author";
|
||||
|
||||
pub struct Books<R: Repository<Book, BookFilter>> {
|
||||
pub repo: Arc<Mutex<R>>,
|
||||
root: PathBuf,
|
||||
base_url: Url,
|
||||
}
|
||||
|
||||
impl<R: Repository<Book, BookFilter>> Books<R>
|
||||
where
|
||||
R: 'static,
|
||||
{
|
||||
pub fn new(repo: R, root: PathBuf, base_url: String) -> Self {
|
||||
Books {
|
||||
repo: Arc::new(Mutex::new(repo)),
|
||||
root,
|
||||
base_url: Url::parse(&base_url).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
fn book_entries(&self, filter: BookFilter) -> Vec<Entry> {
|
||||
let mut res = self
|
||||
.repo
|
||||
.lock()
|
||||
.unwrap()
|
||||
.filter(filter)
|
||||
.map(|book| Entry::from(&book))
|
||||
.collect::<Vec<Entry>>();
|
||||
|
||||
for entry in &mut res {
|
||||
for author in &mut entry.author {
|
||||
author.url = self.build_author_url(author.url.as_str()).to_string();
|
||||
}
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
fn build_author_url(&self, author_url: &str) -> Url {
|
||||
let mut url = self.base_url.clone();
|
||||
|
||||
match url.join([AUTHOR_URL_PREFIX, author_url].join("/").as_str()) {
|
||||
Ok(u) => url = u,
|
||||
Err(err) => {
|
||||
println!("{}", err);
|
||||
}
|
||||
}
|
||||
url
|
||||
}
|
||||
|
||||
pub fn books_feed(&self, filter: BookFilter) -> BooksFeed {
|
||||
let mut feed: BooksFeed = Default::default();
|
||||
|
||||
feed.entry = self.book_entries(filter);
|
||||
|
||||
feed
|
||||
}
|
||||
|
||||
pub fn add_books_from_path(&mut self) {
|
||||
let iter = fs::Loader::new(PathBuf::from(&self.root));
|
||||
self.repo.lock().unwrap().bulk_add(iter);
|
||||
}
|
||||
|
||||
pub fn watch_dir(&mut self) -> Result<(), io::Error> {
|
||||
let root = self.root.clone();
|
||||
let repo = Arc::clone(&self.repo);
|
||||
let mut loader = inotify::Loader::new(root.clone())?;
|
||||
|
||||
thread::spawn(move || loop {
|
||||
for book in loader.iter() {
|
||||
repo.lock().unwrap().add(book);
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
1
src/application/services/mod.rs
Normal file
1
src/application/services/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod books;
|
||||
85
src/domain/author.rs
Normal file
85
src/domain/author.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
use std::fmt;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub struct Author {
|
||||
pub id: Uuid,
|
||||
pub first_name: String,
|
||||
pub last_name: Option<String>,
|
||||
pub middle_name: Option<String>,
|
||||
}
|
||||
|
||||
impl Author {
|
||||
pub fn new() -> Author {
|
||||
Author{
|
||||
id: Uuid::new_v4(),
|
||||
first_name: "".to_string(),
|
||||
last_name: None,
|
||||
middle_name: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name_contains(&self, name: &str) -> bool {
|
||||
let name = name.to_lowercase();
|
||||
let first_name = self.first_name.to_lowercase();
|
||||
let last_name = self.last_name.as_ref().map(|s| s.to_lowercase());
|
||||
let middle_name = self.middle_name.as_ref().map(|s| s.to_lowercase());
|
||||
|
||||
name.contains(&first_name) ||
|
||||
last_name.map_or(false, |s| s.contains(&name)) ||
|
||||
middle_name.map_or(false, |s| s.contains(&name))
|
||||
}
|
||||
|
||||
pub fn uniq_id(&self) -> Uuid {
|
||||
let mut parts = Vec::new();
|
||||
|
||||
if let Some(ref last) = self.last_name {
|
||||
if !last.is_empty() {
|
||||
parts.push(last.as_str());
|
||||
}
|
||||
}
|
||||
|
||||
if !self.first_name.is_empty() {
|
||||
parts.push(self.first_name.as_str());
|
||||
}
|
||||
|
||||
if let Some(ref middle) = self.middle_name {
|
||||
if !middle.is_empty() {
|
||||
parts.push(middle.as_str());
|
||||
}
|
||||
}
|
||||
|
||||
Uuid::new_v5(&Uuid::NAMESPACE_URL, parts.join(" ").as_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Author {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Author {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut parts = Vec::new();
|
||||
|
||||
if let Some(ref last) = self.last_name {
|
||||
if !last.is_empty() {
|
||||
parts.push(last.as_str());
|
||||
}
|
||||
}
|
||||
|
||||
if !self.first_name.is_empty() {
|
||||
parts.push(self.first_name.as_str());
|
||||
}
|
||||
|
||||
if let Some(ref middle) = self.middle_name {
|
||||
if !middle.is_empty() {
|
||||
parts.push(middle.as_str());
|
||||
}
|
||||
}
|
||||
|
||||
write!(f, "{}", parts.join(" "))
|
||||
}
|
||||
}
|
||||
|
||||
65
src/domain/book.rs
Normal file
65
src/domain/book.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
use crate::domain::author;
|
||||
use std::fmt;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct Book {
|
||||
pub id: Uuid,
|
||||
pub title: String,
|
||||
pub author: Vec<author::Author>,
|
||||
pub language: String,
|
||||
pub description: String,
|
||||
pub tags: Vec<String>,
|
||||
pub published_at: String,
|
||||
pub publisher: String,
|
||||
pub updated: String,
|
||||
}
|
||||
|
||||
impl Book {
|
||||
pub fn new() -> Book {
|
||||
Book {
|
||||
id: Uuid::new_v4(),
|
||||
title: "".to_string(),
|
||||
author: Default::default(),
|
||||
language: "".to_string(),
|
||||
description: "".to_string(),
|
||||
tags: vec![],
|
||||
published_at: "".to_string(),
|
||||
publisher: "".to_string(),
|
||||
updated: "".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn uniq_id(&self) -> Uuid {
|
||||
let mut parts = Vec::new();
|
||||
parts.push(self.title.as_str());
|
||||
|
||||
let authors = self.author.iter()
|
||||
.map(|a| a.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(";");
|
||||
parts.push(authors.as_str());
|
||||
|
||||
parts.push(self.language.as_str());
|
||||
parts.push(self.publisher.as_str());
|
||||
parts.push(self.published_at.as_str());
|
||||
|
||||
Uuid::new_v5(&Uuid::NAMESPACE_OID, parts.join("-").as_bytes())
|
||||
}
|
||||
|
||||
pub fn same(&self, other: &Book) -> bool {
|
||||
self.uniq_id() == other.uniq_id()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Book {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> fmt::Result {
|
||||
let authors = self.author.iter()
|
||||
.map(|a| a.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(";");
|
||||
|
||||
write!(f, "{} by {}", self.title, authors)
|
||||
}
|
||||
}
|
||||
|
||||
101
src/domain/feed.rs
Normal file
101
src/domain/feed.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::domain::author;
|
||||
use crate::domain::book::Book;
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename = "feed")]
|
||||
pub struct BooksFeed {
|
||||
#[serde(rename = "@xmlns", default = "default_atom_ns")]
|
||||
pub xmlns: String,
|
||||
|
||||
#[serde(rename = "@xmlns:dc", default = "default_dc_ns")]
|
||||
pub xmlns_dc: String,
|
||||
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
pub updated: String,
|
||||
#[serde(default)]
|
||||
pub author: Option<Author>,
|
||||
#[serde(default)]
|
||||
pub entry: Vec<Entry>,
|
||||
#[serde(default)]
|
||||
pub link: Vec<Link>,
|
||||
}
|
||||
|
||||
impl Default for BooksFeed {
|
||||
fn default() -> Self {
|
||||
BooksFeed {
|
||||
xmlns: default_atom_ns(),
|
||||
xmlns_dc: default_dc_ns(),
|
||||
id: Default::default(),
|
||||
title: Default::default(),
|
||||
updated: Default::default(),
|
||||
author: Default::default(),
|
||||
entry: Default::default(),
|
||||
link: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_atom_ns() -> String {
|
||||
"http://www.w3.org/2005/Atom".to_string()
|
||||
}
|
||||
fn default_dc_ns() -> String {
|
||||
"http://purl.org/dc/terms/".to_string()
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Author {
|
||||
pub name: String,
|
||||
pub url: String
|
||||
}
|
||||
|
||||
impl From<author::Author> for Author {
|
||||
fn from(value: author::Author) -> Self {
|
||||
Author {
|
||||
name: value.to_string(),
|
||||
url: value.id.to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Link {
|
||||
#[serde(rename = "rel", default)]
|
||||
pub rel: Option<String>,
|
||||
#[serde(rename = "href")]
|
||||
pub href: String,
|
||||
#[serde(rename = "type", default)]
|
||||
pub media_type: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename = "entry")]
|
||||
pub struct Entry {
|
||||
pub title: String,
|
||||
pub id: String,
|
||||
pub updated: String,
|
||||
pub author: Vec<Author>,
|
||||
#[serde(rename = "dc:language", default)]
|
||||
pub language: Option<String>,
|
||||
#[serde(rename = "dc:issued", default)]
|
||||
pub issued: Option<String>,
|
||||
#[serde(default)]
|
||||
pub summary: Option<String>,
|
||||
#[serde(default)]
|
||||
pub link: Vec<Link>, // acquisition, cover, etc.
|
||||
}
|
||||
|
||||
impl From<&Book> for Entry {
|
||||
fn from(book: &Book) -> Self {
|
||||
Entry{
|
||||
title: book.title.clone(),
|
||||
id: book.id.to_string().clone(),
|
||||
updated: book.updated.clone(),
|
||||
author: book.author.clone().into_iter().map(|a| a.into()).collect(),
|
||||
language: (!book.language.is_empty()).then(|| book.language.clone()),
|
||||
issued: (!book.published_at.is_empty()).then(|| book.published_at.clone()),
|
||||
summary: (!book.description.is_empty()).then(|| book.description.clone()),
|
||||
link: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
4
src/domain/mod.rs
Normal file
4
src/domain/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod author;
|
||||
pub mod book;
|
||||
pub mod feed;
|
||||
pub mod repository;
|
||||
24
src/domain/repository.rs
Normal file
24
src/domain/repository.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
pub trait Repository<T, F>: Send + Sync + Sized{
|
||||
fn add(&mut self, item: T);
|
||||
fn bulk_add<I>(&mut self, items: I) where I: IntoIterator<Item = T>;
|
||||
fn remove(&mut self, item: T);
|
||||
fn get(&self, id: String) -> Option<T>;
|
||||
fn update(&mut self, item: T);
|
||||
fn filter(&self, filter: F) -> Box<dyn Iterator<Item = T>>;
|
||||
}
|
||||
|
||||
pub struct BookFilter {
|
||||
pub author: Option<AuthorFilter>,
|
||||
pub title: Option<String>,
|
||||
pub language: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub tags: Option<Vec<String>>,
|
||||
pub published_at: Option<String>,
|
||||
pub publisher: Option<String>,
|
||||
pub updated: Option<String>,
|
||||
}
|
||||
|
||||
pub struct AuthorFilter {
|
||||
pub id: Option<String>,
|
||||
pub name: Option<String>,
|
||||
}
|
||||
1
src/infrastructure/mod.rs
Normal file
1
src/infrastructure/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod repository;
|
||||
251
src/infrastructure/repository/inmem/books.rs
Normal file
251
src/infrastructure/repository/inmem/books.rs
Normal file
@@ -0,0 +1,251 @@
|
||||
use crate::domain::author::Author;
|
||||
use crate::domain::repository::{BookFilter, Repository};
|
||||
use crate::domain::{author, book};
|
||||
use std::collections::HashMap;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Book {
|
||||
id: Uuid,
|
||||
title: String,
|
||||
author: Vec<String>,
|
||||
language: String,
|
||||
description: String,
|
||||
tags: Vec<String>,
|
||||
published_at: String,
|
||||
publisher: String,
|
||||
updated: String,
|
||||
}
|
||||
|
||||
impl From<book::Book> for Book {
|
||||
fn from(book: book::Book) -> Self {
|
||||
Book {
|
||||
id: book.id,
|
||||
title: book.title,
|
||||
author: book.author.iter().map(|a| a.id.to_string()).collect(),
|
||||
language: book.language,
|
||||
description: book.description,
|
||||
tags: book.tags,
|
||||
published_at: book.published_at,
|
||||
publisher: book.publisher,
|
||||
updated: book.updated,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<book::Book> for Book {
|
||||
fn into(self) -> book::Book {
|
||||
book::Book {
|
||||
id: self.id,
|
||||
title: self.title,
|
||||
author: self
|
||||
.author
|
||||
.iter()
|
||||
.map(|a| {
|
||||
let mut na: author::Author = Default::default();
|
||||
if let Ok(id) = Uuid::parse_str(a) {
|
||||
na.id = id;
|
||||
}
|
||||
na
|
||||
})
|
||||
.collect(),
|
||||
language: self.language,
|
||||
description: self.description,
|
||||
tags: self.tags,
|
||||
published_at: self.published_at,
|
||||
publisher: self.publisher,
|
||||
updated: self.updated,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BookRepository {
|
||||
books: Vec<Book>,
|
||||
authors: HashMap<Uuid, Author>,
|
||||
author_uniques: HashMap<String, Uuid>,
|
||||
}
|
||||
|
||||
impl BookRepository {
|
||||
pub fn new() -> Self {
|
||||
BookRepository {
|
||||
books: vec![],
|
||||
authors: HashMap::new(),
|
||||
author_uniques: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn populate_authors(&self, book: &mut book::Book) {
|
||||
for a in &mut book.author {
|
||||
if let Some(stored) = self.authors.get(&a.id) {
|
||||
a.first_name = stored.first_name.clone();
|
||||
a.last_name = stored.last_name.clone();
|
||||
a.middle_name = stored.middle_name.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_authors(&mut self, item: &mut book::Book) {
|
||||
for a in item.author.iter_mut() {
|
||||
let uniq = a.uniq_id().to_string();
|
||||
|
||||
if let Some(&id) = self.author_uniques.get(&uniq) {
|
||||
a.id = id;
|
||||
} else {
|
||||
self.author_uniques.insert(uniq, a.id);
|
||||
}
|
||||
|
||||
self.authors.insert(a.id, a.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Repository<book::Book, BookFilter> for BookRepository {
|
||||
fn add(&mut self, mut item: book::Book) {
|
||||
if self.get(item.id.to_string()).is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.extract_authors(&mut item);
|
||||
|
||||
self.books.push(item.into());
|
||||
}
|
||||
|
||||
fn bulk_add<I>(&mut self, items: I)
|
||||
where
|
||||
I: IntoIterator<Item = book::Book>,
|
||||
{
|
||||
items.into_iter().for_each(|item| {
|
||||
if self.get(item.id.to_string()).is_none() {
|
||||
self.add(item)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn remove(&mut self, item: book::Book) {
|
||||
self.books.retain(|book| book.id != item.id);
|
||||
}
|
||||
|
||||
fn get(&self, id: String) -> Option<book::Book> {
|
||||
let id_uuid: Uuid = id.parse().unwrap();
|
||||
|
||||
let mut book: Option<book::Book> = self
|
||||
.books
|
||||
.iter()
|
||||
.cloned()
|
||||
.find(|x| x.id.eq(&id_uuid))
|
||||
.and_then(|x| Some(x.into()));
|
||||
|
||||
if book.is_none() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(book) = book.as_mut() {
|
||||
self.populate_authors(book);
|
||||
}
|
||||
|
||||
book
|
||||
}
|
||||
|
||||
fn update(&mut self, mut item: book::Book) {
|
||||
self.extract_authors(&mut item);
|
||||
|
||||
for res in &mut self.books {
|
||||
if res.id == item.id {
|
||||
*res = item.into();
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn filter(&self, f: BookFilter) -> Box<dyn Iterator<Item = book::Book>> {
|
||||
let mut author_ids: Vec<String> = vec![];
|
||||
|
||||
if let Some(author) = f.author {
|
||||
if let Some(id) = author.id {
|
||||
author_ids.push(id);
|
||||
}
|
||||
|
||||
if let Some(name) = author.name {
|
||||
for (id, author) in self.authors.iter() {
|
||||
if author.first_name.contains(&name)
|
||||
|| (author.last_name.is_some()
|
||||
&& author.clone().last_name.unwrap().contains(&name))
|
||||
|| (author.middle_name.is_some()
|
||||
&& author.clone().middle_name.unwrap().contains(&name))
|
||||
{
|
||||
author_ids.push(id.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if author_ids.is_empty() {
|
||||
return Box::new(std::iter::empty::<book::Book>())
|
||||
}
|
||||
|
||||
let mut res = self
|
||||
.books
|
||||
.iter()
|
||||
.filter(move |book| {
|
||||
// Фильтр по названию
|
||||
if let Some(ref search_title) = f.title {
|
||||
if !book.title.contains(search_title) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Фильтр по описанию
|
||||
if let Some(ref descr) = f.description {
|
||||
if !book.description.contains(descr) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Фильтр по языку
|
||||
if let Some(ref lang) = f.language {
|
||||
if !book.language.eq(lang) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Фильтр по тегам
|
||||
if let Some(ref tags) = f.tags {
|
||||
if !tags.iter().all(|tag| book.tags.contains(tag)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Фильтр по издателю
|
||||
if let Some(ref publisher) = f.publisher {
|
||||
if !book.publisher.eq(publisher) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Фильтр по датам (пример, можно доработать)
|
||||
if let Some(ref published_at) = f.published_at {
|
||||
if book.published_at != *published_at {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if let Some(ref updated) = f.updated {
|
||||
if book.updated != *updated {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if !author_ids.is_empty() {
|
||||
if !book.author.iter().all(|x| author_ids.contains(x)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
})
|
||||
.cloned()
|
||||
.map(Into::into)
|
||||
.collect::<Vec<book::Book>>();
|
||||
|
||||
for book in &mut res {
|
||||
self.populate_authors(book);
|
||||
}
|
||||
|
||||
Box::new(res.into_iter())
|
||||
}
|
||||
}
|
||||
1
src/infrastructure/repository/inmem/mod.rs
Normal file
1
src/infrastructure/repository/inmem/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod books;
|
||||
1
src/infrastructure/repository/mod.rs
Normal file
1
src/infrastructure/repository/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod inmem;
|
||||
14
src/lib.rs
Normal file
14
src/lib.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
use crate::application::application::Application;
|
||||
use crate::infrastructure::repository::inmem::books::BookRepository;
|
||||
|
||||
pub mod domain;
|
||||
|
||||
mod application;
|
||||
pub mod infrastructure;
|
||||
|
||||
pub fn demo() -> Application<BookRepository> {
|
||||
let mut app = Application::new(BookRepository::new());
|
||||
app.start().expect("Application initialization failed");
|
||||
|
||||
app
|
||||
}
|
||||
46
src/main.rs
Normal file
46
src/main.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
use opds::demo;
|
||||
use opds::domain::repository::{AuthorFilter, Repository};
|
||||
use opds::domain::repository::BookFilter;
|
||||
use quick_xml::se::to_string as to_xml_string;
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
|
||||
fn main() {
|
||||
let app = demo();
|
||||
|
||||
let filter = BookFilter {
|
||||
author: Some(AuthorFilter{
|
||||
id: None,
|
||||
name: Some("rs".to_string()),
|
||||
}),
|
||||
title: Some("service".to_string()),
|
||||
language: None,
|
||||
description: None,
|
||||
tags: None,
|
||||
published_at: None,
|
||||
publisher: None,
|
||||
updated: None,
|
||||
};
|
||||
|
||||
let res = app.books.books_feed(filter);
|
||||
println!("{}", to_xml_string(&res).unwrap());
|
||||
|
||||
if let Some(book) = res.entry.iter().next() {
|
||||
let book = app.books.repo.lock().unwrap().get(book.id.to_string().clone());
|
||||
println!("{:?}", book.unwrap().author);
|
||||
}
|
||||
|
||||
sleep(Duration::new(10, 0));
|
||||
|
||||
let filter = BookFilter {
|
||||
author: None,
|
||||
title: Some("foo".to_string()),
|
||||
language: None,
|
||||
description: None,
|
||||
tags: None,
|
||||
published_at: None,
|
||||
publisher: None,
|
||||
updated: None,
|
||||
};
|
||||
println!("{}", to_xml_string(&app.books.books_feed(filter)).unwrap());
|
||||
}
|
||||
Reference in New Issue
Block a user