├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── Tutorial ├── code │ ├── 03 │ │ └── rtd │ │ │ ├── Cargo.lock │ │ │ ├── Cargo.toml │ │ │ └── src │ │ │ └── main.rs │ ├── 04 │ │ └── rtd │ │ │ ├── Cargo.lock │ │ │ ├── Cargo.toml │ │ │ └── src │ │ │ ├── lib.rs │ │ │ ├── main.rs │ │ │ ├── model.rs │ │ │ ├── service.rs │ │ │ └── storage.rs │ └── 05 │ │ └── rtd │ │ ├── Cargo.lock │ │ ├── Cargo.toml │ │ └── src │ │ ├── lib.rs │ │ ├── main.rs │ │ ├── model.rs │ │ ├── service.rs │ │ └── storage.rs └── doc │ └── img │ ├── 01 │ ├── add_a_todo.jpeg │ ├── add_a_todo_v2.jpeg │ ├── add_a_todo_v3.jpeg │ ├── add_a_todo_v4.jpeg │ ├── complete_a_todo.jpeg │ ├── complete_a_todo_v2.jpeg │ ├── complete_a_todo_v3.jpeg │ ├── list_all_uncompleted_todos.jpeg │ └── list_all_uncompleted_todos_v2.jpeg │ ├── 03 │ ├── cargo_dir_after_run.png │ ├── cargo_new.png │ └── cargo_run.png │ ├── 04 │ └── cargo_run.png │ └── readme │ ├── csv.png │ ├── rtd_add.png │ ├── rtd_arch.jpg │ ├── rtd_arch.png │ ├── rtd_arch.svg │ ├── rtd_arch_zh.jpg │ ├── rtd_arch_zh.png │ ├── rtd_arch_zh.svg │ ├── rtd_clear.png │ ├── rtd_complete_item.png │ ├── rtd_delete_item.png │ ├── rtd_destroy_deleted.png │ ├── rtd_destroy_item.png │ ├── rtd_help.png │ ├── rtd_help_summary.png │ ├── rtd_list_all.png │ ├── rtd_list_completed.png │ ├── rtd_list_deleted.png │ ├── rtd_list_uncompleted.png │ ├── rtd_restore_item.png │ └── rtd_uncomplete_item.png ├── design.md ├── readme.md ├── readme_zh.md └── src ├── lib.rs ├── main.rs ├── model.rs ├── service.rs └── storage.rs /.gitignore: -------------------------------------------------------------------------------- 1 | **/target 2 | .vscode 3 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "android-tzdata" 7 | version = "0.1.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 10 | 11 | [[package]] 12 | name = "android_system_properties" 13 | version = "0.1.5" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 16 | dependencies = [ 17 | "libc", 18 | ] 19 | 20 | [[package]] 21 | name = "anstream" 22 | version = "0.3.2" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" 25 | dependencies = [ 26 | "anstyle", 27 | "anstyle-parse", 28 | "anstyle-query", 29 | "anstyle-wincon", 30 | "colorchoice", 31 | "is-terminal", 32 | "utf8parse", 33 | ] 34 | 35 | [[package]] 36 | name = "anstyle" 37 | version = "1.0.1" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" 40 | 41 | [[package]] 42 | name = "anstyle-parse" 43 | version = "0.2.1" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" 46 | dependencies = [ 47 | "utf8parse", 48 | ] 49 | 50 | [[package]] 51 | name = "anstyle-query" 52 | version = "1.0.0" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" 55 | dependencies = [ 56 | "windows-sys", 57 | ] 58 | 59 | [[package]] 60 | name = "anstyle-wincon" 61 | version = "1.0.1" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" 64 | dependencies = [ 65 | "anstyle", 66 | "windows-sys", 67 | ] 68 | 69 | [[package]] 70 | name = "autocfg" 71 | version = "1.1.0" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 74 | 75 | [[package]] 76 | name = "bitflags" 77 | version = "2.3.3" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" 80 | 81 | [[package]] 82 | name = "bumpalo" 83 | version = "3.13.0" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" 86 | 87 | [[package]] 88 | name = "cc" 89 | version = "1.0.79" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" 92 | 93 | [[package]] 94 | name = "cfg-if" 95 | version = "1.0.0" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 98 | 99 | [[package]] 100 | name = "chrono" 101 | version = "0.4.26" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" 104 | dependencies = [ 105 | "android-tzdata", 106 | "iana-time-zone", 107 | "js-sys", 108 | "num-traits", 109 | "time", 110 | "wasm-bindgen", 111 | "winapi", 112 | ] 113 | 114 | [[package]] 115 | name = "clap" 116 | version = "4.3.19" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "5fd304a20bff958a57f04c4e96a2e7594cc4490a0e809cbd48bb6437edaa452d" 119 | dependencies = [ 120 | "clap_builder", 121 | "clap_derive", 122 | "once_cell", 123 | ] 124 | 125 | [[package]] 126 | name = "clap_builder" 127 | version = "4.3.19" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "01c6a3f08f1fe5662a35cfe393aec09c4df95f60ee93b7556505260f75eee9e1" 130 | dependencies = [ 131 | "anstream", 132 | "anstyle", 133 | "clap_lex", 134 | "strsim", 135 | ] 136 | 137 | [[package]] 138 | name = "clap_derive" 139 | version = "4.3.12" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050" 142 | dependencies = [ 143 | "heck", 144 | "proc-macro2", 145 | "quote", 146 | "syn", 147 | ] 148 | 149 | [[package]] 150 | name = "clap_lex" 151 | version = "0.5.0" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" 154 | 155 | [[package]] 156 | name = "colorchoice" 157 | version = "1.0.0" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 160 | 161 | [[package]] 162 | name = "core-foundation-sys" 163 | version = "0.8.4" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" 166 | 167 | [[package]] 168 | name = "errno" 169 | version = "0.3.1" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" 172 | dependencies = [ 173 | "errno-dragonfly", 174 | "libc", 175 | "windows-sys", 176 | ] 177 | 178 | [[package]] 179 | name = "errno-dragonfly" 180 | version = "0.1.2" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 183 | dependencies = [ 184 | "cc", 185 | "libc", 186 | ] 187 | 188 | [[package]] 189 | name = "heck" 190 | version = "0.4.1" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 193 | 194 | [[package]] 195 | name = "hermit-abi" 196 | version = "0.3.2" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" 199 | 200 | [[package]] 201 | name = "iana-time-zone" 202 | version = "0.1.57" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" 205 | dependencies = [ 206 | "android_system_properties", 207 | "core-foundation-sys", 208 | "iana-time-zone-haiku", 209 | "js-sys", 210 | "wasm-bindgen", 211 | "windows", 212 | ] 213 | 214 | [[package]] 215 | name = "iana-time-zone-haiku" 216 | version = "0.1.2" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 219 | dependencies = [ 220 | "cc", 221 | ] 222 | 223 | [[package]] 224 | name = "is-terminal" 225 | version = "0.4.9" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" 228 | dependencies = [ 229 | "hermit-abi", 230 | "rustix", 231 | "windows-sys", 232 | ] 233 | 234 | [[package]] 235 | name = "js-sys" 236 | version = "0.3.64" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" 239 | dependencies = [ 240 | "wasm-bindgen", 241 | ] 242 | 243 | [[package]] 244 | name = "libc" 245 | version = "0.2.147" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" 248 | 249 | [[package]] 250 | name = "linux-raw-sys" 251 | version = "0.4.3" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" 254 | 255 | [[package]] 256 | name = "log" 257 | version = "0.4.19" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" 260 | 261 | [[package]] 262 | name = "num-traits" 263 | version = "0.2.16" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" 266 | dependencies = [ 267 | "autocfg", 268 | ] 269 | 270 | [[package]] 271 | name = "once_cell" 272 | version = "1.18.0" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" 275 | 276 | [[package]] 277 | name = "proc-macro2" 278 | version = "1.0.66" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" 281 | dependencies = [ 282 | "unicode-ident", 283 | ] 284 | 285 | [[package]] 286 | name = "quote" 287 | version = "1.0.32" 288 | source = "registry+https://github.com/rust-lang/crates.io-index" 289 | checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" 290 | dependencies = [ 291 | "proc-macro2", 292 | ] 293 | 294 | [[package]] 295 | name = "rtd-tutorial" 296 | version = "0.1.23" 297 | dependencies = [ 298 | "chrono", 299 | "clap", 300 | ] 301 | 302 | [[package]] 303 | name = "rustix" 304 | version = "0.38.4" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5" 307 | dependencies = [ 308 | "bitflags", 309 | "errno", 310 | "libc", 311 | "linux-raw-sys", 312 | "windows-sys", 313 | ] 314 | 315 | [[package]] 316 | name = "strsim" 317 | version = "0.10.0" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 320 | 321 | [[package]] 322 | name = "syn" 323 | version = "2.0.27" 324 | source = "registry+https://github.com/rust-lang/crates.io-index" 325 | checksum = "b60f673f44a8255b9c8c657daf66a596d435f2da81a555b06dc644d080ba45e0" 326 | dependencies = [ 327 | "proc-macro2", 328 | "quote", 329 | "unicode-ident", 330 | ] 331 | 332 | [[package]] 333 | name = "time" 334 | version = "0.1.45" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" 337 | dependencies = [ 338 | "libc", 339 | "wasi", 340 | "winapi", 341 | ] 342 | 343 | [[package]] 344 | name = "unicode-ident" 345 | version = "1.0.11" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" 348 | 349 | [[package]] 350 | name = "utf8parse" 351 | version = "0.2.1" 352 | source = "registry+https://github.com/rust-lang/crates.io-index" 353 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 354 | 355 | [[package]] 356 | name = "wasi" 357 | version = "0.10.0+wasi-snapshot-preview1" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" 360 | 361 | [[package]] 362 | name = "wasm-bindgen" 363 | version = "0.2.87" 364 | source = "registry+https://github.com/rust-lang/crates.io-index" 365 | checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" 366 | dependencies = [ 367 | "cfg-if", 368 | "wasm-bindgen-macro", 369 | ] 370 | 371 | [[package]] 372 | name = "wasm-bindgen-backend" 373 | version = "0.2.87" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" 376 | dependencies = [ 377 | "bumpalo", 378 | "log", 379 | "once_cell", 380 | "proc-macro2", 381 | "quote", 382 | "syn", 383 | "wasm-bindgen-shared", 384 | ] 385 | 386 | [[package]] 387 | name = "wasm-bindgen-macro" 388 | version = "0.2.87" 389 | source = "registry+https://github.com/rust-lang/crates.io-index" 390 | checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" 391 | dependencies = [ 392 | "quote", 393 | "wasm-bindgen-macro-support", 394 | ] 395 | 396 | [[package]] 397 | name = "wasm-bindgen-macro-support" 398 | version = "0.2.87" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" 401 | dependencies = [ 402 | "proc-macro2", 403 | "quote", 404 | "syn", 405 | "wasm-bindgen-backend", 406 | "wasm-bindgen-shared", 407 | ] 408 | 409 | [[package]] 410 | name = "wasm-bindgen-shared" 411 | version = "0.2.87" 412 | source = "registry+https://github.com/rust-lang/crates.io-index" 413 | checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" 414 | 415 | [[package]] 416 | name = "winapi" 417 | version = "0.3.9" 418 | source = "registry+https://github.com/rust-lang/crates.io-index" 419 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 420 | dependencies = [ 421 | "winapi-i686-pc-windows-gnu", 422 | "winapi-x86_64-pc-windows-gnu", 423 | ] 424 | 425 | [[package]] 426 | name = "winapi-i686-pc-windows-gnu" 427 | version = "0.4.0" 428 | source = "registry+https://github.com/rust-lang/crates.io-index" 429 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 430 | 431 | [[package]] 432 | name = "winapi-x86_64-pc-windows-gnu" 433 | version = "0.4.0" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 436 | 437 | [[package]] 438 | name = "windows" 439 | version = "0.48.0" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" 442 | dependencies = [ 443 | "windows-targets", 444 | ] 445 | 446 | [[package]] 447 | name = "windows-sys" 448 | version = "0.48.0" 449 | source = "registry+https://github.com/rust-lang/crates.io-index" 450 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 451 | dependencies = [ 452 | "windows-targets", 453 | ] 454 | 455 | [[package]] 456 | name = "windows-targets" 457 | version = "0.48.1" 458 | source = "registry+https://github.com/rust-lang/crates.io-index" 459 | checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" 460 | dependencies = [ 461 | "windows_aarch64_gnullvm", 462 | "windows_aarch64_msvc", 463 | "windows_i686_gnu", 464 | "windows_i686_msvc", 465 | "windows_x86_64_gnu", 466 | "windows_x86_64_gnullvm", 467 | "windows_x86_64_msvc", 468 | ] 469 | 470 | [[package]] 471 | name = "windows_aarch64_gnullvm" 472 | version = "0.48.0" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" 475 | 476 | [[package]] 477 | name = "windows_aarch64_msvc" 478 | version = "0.48.0" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" 481 | 482 | [[package]] 483 | name = "windows_i686_gnu" 484 | version = "0.48.0" 485 | source = "registry+https://github.com/rust-lang/crates.io-index" 486 | checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" 487 | 488 | [[package]] 489 | name = "windows_i686_msvc" 490 | version = "0.48.0" 491 | source = "registry+https://github.com/rust-lang/crates.io-index" 492 | checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" 493 | 494 | [[package]] 495 | name = "windows_x86_64_gnu" 496 | version = "0.48.0" 497 | source = "registry+https://github.com/rust-lang/crates.io-index" 498 | checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" 499 | 500 | [[package]] 501 | name = "windows_x86_64_gnullvm" 502 | version = "0.48.0" 503 | source = "registry+https://github.com/rust-lang/crates.io-index" 504 | checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" 505 | 506 | [[package]] 507 | name = "windows_x86_64_msvc" 508 | version = "0.48.0" 509 | source = "registry+https://github.com/rust-lang/crates.io-index" 510 | checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" 511 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rtd-tutorial" 3 | version = "0.1.23" 4 | edition = "2021" 5 | license = "MIT" 6 | description = "Learn Rust by 500 lines code, build a Rust ToDo cli tool." 7 | homepage = "https://github.com/cuppar/rtd" 8 | documentation = "https://github.com/cuppar/rtd/wiki" 9 | repository = "https://github.com/cuppar/rtd" 10 | readme = "readme.md" 11 | keywords = [ 12 | "tutorial", 13 | "guide", 14 | "todo", 15 | "learning-by-doing", 16 | "step-by-step-guide", 17 | ] 18 | categories = ["command-line-utilities"] 19 | 20 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 21 | 22 | [dependencies] 23 | chrono = "0.4.26" 24 | clap = { version = "4.3.19", features = ["derive"] } 25 | 26 | [[bin]] 27 | name = "rtd" 28 | path = "src/main.rs" 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Tutorial/code/03/rtd/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "rtd" 7 | version = "0.1.0" 8 | -------------------------------------------------------------------------------- /Tutorial/code/03/rtd/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rtd" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | -------------------------------------------------------------------------------- /Tutorial/code/03/rtd/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("Hello, world!"); 3 | } 4 | -------------------------------------------------------------------------------- /Tutorial/code/04/rtd/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "rtd" 7 | version = "0.1.0" 8 | -------------------------------------------------------------------------------- /Tutorial/code/04/rtd/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rtd" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | -------------------------------------------------------------------------------- /Tutorial/code/04/rtd/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod service; 2 | mod model; 3 | mod storage; 4 | 5 | pub use service::*; 6 | -------------------------------------------------------------------------------- /Tutorial/code/04/rtd/src/main.rs: -------------------------------------------------------------------------------- 1 | use rtd::add_item; 2 | 3 | fn main() { 4 | add_item(); 5 | } 6 | -------------------------------------------------------------------------------- /Tutorial/code/04/rtd/src/model.rs: -------------------------------------------------------------------------------- 1 | #[allow(unused)] 2 | pub struct Item { 3 | name: String, 4 | } 5 | 6 | #[allow(unused)] 7 | impl Item { 8 | pub fn new() { 9 | todo!("implement new function"); 10 | } 11 | 12 | pub fn to_prettier_string() { 13 | todo!(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Tutorial/code/04/rtd/src/service.rs: -------------------------------------------------------------------------------- 1 | use crate::storage::{self, add_item as storage_add_item, update_item}; 2 | 3 | pub fn add_item() { 4 | storage_add_item(); 5 | } 6 | 7 | pub fn complete_item() { 8 | update_item(); 9 | } 10 | 11 | pub fn uncomplete_item() { 12 | update_item(); 13 | } 14 | 15 | pub fn delete_item() { 16 | update_item(); 17 | } 18 | 19 | pub fn restore_item() { 20 | update_item(); 21 | } 22 | 23 | pub fn destroy_deleted() { 24 | storage::delete_item(); 25 | } 26 | 27 | pub fn destroy_item() { 28 | storage::delete_item(); 29 | } 30 | 31 | pub fn clear() { 32 | storage::delete_item(); 33 | } 34 | 35 | pub fn list_uncompleted() { 36 | storage::get_all(); 37 | } 38 | 39 | pub fn list_completed() { 40 | storage::get_all(); 41 | } 42 | 43 | pub fn list_deleted() { 44 | storage::get_all(); 45 | } 46 | 47 | pub fn list_all() { 48 | storage::get_all(); 49 | } 50 | -------------------------------------------------------------------------------- /Tutorial/code/04/rtd/src/storage.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | 3 | use std::fs::File; 4 | 5 | pub fn add_item() { 6 | println!("add_item"); 7 | } 8 | 9 | pub fn update_item() { 10 | println!("update_item"); 11 | } 12 | 13 | pub fn delete_item() {} 14 | 15 | pub fn get_all() {} 16 | 17 | pub fn get_item_by_id() {} 18 | 19 | pub fn get_max_id() {} 20 | 21 | struct Csv { 22 | filename: String, 23 | file: File, 24 | } 25 | 26 | impl Csv { 27 | fn new() {} 28 | 29 | fn filename() {} 30 | 31 | fn content() {} 32 | 33 | fn splice() {} 34 | } 35 | -------------------------------------------------------------------------------- /Tutorial/code/05/rtd/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "rtd" 7 | version = "0.1.0" 8 | -------------------------------------------------------------------------------- /Tutorial/code/05/rtd/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rtd" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | -------------------------------------------------------------------------------- /Tutorial/code/05/rtd/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod service; 2 | mod model; 3 | mod storage; 4 | 5 | pub use service::*; 6 | -------------------------------------------------------------------------------- /Tutorial/code/05/rtd/src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | use rtd::add_item; 3 | 4 | fn main() { 5 | let result_message = add_item("todo name"); 6 | } 7 | -------------------------------------------------------------------------------- /Tutorial/code/05/rtd/src/model.rs: -------------------------------------------------------------------------------- 1 | #[allow(unused)] 2 | pub struct Item { 3 | pub(crate) id: u32, 4 | pub(crate) name: String, 5 | pub(crate) completed: bool, 6 | pub(crate) deleted: bool, 7 | pub(crate) created_at: Option, 8 | pub(crate) completed_at: Option, 9 | pub(crate) deleted_at: Option, 10 | } 11 | 12 | #[allow(unused)] 13 | impl Item { 14 | /// Associated Functions, 15 | /// which first parameter is NOT `&self(self: &Self)`, 16 | /// `&mut self(self: &mut Self)` or `self(self: Self)` 17 | pub fn new( 18 | id: u32, 19 | name: &str, 20 | completed: bool, 21 | deleted: bool, 22 | created_at: Option, 23 | completed_at: Option, 24 | deleted_at: Option, 25 | ) -> Self { 26 | todo!("implement new function"); 27 | } 28 | 29 | /// methods, 30 | /// which first parameter is `&self(self: &Self)`, 31 | /// `&mut self(self: &mut Self)` or `self(self: Self)` 32 | /// if `id` property is not `pub` 33 | /// we can use methods to support `getter` or `setter` 34 | pub fn id(&self) -> u32 { 35 | self.id 36 | } 37 | 38 | pub fn to_prettier_string(&self) -> String { 39 | todo!(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Tutorial/code/05/rtd/src/service.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | use crate::storage::{self, add_item as storage_add_item, update_item}; 3 | 4 | pub fn add_item(name: &str) -> Result { 5 | todo!(); 6 | } 7 | 8 | pub fn complete_item(id: u32) -> Result { 9 | todo!(); 10 | } 11 | 12 | pub fn uncomplete_item(id: u32) -> Result { 13 | todo!(); 14 | } 15 | 16 | pub fn delete_item(id: u32) -> Result { 17 | todo!(); 18 | } 19 | 20 | pub fn restore_item(id: u32) -> Result { 21 | todo!(); 22 | } 23 | 24 | pub fn destroy_deleted() -> Result { 25 | todo!(); 26 | } 27 | 28 | pub fn destroy_item(id: u32) -> Result { 29 | todo!(); 30 | } 31 | 32 | pub fn clear() -> Result { 33 | todo!(); 34 | } 35 | 36 | pub fn list_uncompleted() -> Result { 37 | storage::get_all(); 38 | todo!(); 39 | } 40 | 41 | pub fn list_completed() -> Result { 42 | storage::get_all(); 43 | todo!(); 44 | } 45 | 46 | pub fn list_deleted() -> Result { 47 | storage::get_all(); 48 | todo!(); 49 | } 50 | 51 | pub fn list_all() -> Result { 52 | storage::get_all(); 53 | todo!(); 54 | } 55 | 56 | type Result = std::result::Result; 57 | 58 | #[derive(Debug)] 59 | pub enum ServiceError { 60 | } -------------------------------------------------------------------------------- /Tutorial/code/05/rtd/src/storage.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | use crate::model::{self, *}; 3 | use std::fs::File; 4 | 5 | pub fn add_item(item: Item) -> Result<()> { 6 | println!("add_item"); 7 | todo!(); 8 | } 9 | 10 | pub fn update_item(item: Item) -> Result<()> { 11 | println!("update_item"); 12 | todo!(); 13 | } 14 | 15 | pub fn delete_item(id: u32) -> Result<()> { 16 | 17 | todo!(); 18 | } 19 | 20 | pub fn get_all() -> Result> { 21 | todo!(); 22 | } 23 | 24 | pub fn get_item_by_id(id: u32) -> Result { 25 | todo!(); 26 | } 27 | 28 | pub fn get_max_id() -> Result { 29 | todo!(); 30 | } 31 | 32 | struct Csv { 33 | filename: String, 34 | file: File, 35 | } 36 | 37 | impl Csv { 38 | fn new() -> Result { 39 | todo!(); 40 | } 41 | 42 | fn filename() -> Result { 43 | todo!(); 44 | } 45 | 46 | fn content(&mut self) -> Result { 47 | todo!(); 48 | } 49 | 50 | fn splice(&mut self, offset: u64, delete_size: u64, write_content: &str) -> Result<()> { 51 | todo!(); 52 | } 53 | } 54 | 55 | type Result = std::result::Result; 56 | 57 | #[derive(Debug)] 58 | pub enum StorageError { 59 | } -------------------------------------------------------------------------------- /Tutorial/doc/img/01/add_a_todo.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cuppar/rtd/6934787bd3a0c877afd2e6e2498c3ba24340ca13/Tutorial/doc/img/01/add_a_todo.jpeg -------------------------------------------------------------------------------- /Tutorial/doc/img/01/add_a_todo_v2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cuppar/rtd/6934787bd3a0c877afd2e6e2498c3ba24340ca13/Tutorial/doc/img/01/add_a_todo_v2.jpeg -------------------------------------------------------------------------------- /Tutorial/doc/img/01/add_a_todo_v3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cuppar/rtd/6934787bd3a0c877afd2e6e2498c3ba24340ca13/Tutorial/doc/img/01/add_a_todo_v3.jpeg -------------------------------------------------------------------------------- /Tutorial/doc/img/01/add_a_todo_v4.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cuppar/rtd/6934787bd3a0c877afd2e6e2498c3ba24340ca13/Tutorial/doc/img/01/add_a_todo_v4.jpeg -------------------------------------------------------------------------------- /Tutorial/doc/img/01/complete_a_todo.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cuppar/rtd/6934787bd3a0c877afd2e6e2498c3ba24340ca13/Tutorial/doc/img/01/complete_a_todo.jpeg -------------------------------------------------------------------------------- /Tutorial/doc/img/01/complete_a_todo_v2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cuppar/rtd/6934787bd3a0c877afd2e6e2498c3ba24340ca13/Tutorial/doc/img/01/complete_a_todo_v2.jpeg -------------------------------------------------------------------------------- /Tutorial/doc/img/01/complete_a_todo_v3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cuppar/rtd/6934787bd3a0c877afd2e6e2498c3ba24340ca13/Tutorial/doc/img/01/complete_a_todo_v3.jpeg -------------------------------------------------------------------------------- /Tutorial/doc/img/01/list_all_uncompleted_todos.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cuppar/rtd/6934787bd3a0c877afd2e6e2498c3ba24340ca13/Tutorial/doc/img/01/list_all_uncompleted_todos.jpeg -------------------------------------------------------------------------------- /Tutorial/doc/img/01/list_all_uncompleted_todos_v2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cuppar/rtd/6934787bd3a0c877afd2e6e2498c3ba24340ca13/Tutorial/doc/img/01/list_all_uncompleted_todos_v2.jpeg -------------------------------------------------------------------------------- /Tutorial/doc/img/03/cargo_dir_after_run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cuppar/rtd/6934787bd3a0c877afd2e6e2498c3ba24340ca13/Tutorial/doc/img/03/cargo_dir_after_run.png -------------------------------------------------------------------------------- /Tutorial/doc/img/03/cargo_new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cuppar/rtd/6934787bd3a0c877afd2e6e2498c3ba24340ca13/Tutorial/doc/img/03/cargo_new.png -------------------------------------------------------------------------------- /Tutorial/doc/img/03/cargo_run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cuppar/rtd/6934787bd3a0c877afd2e6e2498c3ba24340ca13/Tutorial/doc/img/03/cargo_run.png -------------------------------------------------------------------------------- /Tutorial/doc/img/04/cargo_run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cuppar/rtd/6934787bd3a0c877afd2e6e2498c3ba24340ca13/Tutorial/doc/img/04/cargo_run.png -------------------------------------------------------------------------------- /Tutorial/doc/img/readme/csv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cuppar/rtd/6934787bd3a0c877afd2e6e2498c3ba24340ca13/Tutorial/doc/img/readme/csv.png -------------------------------------------------------------------------------- /Tutorial/doc/img/readme/rtd_add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cuppar/rtd/6934787bd3a0c877afd2e6e2498c3ba24340ca13/Tutorial/doc/img/readme/rtd_add.png -------------------------------------------------------------------------------- /Tutorial/doc/img/readme/rtd_arch.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cuppar/rtd/6934787bd3a0c877afd2e6e2498c3ba24340ca13/Tutorial/doc/img/readme/rtd_arch.jpg -------------------------------------------------------------------------------- /Tutorial/doc/img/readme/rtd_arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cuppar/rtd/6934787bd3a0c877afd2e6e2498c3ba24340ca13/Tutorial/doc/img/readme/rtd_arch.png -------------------------------------------------------------------------------- /Tutorial/doc/img/readme/rtd_arch.svg: -------------------------------------------------------------------------------- 1 | Running EnvironmentLinuxMacOSWindowsData ModelBusinessAdd todoComplete todoSerializationAdd to recycle binClear recycle binList all todos… …Command Line InterfaceCall library Interfacehelp docsCLI arguments parseError handleApplication InterfaceExpose interfaces through the Rust libraryModel MappingData StructureDisplay FormatDeserializationbinary crateData StoreLocal CSV Filelibrary crateOSmain.rslib.rsservice.rsmodel.rsstorage.rsFile I/OORMLearn Rust by 500 lines code: https://github.com/cuppar/rtdGive me a star!English/Chinese -------------------------------------------------------------------------------- /Tutorial/doc/img/readme/rtd_arch_zh.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cuppar/rtd/6934787bd3a0c877afd2e6e2498c3ba24340ca13/Tutorial/doc/img/readme/rtd_arch_zh.jpg -------------------------------------------------------------------------------- /Tutorial/doc/img/readme/rtd_arch_zh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cuppar/rtd/6934787bd3a0c877afd2e6e2498c3ba24340ca13/Tutorial/doc/img/readme/rtd_arch_zh.png -------------------------------------------------------------------------------- /Tutorial/doc/img/readme/rtd_arch_zh.svg: -------------------------------------------------------------------------------- 1 | 运行环境LinuxMacOSWindows数据模型业务逻辑添加todo完成todo序列化放入回收站清空回收站列出todo列表… …命令行接口调用lib接口帮助文档参数解析错误处理应用接口通过Rust Library暴露接口模型映射数据结构数据显示定义反序列化binary crate数据存储本地CSV文件library crateOSmain.rslib.rsservice.rsmodel.rsstorage.rs文件读写ORM500行代码学会Rust教程: https://github.com/cuppar/rtdStar!中英双语 -------------------------------------------------------------------------------- /Tutorial/doc/img/readme/rtd_clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cuppar/rtd/6934787bd3a0c877afd2e6e2498c3ba24340ca13/Tutorial/doc/img/readme/rtd_clear.png -------------------------------------------------------------------------------- /Tutorial/doc/img/readme/rtd_complete_item.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cuppar/rtd/6934787bd3a0c877afd2e6e2498c3ba24340ca13/Tutorial/doc/img/readme/rtd_complete_item.png -------------------------------------------------------------------------------- /Tutorial/doc/img/readme/rtd_delete_item.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cuppar/rtd/6934787bd3a0c877afd2e6e2498c3ba24340ca13/Tutorial/doc/img/readme/rtd_delete_item.png -------------------------------------------------------------------------------- /Tutorial/doc/img/readme/rtd_destroy_deleted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cuppar/rtd/6934787bd3a0c877afd2e6e2498c3ba24340ca13/Tutorial/doc/img/readme/rtd_destroy_deleted.png -------------------------------------------------------------------------------- /Tutorial/doc/img/readme/rtd_destroy_item.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cuppar/rtd/6934787bd3a0c877afd2e6e2498c3ba24340ca13/Tutorial/doc/img/readme/rtd_destroy_item.png -------------------------------------------------------------------------------- /Tutorial/doc/img/readme/rtd_help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cuppar/rtd/6934787bd3a0c877afd2e6e2498c3ba24340ca13/Tutorial/doc/img/readme/rtd_help.png -------------------------------------------------------------------------------- /Tutorial/doc/img/readme/rtd_help_summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cuppar/rtd/6934787bd3a0c877afd2e6e2498c3ba24340ca13/Tutorial/doc/img/readme/rtd_help_summary.png -------------------------------------------------------------------------------- /Tutorial/doc/img/readme/rtd_list_all.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cuppar/rtd/6934787bd3a0c877afd2e6e2498c3ba24340ca13/Tutorial/doc/img/readme/rtd_list_all.png -------------------------------------------------------------------------------- /Tutorial/doc/img/readme/rtd_list_completed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cuppar/rtd/6934787bd3a0c877afd2e6e2498c3ba24340ca13/Tutorial/doc/img/readme/rtd_list_completed.png -------------------------------------------------------------------------------- /Tutorial/doc/img/readme/rtd_list_deleted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cuppar/rtd/6934787bd3a0c877afd2e6e2498c3ba24340ca13/Tutorial/doc/img/readme/rtd_list_deleted.png -------------------------------------------------------------------------------- /Tutorial/doc/img/readme/rtd_list_uncompleted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cuppar/rtd/6934787bd3a0c877afd2e6e2498c3ba24340ca13/Tutorial/doc/img/readme/rtd_list_uncompleted.png -------------------------------------------------------------------------------- /Tutorial/doc/img/readme/rtd_restore_item.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cuppar/rtd/6934787bd3a0c877afd2e6e2498c3ba24340ca13/Tutorial/doc/img/readme/rtd_restore_item.png -------------------------------------------------------------------------------- /Tutorial/doc/img/readme/rtd_uncomplete_item.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cuppar/rtd/6934787bd3a0c877afd2e6e2498c3ba24340ca13/Tutorial/doc/img/readme/rtd_uncomplete_item.png -------------------------------------------------------------------------------- /design.md: -------------------------------------------------------------------------------- 1 | # features 2 | - add a item 3 | - cmd: rtd -a(--add) 4 | - add a item to list, default uncompleted, undeleted, gen a id 5 | - complete a item 6 | - cmd: rtd -c(--complete) 7 | - uncomplete a item 8 | - cmd: rtd -u(--uncomplete) 9 | - delete a item 10 | - cmd: rtd -d(--delete) 11 | - mark a item to deleted 12 | - restore a deleted item 13 | - cmd: rtd -r(--restore) 14 | - restore a deleted marked item 15 | - destroy 16 | - cmd: rtd --destroy 17 | - destroy a item 18 | - destroy deleted 19 | - cmd: rtd --destroy-deleted 20 | - destroy all deleted item 21 | - clear 22 | - cmd: rtd --clear 23 | - clear all items, make list empty 24 | - list all item 25 | - cmd: rtd -l all(--list all) 26 | - list all items(completed,uncompleted,deleted) 27 | - list all uncompleted item 28 | - cmd: rtd -l(--list) 29 | - list all uncompleted items 30 | - list all completed item 31 | - cmd: rtd -l completed(--list completed) 32 | - list all completed items 33 | - list all deleted item 34 | - cmd: rtd -l deleted(--list deleted) 35 | - list all deleted items 36 | 37 | # storage 38 | save to a `${HOME}/.rtd.csv` file 39 | 40 | |id|name|completed|deleted|createdAt|completedAt|deletedAt| 41 | |-|-|-|-|-|-|-| 42 | |1|homework|false|false|234234234|234234234|234234234| 43 | |2|todo item|false|false|234234234|234234234|234234234| 44 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Learn Rust by 500 lines code 2 | [![crate_version](https://img.shields.io/crates/v/rtd-tutorial)](https://crates.io/crates/rtd-tutorial) 3 | [![crate_downloads](https://img.shields.io/crates/d/rtd-tutorial?color=blue)](https://crates.io/crates/rtd-tutorial) 4 | [![license](https://img.shields.io/crates/l/rtd-tutorial?color=red)](https://github.com/cuppar/rtd/blob/master/LICENSE) 5 | 6 | English | [中文](https://github.com/cuppar/rtd/blob/master/readme_zh.md#500%E8%A1%8C%E4%BB%A3%E7%A0%81%E5%AD%A6%E4%BC%9Arust) 7 | 8 | - RTD (Rust To Do) is a todo app cli tool write by 500 lines Rust code. (exclude space lines/comments/long line break display/test code) 9 | - RTD is also a [tutorial](https://github.com/cuppar/rtd/wiki), designed to learn Rust by doing. 10 | 11 | ![rtd_list_all](Tutorial/doc/img/readme/rtd_list_all.png) 12 | 13 | ## Table of contents 14 | 15 | - [What can you learn from rtd?](#what-can-you-learn-from-rtd) 16 | - [Prepare](#prepare) 17 | - [Install](#install) 18 | - [Usage](#usage) 19 | - [About author](#about-author) 20 | 21 | --- 22 | 23 | ## What can you learn from RTD? 24 | 25 | #### Assuming you know nothing about Rust, through the [Learn Rust by 500 lines code](https://github.com/cuppar/rtd/wiki) tutorial, step by step to build the project from scratch, you will learn: 26 | - Rust common syntax 27 | - Rust module system (`package`/`binary crate`/`library crate`/`mod`/`pub`/`use`) 28 | - Rust ownership model (Bernstein conditions) 29 | - Rust lifetime 30 | - Rust error/null handling model (`Result`/`Option`) 31 | - Rust generic 32 | - Rust pattern matching (`if let` , `match` ...) 33 | - Rust unit test 34 | - Rust file I/O (`File Seek`/`Buffed File I/O`) 35 | - Rust manipulating environment variables 36 | - Rust command line parameter parsing 37 | - Rust release package to crate.io 38 | - The general process of developing a program (requirements/design/implementation/testing/release/maintenance) 39 | - Layered abstraction (data storage layer/model mapping layer/data model layer/business logic layer/application interface layer/user interface layer) 40 | - Handwritten serialization/deserialization from scratch 41 | - Implement the recycle bin function (virtualization concept) 42 | 43 | ###### Architecture 44 | 45 | ![rtd_arch](Tutorial/doc/img/readme/rtd_arch.svg) 46 | 47 | ###### Storage 48 | Use a local file `$HOME/.rtd.csv` store all data 49 | 50 | ![data_storage](Tutorial/doc/img/readme/csv.png) 51 | ![rtd_list_all](Tutorial/doc/img/readme/rtd_list_all.png) 52 | 53 | #### After completing this tutorial or installing RTD directly, you will get: 54 | - Extremely lightweight and concise todo command line application 55 | - Cross-platform, Rust's excellent build system naturally supports cross-platform 56 | - Safe, supports recycle bin, completely local, no network, no database 57 | - All data storage uses only one local csv file, which can be switched between different machines by copying the csv file 58 | 59 | --- 60 | 61 | Interesting fact: I use RTD to complete the RTD tutorial :) 62 | 63 | If you like my tutorial, don't forget to give me a star~ 64 | 65 | --- 66 | 67 | ## Prepare 68 | 69 | - Rust is naturally cross-platform. This project is built and tested in the `linux` environment, and `Windows`/`MacOS` are also supported. You can choose to download the corresponding `Cargo` according to your own operating system. 70 | - `Cargo` Rust's package management and build tool, can be installed directly through the [Rust official website](https://www.rust-lang.org/tools/install) `rustup` one line command. Then, all things will be done by `Cargo`, so cute, right? 71 | 72 | ## Install 73 | 74 | #### Via `crate.io` : 75 | 76 | ```bash 77 | cargo install rtd-tutorial 78 | ``` 79 | 80 | #### Or via `git repo` : 81 | ```bash 82 | git clone https://github.com/cuppar/rtd.git 83 | cargo install --path rtd 84 | ``` 85 | 86 | ## Usage 87 | 88 | #### View help document 89 | ```bash 90 | rtd -h 91 | rtd --help 92 | ``` 93 | 94 | ![rtd_help_summary](Tutorial/doc/img/readme/rtd_help_summary.png) 95 | ![rtd_help](Tutorial/doc/img/readme/rtd_help.png) 96 | 97 | #### Add a todo 98 | ```bash 99 | rtd -a 100 | rtd --add 101 | ``` 102 | 103 | ![rtd_add](Tutorial/doc/img/readme/rtd_add.png) 104 | 105 | #### List all uncompleted todos 106 | ```bash 107 | rtd 108 | rtd -l 109 | rtd -l uncompleted 110 | rtd --list 111 | rtd --list uncompleted 112 | ``` 113 | 114 | ![rtd_list_uncompleted](Tutorial/doc/img/readme/rtd_list_uncompleted.png) 115 | 116 | #### Complete a todo 117 | ```bash 118 | rtd -c 119 | rtd --complete 120 | ``` 121 | 122 | ![rtd_complete_item](Tutorial/doc/img/readme/rtd_complete_item.png) 123 | 124 | #### List all completed todos 125 | ```bash 126 | rtd -l completed 127 | rtd --list completed 128 | ``` 129 | 130 | ![rtd_list_completed](Tutorial/doc/img/readme/rtd_list_completed.png) 131 | 132 | #### Uncomplete a todo 133 | ```bash 134 | rtd -u 135 | rtd --uncomplete 136 | ``` 137 | 138 | ![rtd_uncomplete_item](Tutorial/doc/img/readme/rtd_uncomplete_item.png) 139 | 140 | #### Throw a todo into the recycle bin 141 | ```bash 142 | rtd -d 143 | rtd --delete 144 | ``` 145 | 146 | ![rtd_delete_item](Tutorial/doc/img/readme/rtd_delete_item.png) 147 | 148 | #### List all recycle bin todos 149 | ```bash 150 | rtd -l deleted 151 | rtd --list deleted 152 | ``` 153 | 154 | ![rtd_list_deleted](Tutorial/doc/img/readme/rtd_list_deleted.png) 155 | 156 | #### Restore a todo from the recycle bin 157 | ```bash 158 | rtd -r 159 | rtd --restore 160 | ``` 161 | 162 | ![rtd_restore_item](Tutorial/doc/img/readme/rtd_restore_item.png) 163 | 164 | #### Physically destroy a todo 165 | ```bash 166 | rtd --destroy 167 | ``` 168 | 169 | ![rtd_destroy_item](Tutorial/doc/img/readme/rtd_destroy_item.png) 170 | 171 | #### Empty recycle bin 172 | ```bash 173 | rtd --destroy-deleted 174 | ``` 175 | 176 | ![rtd_destroy_deleted](Tutorial/doc/img/readme/rtd_destroy_deleted.png) 177 | 178 | #### List all todos 179 | ```bash 180 | rtd -l all 181 | rtd --list all 182 | ``` 183 | 184 | ![rtd_list_all](Tutorial/doc/img/readme/rtd_list_all.png) 185 | 186 | #### Clear all todos 187 | ```bash 188 | rtd --clear 189 | ``` 190 | 191 | ![rtd_clear](Tutorial/doc/img/readme/rtd_clear.png) 192 | 193 | ## About author 194 | 195 | Cuppar He(He Zhiying), software development engineer, likes programming, technical writing, learning new things, especially computer science, worked for [SAP](https://www.sap.com/)([World Top 100](https://www.rankingthebrands.com/Brand-detail.aspx?brandID=22)) and [Alibaba Group](https://www.alibabagroup.com/)([World Top 100](https://www.rankingthebrands.com/Brand-detail.aspx?brandID=6245) & Chinese internet giant). I am currently in Gap Year, if you are looking for a software development engineer and can provide a high-quality offer(Both remote and on-site), please contact me `cuppar.hzy@gmail.com`. -------------------------------------------------------------------------------- /readme_zh.md: -------------------------------------------------------------------------------- 1 | # 500行代码学会Rust 2 | [![crate_version](https://img.shields.io/crates/v/rtd-tutorial)](https://crates.io/crates/rtd-tutorial) 3 | [![crate_downloads](https://img.shields.io/crates/d/rtd-tutorial?color=blue)](https://crates.io/crates/rtd-tutorial) 4 | [![license](https://img.shields.io/crates/l/rtd-tutorial?color=red)](https://github.com/cuppar/rtd/blob/master/LICENSE) 5 | 6 | [English](https://github.com/cuppar/rtd#learn-rust-by-500-lines-code) | 中文 7 | 8 | - `RTD` (Rust To Do) 是一个用500行Rust 代码编写的todo app命令行应用。(不包括空行/注释/长行折断显示/测试代码) 9 | - `RTD` 同时也是一个[教程](https://github.com/cuppar/rtd/wiki/%5Bzh%5D00_%E9%A6%96%E9%A1%B5),旨在通过实践学习Rust。 10 | - Learning by doing! 11 | 12 | ![rtd_list_all](Tutorial/doc/img/readme/rtd_list_all.png) 13 | 14 | ## 目录 15 | 16 | - [你能从RTD学到什么?](#你能从rtd学到什么) 17 | - [准备](#准备) 18 | - [安装](#安装) 19 | - [使用](#使用) 20 | - [关于作者](#关于作者) 21 | 22 | --- 23 | 24 | ## 你能从RTD学到什么? 25 | 26 | #### 假设你对Rust一无所知,通过[500行代码学会Rust](https://github.com/cuppar/rtd/wiki/%5Bzh%5D00_%E9%A6%96%E9%A1%B5)教程,一步步从零构建该项目,你将学会: 27 | - Rust常用语法 28 | - Rust模块系统(`package`/`binary crate`/`library crate`/`mod`/`pub`/`use`) 29 | - Rust所有权模型(伯恩斯坦条件) 30 | - Rust生命周期 31 | - Rust错误/空值处理(`Result`/`Option`) 32 | - Rust泛型 33 | - Rust模式匹配(`if let` , `match` ...) 34 | - Rust单元测试 35 | - Rust文件读写(`File Seek`/`Buffed File I/O`) 36 | - Rust操作环境变量 37 | - Rust命令行参数解析 38 | - Rust发布包至crate.io 39 | - 开发一个程序的一般过程(需求/设计/实现/测试/发布/维护) 40 | - 分层抽象(数据存储层/模型映射层/数据模型层/业务逻辑层/应用接口层/用户接口层) 41 | - 从零手写序列化/反序列化 42 | - 实现回收站功能(虚拟化概念) 43 | 44 | ###### 架构图 45 | 46 | ![rtd_arch_zh](Tutorial/doc/img/readme/rtd_arch_zh.svg) 47 | 48 | ###### 存储 49 | 使用一个本地文件 `$HOME/.rtd.csv` 存储所有数据 50 | 51 | ![data_storage](Tutorial/doc/img/readme/csv.png) 52 | ![rtd_list_all](Tutorial/doc/img/readme/rtd_list_all.png) 53 | 54 | #### 完成该教程或直接安装RTD,你将得到: 55 | - 极其轻量级且简洁的todo命令行应用 56 | - 跨平台,Rust项目优秀的构建系统天然支持跨平台 57 | - 安全,支持回收站,完全本地,不联网,无数据库 58 | - 所有数据存储仅使用一个本地csv文件,可以通过复制csv文件来在不同机器间切换 59 | 60 | --- 61 | 62 | 有趣的事实: 我使用RTD来完成RTD教程, :) 63 | 64 | 如果你喜欢我的教程,别忘了给我点个Star哦~ 65 | 66 | --- 67 | 68 | ## 准备 69 | 70 | - Rust 天然跨平台,本项目在 `linux` 环境构建测试,`Windows`/`MacOS` 同样支持,根据自身操作系统选择下载对应 `Cargo` 即可。 71 | - `Cargo` Rust 包管理和构建工具, 可通过[Rust官网](https://www.rust-lang.org/tools/install)`rustup`一行命令直接安装,接下来的事情,它会帮你全搞定,很可爱,不是吗? 72 | 73 | ## 安装 74 | 75 | #### 通过 `crate.io` : 76 | 77 | ```bash 78 | cargo install rtd-tutorial 79 | ``` 80 | 81 | (PS: 如果遇到网络不稳定的情况,可考虑使用 [crates.io mirror](https://rsproxy.cn/) 82 | 83 | #### 或者通过 `git repo` : 84 | ```bash 85 | git clone https://github.com/cuppar/rtd.git 86 | cargo install --path rtd 87 | ``` 88 | 89 | ## 使用 90 | 91 | #### 查看帮助说明 92 | ```bash 93 | rtd -h 94 | rtd --help 95 | ``` 96 | 97 | ![rtd_help_summary](Tutorial/doc/img/readme/rtd_help_summary.png) 98 | ![rtd_help](Tutorial/doc/img/readme/rtd_help.png) 99 | 100 | #### 添加一个todo 101 | ```bash 102 | rtd -a 103 | rtd --add 104 | ``` 105 | 106 | ![rtd_add](Tutorial/doc/img/readme/rtd_add.png) 107 | 108 | #### 列出所有未完成的todo 109 | ```bash 110 | rtd 111 | rtd -l 112 | rtd -l uncompleted 113 | rtd --list 114 | rtd --list uncompleted 115 | ``` 116 | 117 | ![rtd_list_uncompleted](Tutorial/doc/img/readme/rtd_list_uncompleted.png) 118 | 119 | #### 完成一个todo 120 | ```bash 121 | rtd -c 122 | rtd --complete 123 | ``` 124 | 125 | ![rtd_complete_item](Tutorial/doc/img/readme/rtd_complete_item.png) 126 | 127 | #### 列出所有已完成的todo 128 | ```bash 129 | rtd -l completed 130 | rtd --list completed 131 | ``` 132 | 133 | ![rtd_list_completed](Tutorial/doc/img/readme/rtd_list_completed.png) 134 | 135 | #### 标记一个todo为未完成 136 | ```bash 137 | rtd -u 138 | rtd --uncomplete 139 | ``` 140 | 141 | ![rtd_uncomplete_item](Tutorial/doc/img/readme/rtd_uncomplete_item.png) 142 | 143 | #### 把一个todo扔进回收站 144 | ```bash 145 | rtd -d 146 | rtd --delete 147 | ``` 148 | 149 | ![rtd_delete_item](Tutorial/doc/img/readme/rtd_delete_item.png) 150 | 151 | #### 列出所有回收站的todo 152 | ```bash 153 | rtd -l deleted 154 | rtd --list deleted 155 | ``` 156 | 157 | ![rtd_list_deleted](Tutorial/doc/img/readme/rtd_list_deleted.png) 158 | 159 | #### 从回收站恢复一个todo 160 | ```bash 161 | rtd -r 162 | rtd --restore 163 | ``` 164 | 165 | ![rtd_restore_item](Tutorial/doc/img/readme/rtd_restore_item.png) 166 | 167 | #### 物理销毁一个todo 168 | ```bash 169 | rtd --destroy 170 | ``` 171 | 172 | ![rtd_destroy_item](Tutorial/doc/img/readme/rtd_destroy_item.png) 173 | 174 | #### 清空回收站 175 | ```bash 176 | rtd --destroy-deleted 177 | ``` 178 | 179 | ![rtd_destroy_deleted](Tutorial/doc/img/readme/rtd_destroy_deleted.png) 180 | 181 | #### 列出所有todo 182 | ```bash 183 | rtd -l all 184 | rtd --list all 185 | ``` 186 | 187 | ![rtd_list_all](Tutorial/doc/img/readme/rtd_list_all.png) 188 | 189 | #### 清空所有todo 190 | ```bash 191 | rtd --clear 192 | ``` 193 | 194 | ![rtd_clear](Tutorial/doc/img/readme/rtd_clear.png) 195 | 196 | ## 关于作者 197 | 198 | 何志颖(Cuppar He), 软件开发工程师, 喜欢编程, 技术写作, 学习新东西, 尤其是计算机科学, 曾就职于[SAP](https://www.sap.com/)([世界百强](https://www.rankingthebrands.com/Brand-detail.aspx?brandID=22))和[阿里巴巴集团](https://www.alibabagroup.com/)([世界百强](https://www.rankingthebrands.com/Brand-detail.aspx?brandID=6245)&中国互联网巨头)。目前我在Gap Year, 如果你在寻找软件开发工程师并且能提供优质的offer(远程和现场都可以), 请与我联系`cuppar.hzy@gmail.com`。 -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod service; 2 | mod model; 3 | mod storage; 4 | 5 | pub use service::*; 6 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::{Parser, ValueEnum}; 2 | use rtd_tutorial::*; 3 | 4 | /// Rust To Do, tutorial: https://github.com/cuppar/rtd 5 | #[derive(Parser, Debug)] 6 | #[command( 7 | author = "Cuppar He ", 8 | version = "0.1.2", 9 | long_about = "A simple todo app write by Rust.\nYou can use it to make life pleasant or use it to learn the Rust language!\nLearn Rust in 500 lines of code tutorial: https://github.com/cuppar/rtd" 10 | )] 11 | struct Args { 12 | /// Name of todo item to add 13 | #[arg(short, long, value_name = "item-name")] 14 | add: Option, 15 | 16 | /// Id of item to complete 17 | #[arg(short, long, value_name = "item-id")] 18 | complete: Option, 19 | 20 | /// Id of item to uncomplete 21 | #[arg(short, long, value_name = "item-id")] 22 | uncomplete: Option, 23 | 24 | /// Id of item to delete, rtd use lazy delete, 25 | /// this just mark the item to `deleted`, 26 | /// it will not destroy data record, 27 | /// if you want to destory a item, use `--destroy` option. 28 | #[arg(short, long, value_name = "item-id")] 29 | delete: Option, 30 | 31 | /// restore a deleted todo item 32 | #[arg(short, long, value_name = "item-id")] 33 | restore: Option, 34 | 35 | /// Id of item to destroy, this will real destroy data record, 36 | /// use `--delete` to logic delete a todo item 37 | #[arg(long, value_name = "item-id")] 38 | destroy: Option, 39 | 40 | /// destory all `deleted` marked todo items, 41 | /// this will real destroy data records, 42 | /// use `--delete` to logic delete a todo item 43 | #[arg(long)] 44 | destroy_deleted: bool, 45 | 46 | /// Clear all records, make all list empty 47 | #[arg(long, value_name = "item-id")] 48 | clear: bool, 49 | 50 | /// List todo items 51 | #[arg(short, long, value_name = "list-type")] 52 | list: Option>, 53 | } 54 | 55 | #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] 56 | enum ListType { 57 | /// All todo items 58 | All, 59 | 60 | /// All completed todo tiems [default] 61 | Completed, 62 | 63 | /// All uncompleted todo tiems 64 | Uncompleted, 65 | 66 | /// All deleted todo tiems 67 | Deleted, 68 | } 69 | 70 | fn main() { 71 | let args = Args::parse(); 72 | 73 | if let Some(name) = args.add { 74 | match add_item(&name) { 75 | Ok(s) => println!("{s}"), 76 | Err(e) => eprintln!("Add '{name}' fail: {e}"), 77 | } 78 | } 79 | 80 | if let Some(id) = args.complete { 81 | match complete_item(id) { 82 | Ok(s) => println!("{s}"), 83 | Err(e) => eprintln!("Complete todo '{id}' fail: {e}"), 84 | } 85 | } 86 | 87 | if let Some(id) = args.uncomplete { 88 | match uncomplete_item(id) { 89 | Ok(s) => println!("{s}"), 90 | Err(e) => eprintln!("Uncomplete todo '{id}' fail: {e}"), 91 | } 92 | } 93 | 94 | if let Some(id) = args.delete { 95 | match delete_item(id) { 96 | Ok(s) => println!("{s}"), 97 | Err(e) => eprintln!("Delete todo '{id}' fail: {e}"), 98 | } 99 | } 100 | 101 | if let Some(id) = args.restore { 102 | match restore_item(id) { 103 | Ok(s) => println!("{s}"), 104 | Err(e) => eprintln!("Restore todo '{id}' fail: {e}"), 105 | } 106 | } 107 | 108 | // destroy operation need be after all operation which need a todo exist 109 | // or it can NOT find a todo when user execute the operation and destroy it at same time 110 | if let Some(id) = args.destroy { 111 | match destroy_item(id) { 112 | Ok(s) => println!("{s}"), 113 | Err(e) => eprintln!("Destroy todo '{id}' fail: {e}"), 114 | }; 115 | } 116 | 117 | // destroy operation need be after all operation which need a todo exist 118 | // or it can NOT find a todo when user execute the operation and destroy it at same time 119 | if args.destroy_deleted { 120 | match destroy_deleted() { 121 | Ok(s) => println!("{s}"), 122 | Err(e) => eprintln!("Destroy all deleted todos fail: {e}"), 123 | }; 124 | } 125 | 126 | // destroy operation need be after all operation which need a todo exist 127 | // or it can NOT find a todo when user execute the operation and destroy it at same time 128 | if args.clear { 129 | match clear() { 130 | Ok(s) => println!("{s}"), 131 | Err(e) => eprintln!("Clear all todos fail: {e}"), 132 | }; 133 | } 134 | 135 | let mut already_listed = false; 136 | 137 | if let Some(None) = args.list { 138 | default_list(); 139 | already_listed = true; 140 | } 141 | 142 | if let Some(Some(list_type)) = args.list { 143 | use ListType::*; 144 | match list_type { 145 | All => match list_all() { 146 | Ok(s) => println!("{s}"), 147 | Err(e) => eprint!("List all todos fail: {e}"), 148 | }, 149 | Completed => match list_completed() { 150 | Ok(s) => println!("{s}"), 151 | Err(e) => eprint!("List completed todos fail: {e}"), 152 | }, 153 | Uncompleted => default_list(), 154 | Deleted => match list_deleted() { 155 | Ok(s) => println!("{s}"), 156 | Err(e) => eprint!("List deleted todos fail: {e}"), 157 | }, 158 | } 159 | already_listed = true; 160 | } 161 | 162 | if !already_listed { 163 | default_list(); 164 | } 165 | } 166 | 167 | fn default_list() { 168 | match list_uncompleted() { 169 | Ok(s) => println!("{s}"), 170 | Err(e) => eprint!("List uncompleted todos fail: {e}"), 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/model.rs: -------------------------------------------------------------------------------- 1 | use chrono::*; 2 | use std::{ 3 | error::Error, 4 | fmt::Display, 5 | num::ParseIntError, 6 | str::{FromStr, ParseBoolError}, 7 | }; 8 | 9 | #[derive(Debug, Clone)] 10 | pub struct Item { 11 | pub(crate) id: u32, 12 | pub(crate) name: String, 13 | pub(crate) completed: bool, 14 | pub(crate) deleted: bool, 15 | pub(crate) created_at: Option, 16 | pub(crate) completed_at: Option, 17 | pub(crate) deleted_at: Option, 18 | } 19 | 20 | impl Item { 21 | /// Associated Functions, 22 | /// which first parameter is NOT `&self(self: &Self)`, 23 | /// `&mut self(self: &mut Self)` or `self(self: Self)` 24 | pub fn new( 25 | id: u32, 26 | name: &str, 27 | completed: bool, 28 | deleted: bool, 29 | created_at: Option, 30 | completed_at: Option, 31 | deleted_at: Option, 32 | ) -> Self { 33 | Item { 34 | id, 35 | name: name.to_string(), 36 | completed, 37 | deleted, 38 | created_at, 39 | completed_at, 40 | deleted_at, 41 | } 42 | } 43 | 44 | /// methods, 45 | /// which first parameter is `&self(self: &Self)`, 46 | /// `&mut self(self: &mut Self)` or `self(self: Self)` 47 | /// if `id` property is not `pub` 48 | /// we can use methods to support `getter` or `setter` 49 | pub fn id(&self) -> u32 { 50 | self.id 51 | } 52 | 53 | pub fn to_prettier_string(&self) -> String { 54 | let created_at = timestamp_to_datetime_string(self.created_at); 55 | let completed_at = timestamp_to_datetime_string(self.completed_at); 56 | let deleted_at = timestamp_to_datetime_string(self.deleted_at); 57 | 58 | let mut result = format!( 59 | "{:3} {} {} {}\n\n", 60 | self.id, 61 | if self.completed { 62 | "\u{2705}" 63 | } else { 64 | "\u{1f532}" 65 | }, 66 | if self.deleted { "\u{1f6ae}" } else { "" }, 67 | self.name, 68 | ); 69 | 70 | if !created_at.is_empty() { 71 | result += &format!("\tCreated at: {}\n", created_at); 72 | } 73 | if !completed_at.is_empty() { 74 | result += &format!("\tCompleted at: {}\n", completed_at); 75 | } 76 | if !deleted_at.is_empty() { 77 | result += &format!("\tDeleted at: {}\n", deleted_at); 78 | } 79 | 80 | result 81 | } 82 | } 83 | 84 | /// Serialization 85 | impl ToString for Item { 86 | fn to_string(&self) -> String { 87 | let created_at = timestamp_to_raw_string(self.created_at); 88 | let completed_at = timestamp_to_raw_string(self.completed_at); 89 | let deleted_at = timestamp_to_raw_string(self.deleted_at); 90 | 91 | let name = self 92 | .name 93 | .replace(',', COMMA_FAKE) 94 | .replace(r"\n", NEWLINE_FAKE); 95 | 96 | format!( 97 | "{},{},{},{},{},{},{}", 98 | self.id, name, self.completed, self.deleted, created_at, completed_at, deleted_at, 99 | ) 100 | } 101 | } 102 | 103 | /// Deserialization 104 | impl FromStr for Item { 105 | type Err = ParseItemError; 106 | 107 | fn from_str(s: &str) -> std::result::Result { 108 | let splited = s.split(',').collect::>(); 109 | if splited.len() != ITEM_COUNT { 110 | return Err(ParseItemError(format!( 111 | "Expected {} properties, found {}", 112 | ITEM_COUNT, 113 | splited.len() 114 | ))); 115 | } 116 | 117 | let id = splited[0].parse::()?; 118 | let name = &splited[1] 119 | .replace(COMMA_FAKE, ",") 120 | .replace(NEWLINE_FAKE, "\n"); 121 | 122 | let completed = splited[2].parse::()?; 123 | let deleted = splited[3].parse::()?; 124 | 125 | let created_at = str_to_timestamp(splited[4])?; 126 | let completed_at = str_to_timestamp(splited[5])?; 127 | let deleted_at = str_to_timestamp(splited[6])?; 128 | 129 | Ok(Item::new( 130 | id, 131 | name, 132 | completed, 133 | deleted, 134 | created_at, 135 | completed_at, 136 | deleted_at, 137 | )) 138 | } 139 | } 140 | 141 | #[derive(Debug)] 142 | pub struct ParseItemError(String); 143 | 144 | type Result = std::result::Result; 145 | 146 | impl Error for ParseItemError {} 147 | 148 | impl Display for ParseItemError { 149 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 150 | writeln!(f, "Deserialization todo fail: {}", self.0) 151 | } 152 | } 153 | 154 | impl From for ParseItemError { 155 | fn from(value: ParseIntError) -> Self { 156 | Self(value.to_string()) 157 | } 158 | } 159 | 160 | impl From for ParseItemError { 161 | fn from(value: ParseBoolError) -> Self { 162 | Self(value.to_string()) 163 | } 164 | } 165 | 166 | const ITEM_COUNT: usize = 7; 167 | const COMMA_FAKE: &str = "<@^_fake_comma_$#>"; 168 | const NEWLINE_FAKE: &str = "<@^_fake_newline_$#>"; 169 | 170 | fn timestamp_to_datetime_string(timestamp: Option) -> String { 171 | if let Some(time_stamp) = timestamp { 172 | if let Some(utc) = NaiveDateTime::from_timestamp_opt(time_stamp, 0) { 173 | Local.from_utc_datetime(&utc).to_string() 174 | } else { 175 | String::new() 176 | } 177 | } else { 178 | String::new() 179 | } 180 | } 181 | 182 | fn timestamp_to_raw_string(timestamp: Option) -> String { 183 | if let Some(x) = timestamp { 184 | x.to_string() 185 | } else { 186 | String::new() 187 | } 188 | } 189 | 190 | fn str_to_timestamp(s: &str) -> Result> { 191 | if s.is_empty() { 192 | Ok(None) 193 | } else { 194 | Ok(Some(s.parse::()?)) 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/service.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | model::Item, 3 | storage::{self, get_item_by_id, get_max_id, update_item, StorageError}, 4 | }; 5 | use chrono::Local; 6 | use std::{error::Error, fmt::Display}; 7 | 8 | pub fn add_item(name: &str) -> Result { 9 | let max_id = get_max_id()?; 10 | let item = Item::new( 11 | max_id + 1, 12 | name, 13 | false, 14 | false, 15 | Some(Local::now().timestamp()), 16 | None, 17 | None, 18 | ); 19 | storage::add_item(item.clone())?; 20 | Ok(format!("Added [{}]: {}\n", item.id, item.name)) 21 | } 22 | 23 | pub fn complete_item(id: u32) -> Result { 24 | let item = get_item_by_id(id)?; 25 | update_item(Item { 26 | completed: true, 27 | completed_at: Some(Local::now().timestamp()), 28 | ..item.clone() 29 | })?; 30 | Ok(format!("Completed [{}]: {}\n", item.id, item.name)) 31 | } 32 | 33 | pub fn uncomplete_item(id: u32) -> Result { 34 | let item = get_item_by_id(id)?; 35 | update_item(Item { 36 | completed: false, 37 | completed_at: None, 38 | ..item.clone() 39 | })?; 40 | Ok(format!("Uncompleted [{}]: {}\n", item.id, item.name)) 41 | } 42 | 43 | pub fn delete_item(id: u32) -> Result { 44 | let item = get_item_by_id(id)?; 45 | update_item(Item { 46 | deleted: true, 47 | deleted_at: Some(Local::now().timestamp()), 48 | ..item.clone() 49 | })?; 50 | Ok(format!("Deleted [{}]: {}\n", item.id, item.name)) 51 | } 52 | 53 | pub fn restore_item(id: u32) -> Result { 54 | let item = get_item_by_id(id)?; 55 | update_item(Item { 56 | deleted: false, 57 | deleted_at: None, 58 | ..item.clone() 59 | })?; 60 | Ok(format!("Restored [{}]: {}\n", item.id, item.name)) 61 | } 62 | 63 | pub fn destroy_deleted() -> Result { 64 | let items = storage::get_all()? 65 | .into_iter() 66 | .filter(|item| item.deleted) 67 | .collect::>(); 68 | if items.is_empty() { 69 | return Ok("Nothing to destory.".to_string()); 70 | } 71 | let mut result_string = String::new(); 72 | for item in items { 73 | result_string += &destroy_item(item.id)? 74 | } 75 | result_string += "All deleted todos were destroyed.\n"; 76 | Ok(result_string) 77 | } 78 | 79 | pub fn destroy_item(id: u32) -> Result { 80 | storage::delete_item(id)?; 81 | Ok(format!("Destroyed [{}]\n", id)) 82 | } 83 | 84 | pub fn clear() -> Result { 85 | let items = storage::get_all()?; 86 | if items.is_empty() { 87 | return Ok("Nothing to clear.".to_string()); 88 | } 89 | let mut result_string = String::new(); 90 | for item in items { 91 | result_string += &destroy_item(item.id)? 92 | } 93 | Ok("All todos were destroyed.\n".to_string()) 94 | } 95 | 96 | pub fn list_uncompleted() -> Result { 97 | let items = storage::get_all()? 98 | .into_iter() 99 | .filter(|item| !item.deleted && !item.completed) 100 | .collect::>(); 101 | if items.is_empty() { 102 | return Ok("Nothing need to do.".to_string()); 103 | } 104 | let mut result = "Uncompleted todos:\n\n".to_string(); 105 | for item in items { 106 | result += &item.to_prettier_string(); 107 | result += "\n"; 108 | } 109 | Ok(result) 110 | } 111 | 112 | pub fn list_completed() -> Result { 113 | let items = storage::get_all()? 114 | .into_iter() 115 | .filter(|item| !item.deleted && item.completed) 116 | .collect::>(); 117 | if items.is_empty() { 118 | return Ok("Nothing completed.".to_string()); 119 | } 120 | let mut result = "Completed todos:\n\n".to_string(); 121 | for item in items { 122 | result += &item.to_prettier_string(); 123 | result += "\n"; 124 | } 125 | Ok(result) 126 | } 127 | 128 | pub fn list_deleted() -> Result { 129 | let items = storage::get_all()? 130 | .into_iter() 131 | .filter(|item| item.deleted) 132 | .collect::>(); 133 | if items.is_empty() { 134 | return Ok("Nothing deleted.".to_string()); 135 | } 136 | let mut result = "Deleted todos:\n\n".to_string(); 137 | for item in items { 138 | result += &item.to_prettier_string(); 139 | result += "\n"; 140 | } 141 | Ok(result) 142 | } 143 | 144 | pub fn list_all() -> Result { 145 | let items = storage::get_all()?; 146 | if items.is_empty() { 147 | return Ok("Nothing need to do.".to_string()); 148 | } 149 | let mut result = "All todos:\n\n".to_string(); 150 | for item in items { 151 | result += &item.to_prettier_string(); 152 | result += "\n"; 153 | } 154 | Ok(result) 155 | } 156 | 157 | type Result = std::result::Result; 158 | 159 | #[derive(Debug)] 160 | pub enum ServiceError { 161 | Storage(StorageError), 162 | } 163 | 164 | impl Error for ServiceError {} 165 | 166 | impl Display for ServiceError { 167 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 168 | use ServiceError::*; 169 | match self { 170 | Storage(e) => writeln!(f, "Rtd service storage error: {}", e), 171 | } 172 | } 173 | } 174 | 175 | impl From for ServiceError { 176 | fn from(value: StorageError) -> Self { 177 | Self::Storage(value) 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/storage.rs: -------------------------------------------------------------------------------- 1 | use crate::model::{self, *}; 2 | use std::env::{self, VarError}; 3 | use std::error::Error; 4 | use std::fmt::Display; 5 | use std::fs::File; 6 | use std::fs::{self, OpenOptions}; 7 | use std::io::{BufReader, BufWriter, Read, Seek, Write}; 8 | use std::path::Path; 9 | 10 | pub fn add_item(item: Item) -> Result<()> { 11 | let mut csv = Csv::new()?; 12 | csv.file.seek(std::io::SeekFrom::End(0))?; 13 | writeln!(csv.file, "{}", item.to_string())?; 14 | 15 | Ok(()) 16 | } 17 | 18 | pub fn update_item(item: Item) -> Result<()> { 19 | let to_update_item = get_item_by_id(item.id())?; 20 | let offset = get_offset_by_id(item.id())?; 21 | Csv::new()?.splice( 22 | offset as u64, 23 | to_update_item.to_string().len() as u64, 24 | &item.to_string(), 25 | ) 26 | } 27 | 28 | pub fn delete_item(id: u32) -> Result<()> { 29 | let to_delete_item = get_item_by_id(id)?; 30 | let offset = get_offset_by_id(id)?; 31 | Csv::new()?.splice( 32 | offset as u64, 33 | to_delete_item.to_string().len() as u64 + 1, 34 | "", 35 | ) 36 | } 37 | 38 | pub fn get_all() -> Result> { 39 | Ok(Csv::new()? 40 | .content()? 41 | .lines() 42 | .filter_map(|line| line.parse::().ok()) 43 | .collect()) 44 | } 45 | 46 | pub fn get_item_by_id(id: u32) -> Result { 47 | let content = Csv::new()?.content()?; 48 | let item_str = content.lines().find(|line| { 49 | if let Ok(item) = line.parse::() { 50 | item.id() == id 51 | } else { 52 | false 53 | } 54 | }); 55 | 56 | if let Some(item_str) = item_str { 57 | Ok(item_str.parse().unwrap()) 58 | } else { 59 | Err(StorageError::ItemNoExist(id)) 60 | } 61 | } 62 | 63 | /// It is not an efficient way to find the largest id for all records, 64 | /// but it works while ensuring the simplicity of the tutorial. 65 | /// In actual production projects, 66 | /// please use other efficient ways to generate unique identifiers, 67 | /// such as using `uuid`, etc. 68 | pub fn get_max_id() -> Result { 69 | let max_id = get_all()?.iter().map(|item| item.id()).reduce(u32::max); 70 | 71 | if let Some(max_id) = max_id { 72 | Ok(max_id) 73 | } else { 74 | Ok(0) 75 | } 76 | } 77 | 78 | #[allow(unused)] 79 | struct Csv { 80 | filename: String, 81 | file: File, 82 | } 83 | 84 | impl Csv { 85 | fn new() -> Result { 86 | let filename = Csv::filename()?; 87 | let path = Path::new(&filename); 88 | 89 | if !path.exists() { 90 | let mut file = Csv::create(path)?; 91 | file.write_all(b"id,name,completed,deleted,createdAt,completedAt,deletedAt\n")?; 92 | Ok(Self { 93 | filename: filename.to_string(), 94 | file, 95 | }) 96 | } else { 97 | Ok(Self { 98 | filename: filename.to_string(), 99 | file: Csv::open(path)?, 100 | }) 101 | } 102 | } 103 | 104 | fn create(path: &Path) -> Result { 105 | let csv = OpenOptions::new() 106 | .read(true) 107 | .write(true) 108 | .create(true) 109 | .open(path)?; 110 | Ok(csv) 111 | } 112 | 113 | fn open(path: &Path) -> Result { 114 | let csv = OpenOptions::new().read(true).write(true).open(path)?; 115 | Ok(csv) 116 | } 117 | 118 | fn filename() -> Result { 119 | let home = env::var("HOME")?; 120 | let filename = home + "/" + CSV_FILE_NAME; 121 | Ok(filename) 122 | } 123 | 124 | fn content(&mut self) -> Result { 125 | let mut contents = String::new(); 126 | self.file.read_to_string(&mut contents)?; 127 | Ok(contents) 128 | } 129 | 130 | /// Specify any position in the file to delete the specified byte, 131 | /// and then insert any byte string 132 | fn splice(&mut self, offset: u64, delete_size: u64, write_content: &str) -> Result<()> { 133 | use std::io::SeekFrom; 134 | let file = &self.file; 135 | 136 | // Create a buffered reader form csv file 137 | let mut reader = BufReader::new(file); 138 | 139 | // Adjust the appropriate reading position 140 | reader.seek(SeekFrom::Start(offset + delete_size))?; 141 | 142 | // Save the rest of the file, 143 | // starting at the position after the last character that was deleted 144 | let mut rest_content = String::new(); 145 | reader.read_to_string(&mut rest_content)?; 146 | 147 | // The final to be write content is spliced 148 | // by the `write_content` and the `rest_content` 149 | let write_content = write_content.to_owned() + &rest_content; 150 | 151 | // Create a buffered writer from csv file 152 | let mut writer = BufWriter::new(file); 153 | 154 | // Adjust the appropriate writing position 155 | writer.seek(SeekFrom::Start(offset))?; 156 | 157 | // Insert `write_content` and overwrite old file content 158 | writer.write_all(write_content.as_bytes())?; 159 | 160 | // Make sure there is no redundant old file content left 161 | file.set_len(offset + write_content.len() as u64)?; 162 | 163 | Ok(()) 164 | } 165 | } 166 | 167 | #[cfg(test)] 168 | mod tests { 169 | use super::*; 170 | 171 | #[test] 172 | fn create_file() -> Result<()> { 173 | Csv::new()?; 174 | Ok(()) 175 | } 176 | 177 | #[test] 178 | fn title_line() -> Result<()> { 179 | let mut csv = Csv::new()?; 180 | let content = csv.content()?; 181 | let lines: Vec<&str> = content.lines().collect(); 182 | assert!(!lines.is_empty()); 183 | assert_eq!( 184 | lines[0], 185 | "id,name,completed,deleted,createdAt,completedAt,deletedAt" 186 | ); 187 | 188 | Ok(()) 189 | } 190 | } 191 | 192 | const CSV_FILE_NAME: &str = ".rtd.csv"; 193 | 194 | type Result = std::result::Result; 195 | 196 | #[derive(Debug)] 197 | pub enum StorageError { 198 | FileHandle(FileHandleError), 199 | ParseItem(ParseItemError), 200 | ItemNoExist(u32), 201 | } 202 | 203 | #[derive(Debug)] 204 | pub enum FileHandleError { 205 | EnvVar(VarError), 206 | Io(std::io::Error), 207 | } 208 | 209 | impl Display for StorageError { 210 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 211 | use FileHandleError::*; 212 | use StorageError::*; 213 | match &self { 214 | FileHandle(source) => match source { 215 | EnvVar(e) => write!(f, "Rtd storage file handle env var error: {}", e), 216 | Io(e) => write!(f, "Rtd storage file handle io error: {}", e), 217 | }, 218 | ParseItem(e) => write!(f, "Rtd storage parse todo error: {}", e), 219 | ItemNoExist(id) => write!(f, "Rtd storage todo no exist: {}", id), 220 | } 221 | } 222 | } 223 | 224 | impl Error for StorageError { 225 | fn source(&self) -> Option<&(dyn Error + 'static)> { 226 | use FileHandleError::*; 227 | use StorageError::*; 228 | match self { 229 | FileHandle(source) => match source { 230 | EnvVar(source) => Some(source), 231 | Io(source) => Some(source), 232 | }, 233 | ParseItem(e) => Some(e), 234 | ItemNoExist(_) => None, 235 | } 236 | } 237 | } 238 | 239 | impl From for StorageError { 240 | fn from(value: VarError) -> Self { 241 | Self::FileHandle(FileHandleError::EnvVar(value)) 242 | } 243 | } 244 | 245 | impl From for StorageError { 246 | fn from(value: std::io::Error) -> Self { 247 | Self::FileHandle(FileHandleError::Io(value)) 248 | } 249 | } 250 | 251 | impl From for StorageError { 252 | fn from(value: model::ParseItemError) -> Self { 253 | Self::ParseItem(value) 254 | } 255 | } 256 | 257 | fn get_offset_by_id(id: u32) -> Result { 258 | let mut csv = Csv::new()?; 259 | let content = csv.content()?; 260 | let prev_lines = content.lines().take_while(|line| { 261 | if let Ok(item) = line.parse::() { 262 | item.id() != id 263 | } else { 264 | true 265 | } 266 | }); 267 | let offset: usize = prev_lines.map(|line| line.len() + 1).sum(); 268 | Ok(offset) 269 | } 270 | --------------------------------------------------------------------------------