diff --git a/.cargo/audit.toml b/.cargo/audit.toml index 85b91746..9db3c846 100644 --- a/.cargo/audit.toml +++ b/.cargo/audit.toml @@ -1,6 +1,7 @@ [advisories] ignore = [ "RUSTSEC-2024-0320", # ignore yaml-rust being unmaintained + "RUSTSEC-2023-0071", # marvin attack, we need to wait for a fix ] # advisory IDs to ignore e.g. ["RUSTSEC-2019-0001", ...] informational_warnings = [ "unmaintained", diff --git a/.github/codecov.yml b/.github/codecov.yml index 3fd0a6bb..e8186e6c 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -13,9 +13,6 @@ coverage: ignore: - "tests" # Test files aren't important for coverage - - "crates/testing" # Testing crate contains Test helpers which aren't important for coverage - - "crates/cli" # CLI crate contains prompts to the user, which we currently deem not important for coverage - - "src/" # Contains only a slim wrapper around pace_core based on abscissa, which we currently deem not important for coverage # Make comments less noisy comment: diff --git a/.gitignore b/.gitignore index d255760e..942620a6 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,4 @@ logs/* .cargo/config.toml # Database -data/pace.sqlite3 +db/activities.pace.sqlite3* diff --git a/Cargo.lock b/Cargo.lock index b2937ff2..3c9b4223 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -56,6 +56,30 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -65,6 +89,18 @@ dependencies = [ "memchr", ] +[[package]] +name = "aliasable" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" + +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -134,6 +170,12 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + [[package]] name = "assert_cmd" version = "2.0.14" @@ -149,6 +191,48 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "async-stream" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "async-trait" +version = "0.1.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507401cad91ec6a857ed5513a2073c82a9b9048762b885bb98655b306964681" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -179,6 +263,29 @@ dependencies = [ "backtrace", ] +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bigdecimal" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6773ddc0eafc0e509fb60e48dff7f450f8e674a0686ae8605e8d9901bd5eefa" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -190,6 +297,21 @@ name = "bitflags" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +dependencies = [ + "serde", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] [[package]] name = "block-buffer" @@ -200,6 +322,30 @@ dependencies = [ "generic-array", ] +[[package]] +name = "borsh" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0901fc8eb0aca4c83be0106d6f2db17d86a08dfc2c25f0e84464bf381158add6" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51670c3aa053938b0ee3bd67c3817e471e626151131b934038e83c5bf8de48f5" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.55", + "syn_derive", +] + [[package]] name = "bstr" version = "0.2.17" @@ -228,12 +374,46 @@ version = "3.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "bytecount" version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1e5f035d16fc623ae5f74981db80a439803888314e3a555fd6f04acd51a3205" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" + [[package]] name = "canonical-path" version = "2.0.2" @@ -252,6 +432,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + [[package]] name = "chrono" version = "0.4.35" @@ -382,6 +568,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "core-foundation-sys" version = "0.8.6" @@ -397,6 +589,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crossbeam-deque" version = "0.8.5" @@ -416,6 +623,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.19" @@ -432,6 +648,17 @@ dependencies = [ "typenum", ] +[[package]] +name = "der" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "deranged" version = "0.3.11" @@ -439,6 +666,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", + "serde", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", ] [[package]] @@ -472,39 +711,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "diesel" -version = "2.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03fc05c17098f21b89bc7d98fe1dd3cce2c11c2ad8e145f2a44fe08ed28eb559" -dependencies = [ - "chrono", - "diesel_derives", - "libsqlite3-sys", - "time", -] - -[[package]] -name = "diesel_derives" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d02eecb814ae714ffe61ddc2db2dd03e6c49a42e269b5001355500d431cce0c" -dependencies = [ - "diesel_table_macro_syntax", - "proc-macro2", - "quote", - "syn 2.0.55", -] - -[[package]] -name = "diesel_table_macro_syntax" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5" -dependencies = [ - "syn 2.0.55", -] - [[package]] name = "difflib" version = "0.4.0" @@ -518,7 +724,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", + "subtle", ] [[package]] @@ -559,11 +767,20 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "either" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" +dependencies = [ + "serde", +] [[package]] name = "encode_unicode" @@ -571,18 +788,6 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" -[[package]] -name = "enum_dispatch" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f33313078bb8d4d05a2733a94ac4c2d8a0df9a2b84424ebf4f33bfc224a890e" -dependencies = [ - "once_cell", - "proc-macro2", - "quote", - "syn 2.0.55", -] - [[package]] name = "equivalent" version = "1.0.1" @@ -599,6 +804,23 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + [[package]] name = "eyre" version = "0.6.12" @@ -615,6 +837,12 @@ version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" +[[package]] +name = "finl_unicode" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" + [[package]] name = "fixedbitset" version = "0.4.2" @@ -630,12 +858,32 @@ dependencies = [ "num-traits", ] +[[package]] +name = "flume" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +dependencies = [ + "futures-core", + "futures-sink", + "spin 0.9.8", +] + [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + [[package]] name = "fs-err" version = "2.11.0" @@ -645,6 +893,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.30" @@ -687,6 +941,17 @@ dependencies = [ "futures-util", ] +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + [[package]] name = "futures-io" version = "0.3.30" @@ -820,17 +1085,42 @@ dependencies = [ "walkdir", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] + [[package]] name = "hashbrown" version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +dependencies = [ + "ahash 0.8.11", + "allocator-api2", +] + +[[package]] +name = "hashlink" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +dependencies = [ + "hashbrown 0.14.3", +] [[package]] name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +dependencies = [ + "unicode-segmentation", +] [[package]] name = "heck" @@ -839,12 +1129,51 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] -name = "human-panic" -version = "1.2.3" +name = "hermit-abi" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4f016c89920bbb30951a8405ecacbb4540db5524313b9445736e7e1855cf370" -dependencies = [ - "anstream", +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "human-panic" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4f016c89920bbb30951a8405ecacbb4540db5524313b9445736e7e1855cf370" +dependencies = [ + "anstream", "anstyle", "backtrace", "os_info", @@ -898,6 +1227,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "ignore" version = "0.4.22" @@ -927,7 +1266,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.14.3", +] + +[[package]] +name = "inherent" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0122b7114117e64a63ac49f752a5ca4624d534c7b1c7de796ac196381cd2d947" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", ] [[package]] @@ -1012,6 +1362,9 @@ name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin 0.5.2", +] [[package]] name = "libc" @@ -1084,6 +1437,16 @@ dependencies = [ "regex-automata 0.1.10", ] +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + [[package]] name = "memchr" version = "2.7.1" @@ -1143,6 +1506,12 @@ dependencies = [ "syn 2.0.55", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.7.2" @@ -1152,6 +1521,27 @@ dependencies = [ "adler", ] +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "normalize-line-endings" version = "0.3.0" @@ -1168,12 +1558,60 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d869c01cc0c455284163fd0092f1f93835385ccab5a98a0dcc497b2f8bf055a9" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.18" @@ -1181,6 +1619,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" dependencies = [ "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", ] [[package]] @@ -1224,6 +1673,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-float" +version = "3.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1e1c390732d15f1d48471625cd92d154e66db2c56645e29a9cd26f4699f72dc" +dependencies = [ + "num-traits", +] + [[package]] name = "os_info" version = "3.8.2" @@ -1235,6 +1693,30 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "ouroboros" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2ba07320d39dfea882faa70554b4bd342a5f273ed59ba7c1c6b4c840492c954" +dependencies = [ + "aliasable", + "ouroboros_macro", + "static_assertions", +] + +[[package]] +name = "ouroboros_macro" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec4c6225c69b4ca778c0aea097321a64c421cf4577b331c61b229267edabb6f8" +dependencies = [ + "heck 0.4.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.55", +] + [[package]] name = "overload" version = "0.1.1" @@ -1264,7 +1746,6 @@ dependencies = [ "clap_complete", "clap_complete_nushell", "dialoguer", - "diesel", "directories", "eyre", "human-panic", @@ -1273,11 +1754,16 @@ dependencies = [ "once_cell", "pace_cli", "pace_core", + "pace_error", + "pace_service", + "pace_storage", "pace_time", "predicates", + "rstest", "serde", "serde_derive", "similar-asserts", + "strum 0.26.2", "tempfile", "thiserror", "toml 0.8.12", @@ -1289,10 +1775,19 @@ version = "0.4.5" dependencies = [ "chrono", "chrono-tz", + "clap", "dialoguer", "eyre", "getset", + "open", "pace_core", + "pace_error", + "pace_service", + "pace_time", + "serde", + "serde_derive", + "serde_json", + "tera", "tracing", "typed-builder", ] @@ -1304,44 +1799,94 @@ dependencies = [ "chrono", "chrono-tz", "clap", - "diesel", "directories", "displaydoc", - "enum_dispatch", "eyre", "getset", "insta", "itertools", - "libsqlite3-sys", "merge", - "miette", "once_cell", - "open", + "pace_error", "pace_time", "parking_lot", "rayon", "rstest", + "sea-orm", "serde", "serde_derive", "serde_json", "similar-asserts", "simplelog", - "strum", + "strum 0.26.2", "strum_macros", "tabled", "tera", - "thiserror", "toml 0.8.12", "tracing", "typed-builder", "ulid", - "wildmatch", +] + +[[package]] +name = "pace_error" +version = "0.1.0" +dependencies = [ + "chrono", + "displaydoc", + "miette", + "sea-orm", + "serde_json", + "tera", + "thiserror", + "toml 0.8.12", + "ulid", ] [[package]] name = "pace_server" version = "0.1.3" +[[package]] +name = "pace_service" +version = "0.1.0" +dependencies = [ + "getset", + "pace_core", + "pace_error", + "pace_storage", + "pace_time", + "tracing", + "typed-builder", + "wildmatch", +] + +[[package]] +name = "pace_storage" +version = "0.1.0" +dependencies = [ + "chrono", + "displaydoc", + "getset", + "itertools", + "libsqlite3-sys", + "merge", + "pace_core", + "pace_error", + "pace_time", + "parking_lot", + "rayon", + "sea-orm", + "sea-orm-migration", + "strum 0.26.2", + "thiserror", + "tokio", + "toml 0.8.12", + "tracing", + "typed-builder", + "ulid", +] + [[package]] name = "pace_time" version = "0.1.2" @@ -1354,6 +1899,7 @@ dependencies = [ "eyre", "getset", "humantime", + "pace_error", "rstest", "serde", "serde_derive", @@ -1408,12 +1954,27 @@ dependencies = [ "regex", ] +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + [[package]] name = "pathdiff" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1525,6 +2086,27 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.30" @@ -1573,6 +2155,15 @@ dependencies = [ "termtree", ] +[[package]] +name = "proc-macro-crate" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" +dependencies = [ + "toml_edit 0.21.1", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -1606,6 +2197,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "quote" version = "1.0.35" @@ -1615,6 +2226,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.8.5" @@ -1736,36 +2353,125 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e898588f33fdd5b9420719948f9f2a32c922a246964576f71ba7f24f80610fbc" [[package]] -name = "rstest" -version = "0.18.2" +name = "rend" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97eeab2f3c0a199bc4be135c36c924b6590b88c377d416494288c14f2db30199" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" dependencies = [ - "futures", - "futures-timer", - "rstest_macros", - "rustc_version", + "bytecheck", ] [[package]] -name = "rstest_macros" -version = "0.18.2" +name = "ring" +version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d428f8247852f894ee1be110b375111b586d4fa431f6c46e64ba5a0dcccbe605" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ + "cc", "cfg-if", - "glob", - "proc-macro2", - "quote", - "regex", - "relative-path", - "rustc_version", - "syn 2.0.55", - "unicode-ident", + "getrandom", + "libc", + "spin 0.9.8", + "untrusted", + "windows-sys 0.52.0", ] [[package]] -name = "rustc-demangle" +name = "rkyv" +version = "0.7.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cba464629b3394fc4dbc6f940ff8f5b4ff5c7aef40f29166fd4ad12acbc99c0" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7dddfff8de25e6f62b9d64e6e432bf1c6736c57d20323e15ee10435fbda7c65" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rsa" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rstest" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97eeab2f3c0a199bc4be135c36c924b6590b88c377d416494288c14f2db30199" +dependencies = [ + "futures", + "futures-timer", + "rstest_macros", + "rustc_version", +] + +[[package]] +name = "rstest_macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d428f8247852f894ee1be110b375111b586d4fa431f6c46e64ba5a0dcccbe605" +dependencies = [ + "cfg-if", + "glob", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn 2.0.55", + "unicode-ident", +] + +[[package]] +name = "rust_decimal" +version = "1.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1790d1c4c0ca81211399e0e0af16333276f375209e71a37b67698a373db5b47a" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand", + "rkyv", + "serde", + "serde_json", +] + +[[package]] +name = "rustc-demangle" version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" @@ -1792,6 +2498,36 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustls" +version = "0.21.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" +dependencies = [ + "ring", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.14" @@ -1819,6 +2555,181 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "sea-bae" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bd3534a9978d0aa7edd2808dc1f8f31c4d0ecd31ddf71d997b3c98e9f3c9114" +dependencies = [ + "heck 0.4.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "sea-orm" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8814e37dc25de54398ee62228323657520b7f29713b8e238649385dbe473ee0" +dependencies = [ + "async-stream", + "async-trait", + "bigdecimal", + "chrono", + "futures", + "log", + "ouroboros", + "rust_decimal", + "sea-orm-macros", + "sea-query", + "sea-query-binder", + "serde", + "serde_json", + "sqlx", + "strum 0.25.0", + "thiserror", + "time", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "sea-orm-cli" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "620bc560062ae251b1366bde43b3f1508445cab5c2c8cbdb397034638ab1b357" +dependencies = [ + "chrono", + "clap", + "dotenvy", + "glob", + "regex", + "sea-schema", + "tracing", + "tracing-subscriber", + "url", +] + +[[package]] +name = "sea-orm-macros" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e115c6b078e013aa963cc2d38c196c2c40b05f03d0ac872fe06b6e0d5265603" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "sea-bae", + "syn 2.0.55", + "unicode-ident", +] + +[[package]] +name = "sea-orm-migration" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee8269bc6ff71afd6b78aa4333ac237a69eebd2cdb439036291e64fb4b8db23c" +dependencies = [ + "async-trait", + "clap", + "dotenvy", + "futures", + "sea-orm", + "sea-orm-cli", + "sea-schema", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "sea-query" +version = "0.30.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4166a1e072292d46dc91f31617c2a1cdaf55a8be4b5c9f4bf2ba248e3ac4999b" +dependencies = [ + "bigdecimal", + "chrono", + "derivative", + "inherent", + "ordered-float", + "rust_decimal", + "sea-query-derive", + "serde_json", + "time", + "uuid", +] + +[[package]] +name = "sea-query-binder" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36bbb68df92e820e4d5aeb17b4acd5cc8b5d18b2c36a4dd6f4626aabfa7ab1b9" +dependencies = [ + "bigdecimal", + "chrono", + "rust_decimal", + "sea-query", + "serde_json", + "sqlx", + "time", + "uuid", +] + +[[package]] +name = "sea-query-derive" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a82fcb49253abcb45cdcb2adf92956060ec0928635eb21b4f7a6d8f25ab0bc" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.55", + "thiserror", +] + +[[package]] +name = "sea-schema" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d148608012d25222442d1ebbfafd1228dbc5221baf4ec35596494e27a2394e" +dependencies = [ + "futures", + "sea-query", + "sea-schema-derive", +] + +[[package]] +name = "sea-schema-derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f686050f76bffc4f635cda8aea6df5548666b830b52387e8bc7de11056d11e" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + [[package]] name = "secrecy" version = "0.8.0" @@ -1878,100 +2789,406 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha2" version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "simdutf8" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" + +[[package]] +name = "similar" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32fea41aca09ee824cc9724996433064c89f7777e60762749a4170a14abbfa21" +dependencies = [ + "bstr 0.2.17", + "unicode-segmentation", +] + +[[package]] +name = "similar-asserts" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e041bb827d1bfca18f213411d51b665309f1afb37a04a5d1464530e13779fc0f" +dependencies = [ + "console", + "serde", + "similar", +] + +[[package]] +name = "simplelog" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16257adbfaef1ee58b1363bdc0664c9b8e1e30aed86049635fb5f147d065a9c0" +dependencies = [ + "log", + "termcolor", + "time", +] + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "slug" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bd94acec9c8da640005f8e135a39fc0372e74535e6b368b7a04b875f784c8c4" +dependencies = [ + "deunicode", + "wasm-bindgen", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "smawk" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" + +[[package]] +name = "socket2" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlformat" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce81b7bd7c4493975347ef60d8c7e8b742d4694f4c49f93e0a12ea263938176c" dependencies = [ - "cfg-if", - "cpufeatures", - "digest", + "itertools", + "nom", + "unicode_categories", ] [[package]] -name = "sharded-slab" -version = "0.1.7" +name = "sqlx" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +checksum = "c9a2ccff1a000a5a59cd33da541d9f2fdcd9e6e8229cc200565942bff36d0aaa" dependencies = [ - "lazy_static", + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", ] [[package]] -name = "shell-words" -version = "1.1.0" +name = "sqlx-core" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" +checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6" +dependencies = [ + "ahash 0.8.11", + "atoi", + "bigdecimal", + "byteorder", + "bytes", + "chrono", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-channel", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashlink", + "hex", + "indexmap", + "log", + "memchr", + "once_cell", + "paste", + "percent-encoding", + "rust_decimal", + "rustls", + "rustls-pemfile", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlformat", + "thiserror", + "time", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", + "webpki-roots", +] [[package]] -name = "similar" -version = "2.4.0" +name = "sqlx-macros" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32fea41aca09ee824cc9724996433064c89f7777e60762749a4170a14abbfa21" +checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127" dependencies = [ - "bstr 0.2.17", - "unicode-segmentation", + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 1.0.109", ] [[package]] -name = "similar-asserts" -version = "1.5.0" +name = "sqlx-macros-core" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e041bb827d1bfca18f213411d51b665309f1afb37a04a5d1464530e13779fc0f" +checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8" dependencies = [ - "console", + "dotenvy", + "either", + "heck 0.4.1", + "hex", + "once_cell", + "proc-macro2", + "quote", "serde", - "similar", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 1.0.109", + "tempfile", + "tokio", + "url", ] [[package]] -name = "simplelog" -version = "0.12.2" +name = "sqlx-mysql" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16257adbfaef1ee58b1363bdc0664c9b8e1e30aed86049635fb5f147d065a9c0" +checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" dependencies = [ + "atoi", + "base64", + "bigdecimal", + "bitflags 2.5.0", + "byteorder", + "bytes", + "chrono", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", "log", - "termcolor", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand", + "rsa", + "rust_decimal", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", "time", + "tracing", + "uuid", + "whoami", ] [[package]] -name = "siphasher" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" - -[[package]] -name = "slab" -version = "0.4.9" +name = "sqlx-postgres" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" dependencies = [ - "autocfg", + "atoi", + "base64", + "bigdecimal", + "bitflags 2.5.0", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "num-bigint", + "once_cell", + "rand", + "rust_decimal", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "time", + "tracing", + "uuid", + "whoami", ] [[package]] -name = "slug" -version = "0.1.5" +name = "sqlx-sqlite" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bd94acec9c8da640005f8e135a39fc0372e74535e6b368b7a04b875f784c8c4" +checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa" dependencies = [ - "deunicode", - "wasm-bindgen", + "atoi", + "chrono", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "sqlx-core", + "time", + "tracing", + "url", + "urlencoding", + "uuid", ] [[package]] -name = "smallvec" -version = "1.13.2" +name = "static_assertions" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] -name = "smawk" -version = "0.3.2" +name = "stringprep" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" +checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6" +dependencies = [ + "finl_unicode", + "unicode-bidi", + "unicode-normalization", +] [[package]] name = "strsim" @@ -1979,6 +3196,12 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" + [[package]] name = "strum" version = "0.26.2" @@ -2001,6 +3224,12 @@ dependencies = [ "syn 2.0.55", ] +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + [[package]] name = "supports-color" version = "3.0.0" @@ -2044,6 +3273,18 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1329189c02ff984e9736652b1631330da25eaa6bc639089ed4915d25446cbe7b" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.55", +] + [[package]] name = "synstructure" version = "0.12.6" @@ -2080,6 +3321,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tempfile" version = "3.10.1" @@ -2223,6 +3470,48 @@ dependencies = [ "time-core", ] +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "pin-project-lite", + "socket2", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-stream" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml" version = "0.5.11" @@ -2242,7 +3531,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit", + "toml_edit 0.22.9", ] [[package]] @@ -2254,6 +3543,17 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_edit" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow 0.5.40", +] + [[package]] name = "toml_edit" version = "0.22.9" @@ -2264,7 +3564,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "winnow", + "winnow 0.6.5", ] [[package]] @@ -2434,6 +3734,12 @@ dependencies = [ "unic-common", ] +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + [[package]] name = "unicode-ident" version = "1.0.12" @@ -2446,6 +3752,15 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-segmentation" version = "1.11.0" @@ -2464,6 +3779,35 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf8parse" version = "0.2.1" @@ -2477,6 +3821,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" dependencies = [ "getrandom", + "serde", ] [[package]] @@ -2522,6 +3867,12 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasm-bindgen" version = "0.2.92" @@ -2586,6 +3937,22 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + +[[package]] +name = "whoami" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" +dependencies = [ + "redox_syscall", + "wasite", +] + [[package]] name = "wildmatch" version = "2.3.3" @@ -2764,6 +4131,15 @@ version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + [[package]] name = "winnow" version = "0.6.5" @@ -2773,6 +4149,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "yaml-rust" version = "0.4.5" @@ -2782,6 +4167,26 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "zerocopy" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + [[package]] name = "zeroize" version = "1.7.0" diff --git a/Cargo.toml b/Cargo.toml index e5d2e17f..c09548c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,10 @@ members = [ "crates/cli", "crates/core", + "crates/error", "crates/server", + "crates/service", + "crates/storage", "crates/time", ] @@ -26,9 +29,9 @@ clap_complete = "4.5.1" clap_complete_nushell = "4.5.1" derive_more = { version = "0.99.17", default-features = false } dialoguer = "0.11.0" -diesel = "2.1.5" directories = "5.0.1" displaydoc = "0.2.4" +dotenvy = "0.15.7" enum_dispatch = "0.3.12" eyre = "0.6.12" getset = "0.1.2" @@ -37,18 +40,23 @@ humantime = "2.1.0" insta = "1.36.1" insta-cmd = "0.5.0" itertools = "0.12.1" -libsqlite3-sys = "0.27" +libsqlite3-sys = "0.27.0" merge = "0.1.0" miette = "7.2.0" once_cell = "1.19.0" open = "5.1.2" pace_cli = { path = "crates/cli", version = "0" } pace_core = { path = "crates/core", version = "0" } +pace_error = { path = "crates/error", version = "0" } +pace_service = { path = "crates/service", version = "0" } +pace_storage = { path = "crates/storage", version = "0" } pace_time = { path = "crates/time", version = "0" } parking_lot = "0.12.1" predicates = "3.1.0" rayon = "1.10.0" rstest = "0.18.2" +sea-orm = { version = "0.12.15" } +sea-orm-migration = "0.12.15" serde = "1.0.197" serde_derive = "1.0.197" serde_json = "1.0.114" @@ -60,6 +68,7 @@ tabled = "0.15.0" tempfile = "3.10.1" tera = "1.19.1" thiserror = "1.0.58" +tokio = { version = "1.37.0", default-features = false } toml = "0.8.12" tracing = "0.1.40" typed-builder = "0.18.1" @@ -100,12 +109,13 @@ clap = { workspace = true, features = ["env", "wrap_help", "derive"] } clap_complete = { workspace = true } clap_complete_nushell = { workspace = true } dialoguer = { workspace = true, features = ["history", "fuzzy-select"] } -diesel = { workspace = true, features = ["sqlite"] } directories = { workspace = true } eyre = { workspace = true } human-panic = { workspace = true } pace_cli = { workspace = true } pace_core = { workspace = true, features = ["cli"] } +pace_error = { workspace = true } +pace_service = { workspace = true } pace_time = { workspace = true, features = ["cli"] } serde = { workspace = true } serde_derive = { workspace = true } @@ -125,8 +135,11 @@ assert_cmd = { workspace = true } insta = { workspace = true, features = ["toml"] } insta-cmd = { workspace = true } once_cell = { workspace = true } +pace_storage = { workspace = true } predicates = { workspace = true } +rstest = { workspace = true } similar-asserts = { workspace = true } +strum = { workspace = true } tempfile = { workspace = true } # The profile that 'cargo dist' will build with diff --git a/config/pace.toml b/config/pace.toml index 7e5cb80f..126dd5e2 100644 --- a/config/pace.toml +++ b/config/pace.toml @@ -1,10 +1,4 @@ [general] -# Define where to store the activity log: options include "file", "database", etc. -storage-kind = "file" -# Path to the activity log file, used if storage-kind is set to "file" -path = "/path/to/your/activity.pace.toml" -# Specify the default format for new activity logs: "toml" or "yaml" -format-kind = "toml" # Category separator used in the cli category-separator = "::" # Default priority for new tasks @@ -12,6 +6,25 @@ default-priority = "medium" # Default time zone for new tasks default-timezone = "UTC" +# The storage location +[storage.database] +# The kind of database storage +# Supported values: "sqlite", "mysql", "postgres", "mssql" +# If you choose "sqlite", the url should be the path to the database file +# Currently, only "sqlite" is supported +kind = "sqlite" +# In case of a SQLite database, this is the url: `/path/to/database?mode=rwc` +# '?mode=rwc' is required for read-write connections +# Otherwise, this is the connection string: `username:password@host/database` +url = "./db/activities.pace.sqlite3?mode=rwc" + +# For completeness, here is the file storage configuration +# +# The storage location in case it is a file +# [storage.file] +# In case of a file, this is the path to the file: `/path/to/file` +# location = "/path/to/your/storage" + [reflections] # Format of the reflections generated by the pace: "console", "template", "json", or "csv" etc. format = "console" @@ -23,11 +36,6 @@ include-tags = true include-descriptions = true time-format = "%Y-%m-%d %H:%M" -[database] -# Database configurations are used if storage-kind is set to "database" -engine = "sqlite" # only option supported for now is "sqlite" -connection-string = "path/to/your/database.db" - [pomodoro] # Pomodoro technique specific configurations work-duration-minutes = 25 diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index a15b5128..07424521 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -18,14 +18,27 @@ include = [ "src/**/*", "Cargo.toml", ] +[features] +default = ["cli"] +cli = ["clap"] +clap = ["dep:clap"] [dependencies] chrono = { workspace = true } chrono-tz = { workspace = true } +clap = { workspace = true, optional = true, features = ["env", "wrap_help", "derive"] } dialoguer = { workspace = true, features = ["fuzzy-select"] } eyre = { workspace = true } getset = { workspace = true } +open = { workspace = true } pace_core = { workspace = true } +pace_error = { workspace = true } +pace_service = { workspace = true } +pace_time = { workspace = true } +serde = { workspace = true } +serde_derive = { workspace = true } +serde_json = { workspace = true } +tera = { workspace = true } tracing = { workspace = true } typed-builder = { workspace = true } diff --git a/crates/cli/README.md b/crates/cli/README.md index 9b9369ee..a9a8eaae 100644 --- a/crates/cli/README.md +++ b/crates/cli/README.md @@ -33,6 +33,16 @@ You can ask questions in the | Discord | [![Discord](https://dcbadge.vercel.app/api/server/RKSWrAcYdG?style=flat-square)](https://discord.gg/RKSWrAcYdG) | | Discussions | [GitHub Discussions](https://github.com/orgs/pace-rs/discussions) | +## Crate features + +This crate exposes a few features for controlling dependency usage: + +- **clap** - Enables a dependency on the `clap` crate and enables parsing from + the commandline. *This feature is enabled by default*. + +- **cli** - Enables support for CLI features by enabling `merge` and `clap` + features. *This feature is enabled by default*. + ## Examples TODO! diff --git a/crates/cli/src/commands.rs b/crates/cli/src/commands.rs new file mode 100644 index 00000000..1c188309 --- /dev/null +++ b/crates/cli/src/commands.rs @@ -0,0 +1,8 @@ +pub mod adjust; +pub mod begin; +pub mod docs; +pub mod end; +pub mod hold; +pub mod now; +pub mod reflect; +pub mod resume; diff --git a/crates/core/src/commands/adjust.rs b/crates/cli/src/commands/adjust.rs similarity index 83% rename from crates/core/src/commands/adjust.rs rename to crates/cli/src/commands/adjust.rs index 9ee3b58f..7ac65156 100644 --- a/crates/core/src/commands/adjust.rs +++ b/crates/cli/src/commands/adjust.rs @@ -1,21 +1,22 @@ -use std::collections::HashSet; +use std::sync::Arc; use chrono::{FixedOffset, NaiveTime}; use chrono_tz::Tz; #[cfg(feature = "clap")] use clap::Parser; use getset::Getters; -use pace_time::{date_time::PaceDateTime, time_zone::PaceTimeZoneKind, Validate}; use tracing::debug; use typed_builder::TypedBuilder; -use crate::{ - commands::UpdateOptions, +use pace_core::{ config::PaceConfig, - error::{ActivityLogErrorKind, PaceResult, UserMessage}, - service::activity_store::ActivityStore, - storage::{get_storage_from_config, ActivityQuerying, ActivityWriteOps, SyncStorage}, + domain::{category::PaceCategory, description::PaceDescription, tag::PaceTagCollection}, + options::UpdateOptions, + storage::{ActivityQuerying, ActivityStorage, ActivityWriteOps, SyncStorage}, }; +use pace_error::{ActivityLogErrorKind, PaceResult, UserMessage}; +use pace_service::activity_store::ActivityStore; +use pace_time::{date_time::PaceDateTime, time_zone::PaceTimeZoneKind, Validate}; /// `adjust` subcommand options #[derive(Debug, Clone, PartialEq, TypedBuilder, Eq, Hash, Default, Getters)] @@ -38,7 +39,7 @@ pub struct AdjustCommandOptions { visible_alias = "cat" ) )] - category: Option, + category: Option, /// The description of the activity #[cfg_attr( @@ -51,7 +52,7 @@ pub struct AdjustCommandOptions { visible_alias = "desc" ) )] - description: Option, + description: Option, /// The start time of the activity. Format: HH:MM #[cfg_attr( @@ -138,7 +139,11 @@ impl AdjustCommandOptions { /// A `UserMessage` to be printed to the user indicating the result of the operation and /// some additional information #[tracing::instrument(skip(self))] - pub fn handle_adjust(&self, config: &PaceConfig) -> PaceResult { + pub fn handle_adjust( + &self, + config: &PaceConfig, + storage: Arc, + ) -> PaceResult { let Self { category, description, @@ -160,40 +165,40 @@ impl AdjustCommandOptions { debug!("Parsed time: {date_time:?}"); - let activity_store = ActivityStore::with_storage(get_storage_from_config(config)?)?; + let activity_store = ActivityStore::with_storage(storage)?; let activity_item = activity_store .most_recent_active_activity()? .ok_or_else(|| ActivityLogErrorKind::NoActiveActivityToAdjust)?; - debug!("Most recent active activity item: {:?}", activity_item); + debug!("Most recent active activity item: {activity_item:?}"); let guid = *activity_item.guid(); let mut activity = activity_item.activity().clone(); if let Some(category) = category { - debug!("Setting category to: {:?}", category); + debug!("Setting category to: {category:?}"); _ = activity.set_category(category.clone().into()); } if let Some(description) = description { - debug!("Setting description to: {:?}", description); + debug!("Setting description to: {description:?}"); _ = activity.set_description(description.clone()); } if start.is_some() { - debug!("Setting start time to: {:?}", date_time); + debug!("Setting start time to: {date_time:?}"); _ = activity.set_begin(date_time); } if let Some(tags) = tags { - let tags = tags.iter().cloned().collect::>(); + let tags = tags.iter().cloned().collect::(); if *override_tags { - debug!("Overriding tags with: {:?}", tags); + debug!("Overriding tags with: {tags:?}"); _ = activity.set_tags(Some(tags)); } else { let merged_tags = activity.tags_mut().as_mut().map_or_else( @@ -201,7 +206,7 @@ impl AdjustCommandOptions { |existing_tags| existing_tags.union(&tags).cloned().collect(), ); - debug!("Setting merged tags: {:?}", merged_tags); + debug!("Setting merged tags: {merged_tags:?}"); _ = activity.set_tags(Some(merged_tags)); } diff --git a/crates/core/src/commands/begin.rs b/crates/cli/src/commands/begin.rs similarity index 83% rename from crates/core/src/commands/begin.rs rename to crates/cli/src/commands/begin.rs index f71f8e80..6f6c958f 100644 --- a/crates/core/src/commands/begin.rs +++ b/crates/cli/src/commands/begin.rs @@ -1,20 +1,25 @@ -use std::collections::HashSet; +use std::sync::Arc; use chrono::{FixedOffset, NaiveTime}; use chrono_tz::Tz; #[cfg(feature = "clap")] use clap::Parser; use getset::Getters; -use pace_time::{date_time::PaceDateTime, time_zone::PaceTimeZoneKind, Validate}; use tracing::debug; -use crate::{ +use pace_core::{ config::PaceConfig, - domain::activity::{Activity, ActivityKind}, - error::{PaceResult, UserMessage}, - service::activity_store::ActivityStore, - storage::{get_storage_from_config, ActivityStateManagement, SyncStorage}, + domain::{ + activity::{Activity, ActivityKind}, + category::PaceCategory, + description::PaceDescription, + tag::PaceTagCollection, + }, + storage::{ActivityStateManagement, ActivityStorage, SyncStorage}, }; +use pace_error::{PaceResult, UserMessage}; +use pace_service::activity_store::ActivityStore; +use pace_time::{date_time::PaceDateTime, time_zone::PaceTimeZoneKind, Validate}; /// `begin` subcommand options #[derive(Debug, Clone, PartialEq, Eq, Getters)] @@ -28,7 +33,7 @@ pub struct BeginCommandOptions { /// You can use the separator you setup in the configuration file /// to specify a subcategory. #[cfg_attr(feature = "clap", clap(short, long, name = "Category"))] - category: Option, + category: Option, /// The time the activity has been started at. Format: HH:MM #[cfg_attr( @@ -39,7 +44,7 @@ pub struct BeginCommandOptions { /// The description of the activity you want to start #[cfg_attr(feature = "clap", clap(value_name = "Activity Description"))] - description: String, + description: PaceDescription, /// The tags you want to associate with the activity, separated by a comma #[cfg_attr( @@ -102,7 +107,11 @@ impl BeginCommandOptions { /// Returns a `UserMessage` with the information about the started activity /// that can be displayed to the user #[tracing::instrument(skip(self))] - pub fn handle_begin(&self, config: &PaceConfig) -> PaceResult { + pub fn handle_begin( + &self, + config: &PaceConfig, + storage: Arc, + ) -> PaceResult { let Self { category, at, @@ -125,7 +134,7 @@ impl BeginCommandOptions { // parse tags from string or get an empty set let tags = tags .as_ref() - .map(|tags| tags.iter().cloned().collect::>()); + .map(|tags| tags.iter().cloned().collect::()); debug!("Parsed tags: {tags:?}"); @@ -139,11 +148,11 @@ impl BeginCommandOptions { .tags(tags) .build(); - let activity_store = ActivityStore::with_storage(get_storage_from_config(config)?)?; + let activity_store = ActivityStore::with_storage(storage)?; let activity_item = activity_store.begin_activity(activity)?; - debug!("Started Activity: {:?}", activity_item); + debug!("Started Activity: {activity_item:?}"); activity_store.sync()?; diff --git a/crates/core/src/commands/docs.rs b/crates/cli/src/commands/docs.rs similarity index 96% rename from crates/core/src/commands/docs.rs rename to crates/cli/src/commands/docs.rs index af265f60..3c6b5eb3 100644 --- a/crates/core/src/commands/docs.rs +++ b/crates/cli/src/commands/docs.rs @@ -1,12 +1,13 @@ #[cfg(feature = "clap")] use clap::Parser; -use crate::{ +use pace_core::{ constants::PACE_DOCS_URL, constants::{PACE_CONFIG_DOCS_URL, PACE_DEV_DOCS_URL}, - error::{PaceResult, UserMessage}, }; +use pace_error::{PaceResult, UserMessage}; + /// `docs` subcommand options #[derive(Debug, Clone)] #[cfg_attr(feature = "clap", derive(Parser))] diff --git a/crates/core/src/commands/end.rs b/crates/cli/src/commands/end.rs similarity index 86% rename from crates/core/src/commands/end.rs rename to crates/cli/src/commands/end.rs index 3e4304e0..4b2b1f9f 100644 --- a/crates/core/src/commands/end.rs +++ b/crates/cli/src/commands/end.rs @@ -1,19 +1,21 @@ +use std::sync::Arc; + use chrono::{FixedOffset, NaiveTime}; use chrono_tz::Tz; #[cfg(feature = "clap")] use clap::Parser; use getset::Getters; -use pace_time::{date_time::PaceDateTime, time_zone::PaceTimeZoneKind, Validate}; use tracing::debug; use typed_builder::TypedBuilder; -use crate::{ - commands::EndOptions, +use pace_core::{ config::PaceConfig, - error::{PaceResult, UserMessage}, - service::activity_store::ActivityStore, - storage::{get_storage_from_config, ActivityStateManagement, SyncStorage}, + options::EndOptions, + storage::{ActivityStateManagement, ActivityStorage, SyncStorage}, }; +use pace_error::{PaceResult, UserMessage}; +use pace_service::activity_store::ActivityStore; +use pace_time::{date_time::PaceDateTime, time_zone::PaceTimeZoneKind, Validate}; /// `end` subcommand options #[derive(Debug, Clone, PartialEq, TypedBuilder, Eq, Hash, Default, Getters)] @@ -73,7 +75,11 @@ impl EndCommandOptions { /// Returns a `UserMessage` with the information about the ended activity /// that can be displayed to the user #[tracing::instrument(skip(self))] - pub fn handle_end(&self, config: &PaceConfig) -> PaceResult { + pub fn handle_end( + &self, + config: &PaceConfig, + storage: Arc, + ) -> PaceResult { let Self { at, time_zone, @@ -89,9 +95,9 @@ impl EndCommandOptions { ))? .validate()?; - debug!("Parsed date time: {:?}", date_time); + debug!("Parsed date time: {date_time:?}"); - let activity_store = ActivityStore::with_storage(get_storage_from_config(config)?)?; + let activity_store = ActivityStore::with_storage(storage)?; let end_opts = EndOptions::builder().end_time(date_time).build(); diff --git a/crates/core/src/commands/hold.rs b/crates/cli/src/commands/hold.rs similarity index 78% rename from crates/core/src/commands/hold.rs rename to crates/cli/src/commands/hold.rs index f2dbb510..57002953 100644 --- a/crates/core/src/commands/hold.rs +++ b/crates/cli/src/commands/hold.rs @@ -1,20 +1,20 @@ +use std::sync::Arc; + use chrono::{FixedOffset, NaiveTime}; use chrono_tz::Tz; #[cfg(feature = "clap")] use clap::Parser; - -use getset::Getters; -use pace_time::{date_time::PaceDateTime, time_zone::PaceTimeZoneKind, Validate}; use tracing::debug; -use typed_builder::TypedBuilder; -use crate::{ +use pace_core::{ config::PaceConfig, - domain::intermission::IntermissionAction, - error::{PaceResult, UserMessage}, - service::activity_store::ActivityStore, - storage::{get_storage_from_config, ActivityStateManagement, SyncStorage}, + domain::{description::PaceDescription, intermission::IntermissionAction}, + options::HoldOptions, + storage::{ActivityStateManagement, ActivityStorage, SyncStorage}, }; +use pace_error::{PaceResult, UserMessage}; +use pace_service::activity_store::ActivityStore; +use pace_time::{date_time::PaceDateTime, time_zone::PaceTimeZoneKind, Validate}; /// `hold` subcommand options #[derive(Debug)] @@ -31,7 +31,7 @@ pub struct HoldCommandOptions { /// The reason for the intermission, if this is not set, the description of the activity to be held will be used #[cfg_attr(feature = "clap", clap(short, long, value_name = "Reason"))] - reason: Option, + reason: Option, /// If there are existing intermissions, they will be finished and a new one is being created /// @@ -81,7 +81,11 @@ impl HoldCommandOptions { /// /// A `UserMessage` with the information about the held activity that can be displayed to the user #[tracing::instrument(skip(self))] - pub fn handle_hold(&self, config: &PaceConfig) -> PaceResult { + pub fn handle_hold( + &self, + config: &PaceConfig, + storage: Arc, + ) -> PaceResult { let Self { pause_at, reason, @@ -112,7 +116,7 @@ impl HoldCommandOptions { debug!("Hold options: {hold_opts:?}"); - let activity_store = ActivityStore::with_storage(get_storage_from_config(config)?)?; + let activity_store = ActivityStore::with_storage(storage)?; let user_message = if let Some(activity) = activity_store.hold_most_recent_active_activity(hold_opts)? { @@ -128,21 +132,3 @@ impl HoldCommandOptions { Ok(UserMessage::new(user_message)) } } - -/// Options for holding an activity -#[derive(Debug, Clone, PartialEq, TypedBuilder, Eq, Hash, Default, Getters)] -#[getset(get = "pub")] -#[non_exhaustive] -pub struct HoldOptions { - /// The action to take on the intermission - #[builder(default)] - action: IntermissionAction, - - /// The start time of the intermission - #[builder(default, setter(into))] - begin_time: PaceDateTime, - - /// The reason for holding the activity - #[builder(default, setter(into))] - reason: Option, -} diff --git a/crates/core/src/commands/now.rs b/crates/cli/src/commands/now.rs similarity index 76% rename from crates/core/src/commands/now.rs rename to crates/cli/src/commands/now.rs index 9c91d41b..99490c5e 100644 --- a/crates/core/src/commands/now.rs +++ b/crates/cli/src/commands/now.rs @@ -1,14 +1,16 @@ +use std::sync::Arc; + #[cfg(feature = "clap")] use clap::Parser; use tracing::debug; -use crate::{ +use pace_core::{ config::PaceConfig, domain::{activity::ActivityItem, filter::ActivityFilterKind}, - error::{PaceResult, UserMessage}, - service::activity_store::ActivityStore, - storage::{get_storage_from_config, ActivityQuerying, ActivityReadOps}, + storage::{ActivityQuerying, ActivityReadOps, ActivityStorage}, }; +use pace_error::{PaceResult, UserMessage}; +use pace_service::activity_store::ActivityStore; /// `now` subcommand options #[derive(Debug)] @@ -30,14 +32,18 @@ impl NowCommandOptions { /// /// Returns a `UserMessage` with the information about the current activities that can be displayed to the user #[tracing::instrument(skip(self))] - pub fn handle_now(&self, config: &PaceConfig) -> PaceResult { - let activity_store = ActivityStore::with_storage(get_storage_from_config(config)?)?; + pub fn handle_now( + &self, + config: &PaceConfig, + storage: Arc, + ) -> PaceResult { + let activity_store = ActivityStore::with_storage(storage)?; let user_message = (activity_store.list_current_activities(ActivityFilterKind::Active)?) .map_or_else( || "No activities are currently running.".to_string(), |activities| { - debug!("Current Activities: {:?}", activities); + debug!("Current Activities: {activities:?}"); // Get the activity items let activity_items = activities diff --git a/crates/core/src/commands/reflect.rs b/crates/cli/src/commands/reflect.rs similarity index 86% rename from crates/core/src/commands/reflect.rs rename to crates/cli/src/commands/reflect.rs index f3fda45d..3b72ce7d 100644 --- a/crates/core/src/commands/reflect.rs +++ b/crates/cli/src/commands/reflect.rs @@ -1,24 +1,25 @@ #[cfg(feature = "clap")] use clap::Parser; use getset::{Getters, MutGetters, Setters}; -use pace_time::{ - flags::{DateFlags, TimeFlags}, - time_frame::PaceTimeFrame, - time_zone::PaceTimeZoneKind, -}; use serde_derive::Serialize; -use std::path::PathBuf; +use std::{path::PathBuf, sync::Arc}; use tracing::debug; use typed_builder::TypedBuilder; -use crate::{ +use pace_core::{ config::PaceConfig, - domain::{activity::ActivityKind, filter::FilterOptions, reflection::ReflectionsFormatKind}, - error::{PaceResult, TemplatingErrorKind, UserMessage}, - service::{activity_store::ActivityStore, activity_tracker::ActivityTracker}, - storage::get_storage_from_config, + domain::{activity::ActivityKind, category::PaceCategory, reflection::ReflectionsFormatKind}, + options::FilterOptions, + storage::ActivityStorage, template::{PaceReflectionTemplate, TEMPLATES}, }; +use pace_error::{PaceResult, TemplatingErrorKind, UserMessage}; +use pace_service::{activity_store::ActivityStore, activity_tracker::ActivityTracker}; +use pace_time::{ + flags::{DateFlags, TimeFlags}, + time_frame::PaceTimeFrame, + time_zone::PaceTimeZoneKind, +}; /// `reflect` subcommand options #[derive(Debug, Getters)] @@ -39,7 +40,7 @@ pub struct ReflectCommandOptions { feature = "clap", clap(short, long, value_name = "Category", visible_alias = "cat") )] - category: Option, + category: Option, /// Case sensitive category filter #[cfg_attr( @@ -123,13 +124,19 @@ pub struct ReflectCommandOptions { impl ReflectCommandOptions { #[tracing::instrument(skip(self))] - pub fn handle_reflect(&self, config: &PaceConfig) -> PaceResult { + pub fn handle_reflect( + &self, + config: &PaceConfig, + storage: Arc, + ) -> PaceResult { let Self { export_file, time_flags, date_flags, template_file, output_format, + category, + case_sensitive, // time_zone, // time_zone_offset, .. // TODO: ignore the rest of the fields for now, @@ -143,14 +150,18 @@ impl ReflectCommandOptions { PaceTimeZoneKind::NotSet, ))?; - let activity_store = ActivityStore::with_storage(get_storage_from_config(config)?)?; + let activity_store = ActivityStore::with_storage(storage)?; let activity_tracker = ActivityTracker::with_activity_store(activity_store); - debug!("Displaying reflection for time frame: {}", time_frame); + debug!("Displaying reflection for time frame: {time_frame}"); + + let filter_opts = FilterOptions::builder() + .category(category.clone()) + .case_sensitive(*case_sensitive) + .build(); - let Some(reflection) = - activity_tracker.generate_reflection(FilterOptions::from(self), time_frame)? + let Some(reflection) = activity_tracker.generate_reflection(filter_opts, time_frame)? else { return Ok(UserMessage::new( "No activities found for the specified time frame", @@ -164,7 +175,7 @@ impl ReflectCommandOptions { Some(ReflectionsFormatKind::Json) => { let json = serde_json::to_string_pretty(&reflection)?; - debug!("Reflection: {}", json); + debug!("Reflection: {json}"); // write to file if export file is specified if let Some(export_file) = export_file { @@ -178,7 +189,6 @@ impl ReflectCommandOptions { return Ok(UserMessage::new(json)); } - Some(ReflectionsFormatKind::Template) => { let context = PaceReflectionTemplate::from(reflection).into_context(); @@ -198,7 +208,7 @@ impl ReflectCommandOptions { .map_err(TemplatingErrorKind::RenderingToTemplateFailed)? }; - debug!("Reflection: {}", templated); + debug!("Reflection: {templated}"); // write to file if export file is specified if let Some(export_file) = export_file { @@ -213,6 +223,7 @@ impl ReflectCommandOptions { return Ok(UserMessage::new(templated)); } Some(ReflectionsFormatKind::Csv) => unimplemented!("CSV format not yet supported"), + _ => unimplemented!("Unsupported output format"), } } } diff --git a/crates/core/src/commands/resume.rs b/crates/cli/src/commands/resume.rs similarity index 83% rename from crates/core/src/commands/resume.rs rename to crates/cli/src/commands/resume.rs index 41d3ffb6..8dd63f86 100644 --- a/crates/core/src/commands/resume.rs +++ b/crates/cli/src/commands/resume.rs @@ -3,7 +3,6 @@ use chrono_tz::Tz; #[cfg(feature = "clap")] use clap::Parser; use getset::Getters; -use pace_time::date_time::PaceDateTime; use typed_builder::TypedBuilder; /// `resume` subcommand options @@ -56,13 +55,3 @@ impl ResumeCommandOptions { // FIXME: Inner run implementation for the resume command kept in pace-rs crate for now // FIXME: due to the dependency on pace-cli } - -/// Options for resuming an activity -#[derive(Debug, Clone, PartialEq, TypedBuilder, Eq, Hash, Default, Getters)] -#[getset(get = "pub")] -#[non_exhaustive] -pub struct ResumeOptions { - /// The resume time of the intermission - #[builder(default, setter(into))] - resume_time: Option, -} diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index 2b1d7967..89276d17 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -1,14 +1,10 @@ //! `pace_cli` contains utilities for the `pace` command-line interface /// Contains the main logic for prompting the user for input -pub(crate) mod prompt; +pub mod prompt; /// Contains the main logic for the `setup` command -pub(crate) mod setup; +pub mod setup; -pub(crate) static PACE_ART: &str = include_str!("pace.art"); +pub mod commands; -// Public API -pub use crate::{ - prompt::{confirmation_or_break, prompt_resume_activity, prompt_time_zone}, - setup::{setup_config, PathOptions}, -}; +pub(crate) static PACE_ART: &str = include_str!("pace.art"); diff --git a/crates/cli/src/prompt.rs b/crates/cli/src/prompt.rs index 4eade99a..102eb345 100644 --- a/crates/cli/src/prompt.rs +++ b/crates/cli/src/prompt.rs @@ -111,7 +111,7 @@ pub fn prompt_config_file_path( /// # Returns /// /// Returns `Ok(())` if the user confirms their choices -pub fn confirmation_or_break(prompt: &str) -> Result<()> { +pub fn confirmation_or_break_default_false(prompt: &str) -> Result<()> { let confirmation = Confirm::with_theme(&ColorfulTheme::default()) .with_prompt(prompt) .default(false) @@ -124,6 +124,33 @@ pub fn confirmation_or_break(prompt: &str) -> Result<()> { Ok(()) } +/// Prompts the user to confirm their choices or break the setup assistant +/// +/// # Arguments +/// +/// * `prompt` - The prompt to display to the user +/// +/// # Errors +/// +/// Returns an error if the wants to break the setup assistant or +/// if the prompt fails +/// +/// # Returns +/// +/// Returns `Ok(())` if the user confirms their choices +pub fn confirmation_or_break_default_true(prompt: &str) -> Result<()> { + let confirmation = Confirm::with_theme(&ColorfulTheme::default()) + .with_prompt(prompt) + .default(true) + .interact()?; + + if !confirmation { + eyre::bail!("Setup exited without changes. No changes were made."); + } + + Ok(()) +} + /// Prompts the user to select an activity to resume /// /// # Arguments diff --git a/crates/cli/src/setup.rs b/crates/cli/src/setup.rs index cff7f7ca..043f9840 100644 --- a/crates/cli/src/setup.rs +++ b/crates/cli/src/setup.rs @@ -22,8 +22,11 @@ use pace_core::{ }; use crate::{ - prompt::{prompt_activity_log_path, prompt_config_file_path}, - prompt_time_zone, PACE_ART, + prompt::{ + confirmation_or_break_default_true, prompt_activity_log_path, prompt_config_file_path, + prompt_time_zone, + }, + PACE_ART, }; #[derive(Debug, TypedBuilder, Getters)] @@ -279,33 +282,6 @@ Use Q, ESC, or Ctrl-C to exit gracefully at any time."; Ok(()) } -/// Prompts the user to confirm their choices or break the setup assistant -/// -/// # Arguments -/// -/// * `prompt` - The prompt to display to the user -/// -/// # Errors -/// -/// Returns an error if the wants to break the setup assistant or -/// if the prompt fails -/// -/// # Returns -/// -/// Returns `Ok(())` if the user confirms their choices -pub fn confirmation_or_break(prompt: &str) -> Result<()> { - let confirmation = Confirm::with_theme(&ColorfulTheme::default()) - .with_prompt(prompt) - .default(true) - .interact()?; - - if !confirmation { - eyre::bail!("Setup exited without changes. No changes were made."); - } - - Ok(()) -} - /// The `setup` commands interior for the pace application /// /// # Arguments @@ -355,7 +331,7 @@ pub fn setup_config(term: &Term, path_opts: &PathOptions) -> Result<()> { let prompt = "Do you want the files to be written?"; - confirmation_or_break(prompt)?; + confirmation_or_break_default_true(prompt)?; term.clear_screen()?; diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 81b2d328..b09794da 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -19,32 +19,27 @@ include = [ "Cargo.toml", ] -# TODO!: Use features for adding optional dependencies for testing and merging etc. [features] default = ["cli"] cli = ["clap"] -sqlite = ["dep:diesel", "dep:libsqlite3-sys"] clap = ["dep:clap"] -# testing = ["dep:arbitrary"] [dependencies] chrono = { workspace = true, features = ["serde"] } chrono-tz = { workspace = true, features = ["serde"] } clap = { workspace = true, optional = true, features = ["env", "wrap_help", "derive"] } -diesel = { workspace = true, features = ["sqlite", "chrono"], optional = true } directories = { workspace = true } displaydoc = { workspace = true } -enum_dispatch = { workspace = true } +# dotenvy = { workspace = true } getset = { workspace = true } itertools = { workspace = true } -libsqlite3-sys = { workspace = true, features = ["bundled"], optional = true } merge = { workspace = true } -miette = { workspace = true, features = ["fancy"] } once_cell = { workspace = true } -open = { workspace = true } +pace_error = { workspace = true } pace_time = { workspace = true } parking_lot = { workspace = true, features = ["deadlock_detection"] } rayon = { workspace = true } +sea-orm = { workspace = true } serde = { workspace = true } serde_derive = { workspace = true } serde_json = { workspace = true } @@ -52,12 +47,10 @@ strum = { workspace = true, features = ["derive"] } strum_macros = { workspace = true } tabled = { workspace = true } tera = { workspace = true } -thiserror = { workspace = true } toml = { workspace = true, features = ["indexmap", "preserve_order"] } tracing = { workspace = true } typed-builder = { workspace = true } ulid = { workspace = true, features = ["serde"] } -wildmatch = { workspace = true } [dev-dependencies] eyre = { workspace = true } diff --git a/crates/core/README.md b/crates/core/README.md index 82260d40..75faa48c 100644 --- a/crates/core/README.md +++ b/crates/core/README.md @@ -42,9 +42,8 @@ This crate exposes a few features for controlling dependency usage: - **cli** - Enables support for CLI features by enabling `merge` and `clap` features. *This feature is enabled by default*. -- **sqlite** - Enables a dependency on the `rusqlite` crate and enables - persistence to a SQLite database. *This feature is disabled by default as it's - not yet implemented*. +- **rusqlite** - Enables a dependency on the `rusqlite` crate and enables + persistence to a SQLite database. *This feature is enabled by default*. ## Examples diff --git a/crates/core/src/config.rs b/crates/core/src/config.rs index 3f212f12..dde1ed9d 100644 --- a/crates/core/src/config.rs +++ b/crates/core/src/config.rs @@ -10,10 +10,9 @@ use serde_derive::{Deserialize, Serialize}; use directories::ProjectDirs; use strum_macros::EnumString; -use crate::{ - domain::{priority::ItemPriorityKind, reflection::ReflectionsFormatKind}, - error::{PaceErrorKind, PaceResult}, -}; +use crate::domain::{priority::ItemPriorityKind, reflection::ReflectionsFormatKind}; + +use pace_error::{ConfigErrorKind, PaceResult}; /// The pace configuration file /// @@ -27,6 +26,11 @@ pub struct PaceConfig { #[getset(get = "pub", get_mut = "pub")] general: GeneralConfig, + /// Storage configuration for the pace application + #[serde(default)] + #[getset(get = "pub", get_mut = "pub")] + storage: StorageConfig, + /// Reflections configuration for the pace application #[serde(default, skip_serializing_if = "Option::is_none")] #[getset(get = "pub", get_mut = "pub")] @@ -37,11 +41,6 @@ pub struct PaceConfig { #[getset(get = "pub", get_mut = "pub")] export: Option, - /// Database configuration for the pace application - #[serde(default, skip_serializing_if = "Option::is_none")] - #[getset(get = "pub", get_mut = "pub")] - database: Option, // Optional because it's only needed if log_storage is "database" - /// Pomodoro configuration for the pace application #[serde(default, skip_serializing_if = "Option::is_none")] #[getset(get = "pub", get_mut = "pub")] @@ -65,8 +64,11 @@ impl PaceConfig { /// /// `activity_log` - The path to the activity log file pub fn set_activity_log_path(&mut self, activity_log: impl AsRef) { - *self.general_mut().activity_log_options_mut().path_mut() = - activity_log.as_ref().to_path_buf(); + *self.storage_mut() = StorageConfig { + storage: ActivityLogStorageKind::File { + location: activity_log.as_ref().to_path_buf(), + }, + }; } pub fn set_time_zone(&mut self, time_zone: Tz) { @@ -79,10 +81,6 @@ impl PaceConfig { #[getset(get = "pub")] #[serde(rename_all = "kebab-case")] pub struct GeneralConfig { - #[serde(flatten)] - #[getset(get = "pub", get_mut = "pub")] - activity_log_options: ActivityLogOptions, - /// The default category separator /// Default: `::` #[serde(default, skip_serializing_if = "Option::is_none")] @@ -105,57 +103,29 @@ pub struct GeneralConfig { default_time_zone: Option, } -#[derive(Debug, Deserialize, Serialize, Getters, MutGetters, Clone, Default)] -#[getset(get = "pub")] -#[serde(rename_all = "kebab-case")] -pub struct ActivityLogOptions { - /// The path to the activity log file - /// Default is operating system dependent - /// Use `pace setup config` to set this value initially - #[getset(get_mut = "pub")] - path: PathBuf, - - /// The format for the activity log - /// Default: `toml` - #[getset(get_mut = "pub")] - format_kind: Option, - - /// The storage type for the activity log - /// Default: `file` - storage_kind: ActivityLogStorageKind, -} - -/// The kind of activity log format -/// Default: `toml` -/// -/// Options: `toml`, `json`, `yaml` -#[derive(Debug, Deserialize, Serialize, Clone, Copy, Default, EnumString)] -#[serde(rename_all = "lowercase")] -#[non_exhaustive] -pub enum ActivityLogFormatKind { - #[default] - Toml, -} - /// The kind of log storage -/// Default: `file` -/// -/// Options: `file`, `database` -#[derive(Debug, Deserialize, Serialize, Clone, Copy, Default)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] #[non_exhaustive] pub enum ActivityLogStorageKind { - #[default] - File, - Database, - #[cfg(test)] + File { + /// The location of the activity log file + location: PathBuf, + }, + Database { + /// The database engine to use + kind: DatabaseEngineKind, + + /// The URL to the database + /// In case of [`DatabaseEngineKind::Sqlite`], this is the path to the database file + url: String, + }, InMemory, } impl Default for GeneralConfig { fn default() -> Self { Self { - activity_log_options: ActivityLogOptions::default(), category_separator: Some("::".to_string()), default_priority: Some(ItemPriorityKind::default()), most_recent_count: Some(9), @@ -195,27 +165,56 @@ pub struct ExportConfig { /// Default: `sqlite` /// /// Options: `sqlite`, `postgres`, `mysql`, `sql-server` -#[derive(Debug, Deserialize, Serialize, Clone, Copy, Default)] +#[derive( + Debug, + Deserialize, + Serialize, + Clone, + Copy, + Default, + displaydoc::Display, + EnumString, + PartialEq, + Eq, +)] #[serde(rename_all = "kebab-case")] #[non_exhaustive] pub enum DatabaseEngineKind { #[default] + /// SQLite Sqlite, + + /// Postgres Postgres, + + /// MySQL Mysql, + + /// SQL Server SqlServer, } -/// The database configuration for the pace application -#[derive(Debug, Deserialize, Default, Serialize, Getters, Clone)] +/// The storage configuration for the pace application +#[derive(Debug, Deserialize, Serialize, Getters, Clone)] #[getset(get = "pub")] #[serde(rename_all = "kebab-case")] -pub struct DatabaseConfig { - /// The connection string for the database - connection_string: String, +pub struct StorageConfig { + /// The storage location + /// In case of a file, this is the path to the file: `file://path/to/file` + /// In case of a database, this is the connection string: `sqlite://path/to/database` + #[serde(flatten)] + storage: ActivityLogStorageKind, +} - /// The kind of database engine - engine: DatabaseEngineKind, +impl Default for StorageConfig { + fn default() -> Self { + Self { + storage: ActivityLogStorageKind::Database { + kind: DatabaseEngineKind::Sqlite, + url: "./db/activities.pace.sqlite3".to_string(), + }, + } + } } /// The pomodoro configuration for the pace application @@ -336,7 +335,7 @@ pub fn find_root_config_file_path( file_name: &str, ) -> PaceResult { find_root_project_file(¤t_dir, file_name).ok_or_else(|| { - PaceErrorKind::ConfigFileNotFound { + ConfigErrorKind::ConfigFileNotFound { current_dir: current_dir.as_ref().to_string_lossy().to_string(), file_name: file_name.to_string(), } @@ -497,7 +496,7 @@ impl Display for PaceConfig { #[cfg(test)] mod tests { - use crate::error::TestResult; + use pace_error::TestResult; use super::*; use rstest::*; @@ -520,8 +519,10 @@ mod tests { config.set_activity_log_path(activity_log); assert_eq!( - config.general().activity_log_options().path(), - Path::new(activity_log) + *config.storage().storage(), + ActivityLogStorageKind::File { + location: PathBuf::from(activity_log) + } ); } diff --git a/crates/core/src/domain.rs b/crates/core/src/domain.rs index 29540f79..a75e652d 100644 --- a/crates/core/src/domain.rs +++ b/crates/core/src/domain.rs @@ -1,5 +1,7 @@ //! Domain models and business rules +pub mod id; + /// The kind of activity a user can track pub mod activity; @@ -9,6 +11,7 @@ pub mod activity_log; /// A category for activities pub mod category; +pub mod description; /// A filter for activities pub mod filter; pub mod inbox; diff --git a/crates/core/src/domain/activity.rs b/crates/core/src/domain/activity.rs index 6899fe13..6919dc91 100644 --- a/crates/core/src/domain/activity.rs +++ b/crates/core/src/domain/activity.rs @@ -8,18 +8,21 @@ use pace_time::{ date_time::PaceDateTime, duration::{calculate_duration, duration_to_str, PaceDuration}, }; +use sea_orm::DeriveActiveEnum; + use serde_derive::{Deserialize, Serialize}; -use std::{collections::HashSet, fmt::Display}; +use std::fmt::Display; use strum_macros::EnumString; use tracing::debug; use typed_builder::TypedBuilder; -use ulid::Ulid; -use crate::{ - domain::status::ActivityStatusKind, - error::{ActivityLogErrorKind, PaceResult}, +use crate::domain::{ + category::PaceCategory, description::PaceDescription, id::ActivityGuid, + status::ActivityStatusKind, tag::PaceTagCollection, }; +use pace_error::{ActivityLogErrorKind, PaceResult}; + #[derive( Debug, TypedBuilder, Serialize, Getters, Setters, MutGetters, Clone, Eq, PartialEq, Default, )] @@ -81,25 +84,35 @@ impl From<(ActivityGuid, Activity)> for ActivityItem { PartialOrd, Ord, EnumString, + sea_orm::EnumIter, + strum::Display, + DeriveActiveEnum, )] #[serde(rename_all = "kebab-case")] +#[strum(serialize_all = "kebab-case")] +#[sea_orm(rs_type = "i32", db_type = "Integer")] // #[serde(untagged)] pub enum ActivityKind { /// A generic activity #[default] + #[sea_orm(num_value = 0)] Activity, - /// A task - Task, - /// A break + #[sea_orm(num_value = 1)] Intermission, + /// A pomodoro break + #[sea_orm(num_value = 2)] + PomodoroIntermission, + /// A pomodoro work session + #[sea_orm(num_value = 3)] PomodoroWork, - /// A pomodoro break - PomodoroIntermission, + /// A task + #[sea_orm(num_value = 4)] + Task, } #[allow(clippy::trivially_copy_pass_by_ref)] @@ -199,14 +212,14 @@ pub struct Activity { #[getset(get = "pub", get_mut = "pub")] #[serde(skip_serializing_if = "Option::is_none")] #[merge(strategy = crate::util::overwrite_left_with_right)] - category: Option, + category: Option, /// The description of the activity // This needs to be an Optional, because we use the whole activity struct // as well for intermissions, which don't have a description #[builder(setter(into))] #[merge(strategy = crate::util::overwrite_left_with_right)] - description: String, + description: PaceDescription, /// The start date and time of the activity #[builder(default, setter(into))] @@ -234,7 +247,12 @@ pub struct Activity { /// Tags for the activity #[builder(default, setter(into))] #[merge(strategy = crate::util::overwrite_left_with_right)] - tags: Option>, + tags: Option, + + #[serde(default)] + #[builder(default)] + #[merge(strategy = crate::util::overwrite_left_with_right)] + status: ActivityStatusKind, // Pomodoro-specific attributes /// The pomodoro cycle of the activity @@ -242,11 +260,6 @@ pub struct Activity { #[serde(skip_serializing_if = "Option::is_none")] #[merge(strategy = crate::util::overwrite_left_with_right)] pomodoro_cycle_options: Option, - - #[serde(default)] - #[builder(default)] - #[merge(strategy = crate::util::overwrite_left_with_right)] - status: ActivityStatusKind, } #[derive( @@ -270,6 +283,11 @@ impl ActivityEndOptions { pub const fn new(end: PaceDateTime, duration: PaceDuration) -> Self { Self { end, duration } } + + #[must_use] + pub const fn as_tuple(&self) -> (PaceDateTime, PaceDuration) { + (self.end, self.duration) + } } #[derive( @@ -303,28 +321,12 @@ impl ActivityKindOptions { } } -/// The unique identifier of an activity -#[derive(Debug, Clone, Serialize, Deserialize, Ord, PartialEq, PartialOrd, Eq, Copy, Hash)] -pub struct ActivityGuid(Ulid); - -impl Display for ActivityGuid { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) - } -} - -impl Default for ActivityGuid { - fn default() -> Self { - Self(Ulid::new()) - } -} - impl Display for Activity { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { let time = self.begin.and_local_timezone(&Local); let utc_offset = time.offset(); let symbol = self.kind.as_symbol(); - let nop_cat = "Uncategorized".to_string(); + let nop_cat = PaceCategory::new("Uncategorized"); let description = self.description(); let category = self.category().as_ref().unwrap_or(&nop_cat); let started_at = duration_to_str(time); @@ -341,10 +343,7 @@ impl Activity { /// an already ended/archived/etc. activity #[must_use] pub fn new_from_self(&self) -> Self { - debug!( - "Creating a new activity from the current activity: {:?}.", - self - ); + debug!("Creating a new activity from the current activity: {self:?}."); Self::builder() .description(self.description.clone()) @@ -358,7 +357,7 @@ impl Activity { /// If the activity is held pub fn is_paused(&self) -> bool { - debug!("Checking if activity is held: {:?}", self); + debug!("Checking if activity is held: {self:?}"); self.status.is_paused() } @@ -367,7 +366,7 @@ impl Activity { /// [`is_active_intermission`] for that #[must_use] pub fn is_in_progress(&self) -> bool { - debug!("Checking if activity is active: {:?}", self); + debug!("Checking if activity is active: {self:?}"); self.activity_end_options().is_none() && (!self.kind.is_intermission() || !self.kind.is_pomodoro_intermission()) && self.status.is_in_progress() @@ -375,13 +374,13 @@ impl Activity { /// Make the activity active pub fn make_active(&mut self) { - debug!("Making activity active: {:?}", self); + debug!("Making activity active: {self:?}"); self.status = ActivityStatusKind::InProgress; } /// Make the activity inactive pub fn make_inactive(&mut self) { - debug!("Making activity inactive: {:?}", self); + debug!("Making activity inactive: {self:?}"); self.status = ActivityStatusKind::Created; } @@ -389,7 +388,7 @@ impl Activity { /// This is only possible if the activity is not active and has ended pub fn archive(&mut self) { if !self.is_in_progress() && self.is_completed() { - debug!("Archiving activity: {:?}", self); + debug!("Archiving activity: {self:?}"); self.status = ActivityStatusKind::Archived; } } @@ -398,21 +397,21 @@ impl Activity { /// This is only possible if the activity is archived pub fn unarchive(&mut self) { if self.is_archived() { - debug!("Unarchiving activity: {:?}", self); + debug!("Unarchiving activity: {self:?}"); self.status = ActivityStatusKind::Unarchived; } } /// If the activity is endable, meaning if it is active or held pub fn is_completable(&self) -> bool { - debug!("Checking if activity is endable: {:?}", self); + debug!("Checking if activity is endable: {self:?}"); self.is_in_progress() || self.is_paused() } /// If the activity is an active intermission #[must_use] pub fn is_active_intermission(&self) -> bool { - debug!("Checking if activity is an active intermission: {:?}", self); + debug!("Checking if activity is an active intermission: {self:?}"); self.activity_end_options().is_none() && (self.kind.is_intermission() || self.kind.is_pomodoro_intermission()) && self.status.is_in_progress() @@ -421,21 +420,21 @@ impl Activity { /// If the activity is archived #[must_use] pub fn is_archived(&self) -> bool { - debug!("Checking if activity is archived: {:?}", self); + debug!("Checking if activity is archived: {self:?}"); self.status.is_archived() } /// If the activity is inactive #[must_use] pub fn is_inactive(&self) -> bool { - debug!("Checking if activity is inactive: {:?}", self); + debug!("Checking if activity is inactive: {self:?}"); self.status.is_created() } /// If the activity has ended and is not archived #[must_use] pub fn is_completed(&self) -> bool { - debug!("Checking if activity has ended: {:?}", self); + debug!("Checking if activity has ended: {self:?}"); self.activity_end_options().is_some() && (!self.kind.is_intermission() || !self.kind.is_pomodoro_intermission()) && !self.is_archived() @@ -445,7 +444,7 @@ impl Activity { /// If the activity is resumable #[must_use] pub fn is_resumable(&self) -> bool { - debug!("Checking if activity is resumable: {:?}", self); + debug!("Checking if activity is resumable: {self:?}"); self.is_inactive() || self.is_archived() || self.is_paused() || self.is_completed() } @@ -530,7 +529,7 @@ impl Activity { #[getset(get = "pub")] pub struct ActivitySession { /// A description of the activity group - description: String, + description: PaceDescription, /// Root Activity within the activity group root_activity: ActivityItem, @@ -589,7 +588,7 @@ impl ActivitySession { #[getset(get = "pub")] pub struct ActivityGroup { /// A description of the activity group - description: String, + description: PaceDescription, /// Duration spent on the grouped activities, essentially the sum of all durations /// of the activities within the group and their children. Intermissions are counting @@ -622,7 +621,7 @@ impl ActivityGroup { } pub fn with_multiple_sessions( - description: String, + description: PaceDescription, activity_sessions: Vec, ) -> Self { debug!("Creating new activity group"); @@ -651,7 +650,7 @@ impl ActivityGroup { pub fn add_session(&mut self, session: ActivitySession) { debug!("Adding session to activity session"); - debug!("Session: {:#?}", session); + debug!("Session: {session:#?}"); self.intermission_duration += *session.intermission_duration(); self.adjusted_duration -= *session.adjusted_duration(); @@ -676,7 +675,7 @@ mod tests { use eyre::{eyre, OptionExt}; use pace_time::time_zone::PaceTimeZoneKind; - use crate::error::TestResult; + use pace_error::TestResult; use super::*; @@ -693,13 +692,19 @@ mod tests { let activity: Activity = toml::from_str(toml)?; - assert_eq!(activity.category.as_ref().ok_or("No category.")?, "Work"); + assert_eq!( + activity.category.as_ref().ok_or("No category.")?, + &PaceCategory::new("Work") + ); - assert_eq!(activity.description, "This is an example activity"); + assert_eq!( + activity.description, + PaceDescription::new("This is an example activity") + ); let ActivityEndOptions { end, duration } = activity .activity_end_options() - .clone() + .as_ref() .ok_or("No end options")?; let begin_time = PaceDateTime::try_from(( @@ -724,9 +729,9 @@ mod tests { assert_eq!(activity.begin, begin_time); - assert_eq!(end, end_time); + assert_eq!(end, &end_time); - assert_eq!(duration, PaceDuration::from_str("19")?); + assert_eq!(duration, &PaceDuration::from_str("19")?); assert_eq!(activity.kind, ActivityKind::Activity); diff --git a/crates/core/src/domain/activity_log.rs b/crates/core/src/domain/activity_log.rs index 51971f34..b64f2ab4 100644 --- a/crates/core/src/domain/activity_log.rs +++ b/crates/core/src/domain/activity_log.rs @@ -3,7 +3,10 @@ use rayon::iter::{FromParallelIterator, IntoParallelIterator}; use serde_derive::{Deserialize, Serialize}; use std::collections::BTreeMap; -use crate::domain::activity::{Activity, ActivityGuid, ActivityItem}; +use crate::domain::{ + activity::{Activity, ActivityItem}, + id::ActivityGuid, +}; /// The activity log entity /// @@ -62,7 +65,7 @@ impl FromParallelIterator<(ActivityGuid, Activity)> for ActivityLog { #[cfg(test)] mod tests { - use crate::error::TestResult; + use pace_error::TestResult; use super::*; use rstest::*; diff --git a/crates/core/src/domain/category.rs b/crates/core/src/domain/category.rs index 22bf5f7f..2626827e 100644 --- a/crates/core/src/domain/category.rs +++ b/crates/core/src/domain/category.rs @@ -1,18 +1,56 @@ //! Category entity and business logic +use std::{convert::Infallible, str::FromStr}; + use serde_derive::{Deserialize, Serialize}; use typed_builder::TypedBuilder; -use ulid::Ulid; -use crate::config::GeneralConfig; +use crate::{config::GeneralConfig, domain::description::PaceDescription, prelude::CategoryGuid}; + +#[derive(Debug, Serialize, Deserialize, Clone, Eq, Hash, PartialEq, Default, PartialOrd, Ord)] +pub struct PaceCategory(String); + +impl PaceCategory { + #[must_use] + pub fn new(category: &str) -> Self { + Self(category.to_owned()) + } +} +impl<'a, T: AsRef<&'a str>> From for PaceCategory { + fn from(category: T) -> Self { + Self::new(category.as_ref()) + } +} + +impl FromStr for PaceCategory { + type Err = Infallible; + + fn from_str(s: &str) -> Result { + Ok(Self::new(s)) + } +} + +impl std::fmt::Display for PaceCategory { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl std::ops::Deref for PaceCategory { + type Target = String; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} /// The category entity #[derive(Debug, Serialize, Deserialize, TypedBuilder, Clone)] -pub struct Category { +struct NewCategory { /// The category description #[builder(default, setter(strip_option))] #[serde(skip_serializing_if = "Option::is_none")] - description: Option, + description: Option, /// The category id #[builder(default = Some(CategoryGuid::default()), setter(strip_option))] @@ -26,7 +64,7 @@ pub struct Category { // TODO: Add support for subcategories #[builder(default, setter(strip_option))] #[serde(skip_serializing_if = "Option::is_none")] - subcategories: Option>, + subcategories: Option>, } /// Extracts the category and subcategory from a string @@ -40,19 +78,28 @@ pub struct Category { /// /// A tuple containing the category and subcategory #[must_use] -pub fn extract_categories(category_string: &str, separator: &str) -> (Category, Option) { +fn extract_categories( + category_string: &str, + separator: &str, +) -> (NewCategory, Option) { let parts: Vec<_> = category_string.split(separator).collect(); if parts.len() > 1 { // if there are more than one part, the first part is the category // and the rest is the subcategory ( - Category::builder().name(parts[0].to_string()).build(), - Some(Category::builder().name(parts[1..].join(separator)).build()), + NewCategory::builder().name(parts[0].to_string()).build(), + Some( + NewCategory::builder() + .name(parts[1..].join(separator)) + .build(), + ), ) } else { // if there is only one part, it's the category ( - Category::builder().name(category_string.to_owned()).build(), + NewCategory::builder() + .name(category_string.to_owned()) + .build(), None, ) } @@ -93,22 +140,12 @@ pub fn split_category_by_category_separator( } } -/// The category id -#[derive(Debug, Serialize, Deserialize, Clone, Copy)] -pub struct CategoryGuid(Ulid); - -impl Default for CategoryGuid { - fn default() -> Self { - Self(Ulid::new()) - } -} - -impl Default for Category { +impl Default for NewCategory { fn default() -> Self { Self { guid: Some(CategoryGuid::default()), name: "Uncategorized".to_string(), - description: Some("Uncategorized category".to_string()), + description: Some(PaceDescription::new("Uncategorized category")), subcategories: Option::default(), } } diff --git a/crates/core/src/domain/description.rs b/crates/core/src/domain/description.rs new file mode 100644 index 00000000..ed35c445 --- /dev/null +++ b/crates/core/src/domain/description.rs @@ -0,0 +1,40 @@ +use std::{convert::Infallible, str::FromStr}; + +use serde_derive::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Clone, Eq, Hash, PartialEq, Default, PartialOrd, Ord)] +pub struct PaceDescription(String); + +impl PaceDescription { + #[must_use] + pub fn new(description: &str) -> Self { + Self(description.to_owned()) + } +} +impl<'a, T: AsRef<&'a str>> From for PaceDescription { + fn from(description: T) -> Self { + Self::new(description.as_ref()) + } +} + +impl FromStr for PaceDescription { + type Err = Infallible; + + fn from_str(s: &str) -> Result { + Ok(Self::new(s)) + } +} + +impl std::fmt::Display for PaceDescription { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl std::ops::Deref for PaceDescription { + type Target = String; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/crates/core/src/domain/filter.rs b/crates/core/src/domain/filter.rs index 7cead0f5..61c3c96c 100644 --- a/crates/core/src/domain/filter.rs +++ b/crates/core/src/domain/filter.rs @@ -1,10 +1,8 @@ -use getset::{Getters, MutGetters, Setters}; -use pace_time::time_range::TimeRangeOptions; -use serde_derive::Serialize; use strum::EnumIter; -use typed_builder::TypedBuilder; -use crate::{commands::reflect::ReflectCommandOptions, domain::activity::ActivityGuid}; +use pace_time::time_range::TimeRangeOptions; + +use crate::domain::id::ActivityGuid; /// Filter for activities #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, EnumIter)] @@ -86,29 +84,3 @@ impl FilteredActivities { } } } - -#[derive( - Debug, TypedBuilder, Serialize, Getters, Setters, MutGetters, Clone, Eq, PartialEq, Default, -)] -#[getset(get = "pub")] -pub struct FilterOptions { - category: Option, - case_sensitive: bool, -} - -impl From for FilterOptions { - fn from(options: ReflectCommandOptions) -> Self { - Self { - category: options.category().clone(), - case_sensitive: *options.case_sensitive(), - } - } -} -impl From<&ReflectCommandOptions> for FilterOptions { - fn from(options: &ReflectCommandOptions) -> Self { - Self { - category: options.category().clone(), - case_sensitive: *options.case_sensitive(), - } - } -} diff --git a/crates/core/src/domain/id.rs b/crates/core/src/domain/id.rs new file mode 100644 index 00000000..b53dc82a --- /dev/null +++ b/crates/core/src/domain/id.rs @@ -0,0 +1,241 @@ +use std::{ + fmt::{Display, Formatter}, + str::FromStr, +}; + +use pace_error::PaceErrorKind; +use serde_derive::{Deserialize, Serialize}; +use ulid::Ulid; + +#[derive(Debug, Clone, Serialize, Deserialize, Ord, PartialEq, PartialOrd, Eq, Copy, Hash)] +pub struct Guid(Ulid); + +impl Guid { + #[must_use] + pub fn new() -> Self { + Self(Ulid::new()) + } +} + +impl Default for Guid { + fn default() -> Self { + Self::new() + } +} + +impl FromStr for Guid { + type Err = PaceErrorKind; + + fn from_str(value: &str) -> Result { + Ok(Self(Ulid::from_string(value).map_err(|source| { + PaceErrorKind::InvalidGuid { + value: value.to_string(), + source, + } + })?)) + } +} + +impl Display for Guid { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0.to_string()) + } +} + +/// The unique identifier of an activity +#[derive(Debug, Clone, Serialize, Deserialize, Ord, PartialEq, PartialOrd, Eq, Copy, Hash)] +pub struct ActivityGuid(Guid); + +impl ActivityGuid { + #[must_use] + pub fn new() -> Self { + Self(Guid::new()) + } + + #[must_use] + pub const fn with_id(id: Guid) -> Self { + Self(id) + } + + #[must_use] + pub const fn inner(&self) -> &Guid { + &self.0 + } +} + +impl Display for ActivityGuid { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl Default for ActivityGuid { + fn default() -> Self { + Self(Guid::new()) + } +} + +/// The unique identifier of a category +#[derive(Debug, Clone, Serialize, Deserialize, Ord, PartialEq, PartialOrd, Eq, Copy, Hash)] +pub struct CategoryGuid(Guid); + +impl CategoryGuid { + #[must_use] + pub fn new() -> Self { + Self(Guid::new()) + } + + #[must_use] + pub const fn with_id(id: Guid) -> Self { + Self(id) + } + + #[must_use] + pub const fn inner(&self) -> &Guid { + &self.0 + } +} + +impl Display for CategoryGuid { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl Default for CategoryGuid { + fn default() -> Self { + Self(Guid::new()) + } +} + +/// The unique identifier of a description +#[derive(Debug, Clone, Serialize, Deserialize, Ord, PartialEq, PartialOrd, Eq, Copy, Hash)] +pub struct DescriptionGuid(Guid); + +impl DescriptionGuid { + #[must_use] + pub fn new() -> Self { + Self(Guid::new()) + } + + #[must_use] + pub const fn with_id(id: Guid) -> Self { + Self(id) + } + + #[must_use] + pub const fn inner(&self) -> &Guid { + &self.0 + } +} + +impl Display for DescriptionGuid { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl Default for DescriptionGuid { + fn default() -> Self { + Self(Guid::new()) + } +} + +/// The unique identifier of an activity kind +#[derive(Debug, Clone, Serialize, Deserialize, Ord, PartialEq, PartialOrd, Eq, Copy, Hash)] +pub struct ActivityKindGuid(Guid); + +impl ActivityKindGuid { + #[must_use] + pub fn new() -> Self { + Self(Guid::new()) + } + + #[must_use] + pub const fn with_id(id: Guid) -> Self { + Self(id) + } + + #[must_use] + pub const fn inner(&self) -> &Guid { + &self.0 + } +} + +impl Display for ActivityKindGuid { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl Default for ActivityKindGuid { + fn default() -> Self { + Self(Guid::new()) + } +} + +/// The unique identifier of an activity status +#[derive(Debug, Clone, Serialize, Deserialize, Ord, PartialEq, PartialOrd, Eq, Copy, Hash)] +pub struct ActivityStatusGuid(Guid); + +impl ActivityStatusGuid { + #[must_use] + pub fn new() -> Self { + Self(Guid::new()) + } + + #[must_use] + pub const fn with_id(id: Guid) -> Self { + Self(id) + } + + #[must_use] + pub const fn inner(&self) -> &Guid { + &self.0 + } +} + +impl Display for ActivityStatusGuid { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl Default for ActivityStatusGuid { + fn default() -> Self { + Self(Guid::new()) + } +} + +/// The unique identifier of a tag +#[derive(Debug, Clone, Serialize, Deserialize, Ord, PartialEq, PartialOrd, Eq, Copy, Hash)] +pub struct TagGuid(Guid); + +impl TagGuid { + #[must_use] + pub fn new() -> Self { + Self(Guid::new()) + } + + #[must_use] + pub const fn with_id(id: Guid) -> Self { + Self(id) + } + + #[must_use] + pub const fn inner(&self) -> &Guid { + &self.0 + } +} + +impl Display for TagGuid { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl Default for TagGuid { + fn default() -> Self { + Self(Guid::new()) + } +} diff --git a/crates/core/src/domain/project.rs b/crates/core/src/domain/project.rs index 248fbc8f..a0dd4378 100644 --- a/crates/core/src/domain/project.rs +++ b/crates/core/src/domain/project.rs @@ -8,6 +8,8 @@ use serde_derive::{Deserialize, Serialize}; use typed_builder::TypedBuilder; use ulid::Ulid; +use crate::domain::description::PaceDescription; + #[derive(Serialize, Deserialize, Debug, Clone)] pub struct ProjectList { /// The tasks in the list @@ -18,14 +20,14 @@ pub struct ProjectList { #[derive(Serialize, Deserialize, Debug, Clone)] pub struct DefaultOptions { - categories: Option>, + categories: Option>, } #[derive(Serialize, Deserialize, Debug, Clone)] -pub struct Category { +pub struct ProjectCategory { id: Ulid, name: String, - description: Option, + description: Option, } #[derive(Debug, TypedBuilder, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] @@ -34,7 +36,7 @@ pub struct Project { name: String, #[serde(skip_serializing_if = "Option::is_none")] - description: Option, + description: Option, tasks_file: PathBuf, @@ -69,7 +71,7 @@ impl Default for ProjectGuid { #[cfg(test)] mod tests { - use crate::error::TestResult; + use pace_error::TestResult; use super::*; use rstest::*; diff --git a/crates/core/src/domain/reflection.rs b/crates/core/src/domain/reflection.rs index 75886b62..834c90a1 100644 --- a/crates/core/src/domain/reflection.rs +++ b/crates/core/src/domain/reflection.rs @@ -10,7 +10,10 @@ use tabled::{ use typed_builder::TypedBuilder; -use crate::domain::activity::{ActivityGroup, ActivityItem, ActivityKind}; +use crate::domain::{ + activity::{ActivityGroup, ActivityItem, ActivityKind}, + description::PaceDescription, +}; /// The kind of review format /// Default: `console` @@ -111,7 +114,7 @@ impl std::fmt::Display for ReflectionSummary { for (description, activity_group) in summary_group.activity_groups_by_description() { builder.push_record(vec![ subcategory, - description, + description.to_string().as_str(), format!( "{} ({})", &activity_group.adjusted_duration().to_string(), @@ -168,7 +171,7 @@ pub struct SummaryActivityGroup { total_break_count: usize, /// The groups of activities for a summary category - activity_groups_by_description: BTreeMap, + activity_groups_by_description: BTreeMap, } impl SummaryActivityGroup { diff --git a/crates/core/src/domain/status.rs b/crates/core/src/domain/status.rs index 8e4383b9..f452eb30 100644 --- a/crates/core/src/domain/status.rs +++ b/crates/core/src/domain/status.rs @@ -1,4 +1,6 @@ +use sea_orm::DeriveActiveEnum; use serde_derive::{Deserialize, Serialize}; +use strum::EnumString; #[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] #[serde(rename_all = "kebab-case")] @@ -16,34 +18,59 @@ pub enum TaskStatus { Waiting, } -#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +#[derive( + Debug, + Clone, + Copy, + Default, + Serialize, + Deserialize, + PartialEq, + Eq, + PartialOrd, + Ord, + EnumString, + sea_orm::EnumIter, + strum::Display, + DeriveActiveEnum, +)] #[serde(rename_all = "kebab-case")] +#[strum(serialize_all = "kebab-case")] +#[sea_orm(rs_type = "i32", db_type = "Integer")] pub enum ActivityStatusKind { /// The initial state of an activity once it's created in the system but not yet started. #[default] + #[sea_orm(num_value = 0)] Created, /// The activity is scheduled to start at a specific time. /// It remains in this state until the activity begins. + #[sea_orm(num_value = 1)] Scheduled, /// The active state of an activity. It transitions to this state from "Scheduled" when /// the activity begins or from "Paused" when it's resumed. The start time is recorded /// upon entering this state for the first time, and the resume time is noted for /// subsequent entries. + #[sea_orm(num_value = 2)] InProgress, /// Represents an activity that has been temporarily halted. /// This could apply to tasks being paused for a break or intermission. /// The activity can move back to "InProgress" when work on it resumes. + #[sea_orm(num_value = 3)] Paused, /// The final state of an activity, indicating it has been finished. /// The end time of the activity is recorded, marking its completion. + #[sea_orm(num_value = 4)] Completed, + #[sea_orm(num_value = 98)] + Unarchived, + + #[sea_orm(num_value = 99)] Archived, - Unarchived, // TODO: Do we need this or can be unarchiving done without it? } #[allow(clippy::trivially_copy_pass_by_ref)] diff --git a/crates/core/src/domain/tag.rs b/crates/core/src/domain/tag.rs index ff06ab91..d704a653 100644 --- a/crates/core/src/domain/tag.rs +++ b/crates/core/src/domain/tag.rs @@ -1,14 +1,85 @@ +use typed_builder::TypedBuilder; + +use std::{collections::HashSet, convert::Infallible, str::FromStr}; + use serde_derive::{Deserialize, Serialize}; -use typed_builder::TypedBuilder; -use ulid::Ulid; +use crate::domain::id::TagGuid; + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)] +pub struct PaceTagCollection(HashSet); + +impl FromIterator for PaceTagCollection { + fn from_iter>(iter: T) -> Self { + Self(iter.into_iter().collect()) + } +} + +impl FromIterator for PaceTagCollection { + fn from_iter>(iter: T) -> Self { + Self( + iter.into_iter() + .map(|tag_string| PaceTag::new(&tag_string)) + .collect(), + ) + } +} + +impl std::ops::DerefMut for PaceTagCollection { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl std::ops::Deref for PaceTagCollection { + type Target = HashSet; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl PaceTagCollection { + #[must_use] + pub const fn new(tags: HashSet) -> Self { + Self(tags) + } +} + +#[derive(Debug, Serialize, Deserialize, Clone, Eq, Hash, PartialEq, Default, PartialOrd, Ord)] +pub struct PaceTag(String); + +impl PaceTag { + #[must_use] + pub fn new(tag: &str) -> Self { + Self(tag.to_owned()) + } +} +impl<'a, T: AsRef<&'a str>> From for PaceTag { + fn from(tag: T) -> Self { + Self::new(tag.as_ref()) + } +} + +impl FromStr for PaceTag { + type Err = Infallible; + + fn from_str(s: &str) -> Result { + Ok(Self::new(s)) + } +} + +impl std::fmt::Display for PaceTag { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Hash, Clone)] -pub struct TagGuid(Ulid); +impl std::ops::Deref for PaceTag { + type Target = String; -impl Default for TagGuid { - fn default() -> Self { - Self(Ulid::new()) + fn deref(&self) -> &Self::Target { + &self.0 } } @@ -22,6 +93,7 @@ pub struct Tag { } impl Tag { + #[must_use] pub const fn new(guid: Option, text: String) -> Self { Self { guid, text } } diff --git a/crates/core/src/domain/task.rs b/crates/core/src/domain/task.rs index b3c32f8f..d103ce8b 100644 --- a/crates/core/src/domain/task.rs +++ b/crates/core/src/domain/task.rs @@ -10,13 +10,13 @@ use serde_derive::{Deserialize, Serialize}; use typed_builder::TypedBuilder; use ulid::Ulid; -use crate::domain::{priority::ItemPriorityKind, status::TaskStatus}; +use crate::domain::{description::PaceDescription, priority::ItemPriorityKind, status::TaskStatus}; #[derive(Debug, TypedBuilder, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone)] pub struct Task { created_at: NaiveDateTime, - description: String, + description: PaceDescription, #[builder(default, setter(strip_option))] #[serde(skip_serializing_if = "Option::is_none")] @@ -58,7 +58,7 @@ impl Default for TaskGuid { #[cfg(test)] mod tests { - use crate::error::TestResult; + use pace_error::TestResult; use super::*; use rstest::*; diff --git a/crates/core/src/error.rs b/crates/core/src/error.rs deleted file mode 100644 index 9582667d..00000000 --- a/crates/core/src/error.rs +++ /dev/null @@ -1,329 +0,0 @@ -//! Error types and Result module. - -use displaydoc::Display; -use miette::Diagnostic; -use pace_time::error::PaceTimeErrorKind; -use std::{error::Error, io, path::PathBuf}; -use thiserror::Error; - -use crate::domain::activity::{Activity, ActivityGuid}; - -/// Result type that is being returned from test functions and methods that can fail and thus have errors. -pub type TestResult = Result>; - -/// Result type that is being returned from methods that can fail and thus have [`PaceError`]s. -pub type PaceResult = Result; - -/// Result type that is being returned from methods that have optional return values and can fail thus having [`PaceError`]s. -pub type PaceOptResult = PaceResult>; - -/// User message type that is being returned from methods that need to print a message to the user. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct UserMessage { - /// The message to be printed to the user - msg: String, -} - -impl std::fmt::Display for UserMessage { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.msg) - } -} - -impl UserMessage { - pub fn new(msg: impl Into) -> Self { - Self { msg: msg.into() } - } - - pub fn display(&self) { - println!("{}", self.msg); - } -} - -impl std::ops::DerefMut for UserMessage { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.msg - } -} - -impl std::ops::Deref for UserMessage { - type Target = String; - - fn deref(&self) -> &Self::Target { - &self.msg - } -} - -// [`Error`] is public, but opaque and easy to keep compatible. -/// Errors that can result from pace. -#[derive(Error, Debug, Diagnostic)] -#[diagnostic(url(docsrs))] -pub struct PaceError(#[from] PaceErrorKind); - -impl std::fmt::Display for PaceError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -// Accessors for anything we do want to expose publicly. -impl PaceError { - /// Expose the inner error kind. - /// - /// This is useful for matching on the error kind. - #[must_use] - pub fn into_inner(self) -> PaceErrorKind { - self.0 - } - - /// Is this error related to a resumable activity so that we can prompt the user? - /// - /// This is useful for matching on the error kind. - #[must_use] - pub const fn possible_new_activity_from_resume(&self) -> bool { - matches!( - self.0, - PaceErrorKind::ActivityLog(ActivityLogErrorKind::NoHeldActivityFound(_)) - ) || matches!( - self.0, - PaceErrorKind::ActivityLog(ActivityLogErrorKind::ActivityAlreadyEnded(_)) - ) || matches!( - self.0, - PaceErrorKind::ActivityLog(ActivityLogErrorKind::ActivityAlreadyArchived(_)) - ) - } -} - -/// [`PaceErrorKind`] describes the errors that can happen while executing a high-level command. -/// -/// This is a non-exhaustive enum, so additional variants may be added in future. It is -/// recommended to match against the wildcard `_` instead of listing all possible variants, -/// to avoid problems when new variants are added. -#[non_exhaustive] -#[derive(Error, Debug, Display)] -pub enum PaceErrorKind { - // /// [`CommandErrorKind`] describes the errors that can happen while executing a high-level command - // #[error(transparent)] - // Command(#[from] CommandErrorKind), - /// [`std::io::Error`] - #[error(transparent)] - StdIo(#[from] std::io::Error), - - /// Serialization to TOML failed: `{0}` - #[error(transparent)] - SerializationToTomlFailed(#[from] toml::ser::Error), - - /// Deserialization from TOML failed: `{0}` - #[error(transparent)] - DeserializationFromTomlFailed(#[from] toml::de::Error), - - /// Activity store error: `{0}` - #[error(transparent)] - ActivityStore(#[from] ActivityStoreErrorKind), - - /// Activity log error: `{0}` - #[error(transparent)] - ActivityLog(#[from] ActivityLogErrorKind), - - /// Time related error: `{0}` - #[error(transparent)] - PaceTime(#[from] PaceTimeErrorKind), - - /// JSON error: `{0}` - #[error(transparent)] - Json(#[from] serde_json::Error), - - // /// SQLite error: {0} - // #[error(transparent)] - // #[cfg(feature = "sqlite")] - // SQLite(#[from] diesel::ConnectionError), - /// Chrono parse error: `{0}` - #[error(transparent)] - ChronoParse(#[from] chrono::ParseError), - - /// Time chosen is not valid, because it lays before the current activity's beginning: `{0}` - #[error(transparent)] - ChronoDurationIsNegative(#[from] chrono::OutOfRangeError), - - /// Config file {file_name} not found in directory hierarchy starting from {current_dir} - ConfigFileNotFound { - /// The current directory - current_dir: String, - - /// The file name - file_name: String, - }, - - /// Configuration file not found, please run `pace setup config` to initialize `pace` - ParentDirNotFound(PathBuf), - - /// Database storage not implemented, yet! - DatabaseStorageNotImplemented, - - /// There is no path available to store the activity log - NoPathAvailable, - - /// {0} - #[error(transparent)] - Template(#[from] TemplatingErrorKind), -} - -/// [`ActivityLogErrorKind`] describes the errors that can happen while dealing with the activity log. -#[non_exhaustive] -#[derive(Error, Debug, Display)] -pub enum ActivityLogErrorKind { - /// No activities found in the activity log - NoActivitiesFound, - - /// Activity with ID {0} not found - FailedToReadActivity(ActivityGuid), - - /// Negative duration for activity - NegativeDuration, - - /// There are no activities to hold - NoActivityToHold, - - /// Failed to unwrap Arc - ArcUnwrapFailed, - - /// There are no unfinished activities to end - NoUnfinishedActivities, - - /// There is no cache to sync - NoCacheToSync, - - /// Cache not available - CacheNotAvailable, - - /// `Activity` with id {0} not found - ActivityNotFound(ActivityGuid), - - /// `Activity` with id {0} can't be removed from the activity log - ActivityCantBeRemoved(usize), - - /// This activity has no id - ActivityIdNotSet, - - /// `Activity` with id {0} already in use, can't create a new activity with the same id - ActivityIdAlreadyInUse(ActivityGuid), - - /// `Activity` in the `ActivityLog` has a different id than the one provided: {0} != {1} - ActivityIdMismatch(ActivityGuid, ActivityGuid), - - /// `Activity` already has an intermission: {0} - ActivityAlreadyHasIntermission(Box), - - /// There have been some activities that have not been ended - ActivityNotEnded, - - /// No active activity found with id {0} - NoActiveActivityFound(ActivityGuid), - - /// `Activity` with id {0} already ended - ActivityAlreadyEnded(ActivityGuid), - - /// Activity with id {0} already has been archived - ActivityAlreadyArchived(ActivityGuid), - - /// Active activity with id {0} found, although we wanted a held activity - ActiveActivityFound(ActivityGuid), - - /// Activity with id {0} is not held, but we wanted to resume it - NoHeldActivityFound(ActivityGuid), - - /// No activity kind options found for activity with id {0} - ActivityKindOptionsNotFound(ActivityGuid), - - /// `ParentId` not set for activity with id {0} - ParentIdNotSet(ActivityGuid), - - /// Category not set for activity with id {0} - CategoryNotSet(ActivityGuid), - - /// No active activity to adjust - NoActiveActivityToAdjust, - - /// Failed to group activities by keywords - FailedToGroupByKeywords, - - /// No end options found for activity - NoEndOptionsFound, -} - -/// [`TemplatingErrorKind`] describes the errors that can happen while dealing with templating. -#[non_exhaustive] -#[derive(Error, Debug, Display)] -pub enum TemplatingErrorKind { - /// Failed to generate context from serializable struct: {0} - FailedToGenerateContextFromSerialize(tera::Error), - - /// Failed to render template: {0} - RenderingToTemplateFailed(tera::Error), - - /// Failed to read template file: {0} - FailedToReadTemplateFile(io::Error), - - /// Template file not specified - TemplateFileNotSpecified, -} - -/// [`ActivityStoreErrorKind`] describes the errors that can happen while dealing with time. -#[non_exhaustive] -#[derive(Error, Debug, Display)] -pub enum ActivityStoreErrorKind { - /// Failed to list activities by id - ListActivitiesById, - - /// Failed to group activities by duration range - GroupByDurationRange, - - /// Failed to group activities by start date - GroupByStartDate, - - /// Failed to list activities with intermissions - ListActivitiesWithIntermissions, - - /// Failed to group activities by keywords - GroupByKeywords, - - /// Failed to group activities by kind - GroupByKind, - - /// Failed to list activities by time range - ListActivitiesByTimeRange, - - /// Failed to populate `ActivityStore` cache - PopulatingCache, - - /// Failed to list activities for activity: {0} - ListIntermissionsForActivity(ActivityGuid), - - /// Missing category for activity: {0} - MissingCategoryForActivity(ActivityGuid), -} - -trait PaceErrorMarker: Error {} - -impl PaceErrorMarker for std::io::Error {} -impl PaceErrorMarker for toml::de::Error {} -impl PaceErrorMarker for toml::ser::Error {} -impl PaceErrorMarker for serde_json::Error {} -#[cfg(feature = "sqlite")] -impl PaceErrorMarker for diesel::ConnectionError {} -impl PaceErrorMarker for chrono::ParseError {} -impl PaceErrorMarker for chrono::OutOfRangeError {} -impl PaceErrorMarker for ActivityLogErrorKind {} -impl PaceErrorMarker for PaceTimeErrorKind {} -impl PaceErrorMarker for ActivityStoreErrorKind {} -impl PaceErrorMarker for TemplatingErrorKind {} - -impl From for PaceError -where - E: PaceErrorMarker, - PaceErrorKind: From, -{ - fn from(value: E) -> Self { - Self(PaceErrorKind::from(value)) - } -} diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 1ff8e1a5..1cdfaef1 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -1,13 +1,11 @@ //! # Pace Core -pub(crate) mod commands; -pub(crate) mod config; -pub(crate) mod domain; -pub(crate) mod error; -pub(crate) mod service; -pub(crate) mod storage; -pub(crate) mod template; -pub(crate) mod util; +pub mod config; +pub mod domain; +pub mod options; +pub mod storage; +pub mod template; +pub mod util; // Constants pub mod constants { @@ -28,45 +26,37 @@ pub use toml; pub mod prelude { // Public Prelude API pub use crate::{ - commands::{ - adjust::AdjustCommandOptions, - begin::BeginCommandOptions, - docs::DocsCommandOptions, - end::EndCommandOptions, - hold::{HoldCommandOptions, HoldOptions}, - now::NowCommandOptions, - reflect::{ExpensiveFlags, ReflectCommandOptions}, - resume::{ResumeCommandOptions, ResumeOptions}, - DeleteOptions, EndOptions, KeywordOptions, UpdateOptions, - }, config::{ find_root_config_file_path, find_root_project_file, get_activity_log_paths, get_config_paths, get_home_activity_log_path, get_home_config_path, - ActivityLogFormatKind, ActivityLogStorageKind, AutoArchivalConfig, DatabaseConfig, - ExportConfig, GeneralConfig, InboxConfig, PaceConfig, PomodoroConfig, - ReflectionsConfig, + ActivityLogStorageKind, AutoArchivalConfig, DatabaseEngineKind, ExportConfig, + GeneralConfig, InboxConfig, PaceConfig, PomodoroConfig, ReflectionsConfig, + StorageConfig, }, domain::{ activity::{ - Activity, ActivityEndOptions, ActivityGroup, ActivityGuid, ActivityItem, - ActivityKind, ActivityKindOptions, ActivitySession, + Activity, ActivityEndOptions, ActivityGroup, ActivityItem, ActivityKind, + ActivityKindOptions, ActivitySession, }, activity_log::ActivityLog, - category::split_category_by_category_separator, - filter::{ActivityFilterKind, FilterOptions, FilteredActivities}, + category::{split_category_by_category_separator, PaceCategory}, + description::PaceDescription, + filter::{ActivityFilterKind, FilteredActivities}, + id::{ + ActivityGuid, ActivityKindGuid, ActivityStatusGuid, CategoryGuid, DescriptionGuid, + Guid, TagGuid, + }, intermission::IntermissionAction, reflection::{ Highlights, ReflectionSummary, ReflectionsFormatKind, SummaryActivityGroup, SummaryCategories, SummaryGroupByCategory, }, status::ActivityStatusKind, + tag::{PaceTag, PaceTagCollection}, }, - error::{PaceError, PaceErrorKind, PaceOptResult, PaceResult, TestResult, UserMessage}, - service::{activity_store::ActivityStore, activity_tracker::ActivityTracker}, storage::{ - file::TomlActivityStorage, get_storage_from_config, in_memory::InMemoryActivityStorage, ActivityQuerying, ActivityReadOps, ActivityStateManagement, ActivityStorage, - ActivityWriteOps, StorageKind, SyncStorage, + ActivityWriteOps, SyncStorage, }, util::overwrite_left_with_right, }; diff --git a/crates/core/src/commands.rs b/crates/core/src/options.rs similarity index 50% rename from crates/core/src/commands.rs rename to crates/core/src/options.rs index 9a6b2911..1baa3f1c 100644 --- a/crates/core/src/commands.rs +++ b/crates/core/src/options.rs @@ -1,17 +1,29 @@ -pub mod adjust; -pub mod begin; -pub mod docs; -pub mod end; -pub mod hold; -pub mod now; -pub mod reflect; -pub mod resume; - -use getset::Getters; +use getset::{Getters, MutGetters, Setters}; use pace_time::date_time::PaceDateTime; +use serde_derive::Serialize; use typed_builder::TypedBuilder; -use crate::commands::{hold::HoldOptions, resume::ResumeOptions}; +use crate::domain::{ + category::PaceCategory, description::PaceDescription, intermission::IntermissionAction, +}; + +/// Options for holding an activity +#[derive(Debug, Clone, PartialEq, TypedBuilder, Eq, Hash, Default, Getters)] +#[getset(get = "pub")] +#[non_exhaustive] +pub struct HoldOptions { + /// The action to take on the intermission + #[builder(default)] + action: IntermissionAction, + + /// The start time of the intermission + #[builder(default, setter(into))] + begin_time: PaceDateTime, + + /// The reason for holding the activity + #[builder(default, setter(into))] + reason: Option, +} /// Options for ending an activity #[derive(Debug, Clone, PartialEq, TypedBuilder, Eq, Hash, Default, Getters)] @@ -56,5 +68,24 @@ pub struct DeleteOptions {} #[non_exhaustive] pub struct KeywordOptions { #[builder(default, setter(into, strip_option))] - category: Option, + category: Option, +} + +/// Options for resuming an activity +#[derive(Debug, Clone, PartialEq, TypedBuilder, Eq, Hash, Default, Getters)] +#[getset(get = "pub")] +#[non_exhaustive] +pub struct ResumeOptions { + /// The resume time of the intermission + #[builder(default, setter(into))] + resume_time: Option, +} + +#[derive( + Debug, TypedBuilder, Serialize, Getters, Setters, MutGetters, Clone, Eq, PartialEq, Default, +)] +#[getset(get = "pub")] +pub struct FilterOptions { + category: Option, + case_sensitive: bool, } diff --git a/crates/core/src/service.rs b/crates/core/src/service.rs deleted file mode 100644 index 5e21f2a6..00000000 --- a/crates/core/src/service.rs +++ /dev/null @@ -1,7 +0,0 @@ -/// An activity store service -/// -/// This module contains the domain logic for tracking activities and their intermissions. -/// -pub mod activity_store; - -pub mod activity_tracker; diff --git a/crates/core/src/storage.rs b/crates/core/src/storage.rs index 91d41d62..e24c36ba 100644 --- a/crates/core/src/storage.rs +++ b/crates/core/src/storage.rs @@ -1,88 +1,37 @@ -use std::{collections::BTreeMap, fmt::Display, sync::Arc}; +// #[cfg(feature = "rusqlite")] +// pub mod rusqlite; -use enum_dispatch::enum_dispatch; use itertools::Itertools; -use pace_time::{date::PaceDate, duration::PaceDurationRange, time_range::TimeRangeOptions}; +use std::{ + collections::BTreeMap, + fmt::{self, Debug, Formatter}, +}; use tracing::debug; +use pace_error::{PaceOptResult, PaceResult}; +use pace_time::{date::PaceDate, duration::PaceDurationRange, time_range::TimeRangeOptions}; + use crate::{ - commands::{ - hold::HoldOptions, resume::ResumeOptions, DeleteOptions, EndOptions, KeywordOptions, - UpdateOptions, - }, - config::{ActivityLogStorageKind, PaceConfig}, domain::{ - activity::{Activity, ActivityGuid, ActivityItem, ActivityKind}, + activity::{Activity, ActivityItem, ActivityKind}, filter::{ActivityFilterKind, FilteredActivities}, + id::ActivityGuid, status::ActivityStatusKind, }, - error::{PaceErrorKind, PaceOptResult, PaceResult}, - service::activity_store::ActivityStore, - storage::{file::TomlActivityStorage, in_memory::InMemoryActivityStorage}, + options::{ + DeleteOptions, EndOptions, HoldOptions, KeywordOptions, ResumeOptions, UpdateOptions, + }, }; -/// A type of storage that can be synced to a persistent medium - a file -pub mod file; - -/// An in-memory storage backend for activities. -pub mod in_memory; -// TODO: Implement conversion FromSQL and ToSQL -// #[cfg(feature = "sqlite")] -// pub mod sqlite; - -/// Get the storage backend from the configuration. -/// -/// # Arguments -/// -/// * `config` - The application configuration. -/// -/// # Errors -/// -/// This function should return an error if the storage backend cannot be created or is not supported. -/// -/// # Returns -/// -/// The storage backend. -pub fn get_storage_from_config(config: &PaceConfig) -> PaceResult> { - let storage: StorageKind = match config.general().activity_log_options().storage_kind() { - ActivityLogStorageKind::File => { - TomlActivityStorage::new(config.general().activity_log_options().path())?.into() - } - ActivityLogStorageKind::Database => { - return Err(PaceErrorKind::DatabaseStorageNotImplemented.into()) - } - #[cfg(test)] - ActivityLogStorageKind::InMemory => InMemoryActivityStorage::new().into(), - }; - - debug!("Using storage backend: {}", storage); - - Ok(Arc::new(storage)) -} - -#[enum_dispatch] -pub enum StorageKind { - ActivityStore, - InMemoryActivityStorage, - TomlActivityStorage, -} - -impl Display for StorageKind { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::ActivityStore(_) => write!(f, "StorageKind: ActivityStore"), - Self::InMemoryActivityStorage(_) => { - write!(f, "StorageKind: InMemoryActivityStorage") - } - Self::TomlActivityStorage(_) => write!(f, "StorageKind: TomlActivityStorage"), - } +impl Debug for dyn ActivityStorage { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "ActivityStorage: {}", self.identify()) } } /// A type of storage that can be synced to a persistent medium. /// /// This is useful for in-memory storage that needs to be persisted to disk or a database. -#[enum_dispatch(StorageKind)] pub trait SyncStorage { /// Sync the storage to a persistent medium. /// @@ -101,7 +50,6 @@ pub trait SyncStorage { /// /// Storage backends can be in-memory, on-disk, or in a database. They can be any kind of /// persistent storage that can be used to store activities. -#[enum_dispatch(StorageKind)] pub trait ActivityStorage: ActivityReadOps + ActivityWriteOps + ActivityStateManagement + SyncStorage + ActivityQuerying // TODO!: Implement other traits @@ -120,7 +68,29 @@ pub trait ActivityStorage: /// # Errors /// /// This function should return an error if the storage backend cannot be setup. - fn setup_storage(&self) -> PaceResult<()>; + fn setup(&self) -> PaceResult<()>; + + /// Teardown the storage backend. This is called once when the application stops. + /// + /// This is where you would close the database connection, save the file, etc. + /// + /// # Errors + /// + /// This function should return an error if the storage backend cannot be torn down. + /// + /// # Returns + /// + /// If the storage backend was torn down successfully it should return `Ok(())`. + fn teardown(&self) -> PaceResult<()>; + + /// Identify the storage backend. + /// + /// This is useful for logging and debugging purposes. + /// + /// # Returns + /// + /// The identifier of the storage backend. + fn identify(&self) -> String; } /// Basic Read Operations for Activities in the storage backend. @@ -128,7 +98,6 @@ pub trait ActivityStorage: /// Read operations are essential for loading activities from the storage backend. /// These operations are used to get activities by their ID, list all activities, or filter activities by a specific criterion. /// They are also used to get the current state of activities, such as the currently active activities. -#[enum_dispatch(StorageKind)] pub trait ActivityReadOps { /// Read an activity from the storage backend. /// @@ -143,7 +112,7 @@ pub trait ActivityReadOps { /// # Returns /// /// The activity that was read from the storage backend. If no activity is found, it should return `Ok(None)`. - fn read_activity(&self, activity_id: ActivityGuid) -> PaceResult; + fn read_activity(&self, activity_id: ActivityGuid) -> PaceOptResult; /// List activities from the storage backend. /// @@ -165,7 +134,6 @@ pub trait ActivityReadOps { /// /// CUD stands for Create, Update, and Delete. These are the basic operations that can be performed on activities. /// These operations are essential for managing activities in the storage backend. -#[enum_dispatch(StorageKind)] pub trait ActivityWriteOps: ActivityReadOps { /// Create an activity in the storage backend. /// @@ -238,7 +206,6 @@ pub trait ActivityWriteOps: ActivityReadOps { /// This is useful for keeping track of the current state of activities and making sure they are properly managed. /// /// For example, you might want to start a new activity, end an activity that is currently running, or hold an activity temporarily. -#[enum_dispatch(StorageKind)] pub trait ActivityStateManagement: ActivityReadOps + ActivityWriteOps + ActivityQuerying { /// Begin an activity in the storage backend. This makes the activity active. /// @@ -423,7 +390,6 @@ pub trait ActivityStateManagement: ActivityReadOps + ActivityWriteOps + Activity /// /// For example, you might want to list all activities that are currently active, /// find all activities within a specific date range, or get a specific activity by its ID. -#[enum_dispatch(StorageKind)] pub trait ActivityQuerying: ActivityReadOps { /// Group activities by predefined duration ranges (e.g., short, medium, long). /// @@ -619,10 +585,7 @@ pub trait ActivityQuerying: ActivityReadOps { }; if filtered.len() > count { - debug!( - "Found more than {} recent activities, dropping some...", - count - ); + debug!("Found more than {count} recent activities, dropping some..."); Ok(Some( (*filtered) @@ -654,19 +617,21 @@ pub trait ActivityQuerying: ActivityReadOps { /// /// If the activity is active, it should return `Ok(true)`. If it is not active, it should return `Ok(false)`. fn is_activity_active(&self, activity_id: ActivityGuid) -> PaceResult { - let activity = self.read_activity(activity_id)?; - - debug!( - "Checking if Activity with id {:?} is active: {}", - activity_id, - if activity.activity().is_in_progress() { - "yes" - } else { - "no" - } - ); - - Ok(activity.activity().is_in_progress()) + if let Some(activity) = self.read_activity(activity_id)? { + debug!( + "Checking if Activity with id {:?} is active: {}", + activity_id, + if activity.activity().is_in_progress() { + "yes" + } else { + "no" + } + ); + + Ok(activity.activity().is_in_progress()) + } else { + Ok(false) + } } /// List all intermissions for an activity id from the storage backend. @@ -698,27 +663,26 @@ pub trait ActivityQuerying: ActivityReadOps { let intermissions = filtered .iter() .filter_map(|activity| { - let activity_item = self.read_activity(*activity).ok()?; - - if activity_item.activity().parent_id() == Some(activity_id) { - debug!("Found intermission for activity: {}", activity_id); - Some(activity_item) + if let Some(activity_item) = self.read_activity(*activity).ok()? { + if activity_item.activity().parent_id() == Some(activity_id) { + debug!("Found intermission for activity: {activity_id}"); + Some(activity_item) + } else { + debug!("Not an intermission for activity: {activity_id}"); + None + } } else { - debug!("Not an intermission for activity: {}", activity_id); None } }) .collect::>(); if intermissions.is_empty() { - debug!("No intermissions found for activity: {}", activity_id); + debug!("No intermissions found for activity: {activity_id}"); return Ok(None); } - debug!( - "Activity with id {:?} has intermissions: {:?}", - activity_id, intermissions - ); + debug!("Activity with id {activity_id:?} has intermissions: {intermissions:?}"); Ok(Some(intermissions)) } @@ -744,27 +708,22 @@ pub trait ActivityQuerying: ActivityReadOps { let guids = self.list_active_intermissions()?.map(|log| { log.iter() .filter_map(|active_intermission_id| { - if self - .read_activity(*active_intermission_id) - .ok()? - .activity() - .parent_id() - == Some(activity_id) - { - debug!("Found active intermission for activity: {}", activity_id); - Some(*active_intermission_id) + if let Some(activity) = self.read_activity(*active_intermission_id).ok()? { + if activity.activity().parent_id() == Some(activity_id) { + debug!("Found active intermission for activity: {activity_id}"); + Some(*active_intermission_id) + } else { + debug!("No active intermission found for activity: {activity_id}"); + None + } } else { - debug!("No active intermission found for activity: {}", activity_id); None } }) .collect::>() }); - debug!( - "Activity with id {:?} has active intermissions: {:?}", - activity_id, guids - ); + debug!("Activity with id {activity_id:?} has active intermissions: {guids:?}"); Ok(guids) } @@ -787,21 +746,23 @@ pub trait ActivityQuerying: ActivityReadOps { return Ok(None); }; - current + Ok(current .into_iter() .sorted() .rev() .find(|activity_id| { self.read_activity(*activity_id) - .map(|activity| { + .ok() + .flatten() + .is_some_and(|activity| { activity.activity().is_in_progress() && activity.activity().kind().is_activity() && !activity.activity().is_active_intermission() }) - .unwrap_or(false) }) .map(|activity_id| self.read_activity(activity_id)) - .transpose() + .transpose()? + .flatten()) } /// Get the latest held activity. @@ -822,21 +783,23 @@ pub trait ActivityQuerying: ActivityReadOps { return Ok(None); }; - current + Ok(current .into_iter() .sorted() .rev() .find(|activity_id| { self.read_activity(*activity_id) - .map(|activity| { + .ok() + .flatten() + .is_some_and(|activity| { activity.activity().is_paused() && activity.activity().kind().is_activity() && !activity.activity().is_active_intermission() }) - .unwrap_or(false) }) .map(|activity_id| self.read_activity(activity_id)) - .transpose() + .transpose()? + .flatten()) } } diff --git a/crates/core/src/storage/rusqlite.rs b/crates/core/src/storage/rusqlite.rs new file mode 100644 index 00000000..48c9465b --- /dev/null +++ b/crates/core/src/storage/rusqlite.rs @@ -0,0 +1,189 @@ +use std::str::FromStr; + +use rusqlite::{types::FromSql, ToSql}; + +// TODO: handle ActivityEndOptions +// TODO: handle ActivityKindOptions +// TODO: handle PaceTagCollection + +use crate::{ + domain::id::{ + ActivityGuid, ActivityKindGuid, ActivityStatusGuid, CategoryGuid, DescriptionGuid, Guid, + TagGuid, + }, + prelude::{ActivityKind, ActivityStatusKind, PaceCategory, PaceDescription, PaceTag}, +}; + +impl ToSql for ActivityGuid { + fn to_sql(&self) -> rusqlite::Result> { + self.inner().to_sql() + } +} + +impl FromSql for ActivityGuid { + fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult { + Ok(Self::with_id(Guid::from_str(value.as_str()?).map_err( + |err| rusqlite::types::FromSqlError::Other(Box::new(err)), + )?)) + } +} + +impl ToSql for ActivityKind { + fn to_sql(&self) -> rusqlite::Result> { + Ok(rusqlite::types::ToSqlOutput::Owned( + rusqlite::types::Value::Text(self.to_string()), + )) + } +} + +impl FromSql for ActivityKind { + fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult { + Self::from_str(value.as_str()?) + .map_err(|err| rusqlite::types::FromSqlError::Other(Box::new(err))) + } +} + +impl ToSql for ActivityStatusKind { + fn to_sql(&self) -> rusqlite::Result> { + Ok(rusqlite::types::ToSqlOutput::Owned( + rusqlite::types::Value::Text(self.to_string()), + )) + } +} + +impl FromSql for ActivityStatusKind { + fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult { + Self::from_str(value.as_str()?) + .map_err(|err| rusqlite::types::FromSqlError::Other(Box::new(err))) + } +} + +impl ToSql for Guid { + fn to_sql(&self) -> rusqlite::Result> { + Ok(rusqlite::types::ToSqlOutput::Owned( + rusqlite::types::Value::Text(self.to_string()), + )) + } +} + +impl FromSql for Guid { + fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult { + Self::from_str(value.as_str()?) + .map_err(|err| rusqlite::types::FromSqlError::Other(Box::new(err))) + } +} + +impl ToSql for PaceCategory { + fn to_sql(&self) -> rusqlite::Result> { + Ok(rusqlite::types::ToSqlOutput::Owned( + rusqlite::types::Value::Text(self.to_string()), + )) + } +} + +impl FromSql for PaceCategory { + fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult { + Self::from_str(value.as_str()?) + .map_err(|err| rusqlite::types::FromSqlError::Other(Box::new(err))) + } +} + +impl ToSql for PaceDescription { + fn to_sql(&self) -> rusqlite::Result> { + Ok(rusqlite::types::ToSqlOutput::Owned( + rusqlite::types::Value::Text(self.to_string()), + )) + } +} + +impl FromSql for PaceDescription { + fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult { + Self::from_str(value.as_str()?) + .map_err(|err| rusqlite::types::FromSqlError::Other(Box::new(err))) + } +} + +impl ToSql for PaceTag { + fn to_sql(&self) -> rusqlite::Result> { + Ok(rusqlite::types::ToSqlOutput::Owned( + rusqlite::types::Value::Text(self.to_string()), + )) + } +} + +impl FromSql for PaceTag { + fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult { + Self::from_str(value.as_str()?) + .map_err(|err| rusqlite::types::FromSqlError::Other(Box::new(err))) + } +} + +impl ToSql for ActivityKindGuid { + fn to_sql(&self) -> rusqlite::Result> { + self.inner().to_sql() + } +} + +impl FromSql for ActivityKindGuid { + fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult { + Ok(Self::with_id(Guid::from_str(value.as_str()?).map_err( + |err| rusqlite::types::FromSqlError::Other(Box::new(err)), + )?)) + } +} + +impl ToSql for ActivityStatusGuid { + fn to_sql(&self) -> rusqlite::Result> { + self.inner().to_sql() + } +} + +impl FromSql for ActivityStatusGuid { + fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult { + Ok(Self::with_id(Guid::from_str(value.as_str()?).map_err( + |err| rusqlite::types::FromSqlError::Other(Box::new(err)), + )?)) + } +} + +impl ToSql for CategoryGuid { + fn to_sql(&self) -> rusqlite::Result> { + self.inner().to_sql() + } +} + +impl FromSql for CategoryGuid { + fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult { + Ok(Self::with_id(Guid::from_str(value.as_str()?).map_err( + |err| rusqlite::types::FromSqlError::Other(Box::new(err)), + )?)) + } +} + +impl ToSql for DescriptionGuid { + fn to_sql(&self) -> rusqlite::Result> { + self.inner().to_sql() + } +} + +impl FromSql for DescriptionGuid { + fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult { + Ok(Self::with_id(Guid::from_str(value.as_str()?).map_err( + |err| rusqlite::types::FromSqlError::Other(Box::new(err)), + )?)) + } +} + +impl ToSql for TagGuid { + fn to_sql(&self) -> rusqlite::Result> { + self.inner().to_sql() + } +} + +impl FromSql for TagGuid { + fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult { + Ok(Self::with_id(Guid::from_str(value.as_str()?).map_err( + |err| rusqlite::types::FromSqlError::Other(Box::new(err)), + )?)) + } +} diff --git a/crates/core/src/storage/sqlite.rs b/crates/core/src/storage/sqlite.rs deleted file mode 100644 index e6c252c1..00000000 --- a/crates/core/src/storage/sqlite.rs +++ /dev/null @@ -1,11 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use crate::{ - domain::activity::{Activity, ActivityId, ActivityLog}, - error::PaceResult, - storage::ActivityStorage, -}; - -struct SqliteActivityStorage { - conn: Connection, -} diff --git a/crates/core/src/template.rs b/crates/core/src/template.rs index e8193fba..ddd78dfd 100644 --- a/crates/core/src/template.rs +++ b/crates/core/src/template.rs @@ -20,6 +20,15 @@ pub static TEMPLATES: Lazy = Lazy::new(|| { }); /// Returns the human duration of the argument. +/// +/// # Errors +/// +/// Returns an error if the argument is not a valid `PaceDuration`. +/// +/// # Returns +/// +/// Returns a `Value` with the human readable duration. +#[allow(clippy::implicit_hasher)] pub fn human_duration(value: &Value, _: &HashMap) -> Result { let Ok(duration) = from_value::(value.clone()) else { return Err(Error::msg(format!( @@ -36,6 +45,7 @@ pub struct PaceReflectionTemplate { } impl PaceReflectionTemplate { + #[must_use] pub fn into_context(self) -> Context { self.context } diff --git a/crates/core/src/util.rs b/crates/core/src/util.rs index 316e604a..e1fefe2f 100644 --- a/crates/core/src/util.rs +++ b/crates/core/src/util.rs @@ -13,7 +13,7 @@ pub fn overwrite_left_with_right(left: &mut T, right: T) { #[cfg(test)] mod tests { - use crate::domain::activity::Activity; + use crate::domain::{activity::Activity, category::PaceCategory}; use super::*; @@ -37,8 +37,8 @@ mod tests { fn test_overwrite_activity_passes() { let mut left = Activity::default(); let mut right = Activity::default(); - _ = right.category_mut().replace("right".to_string()); + _ = right.category_mut().replace(PaceCategory::new("right")); overwrite_left_with_right(&mut left, right); - assert_eq!(left.category(), &Some("right".to_string())); + assert_eq!(left.category(), &Some(PaceCategory::new("right"))); } } diff --git a/crates/ecosystem.png b/crates/ecosystem.png new file mode 100644 index 00000000..7947abb0 Binary files /dev/null and b/crates/ecosystem.png differ diff --git a/crates/error/Cargo.toml b/crates/error/Cargo.toml new file mode 100644 index 00000000..d9f28e62 --- /dev/null +++ b/crates/error/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "pace_error" +version = "0.1.0" +authors = { workspace = true } +categories = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +keywords = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +rust-version = { workspace = true } +description = "pace-error - library for error handling in pace ecosystem" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +chrono = { workspace = true } +displaydoc = { workspace = true } +miette = { workspace = true, features = ["fancy"] } +sea-orm = { workspace = true } +serde_json = { workspace = true } +tera = { workspace = true } +thiserror = { workspace = true } +toml = { workspace = true } +ulid = { workspace = true } + +[lints] +workspace = true diff --git a/crates/core/LICENSE b/crates/error/LICENSE similarity index 100% rename from crates/core/LICENSE rename to crates/error/LICENSE diff --git a/crates/error/README.md b/crates/error/README.md new file mode 100644 index 00000000..468a1950 --- /dev/null +++ b/crates/error/README.md @@ -0,0 +1,73 @@ +

+ +

+

pace-error - library for error handling in pace ecosystem

+ +

+ + + + +

+ +## About + +`pace-error` is a library to support timetracking on the command line. It is the +error handling library for the `pace` timetracking application. + +⚠️ **Note:** `pace-error` is currently in active development and is not yet ready +for production use. Expect breaking changes and incomplete features. We +encourage you to try it out and provide feedback, but please be aware that it is +not yet stable. + +## Contact + +You can ask questions in the +[Discussions](https://github.com/orgs/pace-rs/discussions) or have a look at the +[FAQ](https://pace.cli.rs/docs/FAQ.html). + +| Contact | Where? | +| ------------- | --------------------------------------------------------------------------------------------------------------- | +| Issue Tracker | [GitHub Issues](https://github.com/pace-rs/pace/issues/new/choose) | +| Discord | [![Discord](https://dcbadge.vercel.app/api/server/RKSWrAcYdG?style=flat-square)](https://discord.gg/RKSWrAcYdG) | +| Discussions | [GitHub Discussions](https://github.com/orgs/pace-rs/discussions) | + +## Examples + +TODO! + +## Contributing + +Found a bug? [Open an issue!](https://github.com/pace-rs/pace/issues/new/choose) + +Got an idea for an improvement? Don't keep it to yourself! + +- [Contribute fixes](https://github.com/pace-rs/pace/contribute) or new features + via a pull requests! + +Please make sure, that you read the +[contribution guide](https://pace.cli.rs/docs/contributing_to_pace.html). + +## Code of Conduct + +Please review and abide by the general +[Rust Community Code of Conduct](https://www.rust-lang.org/policies/code-of-conduct) +when contributing to this project. In the future, we might create our own Code +of Conduct and supplement it at this location. + +## Minimum Rust version policy + +This crate's minimum supported `rustc` version is `1.74.1`. + +The current policy is that the minimum Rust version required to use this crate +can be increased in minor version updates. For example, if `crate 1.0` requires +Rust 1.20.0, then `crate 1.0.z` for all values of `z` will also require Rust +1.20.0 or newer. However, `crate 1.y` for `y > 0` may require a newer minimum +version of Rust. + +In general, this crate will be conservative with respect to the minimum +supported version of Rust. + +## License + +**AGPL-3.0-or-later**; see [LICENSE](./LICENSE). diff --git a/crates/error/src/lib.rs b/crates/error/src/lib.rs new file mode 100644 index 00000000..6fbda321 --- /dev/null +++ b/crates/error/src/lib.rs @@ -0,0 +1,566 @@ +//! Error types and Result module. + +use displaydoc::Display; +use miette::Diagnostic; +use std::num::TryFromIntError; +use std::{error::Error, io, path::PathBuf}; +use thiserror::Error; + +macro_rules! impl_pace_error_marker { + ($error:ty) => { + impl PaceErrorMarker for $error {} + }; +} + +/// Result type that is being returned from test functions and methods that can fail and thus have errors. +pub type TestResult = Result>; + +/// We use a boxed error type for flexibility and size. +pub type BoxedPaceError = Box; + +/// Result type that is being returned from methods that can fail and thus have [`PaceError`]s. +pub type PaceResult = Result; + +/// Result type that is being returned from methods that have optional return values and can fail thus having [`PaceError`]s. +pub type PaceOptResult = PaceResult>; + +/// User message type that is being returned from methods that need to print a message to the user. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UserMessage { + /// The message to be printed to the user + msg: String, +} + +impl std::fmt::Display for UserMessage { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.msg) + } +} + +impl UserMessage { + pub fn new(msg: impl Into) -> Self { + Self { msg: msg.into() } + } + + pub fn display(&self) { + println!("{}", self.msg); + } +} + +impl std::ops::DerefMut for UserMessage { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.msg + } +} + +impl std::ops::Deref for UserMessage { + type Target = String; + + fn deref(&self) -> &Self::Target { + &self.msg + } +} + +// [`Error`] is public, but opaque and easy to keep compatible. +/// Errors that can result from pace. +#[derive(Error, Debug, Diagnostic)] +#[diagnostic(url(docsrs))] +pub struct PaceError(#[from] PaceErrorKind); + +impl std::fmt::Display for PaceError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +// Accessors for anything we do want to expose publicly. +impl PaceError { + /// Create a new [`PaceError`]. + #[must_use] + pub const fn new(kind: PaceErrorKind) -> Self { + Self(kind) + } + + /// Expose the inner error kind. + /// + /// This is useful for matching on the error kind. + #[must_use] + pub fn into_inner(self) -> PaceErrorKind { + self.0 + } + + /// Is this error related to a resumable activity so that we can prompt the user? + /// + /// This is useful for matching on the error kind. + #[must_use] + pub const fn possible_new_activity_from_resume(&self) -> bool { + matches!( + self.0, + PaceErrorKind::ActivityLog(ActivityLogErrorKind::NoHeldActivityFound(_)) + ) || matches!( + self.0, + PaceErrorKind::ActivityLog(ActivityLogErrorKind::ActivityAlreadyEnded(_)) + ) || matches!( + self.0, + PaceErrorKind::ActivityLog(ActivityLogErrorKind::ActivityAlreadyArchived(_)) + ) + } +} + +/// [`PaceErrorKind`] describes the errors that can happen while executing a high-level command. +/// +/// This is a non-exhaustive enum, so additional variants may be added in future. It is +/// recommended to match against the wildcard `_` instead of listing all possible variants, +/// to avoid problems when new variants are added. +#[non_exhaustive] +#[derive(Error, Debug, Display)] +pub enum PaceErrorKind { + /// [`std::io::Error`] + #[error(transparent)] + StdIo(#[from] std::io::Error), + + /// Serialization to TOML failed: {0} + #[error(transparent)] + SerializationToTomlFailed(#[from] toml::ser::Error), + + /// Deserialization from TOML failed: {0} + #[error(transparent)] + DeserializationFromTomlFailed(#[from] toml::de::Error), + + /// Activity store error: {0} + #[error(transparent)] + ActivityStore(#[from] ActivityStoreErrorKind), + + /// Activity log error: {0} + #[error(transparent)] + ActivityLog(#[from] ActivityLogErrorKind), + + /// Time related error: {0} + #[error(transparent)] + Time(#[from] TimeErrorKind), + + /// Config related error: {0} + #[error(transparent)] + Config(#[from] ConfigErrorKind), + + /// JSON error: {0} + #[error(transparent)] + Json(#[from] serde_json::Error), + + /// Chrono parse error: {0} + #[error(transparent)] + ChronoParse(#[from] chrono::ParseError), + + /// Time chosen is not valid, because it lays before the current activity's beginning: {0} + #[error(transparent)] + ChronoDurationIsNegative(#[from] chrono::OutOfRangeError), + + /// There is no path available to store the activity log + NoPathAvailable, + + /// Templating error: {0} + #[error(transparent)] + Template(#[from] TemplatingErrorKind), + + /// Database error: {0} + #[error(transparent)] + Database(#[from] DatabaseStorageErrorKind), + + /// Toml file error: {0} + #[error(transparent)] + TomlFile(#[from] TomlFileStorageErrorKind), + + /// Invalid Ulid parsed from string: {value} due to {source} + InvalidGuid { + value: String, + #[source] + source: ulid::DecodeError, + }, +} + +/// [`ConfigErrorKind`] describes the errors that can happen while dealing with our configuration. +#[non_exhaustive] +#[derive(Error, Debug, Display)] +pub enum ConfigErrorKind { + /// Config file {file_name} not found in directory hierarchy starting from {current_dir} + ConfigFileNotFound { + /// The current directory + current_dir: String, + + /// The file name + file_name: String, + }, + + /// Configuration file not found, please run `pace setup config` to initialize `pace` + ParentDirNotFound(PathBuf), +} + +/// [`DatabaseErrorKind`] describes the errors that can happen while dealing with the `SQLite` database. +#[non_exhaustive] +#[derive(Error, Debug, Display)] +pub enum DatabaseStorageErrorKind { + /// Error connecting to database {url}: {source} + ConnectionFailed { + url: String, + #[source] + source: sea_orm::error::DbErr, + }, + + /// No connection string provided + NoConnectionString, + + /// This database engine is currently not supported: {0} + UnsupportedDatabaseEngine(String), + + /// Activity with id {guid} not found + ActivityNotFound { guid: String }, + + /// Failed to create activity: {0} + ActivityCreationFailed(String), + + /// Failed to delete activity: {0} + ActivityDeletionFailed(String), + + /// Database storage not configured + DatabaseStorageNotConfigured, + + /// Database storage not implemented, yet! + StorageNotImplemented, + + /// Database migration failed: {source} + MigrationFailed { + #[source] + source: sea_orm::error::DbErr, + }, + + /// No migrations found for table: {table} + NoMigrationsFound { table: String }, + + /// Building migration query failed. Version: {version}, Table: {table}, Query: {query}, Source: {source} + BuildingMigrationQueryFailed { + version: String, + table: String, + query: String, + #[source] + source: sea_orm::error::SqlErr, + }, + + /// No migrations to rollback + NoMigrationsToRollback, + + /// Migration affected multiple rows + MigrationAffectedMultipleRows, + + /// Checking if migration exists failed. Version: {version}, Table: {table}, Query: {query}, Source: {source} + CheckingMigrationExistsFailed { + version: String, + table: String, + query: String, + #[source] + source: sea_orm::error::SqlErr, + }, + + /// Selection query failed for migration version: {version}, table: {table}, query: {query}, source: {source} + SelectionQueryFailed { + version: String, + table: String, + query: String, + #[source] + source: sea_orm::error::SqlErr, + }, + + /// Row does not contain migration version: {version}, source: {source} + RowDoesNotContainMigrationVersion { + version: String, + #[source] + source: sea_orm::error::SqlErr, + }, + + /// Failed to read activity {guid}: {source} + ActivityReadFailed { + guid: String, + #[source] + source: sea_orm::error::DbErr, + }, + + /// There is no item contained with id {0} + NoItemContained(String), + + /// Failed to add values to database table: {version}, query: {query}, source: {source} + AddingValuesToDatabaseTableFailed { + version: String, + query: String, + #[source] + source: sea_orm::error::SqlErr, + }, + + /// Failed to read item {item_type}::{item_id} from database: {source} + RepositoryReadFailed { + source: sea_orm::DbErr, + item_type: String, + item_id: String, + }, + + /// Failed to delete item {item_type}::{item_id} from database: {source} + RepositoryDeleteFailed { + source: sea_orm::prelude::DbErr, + item_type: String, + item_id: String, + }, + + /// Failed to create item {item_type} in database: {source} + RepositoryCreateFailed { + source: sea_orm::prelude::DbErr, + item_type: String, + }, +} + +/// [`TomlFileStorageErrorKind`] describes the errors that can happen while dealing with the Toml file storage. +#[non_exhaustive] +#[derive(Error, Debug, Display)] +pub enum TomlFileStorageErrorKind { + /// Parent directory not found: {0} + ParentDirNotFound(PathBuf), +} + +/// [`ActivityLogErrorKind`] describes the errors that can happen while dealing with the activity log. +#[non_exhaustive] +#[derive(Error, Debug, Display)] +pub enum ActivityLogErrorKind { + /// No activities found in the activity log + NoActivitiesFound, + + /// Activity with ID {0} not found + FailedToReadActivity(String), + + /// Negative duration for activity + NegativeDuration, + + /// There are no activities to hold + NoActivityToHold, + + /// Failed to unwrap Arc + ArcUnwrapFailed, + + /// There are no unfinished activities to end + NoUnfinishedActivities, + + /// There is no cache to sync + NoCacheToSync, + + /// Cache not available + CacheNotAvailable, + + /// `Activity` with id {0} not found + ActivityNotFound(String), + + /// `Activity` with id {0} can't be removed from the activity log + ActivityCantBeRemoved(usize), + + /// This activity has no id + ActivityIdNotSet, + + /// `Activity` with id {0} already in use, can't create a new activity with the same id + ActivityIdAlreadyInUse(String), + + /// `Activity` in the `ActivityLog` has a different id than the one provided: {0} != {1} + ActivityIdMismatch(String, String), + + /// `Activity` already has an intermission: {0} + ActivityAlreadyHasIntermission(Box), + + /// There have been some activities that have not been ended + ActivityNotEnded, + + /// No active activity found with id {0} + NoActiveActivityFound(String), + + /// `Activity` with id {0} already ended + ActivityAlreadyEnded(String), + + /// Activity with id {0} already has been archived + ActivityAlreadyArchived(String), + + /// Active activity with id {0} found, although we wanted a held activity + ActiveActivityFound(String), + + /// Activity with id {0} is not held, but we wanted to resume it + NoHeldActivityFound(String), + + /// No activity kind options found for activity with id {0} + ActivityKindOptionsNotFound(String), + + /// `ParentId` not set for activity with id {0} + ParentIdNotSet(String), + + /// Category not set for activity with id {0} + CategoryNotSet(String), + + /// No active activity to adjust + NoActiveActivityToAdjust, + + /// Failed to group activities by keywords + FailedToGroupByKeywords, + + /// No end options found for activity + NoEndOptionsFound, +} + +/// [`TemplatingErrorKind`] describes the errors that can happen while dealing with templating. +#[non_exhaustive] +#[derive(Error, Debug, Display)] +pub enum TemplatingErrorKind { + /// Failed to generate context from serializable struct: {0} + FailedToGenerateContextFromSerialize(tera::Error), + + /// Failed to render template: {0} + RenderingToTemplateFailed(tera::Error), + + /// Failed to read template file: {0} + FailedToReadTemplateFile(io::Error), + + /// Template file not specified + TemplateFileNotSpecified, +} + +/// [`ActivityStoreErrorKind`] describes the errors that can happen while dealing with time. +#[non_exhaustive] +#[derive(Error, Debug, Display)] +pub enum ActivityStoreErrorKind { + /// Failed to list activities by id + ListActivitiesById, + + /// Failed to group activities by duration range + GroupByDurationRange, + + /// Failed to group activities by start date + GroupByStartDate, + + /// Failed to list activities with intermissions + ListActivitiesWithIntermissions, + + /// Failed to group activities by keywords + GroupByKeywords, + + /// Failed to group activities by kind + GroupByKind, + + /// Failed to list activities by time range + ListActivitiesByTimeRange, + + /// Failed to populate `ActivityStore` cache + PopulatingCache, + + /// Failed to list activities for activity: {0} + ListIntermissionsForActivity(String), + + /// Missing category for activity: {0} + MissingCategoryForActivity(String), + + /// Creating ActivityStore from storage failed + CreatingFromStorageFailed, +} + +/// [`TimeErrorKind`] describes the errors that can happen while dealing with time. +#[non_exhaustive] +#[derive(Error, Debug, Display)] +pub enum TimeErrorKind { + /// {0} + #[error(transparent)] + OutOfRange(#[from] chrono::OutOfRangeError), + + /// Failed to parse time '{0}' from user input, please use the format HH:MM + ParsingTimeFromUserInputFailed(String), + + /// The start time cannot be in the future, please use a time in the past: '{0}' + StartTimeInFuture(String), + + /// Failed to parse duration '{0}', please use only numbers >= 0 + ParsingDurationFailed(String), + + /// Failed to parse date '{0}', please use the format YYYY-MM-DD + InvalidDate(String), + /// Date is not present! + DateShouldBePresent, + + /// Failed to parse date '{0}' + ParsingDateFailed(String), + + /// Invalid time range: Start '{0}' - End '{1}' + InvalidTimeRange(String, String), + + /// Invalid time zone: '{0}' + InvalidTimeZone(String), + + /// Failed to parse fixed offset '{0}' from user input, please use the format ±HHMM + ParsingFixedOffsetFailed(String), + + /// Failed to create PaceDateTime from user input, please use the format HH:MM and ±HHMM + InvalidUserInput, + + /// Time zone not found + UndefinedTimeZone, + + /// Both time zone and time zone offset are defined, please use only one + AmbiguousTimeZones, + + /// Ambiguous conversion result + AmbiguousConversionResult, + + /// Conversion to PaceDateTime failed + ConversionToPaceDateTimeFailed, + + /// Failed to parse time '{0}', please use the format HH:MM + InvalidTime(String), + + /// Failed to parse time '{0}', please use rfc3339 format + ParseError(String), + + /// Setting start of day failed + SettingStartOfDayFailed, + + /// Adding time delta failed: '{0}' + AddingTimeDeltaFailed(String), + + /// Failed to convert duration to i64: '{0}' + FailedToConvertDurationToI64(TryFromIntError), + + /// Failed to convert PaceDuration to Standard Duration: '{0}' + ConversionToDurationFailed(String), +} + +trait PaceErrorMarker: Error {} + +impl_pace_error_marker!(std::io::Error); +impl_pace_error_marker!(toml::de::Error); +impl_pace_error_marker!(toml::ser::Error); +impl_pace_error_marker!(serde_json::Error); +impl_pace_error_marker!(chrono::ParseError); +impl_pace_error_marker!(chrono::OutOfRangeError); +impl_pace_error_marker!(ActivityLogErrorKind); +impl_pace_error_marker!(ActivityStoreErrorKind); +impl_pace_error_marker!(TimeErrorKind); +impl_pace_error_marker!(TemplatingErrorKind); +impl_pace_error_marker!(DatabaseStorageErrorKind); +impl_pace_error_marker!(TomlFileStorageErrorKind); +impl_pace_error_marker!(ConfigErrorKind); + +impl From for PaceError +where + E: PaceErrorMarker, + PaceErrorKind: From, +{ + fn from(value: E) -> Self { + Self(PaceErrorKind::from(value)) + } +} + +impl From for Box +where + E: PaceErrorMarker, + PaceErrorKind: From, +{ + fn from(value: E) -> Self { + Self::new(PaceError::new(PaceErrorKind::from(value))) + } +} diff --git a/crates/pace.excalidraw b/crates/pace.excalidraw new file mode 100644 index 00000000..b6d30d81 --- /dev/null +++ b/crates/pace.excalidraw @@ -0,0 +1,1300 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor", + "elements": [ + { + "type": "rectangle", + "version": 1051, + "versionNonce": 1545238859, + "isDeleted": false, + "id": "tM7qo1qvT6iCQSrBzWh4S", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 18270.637964102305, + "y": 6885.0705690142595, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "width": 467.85714285714494, + "height": 249.28571428571377, + "seed": 2126969291, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "8lKV9ippicf8jSLDeTDc_" + }, + { + "id": "JaaM_Q8KPd65d_XLWIy5H", + "type": "arrow" + }, + { + "id": "3bC8t3lY7r8U_iD60G2xt", + "type": "arrow" + }, + { + "id": "9v-DwcYzOPyEhvqe43f4S", + "type": "arrow" + } + ], + "updated": 1711736235449, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 991, + "versionNonce": 1957894123, + "isDeleted": false, + "id": "8lKV9ippicf8jSLDeTDc_", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 18439.881318304462, + "y": 6987.213426157116, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "width": 129.37043445284218, + "height": 45, + "seed": 1044204229, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1711736235449, + "link": null, + "locked": false, + "fontSize": 36, + "fontFamily": 1, + "text": "pace-rs", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "tM7qo1qvT6iCQSrBzWh4S", + "originalText": "pace-rs", + "lineHeight": 1.25, + "baseline": 37 + }, + { + "type": "rectangle", + "version": 464, + "versionNonce": 1687965451, + "isDeleted": false, + "id": "TXq2OqWLQyf90BWAjNJJf", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 18982.66177362611, + "y": 7878.0467594904485, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "width": 342.8571428571413, + "height": 218.57142857142935, + "seed": 427573829, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "rhatcvwFNDaJIdZB2aRR2" + }, + { + "id": "JaaM_Q8KPd65d_XLWIy5H", + "type": "arrow" + }, + { + "id": "DmT7fM35Jkro7gi4CwLjV", + "type": "arrow" + }, + { + "id": "2sEGnJIa_Tw0UsRPWjooE", + "type": "arrow" + }, + { + "id": "KygNHOII4YE0NsSC9VRe0", + "type": "arrow" + }, + { + "id": "9CgduZ8cBQtWZgqAsE2Sp", + "type": "arrow" + }, + { + "id": "c2guM95anTFv9XWw8Cp2q", + "type": "arrow" + } + ], + "updated": 1711736180651, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 416, + "versionNonce": 1690423332, + "isDeleted": false, + "id": "rhatcvwFNDaJIdZB2aRR2", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 19070.33638539892, + "y": 7964.832473776163, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "width": 167.50791931152344, + "height": 45, + "seed": 2080596587, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1711759006596, + "link": null, + "locked": false, + "fontSize": 36, + "fontFamily": 1, + "text": "pace-core", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "TXq2OqWLQyf90BWAjNJJf", + "originalText": "pace-core", + "lineHeight": 1.25, + "baseline": 32 + }, + { + "type": "rectangle", + "version": 462, + "versionNonce": 2030785931, + "isDeleted": false, + "id": "mgPVbyNloshnv-FUYisNP", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 19556.471297435648, + "y": 7595.546759490453, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "width": 342.8571428571413, + "height": 218.57142857142935, + "seed": 279782181, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "e-KRZnbVCkxmR7DjCYbMW" + }, + { + "id": "2inE77MDCxUU2eTZZsQ0N", + "type": "arrow" + }, + { + "id": "KygNHOII4YE0NsSC9VRe0", + "type": "arrow" + }, + { + "id": "s93yqN-VXLPIlE7rMqmug", + "type": "arrow" + } + ], + "updated": 1711736193108, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 424, + "versionNonce": 1488545316, + "isDeleted": false, + "id": "e-KRZnbVCkxmR7DjCYbMW", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 19645.387913602986, + "y": 7682.332473776168, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "width": 165.02391052246094, + "height": 45, + "seed": 1988480427, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1711759000883, + "link": null, + "locked": false, + "fontSize": 36, + "fontFamily": 1, + "text": "pace-time", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "mgPVbyNloshnv-FUYisNP", + "originalText": "pace-time", + "lineHeight": 1.25, + "baseline": 32 + }, + { + "type": "rectangle", + "version": 687, + "versionNonce": 1494593227, + "isDeleted": false, + "id": "ZyUdkNnhMgXouKQKtZdzw", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 19824.56653553088, + "y": 8109.118188061881, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "width": 342.8571428571413, + "height": 218.57142857142935, + "seed": 1731857771, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "uI91Ze6km-1Qgloy_uiVK" + }, + { + "id": "DmT7fM35Jkro7gi4CwLjV", + "type": "arrow" + }, + { + "id": "2inE77MDCxUU2eTZZsQ0N", + "type": "arrow" + }, + { + "id": "3euXcOYnOet5CLkuQfuZm", + "type": "arrow" + } + ], + "updated": 1711736197067, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 659, + "versionNonce": 2145876772, + "isDeleted": false, + "id": "uI91Ze6km-1Qgloy_uiVK", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 19905.79714705955, + "y": 8195.903902347596, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "width": 180.3959197998047, + "height": 45, + "seed": 1263014597, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1711759003805, + "link": null, + "locked": false, + "fontSize": 36, + "fontFamily": 1, + "text": "pace-error", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "ZyUdkNnhMgXouKQKtZdzw", + "originalText": "pace-error", + "lineHeight": 1.25, + "baseline": 32 + }, + { + "type": "rectangle", + "version": 584, + "versionNonce": 1241183403, + "isDeleted": false, + "id": "H2nQkbRLhThymO2gq-6SA", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 18998.376059340408, + "y": 6909.475330919021, + "strokeColor": "#1e1e1e", + "backgroundColor": "#e9ecef", + "width": 342.8571428571413, + "height": 218.57142857142935, + "seed": 1578321323, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "JKw6Fka1rhYs9IZYcBS2i" + }, + { + "id": "0uVxqaBYM3PL3btgoyHyt", + "type": "arrow" + }, + { + "id": "c2guM95anTFv9XWw8Cp2q", + "type": "arrow" + }, + { + "id": "9v-DwcYzOPyEhvqe43f4S", + "type": "arrow" + } + ], + "updated": 1711736232498, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 564, + "versionNonce": 1478903836, + "isDeleted": false, + "id": "JKw6Fka1rhYs9IZYcBS2i", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 19063.046696198668, + "y": 6996.261045204736, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "width": 213.515869140625, + "height": 45, + "seed": 871202437, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1711758992428, + "link": null, + "locked": false, + "fontSize": 36, + "fontFamily": 1, + "text": "pace-service", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "H2nQkbRLhThymO2gq-6SA", + "originalText": "pace-service", + "lineHeight": 1.25, + "baseline": 32 + }, + { + "type": "rectangle", + "version": 717, + "versionNonce": 368991461, + "isDeleted": false, + "id": "d-UItuVV1OxpBnHLSdCSR", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 17856.947487911835, + "y": 7415.189616633308, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "width": 342.8571428571413, + "height": 218.57142857142935, + "seed": 592231077, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "iyTwjXWeoJ_GeGuuzsXq5" + } + ], + "updated": 1711736134786, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 710, + "versionNonce": 1404824228, + "isDeleted": false, + "id": "iyTwjXWeoJ_GeGuuzsXq5", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 17926.856116103103, + "y": 7501.975330919023, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "width": 203.03988647460938, + "height": 45, + "seed": 283995691, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1711758979334, + "link": null, + "locked": false, + "fontSize": 36, + "fontFamily": 1, + "text": "pace-server", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "d-UItuVV1OxpBnHLSdCSR", + "originalText": "pace-server", + "lineHeight": 1.25, + "baseline": 32 + }, + { + "type": "rectangle", + "version": 999, + "versionNonce": 602440427, + "isDeleted": false, + "id": "RsIzHYLG8TzWQr8DnEfp5", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 19852.78082124517, + "y": 6893.046759490449, + "strokeColor": "#1e1e1e", + "backgroundColor": "#e9ecef", + "width": 342.8571428571413, + "height": 218.57142857142935, + "seed": 1265502795, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "iPy_SZbSRtE4fz62LFeCD" + }, + { + "id": "0uVxqaBYM3PL3btgoyHyt", + "type": "arrow" + }, + { + "id": "3euXcOYnOet5CLkuQfuZm", + "type": "arrow" + }, + { + "id": "2sEGnJIa_Tw0UsRPWjooE", + "type": "arrow" + }, + { + "id": "s93yqN-VXLPIlE7rMqmug", + "type": "arrow" + } + ], + "updated": 1711736274513, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 989, + "versionNonce": 1902612772, + "isDeleted": false, + "id": "iPy_SZbSRtE4fz62LFeCD", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 19908.523441440833, + "y": 6979.832473776164, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "width": 231.3719024658203, + "height": 45, + "seed": 1913050085, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1711758996683, + "link": null, + "locked": false, + "fontSize": 36, + "fontFamily": 1, + "text": "pace-storage", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "RsIzHYLG8TzWQr8DnEfp5", + "originalText": "pace-storage", + "lineHeight": 1.25, + "baseline": 32 + }, + { + "type": "rectangle", + "version": 630, + "versionNonce": 181234277, + "isDeleted": false, + "id": "czADXrtUBMGnnu0EMFNCd", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 18368.018916483263, + "y": 7423.284854728545, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "width": 342.8571428571413, + "height": 218.57142857142935, + "seed": 85245003, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "sSvOMwPllQxFiACHwqppD" + }, + { + "id": "3bC8t3lY7r8U_iD60G2xt", + "type": "arrow" + }, + { + "id": "9CgduZ8cBQtWZgqAsE2Sp", + "type": "arrow" + } + ], + "updated": 1711736239398, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 638, + "versionNonce": 1782054940, + "isDeleted": false, + "id": "sSvOMwPllQxFiACHwqppD", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 18474.575532040253, + "y": 7510.0705690142595, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "width": 129.74391174316406, + "height": 45, + "seed": 282870245, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1711758982012, + "link": null, + "locked": false, + "fontSize": 36, + "fontFamily": 1, + "text": "pace-cli", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "czADXrtUBMGnnu0EMFNCd", + "originalText": "pace-cli", + "lineHeight": 1.25, + "baseline": 32 + }, + { + "type": "arrow", + "version": 2121, + "versionNonce": 2069055179, + "isDeleted": false, + "id": "0uVxqaBYM3PL3btgoyHyt", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 19353.61415457852, + "y": 7014.362302903005, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 483.9285714285652, + "height": 1.396201362697866, + "seed": 844762859, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1711736274513, + "link": null, + "locked": false, + "startBinding": { + "elementId": "H2nQkbRLhThymO2gq-6SA", + "focus": -0.02897112233130823, + "gap": 12.380952380969575 + }, + "endBinding": { + "elementId": "RsIzHYLG8TzWQr8DnEfp5", + "focus": -0.09195700445627356, + "gap": 15.238095238087226 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 483.9285714285652, + -1.396201362697866 + ] + ] + }, + { + "type": "arrow", + "version": 2687, + "versionNonce": 79190315, + "isDeleted": false, + "id": "JaaM_Q8KPd65d_XLWIy5H", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 18587.283750899896, + "y": 7145.3086642523585, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 510.9039690631944, + "height": 728.9285714285797, + "seed": 1452357579, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1711736235449, + "link": null, + "locked": false, + "startBinding": { + "elementId": "tM7qo1qvT6iCQSrBzWh4S", + "focus": 0.038204332155746694, + "gap": 10.952380952385283 + }, + "endBinding": { + "elementId": "TXq2OqWLQyf90BWAjNJJf", + "focus": 0.09420564116423788, + "gap": 3.8095238095102104 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 510.9039690631944, + 728.9285714285797 + ] + ] + }, + { + "type": "arrow", + "version": 2577, + "versionNonce": 496756869, + "isDeleted": false, + "id": "3bC8t3lY7r8U_iD60G2xt", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 18528.73369262869, + "y": 7141.975330919026, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 5.912691550332966, + "height": 275.11904761902497, + "seed": 187010917, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1711736239398, + "link": null, + "locked": false, + "startBinding": { + "elementId": "tM7qo1qvT6iCQSrBzWh4S", + "focus": -0.11423223700366737, + "gap": 7.619047619052253 + }, + "endBinding": { + "elementId": "czADXrtUBMGnnu0EMFNCd", + "focus": -0.10995817009520573, + "gap": 6.190476190493428 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + -5.912691550332966, + 275.11904761902497 + ] + ] + }, + { + "type": "arrow", + "version": 1677, + "versionNonce": 1612224747, + "isDeleted": false, + "id": "DmT7fM35Jkro7gi4CwLjV", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 19219.09034505468, + "y": 8112.448460125291, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 597.8571428571558, + "height": 134.73299814853908, + "seed": 731557797, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1711736264042, + "link": null, + "locked": false, + "startBinding": { + "elementId": "TXq2OqWLQyf90BWAjNJJf", + "focus": 0.36874982671100137, + "gap": 15.830272063412849 + }, + "endBinding": { + "elementId": "ZyUdkNnhMgXouKQKtZdzw", + "focus": -0.3116721472570393, + "gap": 7.619047619047706 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 176.90476190476693, + 117.02687079372936 + ], + [ + 597.8571428571558, + 134.73299814853908 + ] + ] + }, + { + "type": "arrow", + "version": 1486, + "versionNonce": 529552389, + "isDeleted": false, + "id": "2inE77MDCxUU2eTZZsQ0N", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 19750.256592584647, + "y": 7826.856283299974, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 182.13752831582678, + "height": 274.6428571428578, + "seed": 739271051, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1711736189176, + "link": null, + "locked": false, + "startBinding": { + "elementId": "mgPVbyNloshnv-FUYisNP", + "focus": 0.24012243594642627, + "gap": 12.738095238091773 + }, + "endBinding": { + "elementId": "ZyUdkNnhMgXouKQKtZdzw", + "focus": 0.05710372346795993, + "gap": 7.619047619049525 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 182.13752831582678, + 274.6428571428578 + ] + ] + }, + { + "type": "arrow", + "version": 2227, + "versionNonce": 1216499723, + "isDeleted": false, + "id": "3euXcOYnOet5CLkuQfuZm", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 19992.816172340958, + "y": 7121.856283299968, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 1.1849871818412794, + "height": 973.6904761905062, + "seed": 1104514501, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1711736274513, + "link": null, + "locked": false, + "startBinding": { + "elementId": "RsIzHYLG8TzWQr8DnEfp5", + "focus": 0.18213416095774063, + "gap": 10.238095238089045 + }, + "endBinding": { + "elementId": "ZyUdkNnhMgXouKQKtZdzw", + "focus": -0.026307987333239663, + "gap": 13.571428571407523 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + -1.1849871818412794, + 973.6904761905062 + ] + ] + }, + { + "type": "arrow", + "version": 1925, + "versionNonce": 368052555, + "isDeleted": false, + "id": "2sEGnJIa_Tw0UsRPWjooE", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 19849.209392673747, + "y": 7093.634497860888, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 602.8128737277402, + "height": 776.555118772415, + "seed": 369477669, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1711736274513, + "link": null, + "locked": false, + "startBinding": { + "elementId": "RsIzHYLG8TzWQr8DnEfp5", + "focus": 0.40617013508570504, + "gap": 3.5714285714248035 + }, + "endBinding": { + "elementId": "TXq2OqWLQyf90BWAjNJJf", + "focus": 0.005354752342787572, + "gap": 7.8571428571458455 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + -602.8128737277402, + 776.555118772415 + ] + ] + }, + { + "type": "arrow", + "version": 1198, + "versionNonce": 575138405, + "isDeleted": false, + "id": "KygNHOII4YE0NsSC9VRe0", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 19332.185583149938, + "y": 7945.207866643787, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 220.2920030980422, + "height": 136.92301191526076, + "seed": 360086661, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1711736186677, + "link": null, + "locked": false, + "startBinding": { + "elementId": "TXq2OqWLQyf90BWAjNJJf", + "focus": 0.31769684109495916, + "gap": 6.66666666668425 + }, + "endBinding": { + "elementId": "mgPVbyNloshnv-FUYisNP", + "focus": 0.0258608890380365, + "gap": 3.993711187666122 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 220.2920030980422, + -136.92301191526076 + ] + ] + }, + { + "type": "arrow", + "version": 1053, + "versionNonce": 1252126501, + "isDeleted": false, + "id": "9CgduZ8cBQtWZgqAsE2Sp", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 18542.0665355309, + "y": 7645.348064744582, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 420.1076434318493, + "height": 331.3891709363579, + "seed": 26437003, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1711736250281, + "link": null, + "locked": false, + "startBinding": { + "elementId": "czADXrtUBMGnnu0EMFNCd", + "focus": 0.07307590522028012, + "gap": 3.491781444608023 + }, + "endBinding": { + "elementId": "TXq2OqWLQyf90BWAjNJJf", + "focus": -0.09332561198817191, + "gap": 20.48759466336378 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 41.428571428547, + 286.62726617443786 + ], + [ + 420.1076434318493, + 331.3891709363579 + ] + ] + }, + { + "type": "arrow", + "version": 845, + "versionNonce": 873834955, + "isDeleted": false, + "id": "c2guM95anTFv9XWw8Cp2q", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 19169.856640522972, + "y": 7137.094378538086, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 5.959020579139178, + "height": 727.142857142856, + "seed": 1772993189, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1711736232498, + "link": null, + "locked": false, + "startBinding": { + "elementId": "H2nQkbRLhThymO2gq-6SA", + "focus": -0.006239451746593869, + "gap": 9.047619047634726 + }, + "endBinding": { + "elementId": "TXq2OqWLQyf90BWAjNJJf", + "focus": 0.05105780869098277, + "gap": 13.809523809506572 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + -5.959020579139178, + 727.142857142856 + ] + ] + }, + { + "type": "arrow", + "version": 614, + "versionNonce": 866380517, + "isDeleted": false, + "id": "9v-DwcYzOPyEhvqe43f4S", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 18741.59034505469, + "y": 7015.843849879741, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 247.73809523809905, + "height": 0.22165181620675867, + "seed": 996457413, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1711736238068, + "link": null, + "locked": false, + "startBinding": { + "elementId": "tM7qo1qvT6iCQSrBzWh4S", + "focus": 0.050800000350119695, + "gap": 3.0952380952403473 + }, + "endBinding": { + "elementId": "H2nQkbRLhThymO2gq-6SA", + "focus": 0.030156679204429684, + "gap": 9.047619047620174 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 247.73809523809905, + -0.22165181620675867 + ] + ] + }, + { + "type": "arrow", + "version": 433, + "versionNonce": 129004171, + "isDeleted": false, + "id": "s93yqN-VXLPIlE7rMqmug", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 19953.71168186351, + "y": 7124.47533091902, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "width": 230.18324020348882, + "height": 447.5, + "seed": 1769290635, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1711736274513, + "link": null, + "locked": false, + "startBinding": { + "elementId": "RsIzHYLG8TzWQr8DnEfp5", + "focus": 0.03345478645740184, + "gap": 12.857142857141298 + }, + "endBinding": { + "elementId": "mgPVbyNloshnv-FUYisNP", + "focus": -0.3194040752375057, + "gap": 23.5714285714339 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + -230.18324020348882, + 447.5 + ] + ] + } + ], + "appState": { + "gridSize": null, + "viewBackgroundColor": "#ffffff" + }, + "files": {} +} \ No newline at end of file diff --git a/crates/service/Cargo.toml b/crates/service/Cargo.toml new file mode 100644 index 00000000..4d110205 --- /dev/null +++ b/crates/service/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "pace_service" +version = "0.1.0" +authors = { workspace = true } +categories = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +keywords = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +rust-version = { workspace = true } +description = "pace-service - service support library for pace" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +getset = { workspace = true } +pace_core = { workspace = true } +pace_error = { workspace = true } +pace_storage = { workspace = true } +pace_time = { workspace = true } +tracing = { workspace = true } +typed-builder = { workspace = true } +wildmatch = { workspace = true } + +[lints] +workspace = true diff --git a/crates/service/LICENSE b/crates/service/LICENSE new file mode 100644 index 00000000..0ad25db4 --- /dev/null +++ b/crates/service/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/crates/service/README.md b/crates/service/README.md new file mode 100644 index 00000000..4471a514 --- /dev/null +++ b/crates/service/README.md @@ -0,0 +1,89 @@ +

+ +

+

pace-service - a service library for the pace ecosystem

+ +

+ + + + +

+ +## About + +`pace-service` is a library to support timetracking on the command line. It is +the service library for the `pace` timetracking application. + +⚠️ **Note:** `pace-service` is currently in active development and is not yet +ready for production use. Expect breaking changes and incomplete features. We +encourage you to try it out and provide feedback, but please be aware that it is +not yet stable. + +## Contact + +You can ask questions in the +[Discussions](https://github.com/orgs/pace-rs/discussions) or have a look at the +[FAQ](https://pace.cli.rs/docs/FAQ.html). + +| Contact | Where? | +| ------------- | --------------------------------------------------------------------------------------------------------------- | +| Issue Tracker | [GitHub Issues](https://github.com/pace-rs/pace/issues/new/choose) | +| Discord | [![Discord](https://dcbadge.vercel.app/api/server/RKSWrAcYdG?style=flat-square)](https://discord.gg/RKSWrAcYdG) | +| Discussions | [GitHub Discussions](https://github.com/orgs/pace-rs/discussions) | + + + +## Examples + +TODO! + +## Contributing + +Found a bug? [Open an issue!](https://github.com/pace-rs/pace/issues/new/choose) + +Got an idea for an improvement? Don't keep it to yourself! + +- [Contribute fixes](https://github.com/pace-rs/pace/contribute) or new features + via a pull requests! + +Please make sure, that you read the +[contribution guide](https://pace.cli.rs/docs/contributing_to_pace.html). + +## Code of Conduct + +Please review and abide by the general +[Rust Community Code of Conduct](https://www.rust-lang.org/policies/code-of-conduct) +when contributing to this project. In the future, we might create our own Code +of Conduct and supplement it at this location. + +## Minimum Rust version policy + +This crate's minimum supported `rustc` version is `1.74.1`. + +The current policy is that the minimum Rust version required to use this crate +can be increased in minor version updates. For example, if `crate 1.0` requires +Rust 1.20.0, then `crate 1.0.z` for all values of `z` will also require Rust +1.20.0 or newer. However, `crate 1.y` for `y > 0` may require a newer minimum +version of Rust. + +In general, this crate will be conservative with respect to the minimum +supported version of Rust. + +## License + +**AGPL-3.0-or-later**; see [LICENSE](./LICENSE). diff --git a/crates/core/src/service/activity_store.rs b/crates/service/src/activity_store.rs similarity index 88% rename from crates/core/src/service/activity_store.rs rename to crates/service/src/activity_store.rs index fe642ac4..923675a1 100644 --- a/crates/core/src/service/activity_store.rs +++ b/crates/service/src/activity_store.rs @@ -10,27 +10,28 @@ use typed_builder::TypedBuilder; use wildmatch::WildMatch; -use crate::{ - commands::{ - hold::HoldOptions, resume::ResumeOptions, DeleteOptions, EndOptions, KeywordOptions, - UpdateOptions, - }, +use pace_core::{ domain::{ - activity::{ - Activity, ActivityGroup, ActivityGuid, ActivityItem, ActivityKind, ActivitySession, - }, - category, - filter::{ActivityFilterKind, FilterOptions, FilteredActivities}, + activity::{Activity, ActivityGroup, ActivityItem, ActivityKind, ActivitySession}, + category::{self, PaceCategory}, + description::PaceDescription, + filter::{ActivityFilterKind, FilteredActivities}, + id::ActivityGuid, reflection::{SummaryActivityGroup, SummaryGroupByCategory}, status::ActivityStatusKind, }, - error::{ActivityStoreErrorKind, PaceOptResult, PaceResult}, + options::{ + DeleteOptions, EndOptions, FilterOptions, HoldOptions, KeywordOptions, ResumeOptions, + UpdateOptions, + }, storage::{ ActivityQuerying, ActivityReadOps, ActivityStateManagement, ActivityStorage, - ActivityWriteOps, StorageKind, SyncStorage, + ActivityWriteOps, SyncStorage, }, }; +use pace_error::{ActivityStoreErrorKind, PaceOptResult, PaceResult}; + /// The activity store entity #[derive(TypedBuilder, Getters, Setters, MutGetters)] #[getset(get = "pub", get_mut = "pub", set = "pub")] @@ -39,7 +40,7 @@ pub struct ActivityStore { cache: ActivityStoreCache, /// The storage backend - storage: Arc, + storage: Arc, } #[derive(Debug, TypedBuilder, Getters, Setters, MutGetters, Clone, Eq, PartialEq, Default)] @@ -69,15 +70,18 @@ impl ActivityStore { /// /// This method returns a new `ActivityStore` if the storage backend /// was successfully created - pub fn with_storage(storage: Arc) -> PaceResult { - debug!("Creating activity store with storage: {}", storage); + pub fn with_storage(storage: Arc) -> PaceResult { + debug!( + "Creating activity store with storage: {}", + storage.identify() + ); let mut store = Self { cache: ActivityStoreCache::default(), storage, }; - store.setup_storage()?; + store.setup()?; store.populate_caches()?; @@ -120,7 +124,7 @@ impl ActivityStore { let mut summary_groups: SummaryGroupByCategory = BTreeMap::new(); let mut activity_sessions_lookup_by_category: HashMap< - (Category, Subcategory, Description), + (Category, Subcategory, PaceDescription), Vec, > = HashMap::new(); @@ -131,19 +135,23 @@ impl ActivityStore { for activity_guid in activity_guids { let activity_item = self.read_activity(activity_guid)?; + let fallback_category = PaceCategory::new("Uncategorized"); + let activity_category = activity_item .activity() .category() - .as_deref() - .unwrap_or("Uncategorized") - .to_string(); + .as_ref() + .unwrap_or(&fallback_category); // Skip if category does not match user input if let Some(category) = filter_opts.category() { let (filter_category, activity_category) = if *filter_opts.case_sensitive() { (category.clone(), activity_category.clone()) } else { - (category.to_lowercase(), activity_category.to_lowercase()) + ( + PaceCategory::new(&category.to_lowercase()), + PaceCategory::new(&activity_category.to_lowercase()), + ) }; if !WildMatch::new(&filter_category).matches(&activity_category) { @@ -161,7 +169,7 @@ impl ActivityStore { // Handle splitting subcategories let (category, subcategory) = - category::split_category_by_category_separator(&activity_category, None); + category::split_category_by_category_separator(activity_category, None); // Deduplicate activities by category and description first _ = activity_sessions_lookup_by_category @@ -210,8 +218,18 @@ impl ActivityStore { impl ActivityStorage for ActivityStore { #[tracing::instrument(skip(self))] - fn setup_storage(&self) -> PaceResult<()> { - self.storage.setup_storage() + fn setup(&self) -> PaceResult<()> { + self.storage.setup() + } + + #[tracing::instrument(skip(self))] + fn identify(&self) -> String { + self.storage.identify() + } + + #[tracing::instrument(skip(self))] + fn teardown(&self) -> PaceResult<()> { + self.storage.teardown() } } diff --git a/crates/core/src/service/activity_tracker.rs b/crates/service/src/activity_tracker.rs similarity index 86% rename from crates/core/src/service/activity_tracker.rs rename to crates/service/src/activity_tracker.rs index e96e8c26..3f741791 100644 --- a/crates/core/src/service/activity_tracker.rs +++ b/crates/service/src/activity_tracker.rs @@ -3,11 +3,10 @@ use pace_time::{time_frame::PaceTimeFrame, time_range::TimeRangeOptions}; use tracing::debug; -use crate::{ - domain::{filter::FilterOptions, reflection::ReflectionSummary}, - error::PaceOptResult, - service::activity_store::ActivityStore, -}; +use pace_core::{domain::reflection::ReflectionSummary, options::FilterOptions}; +use pace_error::PaceOptResult; + +use crate::activity_store::ActivityStore; // This struct represents the overall structure for tracking activities and their intermissions. pub struct ActivityTracker { @@ -44,7 +43,7 @@ impl ActivityTracker { let summary = ReflectionSummary::new(time_range_opts, summary_groups); - debug!("Generated reflection: {:#?}", summary); + debug!("Generated reflection: {summary:#?}"); Ok(Some(summary)) } diff --git a/crates/service/src/lib.rs b/crates/service/src/lib.rs new file mode 100644 index 00000000..fb56e79b --- /dev/null +++ b/crates/service/src/lib.rs @@ -0,0 +1,47 @@ +/// An activity store service +/// +/// This module contains the domain logic for tracking activities and their intermissions. +/// +pub mod activity_store; + +pub mod activity_tracker; + +use std::sync::Arc; + +use pace_core::prelude::{ActivityLogStorageKind, ActivityStorage, PaceConfig}; +use pace_error::{DatabaseStorageErrorKind, PaceResult}; +use tracing::debug; + +use pace_storage::storage::{ + file::TomlActivityStorage, in_memory::InMemoryActivityStorage, sqlite::SQLiteActivityStorage, +}; + +/// Get the storage backend from the configuration. +/// +/// # Arguments +/// +/// * `config` - The application configuration. +/// +/// # Errors +/// +/// This function should return an error if the storage backend cannot be created or is not supported. +/// +/// # Returns +/// +/// The storage backend. +pub fn get_storage_from_config(config: &PaceConfig) -> PaceResult> { + let storage: Arc = match config.storage().storage() { + ActivityLogStorageKind::File { location } => Arc::new(TomlActivityStorage::new(location)?), + ActivityLogStorageKind::Database { kind, url } => { + debug!("Connecting to SQLite database: {url}"); + + Arc::new(SQLiteActivityStorage::new(*kind, url)?) + } + ActivityLogStorageKind::InMemory => Arc::new(InMemoryActivityStorage::new()), + _ => return Err(DatabaseStorageErrorKind::StorageNotImplemented.into()), + }; + + debug!("Using storage backend: {storage:?}"); + + Ok(storage) +} diff --git a/crates/storage/Cargo.toml b/crates/storage/Cargo.toml new file mode 100644 index 00000000..0794d360 --- /dev/null +++ b/crates/storage/Cargo.toml @@ -0,0 +1,48 @@ +[package] +name = "pace_storage" +version = "0.1.0" +authors = { workspace = true } +categories = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +keywords = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +rust-version = { workspace = true } +description = "pace-storage - a library handling storage engines for pace" + +include = [ + "LICENSE", + "README.md", + "CHANGELOG.md", + "src/**/*", + "Cargo.toml", +] + +[dependencies] +chrono = { workspace = true, features = ["serde"] } +displaydoc = { workspace = true } +getset = { workspace = true } +itertools = { workspace = true } +libsqlite3-sys = { workspace = true, features = ["bundled"] } +merge = { workspace = true } +pace_core = { workspace = true } +pace_error = { workspace = true } +pace_time = { workspace = true } +parking_lot = { workspace = true, features = ["deadlock_detection"] } +rayon = { workspace = true } +sea-orm = { workspace = true, features = ["sqlx-sqlite", "runtime-tokio-rustls", "macros", "mock", "with-chrono", "debug-print"] } +sea-orm-migration = { workspace = true } +strum = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true, default-features = false, features = ["rt-multi-thread"] } +toml = { workspace = true, features = ["indexmap", "preserve_order"] } +tracing = { workspace = true } +typed-builder = { workspace = true } +ulid = { workspace = true, features = ["serde"] } + +[dev-dependencies] +chrono = { workspace = true, features = ["serde"] } + +[lints] +workspace = true diff --git a/crates/storage/LICENSE b/crates/storage/LICENSE new file mode 100644 index 00000000..0ad25db4 --- /dev/null +++ b/crates/storage/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/crates/storage/README.md b/crates/storage/README.md new file mode 100644 index 00000000..e9d3854d --- /dev/null +++ b/crates/storage/README.md @@ -0,0 +1,89 @@ +

+ +

+

pace-storage - a library handling storage engines for pace

+ +

+ + + + +

+ +## About + +`pace-storage` is a library to support timetracking on the command line. It is +the storage handling library for the `pace` timetracking application. + +⚠️ **Note:** `pace-storage` is currently in active development and is not yet +ready for production use. Expect breaking changes and incomplete features. We +encourage you to try it out and provide feedback, but please be aware that it is +not yet stable. + +## Contact + +You can ask questions in the +[Discussions](https://github.com/orgs/pace-rs/discussions) or have a look at the +[FAQ](https://pace.cli.rs/docs/FAQ.html). + +| Contact | Where? | +| ------------- | --------------------------------------------------------------------------------------------------------------- | +| Issue Tracker | [GitHub Issues](https://github.com/pace-rs/pace/issues/new/choose) | +| Discord | [![Discord](https://dcbadge.vercel.app/api/server/RKSWrAcYdG?style=flat-square)](https://discord.gg/RKSWrAcYdG) | +| Discussions | [GitHub Discussions](https://github.com/orgs/pace-rs/discussions) | + + + +## Examples + +TODO! + +## Contributing + +Found a bug? [Open an issue!](https://github.com/pace-rs/pace/issues/new/choose) + +Got an idea for an improvement? Don't keep it to yourself! + +- [Contribute fixes](https://github.com/pace-rs/pace/contribute) or new features + via a pull requests! + +Please make sure, that you read the +[contribution guide](https://pace.cli.rs/docs/contributing_to_pace.html). + +## Code of Conduct + +Please review and abide by the general +[Rust Community Code of Conduct](https://www.rust-lang.org/policies/code-of-conduct) +when contributing to this project. In the future, we might create our own Code +of Conduct and supplement it at this location. + +## Minimum Rust version policy + +This crate's minimum supported `rustc` version is `1.74.1`. + +The current policy is that the minimum Rust version required to use this crate +can be increased in minor version updates. For example, if `crate 1.0` requires +Rust 1.20.0, then `crate 1.0.z` for all values of `z` will also require Rust +1.20.0 or newer. However, `crate 1.y` for `y > 0` may require a newer minimum +version of Rust. + +In general, this crate will be conservative with respect to the minimum +supported version of Rust. + +## License + +**AGPL-3.0-or-later**; see [LICENSE](./LICENSE). diff --git a/crates/storage/src/convert.rs b/crates/storage/src/convert.rs new file mode 100644 index 00000000..3f09d096 --- /dev/null +++ b/crates/storage/src/convert.rs @@ -0,0 +1,29 @@ +use pace_core::prelude::ActivityItem; +use pace_error::PaceResult; + +use crate::entities::SQLiteActivityItem; + +pub trait Convert { + type Options; + type Source; + type Target; + + fn to_stored(source: Self::Source, opts: Self::Options) -> PaceResult; + fn from_stored(stored: Self::Target, opts: Self::Options) -> PaceResult; +} + +pub struct SQLiteActivityConverter; + +impl Convert for SQLiteActivityConverter { + type Options = (); + type Source = ActivityItem; + type Target = SQLiteActivityItem; + + fn to_stored(source: Self::Source, opts: Self::Options) -> PaceResult { + todo!() + } + + fn from_stored(stored: Self::Target, opts: Self::Options) -> PaceResult { + todo!() + } +} diff --git a/crates/storage/src/entity.rs b/crates/storage/src/entity.rs new file mode 100644 index 00000000..6542341d --- /dev/null +++ b/crates/storage/src/entity.rs @@ -0,0 +1,9 @@ +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 + +pub mod prelude; + +pub mod activities; +pub mod activities_categories; +pub mod activities_tags; +pub mod categories; +pub mod tags; diff --git a/crates/storage/src/entity/activities.rs b/crates/storage/src/entity/activities.rs new file mode 100644 index 00000000..e1fad052 --- /dev/null +++ b/crates/storage/src/entity/activities.rs @@ -0,0 +1,68 @@ +#![allow(unused_qualifications)] +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 + +use chrono::{DateTime, FixedOffset, Utc}; +use pace_core::domain::{activity::ActivityKind, status::ActivityStatusKind}; +use sea_orm::entity::prelude::*; +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "activities")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub guid: String, + pub description: String, + pub begin: DateTime, + pub end: Option>, + pub duration: Option, + pub kind: ActivityKind, + pub status: ActivityStatusKind, + pub parent_guid: Option, + pub created_at: DateTime, + pub updated_at: DateTime, + pub deleted_at: DateTime, +} + +#[derive(DeriveIden)] +pub enum Activities { + Table, + Guid, + Description, + Begin, + End, + Duration, + Kind, + Status, + ParentGuid, + CreatedAt, + UpdatedAt, + DeletedAt, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "Entity", + from = "Column::ParentGuid", + to = "Column::Guid", + on_update = "NoAction", + on_delete = "NoAction" + )] + SelfRef, + #[sea_orm(has_many = "super::activities_categories::Entity")] + ActivitiesCategories, + #[sea_orm(has_many = "super::activities_tags::Entity")] + ActivitiesTags, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::ActivitiesCategories.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::ActivitiesTags.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/storage/src/entity/activities_categories.rs b/crates/storage/src/entity/activities_categories.rs new file mode 100644 index 00000000..e57fe09f --- /dev/null +++ b/crates/storage/src/entity/activities_categories.rs @@ -0,0 +1,54 @@ +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "activities_categories")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub guid: String, + pub activity_guid: String, + pub category_guid: String, +} + +#[derive(DeriveIden)] +pub enum ActivitiesCategories { + Table, + Guid, + ActivityGuid, + CategoryGuid, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::activities::Entity", + from = "Column::ActivityGuid", + to = "super::activities::Column::Guid", + on_update = "NoAction", + on_delete = "NoAction" + )] + Activities, + #[sea_orm( + belongs_to = "super::categories::Entity", + from = "Column::CategoryGuid", + to = "super::categories::Column::Guid", + on_update = "NoAction", + on_delete = "NoAction" + )] + Categories, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Activities.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Categories.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/storage/src/entity/activities_tags.rs b/crates/storage/src/entity/activities_tags.rs new file mode 100644 index 00000000..9b167057 --- /dev/null +++ b/crates/storage/src/entity/activities_tags.rs @@ -0,0 +1,54 @@ +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "activities_tags")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub guid: String, + pub tag_guid: String, + pub activity_guid: String, +} + +#[derive(DeriveIden)] +pub enum ActivitiesTags { + Table, + Guid, + TagGuid, + ActivityGuid, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::activities::Entity", + from = "Column::ActivityGuid", + to = "super::activities::Column::Guid", + on_update = "NoAction", + on_delete = "NoAction" + )] + Activities, + #[sea_orm( + belongs_to = "super::tags::Entity", + from = "Column::TagGuid", + to = "super::tags::Column::Guid", + on_update = "NoAction", + on_delete = "NoAction" + )] + Tags, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Activities.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Tags.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/storage/src/entity/categories.rs b/crates/storage/src/entity/categories.rs new file mode 100644 index 00000000..4121fa16 --- /dev/null +++ b/crates/storage/src/entity/categories.rs @@ -0,0 +1,34 @@ +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "categories")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub guid: String, + pub category: String, + pub description: Option, +} + +#[derive(DeriveIden)] +pub enum Categories { + Table, + Guid, + Category, + Description, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::activities_categories::Entity")] + ActivitiesCategories, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::ActivitiesCategories.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/storage/src/entity/prelude.rs b/crates/storage/src/entity/prelude.rs new file mode 100644 index 00000000..cb3a2939 --- /dev/null +++ b/crates/storage/src/entity/prelude.rs @@ -0,0 +1,7 @@ +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 + +pub use super::activities::Entity as Activities; +pub use super::activities_categories::Entity as ActivitiesCategories; +pub use super::activities_tags::Entity as ActivitiesTags; +pub use super::categories::Entity as Categories; +pub use super::tags::Entity as Tags; diff --git a/crates/storage/src/entity/tags.rs b/crates/storage/src/entity/tags.rs new file mode 100644 index 00000000..338b1cd2 --- /dev/null +++ b/crates/storage/src/entity/tags.rs @@ -0,0 +1,32 @@ +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "tags")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub guid: String, + pub tag: String, +} + +#[derive(DeriveIden)] +pub enum Tags { + Table, + Guid, + Tag, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::activities_tags::Entity")] + ActivitiesTags, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::ActivitiesTags.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/storage/src/lib.rs b/crates/storage/src/lib.rs new file mode 100644 index 00000000..7a725b0c --- /dev/null +++ b/crates/storage/src/lib.rs @@ -0,0 +1,21 @@ +// pub mod convert; +pub mod entity; +pub mod migration; +pub mod query; +pub mod repository; +pub mod storage; + +use std::sync::OnceLock; + +use tokio::runtime::Runtime; + +#[allow(clippy::expect_used)] +fn runtime() -> &'static Runtime { + static RUNTIME: OnceLock = OnceLock::new(); + RUNTIME.get_or_init(|| { + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .expect("Failed to create tokio runtime") + }) +} diff --git a/crates/storage/src/migration.rs b/crates/storage/src/migration.rs new file mode 100644 index 00000000..efc289ed --- /dev/null +++ b/crates/storage/src/migration.rs @@ -0,0 +1,22 @@ +mod m20240325_000001_create_activities; +mod m20240326_000001_create_tags; +mod m20240326_000002_create_categories; +mod m20240326_000003_create_activities_tags; +mod m20240326_000004_create_activities_categories; + +pub use sea_orm_migration::prelude::{async_trait, MigrationTrait, MigratorTrait}; + +pub struct Migrator; + +#[async_trait::async_trait] +impl MigratorTrait for Migrator { + fn migrations() -> Vec> { + vec![ + Box::new(m20240325_000001_create_activities::Migration), + Box::new(m20240326_000001_create_tags::Migration), + Box::new(m20240326_000002_create_categories::Migration), + Box::new(m20240326_000003_create_activities_tags::Migration), + Box::new(m20240326_000004_create_activities_categories::Migration), + ] + } +} diff --git a/crates/storage/src/migration/m20240325_000001_create_activities.rs b/crates/storage/src/migration/m20240325_000001_create_activities.rs new file mode 100644 index 00000000..5105ad79 --- /dev/null +++ b/crates/storage/src/migration/m20240325_000001_create_activities.rs @@ -0,0 +1,81 @@ +use sea_orm_migration::prelude::*; + +use crate::entity::activities::Activities; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &'life1 SchemaManager<'_>) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(Activities::Table) + .if_not_exists() + .col( + ColumnDef::new(Activities::Guid) + .text() + .not_null() + .primary_key(), + ) + .col(ColumnDef::new(Activities::Description).text().not_null()) + .col( + ColumnDef::new(Activities::Begin) + .timestamp_with_time_zone() + .not_null(), + ) + .col( + ColumnDef::new(Activities::End) + .timestamp_with_time_zone() + .null(), + ) + .col(ColumnDef::new(Activities::Duration).integer().null()) + .col(ColumnDef::new(Activities::Kind).integer().not_null()) + .col(ColumnDef::new(Activities::Status).integer().not_null()) + .col(ColumnDef::new(Activities::ParentGuid).text().null()) + .col(ColumnDef::new(Activities::CreatedAt).timestamp().not_null()) + .col(ColumnDef::new(Activities::UpdatedAt).timestamp().null()) + .col(ColumnDef::new(Activities::DeletedAt).timestamp().null()) + .foreign_key( + ForeignKey::create() + .name("fk_activities_parent_guid") + .from(Activities::Table, Activities::ParentGuid) + .to(Activities::Table, Activities::Guid), + ) + .to_owned(), + ) + .await?; + + manager + .create_index( + Index::create() + .table(Activities::Table) + .name("idx_activities_parent_guid") + .col(Activities::ParentGuid) + .to_owned(), + ) + .await?; + + manager + .create_index( + Index::create() + .table(Activities::Table) + .name("idx_activities_description") + .col(Activities::Description) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &'life1 SchemaManager<'_>) -> Result<(), DbErr> { + manager + .drop_table( + Table::drop() + .table(Activities::Table) + .if_exists() + .to_owned(), + ) + .await + } +} diff --git a/crates/storage/src/migration/m20240326_000001_create_tags.rs b/crates/storage/src/migration/m20240326_000001_create_tags.rs new file mode 100644 index 00000000..38493bd1 --- /dev/null +++ b/crates/storage/src/migration/m20240326_000001_create_tags.rs @@ -0,0 +1,38 @@ +use sea_orm_migration::prelude::*; + +use crate::entity::tags::Tags; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &'life1 SchemaManager<'_>) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .if_not_exists() + .table(Tags::Table) + .col(ColumnDef::new(Tags::Guid).text().not_null().primary_key()) + .col(ColumnDef::new(Tags::Tag).text().not_null()) + .to_owned(), + ) + .await?; + + manager + .create_index( + Index::create() + .table(Tags::Table) + .name("idx_tags_tag") + .col(Tags::Tag) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &'life1 SchemaManager<'_>) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(Tags::Table).if_exists().to_owned()) + .await + } +} diff --git a/crates/storage/src/migration/m20240326_000002_create_categories.rs b/crates/storage/src/migration/m20240326_000002_create_categories.rs new file mode 100644 index 00000000..bf6ad4a5 --- /dev/null +++ b/crates/storage/src/migration/m20240326_000002_create_categories.rs @@ -0,0 +1,49 @@ +use sea_orm_migration::prelude::*; + +use crate::entity::categories::Categories; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &'life1 SchemaManager<'_>) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .if_not_exists() + .table(Categories::Table) + .col( + ColumnDef::new(Categories::Guid) + .text() + .not_null() + .primary_key(), + ) + .col(ColumnDef::new(Categories::Category).text().not_null()) + .col(ColumnDef::new(Categories::Description).text().null()) + .to_owned(), + ) + .await?; + + manager + .create_index( + Index::create() + .table(Categories::Table) + .name("idx_categories_category") + .col(Categories::Category) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &'life1 SchemaManager<'_>) -> Result<(), DbErr> { + manager + .drop_table( + Table::drop() + .table(Categories::Table) + .if_exists() + .to_owned(), + ) + .await + } +} diff --git a/crates/storage/src/migration/m20240326_000003_create_activities_tags.rs b/crates/storage/src/migration/m20240326_000003_create_activities_tags.rs new file mode 100644 index 00000000..f11d5719 --- /dev/null +++ b/crates/storage/src/migration/m20240326_000003_create_activities_tags.rs @@ -0,0 +1,57 @@ +use sea_orm_migration::prelude::*; + +use crate::entity::activities::Activities; +use crate::entity::activities_tags::ActivitiesTags; +use crate::entity::tags::Tags; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &'life1 SchemaManager<'_>) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .if_not_exists() + .table(ActivitiesTags::Table) + .col( + ColumnDef::new(ActivitiesTags::Guid) + .text() + .not_null() + .primary_key(), + ) + .col(ColumnDef::new(ActivitiesTags::TagGuid).text().not_null()) + .col( + ColumnDef::new(ActivitiesTags::ActivityGuid) + .text() + .not_null(), + ) + .foreign_key( + ForeignKey::create() + .name("fk_activities_tags_tag_guid") + .from(ActivitiesTags::Table, ActivitiesTags::TagGuid) + .to(Tags::Table, Tags::Guid), + ) + .foreign_key( + ForeignKey::create() + .name("fk_activities_tags_activity_guid") + .from(ActivitiesTags::Table, ActivitiesTags::ActivityGuid) + .to(Activities::Table, Activities::Guid), + ) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &'life1 SchemaManager<'_>) -> Result<(), DbErr> { + manager + .drop_table( + Table::drop() + .table(ActivitiesTags::Table) + .if_exists() + .to_owned(), + ) + .await + } +} diff --git a/crates/storage/src/migration/m20240326_000004_create_activities_categories.rs b/crates/storage/src/migration/m20240326_000004_create_activities_categories.rs new file mode 100644 index 00000000..677524a1 --- /dev/null +++ b/crates/storage/src/migration/m20240326_000004_create_activities_categories.rs @@ -0,0 +1,67 @@ +use sea_orm_migration::prelude::*; + +use crate::entity::activities::Activities; +use crate::entity::activities_categories::ActivitiesCategories; +use crate::entity::categories::Categories; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &'life1 SchemaManager<'_>) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(ActivitiesCategories::Table) + .if_not_exists() + .col( + ColumnDef::new(ActivitiesCategories::Guid) + .text() + .not_null() + .primary_key(), + ) + .col( + ColumnDef::new(ActivitiesCategories::ActivityGuid) + .text() + .not_null(), + ) + .col( + ColumnDef::new(ActivitiesCategories::CategoryGuid) + .text() + .not_null(), + ) + .foreign_key( + ForeignKey::create() + .name("fk_activities_categories_category_guid") + .from( + ActivitiesCategories::Table, + ActivitiesCategories::CategoryGuid, + ) + .to(Categories::Table, Categories::Guid), + ) + .foreign_key( + ForeignKey::create() + .name("fk_activities_categories_activity_guid") + .from( + ActivitiesCategories::Table, + ActivitiesCategories::ActivityGuid, + ) + .to(Activities::Table, Activities::Guid), + ) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &'life1 SchemaManager<'_>) -> Result<(), DbErr> { + manager + .drop_table( + Table::drop() + .table(ActivitiesCategories::Table) + .if_exists() + .to_owned(), + ) + .await + } +} diff --git a/crates/storage/src/query.rs b/crates/storage/src/query.rs new file mode 100644 index 00000000..881ac953 --- /dev/null +++ b/crates/storage/src/query.rs @@ -0,0 +1,8 @@ +use pace_error::PaceResult; + +// Lazy loading related entities +trait LazyLoad { + fn query(&self) -> PaceResult> + where + Self: Sized; +} diff --git a/crates/storage/src/repository.rs b/crates/storage/src/repository.rs new file mode 100644 index 00000000..98c00fcd --- /dev/null +++ b/crates/storage/src/repository.rs @@ -0,0 +1,100 @@ +pub mod activity; +pub mod category; +pub mod tag; + +use getset::Getters; +use pace_error::{PaceOptResult, PaceResult}; + +pub(crate) trait Repository { + /// Read a single entity by its id. + /// + /// # Arguments + /// + /// * `id` - The id of the entity to read. + /// + /// # Errors + /// + /// Returns an error if there was a problem reading the entity. + /// + /// # Returns + /// + /// Returns the entity if it exists or None if it does not. + async fn read(&self, id: &str) -> PaceOptResult; + + /// Read all entities of a given type. + /// + /// # Errors + /// + /// Returns an error if there was a problem reading the entities. + /// + /// # Returns + /// + /// Returns a vector of all entities of the given type or an + /// empty vector if there are none. + async fn read_all(&self) -> PaceOptResult>; + + /// Create a new entity of a given type. + /// + /// # Arguments + /// + /// * `entity` - The entity to create. + /// + /// # Errors + /// + /// Returns an error if there was a problem creating the entity. + /// + /// # Returns + /// + /// Returns the id of the created entity. + async fn create(&self, model: &T) -> PaceResult; + + /// Update an existing entity of a given type. + /// + /// # Arguments + /// + /// * `id` - The id of the entity to update. + /// * `entity` - The entity to update. + /// + /// # Errors + /// + /// Returns an error if there was a problem updating the entity. + /// + /// # Returns + /// + /// Returns nothing if the entity was updated successfully. + async fn update(&self, id: &str, model: &T) -> PaceResult<()>; + + /// Delete an existing entity of a given type. + /// + /// # Arguments + /// + /// * `id` - The id of the entity to delete. + /// + /// # Errors + /// + /// Returns an error if there was a problem deleting the entity. + /// + /// # Returns + /// + /// Returns the deleted entity if it exists. + async fn delete(&self, id: &str) -> PaceOptResult; +} + +#[derive(Debug, Getters)] +#[getset(get = "pub")] +pub struct SeaOrmRepository<'conn> { + activity: activity::ActivityRepository<'conn, sea_orm::DatabaseConnection>, + category: category::CategoryRepository<'conn, sea_orm::DatabaseConnection>, + tag: tag::TagRepository<'conn, sea_orm::DatabaseConnection>, +} + +impl<'conn> SeaOrmRepository<'conn> { + #[must_use] + pub const fn new(connection: &'conn sea_orm::DatabaseConnection) -> Self { + Self { + activity: activity::ActivityRepository::new(connection), + category: category::CategoryRepository::new(connection), + tag: tag::TagRepository::new(connection), + } + } +} diff --git a/crates/storage/src/repository/activity.rs b/crates/storage/src/repository/activity.rs new file mode 100644 index 00000000..366d039e --- /dev/null +++ b/crates/storage/src/repository/activity.rs @@ -0,0 +1,84 @@ +use pace_error::{DatabaseStorageErrorKind, PaceOptResult, PaceResult}; +use sea_orm::{EntityTrait, IntoActiveModel}; + +use crate::entity::activities::{Entity as ActivityEntity, Model as ActivityModel}; +use crate::entity::categories::{Entity as CategoryEntity, Model as CategoryModel}; +use crate::repository::Repository; + +#[derive(Debug)] +pub struct ActivityRepository<'conn, C> { + connection: &'conn C, +} + +impl<'conn, C> ActivityRepository<'conn, C> { + pub const fn new(connection: &'conn C) -> Self { + Self { connection } + } +} + +// TODO!: Implement query for related entities +impl<'conn> Repository for ActivityRepository<'conn, sea_orm::DatabaseConnection> { + async fn read(&self, id: &str) -> PaceOptResult { + Ok(ActivityEntity::find_by_id(id) + .one(self.connection) + .await + .map_err(|source| DatabaseStorageErrorKind::RepositoryReadFailed { + source, + item_type: "activity".to_string(), + item_id: id.to_string(), + })?) + } + + async fn read_all(&self) -> PaceOptResult> { + let items = ActivityEntity::find() + .all(self.connection) + .await + .map_err(|source| DatabaseStorageErrorKind::RepositoryReadFailed { + source, + item_type: "activity".to_string(), + item_id: "all".to_string(), + })?; + + if items.is_empty() { + return Ok(None); + } + + Ok(Some(items)) + } + + async fn create(&self, model: &ActivityModel) -> PaceResult { + // TODO: What else should we do with ActiveModel here? + let active_model = model.clone().into_active_model(); + + let id = ActivityEntity::insert(active_model) + .exec(self.connection) + .await + .map_err(|source| DatabaseStorageErrorKind::RepositoryCreateFailed { + source, + item_type: "activity".to_string(), + })? + .last_insert_id; + + Ok(id) + } + + async fn update(&self, id: &str, model: &ActivityModel) -> PaceResult<()> { + unimplemented!() + } + + async fn delete(&self, id: &str) -> PaceOptResult { + let item = self.read(id).await?; + + // TODO: Unused result here, what should we do with the rows affected? + _ = ActivityEntity::delete_by_id(id) + .exec(self.connection) + .await + .map_err(|source| DatabaseStorageErrorKind::RepositoryDeleteFailed { + source, + item_type: "activity".to_string(), + item_id: id.to_string(), + })?; + + Ok(item) + } +} diff --git a/crates/storage/src/repository/category.rs b/crates/storage/src/repository/category.rs new file mode 100644 index 00000000..3bdc2e73 --- /dev/null +++ b/crates/storage/src/repository/category.rs @@ -0,0 +1,82 @@ +use pace_error::{DatabaseStorageErrorKind, PaceOptResult, PaceResult}; +use sea_orm::{EntityTrait, IntoActiveModel}; + +use crate::entity::categories::{Entity as CategoryEntity, Model as CategoryModel}; +use crate::repository::Repository; + +#[derive(Debug)] +pub struct CategoryRepository<'conn, C> { + connection: &'conn C, +} + +impl<'conn, C> CategoryRepository<'conn, C> { + pub const fn new(connection: &'conn C) -> Self { + Self { connection } + } +} + +impl<'conn> Repository for CategoryRepository<'conn, sea_orm::DatabaseConnection> { + async fn read(&self, id: &str) -> PaceOptResult { + Ok(CategoryEntity::find_by_id(id) + .one(self.connection) + .await + .map_err(|source| DatabaseStorageErrorKind::RepositoryReadFailed { + source, + item_type: "category".to_string(), + item_id: id.to_string(), + })?) + } + + async fn read_all(&self) -> PaceOptResult> { + let items = CategoryEntity::find() + .all(self.connection) + .await + .map_err(|source| DatabaseStorageErrorKind::RepositoryReadFailed { + source, + item_type: "category".to_string(), + item_id: "all".to_string(), + })?; + + if items.is_empty() { + return Ok(None); + } + + Ok(Some(items)) + } + + async fn create(&self, model: &CategoryModel) -> PaceResult { + // TODO: What else should we do with ActiveModel here? + let active_model = model.clone().into_active_model(); + + let id = CategoryEntity::insert(active_model) + .exec(self.connection) + .await + .map_err(|source| DatabaseStorageErrorKind::RepositoryCreateFailed { + source, + item_type: "category".to_string(), + })? + .last_insert_id; + + Ok(id) + } + + async fn update(&self, id: &str, model: &CategoryModel) -> PaceResult<()> { + unimplemented!() + } + + async fn delete(&self, id: &str) -> PaceOptResult { + let item = self.read(id).await?; + + // TODO: Unused result here, what should we do with the rows affected? + _ = CategoryEntity::delete_by_id(id) + .exec(self.connection) + .await + .map_err(|source| DatabaseStorageErrorKind::RepositoryDeleteFailed { + source, + item_type: "category".to_string(), + item_id: id.to_string(), + })?; + + Ok(item) + } +} diff --git a/crates/storage/src/repository/tag.rs b/crates/storage/src/repository/tag.rs new file mode 100644 index 00000000..9d90fb95 --- /dev/null +++ b/crates/storage/src/repository/tag.rs @@ -0,0 +1,82 @@ +use pace_error::{DatabaseStorageErrorKind, PaceOptResult, PaceResult}; +use sea_orm::{EntityTrait, IntoActiveModel}; + +use crate::entity::tags::{Entity as TagEntity, Model as TagModel}; +use crate::repository::Repository; + +#[derive(Debug)] +pub struct TagRepository<'conn, C> { + connection: &'conn C, +} + +impl<'conn, C> TagRepository<'conn, C> { + pub const fn new(connection: &'conn C) -> Self { + Self { connection } + } +} + +impl<'conn> Repository for TagRepository<'conn, sea_orm::DatabaseConnection> { + async fn read(&self, id: &str) -> PaceOptResult { + Ok(TagEntity::find_by_id(id) + .one(self.connection) + .await + .map_err(|source| DatabaseStorageErrorKind::RepositoryReadFailed { + source, + item_type: "tag".to_string(), + item_id: id.to_string(), + })?) + } + + async fn read_all(&self) -> PaceOptResult> { + let items = TagEntity::find() + .all(self.connection) + .await + .map_err(|source| DatabaseStorageErrorKind::RepositoryReadFailed { + source, + item_type: "tag".to_string(), + item_id: "all".to_string(), + })?; + + if items.is_empty() { + return Ok(None); + } + + Ok(Some(items)) + } + + async fn create(&self, model: &TagModel) -> PaceResult { + // TODO: What else should we do with ActiveModel here? + let active_model = model.clone().into_active_model(); + + let id = TagEntity::insert(active_model) + .exec(self.connection) + .await + .map_err(|source| DatabaseStorageErrorKind::RepositoryCreateFailed { + source, + item_type: "tag".to_string(), + })? + .last_insert_id; + + Ok(id) + } + + async fn update(&self, id: &str, model: &TagModel) -> PaceResult<()> { + unimplemented!() + } + + async fn delete(&self, id: &str) -> PaceOptResult { + let item = self.read(id).await?; + + // TODO: Unused result here, what should we do with the rows affected? + _ = TagEntity::delete_by_id(id) + .exec(self.connection) + .await + .map_err(|source| DatabaseStorageErrorKind::RepositoryDeleteFailed { + source, + item_type: "tag".to_string(), + item_id: id.to_string(), + })?; + + Ok(item) + } +} diff --git a/crates/storage/src/storage.rs b/crates/storage/src/storage.rs new file mode 100644 index 00000000..f08b661b --- /dev/null +++ b/crates/storage/src/storage.rs @@ -0,0 +1,7 @@ +/// A type of storage that can be synced to a persistent medium - a file +pub mod file; + +/// An in-memory storage backend for activities. +pub mod in_memory; + +pub mod sqlite; diff --git a/crates/core/src/storage/file.rs b/crates/storage/src/storage/file.rs similarity index 87% rename from crates/core/src/storage/file.rs rename to crates/storage/src/storage/file.rs index 2e59b58b..fa286d07 100644 --- a/crates/core/src/storage/file.rs +++ b/crates/storage/src/storage/file.rs @@ -5,25 +5,20 @@ use std::{ path::{Path, PathBuf}, }; -use pace_time::{date::PaceDate, duration::PaceDurationRange, time_range::TimeRangeOptions}; - -use crate::{ - commands::{ - hold::HoldOptions, resume::ResumeOptions, DeleteOptions, EndOptions, KeywordOptions, - UpdateOptions, - }, - domain::{ - activity::{Activity, ActivityGuid, ActivityItem, ActivityKind}, - activity_log::ActivityLog, - filter::{ActivityFilterKind, FilteredActivities}, - status::ActivityStatusKind, +use pace_core::{ + options::{ + DeleteOptions, EndOptions, HoldOptions, KeywordOptions, ResumeOptions, UpdateOptions, }, - error::{PaceErrorKind, PaceOptResult, PaceResult}, - storage::{ - in_memory::InMemoryActivityStorage, ActivityQuerying, ActivityReadOps, - ActivityStateManagement, ActivityStorage, ActivityWriteOps, SyncStorage, + prelude::{ + Activity, ActivityFilterKind, ActivityGuid, ActivityItem, ActivityKind, ActivityLog, + ActivityQuerying, ActivityReadOps, ActivityStateManagement, ActivityStatusKind, + ActivityStorage, ActivityWriteOps, FilteredActivities, SyncStorage, }, }; +use pace_error::{PaceOptResult, PaceResult, TomlFileStorageErrorKind}; +use pace_time::{date::PaceDate, duration::PaceDurationRange, time_range::TimeRangeOptions}; + +use crate::storage::in_memory::InMemoryActivityStorage; /// In-memory backed TOML activity storage /// @@ -107,12 +102,12 @@ impl TomlActivityStorage { impl ActivityStorage for TomlActivityStorage { #[tracing::instrument(skip(self))] - fn setup_storage(&self) -> PaceResult<()> { + fn setup(&self) -> PaceResult<()> { if !self.path.exists() { create_dir_all( - self.path - .parent() - .ok_or(PaceErrorKind::ParentDirNotFound(self.path.clone()))?, + self.path.parent().ok_or_else(|| { + TomlFileStorageErrorKind::ParentDirNotFound(self.path.clone()) + })?, )?; let mut file = OpenOptions::new() @@ -125,11 +120,21 @@ impl ActivityStorage for TomlActivityStorage { } Ok(()) } + + #[tracing::instrument(skip(self))] + fn teardown(&self) -> PaceResult<()> { + self.sync_to_file() + } + + #[tracing::instrument(skip(self))] + fn identify(&self) -> String { + format!("TOML file storage: {}", self.path.display()) + } } impl ActivityReadOps for TomlActivityStorage { #[tracing::instrument(skip(self))] - fn read_activity(&self, activity_id: ActivityGuid) -> PaceResult { + fn read_activity(&self, activity_id: ActivityGuid) -> PaceOptResult { self.cache.read_activity(activity_id) } diff --git a/crates/core/src/storage/in_memory.rs b/crates/storage/src/storage/in_memory.rs similarity index 81% rename from crates/core/src/storage/in_memory.rs rename to crates/storage/src/storage/in_memory.rs index e78ea361..998fb89a 100644 --- a/crates/core/src/storage/in_memory.rs +++ b/crates/storage/src/storage/in_memory.rs @@ -11,26 +11,18 @@ use merge::Merge; use rayon::prelude::{IntoParallelRefIterator, ParallelIterator}; use tracing::debug; -use crate::{ - commands::{ - hold::HoldOptions, resume::ResumeOptions, DeleteOptions, EndOptions, KeywordOptions, - UpdateOptions, +use pace_core::{ + options::{ + DeleteOptions, EndOptions, HoldOptions, KeywordOptions, ResumeOptions, UpdateOptions, }, - domain::{ - activity::{ - Activity, ActivityEndOptions, ActivityGuid, ActivityItem, ActivityKind, - ActivityKindOptions, - }, - activity_log::ActivityLog, - filter::{ActivityFilterKind, FilteredActivities}, - status::ActivityStatusKind, - }, - error::{ActivityLogErrorKind, PaceOptResult, PaceResult}, - storage::{ - ActivityQuerying, ActivityReadOps, ActivityStateManagement, ActivityStorage, - ActivityWriteOps, SyncStorage, + prelude::{ + Activity, ActivityEndOptions, ActivityFilterKind, ActivityGuid, ActivityItem, ActivityKind, + ActivityKindOptions, ActivityLog, ActivityQuerying, ActivityReadOps, + ActivityStateManagement, ActivityStatusKind, ActivityStorage, ActivityWriteOps, + FilteredActivities, PaceCategory, SyncStorage, }, }; +use pace_error::{ActivityLogErrorKind, PaceOptResult, PaceResult}; /// Type for shared `ActivityLog` type SharedActivityLog = Arc>; @@ -89,10 +81,19 @@ impl Default for InMemoryActivityStorage { } impl ActivityStorage for InMemoryActivityStorage { - fn setup_storage(&self) -> PaceResult<()> { + fn setup(&self) -> PaceResult<()> { debug!("Setting up in-memory storage"); Ok(()) } + + fn teardown(&self) -> PaceResult<()> { + debug!("Tearing down in-memory storage"); + Ok(()) + } + + fn identify(&self) -> String { + "In-memory storage".to_string() + } } impl SyncStorage for InMemoryActivityStorage { @@ -105,19 +106,22 @@ impl SyncStorage for InMemoryActivityStorage { impl ActivityReadOps for InMemoryActivityStorage { #[tracing::instrument(skip(self))] - fn read_activity(&self, activity_id: ActivityGuid) -> PaceResult { + fn read_activity(&self, activity_id: ActivityGuid) -> PaceOptResult { let activities = self.log.read(); - let activity = activities - .get(&activity_id) - .cloned() - .ok_or(ActivityLogErrorKind::ActivityNotFound(activity_id))?; + let activity = + activities + .get(&activity_id) + .cloned() + .ok_or(ActivityLogErrorKind::ActivityNotFound( + activity_id.to_string(), + ))?; drop(activities); - debug!("Activity with id {:?} found: {:?}", activity_id, activity); + debug!("Activity with id {activity_id:?} found: {activity:?}"); - Ok((activity_id, activity).into()) + Ok(Some((activity_id, activity).into())) } #[tracing::instrument(skip(self))] @@ -146,7 +150,7 @@ impl ActivityReadOps for InMemoryActivityStorage { drop(activity_log); - debug!("Filtered activities: {:?}", filtered); + debug!("Filtered activities: {filtered:?}"); if filtered.is_empty() { return Ok(None); @@ -188,7 +192,10 @@ impl ActivityWriteOps for InMemoryActivityStorage { // it's not expected to happen. if activities.contains_key(activity_item.guid()) { debug!("Activity ID already in use: {:?}", activity_item.guid()); - return Err(ActivityLogErrorKind::ActivityIdAlreadyInUse(*activity_item.guid()).into()); + return Err(ActivityLogErrorKind::ActivityIdAlreadyInUse( + activity_item.guid().to_string(), + ) + .into()); } drop(activities); @@ -215,19 +222,22 @@ impl ActivityWriteOps for InMemoryActivityStorage { ) -> PaceResult { let activities = self.log.read(); - let original_activity = activities - .get(&activity_id) - .cloned() - .ok_or(ActivityLogErrorKind::ActivityNotFound(activity_id))?; + let original_activity = + activities + .get(&activity_id) + .cloned() + .ok_or(ActivityLogErrorKind::ActivityNotFound( + activity_id.to_string(), + ))?; - debug!("Original activity: {:?}", original_activity); + debug!("Original activity: {original_activity:?}"); drop(activities); let mut activities = self.log.write(); let _ = activities.entry(activity_id).and_modify(|activity| { - debug!("Updating activity: {:?}", activity); + debug!("Updating activity: {activity:?}"); activity.merge(updated_activity); }); @@ -244,9 +254,12 @@ impl ActivityWriteOps for InMemoryActivityStorage { ) -> PaceResult { let mut activities = self.log.write(); - let activity = activities - .remove(&activity_id) - .ok_or(ActivityLogErrorKind::ActivityNotFound(activity_id))?; + let activity = + activities + .remove(&activity_id) + .ok_or(ActivityLogErrorKind::ActivityNotFound( + activity_id.to_string(), + ))?; drop(activities); @@ -265,7 +278,9 @@ impl ActivityStateManagement for InMemoryActivityStorage { let begin_time = *activities .get(&activity_id) - .ok_or(ActivityLogErrorKind::ActivityNotFound(activity_id))? + .ok_or(ActivityLogErrorKind::ActivityNotFound( + activity_id.to_string(), + ))? .begin(); drop(activities); @@ -275,7 +290,7 @@ impl ActivityStateManagement for InMemoryActivityStorage { calculate_duration(&begin_time, end_opts.end_time())?, ); - debug!("End options: {:?}", end_opts); + debug!("End options: {end_opts:?}"); let mut activities = self.log.write(); @@ -285,7 +300,9 @@ impl ActivityStateManagement for InMemoryActivityStorage { drop(activities); - self.read_activity(activity_id) + Ok(self + .read_activity(activity_id)? + .ok_or_else(|| ActivityLogErrorKind::ActivityNotFound(activity_id.to_string()))?) } #[tracing::instrument(skip(self))] @@ -295,7 +312,7 @@ impl ActivityStateManagement for InMemoryActivityStorage { return Ok(None); }; - debug!("Most recent activity: {:?}", most_recent); + debug!("Most recent activity: {most_recent:?}"); let activity = self.end_activity(*most_recent.guid(), end_opts)?; @@ -319,7 +336,7 @@ impl ActivityStateManagement for InMemoryActivityStorage { drop(activities); - debug!("Endable activities: {:?}", endable_activities); + debug!("Endable activities: {endable_activities:?}"); // There are no active activities if endable_activities.is_empty() { @@ -334,7 +351,7 @@ impl ActivityStateManagement for InMemoryActivityStorage { }) .collect::>>()?; - debug!("Ended activities: {:?}", ended_activities); + debug!("Ended activities: {ended_activities:?}"); if ended_activities.len() != endable_activities.len() { debug!("Not all activities were ended."); @@ -382,7 +399,7 @@ impl ActivityStateManagement for InMemoryActivityStorage { }) .collect::>>()?; - debug!("Ended intermissions: {:?}", ended_intermissions); + debug!("Ended intermissions: {ended_intermissions:?}"); if ended_intermissions.len() != active_intermissions.len() { debug!("Not all intermissions were ended."); @@ -400,23 +417,27 @@ impl ActivityStateManagement for InMemoryActivityStorage { activity_id: ActivityGuid, resume_opts: ResumeOptions, ) -> PaceResult { - let resumable_activity = self.read_activity(activity_id)?; + let resumable_activity = self + .read_activity(activity_id)? + .ok_or_else(|| ActivityLogErrorKind::ActivityNotFound(activity_id.to_string()))?; - debug!("Resumable activity: {:?}", resumable_activity); + debug!("Resumable activity: {resumable_activity:?}"); // If the activity is active, return early with an error if resumable_activity.activity().is_in_progress() { debug!("Activity is already active."); - return Err(ActivityLogErrorKind::ActiveActivityFound(activity_id).into()); + return Err(ActivityLogErrorKind::ActiveActivityFound(activity_id.to_string()).into()); } else if resumable_activity.activity().is_completed() { debug!("Activity has ended."); - return Err(ActivityLogErrorKind::ActivityAlreadyEnded(activity_id).into()); + return Err(ActivityLogErrorKind::ActivityAlreadyEnded(activity_id.to_string()).into()); } else if resumable_activity.activity().is_archived() { debug!("Activity is archived."); - return Err(ActivityLogErrorKind::ActivityAlreadyArchived(activity_id).into()); + return Err( + ActivityLogErrorKind::ActivityAlreadyArchived(activity_id.to_string()).into(), + ); } else if !resumable_activity.activity().is_paused() { debug!("Activity is not held."); - return Err(ActivityLogErrorKind::NoHeldActivityFound(activity_id).into()); + return Err(ActivityLogErrorKind::NoHeldActivityFound(activity_id.to_string()).into()); }; // If there are active intermissions for any activity, end the intermissions @@ -424,7 +445,7 @@ impl ActivityStateManagement for InMemoryActivityStorage { // so you can't have multiple intermissions at once, only one at a time. let ended_intermission_ids = self.end_all_active_intermissions(resume_opts.into())?; - debug!("Ended intermission ids: {:?}", ended_intermission_ids); + debug!("Ended intermission ids: {ended_intermission_ids:?}"); // Update the activity to be active again let mut editable_activity = resumable_activity.clone(); @@ -434,7 +455,7 @@ impl ActivityStateManagement for InMemoryActivityStorage { .set_status(ActivityStatusKind::InProgress) .clone(); - debug!("Updated activity: {:?}", updated_activity); + debug!("Updated activity: {updated_activity:?}"); let _ = self.update_activity( *resumable_activity.guid(), @@ -452,20 +473,26 @@ impl ActivityStateManagement for InMemoryActivityStorage { hold_opts: HoldOptions, ) -> PaceResult { // Get ActivityItem for activity that - let active_activity = self.read_activity(activity_id)?; + let active_activity = self + .read_activity(activity_id)? + .ok_or_else(|| ActivityLogErrorKind::ActivityNotFound(activity_id.to_string()))?; - debug!("Active activity: {:?}", active_activity); + debug!("Active activity: {active_activity:?}"); // make sure, the activity is not already ended or archived if !active_activity.activity().is_in_progress() { debug!("Activity is not active."); - return Err(ActivityLogErrorKind::NoActiveActivityFound(activity_id).into()); + return Err( + ActivityLogErrorKind::NoActiveActivityFound(activity_id.to_string()).into(), + ); } else if active_activity.activity().is_completed() { debug!("Activity has ended."); - return Err(ActivityLogErrorKind::ActivityAlreadyEnded(activity_id).into()); + return Err(ActivityLogErrorKind::ActivityAlreadyEnded(activity_id.to_string()).into()); } else if active_activity.activity().is_archived() { debug!("Activity is archived."); - return Err(ActivityLogErrorKind::ActivityAlreadyArchived(activity_id).into()); + return Err( + ActivityLogErrorKind::ActivityAlreadyArchived(activity_id.to_string()).into(), + ); }; // Check if the latest active activity is already having an intermission @@ -496,10 +523,7 @@ impl ActivityStateManagement for InMemoryActivityStorage { let active_intermission_ids = self.end_all_active_intermissions(hold_opts.clone().into())?; - debug!( - "Ended active intermission ids: {:?}", - active_intermission_ids - ); + debug!("Ended active intermission ids: {active_intermission_ids:?}"); // Create a new intermission for the active activity let activity_kind_opts = ActivityKindOptions::with_parent_id(*active_activity.guid()); @@ -520,7 +544,7 @@ impl ActivityStateManagement for InMemoryActivityStorage { let created_intermission_item = self.begin_activity(intermission)?; - debug!("Created intermission: {:?}", created_intermission_item); + debug!("Created intermission: {created_intermission_item:?}"); // Update the active activity to be held let mut editable_activity = active_activity.clone(); @@ -530,7 +554,7 @@ impl ActivityStateManagement for InMemoryActivityStorage { .set_status(ActivityStatusKind::Paused) .clone(); - debug!("Updated activity: {:?}", updated_activity); + debug!("Updated activity: {updated_activity:?}"); let _ = self.update_activity( *active_activity.guid(), @@ -602,7 +626,7 @@ impl ActivityQuerying for InMemoryActivityStorage { |mut acc: BTreeMap>, (activity_id, activity)| { let begin_date = activity.begin().date_naive(); - debug!("Begin date: {:?}", begin_date); + debug!("Begin date: {begin_date:?}"); acc.entry(begin_date) .or_default() @@ -627,30 +651,36 @@ impl ActivityQuerying for InMemoryActivityStorage { return Ok(None); }; - debug!("Intermissions: {:?}", intermissions); + debug!("Intermissions: {intermissions:?}"); Some(intermissions.into_iter().try_fold( BTreeMap::new(), |mut acc: BTreeMap>, intermission_id| { - let intermission = self.read_activity(intermission_id)?; + let intermission = self.read_activity(intermission_id)?.ok_or_else(|| { + ActivityLogErrorKind::ActivityNotFound(intermission_id.to_string()) + })?; - debug!("Intermission: {:?}", intermission); + debug!("Intermission: {intermission:?}"); let parent_id = intermission .activity() .activity_kind_options() .as_ref() .ok_or(ActivityLogErrorKind::ActivityKindOptionsNotFound( - intermission_id, + intermission_id.to_string(), ))? .parent_id() - .ok_or(ActivityLogErrorKind::ParentIdNotSet(intermission_id))?; + .ok_or(ActivityLogErrorKind::ParentIdNotSet( + intermission_id.to_string(), + ))?; - debug!("Parent id: {:?}", parent_id); + debug!("Parent id: {parent_id:?}"); - let parent_activity = self.read_activity(parent_id)?; + let parent_activity = self + .read_activity(parent_id)? + .ok_or_else(|| ActivityLogErrorKind::ActivityNotFound(parent_id.to_string()))?; - debug!("Parent activity: {:?}", parent_activity); + debug!("Parent activity: {parent_activity:?}"); acc.entry(parent_id).or_default().push(parent_activity); @@ -674,12 +704,14 @@ impl ActivityQuerying for InMemoryActivityStorage { if let Some(category) = keyword_opts.category() { let category = category.to_lowercase(); - debug!("Category: {:?}", category); + debug!("Category: {category:?}"); if activity .category() .as_ref() - .ok_or(ActivityLogErrorKind::CategoryNotSet(*activity_id))? + .ok_or(ActivityLogErrorKind::CategoryNotSet( + activity_id.to_string(), + ))? .to_lowercase() .contains(category.as_str()) { @@ -692,11 +724,13 @@ impl ActivityQuerying for InMemoryActivityStorage { debug!("No category specified. Using 'Uncategorized' as the category."); + let fallback_category = PaceCategory::new("Uncategorized"); + acc.entry( activity .category() .as_ref() - .unwrap_or(&"Uncategorized".to_string()) + .unwrap_or(&fallback_category) .to_string(), ) .or_default() @@ -717,10 +751,8 @@ impl ActivityQuerying for InMemoryActivityStorage { BTreeMap::new(), |mut acc: BTreeMap>, (activity_id, activity)| { debug!( - "Activity kind: {:?} for item {:?} with id {:?}", - activity.kind(), - activity, - activity_id + "Activity kind: {:?} for item {activity:?} with id {activity_id:?}", + activity.kind() ); acc.entry(*activity.kind()) @@ -743,10 +775,8 @@ impl ActivityQuerying for InMemoryActivityStorage { BTreeMap::new(), |mut acc: BTreeMap>, (activity_id, activity)| { debug!( - "Activity status: {:?} for item {:?} with id {:?}", - activity.status(), - activity, - activity_id + "Activity status: {:?} for item {activity:?} with id {activity_id:?}", + activity.status() ); acc.entry(*activity.status()) @@ -795,10 +825,10 @@ impl ActivityQuerying for InMemoryActivityStorage { mod tests { use super::*; - use crate::error::TestResult; use chrono::Local; + use pace_core::prelude::{PaceDescription, PaceTagCollection}; + use pace_error::TestResult; use pace_time::date_time::PaceDateTime; - use std::collections::HashSet; #[test] fn test_in_memory_activity_storage_passes() { @@ -829,10 +859,10 @@ mod tests { let begin = Local::now().fixed_offset(); let kind = ActivityKind::Activity; - let description = "Test activity"; + let description = PaceDescription::new("Test activity"); let tags = vec!["test".to_string(), "activity".to_string()] .into_iter() - .collect::>(); + .collect::(); let activity = Activity::builder() .begin(begin) @@ -849,7 +879,12 @@ mod tests { "Activity was not created." ); - let stored_activity = storage.read_activity(*item.guid())?; + let stored_activity = storage.read_activity(*item.guid())?.ok_or_else(|| { + format!( + "Activity with ID {} was not found.", + item.guid().to_string() + ) + })?; assert_eq!( activity, @@ -866,10 +901,10 @@ mod tests { let begin = Local::now().fixed_offset(); let kind = ActivityKind::Activity; - let description = "Test activity"; + let description = PaceDescription::new("Test activity"); let tags = vec!["test".to_string(), "activity".to_string()] .into_iter() - .collect::>(); + .collect::(); let activity = Activity::builder() .begin(begin) @@ -891,7 +926,14 @@ mod tests { "Amount of activities is not the same as the amount of created activities." ); - let stored_activity = storage.read_activity(filtered_activities[0])?; + let stored_activity = storage + .read_activity(filtered_activities[0])? + .ok_or_else(|| { + format!( + "Activity with ID {} was not found.", + filtered_activities[0].to_string() + ) + })?; assert_eq!( activity, @@ -908,10 +950,10 @@ mod tests { let begin = Local::now().fixed_offset(); let kind = ActivityKind::Activity; - let description = "Test activity"; + let description = PaceDescription::new("Test activity"); let tags = vec!["test".to_string(), "activity".to_string()] .into_iter() - .collect::>(); + .collect::(); let og_activity = Activity::builder() .begin(begin) @@ -922,7 +964,14 @@ mod tests { let activity_item = storage.create_activity(og_activity.clone())?; - let read_activity = storage.read_activity(*activity_item.guid())?; + let read_activity = storage + .read_activity(*activity_item.guid())? + .ok_or_else(|| { + format!( + "Activity with ID {} was not found.", + activity_item.guid().to_string() + ) + })?; assert_eq!( og_activity, @@ -930,11 +979,11 @@ mod tests { "Stored activity is not the same as the original activity." ); - let new_description = "Updated description"; + let new_description = PaceDescription::new("Updated description"); let tags = vec!["bla".to_string(), "test".to_string()] .into_iter() - .collect::>(); + .collect::(); let new_begin = PaceDateTime::from( begin + chrono::TimeDelta::try_seconds(30).ok_or("Invalid time delta")?, @@ -944,7 +993,7 @@ mod tests { .begin(new_begin) .kind(ActivityKind::PomodoroWork) .status(ActivityStatusKind::InProgress) - .description(new_description) + .description(new_description.clone()) .tags(tags.clone()) .build(); @@ -960,7 +1009,15 @@ mod tests { "Stored activity is not the same as the original activity." ); - let new_stored_activity = storage.read_activity(*activity_item.guid())?; + let new_stored_activity = + storage + .read_activity(*activity_item.guid())? + .ok_or_else(|| { + format!( + "Activity with ID {} was not found.", + activity_item.guid().to_string() + ) + })?; assert_eq!( old_activity.guid(), @@ -970,7 +1027,7 @@ mod tests { assert_eq!( new_stored_activity.activity().description(), - new_description, + &new_description, "Description was not updated." ); @@ -1007,10 +1064,10 @@ mod tests { // Create activity let begin = Local::now().fixed_offset(); let kind = ActivityKind::Activity; - let description = "Test activity"; + let description = PaceDescription::new("Test activity"); let tags = vec!["test".to_string(), "activity".to_string()] .into_iter() - .collect::>(); + .collect::(); let mut activity = Activity::builder() .begin(begin) @@ -1034,7 +1091,14 @@ mod tests { ); // Read activity - let stored_activity = storage.read_activity(*activity_item.guid())?; + let stored_activity = storage + .read_activity(*activity_item.guid())? + .ok_or_else(|| { + format!( + "Activity with ID {} was not found.", + activity_item.guid().to_string() + ) + })?; // Make sure the activity is active now, as begin_activity should make it active automatically activity.make_active(); @@ -1052,11 +1116,11 @@ mod tests { ); // Update activity - let new_description = "Updated description"; + let new_description = PaceDescription::new("Updated description"); let tags = vec!["bla".to_string(), "test".to_string()] .into_iter() - .collect::>(); + .collect::(); let new_begin = PaceDateTime::from( begin + chrono::TimeDelta::try_seconds(30).ok_or("Invalid time delta")?, @@ -1066,7 +1130,7 @@ mod tests { .begin(new_begin) .kind(ActivityKind::PomodoroWork) .status(ActivityStatusKind::Created) - .description(new_description) + .description(new_description.clone()) .tags(tags.clone()) .build(); @@ -1076,11 +1140,19 @@ mod tests { UpdateOptions::default(), )?; - let new_stored_activity = storage.read_activity(*activity_item.guid())?; + let new_stored_activity = + storage + .read_activity(*activity_item.guid())? + .ok_or_else(|| { + format!( + "Activity with ID {} was not found.", + activity_item.guid().to_string() + ) + })?; assert_eq!( new_stored_activity.activity().description(), - new_description, + &new_description, "Description was not updated." ); @@ -1141,10 +1213,10 @@ mod tests { let begin_time = now - chrono::TimeDelta::try_seconds(30).ok_or("Invalid time delta")?; let end_time = now + chrono::TimeDelta::try_seconds(30).ok_or("Invalid time delta")?; let kind = ActivityKind::Activity; - let description = "Test activity"; + let description = PaceDescription::new("Test activity"); let tags = vec!["test".to_string(), "activity".to_string()] .into_iter() - .collect::>(); + .collect::(); let activity = Activity::builder() .begin(begin_time) @@ -1166,7 +1238,14 @@ mod tests { assert!(ended_activity.activity().activity_end_options().is_some()); - let ended_activity = storage.read_activity(*activity_item.guid())?; + let ended_activity = storage + .read_activity(*activity_item.guid())? + .ok_or_else(|| { + format!( + "Activity with ID {} was not found.", + activity_item.guid().to_string() + ) + })?; assert!( ended_activity.activity().is_completed(), @@ -1203,10 +1282,10 @@ mod tests { let now = Local::now().fixed_offset(); let begin_time = now - chrono::TimeDelta::try_seconds(30).ok_or("Invalid time delta")?; let kind = ActivityKind::Activity; - let description = "Test activity"; + let description = PaceDescription::new("Test activity"); let tags = vec!["test".to_string(), "activity".to_string()] .into_iter() - .collect::>(); + .collect::(); let activity = Activity::builder() .begin(begin_time) @@ -1262,10 +1341,10 @@ mod tests { let now = Local::now().fixed_offset(); let begin_time = now - chrono::TimeDelta::try_seconds(30).ok_or("Invalid time delta")?; let kind = ActivityKind::Activity; - let description = "Test activity"; + let description = PaceDescription::new("Test activity"); let tags = vec!["test".to_string(), "activity".to_string()] .into_iter() - .collect::>(); + .collect::(); let activity = Activity::builder() .begin(begin_time) @@ -1279,7 +1358,7 @@ mod tests { let begin_time = now - chrono::TimeDelta::try_seconds(60).ok_or("Invalid time delta.")?; let kind = ActivityKind::Activity; - let description = "Test activity 2"; + let description = PaceDescription::new("Test activity 2"); let activity2 = Activity::builder() .begin(begin_time) @@ -1291,7 +1370,14 @@ mod tests { // Begin the second activity, the first one should be ended automatically now let activity_item2 = storage.begin_activity(activity2)?; - let ended_activity = storage.read_activity(*activity_item.guid())?; + let ended_activity = storage + .read_activity(*activity_item.guid())? + .ok_or_else(|| { + format!( + "Activity with ID {} was not found.", + activity_item.guid().to_string() + ) + })?; assert!( ended_activity.activity().is_completed(), @@ -1309,7 +1395,14 @@ mod tests { "End time was not set." ); - let ended_activity2 = storage.read_activity(*activity_item2.guid())?; + let ended_activity2 = storage + .read_activity(*activity_item2.guid())? + .ok_or_else(|| { + format!( + "Activity with ID {} was not found.", + activity_item2.guid().to_string() + ) + })?; assert!( ended_activity2.activity().is_in_progress(), @@ -1330,10 +1423,10 @@ mod tests { let now = Local::now().fixed_offset(); let begin_time = now - chrono::TimeDelta::try_seconds(30).ok_or("Invalid time delta.")?; let kind = ActivityKind::Activity; - let description = "Test activity"; + let description = PaceDescription::new("Test activity"); let tags = vec!["test".to_string(), "activity".to_string()] .into_iter() - .collect::>(); + .collect::(); let activity = Activity::builder() .begin(begin_time) @@ -1374,7 +1467,14 @@ mod tests { assert_eq!(intermission_guids.len(), 1, "Intermission was not created."); - let intermission_item = storage.read_activity(intermission_guids[0])?; + let intermission_item = storage + .read_activity(intermission_guids[0])? + .ok_or_else(|| { + format!( + "Intermission with ID {} was not found.", + intermission_guids[0].to_string() + ) + })?; assert_eq!( *intermission_item.activity().kind(), @@ -1404,10 +1504,10 @@ mod tests { let now = Local::now().fixed_offset(); let begin_time = now - chrono::TimeDelta::try_seconds(30).ok_or("Invalid time delta.")?; let kind = ActivityKind::Activity; - let description = "Test activity"; + let description = PaceDescription::new("Test activity"); let tags = vec!["test".to_string(), "activity".to_string()] .into_iter() - .collect::>(); + .collect::(); let activity = Activity::builder() .begin(begin_time) @@ -1426,7 +1526,14 @@ mod tests { .hold_most_recent_active_activity(hold_opts)? .ok_or("Activity was not held.")?; - let held_activity = storage.read_activity(*active_activity_item.guid())?; + let held_activity = storage + .read_activity(*active_activity_item.guid())? + .ok_or_else(|| { + format!( + "Activity with ID {} was not found.", + active_activity_item.guid().to_string() + ) + })?; assert_eq!( *held_activity.activity().status(), @@ -1461,7 +1568,14 @@ mod tests { "Intermission was created again." ); - let intermission_item = storage.read_activity(intermission_guids[0])?; + let intermission_item = storage + .read_activity(intermission_guids[0])? + .ok_or_else(|| { + format!( + "Intermission with ID {} was not found.", + intermission_guids[0].to_string() + ) + })?; assert_eq!( *intermission_item.activity().kind(), @@ -1484,7 +1598,7 @@ mod tests { let begin_time = now - chrono::TimeDelta::try_seconds(30).ok_or("Invalid time delta.")?; let end_time = now + chrono::TimeDelta::try_seconds(60).ok_or("Invalid time delta.")?; let kind = ActivityKind::Activity; - let description = "Test activity"; + let description = PaceDescription::new("Test activity"); let activity = Activity::builder() .begin(begin_time) @@ -1518,7 +1632,15 @@ mod tests { "Not all intermissions were ended." ); - let ended_intermission = storage.read_activity(intermission_guids[0])?; + let ended_intermission = + storage + .read_activity(intermission_guids[0])? + .ok_or_else(|| { + format!( + "Intermission with ID {} was not found.", + intermission_guids[0].to_string() + ) + })?; assert!( ended_intermission.activity().is_completed(), @@ -1545,18 +1667,20 @@ mod tests { let now = Local::now().fixed_offset(); let begin_time = now - chrono::TimeDelta::try_seconds(30).ok_or("Invalid time delta")?; let kind = ActivityKind::Activity; - let description = "Test activity"; + let description = PaceDescription::new("Test activity"); let activity = Activity::builder() .begin(begin_time) .kind(kind) .description(description) - .category("Project::Test".to_string()) + .category(PaceCategory::new("Project::Test")) .build(); let activity_item = storage.begin_activity(activity)?; - let keyword_opts = KeywordOptions::builder().category("Test").build(); + let keyword_opts = KeywordOptions::builder() + .category(PaceCategory::new("Test")) + .build(); let grouped_activities = storage.group_activities_by_keywords(keyword_opts)?.ok_or( "Grouped activities by keywords returned None, but should have returned Some.", @@ -1591,12 +1715,12 @@ mod tests { let now = Local::now().fixed_offset(); let begin_time = now - chrono::TimeDelta::try_seconds(30).ok_or("Invalid time delta")?; let kind = ActivityKind::Activity; - let description = "Test activity"; + let description = PaceDescription::new("Test activity"); let activity = Activity::builder() .begin(begin_time) .kind(kind) - .description(description) + .description(description.clone()) .build(); let activity_item = storage.begin_activity(activity)?; @@ -1632,8 +1756,8 @@ mod tests { ); assert_eq!( - *grouped_activity.activity().description(), - description, + grouped_activity.activity().description(), + &description, "Grouped activity description is not the same as the original activity description." ); @@ -1646,12 +1770,12 @@ mod tests { let now = Local::now().fixed_offset(); let begin_time = now - chrono::TimeDelta::try_seconds(30).ok_or("Invalid time delta")?; let kind = ActivityKind::Activity; - let description = "Test activity"; + let description = PaceDescription::new("Test activity"); let activity = Activity::builder() .begin(begin_time) .kind(kind) - .description(description) + .description(description.clone()) .build(); let activity_item = storage.begin_activity(activity)?; @@ -1693,8 +1817,8 @@ mod tests { ); assert_eq!( - *grouped_activity.activity().description(), - description, + grouped_activity.activity().description(), + &description, "Grouped activity description is not the same as the original activity description." ); @@ -1707,12 +1831,12 @@ mod tests { let now = Local::now().fixed_offset(); let begin_time = now - chrono::TimeDelta::try_seconds(30).ok_or("Invalid time delta.")?; let kind = ActivityKind::Activity; - let description = "Test activity"; + let description = PaceDescription::new("Test activity"); let activity = Activity::builder() .begin(begin_time) .kind(kind) - .description(description) + .description(description.clone()) .build(); let activity_item = storage.begin_activity(activity)?; @@ -1754,8 +1878,8 @@ mod tests { ); assert_eq!( - *grouped_activity.activity().description(), - description, + grouped_activity.activity().description(), + &description, "Grouped activity description is not the same as the original activity description." ); diff --git a/crates/storage/src/storage/sqlite.rs b/crates/storage/src/storage/sqlite.rs new file mode 100644 index 00000000..cfa6e194 --- /dev/null +++ b/crates/storage/src/storage/sqlite.rs @@ -0,0 +1,322 @@ +use std::{collections::BTreeMap, fs::File, path::PathBuf}; + +// use itertools::Itertools; +use sea_orm::{ConnectionTrait, Database, DatabaseConnection, EntityTrait, ModelTrait}; +use tracing::debug; + +use pace_core::{ + config::DatabaseEngineKind, + options::{ + DeleteOptions, EndOptions, HoldOptions, KeywordOptions, ResumeOptions, UpdateOptions, + }, + prelude::{ + Activity, ActivityFilterKind, ActivityGuid, ActivityItem, ActivityKind, ActivityQuerying, + ActivityReadOps, ActivityStateManagement, ActivityStatusKind, ActivityStorage, + ActivityWriteOps, FilteredActivities, SyncStorage, + }, +}; +use pace_error::{DatabaseStorageErrorKind, PaceOptResult, PaceResult}; +use pace_time::{date::PaceDate, duration::PaceDurationRange, time_range::TimeRangeOptions}; + +use crate::{ + migration::{Migrator, MigratorTrait}, + repository::{Repository, SeaOrmRepository}, + runtime, +}; + +#[derive(Debug)] +pub struct SQLiteActivityStorage { + connection: DatabaseConnection, +} + +impl SQLiteActivityStorage { + /// Create a new database activity storage instance. + /// + /// # Arguments + /// + /// * `kind` - The database engine kind. + /// * `url` - The database connection URL. + /// + /// # Errors + /// + /// This function returns an error if the database connection engine is not supported. + /// + /// # Panics + /// + /// This function panics if the database connection fails as its a critical operation. + /// + /// # Returns + /// + /// A new database activity storage instance. + #[allow(clippy::expect_used)] + pub fn new(kind: DatabaseEngineKind, url: &str) -> PaceResult { + let connection_string = match kind { + DatabaseEngineKind::Sqlite => { + debug!("Connecting to SQLite database: {url}"); + + let path = PathBuf::from(&url); + + if !path.exists() { + _ = File::create(&path)?; + } + + format!("sqlite://{url}") + } + engine => { + return Err( + DatabaseStorageErrorKind::UnsupportedDatabaseEngine(engine.to_string()).into(), + ) + } + }; + + runtime().block_on(async { + let connection = Database::connect(connection_string) + .await + .expect("Failed to connect to the database"); + + Ok(Self { connection }) + }) + } +} + +impl ActivityStorage for SQLiteActivityStorage { + fn setup(&self) -> PaceResult<()> { + runtime().block_on(async { + Migrator::up(&self.connection, None) + .await + .map_err(|source| DatabaseStorageErrorKind::MigrationFailed { source })?; + + Ok(()) + }) + } + + fn teardown(&self) -> PaceResult<()> { + // TODO: Do we need a teardown for sqlite? + unimplemented!("teardown not yet implemented for sqlite storage") + } + + fn identify(&self) -> String { + "sqlite".to_string() + } +} + +impl SyncStorage for SQLiteActivityStorage { + fn sync(&self) -> PaceResult<()> { + // We sync activities to the database in each operation + // so we don't need to do anything here + + Ok(()) + } +} + +impl ActivityReadOps for SQLiteActivityStorage { + #[tracing::instrument] + fn read_activity(&self, activity_id: ActivityGuid) -> PaceOptResult { + runtime().block_on(async { + let repo = SeaOrmRepository::new(&self.connection); + let activity_model = repo + .activity() + .read(&activity_id.to_string()) + .await? + .ok_or_else(|| DatabaseStorageErrorKind::ActivityNotFound { + guid: activity_id.to_string(), + })?; + + unimplemented!("implement read_activity for sqlite"); + + let activity = Activity::builder() + // TODO: Implement conversion from model to activity + ; + + let activity_item = ActivityItem::builder() + .guid(activity_id) + .activity(todo!("activity from model")) + .build(); + + // Ok(ActivityItem::default()) + }) + } + + #[tracing::instrument] + fn list_activities(&self, filter: ActivityFilterKind) -> PaceOptResult { + // let mut stmt = self.connection.prepare(filter.to_sql_statement())?; + + // let activity_item_iter = stmt.query_map([], |row| Ok(ActivityGuid::from_row(&row)))?; + + // let activities = activity_item_iter + // .filter_map_ok(|item| item.ok()) + // .collect::, _>>()?; + + // debug!("Listed activities: {activities:?}"); + + // if activities.is_empty() { + // return Ok(None); + // } + + // let filtered_activities = match filter { + // ActivityFilterKind::Everything => FilteredActivities::Everything(activities), + // ActivityFilterKind::OnlyActivities => FilteredActivities::OnlyActivities(activities), + // ActivityFilterKind::Active => FilteredActivities::Active(activities), + // ActivityFilterKind::ActiveIntermission => { + // FilteredActivities::ActiveIntermission(activities) + // } + // ActivityFilterKind::Archived => FilteredActivities::Archived(activities), + // ActivityFilterKind::Ended => FilteredActivities::Ended(activities), + // ActivityFilterKind::Held => FilteredActivities::Held(activities), + // ActivityFilterKind::Intermission => FilteredActivities::Intermission(activities), + // ActivityFilterKind::TimeRange(_) => FilteredActivities::TimeRange(activities), + // }; + + // Ok(Some(filtered_activities)) + + todo!("implement list_activities for sqlite") + } +} + +impl ActivityWriteOps for SQLiteActivityStorage { + fn create_activity(&self, _activity: Activity) -> PaceResult { + // let tx = self.connection.transaction()?; + + // let mut stmt = tx.prepare(activity.to_sql_prepare_statement())?; + + // let (guid, params) = activity.to_sql_execute_statement()?; + + // if stmt.execute(params.as_slice())? > 0 { + // tx.commit()?; + // return Ok(ActivityItem::from((guid, activity))); + // } + + // return Err(DatabaseStorageErrorKind::ActivityCreationFailed(activity).into()); + todo!("implement create_activity for sqlite") + } + + fn update_activity( + &self, + _activity_id: ActivityGuid, + _updated_activity: Activity, + _update_opts: UpdateOptions, + ) -> PaceResult { + todo!() + } + + fn delete_activity( + &self, + _activity_id: ActivityGuid, + _delete_opts: DeleteOptions, + ) -> PaceResult { + // let activity = self.read_activity(activity_id)?; + + // let tx = self.connection.transaction()?; + // let mut stmt = tx.prepare("DELETE FROM activities WHERE id = ?1 LIMIT = 1")?; + + // if stmt.execute(&[&activity_id])? == 1 { + // tx.commit()?; + // return Ok(activity); + // } + + // Err(DatabaseStorageErrorKind::ActivityDeletionFailed(activity_id).into()) + todo!("implement delete_activity for sqlite") + } +} +impl ActivityStateManagement for SQLiteActivityStorage { + fn hold_activity( + &self, + _activity_id: ActivityGuid, + _hold_opts: HoldOptions, + ) -> PaceResult { + todo!() + } + + fn resume_activity( + &self, + _activity_id: ActivityGuid, + _resume_opts: ResumeOptions, + ) -> PaceResult { + todo!() + } + + fn resume_most_recent_activity( + &self, + _resume_opts: ResumeOptions, + ) -> PaceOptResult { + todo!() + } + + fn end_activity( + &self, + _activity_id: ActivityGuid, + _end_opts: EndOptions, + ) -> PaceResult { + todo!() + } + + fn end_all_activities(&self, _end_opts: EndOptions) -> PaceOptResult> { + todo!() + } + + fn end_all_active_intermissions( + &self, + _end_opts: EndOptions, + ) -> PaceOptResult> { + todo!() + } + + fn end_last_unfinished_activity(&self, _end_opts: EndOptions) -> PaceOptResult { + todo!() + } + + fn hold_most_recent_active_activity( + &self, + _hold_opts: HoldOptions, + ) -> PaceOptResult { + todo!() + } +} + +impl ActivityQuerying for SQLiteActivityStorage { + fn group_activities_by_duration_range( + &self, + ) -> PaceOptResult>> { + todo!() + } + + fn group_activities_by_start_date( + &self, + ) -> PaceOptResult>> { + todo!() + } + + fn list_activities_with_intermissions( + &self, + ) -> PaceOptResult>> { + todo!() + } + + fn group_activities_by_keywords( + &self, + _keyword_opts: KeywordOptions, + ) -> PaceOptResult>> { + todo!() + } + + fn group_activities_by_kind(&self) -> PaceOptResult>> { + todo!() + } + + fn list_activities_by_time_range( + &self, + _time_range_opts: TimeRangeOptions, + ) -> PaceOptResult> { + todo!() + } + + fn group_activities_by_status( + &self, + ) -> PaceOptResult>> { + todo!() + } + + fn list_activities_by_id(&self) -> PaceOptResult> { + todo!() + } +} diff --git a/crates/time/Cargo.toml b/crates/time/Cargo.toml index d8fa4146..977bf548 100644 --- a/crates/time/Cargo.toml +++ b/crates/time/Cargo.toml @@ -9,7 +9,15 @@ keywords = { workspace = true } license = { workspace = true } repository = { workspace = true } rust-version = { workspace = true } -description = "pace-time - a library for handling date times, ranges, and durations for pace" +description = "pace-time - a library handling date times, ranges, and durations for pace" + +include = [ + "LICENSE", + "README.md", + "CHANGELOG.md", + "src/**/*", + "Cargo.toml", +] [features] default = ["cli"] @@ -24,6 +32,7 @@ derive_more = { workspace = true, features = ["add", "add_assign"] } displaydoc = { workspace = true } getset = { workspace = true } humantime = { workspace = true } +pace_error = { workspace = true } serde = { workspace = true } serde_derive = { workspace = true } thiserror = { workspace = true } diff --git a/crates/time/README.md b/crates/time/README.md index 99bd5652..a02246c6 100644 --- a/crates/time/README.md +++ b/crates/time/README.md @@ -42,6 +42,12 @@ This crate exposes a few features for controlling dependency usage: - **cli** - Enables support for CLI features by enabling `merge` and `clap` features. *This feature is enabled by default*. +- **db** - Enables support for database features by enabling `rusqlite` + features. *This feature is enabled by default*. + +- **rusqlite** - Enables a dependency on the `rusqlite` crate and enables + database support. *This feature is enabled by default*. + ## Examples TODO! diff --git a/crates/time/src/date.rs b/crates/time/src/date.rs index 340767a5..5434db9a 100644 --- a/crates/time/src/date.rs +++ b/crates/time/src/date.rs @@ -4,7 +4,8 @@ use chrono::{Local, NaiveDate}; use serde_derive::{Deserialize, Serialize}; -use crate::{date_time::PaceDateTime, error::PaceTimeErrorKind}; +use crate::date_time::PaceDateTime; +use pace_error::TimeErrorKind; /// {0} #[derive( @@ -53,26 +54,22 @@ impl PaceDate { } impl FromStr for PaceDate { - type Err = PaceTimeErrorKind; + type Err = TimeErrorKind; fn from_str(s: &str) -> Result { let date = NaiveDate::parse_from_str(s, "%Y-%m-%d") - .map_err(|_| PaceTimeErrorKind::ParsingDateFailed(format!("Invalid date: {s}")))?; + .map_err(|_| TimeErrorKind::ParsingDateFailed(format!("Invalid date: {s}")))?; Ok(Self(date)) } } impl TryFrom<(i32, u32, u32)> for PaceDate { - type Error = PaceTimeErrorKind; + type Error = TimeErrorKind; fn try_from((year, month, day): (i32, u32, u32)) -> Result { NaiveDate::from_ymd_opt(year, month, day).map_or_else( - || { - Err(PaceTimeErrorKind::InvalidDate(format!( - "{year}/{month}/{day}" - ))) - }, + || Err(TimeErrorKind::InvalidDate(format!("{year}/{month}/{day}"))), |date| Ok(Self(date)), ) } diff --git a/crates/time/src/date_time.rs b/crates/time/src/date_time.rs index 980ab31c..18a7e4cb 100644 --- a/crates/time/src/date_time.rs +++ b/crates/time/src/date_time.rs @@ -12,21 +12,18 @@ use serde_derive::{Deserialize, Serialize}; use tracing::debug; use crate::{ - date::PaceDate, - duration::PaceDuration, - error::{PaceTimeErrorKind, PaceTimeResult}, - time::PaceTime, - time_zone::PaceTimeZoneKind, - Validate, + date::PaceDate, duration::PaceDuration, time::PaceTime, time_zone::PaceTimeZoneKind, Validate, }; +use pace_error::{BoxedPaceError, PaceResult, TimeErrorKind}; + impl TryFrom for PaceDateTime { - type Error = PaceTimeErrorKind; + type Error = TimeErrorKind; fn try_from(_date: PaceDate) -> Result { // if the date is invalid because of the time, use the default time // Ok(Self::new(date.inner().and_hms_opt(0, 0, 0).ok_or_else( - // || PaceTimeErrorKind::InvalidDate(date.to_string()), + // || TimeErrorKind::InvalidDate(date.to_string()), // )?)) unimplemented!("Implement conversion from PaceDate to PaceDateTime") } @@ -37,7 +34,7 @@ impl TryFrom for PaceDateTime { pub struct PaceDateTime(DateTime); impl FromStr for PaceDateTime { - type Err = PaceTimeErrorKind; + type Err = TimeErrorKind; /// Parse a `PaceDateTime` from a string /// @@ -54,13 +51,13 @@ impl FromStr for PaceDateTime { /// Returns the parsed `PaceDateTime` fn from_str(s: &str) -> Result { Ok(Self(DateTime::parse_from_rfc3339(s).map_err(|e| { - PaceTimeErrorKind::ParseError(format!("{e:?}")) + TimeErrorKind::ParseError(format!("{e:?}")) })?)) } } impl TryFrom<(Option<&NaiveTime>, PaceTimeZoneKind, PaceTimeZoneKind)> for PaceDateTime { - type Error = PaceTimeErrorKind; + type Error = BoxedPaceError; /// Try to convert from a tuple of optional naive time, time zone and time zone offset /// @@ -103,7 +100,7 @@ impl TryFrom<(Option<&NaiveTime>, PaceTimeZoneKind, PaceTimeZoneKind)> for PaceD } _ => { debug!("Conversion failed with time zones: {tz:?} and {tz_config:?}"); - Err(PaceTimeErrorKind::ConversionToPaceDateTimeFailed) + Err(TimeErrorKind::ConversionToPaceDateTimeFailed.into()) } } } @@ -125,11 +122,7 @@ impl PaceDateTime { /// # Returns /// /// Returns the date time with a time zone implementation - pub fn new( - date: NaiveDate, - time: NaiveTime, - time_zone: PaceTimeZoneKind, - ) -> PaceTimeResult { + pub fn new(date: NaiveDate, time: NaiveTime, time_zone: PaceTimeZoneKind) -> PaceResult { pace_date_time_from_date_and_time_and_tz(date, time, time_zone) } @@ -146,22 +139,18 @@ impl PaceDateTime { /// # Returns /// /// Returns the new [`PaceDateTime`] with the added [`TimeDelta`] - pub fn add_duration(self, rhs: PaceDuration) -> PaceTimeResult { + pub fn add_duration(self, rhs: PaceDuration) -> PaceResult { Ok(Self( self.0 .checked_add_signed( Duration::new( i64::try_from(rhs.inner()) - .map_err(PaceTimeErrorKind::FailedToConvertDurationToI64)?, + .map_err(TimeErrorKind::FailedToConvertDurationToI64)?, 0, ) - .ok_or_else(|| { - PaceTimeErrorKind::ConversionToDurationFailed(format!("{rhs:?}")) - })?, + .ok_or_else(|| TimeErrorKind::ConversionToDurationFailed(format!("{rhs:?}")))?, ) - .ok_or_else(|| { - PaceTimeErrorKind::AddingTimeDeltaFailed(format!("{self} + {rhs:?}")) - })?, + .ok_or_else(|| TimeErrorKind::AddingTimeDeltaFailed(format!("{self} + {rhs:?}")))?, )) } @@ -195,16 +184,16 @@ impl PaceDateTime { /// /// Returns an error if the time can't be set to the start of the day /// and the time is ambiguous - pub fn start_of_day(mut self) -> PaceTimeResult { + pub fn start_of_day(mut self) -> PaceResult { let time_zone = self.0.offset(); let time = self.0.date_naive().and_time( - NaiveTime::from_hms_opt(0, 0, 0).ok_or(PaceTimeErrorKind::SettingStartOfDayFailed)?, + NaiveTime::from_hms_opt(0, 0, 0).ok_or(TimeErrorKind::SettingStartOfDayFailed)?, ); if let LocalResult::Single(datetime) = time_zone.from_local_datetime(&time) { self.0 = datetime; } else { - return Err(PaceTimeErrorKind::AmbiguousConversionResult); + return Err(TimeErrorKind::AmbiguousConversionResult.into()); } Ok(self) @@ -216,17 +205,16 @@ impl PaceDateTime { /// /// Returns an error if the time can't be set to the end of the day /// and the time is ambiguous - pub fn end_of_day(mut self) -> PaceTimeResult { + pub fn end_of_day(mut self) -> PaceResult { let time_zone = self.0.offset(); let time = self.0.date_naive().and_time( - NaiveTime::from_hms_opt(23, 59, 59) - .ok_or(PaceTimeErrorKind::SettingStartOfDayFailed)?, + NaiveTime::from_hms_opt(23, 59, 59).ok_or(TimeErrorKind::SettingStartOfDayFailed)?, ); if let LocalResult::Single(datetime) = time_zone.from_local_datetime(&time) { self.0 = datetime; } else { - return Err(PaceTimeErrorKind::AmbiguousConversionResult); + return Err(TimeErrorKind::AmbiguousConversionResult.into()); } Ok(self) @@ -290,7 +278,7 @@ impl PaceDateTime { impl Validate for PaceDateTime { type Output = Self; - type Error = PaceTimeErrorKind; + type Error = BoxedPaceError; /// Check if time is in the future /// @@ -301,9 +289,9 @@ impl Validate for PaceDateTime { /// # Returns /// /// Returns the time if it's not in the future - fn validate(self) -> PaceTimeResult { + fn validate(self) -> PaceResult { if self > Self::now() { - Err(PaceTimeErrorKind::StartTimeInFuture(self)) + Err(TimeErrorKind::StartTimeInFuture(self.to_string()).into()) } else { Ok(self) } @@ -336,16 +324,16 @@ impl From>> for PaceDateTime { } impl TryFrom for PaceDateTime { - type Error = PaceTimeErrorKind; + type Error = BoxedPaceError; - fn try_from(time: NaiveDateTime) -> PaceTimeResult { + fn try_from(time: NaiveDateTime) -> PaceResult { // get local time zone let local = Local::now(); let local = local.offset(); // combine NaiveDateTime with local time zone let LocalResult::Single(datetime) = local.from_local_datetime(&time) else { - Err(PaceTimeErrorKind::AmbiguousConversionResult)? + Err(TimeErrorKind::AmbiguousConversionResult)? }; Ok(Self::from(datetime.round_subsecs(0).fixed_offset())) @@ -373,11 +361,11 @@ impl TryFrom for PaceDateTime { pub(crate) fn pace_date_time_from_date_and_tz_with_zero_hms( date: NaiveDate, time_zone: PaceTimeZoneKind, -) -> PaceTimeResult { +) -> PaceResult { pace_date_time_from_date_and_time_and_tz( date, NaiveTime::from_hms_opt(0, 0, 0) - .ok_or_else(|| PaceTimeErrorKind::InvalidDate(date.to_string()))?, + .ok_or_else(|| TimeErrorKind::InvalidDate(date.to_string()))?, time_zone, )? .validate() @@ -402,18 +390,18 @@ pub fn pace_date_time_from_date_and_time_and_tz( date: NaiveDate, time: NaiveTime, tz: PaceTimeZoneKind, -) -> PaceTimeResult { +) -> PaceResult { let date_time = match tz { PaceTimeZoneKind::TimeZone(ref tz) => { let LocalResult::Single(datetime) = tz.from_local_datetime(&date.and_time(time)) else { - return Err(PaceTimeErrorKind::AmbiguousConversionResult); + return Err(TimeErrorKind::AmbiguousConversionResult.into()); }; PaceDateTime::from(datetime.round_subsecs(0).fixed_offset()) } PaceTimeZoneKind::TimeZoneOffset(ref tz) => { let LocalResult::Single(datetime) = tz.from_local_datetime(&date.and_time(time)) else { - return Err(PaceTimeErrorKind::AmbiguousConversionResult); + return Err(TimeErrorKind::AmbiguousConversionResult.into()); }; PaceDateTime::from(datetime.round_subsecs(0).fixed_offset()) @@ -421,7 +409,7 @@ pub fn pace_date_time_from_date_and_time_and_tz( PaceTimeZoneKind::NotSet => { let LocalResult::Single(datetime) = Local.from_local_datetime(&date.and_time(time)) else { - return Err(PaceTimeErrorKind::AmbiguousConversionResult); + return Err(TimeErrorKind::AmbiguousConversionResult.into()); }; PaceDateTime::from(datetime.round_subsecs(0).fixed_offset()) @@ -434,7 +422,7 @@ pub fn pace_date_time_from_date_and_time_and_tz( } impl TryFrom<(NaiveDate, NaiveTime, PaceTimeZoneKind)> for PaceDateTime { - type Error = PaceTimeErrorKind; + type Error = BoxedPaceError; fn try_from( (date, time, tz): (NaiveDate, NaiveTime, PaceTimeZoneKind), @@ -444,7 +432,7 @@ impl TryFrom<(NaiveDate, NaiveTime, PaceTimeZoneKind)> for PaceDateTime { } impl TryFrom<(NaiveDate, PaceTimeZoneKind)> for PaceDateTime { - type Error = PaceTimeErrorKind; + type Error = BoxedPaceError; fn try_from((date, tz): (NaiveDate, PaceTimeZoneKind)) -> Result { pace_date_time_from_date_and_time_and_tz(date, Local::now().time(), tz)?.validate() @@ -452,7 +440,7 @@ impl TryFrom<(NaiveDate, PaceTimeZoneKind)> for PaceDateTime { } impl TryFrom<(NaiveTime, PaceTimeZoneKind)> for PaceDateTime { - type Error = PaceTimeErrorKind; + type Error = BoxedPaceError; fn try_from((time, tz): (NaiveTime, PaceTimeZoneKind)) -> Result { pace_date_time_from_date_and_time_and_tz(Local::now().date_naive(), time, tz)?.validate() @@ -460,7 +448,7 @@ impl TryFrom<(NaiveTime, PaceTimeZoneKind)> for PaceDateTime { } impl TryFrom<(NaiveDate, NaiveTime)> for PaceDateTime { - type Error = PaceTimeErrorKind; + type Error = BoxedPaceError; fn try_from((date, time): (NaiveDate, NaiveTime)) -> Result { pace_date_time_from_date_and_time_and_tz( @@ -473,7 +461,7 @@ impl TryFrom<(NaiveDate, NaiveTime)> for PaceDateTime { } impl TryFrom<(NaiveDateTime, PaceTimeZoneKind)> for PaceDateTime { - type Error = PaceTimeErrorKind; + type Error = BoxedPaceError; fn try_from((date_time, tz): (NaiveDateTime, PaceTimeZoneKind)) -> Result { pace_date_time_from_date_and_time_and_tz(date_time.date(), date_time.time(), tz)?.validate() diff --git a/crates/time/src/duration.rs b/crates/time/src/duration.rs index e05d3c38..92a3ca13 100644 --- a/crates/time/src/duration.rs +++ b/crates/time/src/duration.rs @@ -11,10 +11,9 @@ use humantime::format_duration; use serde_derive::{Deserialize, Serialize}; use tracing::debug; -use crate::{ - date_time::PaceDateTime, - error::{PaceTimeErrorKind, PaceTimeResult}, -}; +use pace_error::{PaceResult, TimeErrorKind}; + +use crate::date_time::PaceDateTime; /// Converts timespec to nice readable relative time string /// @@ -159,11 +158,11 @@ impl PaceDuration { } impl FromStr for PaceDuration { - type Err = PaceTimeErrorKind; + type Err = TimeErrorKind; fn from_str(s: &str) -> Result { s.parse::().map_or_else( - |_| Err(PaceTimeErrorKind::ParsingDurationFailed(s.to_string())), + |_| Err(TimeErrorKind::ParsingDurationFailed(s.to_string())), |duration| Ok(Self(duration)), ) } @@ -176,11 +175,11 @@ impl From for PaceDuration { } impl TryFrom for PaceDuration { - type Error = PaceTimeErrorKind; + type Error = TimeErrorKind; fn try_from(duration: chrono::Duration) -> Result { Ok(Self(duration.num_seconds().try_into().map_err(|_| { - PaceTimeErrorKind::ParsingDurationFailed(duration.to_string()) + TimeErrorKind::ParsingDurationFailed(duration.to_string()) })?)) } } @@ -228,10 +227,7 @@ impl std::ops::SubAssign for PaceDuration { /// /// Returns the duration of the activity #[tracing::instrument] -pub fn calculate_duration( - begin: &PaceDateTime, - end: &PaceDateTime, -) -> PaceTimeResult { +pub fn calculate_duration(begin: &PaceDateTime, end: &PaceDateTime) -> PaceResult { let duration = end.inner().signed_duration_since(begin.inner()).to_std()?; debug!("Duration: {duration:?}"); diff --git a/crates/time/src/error.rs b/crates/time/src/error.rs deleted file mode 100644 index ceb4d391..00000000 --- a/crates/time/src/error.rs +++ /dev/null @@ -1,77 +0,0 @@ -use std::num::TryFromIntError; - -use chrono::OutOfRangeError; -use displaydoc::Display; -use thiserror::Error; - -use crate::date_time::PaceDateTime; - -pub type PaceTimeResult = Result; - -/// [`PaceTimeErrorKind`] describes the errors that can happen while dealing with time. -#[non_exhaustive] -#[derive(Error, Debug, Display)] -pub enum PaceTimeErrorKind { - /// {0} - #[error(transparent)] - OutOfRange(#[from] OutOfRangeError), - - /// Failed to parse time '{0}' from user input, please use the format HH:MM - ParsingTimeFromUserInputFailed(String), - - /// The start time cannot be in the future, please use a time in the past: '{0}' - StartTimeInFuture(PaceDateTime), - - /// Failed to parse duration '{0}', please use only numbers >= 0 - ParsingDurationFailed(String), - - /// Failed to parse date '{0}', please use the format YYYY-MM-DD - InvalidDate(String), - /// Date is not present! - DateShouldBePresent, - - /// Failed to parse date '{0}' - ParsingDateFailed(String), - - /// Invalid time range: Start '{0}' - End '{1}' - InvalidTimeRange(String, String), - - /// Invalid time zone: '{0}' - InvalidTimeZone(String), - - /// Failed to parse fixed offset '{0}' from user input, please use the format ±HHMM - ParsingFixedOffsetFailed(String), - - /// Failed to create PaceDateTime from user input, please use the format HH:MM and ±HHMM - InvalidUserInput, - - /// Time zone not found - UndefinedTimeZone, - - /// Both time zone and time zone offset are defined, please use only one - AmbiguousTimeZones, - - /// Ambiguous conversion result - AmbiguousConversionResult, - - /// Conversion to PaceDateTime failed - ConversionToPaceDateTimeFailed, - - /// Failed to parse time '{0}', please use the format HH:MM - InvalidTime(String), - - /// Failed to parse time '{0}', please use rfc3339 format - ParseError(String), - - /// Setting start of day failed - SettingStartOfDayFailed, - - /// Adding time delta failed: '{0}' - AddingTimeDeltaFailed(String), - - /// Failed to convert duration to i64: '{0}' - FailedToConvertDurationToI64(TryFromIntError), - - /// Failed to convert PaceDuration to Standard Duration: '{0}' - ConversionToDurationFailed(String), -} diff --git a/crates/time/src/lib.rs b/crates/time/src/lib.rs index 50b84ab1..f8a6f6db 100644 --- a/crates/time/src/lib.rs +++ b/crates/time/src/lib.rs @@ -1,8 +1,9 @@ pub mod date; pub mod date_time; pub mod duration; -pub mod error; pub mod flags; +#[cfg(feature = "rusqlite")] +pub mod storage; pub mod time; pub mod time_frame; pub mod time_range; diff --git a/crates/time/src/storage.rs b/crates/time/src/storage.rs new file mode 100644 index 00000000..73ed6a43 --- /dev/null +++ b/crates/time/src/storage.rs @@ -0,0 +1 @@ +// pub mod rusqlite; diff --git a/crates/time/src/storage/rusqlite.rs b/crates/time/src/storage/rusqlite.rs new file mode 100644 index 00000000..d30eee0f --- /dev/null +++ b/crates/time/src/storage/rusqlite.rs @@ -0,0 +1,39 @@ +use rusqlite::{types::FromSql, ToSql}; + +use crate::{date_time::PaceDateTime, duration::PaceDuration}; + +impl ToSql for PaceDateTime { + fn to_sql(&self) -> rusqlite::Result> { + Ok(rusqlite::types::ToSqlOutput::Owned( + rusqlite::types::Value::Text(self.to_string()), + )) + } +} + +impl FromSql for PaceDateTime { + fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult { + value + .as_str()? + .parse::() + .map_err(|err| rusqlite::types::FromSqlError::Other(Box::new(err))) + } +} + +impl ToSql for PaceDuration { + fn to_sql(&self) -> rusqlite::Result> { + Ok(rusqlite::types::ToSqlOutput::Owned( + rusqlite::types::Value::Integer( + i64::try_from(self.inner()) + .map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?, + ), + )) + } +} + +impl FromSql for PaceDuration { + fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult { + Ok(Self::new(u64::try_from(value.as_i64()?).map_err( + |err| rusqlite::types::FromSqlError::Other(Box::new(err)), + )?)) + } +} diff --git a/crates/time/src/time_frame.rs b/crates/time/src/time_frame.rs index 1ffd85db..ba62066f 100644 --- a/crates/time/src/time_frame.rs +++ b/crates/time/src/time_frame.rs @@ -5,12 +5,13 @@ use tracing::debug; use crate::{ date::PaceDate, date_time::PaceDateTime, - error::PaceTimeErrorKind, flags::{DateFlags, TimeFlags}, time_range::TimeRangeOptions, time_zone::PaceTimeZoneKind, }; +use pace_error::{BoxedPaceError, PaceResult}; + #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Serialize, Deserialize, Display)] pub enum PaceTimeFrame { /// Current Month @@ -53,7 +54,7 @@ impl PaceTimeZoneKind, )> for PaceTimeFrame { - type Error = PaceTimeErrorKind; + type Error = BoxedPaceError; fn try_from( (time_flags, date_flags, tz, tz_config): ( @@ -70,7 +71,7 @@ impl } impl TryFrom<(Option<&TimeFlags>, Option<&DateFlags>, PaceTimeZoneKind)> for PaceTimeFrame { - type Error = PaceTimeErrorKind; + type Error = BoxedPaceError; fn try_from( (time_flags, date_flags, tz): (Option<&TimeFlags>, Option<&DateFlags>, PaceTimeZoneKind), @@ -85,7 +86,7 @@ impl TryFrom<(Option<&TimeFlags>, Option<&DateFlags>, PaceTimeZoneKind)> for Pac } impl TryFrom<(&DateFlags, PaceTimeZoneKind)> for PaceTimeFrame { - type Error = PaceTimeErrorKind; + type Error = BoxedPaceError; fn try_from((date_flags, tz): (&DateFlags, PaceTimeZoneKind)) -> Result { time_frame_from_date_and_time_flags_with_time_zone_kind( @@ -98,7 +99,7 @@ impl TryFrom<(&DateFlags, PaceTimeZoneKind)> for PaceTimeFrame { } impl TryFrom<&DateFlags> for PaceTimeFrame { - type Error = PaceTimeErrorKind; + type Error = BoxedPaceError; fn try_from(date_flags: &DateFlags) -> Result { time_frame_from_date_and_time_flags_with_time_zone_kind( @@ -111,7 +112,7 @@ impl TryFrom<&DateFlags> for PaceTimeFrame { } impl TryFrom<(Option<&DateFlags>, PaceTimeZoneKind)> for PaceTimeFrame { - type Error = PaceTimeErrorKind; + type Error = BoxedPaceError; fn try_from( (date_flags, tz): (Option<&DateFlags>, PaceTimeZoneKind), @@ -126,7 +127,7 @@ impl TryFrom<(Option<&DateFlags>, PaceTimeZoneKind)> for PaceTimeFrame { } impl TryFrom<(Option<&TimeFlags>, PaceTimeZoneKind)> for PaceTimeFrame { - type Error = PaceTimeErrorKind; + type Error = BoxedPaceError; fn try_from( (time_flags, tz): (Option<&TimeFlags>, PaceTimeZoneKind), @@ -141,7 +142,7 @@ impl TryFrom<(Option<&TimeFlags>, PaceTimeZoneKind)> for PaceTimeFrame { } impl TryFrom<(&TimeFlags, PaceTimeZoneKind)> for PaceTimeFrame { - type Error = PaceTimeErrorKind; + type Error = BoxedPaceError; fn try_from((time_flags, tz): (&TimeFlags, PaceTimeZoneKind)) -> Result { time_frame_from_date_and_time_flags_with_time_zone_kind( @@ -154,7 +155,7 @@ impl TryFrom<(&TimeFlags, PaceTimeZoneKind)> for PaceTimeFrame { } impl TryFrom<&TimeFlags> for PaceTimeFrame { - type Error = PaceTimeErrorKind; + type Error = BoxedPaceError; fn try_from(time_flags: &TimeFlags) -> Result { time_frame_from_date_and_time_flags_with_time_zone_kind( @@ -167,7 +168,7 @@ impl TryFrom<&TimeFlags> for PaceTimeFrame { } impl TryFrom for PaceTimeFrame { - type Error = PaceTimeErrorKind; + type Error = BoxedPaceError; fn try_from(tz: PaceTimeZoneKind) -> Result { time_frame_from_date_and_time_flags_with_time_zone_kind( @@ -200,7 +201,7 @@ pub(crate) fn time_frame_from_date_and_time_flags_with_time_zone_kind( date_flags: Option<&DateFlags>, tz: PaceTimeZoneKind, tz_config: PaceTimeZoneKind, -) -> Result { +) -> PaceResult { let time_zone = match (tz, tz_config) { (tzk, _) | (PaceTimeZoneKind::NotSet, tzk) if !tzk.is_not_set() => tzk, _ => PaceTimeZoneKind::default(), @@ -275,7 +276,7 @@ pub(crate) fn time_frame_from_date_and_time_flags_with_time_zone_kind( _ => PaceTimeFrame::default(), }; - debug!("Converted Time frame: {:?}", time_frame); + debug!("Converted Time frame: {time_frame:?}"); Ok(time_frame) } diff --git a/crates/time/src/time_range.rs b/crates/time/src/time_range.rs index b9da889f..b5f12b59 100644 --- a/crates/time/src/time_range.rs +++ b/crates/time/src/time_range.rs @@ -7,13 +7,9 @@ use serde_derive::{Deserialize, Serialize}; use tracing::debug; use typed_builder::TypedBuilder; -use crate::{ - date::PaceDate, - date_time::PaceDateTime, - error::{PaceTimeErrorKind, PaceTimeResult}, - time_frame::PaceTimeFrame, - Validate, -}; +use pace_error::{BoxedPaceError, PaceResult, TimeErrorKind}; + +use crate::{date::PaceDate, date_time::PaceDateTime, time_frame::PaceTimeFrame, Validate}; /// `TimeRangeOptions` represents the start and end time of a time range #[derive( @@ -34,7 +30,7 @@ impl Display for TimeRangeOptions { } impl TryFrom for TimeRangeOptions { - type Error = PaceTimeErrorKind; + type Error = BoxedPaceError; fn try_from(time_frame: PaceTimeFrame) -> Result { match time_frame { @@ -54,7 +50,7 @@ impl TryFrom for TimeRangeOptions { impl Validate for TimeRangeOptions { type Output = Self; - type Error = PaceTimeErrorKind; + type Error = BoxedPaceError; /// Validate the time range /// @@ -65,12 +61,13 @@ impl Validate for TimeRangeOptions { /// # Returns /// /// Returns the time range options if they are valid - fn validate(self) -> PaceTimeResult { + fn validate(self) -> PaceResult { if self.start > self.end { - return Err(PaceTimeErrorKind::InvalidTimeRange( + return Err(TimeErrorKind::InvalidTimeRange( self.start.to_string(), self.end.to_string(), - )); + ) + .into()); } Ok(self) @@ -93,18 +90,17 @@ impl TimeRangeOptions { /// # Returns /// /// Returns the time range options for the current month - pub fn current_month() -> PaceTimeResult { + pub fn current_month() -> PaceResult { let now = Local::now().naive_local(); - let start = NaiveDate::from_ymd_opt(now.year(), now.month(), 1).ok_or_else(|| { - PaceTimeErrorKind::InvalidDate(format!("{}/{}", now.year(), now.month())) - })?; + let start = NaiveDate::from_ymd_opt(now.year(), now.month(), 1) + .ok_or_else(|| TimeErrorKind::InvalidDate(format!("{}/{}", now.year(), now.month())))?; Ok(Self::builder() .start(PaceDateTime::try_from( start .and_hms_opt(0, 0, 0) - .ok_or_else(|| PaceTimeErrorKind::InvalidDate(start.to_string()))?, + .ok_or_else(|| TimeErrorKind::InvalidDate(start.to_string()))?, )?) .build()) } @@ -118,13 +114,13 @@ impl TimeRangeOptions { /// # Returns /// /// Returns the time range options for the current week - pub fn current_week() -> PaceTimeResult { + pub fn current_week() -> PaceResult { let now = Local::now(); let start = now .date_naive() .pred_opt() - .ok_or_else(|| PaceTimeErrorKind::InvalidDate(now.to_string()))?; + .ok_or_else(|| TimeErrorKind::InvalidDate(now.to_string()))?; let week = start.week(chrono::Weekday::Mon); @@ -132,7 +128,7 @@ impl TimeRangeOptions { .start(PaceDateTime::try_from( week.first_day() .and_hms_opt(0, 0, 0) - .ok_or_else(|| PaceTimeErrorKind::InvalidDate(week.first_day().to_string()))?, + .ok_or_else(|| TimeErrorKind::InvalidDate(week.first_day().to_string()))?, )?) .build()) } @@ -146,17 +142,17 @@ impl TimeRangeOptions { /// # Returns /// /// Returns the time range options for the current year - pub fn current_year() -> PaceTimeResult { + pub fn current_year() -> PaceResult { let now = Local::now(); let start = NaiveDate::from_ymd_opt(now.year(), 1, 1) - .ok_or_else(|| PaceTimeErrorKind::InvalidDate(format!("{}/{}", now.year(), 1)))?; + .ok_or_else(|| TimeErrorKind::InvalidDate(format!("{}/{}", now.year(), 1)))?; Ok(Self::builder() .start(PaceDateTime::try_from( start .and_hms_opt(0, 0, 0) - .ok_or_else(|| PaceTimeErrorKind::InvalidDate(start.to_string()))?, + .ok_or_else(|| TimeErrorKind::InvalidDate(start.to_string()))?, )?) .build()) } @@ -174,7 +170,7 @@ impl TimeRangeOptions { /// # Returns /// /// Returns the time range options for the specific date - pub fn specific_date(date: PaceDate) -> PaceTimeResult { + pub fn specific_date(date: PaceDate) -> PaceResult { // handle date if it's in the future let (start, end) = if date.is_future() { debug!("Date is in the future, using today."); @@ -183,7 +179,7 @@ impl TimeRangeOptions { PaceDate::default() .inner() .and_hms_opt(0, 0, 0) - .ok_or_else(|| PaceTimeErrorKind::InvalidDate(date.to_string()))?, + .ok_or_else(|| TimeErrorKind::InvalidDate(date.to_string()))?, )?, PaceDateTime::now(), ) @@ -192,12 +188,12 @@ impl TimeRangeOptions { PaceDateTime::try_from( date.inner() .and_hms_opt(0, 0, 0) - .ok_or_else(|| PaceTimeErrorKind::InvalidDate(date.to_string()))?, + .ok_or_else(|| TimeErrorKind::InvalidDate(date.to_string()))?, )?, PaceDateTime::try_from( date.inner() .and_hms_opt(23, 59, 59) - .ok_or_else(|| PaceTimeErrorKind::InvalidDate(date.to_string()))?, + .ok_or_else(|| TimeErrorKind::InvalidDate(date.to_string()))?, )?, ) }; @@ -214,30 +210,30 @@ impl TimeRangeOptions { /// # Returns /// /// Returns the time range options for the last month - pub fn last_month() -> PaceTimeResult { + pub fn last_month() -> PaceResult { let now = Local::now(); let start = NaiveDate::from_ymd_opt(now.year(), now.month() - 1, 1).ok_or_else(|| { - PaceTimeErrorKind::InvalidDate(format!("{}/{}", now.year(), now.month() - 1)) + TimeErrorKind::InvalidDate(format!("{}/{}", now.year(), now.month() - 1)) })?; let end = start .with_day(1) - .ok_or_else(|| PaceTimeErrorKind::InvalidDate(start.to_string()))? + .ok_or_else(|| TimeErrorKind::InvalidDate(start.to_string()))? .with_month(start.month() + 1) - .ok_or_else(|| PaceTimeErrorKind::InvalidDate(start.to_string()))? + .ok_or_else(|| TimeErrorKind::InvalidDate(start.to_string()))? .pred_opt() - .ok_or_else(|| PaceTimeErrorKind::InvalidDate(start.to_string()))?; + .ok_or_else(|| TimeErrorKind::InvalidDate(start.to_string()))?; Ok(Self::builder() .start(PaceDateTime::try_from( start .and_hms_opt(0, 0, 0) - .ok_or_else(|| PaceTimeErrorKind::InvalidDate(start.to_string()))?, + .ok_or_else(|| TimeErrorKind::InvalidDate(start.to_string()))?, )?) .end(PaceDateTime::try_from( end.and_hms_opt(23, 59, 59) - .ok_or_else(|| PaceTimeErrorKind::InvalidDate(end.to_string()))?, + .ok_or_else(|| TimeErrorKind::InvalidDate(end.to_string()))?, )?) .build()) } @@ -251,7 +247,7 @@ impl TimeRangeOptions { /// # Returns /// /// Returns the time range options for the last week - pub fn last_week() -> PaceTimeResult { + pub fn last_week() -> PaceResult { let now = Local::now(); let last_week = now @@ -259,32 +255,30 @@ impl TimeRangeOptions { .iso_week() .week() .checked_sub(1) - .ok_or_else(|| PaceTimeErrorKind::InvalidDate(now.date_naive().to_string()))?; + .ok_or_else(|| TimeErrorKind::InvalidDate(now.date_naive().to_string()))?; let week = NaiveDate::from_isoywd_opt(now.year(), last_week, chrono::Weekday::Mon) - .ok_or_else(|| PaceTimeErrorKind::InvalidDate(format!("{}/{}", now.year(), last_week)))? + .ok_or_else(|| TimeErrorKind::InvalidDate(format!("{}/{}", now.year(), last_week)))? .week(chrono::Weekday::Mon); // handle first week of the year // FIXME: this is a hack, find a better way to handle this if week.first_day().year() != now.year() { - let start = NaiveDate::from_ymd_opt(now.year() - 1, 12, 25).ok_or_else(|| { - PaceTimeErrorKind::InvalidDate(format!("{}/{}", now.year() - 1, 12)) - })?; + let start = NaiveDate::from_ymd_opt(now.year() - 1, 12, 25) + .ok_or_else(|| TimeErrorKind::InvalidDate(format!("{}/{}", now.year() - 1, 12)))?; - let end = NaiveDate::from_ymd_opt(now.year() - 1, 12, 31).ok_or_else(|| { - PaceTimeErrorKind::InvalidDate(format!("{}/{}", now.year() - 1, 12)) - })?; + let end = NaiveDate::from_ymd_opt(now.year() - 1, 12, 31) + .ok_or_else(|| TimeErrorKind::InvalidDate(format!("{}/{}", now.year() - 1, 12)))?; return Ok(Self::builder() .start(PaceDateTime::try_from( start .and_hms_opt(0, 0, 0) - .ok_or_else(|| PaceTimeErrorKind::InvalidDate(start.to_string()))?, + .ok_or_else(|| TimeErrorKind::InvalidDate(start.to_string()))?, )?) .end(PaceDateTime::try_from( end.and_hms_opt(23, 59, 59) - .ok_or_else(|| PaceTimeErrorKind::InvalidDate(end.to_string()))?, + .ok_or_else(|| TimeErrorKind::InvalidDate(end.to_string()))?, )?) .build()); } @@ -293,12 +287,12 @@ impl TimeRangeOptions { .start(PaceDateTime::try_from( week.first_day() .and_hms_opt(0, 0, 0) - .ok_or_else(|| PaceTimeErrorKind::InvalidDate(week.first_day().to_string()))?, + .ok_or_else(|| TimeErrorKind::InvalidDate(week.first_day().to_string()))?, )?) .end(PaceDateTime::try_from( week.last_day() .and_hms_opt(23, 59, 59) - .ok_or_else(|| PaceTimeErrorKind::InvalidDate(week.last_day().to_string()))?, + .ok_or_else(|| TimeErrorKind::InvalidDate(week.last_day().to_string()))?, )?) .build()) } @@ -312,24 +306,24 @@ impl TimeRangeOptions { /// # Returns /// /// Returns the time range options for the last year - pub fn last_year() -> PaceTimeResult { + pub fn last_year() -> PaceResult { let now = Local::now(); let start = NaiveDate::from_ymd_opt(now.year() - 1, 1, 1) - .ok_or_else(|| PaceTimeErrorKind::InvalidDate(format!("{}/{}", now.year() - 1, 1)))?; + .ok_or_else(|| TimeErrorKind::InvalidDate(format!("{}/{}", now.year() - 1, 1)))?; let end = NaiveDate::from_ymd_opt(now.year() - 1, 12, 31) - .ok_or_else(|| PaceTimeErrorKind::InvalidDate(format!("{}/{}", now.year() - 1, 12)))?; + .ok_or_else(|| TimeErrorKind::InvalidDate(format!("{}/{}", now.year() - 1, 12)))?; Ok(Self::builder() .start(PaceDateTime::try_from( start .and_hms_opt(0, 0, 0) - .ok_or_else(|| PaceTimeErrorKind::InvalidDate(start.to_string()))?, + .ok_or_else(|| TimeErrorKind::InvalidDate(start.to_string()))?, )?) .end(PaceDateTime::try_from( end.and_hms_opt(23, 59, 59) - .ok_or_else(|| PaceTimeErrorKind::InvalidDate(end.to_string()))?, + .ok_or_else(|| TimeErrorKind::InvalidDate(end.to_string()))?, )?) .build()) } @@ -343,14 +337,14 @@ impl TimeRangeOptions { /// # Returns /// /// Returns the time range options for today - pub fn today() -> PaceTimeResult { + pub fn today() -> PaceResult { let now = Local::now(); Ok(Self::builder() .start(PaceDateTime::try_from( now.date_naive() .and_hms_opt(0, 0, 0) - .ok_or_else(|| PaceTimeErrorKind::InvalidDate(now.to_string()))?, + .ok_or_else(|| TimeErrorKind::InvalidDate(now.to_string()))?, )?) .build()) } @@ -364,31 +358,31 @@ impl TimeRangeOptions { /// # Returns /// /// Returns the time range options for yesterday - pub fn yesterday() -> PaceTimeResult { + pub fn yesterday() -> PaceResult { let now = Local::now(); let yesterday = now .date_naive() .pred_opt() - .ok_or_else(|| PaceTimeErrorKind::InvalidDate(now.date_naive().to_string()))?; + .ok_or_else(|| TimeErrorKind::InvalidDate(now.date_naive().to_string()))?; Ok(Self::builder() .start(PaceDateTime::try_from( yesterday .and_hms_opt(0, 0, 0) - .ok_or_else(|| PaceTimeErrorKind::InvalidDate(yesterday.to_string()))?, + .ok_or_else(|| TimeErrorKind::InvalidDate(yesterday.to_string()))?, )?) .end(PaceDateTime::try_from( yesterday .and_hms_opt(23, 59, 59) - .ok_or_else(|| PaceTimeErrorKind::InvalidDate(yesterday.to_string()))?, + .ok_or_else(|| TimeErrorKind::InvalidDate(yesterday.to_string()))?, )?) .build()) } } impl TryFrom<(PaceDate, PaceDate)> for TimeRangeOptions { - type Error = PaceTimeErrorKind; + type Error = TimeErrorKind; fn try_from((start, end): (PaceDate, PaceDate)) -> Result { Ok(Self::builder() diff --git a/crates/time/src/time_zone.rs b/crates/time/src/time_zone.rs index 045fac02..2be52286 100644 --- a/crates/time/src/time_zone.rs +++ b/crates/time/src/time_zone.rs @@ -1,6 +1,6 @@ use chrono::{FixedOffset, Local}; -use crate::error::PaceTimeErrorKind; +use pace_error::TimeErrorKind; /// Get the local time zone offset to UTC to guess the time zones /// @@ -92,7 +92,7 @@ impl PaceTimeZoneKind { } impl TryFrom<(Option<&chrono_tz::Tz>, Option<&FixedOffset>)> for PaceTimeZoneKind { - type Error = PaceTimeErrorKind; + type Error = TimeErrorKind; fn try_from( (tz, tz_offset): (Option<&chrono_tz::Tz>, Option<&FixedOffset>), @@ -101,7 +101,7 @@ impl TryFrom<(Option<&chrono_tz::Tz>, Option<&FixedOffset>)> for PaceTimeZoneKin (Some(tz), None) => Ok(Self::TimeZone(tz.to_owned())), (None, Some(tz_offset)) => Ok(Self::TimeZoneOffset(tz_offset.to_owned())), (None, None) => Ok(Self::NotSet), - (Some(_), Some(_)) => Err(PaceTimeErrorKind::AmbiguousTimeZones), + (Some(_), Some(_)) => Err(TimeErrorKind::AmbiguousTimeZones), } } } diff --git a/db/Entity-Relationship.png b/db/Entity-Relationship.png new file mode 100644 index 00000000..ef5d6c81 Binary files /dev/null and b/db/Entity-Relationship.png differ diff --git a/db/README.md b/db/README.md new file mode 100644 index 00000000..543970b4 --- /dev/null +++ b/db/README.md @@ -0,0 +1,14 @@ +# Database + +This directory contains the database schema and migrations for the application. +It also contains the development database used for testing and development. + +## Database Connection + +The database connection string is read from the `DATABASE_URL` environment +variable. This variable should be set to the connection string for the database. +For example: + +```console +export DATABASE_URL="sqlite:./db/db.sqlite3" +``` diff --git a/src/commands.rs b/src/commands.rs index 6cb1655a..b8c91cc2 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -30,7 +30,7 @@ use std::path::PathBuf; use pace_core::{ constants::PACE_CONFIG_FILENAME, - prelude::{get_config_paths, ActivityLogFormatKind, PaceConfig}, + prelude::{get_config_paths, PaceConfig}, }; /// Pace Subcommands @@ -112,7 +112,11 @@ pub struct EntryPoint { /// Use the specified activity log file #[arg(long, env = "PACE_ACTIVITY_LOG_FILE", value_hint = clap::ValueHint::FilePath)] - pub activity_log_file: Option, + pub activity_log: Option, + + /// Use the specified database URL for activity storage + #[arg(long, env = "PACE_ACTIVITY_DATABASE_URL", value_hint = clap::ValueHint::Url)] + pub database_url: Option, /// Pace Home Directory #[arg(long, env = "PACE_HOME", value_hint = clap::ValueHint::DirPath)] @@ -129,8 +133,8 @@ impl Runnable for EntryPoint { impl Override for EntryPoint { fn override_config(&self, mut config: PaceConfig) -> Result { // Override the activity log file if it's set - if let Some(activity_log_file) = &self.activity_log_file { - debug!("Overriding activity log file with: {:?}", activity_log_file); + if let Some(activity_log_file) = &self.activity_log { + debug!("Overriding activity log file with: {activity_log_file:?}"); // Handle not existing activity log file and parent directory match (activity_log_file.parent(), activity_log_file.exists()) { @@ -144,18 +148,10 @@ impl Override for EntryPoint { _ => {} }; - *config.general_mut().activity_log_options_mut().path_mut() = - activity_log_file.to_path_buf(); - - // Set the activity log format to TOML - // TODO: This should be configurable - *config - .general_mut() - .activity_log_options_mut() - .format_kind_mut() = Some(ActivityLogFormatKind::Toml); + config.set_activity_log_path(activity_log_file); }; - debug!("Overridden config: {:?}", config); + debug!("Overridden config: {config:?}"); Ok(config) } @@ -212,7 +208,7 @@ impl Configurable for EntryPoint { _ => None, }; - debug!("Using config path: {:?}", config_path); + debug!("Using config path: {config_path:?}"); config_path } diff --git a/src/commands/adjust.rs b/src/commands/adjust.rs index 10453a01..59f16dde 100644 --- a/src/commands/adjust.rs +++ b/src/commands/adjust.rs @@ -2,11 +2,11 @@ use abscissa_core::{status_err, Application, Command, Runnable, Shutdown}; use clap::Parser; +use pace_cli::commands::adjust::AdjustCommandOptions; +use pace_service::get_storage_from_config; use crate::prelude::PACE_APP; -use pace_core::prelude::AdjustCommandOptions; - /// `adjust` subcommand #[derive(Command, Debug, Parser)] pub struct AdjustCmd { @@ -16,12 +16,17 @@ pub struct AdjustCmd { impl Runnable for AdjustCmd { fn run(&self) { - match self.adjust_opts.handle_adjust(&PACE_APP.config()) { - Ok(user_message) => user_message.display(), - Err(err) => { - status_err!("{}", err); - PACE_APP.shutdown(Shutdown::Crash); - } - }; + if let Ok(storage) = get_storage_from_config(&PACE_APP.config()) { + match self.adjust_opts.handle_adjust(&PACE_APP.config(), storage) { + Ok(user_message) => user_message.display(), + Err(err) => { + status_err!("{}", err); + PACE_APP.shutdown(Shutdown::Crash); + } + }; + } else { + status_err!("Failed to get storage from config"); + PACE_APP.shutdown(Shutdown::Crash); + } } } diff --git a/src/commands/begin.rs b/src/commands/begin.rs index 3443f97a..909b173f 100644 --- a/src/commands/begin.rs +++ b/src/commands/begin.rs @@ -2,11 +2,11 @@ use abscissa_core::{status_err, Application, Command, Runnable, Shutdown}; use clap::Parser; +use pace_cli::commands::begin::BeginCommandOptions; +use pace_service::get_storage_from_config; use crate::prelude::PACE_APP; -use pace_core::prelude::BeginCommandOptions; - /// `begin` subcommand #[derive(Command, Debug, Parser)] pub struct BeginCmd { @@ -16,12 +16,17 @@ pub struct BeginCmd { impl Runnable for BeginCmd { fn run(&self) { - match self.begin_opts.handle_begin(&PACE_APP.config()) { - Ok(user_message) => user_message.display(), - Err(err) => { - status_err!("{}", err); - PACE_APP.shutdown(Shutdown::Crash); - } - }; + if let Ok(storage) = get_storage_from_config(&PACE_APP.config()) { + match self.begin_opts.handle_begin(&PACE_APP.config(), storage) { + Ok(user_message) => user_message.display(), + Err(err) => { + status_err!("{}", err); + PACE_APP.shutdown(Shutdown::Crash); + } + }; + } else { + status_err!("Failed to get storage from config"); + PACE_APP.shutdown(Shutdown::Crash); + } } } diff --git a/src/commands/docs.rs b/src/commands/docs.rs index 06fcbc38..5d974fbf 100644 --- a/src/commands/docs.rs +++ b/src/commands/docs.rs @@ -2,7 +2,7 @@ use abscissa_core::{status_err, Application, Command, Runnable, Shutdown}; use clap::Args; -use pace_core::prelude::DocsCommandOptions; +use pace_cli::commands::docs::DocsCommandOptions; use crate::application::PACE_APP; diff --git a/src/commands/end.rs b/src/commands/end.rs index 6201c220..6a5add56 100644 --- a/src/commands/end.rs +++ b/src/commands/end.rs @@ -2,11 +2,11 @@ use abscissa_core::{status_err, Application, Command, Runnable, Shutdown}; use clap::Parser; +use pace_cli::commands::end::EndCommandOptions; +use pace_service::get_storage_from_config; use crate::prelude::PACE_APP; -use pace_core::prelude::EndCommandOptions; - /// `end` subcommand #[derive(Command, Debug, Parser)] pub struct EndCmd { @@ -16,12 +16,17 @@ pub struct EndCmd { impl Runnable for EndCmd { fn run(&self) { - match self.end_opts.handle_end(&PACE_APP.config()) { - Ok(user_message) => user_message.display(), - Err(err) => { - status_err!("{}", err); - PACE_APP.shutdown(Shutdown::Crash); - } - }; + if let Ok(storage) = get_storage_from_config(&PACE_APP.config()) { + match self.end_opts.handle_end(&PACE_APP.config(), storage) { + Ok(user_message) => user_message.display(), + Err(err) => { + status_err!("{}", err); + PACE_APP.shutdown(Shutdown::Crash); + } + }; + } else { + status_err!("Failed to get storage from config"); + PACE_APP.shutdown(Shutdown::Crash); + } } } diff --git a/src/commands/hold.rs b/src/commands/hold.rs index 6b5c64b2..059e7c3b 100644 --- a/src/commands/hold.rs +++ b/src/commands/hold.rs @@ -3,7 +3,8 @@ use abscissa_core::{status_err, Application, Command, Runnable, Shutdown}; use clap::Parser; -use pace_core::prelude::HoldCommandOptions; +use pace_cli::commands::hold::HoldCommandOptions; +use pace_service::get_storage_from_config; use crate::prelude::PACE_APP; @@ -16,12 +17,17 @@ pub struct HoldCmd { impl Runnable for HoldCmd { fn run(&self) { - match self.hold_opts.handle_hold(&PACE_APP.config()) { - Ok(user_message) => user_message.display(), - Err(err) => { - status_err!("{}", err); - PACE_APP.shutdown(Shutdown::Crash); - } - }; + if let Ok(storage) = get_storage_from_config(&PACE_APP.config()) { + match self.hold_opts.handle_hold(&PACE_APP.config(), storage) { + Ok(user_message) => user_message.display(), + Err(err) => { + status_err!("{}", err); + PACE_APP.shutdown(Shutdown::Crash); + } + }; + } else { + status_err!("Failed to get storage from config"); + PACE_APP.shutdown(Shutdown::Crash); + } } } diff --git a/src/commands/now.rs b/src/commands/now.rs index 89c5bd27..1b56ce21 100644 --- a/src/commands/now.rs +++ b/src/commands/now.rs @@ -2,11 +2,11 @@ use abscissa_core::{status_err, Application, Command, Runnable, Shutdown}; use clap::Parser; +use pace_cli::commands::now::NowCommandOptions; +use pace_service::get_storage_from_config; use crate::prelude::PACE_APP; -use pace_core::prelude::NowCommandOptions; - /// `now` subcommand #[derive(Command, Debug, Parser)] pub struct NowCmd { @@ -16,12 +16,17 @@ pub struct NowCmd { impl Runnable for NowCmd { fn run(&self) { - match self.now_opts.handle_now(&PACE_APP.config()) { - Ok(user_message) => user_message.display(), - Err(err) => { - status_err!("{}", err); - PACE_APP.shutdown(Shutdown::Crash); - } - }; + if let Ok(storage) = get_storage_from_config(&PACE_APP.config()) { + match self.now_opts.handle_now(&PACE_APP.config(), storage) { + Ok(user_message) => user_message.display(), + Err(err) => { + status_err!("{}", err); + PACE_APP.shutdown(Shutdown::Crash); + } + }; + } else { + status_err!("Failed to get storage from config"); + PACE_APP.shutdown(Shutdown::Crash); + } } } diff --git a/src/commands/reflect.rs b/src/commands/reflect.rs index 331f3f2d..2b7698ba 100644 --- a/src/commands/reflect.rs +++ b/src/commands/reflect.rs @@ -6,7 +6,8 @@ use abscissa_core::{status_err, Application, Command, Runnable, Shutdown}; use clap::Parser; -use pace_core::prelude::ReflectCommandOptions; +use pace_cli::commands::reflect::ReflectCommandOptions; +use pace_service::get_storage_from_config; use crate::prelude::PACE_APP; @@ -19,12 +20,17 @@ pub struct ReflectCmd { impl Runnable for ReflectCmd { fn run(&self) { - match self.review_opts.handle_reflect(&PACE_APP.config()) { - Ok(user_message) => user_message.display(), - Err(err) => { - status_err!("{}", err); - PACE_APP.shutdown(Shutdown::Crash); - } - }; + if let Ok(storage) = get_storage_from_config(&PACE_APP.config()) { + match self.review_opts.handle_reflect(&PACE_APP.config(), storage) { + Ok(user_message) => user_message.display(), + Err(err) => { + status_err!("{}", err); + PACE_APP.shutdown(Shutdown::Crash); + } + }; + } else { + status_err!("Failed to get storage from config"); + PACE_APP.shutdown(Shutdown::Crash); + } } } diff --git a/src/commands/resume.rs b/src/commands/resume.rs index 1ce428e2..f69ff9ef 100644 --- a/src/commands/resume.rs +++ b/src/commands/resume.rs @@ -5,11 +5,16 @@ use abscissa_core::{status_err, tracing::debug, Application, Command, Runnable, use clap::Parser; use eyre::Result; -use pace_cli::{confirmation_or_break, prompt_resume_activity}; -use pace_core::prelude::{ - get_storage_from_config, ActivityQuerying, ActivityReadOps, ActivityStateManagement, - ActivityStore, ResumeCommandOptions, ResumeOptions, SyncStorage, UserMessage, +use pace_cli::{ + commands::resume::ResumeCommandOptions, + prompt::{confirmation_or_break_default_true, prompt_resume_activity}, }; +use pace_core::{ + options::ResumeOptions, + prelude::{ActivityQuerying, ActivityReadOps, ActivityStateManagement, SyncStorage}, +}; +use pace_error::UserMessage; +use pace_service::{activity_store::ActivityStore, get_storage_from_config}; use pace_time::{date_time::PaceDateTime, time_zone::PaceTimeZoneKind, Validate}; use crate::prelude::PACE_APP; @@ -111,7 +116,7 @@ impl ResumeCmd { Err(recoverable_err) if recoverable_err.possible_new_activity_from_resume() => { debug!("Activity to resume: {:?}", activity_item.activity()); - confirmation_or_break( + confirmation_or_break_default_true( "We can't resume this already ended activity. Do you want to begin one with the same contents?", )?; @@ -119,11 +124,11 @@ impl ResumeCmd { let new_activity = activity_item.activity().new_from_self(); - debug!("New Activity: {:?}", new_activity); + debug!("New Activity: {new_activity:?}"); let new_stored_activity = activity_store.begin_activity(new_activity)?; - debug!("Started Activity: {:?}", new_stored_activity); + debug!("Started Activity: {new_stored_activity:?}"); format!("Resumed {}", new_stored_activity.activity()) } diff --git a/src/commands/settings/set.rs b/src/commands/settings/set.rs index fe7cd7de..fe58c0a8 100644 --- a/src/commands/settings/set.rs +++ b/src/commands/settings/set.rs @@ -1,7 +1,7 @@ use abscissa_core::{Command, Runnable}; use clap::{Parser, Subcommand}; -use pace_cli::prompt_time_zone; +use pace_cli::prompt::prompt_time_zone; use pace_core::prelude::PaceConfig; use crate::prelude::PACE_APP; diff --git a/src/commands/setup/config.rs b/src/commands/setup/config.rs index c41ec2ef..e418ce5f 100644 --- a/src/commands/setup/config.rs +++ b/src/commands/setup/config.rs @@ -6,7 +6,7 @@ use abscissa_core::{status_warn, Application, Command, Runnable, Shutdown}; use clap::Parser; use dialoguer::console::Term; -use pace_cli::{setup_config, PathOptions}; +use pace_cli::setup::{setup_config, PathOptions}; use crate::prelude::PACE_APP; diff --git a/tests/fixtures/configs/pace.toml b/tests/fixtures/configs/pace.toml index 7e5cb80f..256ba459 100644 --- a/tests/fixtures/configs/pace.toml +++ b/tests/fixtures/configs/pace.toml @@ -1,10 +1,4 @@ [general] -# Define where to store the activity log: options include "file", "database", etc. -storage-kind = "file" -# Path to the activity log file, used if storage-kind is set to "file" -path = "/path/to/your/activity.pace.toml" -# Specify the default format for new activity logs: "toml" or "yaml" -format-kind = "toml" # Category separator used in the cli category-separator = "::" # Default priority for new tasks @@ -23,10 +17,9 @@ include-tags = true include-descriptions = true time-format = "%Y-%m-%d %H:%M" -[database] -# Database configurations are used if storage-kind is set to "database" -engine = "sqlite" # only option supported for now is "sqlite" -connection-string = "path/to/your/database.db" +[storage.file] +# In case of a file, this is the path to the file: `/path/to/file` +location = "/path/to/your/storage" [pomodoro] # Pomodoro technique specific configurations diff --git a/crates/core/tests/integration/activity_store.rs b/tests/integration/activity_store.rs similarity index 93% rename from crates/core/tests/integration/activity_store.rs rename to tests/integration/activity_store.rs index 6ea0e626..35b0874f 100644 --- a/crates/core/tests/integration/activity_store.rs +++ b/tests/integration/activity_store.rs @@ -1,12 +1,17 @@ //! Test the `ActivityStore` implementation with a `InMemoryStorage` backend. -use std::{collections::HashSet, sync::Arc}; - -use pace_core::prelude::{ - Activity, ActivityFilterKind, ActivityGuid, ActivityReadOps, ActivityStateManagement, - ActivityStatusKind, ActivityStore, ActivityWriteOps, DeleteOptions, EndOptions, HoldOptions, - InMemoryActivityStorage, ResumeOptions, TestResult, UpdateOptions, +use std::sync::Arc; + +use pace_core::{ + options::{DeleteOptions, EndOptions, HoldOptions, ResumeOptions, UpdateOptions}, + prelude::{ + Activity, ActivityFilterKind, ActivityGuid, ActivityReadOps, ActivityStateManagement, + ActivityStatusKind, ActivityWriteOps, PaceCategory, PaceDescription, PaceTagCollection, + }, }; +use pace_error::TestResult; +use pace_service::activity_store::ActivityStore; +use pace_storage::storage::in_memory::InMemoryActivityStorage; use crate::util::{ activity_store, activity_store_empty, activity_store_no_intermissions, TestData, @@ -25,8 +30,8 @@ fn test_activity_store_create_activity_passes( } = activity_store_empty?; let activity = Activity::builder() - .description("Test Description".to_string()) - .category("Test::Category".to_string()) + .description(PaceDescription::new("Test Description")) + .category(PaceCategory::new("Test::Category")) .build(); let og_activity = activity.clone(); @@ -248,16 +253,16 @@ fn test_activity_store_update_activity_passes( let tags = vec!["bla".to_string(), "cookie".to_string()] .into_iter() - .collect::>(); + .collect::(); let og_activity = activities[0].clone(); let og_activity_id = *og_activity.guid(); - let updated_test_desc = "Updated Test Description".to_string(); - let updated_test_cat = "Test::UpdatedCategory".to_string(); + let updated_test_desc = PaceDescription::new("Updated Test Description"); + let updated_test_cat = PaceCategory::new("Test::UpdatedCategory"); let new_activity = Activity::builder() - .description(updated_test_desc.to_string()) + .description(updated_test_desc.clone()) .category(updated_test_cat.clone()) .tags(tags.clone()) .build(); @@ -353,8 +358,8 @@ fn test_activity_store_update_activity_fails( } = activity_store?; let new_activity = Activity::builder() - .description("test".to_string()) - .category("test".to_string()) + .description(PaceDescription::new("test")) + .category(PaceCategory::new("test")) .build(); let activity_id = ActivityGuid::default(); @@ -417,12 +422,12 @@ fn test_activity_store_begin_intermission_passes( assert_eq!( intermission.activity().category(), - &Some("Test::Intermission".to_string()) + &Some(PaceCategory::new("Test::Intermission")) ); assert_eq!( intermission.activity().description(), - "Activity with Intermission" + &PaceDescription::new("Activity with Intermission") ); assert_eq!( @@ -483,13 +488,13 @@ fn test_activity_store_begin_intermission_with_existing_does_nothing_passes( assert_eq!( intermission.activity().category(), - &Some("Test::Intermission".to_string()), + &Some(PaceCategory::new("Test::Intermission")), "Category should be the same as original activity." ); assert_eq!( intermission.activity().description(), - "Activity with Intermission" + &PaceDescription::new("Activity with Intermission") ); assert_eq!( @@ -629,11 +634,11 @@ fn test_activity_store_resume_activity_passes( #[rstest] fn test_begin_activity_with_held_activity() -> TestResult<()> { - let store = ActivityStore::with_storage(Arc::new(InMemoryActivityStorage::new().into()))?; + let store = ActivityStore::with_storage(Arc::new(InMemoryActivityStorage::new()))?; // Begin activity let activity = Activity::builder() - .description("Test Description".to_string()) + .description(PaceDescription::new("Test Description")) .build(); let activity = store.begin_activity(activity)?; @@ -657,7 +662,7 @@ fn test_begin_activity_with_held_activity() -> TestResult<()> { // Begin another activity although there is a held activity let new_activity = Activity::builder() - .description("New Description".to_string()) + .description(PaceDescription::new("New Description")) .build(); let new_activity = store.begin_activity(new_activity)?; diff --git a/crates/core/tests/integration/activity_tracker.rs b/tests/integration/activity_tracker.rs similarity index 92% rename from crates/core/tests/integration/activity_tracker.rs rename to tests/integration/activity_tracker.rs index f60b419e..3c3270f2 100644 --- a/crates/core/tests/integration/activity_tracker.rs +++ b/tests/integration/activity_tracker.rs @@ -1,11 +1,14 @@ //! Test the `ActivityStore` implementation with a `InMemoryStorage` backend. use chrono::NaiveDate; -use pace_core::prelude::{ActivityStore, ActivityTracker, FilterOptions, TestResult}; -use pace_time::{duration::PaceDuration, time_range::TimeRangeOptions}; use rstest::rstest; use similar_asserts::assert_eq; +use pace_core::options::FilterOptions; +use pace_error::TestResult; +use pace_service::{activity_store::ActivityStore, activity_tracker::ActivityTracker}; +use pace_time::{duration::PaceDuration, time_range::TimeRangeOptions}; + use crate::util::setup_activity_store_for_activity_tracker; #[rstest] diff --git a/crates/core/tests/integration/find_configs.rs b/tests/integration/config.rs similarity index 61% rename from crates/core/tests/integration/find_configs.rs rename to tests/integration/config.rs index 9bbace44..06d413d8 100644 --- a/crates/core/tests/integration/find_configs.rs +++ b/tests/integration/config.rs @@ -1,4 +1,6 @@ -use pace_core::prelude::{find_root_config_file_path, TestResult}; +use insta::assert_toml_snapshot; +use pace_core::prelude::{find_root_config_file_path, PaceConfig}; +use pace_error::TestResult; use similar_asserts::assert_eq; use std::env; @@ -11,7 +13,7 @@ fn test_find_root_projects_file() -> TestResult<()> { // navigate to the test directory for the fixtures let root = current_dir - .join("../../tests/fixtures/project1/subproject-a/") + .join("tests/fixtures/project1/subproject-a/") .canonicalize()?; // get the path to the projects config file @@ -20,9 +22,16 @@ fn test_find_root_projects_file() -> TestResult<()> { assert_eq!( projects_config_path, current_dir - .join("../../tests/fixtures/project1/projects.pace.toml") + .join("tests/fixtures/project1/projects.pace.toml") .canonicalize()? ); Ok(()) } + +#[test] +fn test_config_default_snapshot_passes() { + let config = PaceConfig::default(); + + assert_toml_snapshot!(config); +} diff --git a/crates/core/tests/integration/main.rs b/tests/integration/main.rs similarity index 74% rename from crates/core/tests/integration/main.rs rename to tests/integration/main.rs index d330e7e8..a01fe552 100644 --- a/crates/core/tests/integration/main.rs +++ b/tests/integration/main.rs @@ -1,4 +1,4 @@ mod activity_store; mod activity_tracker; -mod find_configs; +mod config; mod util; diff --git a/tests/integration/snapshots/integration__config__config_default_snapshot_passes.snap b/tests/integration/snapshots/integration__config__config_default_snapshot_passes.snap new file mode 100644 index 00000000..47856b5b --- /dev/null +++ b/tests/integration/snapshots/integration__config__config_default_snapshot_passes.snap @@ -0,0 +1,12 @@ +--- +source: tests/integration/config.rs +expression: config +--- +[general] +category-separator = '::' +default-priority = 'medium' +most-recent-count = 9 +default-time-zone = 'UTC' +[storage.database] +kind = 'sqlite' +url = './db/activities.pace.sqlite3' diff --git a/crates/core/tests/integration/util.rs b/tests/integration/util.rs similarity index 83% rename from crates/core/tests/integration/util.rs rename to tests/integration/util.rs index 36a666c7..c1a014b6 100644 --- a/crates/core/tests/integration/util.rs +++ b/tests/integration/util.rs @@ -1,13 +1,16 @@ -use std::{collections::HashSet, path::Path, sync::Arc}; +use std::{path::Path, sync::Arc}; use chrono::Local; +use rstest::fixture; use pace_core::prelude::{ Activity, ActivityGuid, ActivityItem, ActivityKind, ActivityKindOptions, ActivityLog, - ActivityStatusKind, ActivityStore, InMemoryActivityStorage, TestResult, TomlActivityStorage, + ActivityStatusKind, PaceCategory, PaceDescription, PaceTagCollection, }; - -use rstest::fixture; +use pace_error::TestResult; +use pace_service::activity_store::ActivityStore; +use pace_storage::storage::{file::TomlActivityStorage, in_memory::InMemoryActivityStorage}; +use pace_time::date_time::PaceDateTime; pub struct TestData { pub activities: Vec, @@ -52,16 +55,14 @@ pub fn activity_store_no_intermissions() -> TestResult { // We need to use `#[cfg(not(tarpaulin_include))]` to exclude this from coverage reports #[cfg(not(tarpaulin_include))] pub fn setup_activity_store(kind: &ActivityStoreTestKind) -> TestResult { - use pace_time::date_time::PaceDateTime; - let begin_time = PaceDateTime::default(); let tags = vec!["test".to_string(), "activity".to_string()] .into_iter() - .collect::>(); + .collect::(); let mut completed = Activity::builder() - .description("Activity with end".to_string()) + .description(PaceDescription::new("Activity with end")) .begin(begin_time) .status(ActivityStatusKind::Completed) .tags(tags.clone()) @@ -71,7 +72,7 @@ pub fn setup_activity_store(kind: &ActivityStoreTestKind) -> TestResult TestResult TestResult TestResult TestResult TestResult { let fixture_path = - Path::new("../../tests/fixtures/activity_tracker/activities.pace.toml").canonicalize()?; - - let storage = TomlActivityStorage::new(fixture_path)?; + Path::new("tests/fixtures/activity_tracker/activities.pace.toml").canonicalize()?; - Ok(ActivityStore::with_storage(Arc::new(storage.into()))?) + Ok(ActivityStore::with_storage(Arc::new( + TomlActivityStorage::new(fixture_path)?, + ))?) } diff --git a/tests/cli.rs b/tests/journey/cli.rs similarity index 94% rename from tests/cli.rs rename to tests/journey/cli.rs index 768dc958..dfa2aa43 100644 --- a/tests/cli.rs +++ b/tests/journey/cli.rs @@ -33,7 +33,7 @@ fn fixture_begin_activity(dir_str: &String) -> TestResult<()> { .args([ "--config", "tests/fixtures/configs/pace.toml", - "--activity-log-file", + "--activity-log", dir_str, "begin", "MyActivity", @@ -77,7 +77,7 @@ fn test_begin_snapshot_passes() -> TestResult<()> { assert_cmd_snapshot!(StdCommand::new(env!("CARGO_BIN_EXE_pace")).args([ "--config", "tests/fixtures/configs/pace.toml", - "--activity-log-file", + "--activity-log", &dir_str, "begin", "MyActivity", @@ -97,7 +97,7 @@ fn test_now_no_activities_snapshot_passes() -> TestResult<()> { assert_cmd_snapshot!(StdCommand::new(env!("CARGO_BIN_EXE_pace")).args([ "--config", "tests/fixtures/configs/pace.toml", - "--activity-log-file", + "--activity-log", &dir_str, "now" ])); @@ -115,7 +115,7 @@ fn test_now_with_active_activity_snapshot_passes() -> TestResult<()> { assert_cmd_snapshot!(StdCommand::new(env!("CARGO_BIN_EXE_pace")).args([ "--config", "tests/fixtures/configs/pace.toml", - "--activity-log-file", + "--activity-log", &dir_str, "now" ])); @@ -133,7 +133,7 @@ fn test_end_with_active_activity_snapshot_passes() -> TestResult<()> { assert_cmd_snapshot!(StdCommand::new(env!("CARGO_BIN_EXE_pace")).args([ "--config", "tests/fixtures/configs/pace.toml", - "--activity-log-file", + "--activity-log", &dir_str, "end" ])); @@ -151,7 +151,7 @@ fn test_hold_with_active_activity_snapshot_passes() -> TestResult<()> { assert_cmd_snapshot!(StdCommand::new(env!("CARGO_BIN_EXE_pace")).args([ "--config", "tests/fixtures/configs/pace.toml", - "--activity-log-file", + "--activity-log", &dir_str, "hold" ])); @@ -170,7 +170,7 @@ fn test_resume_with_held_activity_snapshot_passes() -> TestResult<()> { .args([ "--config", "tests/fixtures/configs/pace.toml", - "--activity-log-file", + "--activity-log", &dir_str, "hold", ]) @@ -179,7 +179,7 @@ fn test_resume_with_held_activity_snapshot_passes() -> TestResult<()> { assert_cmd_snapshot!(StdCommand::new(env!("CARGO_BIN_EXE_pace")).args([ "--config", "tests/fixtures/configs/pace.toml", - "--activity-log-file", + "--activity-log", &dir_str, "resume" ])); @@ -198,7 +198,7 @@ fn test_adjust_activity_snapshot_passes() -> TestResult<()> { .args([ "--config", "tests/fixtures/configs/pace.toml", - "--activity-log-file", + "--activity-log", &dir_str, "adjust", "--description", @@ -211,7 +211,7 @@ fn test_adjust_activity_snapshot_passes() -> TestResult<()> { assert_cmd_snapshot!(StdCommand::new(env!("CARGO_BIN_EXE_pace")).args([ "--config", "tests/fixtures/configs/pace.toml", - "--activity-log-file", + "--activity-log", &dir_str, "now", ])); @@ -228,7 +228,7 @@ fn test_reflect_from_to_snapshot_passes() -> TestResult<()> { assert_cmd_snapshot!(StdCommand::new(env!("CARGO_BIN_EXE_pace")).args([ "--config", "tests/fixtures/configs/pace.toml", - "--activity-log-file", + "--activity-log", &activities, "reflect", "--from", @@ -249,7 +249,7 @@ fn test_reflect_date_snapshot_passes() -> TestResult<()> { assert_cmd_snapshot!(StdCommand::new(env!("CARGO_BIN_EXE_pace")).args([ "--config", "tests/fixtures/configs/pace.toml", - "--activity-log-file", + "--activity-log", &activities, "reflect", "--date", @@ -267,7 +267,7 @@ fn test_reflect_today_snapshot_passes() -> TestResult<()> { assert_cmd_snapshot!(StdCommand::new(env!("CARGO_BIN_EXE_pace")).args([ "--config", "tests/fixtures/configs/pace.toml", - "--activity-log-file", + "--activity-log", &activities, "reflect", ])); @@ -283,7 +283,7 @@ fn test_reflect_current_week_snapshot_passes() -> TestResult<()> { assert_cmd_snapshot!(StdCommand::new(env!("CARGO_BIN_EXE_pace")).args([ "--config", "tests/fixtures/configs/pace.toml", - "--activity-log-file", + "--activity-log", &activities, "reflect", "current-week", @@ -300,7 +300,7 @@ fn test_reflect_current_month_snapshot_passes() -> TestResult<()> { assert_cmd_snapshot!(StdCommand::new(env!("CARGO_BIN_EXE_pace")).args([ "--config", "tests/fixtures/configs/pace.toml", - "--activity-log-file", + "--activity-log", &activities, "reflect", "current-month", @@ -318,7 +318,7 @@ fn test_reflect_from_to_filter_category_glob_front_snapshot_passes() -> TestResu assert_cmd_snapshot!(StdCommand::new(env!("CARGO_BIN_EXE_pace")).args([ "--config", "tests/fixtures/configs/pace.toml", - "--activity-log-file", + "--activity-log", &activities, "reflect", "--from", @@ -341,7 +341,7 @@ fn test_reflect_from_to_filter_category_case_sensitive_snapshot_passes() -> Test assert_cmd_snapshot!(StdCommand::new(env!("CARGO_BIN_EXE_pace")).args([ "--config", "tests/fixtures/configs/pace.toml", - "--activity-log-file", + "--activity-log", &activities, "reflect", "--from", @@ -365,7 +365,7 @@ fn test_reflect_from_to_filter_category_full_snapshot_passes() -> TestResult<()> assert_cmd_snapshot!(StdCommand::new(env!("CARGO_BIN_EXE_pace")).args([ "--config", "tests/fixtures/configs/pace.toml", - "--activity-log-file", + "--activity-log", &activities, "reflect", "--from", @@ -388,7 +388,7 @@ fn test_reflect_from_to_filter_category_glob_back_snapshot_passes() -> TestResul assert_cmd_snapshot!(StdCommand::new(env!("CARGO_BIN_EXE_pace")).args([ "--config", "tests/fixtures/configs/pace.toml", - "--activity-log-file", + "--activity-log", &activities, "reflect", "--from", @@ -412,7 +412,7 @@ fn test_reflect_from_to_filter_category_glob_back_case_sensitive_snapshot_passes assert_cmd_snapshot!(StdCommand::new(env!("CARGO_BIN_EXE_pace")).args([ "--config", "tests/fixtures/configs/pace.toml", - "--activity-log-file", + "--activity-log", &activities, "reflect", "--from", diff --git a/crates/core/tests/journey/hold_resume.rs b/tests/journey/hold_resume.rs similarity index 92% rename from crates/core/tests/journey/hold_resume.rs rename to tests/journey/hold_resume.rs index 33df6017..69f5471a 100644 --- a/crates/core/tests/journey/hold_resume.rs +++ b/tests/journey/hold_resume.rs @@ -1,14 +1,20 @@ -use pace_core::prelude::{ - Activity, ActivityQuerying, ActivityReadOps, ActivityStateManagement, HoldOptions, - InMemoryActivityStorage, ResumeOptions, TestResult, +use pace_core::{ + options::{HoldOptions, ResumeOptions}, + prelude::{ + Activity, ActivityQuerying, ActivityReadOps, ActivityStateManagement, PaceDescription, + }, }; +use pace_error::TestResult; +use pace_storage::storage::in_memory::InMemoryActivityStorage; #[test] #[allow(clippy::too_many_lines)] fn test_hold_resume_journey_for_activities_passes() -> TestResult<()> { let storage = InMemoryActivityStorage::new(); - let first_og_activity = Activity::builder().description("Test activity").build(); + let first_og_activity = Activity::builder() + .description(PaceDescription::new("Test activity")) + .build(); let first_begin_activity = storage.begin_activity(first_og_activity.clone())?; @@ -50,7 +56,9 @@ fn test_hold_resume_journey_for_activities_passes() -> TestResult<()> { // Now we create another activity, which should end the first one automatically - let second_og_activity = Activity::builder().description("Our new activity").build(); + let second_og_activity = Activity::builder() + .description(PaceDescription::new("Our new activity")) + .build(); let second_begin_activity = storage.begin_activity(second_og_activity.clone())?; diff --git a/crates/core/tests/journey/main.rs b/tests/journey/main.rs similarity index 97% rename from crates/core/tests/journey/main.rs rename to tests/journey/main.rs index 5156a1aa..ac8dea14 100644 --- a/crates/core/tests/journey/main.rs +++ b/tests/journey/main.rs @@ -1,3 +1,4 @@ +mod cli; mod hold_resume; mod start_finish_different_time_zone; diff --git a/tests/snapshots/cli__adjust_activity_snapshot_passes.snap b/tests/journey/snapshots/journey__cli__adjust_activity_snapshot_passes.snap similarity index 70% rename from tests/snapshots/cli__adjust_activity_snapshot_passes.snap rename to tests/journey/snapshots/journey__cli__adjust_activity_snapshot_passes.snap index 2f79f961..fdb6851d 100644 --- a/tests/snapshots/cli__adjust_activity_snapshot_passes.snap +++ b/tests/journey/snapshots/journey__cli__adjust_activity_snapshot_passes.snap @@ -1,12 +1,12 @@ --- -source: tests/cli.rs +source: tests/journey/cli.rs info: program: pace args: - "--config" - tests/fixtures/configs/pace.toml - - "--activity-log-file" - - "D:\\a\\pace\\pace\\./tests/generated\\.tmpzdxfi8\\activities.pace.toml" + - "--activity-log" + - "D:\\a\\pace\\pace\\./tests/generated\\.tmpeeQ1O3\\activities.pace.toml" - now --- success: true diff --git a/tests/snapshots/cli__begin_snapshot_passes.snap b/tests/journey/snapshots/journey__cli__begin_snapshot_passes.snap similarity index 76% rename from tests/snapshots/cli__begin_snapshot_passes.snap rename to tests/journey/snapshots/journey__cli__begin_snapshot_passes.snap index 102a55a2..19e25af1 100644 --- a/tests/snapshots/cli__begin_snapshot_passes.snap +++ b/tests/journey/snapshots/journey__cli__begin_snapshot_passes.snap @@ -1,12 +1,12 @@ --- -source: tests/cli.rs +source: tests/journey/cli.rs info: program: pace args: - "--config" - tests/fixtures/configs/pace.toml - - "--activity-log-file" - - "D:\\a\\pace\\pace\\./tests/generated\\.tmpwF7i1d\\activities.pace.toml" + - "--activity-log" + - "D:\\a\\pace\\pace\\./tests/generated\\.tmpg9o2J7\\activities.pace.toml" - begin - MyActivity - "--tags" diff --git a/tests/snapshots/cli__end_with_active_activity_snapshot_passes.snap b/tests/journey/snapshots/journey__cli__end_with_active_activity_snapshot_passes.snap similarity index 70% rename from tests/snapshots/cli__end_with_active_activity_snapshot_passes.snap rename to tests/journey/snapshots/journey__cli__end_with_active_activity_snapshot_passes.snap index 2785e08a..bac5dfdd 100644 --- a/tests/snapshots/cli__end_with_active_activity_snapshot_passes.snap +++ b/tests/journey/snapshots/journey__cli__end_with_active_activity_snapshot_passes.snap @@ -1,12 +1,12 @@ --- -source: tests/cli.rs +source: tests/journey/cli.rs info: program: pace args: - "--config" - tests/fixtures/configs/pace.toml - - "--activity-log-file" - - "D:\\a\\pace\\pace\\./tests/generated\\.tmp6Eme4G\\activities.pace.toml" + - "--activity-log" + - "D:\\a\\pace\\pace\\./tests/generated\\.tmp9oVkZI\\activities.pace.toml" - end --- success: true diff --git a/tests/snapshots/cli__hold_with_active_activity_snapshot_passes.snap b/tests/journey/snapshots/journey__cli__hold_with_active_activity_snapshot_passes.snap similarity index 70% rename from tests/snapshots/cli__hold_with_active_activity_snapshot_passes.snap rename to tests/journey/snapshots/journey__cli__hold_with_active_activity_snapshot_passes.snap index 1ad52678..15650b2b 100644 --- a/tests/snapshots/cli__hold_with_active_activity_snapshot_passes.snap +++ b/tests/journey/snapshots/journey__cli__hold_with_active_activity_snapshot_passes.snap @@ -1,12 +1,12 @@ --- -source: tests/cli.rs +source: tests/journey/cli.rs info: program: pace args: - "--config" - tests/fixtures/configs/pace.toml - - "--activity-log-file" - - "D:\\a\\pace\\pace\\./tests/generated\\.tmpBmC4F5\\activities.pace.toml" + - "--activity-log" + - "D:\\a\\pace\\pace\\./tests/generated\\.tmpXuhKFY\\activities.pace.toml" - hold --- success: true diff --git a/tests/journey/snapshots/journey__cli__now_no_activities_snapshot_passes.snap b/tests/journey/snapshots/journey__cli__now_no_activities_snapshot_passes.snap new file mode 100644 index 00000000..519ee04d --- /dev/null +++ b/tests/journey/snapshots/journey__cli__now_no_activities_snapshot_passes.snap @@ -0,0 +1,17 @@ +--- +source: tests/journey/cli.rs +info: + program: pace + args: + - "--config" + - tests/fixtures/configs/pace.toml + - "--activity-log" + - "D:\\a\\pace\\pace\\./tests/generated\\.tmpOOtqVl\\activities.pace.toml" + - now +--- +success: true +exit_code: 0 +----- stdout ----- +No activities are currently running. + +----- stderr ----- diff --git a/tests/snapshots/cli__now_with_active_activity_snapshot_passes.snap b/tests/journey/snapshots/journey__cli__now_with_active_activity_snapshot_passes.snap similarity index 69% rename from tests/snapshots/cli__now_with_active_activity_snapshot_passes.snap rename to tests/journey/snapshots/journey__cli__now_with_active_activity_snapshot_passes.snap index 24f28c90..a3fa973b 100644 --- a/tests/snapshots/cli__now_with_active_activity_snapshot_passes.snap +++ b/tests/journey/snapshots/journey__cli__now_with_active_activity_snapshot_passes.snap @@ -1,12 +1,12 @@ --- -source: tests/cli.rs +source: tests/journey/cli.rs info: program: pace args: - "--config" - tests/fixtures/configs/pace.toml - - "--activity-log-file" - - "D:\\a\\pace\\pace\\./tests/generated\\.tmpJC8IWS\\activities.pace.toml" + - "--activity-log" + - "D:\\a\\pace\\pace\\./tests/generated\\.tmp3ZXwGR\\activities.pace.toml" - now --- success: true diff --git a/tests/snapshots/cli__reflect_current_week_snapshot_passes.snap b/tests/journey/snapshots/journey__cli__reflect_current_month_snapshot_passes.snap similarity index 79% rename from tests/snapshots/cli__reflect_current_week_snapshot_passes.snap rename to tests/journey/snapshots/journey__cli__reflect_current_month_snapshot_passes.snap index 0307d06c..9bd6bbfb 100644 --- a/tests/snapshots/cli__reflect_current_week_snapshot_passes.snap +++ b/tests/journey/snapshots/journey__cli__reflect_current_month_snapshot_passes.snap @@ -1,14 +1,14 @@ --- -source: tests/cli.rs +source: tests/journey/cli.rs info: program: pace args: - "--config" - tests/fixtures/configs/pace.toml - - "--activity-log-file" + - "--activity-log" - "./tests/fixtures/activity_tracker/activities.pace.toml" - reflect - - "--current-week" + - current-month --- success: true exit_code: 0 diff --git a/tests/snapshots/cli__reflect_current_month_snapshot_passes.snap b/tests/journey/snapshots/journey__cli__reflect_current_week_snapshot_passes.snap similarity index 79% rename from tests/snapshots/cli__reflect_current_month_snapshot_passes.snap rename to tests/journey/snapshots/journey__cli__reflect_current_week_snapshot_passes.snap index 9c169953..170db283 100644 --- a/tests/snapshots/cli__reflect_current_month_snapshot_passes.snap +++ b/tests/journey/snapshots/journey__cli__reflect_current_week_snapshot_passes.snap @@ -1,14 +1,14 @@ --- -source: tests/cli.rs +source: tests/journey/cli.rs info: program: pace args: - "--config" - tests/fixtures/configs/pace.toml - - "--activity-log-file" + - "--activity-log" - "./tests/fixtures/activity_tracker/activities.pace.toml" - reflect - - "--current-month" + - current-week --- success: true exit_code: 0 diff --git a/tests/snapshots/cli__reflect_date_snapshot_passes.snap b/tests/journey/snapshots/journey__cli__reflect_date_snapshot_passes.snap similarity index 97% rename from tests/snapshots/cli__reflect_date_snapshot_passes.snap rename to tests/journey/snapshots/journey__cli__reflect_date_snapshot_passes.snap index e78f3ed3..96fd615d 100644 --- a/tests/snapshots/cli__reflect_date_snapshot_passes.snap +++ b/tests/journey/snapshots/journey__cli__reflect_date_snapshot_passes.snap @@ -1,11 +1,11 @@ --- -source: tests/cli.rs +source: tests/journey/cli.rs info: program: pace args: - "--config" - tests/fixtures/configs/pace.toml - - "--activity-log-file" + - "--activity-log" - "./tests/fixtures/activity_tracker/activities.pace.toml" - reflect - "--date" diff --git a/tests/snapshots/cli__reflect_from_to_filter_category_case_sensitive_snapshot_passes.snap b/tests/journey/snapshots/journey__cli__reflect_from_to_filter_category_case_sensitive_snapshot_passes.snap similarity index 96% rename from tests/snapshots/cli__reflect_from_to_filter_category_case_sensitive_snapshot_passes.snap rename to tests/journey/snapshots/journey__cli__reflect_from_to_filter_category_case_sensitive_snapshot_passes.snap index ff334dcf..ea822906 100644 --- a/tests/snapshots/cli__reflect_from_to_filter_category_case_sensitive_snapshot_passes.snap +++ b/tests/journey/snapshots/journey__cli__reflect_from_to_filter_category_case_sensitive_snapshot_passes.snap @@ -1,11 +1,11 @@ --- -source: tests/cli.rs +source: tests/journey/cli.rs info: program: pace args: - "--config" - tests/fixtures/configs/pace.toml - - "--activity-log-file" + - "--activity-log" - "./tests/fixtures/activity_tracker/activities.pace.toml" - reflect - "--from" diff --git a/tests/snapshots/cli__reflect_from_to_filter_category_full_snapshot_passes.snap b/tests/journey/snapshots/journey__cli__reflect_from_to_filter_category_full_snapshot_passes.snap similarity index 97% rename from tests/snapshots/cli__reflect_from_to_filter_category_full_snapshot_passes.snap rename to tests/journey/snapshots/journey__cli__reflect_from_to_filter_category_full_snapshot_passes.snap index da01f74e..a66f01f6 100644 --- a/tests/snapshots/cli__reflect_from_to_filter_category_full_snapshot_passes.snap +++ b/tests/journey/snapshots/journey__cli__reflect_from_to_filter_category_full_snapshot_passes.snap @@ -1,11 +1,11 @@ --- -source: tests/cli.rs +source: tests/journey/cli.rs info: program: pace args: - "--config" - tests/fixtures/configs/pace.toml - - "--activity-log-file" + - "--activity-log" - "./tests/fixtures/activity_tracker/activities.pace.toml" - reflect - "--from" diff --git a/tests/snapshots/cli__reflect_from_to_filter_category_glob_back_case_sensitive_snapshot_passes.snap b/tests/journey/snapshots/journey__cli__reflect_from_to_filter_category_glob_back_case_sensitive_snapshot_passes.snap similarity index 96% rename from tests/snapshots/cli__reflect_from_to_filter_category_glob_back_case_sensitive_snapshot_passes.snap rename to tests/journey/snapshots/journey__cli__reflect_from_to_filter_category_glob_back_case_sensitive_snapshot_passes.snap index f125891d..3407e0ab 100644 --- a/tests/snapshots/cli__reflect_from_to_filter_category_glob_back_case_sensitive_snapshot_passes.snap +++ b/tests/journey/snapshots/journey__cli__reflect_from_to_filter_category_glob_back_case_sensitive_snapshot_passes.snap @@ -1,11 +1,11 @@ --- -source: tests/cli.rs +source: tests/journey/cli.rs info: program: pace args: - "--config" - tests/fixtures/configs/pace.toml - - "--activity-log-file" + - "--activity-log" - "./tests/fixtures/activity_tracker/activities.pace.toml" - reflect - "--from" diff --git a/tests/snapshots/cli__reflect_from_to_filter_category_glob_back_snapshot_passes.snap b/tests/journey/snapshots/journey__cli__reflect_from_to_filter_category_glob_back_snapshot_passes.snap similarity index 98% rename from tests/snapshots/cli__reflect_from_to_filter_category_glob_back_snapshot_passes.snap rename to tests/journey/snapshots/journey__cli__reflect_from_to_filter_category_glob_back_snapshot_passes.snap index c237c4c7..d56fc703 100644 --- a/tests/snapshots/cli__reflect_from_to_filter_category_glob_back_snapshot_passes.snap +++ b/tests/journey/snapshots/journey__cli__reflect_from_to_filter_category_glob_back_snapshot_passes.snap @@ -1,11 +1,11 @@ --- -source: tests/cli.rs +source: tests/journey/cli.rs info: program: pace args: - "--config" - tests/fixtures/configs/pace.toml - - "--activity-log-file" + - "--activity-log" - "./tests/fixtures/activity_tracker/activities.pace.toml" - reflect - "--from" diff --git a/tests/snapshots/cli__reflect_from_to_filter_category_glob_front_snapshot_passes.snap b/tests/journey/snapshots/journey__cli__reflect_from_to_filter_category_glob_front_snapshot_passes.snap similarity index 97% rename from tests/snapshots/cli__reflect_from_to_filter_category_glob_front_snapshot_passes.snap rename to tests/journey/snapshots/journey__cli__reflect_from_to_filter_category_glob_front_snapshot_passes.snap index ca8549be..fad2e481 100644 --- a/tests/snapshots/cli__reflect_from_to_filter_category_glob_front_snapshot_passes.snap +++ b/tests/journey/snapshots/journey__cli__reflect_from_to_filter_category_glob_front_snapshot_passes.snap @@ -1,11 +1,11 @@ --- -source: tests/cli.rs +source: tests/journey/cli.rs info: program: pace args: - "--config" - tests/fixtures/configs/pace.toml - - "--activity-log-file" + - "--activity-log" - "./tests/fixtures/activity_tracker/activities.pace.toml" - reflect - "--from" diff --git a/tests/snapshots/cli__reflect_from_to_snapshot_passes.snap b/tests/journey/snapshots/journey__cli__reflect_from_to_snapshot_passes.snap similarity index 98% rename from tests/snapshots/cli__reflect_from_to_snapshot_passes.snap rename to tests/journey/snapshots/journey__cli__reflect_from_to_snapshot_passes.snap index 01cb4d18..7ed1f421 100644 --- a/tests/snapshots/cli__reflect_from_to_snapshot_passes.snap +++ b/tests/journey/snapshots/journey__cli__reflect_from_to_snapshot_passes.snap @@ -1,11 +1,11 @@ --- -source: tests/cli.rs +source: tests/journey/cli.rs info: program: pace args: - "--config" - tests/fixtures/configs/pace.toml - - "--activity-log-file" + - "--activity-log" - "./tests/fixtures/activity_tracker/activities.pace.toml" - reflect - "--from" diff --git a/tests/snapshots/cli__reflect_today_snapshot_passes.snap b/tests/journey/snapshots/journey__cli__reflect_today_snapshot_passes.snap similarity index 84% rename from tests/snapshots/cli__reflect_today_snapshot_passes.snap rename to tests/journey/snapshots/journey__cli__reflect_today_snapshot_passes.snap index c427e131..009cd635 100644 --- a/tests/snapshots/cli__reflect_today_snapshot_passes.snap +++ b/tests/journey/snapshots/journey__cli__reflect_today_snapshot_passes.snap @@ -1,11 +1,11 @@ --- -source: tests/cli.rs +source: tests/journey/cli.rs info: program: pace args: - "--config" - tests/fixtures/configs/pace.toml - - "--activity-log-file" + - "--activity-log" - "./tests/fixtures/activity_tracker/activities.pace.toml" - reflect --- diff --git a/tests/snapshots/cli__resume_with_held_activity_snapshot_passes.snap b/tests/journey/snapshots/journey__cli__resume_with_held_activity_snapshot_passes.snap similarity index 70% rename from tests/snapshots/cli__resume_with_held_activity_snapshot_passes.snap rename to tests/journey/snapshots/journey__cli__resume_with_held_activity_snapshot_passes.snap index c0de7f40..ea1f8c2d 100644 --- a/tests/snapshots/cli__resume_with_held_activity_snapshot_passes.snap +++ b/tests/journey/snapshots/journey__cli__resume_with_held_activity_snapshot_passes.snap @@ -1,12 +1,12 @@ --- -source: tests/cli.rs +source: tests/journey/cli.rs info: program: pace args: - "--config" - tests/fixtures/configs/pace.toml - - "--activity-log-file" - - "D:\\a\\pace\\pace\\./tests/generated\\.tmpA4sfe0\\activities.pace.toml" + - "--activity-log" + - "D:\\a\\pace\\pace\\./tests/generated\\.tmp5yU7TA\\activities.pace.toml" - resume --- success: true diff --git a/crates/core/tests/journey/start_finish_different_time_zone.rs b/tests/journey/start_finish_different_time_zone.rs similarity index 91% rename from crates/core/tests/journey/start_finish_different_time_zone.rs rename to tests/journey/start_finish_different_time_zone.rs index 84ce7a15..6ced2a58 100644 --- a/crates/core/tests/journey/start_finish_different_time_zone.rs +++ b/tests/journey/start_finish_different_time_zone.rs @@ -1,9 +1,11 @@ use chrono::FixedOffset; use eyre::OptionExt; -use pace_core::prelude::{ - Activity, ActivityReadOps, ActivityStateManagement, EndOptions, InMemoryActivityStorage, - TestResult, +use pace_core::{ + options::EndOptions, + prelude::{Activity, ActivityReadOps, ActivityStateManagement, PaceDescription}, }; +use pace_error::TestResult; +use pace_storage::storage::in_memory::InMemoryActivityStorage; use pace_time::{date_time::PaceDateTime, duration::PaceDuration}; #[test] @@ -15,7 +17,7 @@ fn test_begin_and_end_activity_in_different_time_zones_passes() -> TestResult<() let now = PaceDateTime::now(); let first_og_activity = Activity::builder() - .description("Our time zone") + .description(PaceDescription::new("Our time zone")) .begin(now) .build(); diff --git a/tests/snapshots/cli__now_no_activities_snapshot_passes.snap b/tests/snapshots/cli__now_no_activities_snapshot_passes.snap deleted file mode 100644 index 5e02fb8b..00000000 --- a/tests/snapshots/cli__now_no_activities_snapshot_passes.snap +++ /dev/null @@ -1,15 +0,0 @@ ---- -source: tests/cli.rs -info: - program: pace - args: - - "--activity-log-file" - - "C:\\Users\\dailyuse\\backup\\PARA\\Personal\\2 Areas\\Developer\\Maintenance\\pace\\pace-rs\\pace\\./tests/generated\\.tmpjGkvhs\\activities.pace.toml" - - now ---- -success: true -exit_code: 0 ------ stdout ----- -No activities are currently running. - ------ stderr ----- diff --git a/tests/snapshots/cli__version_snapshot_passes.snap b/tests/snapshots/cli__version_snapshot_passes.snap deleted file mode 100644 index 549df925..00000000 --- a/tests/snapshots/cli__version_snapshot_passes.snap +++ /dev/null @@ -1,13 +0,0 @@ ---- -source: tests/cli.rs -info: - program: pace - args: - - "--version" ---- -success: true -exit_code: 0 ------ stdout ----- -pace 0.11.0 - ------ stderr -----