├── .github └── workflows │ └── publish.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── docs └── DOCUMENT.md ├── runn-e2e ├── README.md └── runbooks │ ├── test.yml │ ├── test_comment.yml │ ├── test_comment_error.yml │ ├── test_post.yml │ ├── test_post_error.yml │ ├── test_profile.yml │ └── test_profile_error.yml ├── src ├── error.rs ├── main.rs ├── server.rs ├── server │ ├── context.rs │ ├── handler.rs │ ├── handler │ │ ├── delete.rs │ │ ├── get.rs │ │ ├── hc.rs │ │ ├── patch.rs │ │ ├── post.rs │ │ └── put.rs │ └── state.rs ├── storage.rs └── storage │ ├── operation.rs │ ├── operation │ ├── insert.rs │ ├── remove.rs │ ├── replace.rs │ ├── replace_one.rs │ ├── select_all.rs │ ├── select_one.rs │ ├── update.rs │ └── update_one.rs │ ├── reader.rs │ └── writer.rs └── storage.json /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | 4 | name: Publish 5 | 6 | jobs: 7 | publish: 8 | name: Publish 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout sources 12 | uses: actions/checkout@v2 13 | 14 | - name: Install stable toolchain 15 | uses: actions-rs/toolchain@v1 16 | with: 17 | profile: minimal 18 | toolchain: stable 19 | override: true 20 | 21 | - run: cargo publish --token ${CRATES_TOKEN} 22 | env: 23 | CRATES_TOKEN: ${{ secrets.CRATES_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | 3 | # These are backup files generated by rustfmt 4 | **/*.rs.bk 5 | 6 | # Created by https://www.toptal.com/developers/gitignore/api/macos 7 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos 8 | 9 | ### macOS ### 10 | # General 11 | .DS_Store 12 | .AppleDouble 13 | .LSOverride 14 | 15 | # Icon must end with two \r 16 | Icon 17 | 18 | 19 | # Thumbnails 20 | ._* 21 | 22 | # Files that might appear in the root of a volume 23 | .DocumentRevisions-V100 24 | .fseventsd 25 | .Spotlight-V100 26 | .TemporaryItems 27 | .Trashes 28 | .VolumeIcon.icns 29 | .com.apple.timemachine.donotpresent 30 | 31 | # Directories potentially created on remote AFP share 32 | .AppleDB 33 | .AppleDesktop 34 | Network Trash Folder 35 | Temporary Items 36 | .apdisk 37 | 38 | ### macOS Patch ### 39 | # iCloud generated files 40 | *.icloud 41 | 42 | # End of https://www.toptal.com/developers/gitignore/api/macos 43 | 44 | # IntelliJ 45 | .idea 46 | *.iml 47 | 48 | # mocks 49 | *.backup.json 50 | *.debug.json 51 | *.test.json 52 | 53 | *.tar.gz 54 | -------------------------------------------------------------------------------- /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 = "addr2line" 7 | version = "0.24.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "f5fb1d8e4442bd405fdfd1dacb42792696b0cf9cb15882e5d097b742a676d375" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 | 20 | [[package]] 21 | name = "anstream" 22 | version = "0.6.15" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" 25 | dependencies = [ 26 | "anstyle", 27 | "anstyle-parse", 28 | "anstyle-query", 29 | "anstyle-wincon", 30 | "colorchoice", 31 | "is_terminal_polyfill", 32 | "utf8parse", 33 | ] 34 | 35 | [[package]] 36 | name = "anstyle" 37 | version = "1.0.8" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" 40 | 41 | [[package]] 42 | name = "anstyle-parse" 43 | version = "0.2.5" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" 46 | dependencies = [ 47 | "utf8parse", 48 | ] 49 | 50 | [[package]] 51 | name = "anstyle-query" 52 | version = "1.1.1" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" 55 | dependencies = [ 56 | "windows-sys", 57 | ] 58 | 59 | [[package]] 60 | name = "anstyle-wincon" 61 | version = "3.0.4" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" 64 | dependencies = [ 65 | "anstyle", 66 | "windows-sys", 67 | ] 68 | 69 | [[package]] 70 | name = "async-trait" 71 | version = "0.1.82" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" 74 | dependencies = [ 75 | "proc-macro2", 76 | "quote", 77 | "syn", 78 | ] 79 | 80 | [[package]] 81 | name = "autocfg" 82 | version = "1.3.0" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 85 | 86 | [[package]] 87 | name = "axum" 88 | version = "0.7.5" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" 91 | dependencies = [ 92 | "async-trait", 93 | "axum-core", 94 | "bytes", 95 | "futures-util", 96 | "http", 97 | "http-body", 98 | "http-body-util", 99 | "hyper", 100 | "hyper-util", 101 | "itoa", 102 | "matchit", 103 | "memchr", 104 | "mime", 105 | "percent-encoding", 106 | "pin-project-lite", 107 | "rustversion", 108 | "serde", 109 | "serde_json", 110 | "serde_path_to_error", 111 | "serde_urlencoded", 112 | "sync_wrapper 1.0.1", 113 | "tokio", 114 | "tower", 115 | "tower-layer", 116 | "tower-service", 117 | "tracing", 118 | ] 119 | 120 | [[package]] 121 | name = "axum-core" 122 | version = "0.4.3" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" 125 | dependencies = [ 126 | "async-trait", 127 | "bytes", 128 | "futures-util", 129 | "http", 130 | "http-body", 131 | "http-body-util", 132 | "mime", 133 | "pin-project-lite", 134 | "rustversion", 135 | "sync_wrapper 0.1.2", 136 | "tower-layer", 137 | "tower-service", 138 | "tracing", 139 | ] 140 | 141 | [[package]] 142 | name = "backtrace" 143 | version = "0.3.74" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 146 | dependencies = [ 147 | "addr2line", 148 | "cfg-if", 149 | "libc", 150 | "miniz_oxide", 151 | "object", 152 | "rustc-demangle", 153 | "windows-targets", 154 | ] 155 | 156 | [[package]] 157 | name = "bitflags" 158 | version = "2.6.0" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 161 | 162 | [[package]] 163 | name = "bytes" 164 | version = "1.7.1" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" 167 | 168 | [[package]] 169 | name = "cfg-if" 170 | version = "1.0.0" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 173 | 174 | [[package]] 175 | name = "clap" 176 | version = "4.5.17" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac" 179 | dependencies = [ 180 | "clap_builder", 181 | "clap_derive", 182 | ] 183 | 184 | [[package]] 185 | name = "clap_builder" 186 | version = "4.5.17" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73" 189 | dependencies = [ 190 | "anstream", 191 | "anstyle", 192 | "clap_lex", 193 | "strsim", 194 | ] 195 | 196 | [[package]] 197 | name = "clap_derive" 198 | version = "4.5.13" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" 201 | dependencies = [ 202 | "heck", 203 | "proc-macro2", 204 | "quote", 205 | "syn", 206 | ] 207 | 208 | [[package]] 209 | name = "clap_lex" 210 | version = "0.7.2" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" 213 | 214 | [[package]] 215 | name = "colorchoice" 216 | version = "1.0.2" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" 219 | 220 | [[package]] 221 | name = "fnv" 222 | version = "1.0.7" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 225 | 226 | [[package]] 227 | name = "form_urlencoded" 228 | version = "1.2.1" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 231 | dependencies = [ 232 | "percent-encoding", 233 | ] 234 | 235 | [[package]] 236 | name = "futures-channel" 237 | version = "0.3.30" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" 240 | dependencies = [ 241 | "futures-core", 242 | ] 243 | 244 | [[package]] 245 | name = "futures-core" 246 | version = "0.3.30" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 249 | 250 | [[package]] 251 | name = "futures-task" 252 | version = "0.3.30" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" 255 | 256 | [[package]] 257 | name = "futures-util" 258 | version = "0.3.30" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" 261 | dependencies = [ 262 | "futures-core", 263 | "futures-task", 264 | "pin-project-lite", 265 | "pin-utils", 266 | ] 267 | 268 | [[package]] 269 | name = "gimli" 270 | version = "0.31.0" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64" 273 | 274 | [[package]] 275 | name = "heck" 276 | version = "0.5.0" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 279 | 280 | [[package]] 281 | name = "hermit-abi" 282 | version = "0.3.9" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 285 | 286 | [[package]] 287 | name = "http" 288 | version = "1.1.0" 289 | source = "registry+https://github.com/rust-lang/crates.io-index" 290 | checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" 291 | dependencies = [ 292 | "bytes", 293 | "fnv", 294 | "itoa", 295 | ] 296 | 297 | [[package]] 298 | name = "http-body" 299 | version = "1.0.1" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 302 | dependencies = [ 303 | "bytes", 304 | "http", 305 | ] 306 | 307 | [[package]] 308 | name = "http-body-util" 309 | version = "0.1.2" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" 312 | dependencies = [ 313 | "bytes", 314 | "futures-util", 315 | "http", 316 | "http-body", 317 | "pin-project-lite", 318 | ] 319 | 320 | [[package]] 321 | name = "httparse" 322 | version = "1.9.4" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" 325 | 326 | [[package]] 327 | name = "httpdate" 328 | version = "1.0.3" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 331 | 332 | [[package]] 333 | name = "hyper" 334 | version = "1.4.1" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" 337 | dependencies = [ 338 | "bytes", 339 | "futures-channel", 340 | "futures-util", 341 | "http", 342 | "http-body", 343 | "httparse", 344 | "httpdate", 345 | "itoa", 346 | "pin-project-lite", 347 | "smallvec", 348 | "tokio", 349 | ] 350 | 351 | [[package]] 352 | name = "hyper-util" 353 | version = "0.1.8" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "da62f120a8a37763efb0cf8fdf264b884c7b8b9ac8660b900c8661030c00e6ba" 356 | dependencies = [ 357 | "bytes", 358 | "futures-util", 359 | "http", 360 | "http-body", 361 | "hyper", 362 | "pin-project-lite", 363 | "tokio", 364 | ] 365 | 366 | [[package]] 367 | name = "is_terminal_polyfill" 368 | version = "1.70.1" 369 | source = "registry+https://github.com/rust-lang/crates.io-index" 370 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 371 | 372 | [[package]] 373 | name = "itoa" 374 | version = "1.0.11" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 377 | 378 | [[package]] 379 | name = "libc" 380 | version = "0.2.158" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" 383 | 384 | [[package]] 385 | name = "lock_api" 386 | version = "0.4.12" 387 | source = "registry+https://github.com/rust-lang/crates.io-index" 388 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 389 | dependencies = [ 390 | "autocfg", 391 | "scopeguard", 392 | ] 393 | 394 | [[package]] 395 | name = "log" 396 | version = "0.4.22" 397 | source = "registry+https://github.com/rust-lang/crates.io-index" 398 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 399 | 400 | [[package]] 401 | name = "matchit" 402 | version = "0.7.3" 403 | source = "registry+https://github.com/rust-lang/crates.io-index" 404 | checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" 405 | 406 | [[package]] 407 | name = "memchr" 408 | version = "2.7.4" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 411 | 412 | [[package]] 413 | name = "mime" 414 | version = "0.3.17" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 417 | 418 | [[package]] 419 | name = "miniz_oxide" 420 | version = "0.8.0" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" 423 | dependencies = [ 424 | "adler2", 425 | ] 426 | 427 | [[package]] 428 | name = "mio" 429 | version = "1.0.2" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" 432 | dependencies = [ 433 | "hermit-abi", 434 | "libc", 435 | "wasi", 436 | "windows-sys", 437 | ] 438 | 439 | [[package]] 440 | name = "mocks" 441 | version = "0.3.7" 442 | dependencies = [ 443 | "axum", 444 | "clap", 445 | "serde_json", 446 | "tokio", 447 | ] 448 | 449 | [[package]] 450 | name = "object" 451 | version = "0.36.4" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" 454 | dependencies = [ 455 | "memchr", 456 | ] 457 | 458 | [[package]] 459 | name = "once_cell" 460 | version = "1.19.0" 461 | source = "registry+https://github.com/rust-lang/crates.io-index" 462 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 463 | 464 | [[package]] 465 | name = "parking_lot" 466 | version = "0.12.3" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 469 | dependencies = [ 470 | "lock_api", 471 | "parking_lot_core", 472 | ] 473 | 474 | [[package]] 475 | name = "parking_lot_core" 476 | version = "0.9.10" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 479 | dependencies = [ 480 | "cfg-if", 481 | "libc", 482 | "redox_syscall", 483 | "smallvec", 484 | "windows-targets", 485 | ] 486 | 487 | [[package]] 488 | name = "percent-encoding" 489 | version = "2.3.1" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 492 | 493 | [[package]] 494 | name = "pin-project" 495 | version = "1.1.5" 496 | source = "registry+https://github.com/rust-lang/crates.io-index" 497 | checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" 498 | dependencies = [ 499 | "pin-project-internal", 500 | ] 501 | 502 | [[package]] 503 | name = "pin-project-internal" 504 | version = "1.1.5" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" 507 | dependencies = [ 508 | "proc-macro2", 509 | "quote", 510 | "syn", 511 | ] 512 | 513 | [[package]] 514 | name = "pin-project-lite" 515 | version = "0.2.14" 516 | source = "registry+https://github.com/rust-lang/crates.io-index" 517 | checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" 518 | 519 | [[package]] 520 | name = "pin-utils" 521 | version = "0.1.0" 522 | source = "registry+https://github.com/rust-lang/crates.io-index" 523 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 524 | 525 | [[package]] 526 | name = "proc-macro2" 527 | version = "1.0.86" 528 | source = "registry+https://github.com/rust-lang/crates.io-index" 529 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 530 | dependencies = [ 531 | "unicode-ident", 532 | ] 533 | 534 | [[package]] 535 | name = "quote" 536 | version = "1.0.37" 537 | source = "registry+https://github.com/rust-lang/crates.io-index" 538 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 539 | dependencies = [ 540 | "proc-macro2", 541 | ] 542 | 543 | [[package]] 544 | name = "redox_syscall" 545 | version = "0.5.4" 546 | source = "registry+https://github.com/rust-lang/crates.io-index" 547 | checksum = "0884ad60e090bf1345b93da0a5de8923c93884cd03f40dfcfddd3b4bee661853" 548 | dependencies = [ 549 | "bitflags", 550 | ] 551 | 552 | [[package]] 553 | name = "rustc-demangle" 554 | version = "0.1.24" 555 | source = "registry+https://github.com/rust-lang/crates.io-index" 556 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 557 | 558 | [[package]] 559 | name = "rustversion" 560 | version = "1.0.17" 561 | source = "registry+https://github.com/rust-lang/crates.io-index" 562 | checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" 563 | 564 | [[package]] 565 | name = "ryu" 566 | version = "1.0.18" 567 | source = "registry+https://github.com/rust-lang/crates.io-index" 568 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 569 | 570 | [[package]] 571 | name = "scopeguard" 572 | version = "1.2.0" 573 | source = "registry+https://github.com/rust-lang/crates.io-index" 574 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 575 | 576 | [[package]] 577 | name = "serde" 578 | version = "1.0.210" 579 | source = "registry+https://github.com/rust-lang/crates.io-index" 580 | checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" 581 | dependencies = [ 582 | "serde_derive", 583 | ] 584 | 585 | [[package]] 586 | name = "serde_derive" 587 | version = "1.0.210" 588 | source = "registry+https://github.com/rust-lang/crates.io-index" 589 | checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" 590 | dependencies = [ 591 | "proc-macro2", 592 | "quote", 593 | "syn", 594 | ] 595 | 596 | [[package]] 597 | name = "serde_json" 598 | version = "1.0.128" 599 | source = "registry+https://github.com/rust-lang/crates.io-index" 600 | checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" 601 | dependencies = [ 602 | "itoa", 603 | "memchr", 604 | "ryu", 605 | "serde", 606 | ] 607 | 608 | [[package]] 609 | name = "serde_path_to_error" 610 | version = "0.1.16" 611 | source = "registry+https://github.com/rust-lang/crates.io-index" 612 | checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" 613 | dependencies = [ 614 | "itoa", 615 | "serde", 616 | ] 617 | 618 | [[package]] 619 | name = "serde_urlencoded" 620 | version = "0.7.1" 621 | source = "registry+https://github.com/rust-lang/crates.io-index" 622 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 623 | dependencies = [ 624 | "form_urlencoded", 625 | "itoa", 626 | "ryu", 627 | "serde", 628 | ] 629 | 630 | [[package]] 631 | name = "signal-hook-registry" 632 | version = "1.4.2" 633 | source = "registry+https://github.com/rust-lang/crates.io-index" 634 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 635 | dependencies = [ 636 | "libc", 637 | ] 638 | 639 | [[package]] 640 | name = "smallvec" 641 | version = "1.13.2" 642 | source = "registry+https://github.com/rust-lang/crates.io-index" 643 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 644 | 645 | [[package]] 646 | name = "socket2" 647 | version = "0.5.7" 648 | source = "registry+https://github.com/rust-lang/crates.io-index" 649 | checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" 650 | dependencies = [ 651 | "libc", 652 | "windows-sys", 653 | ] 654 | 655 | [[package]] 656 | name = "strsim" 657 | version = "0.11.1" 658 | source = "registry+https://github.com/rust-lang/crates.io-index" 659 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 660 | 661 | [[package]] 662 | name = "syn" 663 | version = "2.0.77" 664 | source = "registry+https://github.com/rust-lang/crates.io-index" 665 | checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" 666 | dependencies = [ 667 | "proc-macro2", 668 | "quote", 669 | "unicode-ident", 670 | ] 671 | 672 | [[package]] 673 | name = "sync_wrapper" 674 | version = "0.1.2" 675 | source = "registry+https://github.com/rust-lang/crates.io-index" 676 | checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" 677 | 678 | [[package]] 679 | name = "sync_wrapper" 680 | version = "1.0.1" 681 | source = "registry+https://github.com/rust-lang/crates.io-index" 682 | checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" 683 | 684 | [[package]] 685 | name = "tokio" 686 | version = "1.40.0" 687 | source = "registry+https://github.com/rust-lang/crates.io-index" 688 | checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" 689 | dependencies = [ 690 | "backtrace", 691 | "bytes", 692 | "libc", 693 | "mio", 694 | "parking_lot", 695 | "pin-project-lite", 696 | "signal-hook-registry", 697 | "socket2", 698 | "tokio-macros", 699 | "windows-sys", 700 | ] 701 | 702 | [[package]] 703 | name = "tokio-macros" 704 | version = "2.4.0" 705 | source = "registry+https://github.com/rust-lang/crates.io-index" 706 | checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" 707 | dependencies = [ 708 | "proc-macro2", 709 | "quote", 710 | "syn", 711 | ] 712 | 713 | [[package]] 714 | name = "tower" 715 | version = "0.4.13" 716 | source = "registry+https://github.com/rust-lang/crates.io-index" 717 | checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" 718 | dependencies = [ 719 | "futures-core", 720 | "futures-util", 721 | "pin-project", 722 | "pin-project-lite", 723 | "tokio", 724 | "tower-layer", 725 | "tower-service", 726 | "tracing", 727 | ] 728 | 729 | [[package]] 730 | name = "tower-layer" 731 | version = "0.3.3" 732 | source = "registry+https://github.com/rust-lang/crates.io-index" 733 | checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 734 | 735 | [[package]] 736 | name = "tower-service" 737 | version = "0.3.3" 738 | source = "registry+https://github.com/rust-lang/crates.io-index" 739 | checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 740 | 741 | [[package]] 742 | name = "tracing" 743 | version = "0.1.40" 744 | source = "registry+https://github.com/rust-lang/crates.io-index" 745 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 746 | dependencies = [ 747 | "log", 748 | "pin-project-lite", 749 | "tracing-core", 750 | ] 751 | 752 | [[package]] 753 | name = "tracing-core" 754 | version = "0.1.32" 755 | source = "registry+https://github.com/rust-lang/crates.io-index" 756 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 757 | dependencies = [ 758 | "once_cell", 759 | ] 760 | 761 | [[package]] 762 | name = "unicode-ident" 763 | version = "1.0.12" 764 | source = "registry+https://github.com/rust-lang/crates.io-index" 765 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 766 | 767 | [[package]] 768 | name = "utf8parse" 769 | version = "0.2.2" 770 | source = "registry+https://github.com/rust-lang/crates.io-index" 771 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 772 | 773 | [[package]] 774 | name = "wasi" 775 | version = "0.11.0+wasi-snapshot-preview1" 776 | source = "registry+https://github.com/rust-lang/crates.io-index" 777 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 778 | 779 | [[package]] 780 | name = "windows-sys" 781 | version = "0.52.0" 782 | source = "registry+https://github.com/rust-lang/crates.io-index" 783 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 784 | dependencies = [ 785 | "windows-targets", 786 | ] 787 | 788 | [[package]] 789 | name = "windows-targets" 790 | version = "0.52.6" 791 | source = "registry+https://github.com/rust-lang/crates.io-index" 792 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 793 | dependencies = [ 794 | "windows_aarch64_gnullvm", 795 | "windows_aarch64_msvc", 796 | "windows_i686_gnu", 797 | "windows_i686_gnullvm", 798 | "windows_i686_msvc", 799 | "windows_x86_64_gnu", 800 | "windows_x86_64_gnullvm", 801 | "windows_x86_64_msvc", 802 | ] 803 | 804 | [[package]] 805 | name = "windows_aarch64_gnullvm" 806 | version = "0.52.6" 807 | source = "registry+https://github.com/rust-lang/crates.io-index" 808 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 809 | 810 | [[package]] 811 | name = "windows_aarch64_msvc" 812 | version = "0.52.6" 813 | source = "registry+https://github.com/rust-lang/crates.io-index" 814 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 815 | 816 | [[package]] 817 | name = "windows_i686_gnu" 818 | version = "0.52.6" 819 | source = "registry+https://github.com/rust-lang/crates.io-index" 820 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 821 | 822 | [[package]] 823 | name = "windows_i686_gnullvm" 824 | version = "0.52.6" 825 | source = "registry+https://github.com/rust-lang/crates.io-index" 826 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 827 | 828 | [[package]] 829 | name = "windows_i686_msvc" 830 | version = "0.52.6" 831 | source = "registry+https://github.com/rust-lang/crates.io-index" 832 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 833 | 834 | [[package]] 835 | name = "windows_x86_64_gnu" 836 | version = "0.52.6" 837 | source = "registry+https://github.com/rust-lang/crates.io-index" 838 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 839 | 840 | [[package]] 841 | name = "windows_x86_64_gnullvm" 842 | version = "0.52.6" 843 | source = "registry+https://github.com/rust-lang/crates.io-index" 844 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 845 | 846 | [[package]] 847 | name = "windows_x86_64_msvc" 848 | version = "0.52.6" 849 | source = "registry+https://github.com/rust-lang/crates.io-index" 850 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 851 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mocks" 3 | version = "0.3.7" 4 | edition = "2021" 5 | authors = ["codemountains "] 6 | description = "Get a mock REST APIs with zero coding within seconds." 7 | homepage = "https://github.com/mocks-rs/mocks" 8 | repository = "https://github.com/mocks-rs/mocks" 9 | documentation = "https://github.com/mocks-rs/mocks" 10 | readme = "README.md" 11 | license = "MIT" 12 | 13 | [dependencies] 14 | axum = "0.7.5" 15 | clap = { version = "4.5.17", features = ["derive"] } 16 | serde_json = "1.0.128" 17 | tokio = { version = "1.40.0", features = ["full"] } 18 | 19 | [profile.release] 20 | lto = true 21 | opt-level = "s" 22 | codegen-units = 1 23 | panic = "abort" 24 | strip = "symbols" 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 kazuno fukuda 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mocks 2 | 3 | [![Crates.io](https://img.shields.io/crates/v/mocks.svg)](https://crates.io/crates/mocks) 4 | [![msrv 1.65.0](https://img.shields.io/badge/msrv-1.74.1-dea584.svg?logo=rust)](https://github.com/rust-lang/rust/releases/tag/1.74.1) 5 | [![License](https://img.shields.io/github/license/mocks-rs/mocks)](LICENSE) 6 | 7 | Get a mock REST APIs with zero coding within seconds. 8 | 9 | ## Install 10 | 11 | If you're a macOS Homebrew user, then you can install `mocks` from [homebrew-tap](https://github.com/mocks-rs/homebrew-tap). 12 | 13 | ```shell 14 | brew install mocks-rs/tap/mocks 15 | ``` 16 | 17 | If you're a Rust programmer, `mocks` can be installed with `cargo`. 18 | 19 | ```shell 20 | cargo install mocks 21 | ``` 22 | 23 | ## Usage 24 | 25 | ### Run a REST API server 26 | 27 | Create a `storage.json`. 28 | 29 | ```json 30 | { 31 | "posts": [ 32 | { "id": "01J7BAKH37HPG116ZRRFKHBDGB", "title": "first post", "views": 100 }, 33 | { "id": "01J7BAKH37GE8B688PT4RC7TP4", "title": "second post", "views": 10 } 34 | ], 35 | "comments": [ 36 | { "id": 1, "text": "a comment", "post_id": "01J7BAKH37HPG116ZRRFKHBDGB" }, 37 | { "id": 2, "text": "another comment", "post_id": "01J7BAKH37HPG116ZRRFKHBDGB" } 38 | ], 39 | "profile": { "id": "01J7BAQE1GMD78FN3J0FJCNS8T", "name": "mocks" }, 40 | "friends": [] 41 | } 42 | ``` 43 | 44 | Pass it to `mocks` CLI. 45 | 46 | ```shell 47 | mocks storage.json 48 | ``` 49 | 50 | ```shell 51 | mocks -H 127.0.0.1 -p 8080 storage.json 52 | ``` 53 | 54 | Get a REST API with `curl`. 55 | 56 | ```shell 57 | % curl http://localhost:3000/posts/01J7BAKH37HPG116ZRRFKHBDGB 58 | {"id":"01J7BAKH37HPG116ZRRFKHBDGB","title":"first post","views":100} 59 | ``` 60 | 61 | ### Routes 62 | 63 | Based on the example [storage.json](storage.json), you'll get the following routes: 64 | 65 | ``` 66 | GET /posts 67 | GET /posts/:id 68 | POST /posts 69 | PUT /posts/:id 70 | PATCH /posts/:id 71 | DELETE /posts/:id 72 | 73 | # Same for comments and friends 74 | ``` 75 | 76 | ``` 77 | GET /profile 78 | PUT /profile 79 | PATCH /profile 80 | ``` 81 | 82 | ``` 83 | GET /_hc 84 | 85 | # Health check endpoint returns a 204 response. 86 | ``` 87 | 88 | ### Options 89 | 90 | Run `mocks --help` for a list of options. 91 | 92 | ### Developer mode 93 | 94 | To help with debugging, you can enable a special feature that saves mock data to a separate file. 95 | 96 | To do this, simply set the environment variable called `MOCKS_DEBUG_OVERWRITTEN_FILE`. 97 | 98 | ```shell 99 | MOCKS_DEBUG_OVERWRITTEN_FILE=storage.debug.json cargo run -- storage.json 100 | ``` 101 | 102 | We recommend specifying the filename as `*.debug.json`. For more details, please check [.gitignore](.gitignore) file. 103 | 104 | ## LICENSE 105 | 106 | This project is licensed under the [MIT license](LICENSE). 107 | -------------------------------------------------------------------------------- /docs/DOCUMENT.md: -------------------------------------------------------------------------------- 1 | # mocks docs 2 | 3 | ## Build for Homebrew 4 | 5 | ```shell 6 | cargo build --release 7 | cd target/release 8 | tar -czf mocks-X.X.X-x86_64-apple-darwin.tar.gz mocks 9 | shasum -a 256 mocks-X.X.X-x86_64-apple-darwin.tar.gz 10 | ``` 11 | -------------------------------------------------------------------------------- /runn-e2e/README.md: -------------------------------------------------------------------------------- 1 | # E2E testing with runn 2 | 3 | ## About 4 | 5 | > `runn` ( means "Run N". is pronounced /rʌ́n én/. ) is a package/tool for running operations following a scenario. 6 | 7 | > Key features of runn are: 8 | > - As a tool for scenario based testing. 9 | > - As a test helper package for the Go language. 10 | > - As a tool for workflow automation. 11 | > - Support HTTP request, gRPC request, DB query, Chrome DevTools Protocol, and SSH/Local command execution 12 | > - OpenAPI Document-like syntax for HTTP request testing. 13 | > - Single binary = CI-Friendly. 14 | 15 | See more: [k1LoW/runn: runn is a package/tool for running operations following a scenario.](https://github.com/k1LoW/runn) 16 | 17 | ## Testing 18 | 19 | ### Install 20 | 21 | Homebrew 22 | 23 | ```shell 24 | brew install k1LoW/tap/runn 25 | ``` 26 | 27 | Golang 28 | 29 | ```shell 30 | go install github.com/k1LoW/runn/cmd/runn@latest 31 | ``` 32 | 33 | ### Run E2E 34 | 35 | ```shell 36 | cd ./runn-e2e 37 | runn run runbooks/test.yml --verbose 38 | ``` 39 | -------------------------------------------------------------------------------- /runn-e2e/runbooks/test.yml: -------------------------------------------------------------------------------- 1 | desc: "mocks tests" 2 | runners: 3 | req: 4 | endpoint: http://localhost:3000 5 | steps: 6 | hc: 7 | desc: "Health Check" 8 | req: 9 | /_hc: 10 | get: 11 | body: 12 | application/json: null 13 | test: | 14 | // Status code must be 204. 15 | current.res.status == 204 16 | resourceNotFound: 17 | desc: "Resource is not found." 18 | req: 19 | /errors: 20 | get: 21 | body: 22 | application/json: null 23 | test: | 24 | // Status code must be 404. 25 | current.res.status == 404 26 | post: 27 | include: 28 | path: test_post.yml 29 | postError: 30 | include: 31 | path: test_post_error.yml 32 | comment: 33 | include: 34 | path: test_comment.yml 35 | commentError: 36 | include: 37 | path: test_comment_error.yml 38 | profile: 39 | include: 40 | path: test_profile.yml 41 | profileError: 42 | include: 43 | path: test_profile_error.yml 44 | -------------------------------------------------------------------------------- /runn-e2e/runbooks/test_comment.yml: -------------------------------------------------------------------------------- 1 | desc: "comment endpoint tests" 2 | runners: 3 | req: 4 | endpoint: http://localhost:3000 5 | vars: 6 | commentId: 3 7 | if: included 8 | steps: 9 | postComment: 10 | desc: "Create new comment" 11 | req: 12 | /comments: 13 | post: 14 | body: 15 | application/json: 16 | id: "{{ vars.commentId }}" 17 | text: "new comment" 18 | post_id: "{{ vars.postId }}" 19 | test: | 20 | // Status code must be 201. 21 | current.res.status == 201 22 | && current.res.body.id == vars.commentId 23 | && current.res.body.text == "new comment" 24 | && current.res.body.post_id == vars.postId 25 | getComment: 26 | desc: "Get created comment" 27 | req: 28 | /comments/{{ vars.commentId }}: 29 | get: 30 | body: 31 | application/json: null 32 | test: | 33 | // Status code must be 200. 34 | current.res.status == 200 35 | && current.res.body.id == vars.commentId 36 | && current.res.body.text == "new comment" 37 | && current.res.body.post_id == vars.postId 38 | putComment: 39 | desc: "Update comment" 40 | req: 41 | /comments/{{ vars.commentId }}: 42 | put: 43 | body: 44 | application/json: 45 | id: "{{ vars.commentId }}" 46 | text: "putted comment" 47 | post_id: "{{ vars.postId }}" 48 | likes: 10 49 | test: | 50 | // Status code must be 200. 51 | current.res.status == 200 52 | && current.res.body.id == vars.commentId 53 | && current.res.body.text == "putted comment" 54 | && current.res.body.post_id == vars.postId 55 | && current.res.body.likes == 10 56 | patchComment: 57 | desc: "Change comment data" 58 | req: 59 | /comments/{{ vars.commentId }}: 60 | patch: 61 | body: 62 | application/json: 63 | text: "patched comment" 64 | likes: 20 65 | test: | 66 | // Status code must be 200. 67 | current.res.status == 200 68 | && current.res.body.id == vars.commentId 69 | && current.res.body.text == "patched comment" 70 | && current.res.body.post_id == vars.postId 71 | && current.res.body.likes == 20 72 | getAllComments: 73 | desc: "Get all comments" 74 | req: 75 | /comments: 76 | get: 77 | body: 78 | application/json: null 79 | test: | 80 | // Status code must be 200. 81 | current.res.status == 200 82 | && len(current.res.body.comments) > 0 83 | deleteComment: 84 | desc: "Delete comment" 85 | req: 86 | /comments/{{ vars.commentId }}: 87 | delete: 88 | body: 89 | application/json: null 90 | test: | 91 | // Status code must be 200. 92 | current.res.status == 200 93 | && current.res.body.id == vars.commentId 94 | -------------------------------------------------------------------------------- /runn-e2e/runbooks/test_comment_error.yml: -------------------------------------------------------------------------------- 1 | desc: "comment endpoint error tests" 2 | runners: 3 | req: 4 | endpoint: http://localhost:3000 5 | vars: 6 | commentId: 1 7 | duplicateCommentId: 1 8 | if: included 9 | steps: 10 | postCommentDuplicateIdError: 11 | desc: "Failed due to duplicate ID" 12 | req: 13 | /comments: 14 | post: 15 | body: 16 | application/json: 17 | id: "{{ vars.duplicateCommentId }}" 18 | text: "new comment" 19 | post_id: "{{ vars.postId }}" 20 | test: | 21 | // Status code must be 409. 22 | current.res.status == 409 23 | postCommentRequiredIdError: 24 | desc: "Failed because the ID does not exist" 25 | req: 26 | /comments: 27 | post: 28 | body: 29 | application/json: 30 | text: "nothing id comment" 31 | post_id: "{{ vars.postId }}" 32 | test: | 33 | // Status code must be 400. 34 | current.res.status == 400 35 | putCommentRequiredIdError: 36 | desc: "Failed because the ID does not exist" 37 | req: 38 | /comments/{{ vars.commentId }}: 39 | put: 40 | body: 41 | application/json: 42 | text: "nothing id comment" 43 | post_id: "{{ vars.postId }}" 44 | test: | 45 | // Status code must be 400. 46 | current.res.status == 400 47 | getCommentNotFoundError: 48 | desc: "Post is not found" 49 | req: 50 | /comments/999999: 51 | get: 52 | body: 53 | application/json: null 54 | test: | 55 | // Status code must be 404. 56 | current.res.status == 404 57 | -------------------------------------------------------------------------------- /runn-e2e/runbooks/test_post.yml: -------------------------------------------------------------------------------- 1 | desc: "post endpoint tests" 2 | runners: 3 | req: 4 | endpoint: http://localhost:3000 5 | vars: 6 | postId: "01J85A9VQ8ZDGXDC7A00YRWKBE" 7 | if: included 8 | steps: 9 | postPost: 10 | desc: "Create new post" 11 | req: 12 | /posts: 13 | post: 14 | body: 15 | application/json: 16 | id: "{{ vars.postId }}" 17 | title: "new post" 18 | views: 0 19 | test: | 20 | // Status code must be 201. 21 | current.res.status == 201 22 | && current.res.body.id == vars.postId 23 | && current.res.body.title == "new post" 24 | && current.res.body.views == 0 25 | getPost: 26 | desc: "Get created post" 27 | req: 28 | /posts/{{ vars.postId }}: 29 | get: 30 | body: 31 | application/json: null 32 | test: | 33 | // Status code must be 200. 34 | current.res.status == 200 35 | && current.res.body.id == vars.postId 36 | && current.res.body.title == "new post" 37 | && current.res.body.views == 0 38 | putPost: 39 | desc: "Update post" 40 | req: 41 | /posts/{{ vars.postId }}: 42 | put: 43 | body: 44 | application/json: 45 | id: "{{ vars.postId }}" 46 | title: "putted post" 47 | views: 1000 48 | likes: 100 49 | test: | 50 | // Status code must be 200. 51 | current.res.status == 200 52 | && current.res.body.id == vars.postId 53 | && current.res.body.title == "putted post" 54 | && current.res.body.views == 1000 55 | && current.res.body.likes == 100 56 | patchPost: 57 | desc: "Change post data" 58 | req: 59 | /posts/{{ vars.postId }}: 60 | patch: 61 | body: 62 | application/json: 63 | title: "patched post" 64 | views: 2000 65 | test: | 66 | // Status code must be 200. 67 | current.res.status == 200 68 | && current.res.body.id == vars.postId 69 | && current.res.body.title == "patched post" 70 | && current.res.body.views == 2000 71 | && current.res.body.likes == 100 72 | getAllPosts: 73 | desc: "Get all posts" 74 | req: 75 | /posts: 76 | get: 77 | body: 78 | application/json: null 79 | test: | 80 | // Status code must be 200. 81 | current.res.status == 200 82 | && len(current.res.body.posts) > 0 83 | deletePost: 84 | desc: "Delete post" 85 | req: 86 | /posts/{{ vars.postId }}: 87 | delete: 88 | body: 89 | application/json: null 90 | test: | 91 | // Status code must be 200. 92 | current.res.status == 200 93 | && current.res.body.id == vars.postId 94 | -------------------------------------------------------------------------------- /runn-e2e/runbooks/test_post_error.yml: -------------------------------------------------------------------------------- 1 | desc: "post endpoint error tests" 2 | runners: 3 | req: 4 | endpoint: http://localhost:3000 5 | vars: 6 | postId: "01J7BAKH37HPG116ZRRFKHBDGB" 7 | duplicatePostId: "01J7BAKH37HPG116ZRRFKHBDGB" 8 | if: included 9 | steps: 10 | postPostDuplicateIdError: 11 | desc: "Failed due to duplicate ID" 12 | req: 13 | /posts: 14 | post: 15 | body: 16 | application/json: 17 | id: "{{ vars.duplicatePostId }}" 18 | title: "new post" 19 | views: 0 20 | test: | 21 | // Status code must be 409. 22 | current.res.status == 409 23 | postPostRequiredIdError: 24 | desc: "Failed because the ID does not exist" 25 | req: 26 | /posts: 27 | post: 28 | body: 29 | application/json: 30 | title: "nothing id post" 31 | views: 0 32 | test: | 33 | // Status code must be 400. 34 | current.res.status == 400 35 | putPostRequiredIdError: 36 | desc: "Failed because the ID does not exist" 37 | req: 38 | /posts/{{ vars.postId }}: 39 | put: 40 | body: 41 | application/json: 42 | title: "nothing id post" 43 | views: 0 44 | test: | 45 | // Status code must be 400. 46 | current.res.status == 400 47 | getPostNotFoundError: 48 | desc: "Comment is not found" 49 | req: 50 | /posts/notfound: 51 | get: 52 | body: 53 | application/json: null 54 | test: | 55 | // Status code must be 404. 56 | current.res.status == 404 57 | -------------------------------------------------------------------------------- /runn-e2e/runbooks/test_profile.yml: -------------------------------------------------------------------------------- 1 | desc: "profile endpoint tests" 2 | runners: 3 | req: 4 | endpoint: http://localhost:3000 5 | vars: 6 | profileId: "01J7BAQE1GMD78FN3J0FJCNS8T" 7 | if: included 8 | steps: 9 | putProfile: 10 | desc: "Update profile" 11 | req: 12 | /profile: 13 | put: 14 | body: 15 | application/json: 16 | id: "{{ vars.profileId }}" 17 | name: "John Smith" 18 | age: 20 19 | test: | 20 | // Status code must be 200. 21 | current.res.status == 200 22 | && current.res.body.id == vars.profileId 23 | && current.res.body.name == "John Smith" 24 | && current.res.body.age == 20 25 | patchProfile: 26 | desc: "Change profile data" 27 | req: 28 | /profile: 29 | patch: 30 | body: 31 | application/json: 32 | age: 30 33 | test: | 34 | // Status code must be 200. 35 | current.res.status == 200 36 | && current.res.body.id == vars.profileId 37 | && current.res.body.name == "John Smith" 38 | && current.res.body.age == 30 39 | getProfile: 40 | desc: "Get profile" 41 | req: 42 | /profile: 43 | get: 44 | body: 45 | application/json: null 46 | test: | 47 | // Status code must be 200. 48 | current.res.status == 200 49 | && current.res.body.profile.id == vars.profileId 50 | && current.res.body.profile.name == "John Smith" 51 | && current.res.body.profile.age == 30 52 | -------------------------------------------------------------------------------- /runn-e2e/runbooks/test_profile_error.yml: -------------------------------------------------------------------------------- 1 | desc: "profile endpoint error tests" 2 | runners: 3 | req: 4 | endpoint: http://localhost:3000 5 | vars: 6 | profileId: "01J7BAQE1GMD78FN3J0FJCNS8T" 7 | if: included 8 | steps: 9 | putProfileRequiredIdError: 10 | desc: "Failed because the ID does not exist" 11 | req: 12 | /profile: 13 | put: 14 | body: 15 | application/json: 16 | name: "John Smith" 17 | age: 20 18 | test: | 19 | // Status code must be 200. 20 | current.res.status == 400 21 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use axum::http::StatusCode; 2 | use axum::response::{IntoResponse, Response}; 3 | use axum::Json; 4 | use serde_json::json; 5 | use std::fmt; 6 | 7 | pub const EXCEPTION_ERROR_MESSAGE: &str = "An unexpected error occurred."; 8 | 9 | #[derive(Debug, Eq, PartialEq)] 10 | pub enum MocksError { 11 | FailedReadFile(String), 12 | FailedWriteFile(String), 13 | InvalidArgs(String), 14 | Exception(String), 15 | ResourceNotFound, 16 | ObjectNotFound, 17 | MethodNotAllowed, 18 | InvalidRequest, 19 | DuplicateId, 20 | } 21 | 22 | impl std::error::Error for MocksError { 23 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 24 | None 25 | } 26 | } 27 | 28 | impl fmt::Display for MocksError { 29 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 30 | match self { 31 | Self::FailedReadFile(err) => write!(fmt, "{err}"), 32 | Self::FailedWriteFile(err) => write!(fmt, "{err}"), 33 | Self::InvalidArgs(err) => write!(fmt, "{err}"), 34 | Self::Exception(err) => write!(fmt, "{err}"), 35 | Self::ResourceNotFound => write!(fmt, "Resource not found."), 36 | Self::ObjectNotFound => write!(fmt, "Object not found."), 37 | Self::MethodNotAllowed => write!(fmt, "Method not allowed."), 38 | Self::InvalidRequest => write!(fmt, "Invalid request."), 39 | Self::DuplicateId => write!(fmt, "Duplicate ID."), 40 | } 41 | } 42 | } 43 | 44 | impl IntoResponse for MocksError { 45 | fn into_response(self) -> Response { 46 | let (status, message) = match self { 47 | MocksError::FailedReadFile(err) 48 | | MocksError::FailedWriteFile(err) 49 | | MocksError::InvalidArgs(err) 50 | | MocksError::Exception(err) => (StatusCode::INTERNAL_SERVER_ERROR, err), 51 | MocksError::ResourceNotFound | MocksError::ObjectNotFound => { 52 | (StatusCode::NOT_FOUND, self.to_string()) 53 | } 54 | MocksError::MethodNotAllowed => (StatusCode::METHOD_NOT_ALLOWED, self.to_string()), 55 | MocksError::InvalidRequest => (StatusCode::BAD_REQUEST, self.to_string()), 56 | MocksError::DuplicateId => (StatusCode::CONFLICT, self.to_string()), 57 | }; 58 | 59 | (status, Json(json!({ "error": message }))).into_response() 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod error; 2 | mod server; 3 | mod storage; 4 | 5 | use crate::error::MocksError; 6 | use crate::server::Server; 7 | use crate::storage::Storage; 8 | use clap::Parser; 9 | use std::net::{IpAddr, SocketAddr}; 10 | 11 | #[derive(Parser, Debug)] 12 | #[command(version, about, long_about = None)] 13 | struct Args { 14 | /// Path of json file for data storage 15 | file: String, 16 | 17 | /// Host 18 | #[arg(short = 'H', long, default_value = "localhost")] 19 | host: String, 20 | 21 | /// Port 22 | #[arg(short, long, default_value_t = 3000)] 23 | port: u16, 24 | 25 | /// No overwrite save to json file 26 | #[arg(long, default_value_t = false)] 27 | no_overwrite: bool, 28 | } 29 | 30 | #[tokio::main] 31 | async fn main() -> Result<(), MocksError> { 32 | let args = Args::parse(); 33 | let socket_addr = init(&args.host, args.port)?; 34 | 35 | println!("`mocks` started"); 36 | println!("Press CTRL-C to stop"); 37 | 38 | let url = format!("http://{}:{}", &args.host, args.port); 39 | let overwrite = !args.no_overwrite; 40 | 41 | print_startup_info(&url, &args.file, overwrite); 42 | 43 | let storage = Storage::new(&args.file, overwrite)?; 44 | Server::startup(socket_addr, &url, storage).await?; 45 | 46 | Ok(()) 47 | } 48 | 49 | fn init(host: &str, port: u16) -> Result { 50 | let ip_addr = if host == "localhost" { 51 | "127.0.0.1" 52 | } else { 53 | host 54 | }; 55 | 56 | ip_addr 57 | .parse::() 58 | .map(|ip| SocketAddr::from((ip, port))) 59 | .map_err(|e| MocksError::InvalidArgs(e.to_string())) 60 | } 61 | 62 | fn print_startup_info(url: &str, file: &str, overwrite: bool) { 63 | println!("\nIndex:\n{}", url); 64 | println!("\nStorage files:\n{}", file); 65 | println!("\nOverwrite:\n{}", if overwrite { "YES" } else { "NO" }); 66 | println!(); 67 | } 68 | -------------------------------------------------------------------------------- /src/server.rs: -------------------------------------------------------------------------------- 1 | mod context; 2 | mod handler; 3 | mod state; 4 | 5 | use crate::error::MocksError; 6 | use crate::server::handler::delete::delete; 7 | use crate::server::handler::get::{get_all, get_one}; 8 | use crate::server::handler::hc::hc; 9 | use crate::server::handler::patch::{patch, patch_one}; 10 | use crate::server::handler::post::post; 11 | use crate::server::handler::put::{put, put_one}; 12 | use crate::server::state::{AppState, SharedState}; 13 | use crate::storage::Storage; 14 | use axum::routing::get; 15 | use axum::Router; 16 | use std::net::SocketAddr; 17 | use tokio::net::TcpListener; 18 | 19 | /// Mock server module 20 | pub struct Server {} 21 | 22 | impl Server { 23 | /// Starts the mock server 24 | /// 25 | /// # Arguments 26 | /// * `socket_addr` - The socket address to bind the server to 27 | /// * `url` - The base URL of the server 28 | /// * `storage` - The storage instance to use 29 | /// 30 | /// # Returns 31 | /// * `Result<(), MocksError>` - Ok if the server starts successfully, Err otherwise 32 | pub async fn startup( 33 | socket_addr: SocketAddr, 34 | url: &str, 35 | storage: Storage, 36 | ) -> Result<(), MocksError> { 37 | let listener = TcpListener::bind(socket_addr) 38 | .await 39 | .map_err(|e| MocksError::Exception(e.to_string()))?; 40 | 41 | println!("Endpoints:"); 42 | print_endpoints(url, storage.resources()); 43 | println!(); 44 | 45 | let state = AppState::new(storage); 46 | let router = create_router(state); 47 | axum::serve(listener, router) 48 | .await 49 | .map_err(|e| MocksError::Exception(e.to_string())) 50 | } 51 | } 52 | 53 | fn print_endpoints(url: &str, resources: Vec) { 54 | let mut endpoints = vec![format!("{}/_hc", url)]; 55 | 56 | for r in resources { 57 | endpoints.push(format!("{}/{}", url, r)); 58 | } 59 | 60 | for endpoint in endpoints { 61 | println!("{}", endpoint); 62 | } 63 | } 64 | 65 | fn create_router(state: SharedState) -> Router { 66 | let hc_router = Router::new().route("/", get(hc)); 67 | let storage_router = Router::new() 68 | .route("/", get(get_all).post(post).put(put_one).patch(patch_one)) 69 | .route("/:id", get(get_one).put(put).patch(patch).delete(delete)); 70 | 71 | Router::new() 72 | .nest("/_hc", hc_router) 73 | .nest("/:resource", storage_router) 74 | .with_state(state) 75 | } 76 | -------------------------------------------------------------------------------- /src/server/context.rs: -------------------------------------------------------------------------------- 1 | use axum::extract::rejection::JsonRejection; 2 | use axum::extract::FromRequest; 3 | use axum::http::StatusCode; 4 | use axum::{async_trait, Json}; 5 | use serde_json::{json, Value}; 6 | 7 | const INVALID_JSON_REQUEST: &str = "Invalid JSON format in request body."; 8 | 9 | #[derive(Debug, Clone, Default)] 10 | pub struct Payload(pub Value); 11 | 12 | #[async_trait] 13 | impl FromRequest for Payload 14 | where 15 | S: Send + Sync, 16 | Json: FromRequest, 17 | { 18 | type Rejection = (StatusCode, Json); 19 | 20 | async fn from_request(req: axum::extract::Request, state: &S) -> Result { 21 | let Json(value) = Json::::from_request(req, state) 22 | .await 23 | .map_err(|e| to_rejection(&e.to_string()))?; 24 | 25 | if !value.is_object() { 26 | return Err(to_rejection(INVALID_JSON_REQUEST)); 27 | } 28 | 29 | Ok(Payload(value)) 30 | } 31 | } 32 | 33 | #[derive(Debug, Clone, Default)] 34 | pub struct PayloadWithId(pub Value); 35 | 36 | #[async_trait] 37 | impl FromRequest for PayloadWithId 38 | where 39 | S: Send + Sync, 40 | Json: FromRequest, 41 | { 42 | type Rejection = (StatusCode, Json); 43 | 44 | async fn from_request(req: axum::extract::Request, state: &S) -> Result { 45 | let Json(value) = Json::::from_request(req, state) 46 | .await 47 | .map_err(|e| to_rejection(&e.to_string()))?; 48 | 49 | if !value.is_object() { 50 | return Err(to_rejection(INVALID_JSON_REQUEST)); 51 | } 52 | 53 | // ID is required for updates 54 | if value.get("id").is_none() { 55 | return Err(to_rejection("ID is required for creation or update.")); 56 | } 57 | 58 | Ok(PayloadWithId(value)) 59 | } 60 | } 61 | 62 | fn to_rejection(message: &str) -> (StatusCode, Json) { 63 | let json = Json::from(json!({"error": message})); 64 | (StatusCode::BAD_REQUEST, json) 65 | } 66 | -------------------------------------------------------------------------------- /src/server/handler.rs: -------------------------------------------------------------------------------- 1 | pub mod delete; 2 | pub mod get; 3 | pub mod hc; 4 | pub mod patch; 5 | pub mod post; 6 | pub mod put; 7 | 8 | #[cfg(test)] 9 | mod tests { 10 | use crate::server::state::AppState; 11 | use crate::server::state::SharedState; 12 | use crate::storage::Storage; 13 | 14 | pub(crate) fn init_state() -> SharedState { 15 | let storage = Storage::new("storage.json", false) 16 | .unwrap_or_else(|e| panic!("Failed to init storage: {}", e)); 17 | AppState::new(storage) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/server/handler/delete.rs: -------------------------------------------------------------------------------- 1 | use crate::error::MocksError; 2 | use crate::server::state::SharedState; 3 | use axum::extract::{Path, State}; 4 | use axum::http::StatusCode; 5 | use axum::response::IntoResponse; 6 | use axum::Json; 7 | 8 | pub async fn delete( 9 | Path((resource, id)): Path<(String, String)>, 10 | state: State, 11 | ) -> Result { 12 | let mut state = state 13 | .lock() 14 | .map_err(|e| MocksError::Exception(e.to_string()))?; 15 | 16 | let value = state.storage.delete(&resource, &id)?; 17 | Ok((StatusCode::OK, Json(value))) 18 | } 19 | 20 | #[cfg(test)] 21 | mod tests { 22 | use crate::server::handler::delete::delete; 23 | use axum::extract::{Path, State}; 24 | 25 | #[tokio::test] 26 | async fn test_delete() { 27 | let state = crate::server::handler::tests::init_state(); 28 | let path: Path<(String, String)> = Path(( 29 | "posts".to_string(), 30 | "01J7BAKH37HPG116ZRRFKHBDGB".to_string(), 31 | )); 32 | assert!(delete(path, State(state)).await.is_ok()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/server/handler/get.rs: -------------------------------------------------------------------------------- 1 | use crate::error::MocksError; 2 | use crate::server::state::SharedState; 3 | use axum::extract::{Path, State}; 4 | use axum::http::StatusCode; 5 | use axum::response::IntoResponse; 6 | use axum::Json; 7 | use serde_json::json; 8 | 9 | pub async fn get_all( 10 | Path(resource): Path, 11 | state: State, 12 | ) -> Result { 13 | let state = state 14 | .lock() 15 | .map_err(|e| MocksError::Exception(e.to_string()))?; 16 | 17 | let value = state.storage.get_all(&resource)?; 18 | let response = json!({ 19 | resource: value 20 | }); 21 | 22 | Ok((StatusCode::OK, Json(response))) 23 | } 24 | 25 | pub async fn get_one( 26 | Path((resource, id)): Path<(String, String)>, 27 | state: State, 28 | ) -> Result { 29 | let state = state 30 | .lock() 31 | .map_err(|e| MocksError::Exception(e.to_string()))?; 32 | 33 | let value = state.storage.get_one(&resource, &id)?; 34 | Ok((StatusCode::OK, Json(value))) 35 | } 36 | 37 | #[cfg(test)] 38 | mod tests { 39 | use crate::server::handler::get::{get_all, get_one}; 40 | use crate::server::handler::tests::init_state; 41 | use axum::extract::{Path, State}; 42 | 43 | #[tokio::test] 44 | async fn test_get_all() { 45 | let state = init_state(); 46 | let path: Path = Path("posts".to_string()); 47 | assert!(get_all(path, State(state)).await.is_ok()); 48 | } 49 | 50 | #[tokio::test] 51 | async fn test_get_one() { 52 | let state = init_state(); 53 | let path: Path<(String, String)> = Path(( 54 | "posts".to_string(), 55 | "01J7BAKH37HPG116ZRRFKHBDGB".to_string(), 56 | )); 57 | assert!(get_one(path, State(state)).await.is_ok()); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/server/handler/hc.rs: -------------------------------------------------------------------------------- 1 | use axum::http::StatusCode; 2 | use axum::response::IntoResponse; 3 | 4 | pub async fn hc() -> impl IntoResponse { 5 | StatusCode::NO_CONTENT 6 | } 7 | 8 | #[cfg(test)] 9 | mod tests { 10 | use crate::server::handler::hc::hc; 11 | use axum::http::StatusCode; 12 | use axum::response::IntoResponse; 13 | 14 | #[tokio::test] 15 | async fn test_hc() { 16 | let resp = hc().await.into_response(); 17 | assert_eq!(resp.status(), StatusCode::NO_CONTENT); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/server/handler/patch.rs: -------------------------------------------------------------------------------- 1 | use crate::error::MocksError; 2 | use crate::server::context::Payload; 3 | use crate::server::state::SharedState; 4 | use axum::extract::{Path, State}; 5 | use axum::http::StatusCode; 6 | use axum::response::IntoResponse; 7 | use axum::Json; 8 | 9 | pub async fn patch( 10 | Path((resource, id)): Path<(String, String)>, 11 | state: State, 12 | Payload(input): Payload, 13 | ) -> Result { 14 | let mut state = state 15 | .lock() 16 | .map_err(|e| MocksError::Exception(e.to_string()))?; 17 | 18 | let value = state.storage.update(&resource, &id, &input)?; 19 | Ok((StatusCode::OK, Json(value))) 20 | } 21 | 22 | pub async fn patch_one( 23 | Path(resource): Path, 24 | state: State, 25 | Payload(input): Payload, 26 | ) -> Result { 27 | let mut state = state 28 | .lock() 29 | .map_err(|e| MocksError::Exception(e.to_string()))?; 30 | 31 | let value = state.storage.update_one(&resource, &input)?; 32 | Ok((StatusCode::OK, Json(value))) 33 | } 34 | 35 | #[cfg(test)] 36 | mod tests { 37 | use crate::server::context::Payload; 38 | use crate::server::handler::patch::{patch, patch_one}; 39 | use crate::server::handler::tests::init_state; 40 | use axum::extract::{Path, State}; 41 | use serde_json::json; 42 | 43 | #[tokio::test] 44 | async fn test_patch() { 45 | let state = init_state(); 46 | let path: Path<(String, String)> = Path(( 47 | "posts".to_string(), 48 | "01J7BAKH37HPG116ZRRFKHBDGB".to_string(), 49 | )); 50 | let payload = json!({"title":"patched post","views":200}); 51 | assert!(patch(path, State(state), Payload(payload)).await.is_ok()); 52 | } 53 | 54 | #[tokio::test] 55 | async fn test_patch_one() { 56 | let state = init_state(); 57 | let path: Path = Path("profile".to_string()); 58 | let payload = json!({"name":"Jane Smith","age":30}); 59 | assert!(patch_one(path, State(state), Payload(payload)) 60 | .await 61 | .is_ok()); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/server/handler/post.rs: -------------------------------------------------------------------------------- 1 | use crate::error::MocksError; 2 | use crate::server::context::PayloadWithId; 3 | use crate::server::state::SharedState; 4 | use axum::extract::{Path, State}; 5 | use axum::http::StatusCode; 6 | use axum::response::IntoResponse; 7 | use axum::Json; 8 | 9 | pub async fn post( 10 | Path(resource): Path, 11 | state: State, 12 | PayloadWithId(input): PayloadWithId, 13 | ) -> Result { 14 | let mut state = state 15 | .lock() 16 | .map_err(|e| MocksError::Exception(e.to_string()))?; 17 | 18 | let value = state.storage.insert(&resource, &input)?; 19 | Ok((StatusCode::CREATED, Json(value))) 20 | } 21 | 22 | #[cfg(test)] 23 | mod tests { 24 | use crate::server::context::PayloadWithId; 25 | use crate::server::handler::post::post; 26 | use crate::server::handler::tests::init_state; 27 | use axum::extract::{Path, State}; 28 | use serde_json::json; 29 | 30 | #[tokio::test] 31 | async fn test_post() { 32 | let state = init_state(); 33 | let path: Path = Path("posts".to_string()); 34 | let payload = json!({"id":"01J8593X0V7Q34X011BYD92CHP","title":"posted post","views":0}); 35 | assert!(post(path, State(state), PayloadWithId(payload)) 36 | .await 37 | .is_ok()); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/server/handler/put.rs: -------------------------------------------------------------------------------- 1 | use crate::error::MocksError; 2 | use crate::server::context::PayloadWithId; 3 | use crate::server::state::SharedState; 4 | use axum::extract::{Path, State}; 5 | use axum::http::StatusCode; 6 | use axum::response::IntoResponse; 7 | use axum::Json; 8 | 9 | pub async fn put( 10 | Path((resource, id)): Path<(String, String)>, 11 | state: State, 12 | PayloadWithId(input): PayloadWithId, 13 | ) -> Result { 14 | let mut state = state 15 | .lock() 16 | .map_err(|e| MocksError::Exception(e.to_string()))?; 17 | 18 | let value = state.storage.replace(&resource, &id, &input)?; 19 | Ok((StatusCode::OK, Json(value))) 20 | } 21 | 22 | pub async fn put_one( 23 | Path(resource): Path, 24 | state: State, 25 | PayloadWithId(input): PayloadWithId, 26 | ) -> Result { 27 | let mut state = state 28 | .lock() 29 | .map_err(|e| MocksError::Exception(e.to_string()))?; 30 | 31 | let value = state.storage.replace_one(&resource, &input)?; 32 | Ok((StatusCode::OK, Json(value))) 33 | } 34 | 35 | #[cfg(test)] 36 | mod tests { 37 | use crate::server::context::PayloadWithId; 38 | use crate::server::handler::put::{put, put_one}; 39 | use crate::server::handler::tests::init_state; 40 | use axum::extract::{Path, State}; 41 | use serde_json::json; 42 | 43 | #[tokio::test] 44 | async fn test_put() { 45 | let state = init_state(); 46 | let path: Path<(String, String)> = Path(( 47 | "posts".to_string(), 48 | "01J7BAKH37HPG116ZRRFKHBDGB".to_string(), 49 | )); 50 | let payload = json!({"id":"01J7BAKH37HPG116ZRRFKHBDGB","title":"putted post","views":200}); 51 | assert!(put(path, State(state), PayloadWithId(payload)) 52 | .await 53 | .is_ok()); 54 | } 55 | 56 | #[tokio::test] 57 | async fn test_put_one() { 58 | let state = init_state(); 59 | let path: Path = Path("profile".to_string()); 60 | let payload = json!({"id":1,"name":"John Smith","age":25}); 61 | assert!(put_one(path, State(state), PayloadWithId(payload)) 62 | .await 63 | .is_ok()); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/server/state.rs: -------------------------------------------------------------------------------- 1 | use crate::storage::Storage; 2 | use std::sync::{Arc, Mutex}; 3 | 4 | pub type SharedState = Arc>; 5 | 6 | pub struct AppState { 7 | pub storage: Storage, 8 | } 9 | 10 | impl AppState { 11 | pub fn new(storage: Storage) -> SharedState { 12 | Arc::new(Mutex::new(AppState { storage })) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/storage.rs: -------------------------------------------------------------------------------- 1 | use crate::error::MocksError; 2 | use crate::storage::operation::insert::insert; 3 | use crate::storage::operation::remove::remove; 4 | use crate::storage::operation::replace::replace; 5 | use crate::storage::operation::replace_one::replace_one; 6 | use crate::storage::operation::select_all::select_all; 7 | use crate::storage::operation::select_one::select_one; 8 | use crate::storage::operation::update::update; 9 | use crate::storage::operation::update_one::update_one; 10 | use crate::storage::reader::Reader; 11 | use crate::storage::writer::Writer; 12 | use serde_json::Value; 13 | 14 | mod operation; 15 | mod reader; 16 | mod writer; 17 | 18 | pub type StorageData = Value; 19 | pub type Input = Value; 20 | 21 | /// Storage module 22 | pub struct Storage { 23 | pub file: String, 24 | pub data: StorageData, 25 | pub overwrite: bool, 26 | } 27 | 28 | impl Storage { 29 | /// Create a new Storage instance 30 | /// 31 | /// # Arguments 32 | /// - `path` - The file path for storage 33 | /// - `overwrite` - Whether to overwrite the file on changes 34 | pub fn new(path: &str, overwrite: bool) -> Result { 35 | let data = Reader::new(path).read()?; 36 | Ok(Storage { 37 | file: path.to_string(), 38 | data, 39 | overwrite, 40 | }) 41 | } 42 | 43 | /// Resources for API endpoints 44 | pub fn resources(&self) -> Vec { 45 | let mut resources = vec![]; 46 | if let Value::Object(obj) = &self.data { 47 | for (key, val) in obj { 48 | if !key.is_empty() && (val.is_object() || val.is_array()) { 49 | resources.push(key.to_string()); 50 | } 51 | } 52 | } 53 | 54 | resources 55 | } 56 | 57 | /// **GET** 58 | /// Retrieve all items for a given resource 59 | pub fn get_all(&self, resource_key: &str) -> Result { 60 | self.fetch(|data| select_all(data, resource_key)) 61 | } 62 | 63 | /// **GET** 64 | /// Retrieve a specific item from a resource 65 | pub fn get_one(&self, resource_key: &str, item_key: &str) -> Result { 66 | self.fetch(|data| select_one(data, resource_key, item_key)) 67 | } 68 | 69 | /// **POST** 70 | /// Insert a new item into a resource 71 | pub fn insert(&mut self, resource_key: &str, input: &Value) -> Result { 72 | self.operate(|data| insert(data, resource_key, input)) 73 | } 74 | 75 | /// **PUT** 76 | /// Replace an entire item in a resource 77 | pub fn replace( 78 | &mut self, 79 | resource_key: &str, 80 | item_key: &str, 81 | input: &Value, 82 | ) -> Result { 83 | self.operate(|data| replace(data, resource_key, item_key, input)) 84 | } 85 | 86 | /// **PUT** 87 | /// Replace the first item in a resource 88 | pub fn replace_one(&mut self, resource_key: &str, input: &Value) -> Result { 89 | self.operate(|data| replace_one(data, resource_key, input)) 90 | } 91 | 92 | /// **PATCH** 93 | /// Update parts of an item in a resource 94 | pub fn update( 95 | &mut self, 96 | resource_key: &str, 97 | item_key: &str, 98 | input: &Value, 99 | ) -> Result { 100 | self.operate(|data| update(data, resource_key, item_key, input)) 101 | } 102 | 103 | /// **PATCH** 104 | /// Update parts of the first item in a resource 105 | pub fn update_one(&mut self, resource_key: &str, input: &Value) -> Result { 106 | self.operate(|data| update_one(data, resource_key, input)) 107 | } 108 | 109 | /// **DELETE** 110 | /// Delete an item from a resource 111 | pub fn delete(&mut self, resource_key: &str, item_key: &str) -> Result { 112 | self.operate(|data| remove(data, resource_key, item_key)) 113 | } 114 | 115 | /// Fetches data from the storage using the provided operation 116 | /// 117 | /// This method abstracts the common pattern of performing a fetch operation, 118 | /// and returning the result. 119 | fn fetch(&self, operation: F) -> Result 120 | where 121 | F: FnOnce(&StorageData) -> Result, 122 | { 123 | let result = operation(&self.data)?; 124 | Ok(result) 125 | } 126 | 127 | /// Perform an operation on the storage data and write changes if successful 128 | /// 129 | /// This method abstracts the common pattern of performing an operation, 130 | /// writing the changes, and returning the result. 131 | fn operate(&mut self, operation: F) -> Result 132 | where 133 | F: FnOnce(&mut StorageData) -> Result, 134 | { 135 | let result = operation(&mut self.data)?; 136 | self.write()?; 137 | Ok(result) 138 | } 139 | 140 | /// Write changes to the storage file if overwrite is enabled 141 | fn write(&mut self) -> Result<(), MocksError> { 142 | if self.overwrite { 143 | let writer = Writer::new(&self.file); 144 | writer.write(&self.data)?; 145 | } 146 | Ok(()) 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/storage/operation.rs: -------------------------------------------------------------------------------- 1 | use crate::error::MocksError; 2 | use crate::storage::{Input, StorageData}; 3 | use serde_json::Value; 4 | 5 | pub mod insert; 6 | pub mod remove; 7 | pub mod replace; 8 | pub mod replace_one; 9 | pub mod select_all; 10 | pub mod select_one; 11 | pub mod update; 12 | pub mod update_one; 13 | 14 | pub fn extract_id_in_input(input: &Input) -> Result { 15 | input 16 | .get("id") 17 | .and_then(|v| match v { 18 | Value::Number(id) => Some(id.to_string()), 19 | Value::String(id) => Some(id.to_string()), 20 | _ => None, 21 | }) 22 | .ok_or(MocksError::InvalidRequest) 23 | } 24 | 25 | pub fn check_duplicate_id( 26 | data: &StorageData, 27 | resource_key: &str, 28 | id: &str, 29 | ) -> Result<(), MocksError> { 30 | if select_one::select_one(data, resource_key, id).is_ok() { 31 | Err(MocksError::DuplicateId) 32 | } else { 33 | Ok(()) 34 | } 35 | } 36 | 37 | pub fn extract_array_resource( 38 | data: &StorageData, 39 | resource_key: &str, 40 | ) -> Result, MocksError> { 41 | data.get(resource_key) 42 | .and_then(Value::as_array) 43 | .ok_or(MocksError::ResourceNotFound) 44 | .cloned() 45 | } 46 | -------------------------------------------------------------------------------- /src/storage/operation/insert.rs: -------------------------------------------------------------------------------- 1 | use crate::error::MocksError; 2 | use crate::storage::operation::{check_duplicate_id, extract_id_in_input}; 3 | use crate::storage::{Input, StorageData}; 4 | use serde_json::Value; 5 | 6 | pub fn insert( 7 | data: &mut StorageData, 8 | resource_key: &str, 9 | input: &Input, 10 | ) -> Result { 11 | // Validation to check duplicate IDs 12 | let id = extract_id_in_input(input)?; 13 | check_duplicate_id(data, resource_key, &id)?; 14 | insert_input(data, resource_key, input) 15 | } 16 | 17 | fn insert_input( 18 | data: &mut StorageData, 19 | resource_key: &str, 20 | input: &Input, 21 | ) -> Result { 22 | data.get_mut(resource_key) 23 | .and_then(Value::as_array_mut) 24 | .map(|values| { 25 | values.push(input.clone()); 26 | input.clone() 27 | }) 28 | .ok_or_else(|| { 29 | if data.get(resource_key).map_or(false, Value::is_object) { 30 | MocksError::MethodNotAllowed 31 | } else { 32 | MocksError::ObjectNotFound 33 | } 34 | }) 35 | } 36 | 37 | #[cfg(test)] 38 | mod tests { 39 | use super::*; 40 | use serde_json::json; 41 | 42 | #[test] 43 | fn test_insert_with_string_id() { 44 | let mut data = json!({"posts":[{"id":"test1","title":"first post","views":100}]}); 45 | let input = json!({"id":"test2","title":"second post","views":0}); 46 | 47 | match insert(&mut data, "posts", &input) { 48 | Ok(v) => { 49 | assert_eq!(v, json!({"id":"test2","title":"second post","views":0})); 50 | 51 | match &data["posts"].as_array() { 52 | None => { 53 | panic!("panic in test_insert_with_string_id"); 54 | } 55 | Some(values) => { 56 | assert_eq!(values.len(), 2); 57 | } 58 | } 59 | } 60 | Err(e) => { 61 | panic!("panic in test_insert_with_string_id: {}", e); 62 | } 63 | } 64 | } 65 | 66 | #[test] 67 | fn test_insert_with_number_id() { 68 | let mut data = json!({"posts":[{"id":1,"title":"first post","views":100}]}); 69 | let input = json!({"id":2,"title":"second post","views":0}); 70 | 71 | match insert(&mut data, "posts", &input) { 72 | Ok(v) => { 73 | assert_eq!(v, json!({"id":2,"title":"second post","views":0})); 74 | 75 | match &data["posts"].as_array() { 76 | None => { 77 | panic!("panic in test_insert_with_number_id"); 78 | } 79 | Some(values) => { 80 | assert_eq!(values.len(), 2); 81 | } 82 | } 83 | } 84 | Err(e) => { 85 | panic!("panic in test_insert_with_string_id: {}", e); 86 | } 87 | } 88 | } 89 | 90 | #[test] 91 | fn test_insert_error_method_not_allowed() { 92 | let mut data = json!({"profile":{"id":"user1","name":"John Smith","age":25}}); 93 | let input = json!({"id":"user2","name":"Jane Smith","age":25}); 94 | 95 | match insert(&mut data, "profile", &input) { 96 | Ok(_v) => { 97 | panic!("panic in test_insert_error_method_not_allowed"); 98 | } 99 | Err(e) => { 100 | assert_eq!(e, MocksError::MethodNotAllowed); 101 | } 102 | } 103 | } 104 | 105 | #[test] 106 | fn test_insert_error_resource_not_found() { 107 | let mut data = json!({"posts":[{"id":1,"title":"first post","views":100}]}); 108 | let input = json!({"id":2,"title":"second post","views":0}); 109 | 110 | match insert(&mut data, "errors", &input) { 111 | Ok(_v) => { 112 | panic!("panic in test_insert_error_resource_not_found"); 113 | } 114 | Err(e) => { 115 | assert_eq!(e, MocksError::ObjectNotFound); 116 | } 117 | } 118 | } 119 | 120 | #[test] 121 | fn test_insert_error_with_duplicated_string_id() { 122 | let mut data = json!({"posts":[{"id":"test1","title":"first post","views":100}]}); 123 | let input = json!({"id":"test1","title":"duplicated id","views":0}); 124 | 125 | match insert(&mut data, "posts", &input) { 126 | Ok(_v) => { 127 | panic!("panic in test_insert_error_with_duplicated_string_id"); 128 | } 129 | Err(e) => { 130 | assert_eq!(e, MocksError::DuplicateId); 131 | } 132 | } 133 | } 134 | 135 | #[test] 136 | fn test_insert_error_with_duplicated_number_id() { 137 | let mut data = json!({"posts":[{"id":1,"title":"first post","views":100}]}); 138 | let input = json!({"id":1,"title":"duplicated id","views":0}); 139 | 140 | match insert(&mut data, "posts", &input) { 141 | Ok(_v) => { 142 | panic!("panic in test_insert_error_with_duplicated_number_id"); 143 | } 144 | Err(e) => { 145 | assert_eq!(e, MocksError::DuplicateId); 146 | } 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/storage/operation/remove.rs: -------------------------------------------------------------------------------- 1 | use crate::error::MocksError; 2 | use crate::storage::operation::extract_array_resource; 3 | use crate::storage::operation::select_one::select_one; 4 | use crate::storage::StorageData; 5 | use serde_json::Value; 6 | 7 | pub fn remove( 8 | data: &mut StorageData, 9 | resource_key: &str, 10 | search_key: &str, 11 | ) -> Result { 12 | let values = extract_array_resource(data, resource_key)?; 13 | 14 | // Get the target to be removed 15 | let remove_one = select_one(data, resource_key, search_key)?; 16 | let removed_resource = remove_target(values, search_key); 17 | data[resource_key] = Value::Array(removed_resource); 18 | Ok(remove_one) 19 | } 20 | 21 | fn remove_target(values: Vec, key: &str) -> Vec { 22 | values 23 | .iter() 24 | .filter(|&value| { 25 | value 26 | .get("id") 27 | .and_then(|id| match id { 28 | Value::Number(n) => Some(n.to_string() != key), 29 | Value::String(s) => Some(s != key), 30 | _ => None, 31 | }) 32 | .unwrap_or(true) 33 | }) 34 | .cloned() 35 | .collect() 36 | } 37 | 38 | #[cfg(test)] 39 | mod tests { 40 | use super::*; 41 | use serde_json::json; 42 | 43 | #[test] 44 | fn test_remove_with_string_id() { 45 | let mut data = json!({"posts":[{"id":"test1","title":"first post","views":100}]}); 46 | 47 | match remove(&mut data, "posts", "test1") { 48 | Ok(v) => { 49 | assert_eq!(v, json!({"id":"test1","title":"first post","views":100})); 50 | 51 | match &data["posts"].as_array() { 52 | None => { 53 | panic!("panic in test_remove_with_string_id"); 54 | } 55 | Some(values) => { 56 | assert_eq!(values.len(), 0); 57 | } 58 | } 59 | } 60 | Err(e) => { 61 | panic!("panic in test_remove_with_string_id: {}", e.to_string()); 62 | } 63 | } 64 | } 65 | 66 | #[test] 67 | fn test_remove_with_number_id() { 68 | let mut data = json!({"posts":[{"id":1,"title":"first post","views":100}]}); 69 | 70 | match remove(&mut data, "posts", "1") { 71 | Ok(v) => { 72 | assert_eq!(v, json!({"id":1,"title":"first post","views":100})); 73 | 74 | match &data["posts"].as_array() { 75 | None => { 76 | panic!("panic in test_remove_with_number_id"); 77 | } 78 | Some(values) => { 79 | assert_eq!(values.len(), 0); 80 | } 81 | } 82 | } 83 | Err(e) => { 84 | panic!("panic in test_remove_with_number_id: {}", e.to_string()); 85 | } 86 | } 87 | } 88 | 89 | #[test] 90 | fn test_remove_error_resource_not_found() { 91 | let mut data = json!({"posts":[{"id":"test1","title":"first post","views":100}]}); 92 | 93 | match remove(&mut data, "errors", "test1") { 94 | Ok(_v) => { 95 | panic!("panic in test_remove_error_resource_not_found"); 96 | } 97 | Err(e) => { 98 | assert_eq!(e, MocksError::ResourceNotFound); 99 | } 100 | } 101 | } 102 | 103 | #[test] 104 | fn test_remove_error_object_not_found() { 105 | let mut data = json!({"posts":[{"id":"test1","title":"first post","views":100}]}); 106 | 107 | match remove(&mut data, "posts", "error") { 108 | Ok(_v) => { 109 | panic!("panic in test_remove_error_object_not_found"); 110 | } 111 | Err(e) => { 112 | assert_eq!(e, MocksError::ObjectNotFound); 113 | } 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/storage/operation/replace.rs: -------------------------------------------------------------------------------- 1 | use crate::error::MocksError; 2 | use crate::storage::operation::extract_array_resource; 3 | use crate::storage::operation::select_one::select_one; 4 | use crate::storage::{Input, StorageData}; 5 | use serde_json::Value; 6 | 7 | pub fn replace( 8 | data: &mut StorageData, 9 | resource_key: &str, 10 | search_key: &str, 11 | input: &Input, 12 | ) -> Result { 13 | let values = extract_array_resource(data, resource_key)?; 14 | 15 | // Validation to confirm the existence 16 | select_one(data, resource_key, search_key)?; 17 | let replaced_resource = replace_target_with_input(values, search_key, input); 18 | data[resource_key] = Value::Array(replaced_resource); 19 | Ok(input.clone()) 20 | } 21 | 22 | fn replace_target_with_input(values: Vec, key: &str, input: &Input) -> Vec { 23 | values 24 | .iter() 25 | .map(|value| { 26 | let id = value.get("id"); 27 | if matches!(id, Some(Value::Number(n)) if n.to_string() == key) 28 | || matches!(id, Some(Value::String(s)) if s == key) 29 | { 30 | input.clone() 31 | } else { 32 | value.clone() 33 | } 34 | }) 35 | .collect() 36 | } 37 | 38 | #[cfg(test)] 39 | mod tests { 40 | use super::*; 41 | use serde_json::json; 42 | 43 | #[test] 44 | fn test_replace_with_string_id() { 45 | let mut data = json!({"posts":[{"id":"test1","title":"first post","views":100}]}); 46 | let input = json!({"id":"test1","title":"replace test","views":200}); 47 | 48 | match replace(&mut data, "posts", "test1", &input) { 49 | Ok(v) => { 50 | assert_eq!(v, json!({"id":"test1","title":"replace test","views":200})); 51 | 52 | let updated_post = &data["posts"][0]; 53 | assert_eq!( 54 | *updated_post, 55 | json!({"id":"test1","title":"replace test","views":200}) 56 | ); 57 | } 58 | Err(e) => { 59 | panic!("panic in test_replace_with_string_id: {}", e.to_string()); 60 | } 61 | } 62 | } 63 | 64 | #[test] 65 | fn test_replace_with_number_id() { 66 | let mut data = json!({"posts":[{"id":1,"title":"first post","views":100}]}); 67 | let input = json!({"id":1,"title":"replace test","views":200}); 68 | 69 | match replace(&mut data, "posts", "1", &input) { 70 | Ok(v) => { 71 | assert_eq!(v, json!({"id":1,"title":"replace test","views":200})); 72 | 73 | let updated_post = &data["posts"][0]; 74 | assert_eq!( 75 | *updated_post, 76 | json!({"id":1,"title":"replace test","views":200}) 77 | ); 78 | } 79 | Err(e) => { 80 | panic!("panic in test_replace_with_string_id: {}", e.to_string()); 81 | } 82 | } 83 | } 84 | 85 | #[test] 86 | fn test_replace_error_resource_not_found() { 87 | let mut data = json!({"posts":[{"id":"test1","title":"first post","views":100}]}); 88 | let input = json!({"id":"error","title":"replace error","views":200}); 89 | 90 | match replace(&mut data, "errors", "test1", &input) { 91 | Ok(_v) => { 92 | panic!("panic in test_replace_error_resource_not_found") 93 | } 94 | Err(e) => { 95 | assert_eq!(e, MocksError::ResourceNotFound); 96 | } 97 | } 98 | } 99 | 100 | #[test] 101 | fn test_replace_error_object_not_found() { 102 | let mut data = json!({"posts":[{"id":"test1","title":"first post","views":100}]}); 103 | let input = json!({"id":"error","title":"replace error","views":200}); 104 | 105 | match replace(&mut data, "posts", "error", &input) { 106 | Ok(_v) => { 107 | panic!("panic in test_replace_error_object_not_found") 108 | } 109 | Err(e) => { 110 | assert_eq!(e, MocksError::ObjectNotFound); 111 | } 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/storage/operation/replace_one.rs: -------------------------------------------------------------------------------- 1 | use crate::error::MocksError; 2 | use crate::storage::operation::extract_id_in_input; 3 | use crate::storage::{Input, StorageData}; 4 | use serde_json::{Map, Value}; 5 | 6 | pub fn replace_one( 7 | data: &mut StorageData, 8 | resource_key: &str, 9 | input: &Input, 10 | ) -> Result { 11 | extract_id_in_input(input)?; 12 | let valid_input = input.as_object().ok_or(MocksError::InvalidRequest)?; 13 | replace_target_with_map_input(data, resource_key, valid_input.clone()) 14 | } 15 | 16 | fn replace_target_with_map_input( 17 | data: &mut StorageData, 18 | resource_key: &str, 19 | map_input: Map, 20 | ) -> Result { 21 | data.get_mut(resource_key) 22 | .and_then(Value::as_object_mut) 23 | .map(|v| { 24 | *v = map_input.clone(); 25 | Value::Object(map_input) 26 | }) 27 | .ok_or(MocksError::ObjectNotFound) 28 | } 29 | 30 | #[cfg(test)] 31 | mod tests { 32 | use super::*; 33 | use serde_json::json; 34 | 35 | #[test] 36 | fn test_replace_one_with_string_id() { 37 | let mut data = json!({"profile":{"id":"user1","name":"John Smith","age":25}}); 38 | let input = json!({"id":"user1","name":"Jane Smith","age":30}); 39 | 40 | match replace_one(&mut data, "profile", &input) { 41 | Ok(v) => { 42 | assert_eq!(v, json!({"id":"user1","name":"Jane Smith","age":30})); 43 | 44 | let updated_value = &data["profile"]; 45 | assert_eq!( 46 | *updated_value, 47 | json!({"id":"user1","name":"Jane Smith","age":30}) 48 | ); 49 | } 50 | Err(e) => { 51 | panic!( 52 | "panic in test_replace_one_with_string_id: {}", 53 | e.to_string() 54 | ); 55 | } 56 | } 57 | } 58 | 59 | #[test] 60 | fn test_replace_one_error_not_found() { 61 | let mut data = json!({"profile":{"id":"user1","name":"John Smith","age":25}}); 62 | let input = json!({"id":"user1","name":"Jane Smith","age":30}); 63 | 64 | match replace_one(&mut data, "error", &input) { 65 | Ok(_v) => { 66 | panic!("panic in test_replace_one_error_not_found") 67 | } 68 | Err(e) => { 69 | assert_eq!(e, MocksError::ObjectNotFound); 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/storage/operation/select_all.rs: -------------------------------------------------------------------------------- 1 | use crate::error::MocksError; 2 | use crate::storage::StorageData; 3 | use serde_json::Value; 4 | 5 | pub fn select_all(data: &StorageData, resource_key: &str) -> Result { 6 | data.get(resource_key) 7 | .filter(|&value| value.is_array() || value.is_object()) 8 | .cloned() 9 | .ok_or(MocksError::ResourceNotFound) 10 | } 11 | 12 | #[cfg(test)] 13 | mod tests { 14 | use super::*; 15 | use serde_json::json; 16 | 17 | #[test] 18 | fn test_select_all_list() { 19 | let data = json!({"posts":[{"id":"test1","title":"first post","views":100}]}); 20 | 21 | match select_all(&data, "posts") { 22 | Ok(v) => { 23 | if let Value::Array(values) = v { 24 | assert_eq!(values.len(), 1); 25 | 26 | let v = &values[0]; 27 | assert_eq!(*v, json!({"id":"test1","title":"first post","views":100})); 28 | } else { 29 | panic!("panic in test_insert_with_string_id"); 30 | } 31 | } 32 | Err(_) => { 33 | panic!("panic in test_insert_with_string_id"); 34 | } 35 | } 36 | } 37 | 38 | #[test] 39 | fn test_select_all_object() { 40 | let mut data = json!({"profile":{"id":1,"name":"John Smith","age":25}}); 41 | 42 | match select_all(&mut data, "profile") { 43 | Ok(v) => { 44 | assert_eq!(v, json!({"id":1,"name":"John Smith","age":25})); 45 | } 46 | Err(_) => { 47 | panic!("panic in test_insert_with_string_id"); 48 | } 49 | } 50 | } 51 | 52 | #[test] 53 | fn test_select_all_error_list() { 54 | let data = json!({"posts":[{"id":"test1","title":"first post","views":100}]}); 55 | 56 | match select_all(&data, "errors") { 57 | Ok(_v) => { 58 | panic!("panic in test_select_all_error_list"); 59 | } 60 | Err(e) => { 61 | assert_eq!(e, MocksError::ResourceNotFound); 62 | } 63 | } 64 | } 65 | 66 | #[test] 67 | fn test_select_all_error_object() { 68 | let mut data = json!({"profile":{"id":1,"name":"John Smith","age":25}}); 69 | 70 | match select_all(&mut data, "error") { 71 | Ok(_v) => { 72 | panic!("panic in test_select_all_error_object"); 73 | } 74 | Err(e) => { 75 | assert_eq!(e, MocksError::ResourceNotFound); 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/storage/operation/select_one.rs: -------------------------------------------------------------------------------- 1 | use crate::error::MocksError; 2 | use crate::storage::StorageData; 3 | use serde_json::Value; 4 | 5 | pub fn select_one( 6 | data: &StorageData, 7 | resource_key: &str, 8 | search_key: &str, 9 | ) -> Result { 10 | data.get(resource_key) 11 | .and_then(Value::as_array) 12 | .ok_or(MocksError::ObjectNotFound)? 13 | .iter() 14 | .find(|&value| { 15 | value.is_object() 16 | && match value.get("id") { 17 | Some(Value::Number(key)) => key.to_string() == search_key, 18 | Some(Value::String(key)) => key == search_key, 19 | _ => false, 20 | } 21 | }) 22 | .cloned() 23 | .ok_or(MocksError::ObjectNotFound) 24 | } 25 | 26 | #[cfg(test)] 27 | mod tests { 28 | use super::*; 29 | use serde_json::json; 30 | 31 | #[test] 32 | fn test_select_one_with_string_id() { 33 | let data = json!({"posts":[{"id":"test1","title":"first post","views":100}]}); 34 | 35 | match select_one(&data, "posts", "test1") { 36 | Ok(v) => { 37 | assert_eq!(v, json!({"id":"test1","title":"first post","views":100})); 38 | } 39 | Err(e) => { 40 | panic!("panic in test_select_one_with_string_id: {}", e.to_string()); 41 | } 42 | } 43 | } 44 | 45 | #[test] 46 | fn test_select_one_with_number_id() { 47 | let data = json!({"posts":[{"id":1,"title":"first post","views":100}]}); 48 | 49 | match select_one(&data, "posts", "1") { 50 | Ok(v) => { 51 | assert_eq!(v, json!({"id":1,"title":"first post","views":100})); 52 | assert_eq!(v["id"], Value::Number(1.into())); 53 | } 54 | Err(e) => { 55 | panic!("panic in test_select_one_with_number_id: {}", e.to_string()); 56 | } 57 | } 58 | } 59 | 60 | #[test] 61 | fn test_select_one_error() { 62 | let data = json!({"posts":[{"id":"test1","title":"first post","views":100}]}); 63 | 64 | match select_one(&data, "posts", "error") { 65 | Ok(_v) => { 66 | panic!("panic in test_select_one_error") 67 | } 68 | Err(e) => { 69 | assert_eq!(e, MocksError::ObjectNotFound); 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/storage/operation/update.rs: -------------------------------------------------------------------------------- 1 | use crate::error::MocksError; 2 | use crate::storage::{Input, StorageData}; 3 | use serde_json::Value; 4 | 5 | pub fn update( 6 | data: &mut StorageData, 7 | resource_key: &str, 8 | search_key: &str, 9 | input: &Input, 10 | ) -> Result { 11 | let values = data 12 | .get_mut(resource_key) 13 | .and_then(Value::as_array_mut) 14 | .ok_or(MocksError::ResourceNotFound)?; 15 | match update_resource_with_input(values, search_key, input) { 16 | Some(value) => Ok(value), 17 | None => Err(MocksError::ObjectNotFound), 18 | } 19 | } 20 | 21 | fn update_resource_with_input( 22 | values: &mut [Value], 23 | search_key: &str, 24 | input: &Input, 25 | ) -> Option { 26 | values.iter_mut().find_map(|value| { 27 | let obj = value.as_object_mut()?; 28 | let id = obj.get("id")?; 29 | 30 | let matches = match id { 31 | Value::Number(key) => key.to_string() == search_key, 32 | Value::String(key) => key == search_key, 33 | _ => false, 34 | }; 35 | 36 | if matches { 37 | if let Value::Object(input_map) = input { 38 | obj.extend(input_map.iter().map(|(k, v)| (k.clone(), v.clone()))); 39 | } 40 | Some(value.clone()) 41 | } else { 42 | None 43 | } 44 | }) 45 | } 46 | 47 | #[cfg(test)] 48 | mod tests { 49 | use super::*; 50 | use serde_json::json; 51 | 52 | #[test] 53 | fn test_update_with_string_id() { 54 | let mut data = json!({"posts":[{"id":"test1","title":"first post","views":100}]}); 55 | let input = json!({"title":"fixed post","views":200}); 56 | 57 | match update(&mut data, "posts", "test1", &input) { 58 | Ok(v) => { 59 | assert_eq!(v, json!({"id":"test1","title":"fixed post","views":200})); 60 | 61 | if let Value::Array(values) = &data["posts"] { 62 | assert_eq!(values.len(), 1); 63 | } else { 64 | panic!("panic in test_update_with_string_id"); 65 | } 66 | } 67 | Err(e) => { 68 | panic!("panic in test_update_with_string_id: {}", e.to_string()); 69 | } 70 | } 71 | } 72 | 73 | #[test] 74 | fn test_update_with_number_id() { 75 | let mut data = json!({"posts":[{"id":1,"title":"first post","views":100}]}); 76 | let input = json!({"title":"fixed post","views":200}); 77 | 78 | match update(&mut data, "posts", "1", &input) { 79 | Ok(v) => { 80 | assert_eq!(v, json!({"id":1,"title":"fixed post","views":200})); 81 | 82 | if let Value::Array(values) = &data["posts"] { 83 | assert_eq!(values.len(), 1); 84 | } else { 85 | panic!("panic in test_update_with_number_id"); 86 | } 87 | } 88 | Err(e) => { 89 | panic!("panic in test_update_with_number_id: {}", e.to_string()); 90 | } 91 | } 92 | } 93 | 94 | #[test] 95 | fn test_update_error_resource_not_found() { 96 | let mut data = json!({"posts":[{"id":"test1","title":"first post","views":100}]}); 97 | let input = json!({"title":"fixed post","views":200}); 98 | 99 | match update(&mut data, "errors", "test1", &input) { 100 | Ok(_v) => { 101 | panic!("panic in test_update_error_resource_not_found") 102 | } 103 | Err(e) => { 104 | assert_eq!(e, MocksError::ResourceNotFound); 105 | } 106 | } 107 | } 108 | 109 | #[test] 110 | fn test_update_error_object_not_found() { 111 | let mut data = json!({"posts":[{"id":"test1","title":"first post","views":100}]}); 112 | let input = json!({"title":"fixed post","views":200}); 113 | 114 | match update(&mut data, "posts", "error", &input) { 115 | Ok(_v) => { 116 | panic!("panic in test_update_error_object_not_found") 117 | } 118 | Err(e) => { 119 | assert_eq!(e, MocksError::ObjectNotFound); 120 | } 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/storage/operation/update_one.rs: -------------------------------------------------------------------------------- 1 | use crate::error::{MocksError, EXCEPTION_ERROR_MESSAGE}; 2 | use crate::storage::{Input, StorageData}; 3 | use serde_json::Value; 4 | 5 | pub fn update_one( 6 | data: &mut StorageData, 7 | resource_key: &str, 8 | input: &Input, 9 | ) -> Result { 10 | let resource = data 11 | .get_mut(resource_key) 12 | .ok_or(MocksError::ObjectNotFound)?; 13 | update_target_with_input(resource, input) 14 | } 15 | 16 | fn update_target_with_input(value: &mut Value, input: &Input) -> Result { 17 | match value { 18 | Value::Object(map) => { 19 | if let Value::Object(input_map) = input { 20 | map.extend(input_map.iter().map(|(k, v)| (k.clone(), v.clone()))); 21 | } else { 22 | // フォーマットエラー 23 | return Err(MocksError::Exception(EXCEPTION_ERROR_MESSAGE.to_string())); 24 | } 25 | 26 | Ok(value.clone()) 27 | } 28 | _ => Err(MocksError::ObjectNotFound), 29 | } 30 | } 31 | 32 | #[cfg(test)] 33 | mod tests { 34 | use super::*; 35 | use serde_json::json; 36 | 37 | #[test] 38 | fn test_update_one() { 39 | let mut data = json!({"profile":{"id":"user1","name":"John Smith","age":25}}); 40 | let input = json!({"id":"user1","name":"Jane Smith","age":30}); 41 | 42 | match update_one(&mut data, "profile", &input) { 43 | Ok(v) => { 44 | assert_eq!(v, json!({"id":"user1","name":"Jane Smith","age":30})); 45 | 46 | let updated_profile = &data["profile"]; 47 | assert_eq!( 48 | *updated_profile, 49 | json!({"id":"user1","name":"Jane Smith","age":30}) 50 | ); 51 | } 52 | Err(e) => { 53 | panic!("panic in test_update_one: {}", e.to_string()); 54 | } 55 | } 56 | } 57 | 58 | #[test] 59 | fn test_update_one_error() { 60 | let mut data = json!({"profile":{"id":"user1","name":"John Smith","age":25}}); 61 | let input = json!({"id":"user1","name":"Jane Smith","age":30}); 62 | 63 | match update_one(&mut data, "error", &input) { 64 | Ok(_v) => { 65 | panic!("panic in test_update_one_error") 66 | } 67 | Err(e) => { 68 | assert_eq!(e, MocksError::ObjectNotFound); 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/storage/reader.rs: -------------------------------------------------------------------------------- 1 | use crate::error::MocksError; 2 | use serde_json::Value; 3 | use std::fs; 4 | use std::path::Path; 5 | 6 | const INVALID_JSON_FORMAT_ERROR: &str = "Storage file is invalid JSON format."; 7 | const UNABLE_TO_GEN_API: &str = "Unable to generate API endpoints."; 8 | 9 | /// Storage file reader 10 | pub struct Reader { 11 | path: String, 12 | } 13 | 14 | impl Reader { 15 | pub fn new(path: &str) -> Reader { 16 | Self { 17 | path: path.to_string(), 18 | } 19 | } 20 | 21 | pub fn read(self) -> Result { 22 | let path = Path::new(&self.path); 23 | let text = 24 | fs::read_to_string(path).map_err(|e| MocksError::FailedReadFile(e.to_string()))?; 25 | 26 | let value: Value = 27 | serde_json::from_str(&text).map_err(|e| MocksError::FailedReadFile(e.to_string()))?; 28 | 29 | let obj = value 30 | .as_object() 31 | .ok_or_else(|| MocksError::FailedReadFile(INVALID_JSON_FORMAT_ERROR.to_string()))?; 32 | 33 | // Allow only Object or Array 34 | if obj 35 | .iter() 36 | .filter(|(k, _)| !k.is_empty()) 37 | .any(|(_, v)| v.is_object() || v.is_array()) 38 | { 39 | Ok(value) 40 | } else { 41 | Err(MocksError::FailedReadFile(UNABLE_TO_GEN_API.to_string())) 42 | } 43 | } 44 | } 45 | 46 | #[cfg(test)] 47 | mod tests { 48 | use super::*; 49 | 50 | #[test] 51 | fn test_reader_read() { 52 | let reader = Reader::new("storage.json"); 53 | 54 | match reader.read() { 55 | Ok(v) => { 56 | if v.is_object() { 57 | assert!(true); 58 | } else { 59 | panic!("panic in test_read"); 60 | } 61 | } 62 | Err(e) => { 63 | panic!("panic in test_read: {}", e.to_string()); 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/storage/writer.rs: -------------------------------------------------------------------------------- 1 | use crate::error::MocksError; 2 | use serde_json::Value; 3 | use std::env; 4 | use std::fs::OpenOptions; 5 | use std::io::Write; 6 | use std::path::Path; 7 | 8 | const ENV_KEY: &str = "MOCKS_DEBUG_OVERWRITTEN_FILE"; 9 | 10 | pub struct Writer { 11 | path: String, 12 | } 13 | 14 | impl Writer { 15 | pub fn new(path: &str) -> Writer { 16 | Self { 17 | path: path.to_string(), 18 | } 19 | } 20 | 21 | pub fn write(&self, value: &Value) -> Result<(), MocksError> { 22 | // Check debug mode 23 | let file_path = env::var(ENV_KEY).unwrap_or_else(|_| self.path.clone()); 24 | let path = Path::new(&file_path); 25 | 26 | let mut file = OpenOptions::new() 27 | .write(true) 28 | .create(true) 29 | .truncate(true) 30 | .open(path) 31 | .map_err(|e| MocksError::FailedWriteFile(e.to_string()))?; 32 | 33 | let json_string = serde_json::to_string_pretty(value) 34 | .map_err(|e| MocksError::FailedWriteFile(e.to_string()))?; 35 | 36 | file.write_all(json_string.as_bytes()) 37 | .map_err(|e| MocksError::FailedWriteFile(e.to_string()))?; 38 | 39 | Ok(()) 40 | } 41 | } 42 | 43 | #[cfg(test)] 44 | mod tests { 45 | use super::*; 46 | use serde_json::json; 47 | 48 | #[test] 49 | fn test_writer_write() { 50 | let writer = Writer::new("storage.test.json"); 51 | let value = json!({"posts":[{"id":"test1","title":"first post","views":100}]}); 52 | assert_eq!(writer.write(&value).is_ok(), true); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /storage.json: -------------------------------------------------------------------------------- 1 | { 2 | "posts": [ 3 | { 4 | "id": "01J7BAKH37HPG116ZRRFKHBDGB", 5 | "title": "first post", 6 | "views": 100 7 | }, 8 | { 9 | "id": "01J7BAKH37GE8B688PT4RC7TP4", 10 | "title": "second post", 11 | "views": 10 12 | } 13 | ], 14 | "comments": [ 15 | { 16 | "id": 1, 17 | "text": "a comment", 18 | "post_id": "01J7BAKH37HPG116ZRRFKHBDGB" 19 | }, 20 | { 21 | "id": 2, 22 | "text": "another comment", 23 | "post_id": "01J7BAKH37HPG116ZRRFKHBDGB" 24 | } 25 | ], 26 | "profile": { 27 | "id": "01J7BAQE1GMD78FN3J0FJCNS8T", 28 | "name": "mocks" 29 | }, 30 | "friends": [] 31 | } --------------------------------------------------------------------------------