From 96f15ab51e31064e649b16004d499dcee71a441b Mon Sep 17 00:00:00 2001 From: derfenix Date: Mon, 8 Sep 2025 14:10:52 +0300 Subject: [PATCH] Initial commit --- .gitignore | 2 + Cargo.lock | 897 +++++++++++++++++++ Cargo.toml | 23 + src/application/application.rs | 36 + src/application/config.rs | 15 + src/application/loaders/fs.rs | 75 ++ src/application/loaders/inotify.rs | 81 ++ src/application/loaders/mod.rs | 2 + src/application/mod.rs | 5 + src/application/parsers/mod.rs | 16 + src/application/parsers/rs.rs | 15 + src/application/services/books.rs | 87 ++ src/application/services/mod.rs | 1 + src/domain/author.rs | 85 ++ src/domain/book.rs | 65 ++ src/domain/feed.rs | 101 +++ src/domain/mod.rs | 4 + src/domain/repository.rs | 24 + src/infrastructure/mod.rs | 1 + src/infrastructure/repository/inmem/books.rs | 251 ++++++ src/infrastructure/repository/inmem/mod.rs | 1 + src/infrastructure/repository/mod.rs | 1 + src/lib.rs | 14 + src/main.rs | 46 + 24 files changed, 1848 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/application/application.rs create mode 100644 src/application/config.rs create mode 100644 src/application/loaders/fs.rs create mode 100644 src/application/loaders/inotify.rs create mode 100644 src/application/loaders/mod.rs create mode 100644 src/application/mod.rs create mode 100644 src/application/parsers/mod.rs create mode 100644 src/application/parsers/rs.rs create mode 100644 src/application/services/books.rs create mode 100644 src/application/services/mod.rs create mode 100644 src/domain/author.rs create mode 100644 src/domain/book.rs create mode 100644 src/domain/feed.rs create mode 100644 src/domain/mod.rs create mode 100644 src/domain/repository.rs create mode 100644 src/infrastructure/mod.rs create mode 100644 src/infrastructure/repository/inmem/books.rs create mode 100644 src/infrastructure/repository/inmem/mod.rs create mode 100644 src/infrastructure/repository/mod.rs create mode 100644 src/lib.rs create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2a0038a --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +.idea \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..cf0575d --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,897 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + +[[package]] +name = "bitflags" +version = "2.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "cfg-if" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" + +[[package]] +name = "convert_case" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "envman" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "197b85007754a50a296eb8e8c18f5fdf8c042c4476d718094dcc462aab6aaa69" +dependencies = [ + "envman_derive", + "thiserror", +] + +[[package]] +name = "envman_derive" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad04bc2ce6745a02566c866bb3a37e43b2d6331cdbe6e3b6d6eae9f8b7aa026" +dependencies = [ + "envman_derive_internals", + "syn", +] + +[[package]] +name = "envman_derive_internals" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c591cb2983452ece83360a86b7bf46ad5e9230678231fa30c0e9f348e462c337" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.3+wasi-0.2.4", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "inotify" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" +dependencies = [ + "bitflags", + "futures-core", + "inotify-sys", + "libc", + "tokio", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + +[[package]] +name = "io-uring" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0b063578492ceec17683ef2f8c5e89121fbd0b172cbc280635ab7567db2738" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.175" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "opds" +version = "0.1.0" +dependencies = [ + "envman", + "inotify", + "quick-xml", + "serde", + "url", + "uuid", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "potential_utf" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.38.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.47.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +dependencies = [ + "backtrace", + "io-uring", + "libc", + "mio", + "pin-project-lite", + "slab", + "socket2", + "windows-sys", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +dependencies = [ + "getrandom", + "js-sys", + "rand", + "sha1_smol", + "uuid-macro-internal", + "wasm-bindgen", + "zerocopy", +] + +[[package]] +name = "uuid-macro-internal" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9384a660318abfbd7f8932c34d67e4d1ec511095f95972ddc01e19d7ba8413f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.3+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51ae83037bdd272a9e28ce236db8c07016dd0d50c27038b3f407533c030c95" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e14915cadd45b529bb8d1f343c4ed0ac1de926144b746e2710f9cd05df6603b" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28d1ba982ca7923fd01448d5c30c6864d0a14109560296a162f80f305fb93bb" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c3d463ae3eff775b0c45df9da45d68837702ac35af998361e2c84e7c5ec1b0d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb4ce89b08211f923caf51d527662b75bdc9c9c7aab40f86dcb9fb85ac552aa" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f143854a3b13752c6950862c906306adb27c7e839f7414cec8fea35beab624c1" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "052283831dbae3d879dc7f51f3d92703a316ca49f91540417d38591826127814" + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..f267e3e --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "opds" +version = "0.1.0" +edition = "2021" + +[dependencies] +url = "2" +inotify = "0.11.0" +serde = { version = "1.0.219", features = ["derive"] } +envman = "2.0.0" +uuid = { version = "1.18.1", features = [ + "v4", + "v7", + "v5", + "fast-rng", + "macro-diagnostics", + "zerocopy", + "rng", +] } +quick-xml = { version = "0.38.3", features = ["serialize", "serde"] } + +[profile.release] +lto = "fat" \ No newline at end of file diff --git a/src/application/application.rs b/src/application/application.rs new file mode 100644 index 0000000..0233a41 --- /dev/null +++ b/src/application/application.rs @@ -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> { + pub books: Books, + cfg: Config, +} + +impl +'static> Application { + 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(()) + } +} diff --git a/src/application/config.rs b/src/application/config.rs new file mode 100644 index 0000000..1773e25 --- /dev/null +++ b/src/application/config.rs @@ -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") + } +} diff --git a/src/application/loaders/fs.rs b/src/application/loaders/fs.rs new file mode 100644 index 0000000..4a08a1d --- /dev/null +++ b/src/application/loaders/fs.rs @@ -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, +} + +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 { + 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 { + 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 + } + } + } +} diff --git a/src/application/loaders/inotify.rs b/src/application/loaders/inotify.rs new file mode 100644 index 0000000..74af8fc --- /dev/null +++ b/src/application/loaders/inotify.rs @@ -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, + queue: VecDeque, +} + +impl Loader { + pub fn new(root: PathBuf) -> io::Result { + 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 { + 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 { + 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 + } + } + } +} diff --git a/src/application/loaders/mod.rs b/src/application/loaders/mod.rs new file mode 100644 index 0000000..e1b869f --- /dev/null +++ b/src/application/loaders/mod.rs @@ -0,0 +1,2 @@ +pub mod fs; +pub mod inotify; \ No newline at end of file diff --git a/src/application/mod.rs b/src/application/mod.rs new file mode 100644 index 0000000..76e4f69 --- /dev/null +++ b/src/application/mod.rs @@ -0,0 +1,5 @@ +pub mod services; +mod loaders; +mod parsers; +mod config; +pub mod application; diff --git a/src/application/parsers/mod.rs b/src/application/parsers/mod.rs new file mode 100644 index 0000000..6ad2cfb --- /dev/null +++ b/src/application/parsers/mod.rs @@ -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 { + match path.extension().and_then(|s| s.to_str()) { + Some("rs") => rs::parse(path).map_err(Error::ParseError), + Some(_) | None => Err(Error::NotSupported), + } +} diff --git a/src/application/parsers/rs.rs b/src/application/parsers/rs.rs new file mode 100644 index 0000000..352f504 --- /dev/null +++ b/src/application/parsers/rs.rs @@ -0,0 +1,15 @@ +use crate::domain::author::Author; +use crate::domain::book::Book; +use std::path::PathBuf; + +pub fn parse(path: &PathBuf) -> Result { + 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); +} diff --git a/src/application/services/books.rs b/src/application/services/books.rs new file mode 100644 index 0000000..969a76c --- /dev/null +++ b/src/application/services/books.rs @@ -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> { + pub repo: Arc>, + root: PathBuf, + base_url: Url, +} + +impl> Books +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 { + let mut res = self + .repo + .lock() + .unwrap() + .filter(filter) + .map(|book| Entry::from(&book)) + .collect::>(); + + 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(()) + } +} diff --git a/src/application/services/mod.rs b/src/application/services/mod.rs new file mode 100644 index 0000000..61f4d25 --- /dev/null +++ b/src/application/services/mod.rs @@ -0,0 +1 @@ +pub mod books; diff --git a/src/domain/author.rs b/src/domain/author.rs new file mode 100644 index 0000000..7d44f7e --- /dev/null +++ b/src/domain/author.rs @@ -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, + pub middle_name: Option, +} + +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(" ")) + } +} + diff --git a/src/domain/book.rs b/src/domain/book.rs new file mode 100644 index 0000000..4ac5f04 --- /dev/null +++ b/src/domain/book.rs @@ -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, + pub language: String, + pub description: String, + pub tags: Vec, + 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::>() + .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::>() + .join(";"); + + write!(f, "{} by {}", self.title, authors) + } +} + diff --git a/src/domain/feed.rs b/src/domain/feed.rs new file mode 100644 index 0000000..3eed4bb --- /dev/null +++ b/src/domain/feed.rs @@ -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, + #[serde(default)] + pub entry: Vec, + #[serde(default)] + pub link: Vec, +} + +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 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, + #[serde(rename = "href")] + pub href: String, + #[serde(rename = "type", default)] + pub media_type: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename = "entry")] +pub struct Entry { + pub title: String, + pub id: String, + pub updated: String, + pub author: Vec, + #[serde(rename = "dc:language", default)] + pub language: Option, + #[serde(rename = "dc:issued", default)] + pub issued: Option, + #[serde(default)] + pub summary: Option, + #[serde(default)] + pub link: Vec, // 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![], + } + } +} \ No newline at end of file diff --git a/src/domain/mod.rs b/src/domain/mod.rs new file mode 100644 index 0000000..4dafd25 --- /dev/null +++ b/src/domain/mod.rs @@ -0,0 +1,4 @@ +pub mod author; +pub mod book; +pub mod feed; +pub mod repository; diff --git a/src/domain/repository.rs b/src/domain/repository.rs new file mode 100644 index 0000000..1c8907d --- /dev/null +++ b/src/domain/repository.rs @@ -0,0 +1,24 @@ +pub trait Repository: Send + Sync + Sized{ + fn add(&mut self, item: T); + fn bulk_add(&mut self, items: I) where I: IntoIterator; + fn remove(&mut self, item: T); + fn get(&self, id: String) -> Option; + fn update(&mut self, item: T); + fn filter(&self, filter: F) -> Box>; +} + +pub struct BookFilter { + pub author: Option, + pub title: Option, + pub language: Option, + pub description: Option, + pub tags: Option>, + pub published_at: Option, + pub publisher: Option, + pub updated: Option, +} + +pub struct AuthorFilter { + pub id: Option, + pub name: Option, +} \ No newline at end of file diff --git a/src/infrastructure/mod.rs b/src/infrastructure/mod.rs new file mode 100644 index 0000000..4e69383 --- /dev/null +++ b/src/infrastructure/mod.rs @@ -0,0 +1 @@ +pub mod repository; \ No newline at end of file diff --git a/src/infrastructure/repository/inmem/books.rs b/src/infrastructure/repository/inmem/books.rs new file mode 100644 index 0000000..1148659 --- /dev/null +++ b/src/infrastructure/repository/inmem/books.rs @@ -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, + language: String, + description: String, + tags: Vec, + published_at: String, + publisher: String, + updated: String, +} + +impl From 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 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, + authors: HashMap, + author_uniques: HashMap, +} + +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 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(&mut self, items: I) + where + I: IntoIterator, + { + 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 { + let id_uuid: Uuid = id.parse().unwrap(); + + let mut book: Option = 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> { + let mut author_ids: Vec = 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::()) + } + + 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::>(); + + for book in &mut res { + self.populate_authors(book); + } + + Box::new(res.into_iter()) + } +} diff --git a/src/infrastructure/repository/inmem/mod.rs b/src/infrastructure/repository/inmem/mod.rs new file mode 100644 index 0000000..61f4d25 --- /dev/null +++ b/src/infrastructure/repository/inmem/mod.rs @@ -0,0 +1 @@ +pub mod books; diff --git a/src/infrastructure/repository/mod.rs b/src/infrastructure/repository/mod.rs new file mode 100644 index 0000000..acca1c0 --- /dev/null +++ b/src/infrastructure/repository/mod.rs @@ -0,0 +1 @@ +pub mod inmem; diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..a35a2b2 --- /dev/null +++ b/src/lib.rs @@ -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 { + let mut app = Application::new(BookRepository::new()); + app.start().expect("Application initialization failed"); + + app +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..81c4d8b --- /dev/null +++ b/src/main.rs @@ -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()); +}