diff --git a/.env.example b/.env.example index 20031a8..b44c805 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,5 @@ DATABASE_URL=postgres://postgres:password@localhost:5432/database -DATABASE_INIT_FILE=db.sql -HTTP_SERVER_HOST=0.0.0.0 -HTTP_SERVER_PORT=8000 \ No newline at end of file +HOST=0.0.0.0 +PORT=8000 +USERNAME=test +PASSWORD=test \ No newline at end of file diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml index b1f54a4..e41408e 100644 --- a/.github/workflows/prod.yml +++ b/.github/workflows/prod.yml @@ -1,47 +1,90 @@ -name: Build, Test Prod +name: Production -on: - push: - branches: - - main +on: + release: + types: [created] + +env: + AWS_REGION: us-east-1 + ECR_REPOSITORY: issues-api + ECS_CLUSTER: production + ECS_SERVICE: issues-api + ECS_CONTAINER: issues-api + TASK_FILE: task.json + DOCKERHUB_REGISTRY: kudosportal/issues + +permissions: + id-token: write + contents: read jobs: - build: + production-deploy: runs-on: ubuntu-latest steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Cache dependencies - uses: actions/cache@v3 - with: - path: | - ~/.cargo - target/ - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - restore-keys: ${{ runner.os }}-cargo- - - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - with: - toolchain: stable - - - name: Build and test code - run: | - cargo build --verbose - cargo test --verbose - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Log in to Docker Hub - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Build and push Docker image - run: | - docker build -t dontelmo/kudos_api:${{ github.sha }} -f Dockerfile . - docker push dontelmo/kudos_api:${{ github.sha }} \ No newline at end of file + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.ECR_ROLE }} + aws-region: ${{ env.AWS_REGION }} + + - name: Login to Amazon ECR Private + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 + + - name: Build, tag, and push docker image to Amazon ECR + id: build-image + env: + REGISTRY: ${{ steps.login-ecr.outputs.registry }} + REPOSITORY: issues-api + run: | + image=$REGISTRY/${{ env.ECR_REPOSITORY }}:${{ github.event.release.tag_name }} + docker build -t $image . + docker push $image + echo "image=$image" >> $GITHUB_OUTPUT + + - name: Download task definition + run: | + aws ecs describe-task-definition --task-definition issues-api --query taskDefinition > ${{ env.TASK_FILE }} + + - name: Fill in the new image ID in the Amazon ECS task definition + id: task-def + uses: aws-actions/amazon-ecs-render-task-definition@v1 + with: + task-definition: ${{ env.TASK_FILE }} + container-name: ${{ env.ECS_CONTAINER }} + image: ${{ steps.build-image.outputs.image }} + + - name: Deploy Amazon ECS task definition + uses: aws-actions/amazon-ecs-deploy-task-definition@v2 + with: + task-definition: ${{ steps.task-def.outputs.task-definition }} + service: ${{ env.ECS_SERVICE }} + cluster: ${{ env.ECS_CLUSTER }} + wait-for-service-stability: true + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build, tag and push docker image to Docker Hub + id: build-dockerhub-image + run: | + image=${{ env.DOCKERHUB_REGISTRY }}:${{ github.event.release.tag_name }} + docker build -t $image . + docker push $image + echo "image=$image" >> $GITHUB_OUTPUT + + - name: Deploy staging + run: | + image=docker.io/${{ steps.build-dockerhub-image.outputs.image }} + encoded_image=$(echo -n $image | jq -sRr @uri) + curl -f "${{ secrets.STAGE_DEPLOY_HOOK }}&imgURL=$encoded_image" \ No newline at end of file diff --git a/.github/workflows/stage.yml b/.github/workflows/stage.yml new file mode 100644 index 0000000..22b5082 --- /dev/null +++ b/.github/workflows/stage.yml @@ -0,0 +1,68 @@ +name: Stage + +on: + push: + branches: + - main + +env: + DOCKERHUB_REGISTRY: kudosportal/issues + +jobs: + stage-deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Cache dependencies + uses: actions/cache@v3 + with: + path: | + ~/.cargo + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + + - name: Run Clippy + run: | + cargo clippy --all-targets --all-features + + - name: Build code + run: | + cargo build --verbose + + - name: Test code + run: | + make test + + # - name: Integration tests + # run: | + # make test-db + # env: + # DATABASE_URL: ${{ secrets.TESTS_DATABASE_URL }} + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build, tag and push docker image to Docker Hub + id: build-dockerhub-image + run: | + image=${{ env.DOCKERHUB_REGISTRY }}:${{ github.sha }} + docker build -t $image . + docker push $image + echo "image=$image" >> $GITHUB_OUTPUT + + - name: Deploy staging + run: | + image=docker.io/${{ steps.build-dockerhub-image.outputs.image }} + encoded_image=$(echo -n $image | jq -sRr @uri) + curl -f "${{ secrets.STAGE_DEPLOY_HOOK }}&imgURL=$encoded_image" \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..9fe0608 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,50 @@ +name: Lint and test + +on: + pull_request: + branches: + - main + types: [opened, synchronize] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Cache dependencies + uses: actions/cache@v3 + with: + path: | + ~/.cargo + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + + - name: Run Clippy + run: | + cargo clippy --all-targets --all-features + + - name: Build code + run: | + cargo build --verbose + + - name: Test code + run: | + make test + + # - name: Integration tests + # run: | + # make test-db + # env: + # DATABASE_URL: ${{ secrets.TESTS_DATABASE_URL }} + + - name: Build docker image + run: | + docker build -t ${{ vars.DOCKER_REGISTRY }}:${{ github.sha }} -f Dockerfile . diff --git a/.gitignore b/.gitignore index c41cc9e..cb380dc 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ -/target \ No newline at end of file +/target +.vscode +.env +.test.env \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 6ee9e01..54cff7f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" dependencies = [ "gimli", ] @@ -18,14 +18,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] -name = "ahash" -version = "0.7.7" +name = "aho-corasick" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a824f2aa7e75a0c98c5a504fceb80649e9c35265d44525b5f94de4771a395cd" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ - "getrandom", - "once_cell", - "version_check", + "memchr", ] [[package]] @@ -44,27 +42,65 @@ dependencies = [ ] [[package]] -name = "async-trait" -version = "0.1.74" +name = "anstream" +version = "0.6.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.39", + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" + +[[package]] +name = "anstyle-parse" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +dependencies = [ + "anstyle", + "windows-sys", ] [[package]] name = "autocfg" -version = "1.1.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "backtrace" -version = "0.3.69" +version = "0.3.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" dependencies = [ "addr2line", "cc", @@ -77,15 +113,21 @@ dependencies = [ [[package]] name = "base64" -version = "0.21.5" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" -version = "1.3.2" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "block-buffer" @@ -98,9 +140,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.14.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "byteorder" @@ -110,17 +152,17 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.5.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" [[package]] name = "cc" -version = "1.0.83" +version = "1.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6" dependencies = [ - "libc", + "shlex", ] [[package]] @@ -131,9 +173,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.31" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", @@ -144,17 +186,23 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "colorchoice" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" + [[package]] name = "core-foundation-sys" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.11" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" +checksum = "51e852e6dc9a5bed1fae92dd2375037bf2b768725bf3be87811edee3249d09ad" dependencies = [ "libc", ] @@ -169,11 +217,94 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.76", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.76", +] + [[package]] name = "data-encoding" -version = "2.5.0" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" + +[[package]] +name = "diesel" +version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" +checksum = "65e13bab2796f412722112327f3e575601a3e9cdcbe426f0d30dbf43f3f5dc71" +dependencies = [ + "bitflags", + "byteorder", + "chrono", + "diesel_derives", + "itoa", + "pq-sys", + "r2d2", +] + +[[package]] +name = "diesel_derives" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f2c3de51e2ba6bf2a648285696137aaf0f5f487bcbea93972fe8a364e131a4" +dependencies = [ + "diesel_table_macro_syntax", + "dsl_auto_type", + "proc-macro2", + "quote", + "syn 2.0.76", +] + +[[package]] +name = "diesel_migrations" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a73ce704bad4231f001bff3314d91dce4aba0770cee8b233991859abc15c1f6" +dependencies = [ + "diesel", + "migrations_internals", + "migrations_macros", +] + +[[package]] +name = "diesel_table_macro_syntax" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "209c735641a413bc68c4923a9d6ad4bcb3ca306b794edaa7eb0b3228a99ffb25" +dependencies = [ + "syn 2.0.76", +] [[package]] name = "digest" @@ -183,7 +314,6 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", - "subtle", ] [[package]] @@ -192,32 +322,63 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" +[[package]] +name = "dsl_auto_type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9abe6314103864cc2d8901b7ae224e0ab1a103a0a416661b4097b0779b607" +dependencies = [ + "darling", + "either", + "heck", + "proc-macro2", + "quote", + "syn 2.0.76", +] + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + [[package]] name = "encoding_rs" -version = "0.8.33" +version = "0.8.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" dependencies = [ "cfg-if", ] [[package]] -name = "equivalent" -version = "1.0.1" +name = "env_filter" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" +dependencies = [ + "log", + "regex", +] [[package]] -name = "fallible-iterator" -version = "0.2.0" +name = "env_logger" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" +checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", +] [[package]] -name = "finl_unicode" -version = "1.2.0" +name = "equivalent" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "fnv" @@ -234,26 +395,11 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "futures" -version = "0.3.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - [[package]] name = "futures-channel" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", @@ -261,69 +407,31 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" - -[[package]] -name = "futures-executor" -version = "0.3.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" - -[[package]] -name = "futures-macro" -version = "0.3.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.39", -] +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-sink" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" - -[[package]] -name = "futures-timer" -version = "3.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ - "futures-channel", "futures-core", - "futures-io", - "futures-macro", "futures-sink", "futures-task", - "memchr", "pin-project-lite", "pin-utils", "slab", @@ -341,9 +449,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.11" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", @@ -352,22 +460,22 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" [[package]] name = "h2" -version = "0.3.22" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" dependencies = [ "bytes", "fnv", "futures-core", "futures-sink", "futures-util", - "http", + "http 0.2.12", "indexmap", "slab", "tokio", @@ -377,9 +485,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "headers" @@ -387,10 +495,10 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" dependencies = [ - "base64", + "base64 0.21.7", "bytes", "headers-core", - "http", + "http 0.2.12", "httpdate", "mime", "sha1", @@ -402,29 +510,37 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" dependencies = [ - "http", + "http 0.2.12", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" -version = "0.3.3" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] -name = "hmac" -version = "0.12.1" +name = "http" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ - "digest", + "bytes", + "fnv", + "itoa", ] [[package]] name = "http" -version = "0.2.11" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" dependencies = [ "bytes", "fnv", @@ -433,20 +549,20 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", - "http", + "http 0.2.12", "pin-project-lite", ] [[package]] name = "httparse" -version = "1.8.0" +version = "1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" +checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" [[package]] name = "httpdate" @@ -454,24 +570,30 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "hyper" -version = "0.14.27" +version = "0.14.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" dependencies = [ "bytes", "futures-channel", "futures-core", "futures-util", "h2", - "http", + "http 0.2.12", "http-body", "httparse", "httpdate", "itoa", "pin-project-lite", - "socket2 0.4.10", + "socket2", "tokio", "tower-service", "tracing", @@ -480,9 +602,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.58" +version = "0.1.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -501,6 +623,12 @@ dependencies = [ "cc", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.5.0" @@ -513,25 +641,31 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.1.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" dependencies = [ "equivalent", "hashbrown", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itoa" -version = "1.0.9" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "js-sys" -version = "0.3.66" +version = "0.3.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" dependencies = [ "wasm-bindgen", ] @@ -540,35 +674,38 @@ dependencies = [ name = "kudos_api" version = "0.1.0" dependencies = [ + "base64 0.22.1", + "bytes", "chrono", + "diesel", + "diesel_migrations", "dotenv", - "mobc", - "mobc-postgres", + "env_logger", + "log", + "rand", + "regex", "serde", "serde_derive", "serde_json", + "serde_path_to_error", "thiserror", "tokio", + "url", + "validator_derive", "warp", ] -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - [[package]] name = "libc" -version = "0.2.150" +version = "0.2.158" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" [[package]] name = "lock_api" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -576,45 +713,35 @@ dependencies = [ [[package]] name = "log" -version = "0.4.20" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" - -[[package]] -name = "md-5" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" -dependencies = [ - "cfg-if", - "digest", -] +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "memchr" -version = "2.6.4" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] -name = "metrics" -version = "0.18.1" +name = "migrations_internals" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e52eb6380b6d2a10eb3434aec0885374490f5b82c8aaf5cd487a183c98be834" +checksum = "fd01039851e82f8799046eabbb354056283fb265c8ec0996af940f4e85a380ff" dependencies = [ - "ahash", - "metrics-macros", + "serde", + "toml", ] [[package]] -name = "metrics-macros" -version = "0.5.1" +name = "migrations_macros" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49e30813093f757be5cf21e50389a24dc7dbb22c49f23b7e8f51d69b508a5ffa" +checksum = "ffb161cc72176cb37aa47f1fc520d3ef02263d67d661f44f05d05a079e1237fd" dependencies = [ + "migrations_internals", "proc-macro2", "quote", - "syn 1.0.109", ] [[package]] @@ -625,9 +752,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "mime_guess" -version = "2.0.4" +version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" dependencies = [ "mime", "unicase", @@ -635,54 +762,25 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.7.1" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ "adler", ] [[package]] name = "mio" -version = "0.8.9" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ + "hermit-abi", "libc", "wasi", "windows-sys", ] -[[package]] -name = "mobc" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90eb49dc5d193287ff80e72a86f34cfb27aae562299d22fea215e06ea1059dd3" -dependencies = [ - "async-trait", - "futures-channel", - "futures-core", - "futures-timer", - "futures-util", - "log", - "metrics", - "thiserror", - "tokio", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "mobc-postgres" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36167a82f9972ccd758596e63c24d63f14e9d1707b41ce9dbfe57c9746b259ce" -dependencies = [ - "futures", - "mobc", - "tokio-postgres", -] - [[package]] name = "multer" version = "2.1.0" @@ -692,7 +790,7 @@ dependencies = [ "bytes", "encoding_rs", "futures-util", - "http", + "http 0.2.12", "httparse", "log", "memchr", @@ -701,61 +799,35 @@ dependencies = [ "version_check", ] -[[package]] -name = "nu-ansi-term" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" -dependencies = [ - "overload", - "winapi", -] - [[package]] name = "num-traits" -version = "0.2.17" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi", - "libc", -] - [[package]] name = "object" -version = "0.32.1" +version = "0.36.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" - -[[package]] -name = "overload" -version = "0.1.1" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", @@ -763,9 +835,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", @@ -780,49 +852,31 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" -[[package]] -name = "phf" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" -dependencies = [ - "phf_shared", -] - -[[package]] -name = "phf_shared" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" -dependencies = [ - "siphasher", -] - [[package]] name = "pin-project" -version = "1.1.3" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.3" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.76", ] [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" @@ -831,59 +885,76 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] -name = "postgres-protocol" -version = "0.6.6" +name = "ppv-lite86" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49b6c5ef183cd3ab4ba005f1ca64c21e8bd97ce4699cfea9e8d9a2c4958ca520" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" dependencies = [ - "base64", - "byteorder", - "bytes", - "fallible-iterator", - "hmac", - "md-5", - "memchr", - "rand", - "sha2", - "stringprep", + "zerocopy", ] [[package]] -name = "postgres-types" -version = "0.2.6" +name = "pq-sys" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d2234cdee9408b523530a9b6d2d6b373d1db34f6a8e51dc03ded1828d7fb67c" +checksum = "a24ff9e4cf6945c988f0db7005d87747bf72864965c3529d259ad155ac41d584" dependencies = [ - "bytes", - "chrono", - "fallible-iterator", - "postgres-protocol", + "vcpkg", ] [[package]] -name = "ppv-lite86" -version = "0.2.17" +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] [[package]] name = "proc-macro2" -version = "1.0.70" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.33" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] +[[package]] +name = "r2d2" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" +dependencies = [ + "log", + "parking_lot", + "scheduled-thread-pool", +] + [[package]] name = "rand" version = "0.8.5" @@ -916,33 +987,62 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.4.1" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" dependencies = [ "bitflags", ] [[package]] -name = "rustc-demangle" -version = "0.1.23" +name = "regex" +version = "1.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] [[package]] -name = "rustls-pemfile" -version = "1.0.4" +name = "regex-automata" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ - "base64", + "aho-corasick", + "memchr", + "regex-syntax", ] +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + [[package]] name = "ryu" -version = "1.0.15" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "scheduled-thread-pool" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" +dependencies = [ + "parking_lot", +] [[package]] name = "scoped-tls" @@ -958,35 +1058,55 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.193" +version = "1.0.209" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.193" +version = "1.0.209" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.76", ] [[package]] name = "serde_json" -version = "1.0.108" +version = "1.0.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -1011,30 +1131,10 @@ dependencies = [ ] [[package]] -name = "sha2" -version = "0.10.8" +name = "shlex" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "siphasher" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "slab" @@ -1047,25 +1147,15 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" - -[[package]] -name = "socket2" -version = "0.4.10" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" -dependencies = [ - "libc", - "winapi", -] +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" -version = "0.5.5" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" dependencies = [ "libc", "windows-sys", @@ -1078,21 +1168,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] -name = "stringprep" -version = "0.1.4" +name = "strsim" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6" -dependencies = [ - "finl_unicode", - "unicode-bidi", - "unicode-normalization", -] - -[[package]] -name = "subtle" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" @@ -1101,15 +1180,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", - "quote", "unicode-ident", ] [[package]] name = "syn" -version = "2.0.39" +version = "2.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525" dependencies = [ "proc-macro2", "quote", @@ -1118,39 +1196,29 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.50" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.50" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", -] - -[[package]] -name = "thread_local" -version = "1.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" -dependencies = [ - "cfg-if", - "once_cell", + "syn 2.0.76", ] [[package]] name = "tinyvec" -version = "1.6.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" dependencies = [ "tinyvec_macros", ] @@ -1163,100 +1231,95 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.34.0" +version = "1.39.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9" +checksum = "9babc99b9923bfa4804bd74722ff02c0381021eafa4db9949217e3be8e84fff5" dependencies = [ "backtrace", "bytes", "libc", "mio", - "num_cpus", "pin-project-lite", - "socket2 0.5.5", + "socket2", "tokio-macros", "windows-sys", ] [[package]] name = "tokio-macros" -version = "2.2.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.76", ] [[package]] -name = "tokio-postgres" -version = "0.7.10" +name = "tokio-tungstenite" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d340244b32d920260ae7448cb72b6e238bddc3d4f7603394e7dd46ed8e48f5b8" +checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" dependencies = [ - "async-trait", - "byteorder", - "bytes", - "fallible-iterator", - "futures-channel", "futures-util", "log", - "parking_lot", - "percent-encoding", - "phf", - "pin-project-lite", - "postgres-protocol", - "postgres-types", - "rand", - "socket2 0.5.5", "tokio", - "tokio-util", - "whoami", + "tungstenite", ] [[package]] -name = "tokio-stream" -version = "0.1.14" +name = "tokio-util" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" dependencies = [ + "bytes", "futures-core", + "futures-sink", "pin-project-lite", "tokio", ] [[package]] -name = "tokio-tungstenite" -version = "0.20.1" +name = "toml" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" dependencies = [ - "futures-util", - "log", - "tokio", - "tungstenite", + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", ] [[package]] -name = "tokio-util" -version = "0.7.10" +name = "toml_datetime" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", - "tracing", + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", ] [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" @@ -1266,21 +1329,9 @@ checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ "log", "pin-project-lite", - "tracing-attributes", "tracing-core", ] -[[package]] -name = "tracing-attributes" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.39", -] - [[package]] name = "tracing-core" version = "0.1.32" @@ -1288,50 +1339,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" -dependencies = [ - "nu-ansi-term", - "sharded-slab", - "smallvec", - "thread_local", - "tracing-core", - "tracing-log", ] [[package]] name = "try-lock" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tungstenite" -version = "0.20.1" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" +checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" dependencies = [ "byteorder", "bytes", "data-encoding", - "http", + "http 1.1.0", "httparse", "log", "rand", @@ -1358,9 +1383,9 @@ dependencies = [ [[package]] name = "unicode-bidi" -version = "0.3.13" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-ident" @@ -1370,18 +1395,18 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" dependencies = [ "tinyvec", ] [[package]] name = "url" -version = "2.5.0" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", "idna", @@ -1395,16 +1420,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] -name = "valuable" -version = "0.1.0" +name = "utf8parse" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "validator_derive" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df0bcf92720c40105ac4b2dda2a4ea3aa717d4d6a862cc217da653a4bd5c6b10" +dependencies = [ + "darling", + "once_cell", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.76", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "want" @@ -1417,15 +1462,15 @@ dependencies = [ [[package]] name = "warp" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1e92e22e03ff1230c03a1a8ee37d2f89cd489e2e541b7550d6afad96faed169" +checksum = "4378d202ff965b011c64817db11d5829506d3404edeadb61f190d111da3f231c" dependencies = [ "bytes", "futures-channel", "futures-util", "headers", - "http", + "http 0.2.12", "hyper", "log", "mime", @@ -1433,13 +1478,11 @@ dependencies = [ "multer", "percent-encoding", "pin-project", - "rustls-pemfile", "scoped-tls", "serde", "serde_json", "serde_urlencoded", "tokio", - "tokio-stream", "tokio-tungstenite", "tokio-util", "tower-service", @@ -1454,34 +1497,35 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.89" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" dependencies = [ "cfg-if", + "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.89" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.76", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.89" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1489,92 +1533,51 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.89" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.76", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.89" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" - -[[package]] -name = "web-sys" -version = "0.3.66" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50c24a44ec86bb68fbecd1b3efed7e85ea5621b39b35ef2766b66cd984f8010f" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "whoami" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" -dependencies = [ - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" [[package]] name = "windows-core" -version = "0.51.1" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ "windows-targets", ] [[package]] name = "windows-sys" -version = "0.48.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", + "windows_i686_gnullvm", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", @@ -1583,42 +1586,78 @@ dependencies = [ [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" -version = "0.48.5" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" -version = "0.48.5" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +dependencies = [ + "memchr", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.76", +] diff --git a/Cargo.toml b/Cargo.toml index 60302f2..3b9a7ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,12 +7,21 @@ edition = "2021" [dependencies] warp = "0.3" -tokio = { version = "1.34", features = ["macros"] } -serde = { version = "1.0" , features = ["derive"]} +tokio = { version = "1.34", features = ["macros", "rt-multi-thread"] } +serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" dotenv = "0.15" -mobc = "0.8.3" -mobc-postgres = { version = "0.8.0", features = ["with-chrono-0_4"]} thiserror = "1.0.50" -chrono = { version = "0.4.31", features= ["serde"] } +chrono = { version = "0.4.31", features = ["serde"] } serde_derive = "1.0.193" +base64 = "0.22.0" +url = "2.5.0" +diesel = { version = "2.1.5", features = ["postgres", "chrono", "r2d2"] } +regex = "1.10.4" +log = "0.4.22" +env_logger = "0.11.5" +bytes = "1.7.1" +serde_path_to_error = "0.1.16" +validator_derive = "0.18.1" +rand = "0.8.5" +diesel_migrations = "2.1.0" diff --git a/Dockerfile b/Dockerfile index c22c9f7..fdaca83 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,24 +1,31 @@ # syntax=docker/dockerfile:1 -ARG RUST_VERSION=1.74.0 -FROM rust:${RUST_VERSION}-slim-bullseye AS build +ARG RUST_VERSION=1.78.0 +FROM rust:${RUST_VERSION} AS build RUN --mount=type=bind,source=src,target=src \ --mount=type=bind,source=Cargo.toml,target=Cargo.toml \ --mount=type=bind,source=Cargo.lock,target=Cargo.lock \ --mount=type=cache,target=/app/target/ \ --mount=type=cache,target=/usr/local/cargo/registry/ \ - # --mount=type=bind,source=migrations,target=migrations \ < issue_created_at + ) + ) +); \ No newline at end of file diff --git a/mock_data.bash b/mock_data.bash new file mode 100644 index 0000000..d778afd --- /dev/null +++ b/mock_data.bash @@ -0,0 +1,118 @@ +#!/bin/bash + +BASE_URL=${BASE_URL:-"http://localhost:8000"} +AUTH_HEADER="Authorization: Basic ${AUTH_TOKEN}" +CONTENT_TYPE_HEADER="Content-Type: application/json" + +# Create Polkadot project +curl --location "$BASE_URL/projects" \ +--header "$AUTH_HEADER" \ +--header "$CONTENT_TYPE_HEADER" \ +--data '{ + "name": "polkadot", + "slug":"polkadot" +}' + +# Create Asar project +curl --location "$BASE_URL/projects" \ +--header "$AUTH_HEADER" \ +--header "$CONTENT_TYPE_HEADER" \ +--data '{ + "name": "astar", + "slug":"astar" +}' +# Create Polkadot SDK repository +curl --location "$BASE_URL/repositories" \ +--header "$AUTH_HEADER" \ +--header "$CONTENT_TYPE_HEADER" \ +--data '{ + "name": "Polkadot SDK", + "slug": "polkadotsdk", + "language_slug": "rust", + "url": "https://github.com/paritytech/polkadot-sdk", + "project_id": 1 +}' + +# Create Zombienet repository +curl --location "$BASE_URL/repositories" \ +--header "$AUTH_HEADER" \ +--header "$CONTENT_TYPE_HEADER" \ +--data '{ + "name": "Zombienet", + "slug": "zombienet", + "language_slug": "rust", + "url": "https://github.com/paritytech/zombienet", + "project_id": 1 +}' + +# Create Astar repository +curl --location "$BASE_URL/repositories" \ +--header "$AUTH_HEADER" \ +--header "$CONTENT_TYPE_HEADER" \ +--data '{ + "name": "Astar", + "slug": "astar", + "language_slug": "rust", + "url": "https://github.com/AstarNetwork/Astar", + "project_id": 2 +}' + +# Create issues +curl --location "$BASE_URL/issues" \ +--header "$AUTH_HEADER" \ +--header "$CONTENT_TYPE_HEADER" \ +--data '{ + "number": 1863, + "title": "Adding separate label to the zombie namespaces", + "open": true, + "certified": false, + "repository_id": 1, + "issue_created_at": "2024-09-03T09:13:54Z" +}' + +curl --location "$BASE_URL/issues" \ +--header "$AUTH_HEADER" \ +--header "$CONTENT_TYPE_HEADER" \ +--data '{ + "number": 5597, + "title": "Fix balance to u256 type", + "open": true, + "certified": false, + "repository_id": 2, + "issue_created_at": "2024-09-03T09:13:54Z" +}' + +curl --location "$BASE_URL/issues" \ +--header "$AUTH_HEADER" \ +--header "$CONTENT_TYPE_HEADER" \ +--data '{ + "number": 1350, + "title": "Update mocks to use TestDefaultConfig", + "open": true, + "certified": false, + "repository_id": 3, + "issue_created_at": "2024-09-03T09:13:54Z" +}' + +# Cretae users + +curl --location "$BASE_URL/users" \ +--header "$AUTH_HEADER" \ +--header "$CONTENT_TYPE_HEADER" \ +--data '{ + "username": "leapalazzolo" +}' + +curl --location "$BASE_URL/users" \ +--header "$AUTH_HEADER" \ +--header "$CONTENT_TYPE_HEADER" \ +--data '{ + "username": "CJ13th" +}' + +curl --location "$BASE_URL/users" \ +--header "$AUTH_HEADER" \ +--header "$CONTENT_TYPE_HEADER" \ +--data '{ + "username": "ipapandinas" +}' \ No newline at end of file diff --git a/src/api/health/db.rs b/src/api/health/db.rs new file mode 100644 index 0000000..593afbc --- /dev/null +++ b/src/api/health/db.rs @@ -0,0 +1,22 @@ +use diesel::sql_query; +use diesel::RunQueryDsl; + +use crate::db::{ + errors::DBError, + pool::{DBAccess, DBAccessor}, +}; + +pub trait DBHealth: Send + Sync + Clone + 'static { + fn health(&self) -> Result<(), DBError>; +} + +impl DBHealth for DBAccess { + fn health(&self) -> Result<(), DBError> { + let conn = &mut self.get_db_conn(); + sql_query("SELECT 1") + .execute(conn) + .map_err(DBError::DBQuery)?; + + Ok(()) + } +} diff --git a/src/health/handlers.rs b/src/api/health/handlers.rs similarity index 77% rename from src/health/handlers.rs rename to src/api/health/handlers.rs index b61b86f..d2ab534 100644 --- a/src/health/handlers.rs +++ b/src/api/health/handlers.rs @@ -3,6 +3,6 @@ use warp::{http::StatusCode, Rejection, Reply}; use super::db::DBHealth; pub async fn health_handler(db_access: impl DBHealth) -> Result { - db_access.health().await?; + db_access.health().map_err(warp::reject::custom)?; Ok(StatusCode::OK) } diff --git a/src/health/mod.rs b/src/api/health/mod.rs similarity index 100% rename from src/health/mod.rs rename to src/api/health/mod.rs diff --git a/src/health/routes.rs b/src/api/health/routes.rs similarity index 67% rename from src/health/routes.rs rename to src/api/health/routes.rs index ec09cbd..9fdddd9 100644 --- a/src/health/routes.rs +++ b/src/api/health/routes.rs @@ -5,9 +5,8 @@ use super::db::DBHealth; use super::handlers; pub fn routes(db_access: impl DBHealth) -> BoxedFilter<(impl Reply,)> { - let health_route = warp::path!("health") + warp::path!("health") .and(warp::any().map(move || db_access.clone())) - .and_then(handlers::health_handler); - - health_route.boxed() + .and_then(handlers::health_handler) + .boxed() } diff --git a/src/api/issues/db.rs b/src/api/issues/db.rs new file mode 100644 index 0000000..c4794ed --- /dev/null +++ b/src/api/issues/db.rs @@ -0,0 +1,239 @@ +use diesel::dsl::now; +use diesel::prelude::*; +use diesel::sql_query; + +use super::models::{Issue, IssueResponse, NewIssue, QueryParams, UpdateIssue}; +use crate::api::projects::models::Project; +use crate::api::projects::models::ProjectResponse; +use crate::api::repositories::models::Repository; +use crate::api::repositories::models::RepositoryResponse; +use crate::schema::issues::dsl as issues_dsl; +use crate::schema::languages::dsl as languages_dsl; +use crate::schema::projects::dsl as projects_dsl; +use crate::schema::repositories::dsl as repositories_dsl; +use crate::schema::users::dsl as users_dsl; + +use crate::db::{ + errors::DBError, + pool::{DBAccess, DBAccessor}, +}; +use crate::types::PaginationParams; +use crate::utils; +pub trait DBIssue: Send + Sync + Clone + 'static { + fn all( + &self, + params: QueryParams, + pagination: PaginationParams, + ) -> Result<(Vec, i64), DBError>; + fn by_id(&self, id: i32) -> Result, DBError>; + fn by_number(&self, repository_id: i32, number: i32) -> Result, DBError>; + fn create(&self, issue: &NewIssue) -> Result; + fn update(&self, id: i32, issue: &UpdateIssue) -> Result; + fn delete_issue_assignee(&self, id: i32) -> Result<(), DBError>; + fn delete(&self, id: i32) -> Result<(), DBError>; +} + +impl DBIssue for DBAccess { + fn all( + &self, + params: QueryParams, + pagination: PaginationParams, + ) -> Result<(Vec, i64), DBError> { + let conn = &mut self.get_db_conn(); + + let build_query = || { + let mut query = issues_dsl::issues + .inner_join( + repositories_dsl::repositories + .on(issues_dsl::repository_id.eq(repositories_dsl::id)), + ) + .inner_join( + projects_dsl::projects.on(repositories_dsl::project_id.eq(projects_dsl::id)), + ) + .left_join( + languages_dsl::languages + .on(repositories_dsl::language_slug.eq(languages_dsl::slug)), + ) + .left_join( + users_dsl::users.on(issues_dsl::assignee_id.eq(users_dsl::id.nullable())), + ) + .into_boxed(); + + if let Some(slug) = params.slug.as_ref() { + query = query.filter(projects_dsl::slug.eq(slug)); + } + + if let Some(purpose) = params.purposes.as_ref() { + query = query.filter(projects_dsl::purposes.contains(vec![purpose])); + } + + if let Some(stack_level) = params.stack_levels.as_ref() { + query = query.filter(projects_dsl::stack_levels.contains(vec![stack_level])); + } + + if let Some(technology) = params.technologies.as_ref() { + query = query.filter(projects_dsl::technologies.contains(vec![technology])); + } + + if let Some(language_slug) = params.language_slug.as_ref() { + query = query.filter(languages_dsl::slug.eq(language_slug)); + } + + if let Some(raw_labels) = params.labels.as_ref() { + let labels: Vec = utils::parse_comma_values(raw_labels); + query = query.filter(issues_dsl::labels.overlaps_with(labels)); + } + + if let Some(certified) = params.certified.as_ref() { + query = query.filter(issues_dsl::certified.eq(certified)); + } + + if let Some(open) = params.open.as_ref() { + query = query.filter(issues_dsl::open.eq(open)); + } + + if let Some(assignee_id) = params.assignee_id.as_ref() { + query = query.filter(issues_dsl::assignee_id.eq(assignee_id)); + } + + if let Some(repository_id) = params.repository_id.as_ref() { + query = query.filter(issues_dsl::repository_id.eq(repository_id)); + } + + if let Some(has_assignee) = params.has_assignee.as_ref() { + if *has_assignee { + query = query.filter(issues_dsl::assignee_id.is_not_null()); + } else { + query = query.filter(issues_dsl::assignee_id.is_null()); + } + } + + if let Some(closed_at_min) = params.issue_closed_at_min.as_ref() { + query = query.filter( + issues_dsl::issue_closed_at + .is_not_null() + .and(issues_dsl::issue_closed_at.ge(closed_at_min)), + ); + } + + if let Some(closed_at_max) = params.issue_closed_at_max.as_ref() { + query = query.filter( + issues_dsl::issue_closed_at + .is_not_null() + .and(issues_dsl::issue_closed_at.le(closed_at_max)), + ); + } + + query + }; + + let total_count = build_query().count().get_result::(conn)?; + + let result = build_query() + .offset(pagination.offset) + .limit(pagination.limit) + .select(( + issues_dsl::issues::all_columns(), + repositories_dsl::repositories::all_columns(), + projects_dsl::projects::all_columns(), + users_dsl::username.nullable(), + )) + .load::<(Issue, Repository, Project, Option)>(conn)?; + + let issues_full = result + .into_iter() + .map(|(issue, repo, project, username)| IssueResponse { + id: issue.id, + issue_id: issue.number, + labels: issue.labels, + open: issue.open, + assignee_id: issue.assignee_id, + assignee_username: username, + title: issue.title, + certified: issue.certified.unwrap_or(false), + repository: RepositoryResponse { + id: repo.id, + slug: repo.slug, + name: repo.name, + url: repo.url, + language_slug: repo.language_slug, + project: ProjectResponse { + id: project.id, + name: project.name, + slug: project.slug, + purposes: project.purposes, + stack_levels: project.stack_levels, + technologies: project.technologies, + created_at: project.created_at, + updated_at: project.updated_at, + }, + created_at: repo.created_at, + updated_at: repo.updated_at, + }, + issue_created_at: issue.issue_created_at, + issue_closed_at: issue.issue_closed_at, + created_at: issue.created_at, + updated_at: issue.updated_at, + }) + .collect(); + + Ok((issues_full, total_count)) + } + fn by_id(&self, id: i32) -> Result, DBError> { + let conn = &mut self.get_db_conn(); + let result = issues_dsl::issues + .find(id) + .first::(conn) + .optional() + .map_err(DBError::from)?; + Ok(result) + } + fn by_number(&self, repository_id: i32, number: i32) -> Result, DBError> { + let conn = &mut self.get_db_conn(); + let result = issues_dsl::issues + .filter(issues_dsl::repository_id.eq(repository_id)) + .filter(issues_dsl::number.eq(number)) + .first::(conn) + .optional() + .map_err(DBError::from)?; + Ok(result) + } + fn create(&self, form: &NewIssue) -> Result { + let conn = &mut self.get_db_conn(); + let issue = diesel::insert_into(issues_dsl::issues) + .values(form) + .get_result(conn) + .map_err(DBError::from)?; + + Ok(issue) + } + + fn update(&self, id: i32, issue: &UpdateIssue) -> Result { + let conn = &mut self.get_db_conn(); + + let issue = diesel::update(issues_dsl::issues.filter(issues_dsl::id.eq(id))) + .set((issue, issues_dsl::updated_at.eq(now))) + .get_result::(conn) + .map_err(DBError::from)?; + + Ok(issue) + } + fn delete_issue_assignee(&self, id: i32) -> Result<(), DBError> { + let conn = &mut self.get_db_conn(); + let query = + format!("UPDATE issues SET assignee_id = NULL, updated_at = now() WHERE id = {id}"); + + sql_query(query).execute(conn).map_err(DBError::from)?; + + Ok(()) + } + + fn delete(&self, id: i32) -> Result<(), DBError> { + let conn = &mut self.get_db_conn(); + diesel::delete(issues_dsl::issues.filter(issues_dsl::id.eq(id))) + .execute(conn) + .map_err(DBError::from)?; + + Ok(()) + } +} diff --git a/src/api/issues/errors.rs b/src/api/issues/errors.rs new file mode 100644 index 0000000..092a100 --- /dev/null +++ b/src/api/issues/errors.rs @@ -0,0 +1,54 @@ +use std::fmt; + +use serde_derive::Deserialize; +use thiserror::Error; +use warp::{ + http::StatusCode, + reject::Reject, + reply::{Reply, Response}, +}; + +use crate::errors::ErrorResponse; + +#[derive(Clone, Error, Debug, Deserialize, PartialEq)] +pub enum IssueError { + AlreadyExists(i32), + NotFound(i32), + RepositoryNotFound(i32), + InvalidPayload(String), + CannotCreate(String), + CannotUpdate(String), +} + +impl fmt::Display for IssueError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + IssueError::AlreadyExists(id) => write!(f, "Issue #{id} already exists"), + IssueError::NotFound(id) => write!(f, "Issue #{id} not found"), + IssueError::InvalidPayload(error) => write!(f, "Invalid payload: {error}"), + IssueError::RepositoryNotFound(id) => write!(f, "Repository #{id} not found"), + IssueError::CannotCreate(error) => write!(f, "error creating the issue: {error}"), + IssueError::CannotUpdate(error) => write!(f, "error updating the issue: {error}"), + } + } +} + +impl Reject for IssueError {} + +impl Reply for IssueError { + fn into_response(self) -> Response { + let code = match self { + IssueError::AlreadyExists(_) => StatusCode::BAD_REQUEST, + IssueError::NotFound(_) => StatusCode::NOT_FOUND, + IssueError::InvalidPayload(_) => StatusCode::UNPROCESSABLE_ENTITY, + IssueError::CannotCreate(_) => StatusCode::INTERNAL_SERVER_ERROR, + IssueError::CannotUpdate(_) => StatusCode::INTERNAL_SERVER_ERROR, + IssueError::RepositoryNotFound(_) => StatusCode::UNPROCESSABLE_ENTITY, + }; + let message = self.to_string(); + + let json = warp::reply::json(&ErrorResponse { message }); + + warp::reply::with_status(json, code).into_response() + } +} diff --git a/src/api/issues/handlers.rs b/src/api/issues/handlers.rs new file mode 100644 index 0000000..1673ea1 --- /dev/null +++ b/src/api/issues/handlers.rs @@ -0,0 +1,193 @@ +use bytes::Buf; +use log::{error, info, warn}; +use warp::{ + http::StatusCode, + reject, + reject::Rejection, + reply::{json, with_status, Reply}, +}; + +use crate::{ + api::{repositories::db::DBRepository, users::db::DBUser}, + types::{PaginatedResponse, PaginationParams}, +}; + +use super::{ + db::DBIssue, + errors::IssueError, + models::{IssueAssignee, NewIssue, QueryParams, UpdateIssue}, +}; + +pub async fn by_id(id: i32, db_access: impl DBIssue) -> Result { + info!("getting issues '{id}'"); + match db_access.by_id(id)? { + None => Err(warp::reject::custom(IssueError::NotFound(id)))?, + Some(repository) => Ok(json(&repository)), + } +} + +pub async fn all_handler( + db_access: impl DBIssue, + params: QueryParams, + pagination: PaginationParams, +) -> Result { + info!("getting all the issues"); + let (issues, total_count) = db_access.all(params, pagination.clone())?; + let has_next_page = pagination.offset + pagination.limit < total_count; + let has_previous_page = pagination.offset > 0; + + let response = PaginatedResponse { + total_count: Some(total_count), + has_next_page, + has_previous_page, + data: issues, + }; + + Ok(json(&response)) +} + +pub async fn create_handler( + buf: impl Buf, + db_access: impl DBIssue + DBRepository, +) -> Result { + let des = &mut serde_json::Deserializer::from_reader(buf.reader()); + let issue: NewIssue = serde_path_to_error::deserialize(des).map_err(|e| { + let e = e.to_string(); + warn!("invalid issue '{e}'",); + reject::custom(IssueError::InvalidPayload(e)) + })?; + + info!("creating issue number '{}'", issue.number); + match DBRepository::by_id(&db_access, issue.repository_id) { + Ok(repo) => match repo { + Some(_) => match db_access.by_number(issue.repository_id, issue.number)? { + Some(r) => { + warn!("issue number '{}' exists", issue.number); + Err(warp::reject::custom(IssueError::AlreadyExists(r.number))) + } + None => match DBIssue::create(&db_access, &issue) { + Ok(issue) => { + info!("issue number '{}' created", issue.number); + Ok(with_status(json(&issue), StatusCode::CREATED)) + } + Err(err) => { + error!("error creating the issue '{:?}': {}", issue, err); + Err(warp::reject::custom(IssueError::CannotCreate( + "error creating the issue".to_owned(), + ))) + } + }, + }, + None => { + warn!("repository '{}' invalid", issue.repository_id); + Err(warp::reject::custom(IssueError::RepositoryNotFound( + issue.repository_id, + ))) + } + }, + Err(_) => Err(warp::reject::custom(IssueError::CannotCreate( + "cannot check if the repository is valid".to_owned(), + ))), + } +} +pub async fn update_handler( + id: i32, + buf: impl Buf, + db_access: impl DBIssue, +) -> Result { + let des = &mut serde_json::Deserializer::from_reader(buf.reader()); + let issue: UpdateIssue = serde_path_to_error::deserialize(des).map_err(|e| { + let e = e.to_string(); + warn!("invalid issue update: '{e}'",); + reject::custom(IssueError::InvalidPayload(e)) + })?; + if !issue.has_any_field() { + let e = "all the fields are empty"; + warn!("invalid issue update: '{e}'",); + return Err(reject::custom(IssueError::InvalidPayload(e.to_string()))); + } + match DBIssue::by_id(&db_access, id)? { + Some(p) => match db_access.update(p.id, &issue) { + Ok(issue) => { + info!("issue '{}' updated", issue.id); + Ok(with_status(json(&issue), StatusCode::OK)) + } + Err(error) => { + error!("error updating the issue '{:?}': {}", issue, error); + if error.to_string().contains("issue_closed_at_check") { + Err(warp::reject::custom(IssueError::InvalidPayload( + "issue_closed_at is lower than issue_created_at".to_owned(), + ))) + } else { + Err(warp::reject::custom(IssueError::CannotUpdate( + "error updating the issue".to_owned(), + ))) + } + } + }, + None => Err(warp::reject::custom(IssueError::NotFound(id))), + } +} +pub async fn delete_handler(id: i32, db_access: impl DBIssue) -> Result { + match DBIssue::by_id(&db_access, id)? { + Some(_) => { + let _ = &db_access.delete(id)?; + Ok(StatusCode::NO_CONTENT) + } + None => Err(warp::reject::custom(IssueError::NotFound(id)))?, + } +} + +pub async fn update_asignee_handler( + id: i32, + buf: impl Buf, + db_access: impl DBIssue + DBUser, +) -> Result { + let des = &mut serde_json::Deserializer::from_reader(buf.reader()); + let assignee: IssueAssignee = serde_path_to_error::deserialize(des).map_err(|e| { + let e = e.to_string(); + warn!("invalid issue assignee: '{e}'",); + reject::custom(IssueError::InvalidPayload(e)) + })?; + + match DBUser::by_username(&db_access, &assignee.username)? { + Some(u) => { + let update_issue = UpdateIssue { + assignee_id: Some(u.id), + ..Default::default() + }; + match DBIssue::update(&db_access, id, &update_issue) { + Ok(issue) => { + info!("issue '{}' assignee '{}' updated", issue.id, u.id); + Ok(with_status(json(&issue), StatusCode::OK)) + } + Err(error) => { + error!("error updating the issue '{id}': {error}"); + Err(warp::reject::custom(IssueError::CannotUpdate( + "error updating the issue assignee".to_owned(), + ))) + } + } + } + None => Err(warp::reject::custom(IssueError::InvalidPayload( + "username not found".to_string(), + ))), + } +} +pub async fn delete_asignee_handler( + id: i32, + db_access: impl DBIssue + DBUser, +) -> Result { + match db_access.delete_issue_assignee(id) { + Ok(issue) => { + info!("issue '{}' assignee deleted", id); + Ok(with_status(json(&issue), StatusCode::NO_CONTENT)) + } + Err(error) => { + error!("error deleteing the issue '{id}' assignee: {error}"); + Err(warp::reject::custom(IssueError::CannotUpdate( + "error deleting the issue assignee".to_owned(), + ))) + } + } +} diff --git a/src/contributions/mod.rs b/src/api/issues/mod.rs similarity index 100% rename from src/contributions/mod.rs rename to src/api/issues/mod.rs diff --git a/src/api/issues/models.rs b/src/api/issues/models.rs new file mode 100644 index 0000000..5248944 --- /dev/null +++ b/src/api/issues/models.rs @@ -0,0 +1,99 @@ +use crate::{api::repositories::models::RepositoryResponse, schema::issues}; +use chrono::{DateTime, Utc}; +use diesel::prelude::*; + +use serde_derive::{Deserialize, Serialize}; + +#[derive( + AsChangeset, Queryable, Identifiable, Selectable, Debug, PartialEq, Serialize, Deserialize, +)] +#[diesel(table_name = issues)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct Issue { + pub id: i32, + pub number: i32, + pub title: String, + pub labels: Option>>, + pub open: bool, + pub certified: Option, + pub assignee_id: Option, + pub repository_id: i32, + pub issue_created_at: DateTime, + pub issue_closed_at: Option>, + pub created_at: DateTime, + pub updated_at: Option>, +} + +#[derive(Insertable, Serialize, Deserialize, Debug)] +#[diesel(table_name = issues)] +pub struct NewIssue { + pub number: i32, + pub title: String, + pub labels: Option>, + pub open: bool, + pub certified: Option, + pub repository_id: i32, + pub assignee_id: Option, + pub issue_created_at: DateTime, +} + +#[derive(AsChangeset, Serialize, Deserialize, Debug, Default)] +#[diesel(table_name = issues)] +pub struct UpdateIssue { + pub title: Option, + pub labels: Option>, + pub open: Option, + pub certified: Option, + pub assignee_id: Option, + pub issue_closed_at: Option>, +} + +impl UpdateIssue { + pub fn has_any_field(&self) -> bool { + self.title.is_some() + || self.labels.is_some() + || self.open.is_some() + || self.certified.is_some() + || self.assignee_id.is_some() + || self.issue_closed_at.is_some() + } +} + +#[derive(Deserialize, Debug)] +pub struct QueryParams { + pub slug: Option, + pub certified: Option, + pub purposes: Option, + pub stack_levels: Option, + pub technologies: Option, + pub labels: Option, + pub language_slug: Option, + pub repository_id: Option, + pub assignee_id: Option, + pub open: Option, + pub has_assignee: Option, + pub issue_closed_at_min: Option>, + pub issue_closed_at_max: Option>, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct IssueAssignee { + pub username: String, +} + +#[derive(Serialize, Debug)] +pub struct IssueResponse { + pub id: i32, + pub issue_id: i32, + pub title: String, + pub labels: Option>>, + pub open: bool, + pub certified: bool, + pub assignee_id: Option, + pub assignee_username: Option, + pub repository: RepositoryResponse, + pub issue_created_at: DateTime, + pub issue_closed_at: Option>, + pub created_at: DateTime, + pub updated_at: Option>, +} diff --git a/src/api/issues/routes.rs b/src/api/issues/routes.rs new file mode 100644 index 0000000..6ff3217 --- /dev/null +++ b/src/api/issues/routes.rs @@ -0,0 +1,80 @@ +use std::convert::Infallible; + +use warp::filters::BoxedFilter; +use warp::{Filter, Reply}; + +use crate::api::repositories::db::DBRepository; +use crate::api::users::db::DBUser; +use crate::auth::with_auth; +use crate::types::PaginationParams; + +use super::db::DBIssue; +use super::handlers; +use super::models::QueryParams; + +fn with_db( + db_pool: impl DBIssue + DBRepository + DBUser, +) -> impl Filter + Clone { + warp::any().map(move || db_pool.clone()) +} + +pub fn routes(db_access: impl DBIssue + DBRepository + DBUser) -> BoxedFilter<(impl Reply,)> { + let issue = warp::path!("issues"); + let issue_id = warp::path!("issues" / i32); + let issue_id_assignee = warp::path!("issues" / i32 / "assignee"); + + let get_issues = issue + .and(warp::get()) + .and(with_db(db_access.clone())) + .and(warp::query::()) + .and(warp::query::()) + .and_then(handlers::all_handler); + + let get_issue = issue_id + .and(warp::get()) + .and(with_db(db_access.clone())) + .and_then(handlers::by_id); + + let create_issue = issue + .and(with_auth()) + .and(warp::post()) + .and(warp::body::aggregate()) + .and(with_db(db_access.clone())) + .and_then(handlers::create_handler); + + let delete_issue = issue_id + .and(with_auth()) + .and(warp::delete()) + .and(with_db(db_access.clone())) + .and_then(handlers::delete_handler); + + let update_issue = issue_id + .and(with_auth()) + .and(warp::put()) + .and(warp::body::aggregate()) + .and(with_db(db_access.clone())) + .and_then(handlers::update_handler); + + let update_issue_assignee = issue_id_assignee + .and(with_auth()) + .and(warp::patch()) + .and(warp::body::aggregate()) + .and(with_db(db_access.clone())) + .and_then(handlers::update_asignee_handler); + + let delete_issue_assignee = issue_id_assignee + .and(with_auth()) + .and(warp::delete()) + .and(with_db(db_access.clone())) + .and_then(handlers::delete_asignee_handler); + + let route = get_issues + .or(get_issue) + .or(create_issue) + .or(delete_issue) + .or(update_issue) + .or(update_issue_assignee) + .or(delete_issue_assignee); + + route.boxed() +} diff --git a/src/api/mod.rs b/src/api/mod.rs new file mode 100644 index 0000000..3781899 --- /dev/null +++ b/src/api/mod.rs @@ -0,0 +1,5 @@ +pub mod health; +pub mod issues; +pub mod projects; +pub mod repositories; +pub mod users; diff --git a/src/api/projects/db.rs b/src/api/projects/db.rs new file mode 100644 index 0000000..93744c4 --- /dev/null +++ b/src/api/projects/db.rs @@ -0,0 +1,119 @@ +use diesel::dsl::now; +use diesel::prelude::*; + +use super::models::{NewProject, Project, QueryParams, UpdateProject}; +use crate::schema::projects::dsl as projects_dsl; + +use crate::db::{ + errors::DBError, + pool::{DBAccess, DBAccessor}, +}; +use crate::types::PaginationParams; +use crate::utils; + +pub trait DBProject: Send + Sync + Clone + 'static { + fn all( + &self, + params: QueryParams, + pagination: PaginationParams, + ) -> Result<(Vec, i64), DBError>; + fn by_id(&self, id: i32) -> Result, DBError>; + fn by_slug(&self, slug: &str) -> Result, DBError>; + fn create(&self, form: &NewProject) -> Result; + fn update(&self, id: i32, form: &UpdateProject) -> Result; + fn delete(&self, id: i32) -> Result<(), DBError>; +} + +impl DBProject for DBAccess { + fn all( + &self, + params: QueryParams, + pagination: PaginationParams, + ) -> Result<(Vec, i64), DBError> { + let conn = &mut self.get_db_conn(); + + let build_query = || { + let mut query = projects_dsl::projects.into_boxed(); + + if let Some(slug) = params.slug.as_ref() { + query = query.filter(projects_dsl::slug.eq(slug)); + } + + if let Some(raw_purposes) = params.purposes.as_ref() { + let purposes: Vec = utils::parse_comma_values(raw_purposes); + query = query.filter(projects_dsl::purposes.overlaps_with(purposes)); + } + + if let Some(raw_technologies) = params.technologies.as_ref() { + let technologies: Vec = utils::parse_comma_values(raw_technologies); + query = query.filter(projects_dsl::technologies.overlaps_with(technologies)); + } + + query + }; + + let total_count = build_query().count().get_result::(conn)?; + + let result = build_query() + .offset(pagination.offset) + .limit(pagination.limit) + .load::(conn)?; + + Ok((result, total_count)) + } + + fn by_id(&self, id: i32) -> Result, DBError> { + let conn = &mut self.get_db_conn(); + + let result = projects_dsl::projects + .find(id) + .first::(conn) + .optional() + .map_err(DBError::from)?; + + Ok(result) + } + + fn by_slug(&self, slug: &str) -> Result, DBError> { + let conn = &mut self.get_db_conn(); + + let result = projects_dsl::projects + .filter(projects_dsl::slug.eq(slug)) + .first::(conn) + .optional() + .map_err(DBError::from)?; + + Ok(result) + } + + fn create(&self, form: &NewProject) -> Result { + let conn = &mut self.get_db_conn(); + + let project = diesel::insert_into(projects_dsl::projects) + .values(form) + .get_result(conn) + .map_err(DBError::from)?; + + Ok(project) + } + + fn update(&self, id: i32, form: &UpdateProject) -> Result { + let conn = &mut self.get_db_conn(); + + let project = diesel::update(projects_dsl::projects.filter(projects_dsl::id.eq(id))) + .set((form, projects_dsl::updated_at.eq(now))) + .get_result::(conn) + .map_err(DBError::from)?; + + Ok(project) + } + + fn delete(&self, id: i32) -> Result<(), DBError> { + let conn = &mut self.get_db_conn(); + diesel::delete(projects_dsl::projects.filter(projects_dsl::id.eq(id))) + .execute(conn) + .map_err(DBError::from)?; + + Ok(()) + } +} diff --git a/src/api/projects/errors.rs b/src/api/projects/errors.rs new file mode 100644 index 0000000..7ff34c1 --- /dev/null +++ b/src/api/projects/errors.rs @@ -0,0 +1,51 @@ +use std::fmt; + +use serde_derive::Deserialize; +use thiserror::Error; +use warp::{ + http::StatusCode, + reject::Reject, + reply::{Reply, Response}, +}; + +use crate::errors::ErrorResponse; + +#[derive(Clone, Error, Debug, Deserialize, PartialEq)] +pub enum ProjectError { + AlreadyExists(i32), + NotFound(i32), + NotFoundBySlug(String), + InvalidPayload(String), + CannotCreate(String), +} + +impl fmt::Display for ProjectError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ProjectError::AlreadyExists(id) => write!(f, "Project #{id} already exists"), + ProjectError::NotFound(id) => write!(f, "Project #{id} not found"), + ProjectError::NotFoundBySlug(slug) => write!(f, "Project {slug} not found"), + ProjectError::InvalidPayload(error) => write!(f, "Invalid payload: {error}"), + ProjectError::CannotCreate(error) => write!(f, "Cannot create the project: {error}"), + } + } +} + +impl Reject for ProjectError {} + +impl Reply for ProjectError { + fn into_response(self) -> Response { + let code = match self { + ProjectError::AlreadyExists(_) => StatusCode::BAD_REQUEST, + ProjectError::NotFound(_) => StatusCode::NOT_FOUND, + ProjectError::NotFoundBySlug(_) => StatusCode::NOT_FOUND, + ProjectError::InvalidPayload(_) => StatusCode::UNPROCESSABLE_ENTITY, + ProjectError::CannotCreate(_) => StatusCode::INTERNAL_SERVER_ERROR, + }; + let message = self.to_string(); + + let json = warp::reply::json(&ErrorResponse { message }); + + warp::reply::with_status(json, code).into_response() + } +} diff --git a/src/api/projects/handlers.rs b/src/api/projects/handlers.rs new file mode 100644 index 0000000..380c63c --- /dev/null +++ b/src/api/projects/handlers.rs @@ -0,0 +1,85 @@ +use crate::types::{PaginatedResponse, PaginationParams}; + +use super::{ + db::DBProject, + errors::ProjectError, + models::{NewProject, QueryParams, UpdateProject}, +}; +use bytes::Buf; +use log::{error, info, warn}; +use warp::{ + http::StatusCode, + reject, + reject::Rejection, + reply::{json, with_status, Reply}, +}; + +pub async fn all_handler( + db_access: impl DBProject, + params: QueryParams, + pagination: PaginationParams, +) -> Result { + let (projects, total_count) = db_access.all(params, pagination.clone())?; + let has_next_page = pagination.offset + pagination.limit < total_count; + let has_previous_page = pagination.offset > 0; + + let response = PaginatedResponse { + total_count: Some(total_count), + has_next_page, + has_previous_page, + data: projects, + }; + + Ok(json(&response)) +} + +pub async fn create_handler( + buf: impl Buf, + db_access: impl DBProject, +) -> Result { + let des = &mut serde_json::Deserializer::from_reader(buf.reader()); + let project: NewProject = serde_path_to_error::deserialize(des).map_err(|e| { + let e = e.to_string(); + warn!("invalid project '{e}'",); + reject::custom(ProjectError::InvalidPayload(e)) + })?; + match db_access.by_slug(&project.slug)? { + Some(p) => Err(warp::reject::custom(ProjectError::AlreadyExists(p.id))), + None => match db_access.create(&project) { + Ok(project) => { + info!("project slug '{}' created", project.slug); + Ok(with_status(json(&project), StatusCode::CREATED)) + } + Err(error) => { + error!("error creating the project '{:?}': {}", project, error); + Err(warp::reject::custom(ProjectError::CannotCreate( + "error creating the project".to_string(), + ))) + } + }, + } +} + +pub async fn update_handler( + id: i32, + form: UpdateProject, + db_access: impl DBProject, +) -> Result { + match db_access.by_id(id)? { + Some(p) => Ok(with_status( + json(&db_access.update(p.id, &form)?), + StatusCode::OK, + )), + None => Err(warp::reject::custom(ProjectError::NotFound(id))), + } +} + +pub async fn delete_handler(id: i32, db_access: impl DBProject) -> Result { + match db_access.by_id(id)? { + Some(p) => Ok(with_status( + json(&db_access.delete(p.id)?), + StatusCode::NO_CONTENT, + )), + None => Err(warp::reject::custom(ProjectError::NotFound(id))), + } +} diff --git a/src/api/projects/mod.rs b/src/api/projects/mod.rs new file mode 100644 index 0000000..93246e5 --- /dev/null +++ b/src/api/projects/mod.rs @@ -0,0 +1,5 @@ +pub mod db; +pub mod errors; +pub mod handlers; +pub mod models; +pub mod routes; diff --git a/src/api/projects/models.rs b/src/api/projects/models.rs new file mode 100644 index 0000000..2b516a7 --- /dev/null +++ b/src/api/projects/models.rs @@ -0,0 +1,62 @@ +use crate::schema::projects; +use chrono::{DateTime, Utc}; +use diesel::prelude::*; + +use serde_derive::{Deserialize, Serialize}; + +#[derive( + AsChangeset, Queryable, Identifiable, Selectable, Debug, PartialEq, Serialize, Deserialize, +)] +#[diesel(table_name = projects)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct Project { + pub id: i32, + pub name: String, + pub slug: String, + pub types: Option>>, + pub purposes: Option>>, + pub stack_levels: Option>>, + pub technologies: Option>>, + pub created_at: DateTime, + pub updated_at: Option>, +} + +#[derive(Deserialize, Debug)] +pub struct QueryParams { + pub slug: Option, + pub purposes: Option, + pub stack_levels: Option, + pub technologies: Option, +} + +#[derive(Insertable, Serialize, Deserialize, Debug)] +#[diesel(table_name = projects)] +pub struct NewProject { + pub name: String, + pub slug: String, + pub purposes: Option>>, + pub stack_levels: Option>>, + pub technologies: Option>>, +} + +#[derive(AsChangeset, Serialize, Deserialize, Debug)] +#[diesel(table_name = projects)] +pub struct UpdateProject { + pub name: Option, + pub slug: Option, + pub purposes: Option>>, + pub stack_levels: Option>>, + pub technologies: Option>>, +} + +#[derive(Serialize, Debug)] +pub struct ProjectResponse { + pub id: i32, + pub name: String, + pub slug: String, + pub purposes: Option>>, + pub stack_levels: Option>>, + pub technologies: Option>>, + pub created_at: DateTime, + pub updated_at: Option>, +} diff --git a/src/api/projects/routes.rs b/src/api/projects/routes.rs new file mode 100644 index 0000000..92c4a82 --- /dev/null +++ b/src/api/projects/routes.rs @@ -0,0 +1,54 @@ +use std::convert::Infallible; +use warp::filters::BoxedFilter; +use warp::{Filter, Reply}; + +use crate::auth::with_auth; +use crate::types::PaginationParams; + +use super::db::DBProject; +use super::handlers; +use super::models::QueryParams; + +fn with_db( + db_pool: impl DBProject, +) -> impl Filter + Clone { + warp::any().map(move || db_pool.clone()) +} + +pub fn routes(db_access: impl DBProject) -> BoxedFilter<(impl Reply,)> { + let project = warp::path!("projects"); + let project_id = warp::path!("projects" / i32); + + let all_route = project + .and(warp::get()) + .and(with_db(db_access.clone())) + .and(warp::query::()) + .and(warp::query::()) + .and_then(handlers::all_handler); + + let create_route = project + .and(with_auth()) + .and(warp::post()) + .and(warp::body::aggregate()) + .and(with_db(db_access.clone())) + .and_then(handlers::create_handler); + + let update_route = project_id + .and(with_auth()) + .and(warp::put()) + .and(warp::body::json()) + .and(with_db(db_access.clone())) + .and_then(handlers::update_handler); + + let delete_route = project_id + .and(with_auth()) + .and(warp::delete()) + .and(with_db(db_access.clone())) + .and_then(handlers::delete_handler); + + all_route + .or(create_route) + .or(update_route) + .or(delete_route) + .boxed() +} diff --git a/src/api/repositories/db.rs b/src/api/repositories/db.rs new file mode 100644 index 0000000..41c55e2 --- /dev/null +++ b/src/api/repositories/db.rs @@ -0,0 +1,119 @@ +use diesel::dsl::sql; +use diesel::sql_types::Text; +use diesel::{dsl::now, prelude::*}; + +use super::models::{NewRepository, QueryParams, Repository, UpdateRepository}; +use crate::schema::repositories::dsl as repositories_dsl; +use crate::utils; +use crate::{ + db::{ + errors::DBError, + pool::{DBAccess, DBAccessor}, + }, + types::PaginationParams, +}; + +pub trait DBRepository: Send + Sync + Clone + 'static { + fn by_id(&self, id: i32) -> Result, DBError>; + fn all( + &self, + params: QueryParams, + pagination: PaginationParams, + ) -> Result, DBError>; + fn create(&self, repo: &NewRepository) -> Result; + fn update(&self, id: i32, repo: &UpdateRepository) -> Result; + fn delete(&self, id: i32) -> Result<(), DBError>; + fn by_slug(&self, slug: &str) -> Result, DBError>; + fn aggregate_languages(&self) -> Result, DBError>; +} + +impl DBRepository for DBAccess { + fn all( + &self, + params: QueryParams, + pagination: PaginationParams, + ) -> Result, DBError> { + let conn = &mut self.get_db_conn(); + let mut query = repositories_dsl::repositories.into_boxed(); + + if let Some(raw_languages) = params.languages { + let languages: Vec = utils::parse_comma_values(&raw_languages); + query = query.filter(repositories_dsl::language_slug.eq_any(languages)); + } + if let Some(project_id) = params.project_ids { + let ids: Vec = utils::parse_ids(&project_id); + if !ids.is_empty() { + query = query.filter(repositories_dsl::project_id.eq_any(ids)); + } + } + + query = query.offset(pagination.offset).limit(pagination.limit); + + let result = query.load::(conn)?; + Ok(result) + } + + fn by_id(&self, id: i32) -> Result, DBError> { + let conn = &mut self.get_db_conn(); + + let result = repositories_dsl::repositories + .find(id) + .first::(conn) + .optional() + .map_err(DBError::from)?; + + Ok(result) + } + + fn create(&self, repository: &NewRepository) -> Result { + let conn = &mut self.get_db_conn(); + + let repository = diesel::insert_into(repositories_dsl::repositories) + .values(repository) + .get_result(conn) + .map_err(DBError::from)?; + + Ok(repository) + } + + fn update(&self, id: i32, repository: &UpdateRepository) -> Result { + let conn = &mut self.get_db_conn(); + + let project = + diesel::update(repositories_dsl::repositories.filter(repositories_dsl::id.eq(id))) + .set((repository, repositories_dsl::updated_at.eq(now))) + .get_result::(conn) + .map_err(DBError::from)?; + + Ok(project) + } + + fn delete(&self, id: i32) -> Result<(), DBError> { + let conn = &mut self.get_db_conn(); + diesel::delete(repositories_dsl::repositories.filter(repositories_dsl::id.eq(id))) + .execute(conn) + .map_err(DBError::from)?; + + Ok(()) + } + + fn by_slug(&self, slug: &str) -> Result, DBError> { + let conn = &mut self.get_db_conn(); + + let result = repositories_dsl::repositories + .filter(repositories_dsl::slug.eq(slug)) + .first::(conn) + .optional() + .map_err(DBError::from)?; + + Ok(result) + } + + fn aggregate_languages(&self) -> Result, DBError> { + let conn = &mut self.get_db_conn(); + let languages = repositories_dsl::repositories + .select(sql::("DISTINCT language_slug")) + .load::(conn)?; + Ok(languages) + } +} diff --git a/src/api/repositories/errors.rs b/src/api/repositories/errors.rs new file mode 100644 index 0000000..8741419 --- /dev/null +++ b/src/api/repositories/errors.rs @@ -0,0 +1,66 @@ +use std::fmt; + +use serde_derive::Deserialize; +use thiserror::Error; +use warp::{ + http::StatusCode, + reject::Reject, + reply::{Reply, Response}, +}; + +use crate::errors::ErrorResponse; + +#[derive(Clone, Error, Debug, Deserialize, PartialEq)] +pub enum RepositoryError { + AlreadyExists(i32), + NotFound(i32), + NotFoundByName(String), + ProjectNotFound(i32), + InvalidPayload(String), + CannotCreate(String), +} + +impl fmt::Display for RepositoryError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + RepositoryError::AlreadyExists(id) => { + write!(f, "Repository '{id}' already exists") + } + RepositoryError::NotFound(id) => { + write!(f, "Repository '{id}' not found") + } + RepositoryError::NotFoundByName(name) => { + write!(f, "Repository '{name}' not found") + } + RepositoryError::InvalidPayload(error) => { + write!(f, "Invalid payload: {error}") + } + RepositoryError::CannotCreate(err) => { + write!(f, "error creating the repository: {err}") + } + RepositoryError::ProjectNotFound(id) => { + write!(f, "Project id '{id}' does not exist") + } + } + } +} + +impl Reject for RepositoryError {} + +impl Reply for RepositoryError { + fn into_response(self) -> Response { + let code = match self { + RepositoryError::AlreadyExists(_) => StatusCode::BAD_REQUEST, + RepositoryError::NotFound(_) => StatusCode::NOT_FOUND, + RepositoryError::NotFoundByName(_) => StatusCode::NOT_FOUND, + RepositoryError::InvalidPayload(_) => StatusCode::UNPROCESSABLE_ENTITY, + RepositoryError::CannotCreate(_) => StatusCode::INTERNAL_SERVER_ERROR, + RepositoryError::ProjectNotFound(_) => StatusCode::UNPROCESSABLE_ENTITY, + }; + let message = self.to_string(); + + let json = warp::reply::json(&ErrorResponse { message }); + + warp::reply::with_status(json, code).into_response() + } +} diff --git a/src/api/repositories/handlers.rs b/src/api/repositories/handlers.rs new file mode 100644 index 0000000..b3b43b4 --- /dev/null +++ b/src/api/repositories/handlers.rs @@ -0,0 +1,116 @@ +use bytes::Buf; +use log::{error, info, warn}; +use warp::{ + http::StatusCode, + reject, + reject::Rejection, + reply::{json, with_status, Reply}, +}; + +use crate::{ + api::projects::db::DBProject, + types::{PaginatedResponse, PaginationParams}, +}; + +use super::{ + db::DBRepository, + errors::RepositoryError, + models::{NewRepository, QueryParams, UpdateRepository}, +}; + +pub async fn by_id(id: i32, db_access: impl DBRepository) -> Result { + match db_access.by_id(id)? { + None => Err(warp::reject::custom(RepositoryError::NotFound(id)))?, + Some(repository) => Ok(json(&repository)), + } +} + +pub async fn all_handler( + db_access: impl DBRepository, + params: QueryParams, + pagination: PaginationParams, +) -> Result { + let repositories = db_access.all(params, pagination.clone())?; + let total_count = repositories.len() as i64; + let has_next_page = pagination.offset + pagination.limit < total_count; + let has_previous_page = pagination.offset > 0; + + let response = PaginatedResponse { + total_count: Some(total_count), + has_next_page, + has_previous_page, + data: repositories, + }; + + Ok(json(&response)) +} + +pub async fn create_handler( + buf: impl Buf, + db_access: impl DBRepository + DBProject, +) -> Result { + let des = &mut serde_json::Deserializer::from_reader(buf.reader()); + let repository: NewRepository = serde_path_to_error::deserialize(des).map_err(|e| { + let e = e.to_string(); + warn!("invalid repository '{e}'",); + reject::custom(RepositoryError::InvalidPayload(e)) + })?; + match DBRepository::by_slug(&db_access, &repository.slug)? { + Some(r) => Err(warp::reject::custom(RepositoryError::AlreadyExists(r.id))), + None => match DBProject::by_id(&db_access, repository.project_id) { + Ok(project) => match project { + Some(_) => match DBRepository::create(&db_access, &repository) { + Ok(_) => { + info!("repository slug '{}' created", repository.slug); + Ok(with_status(json(&repository), StatusCode::CREATED)) + } + Err(err) => { + error!("error creating the repository '{:?}': {}", repository, err); + Err(warp::reject::custom(RepositoryError::CannotCreate( + "error creating the repository".to_owned(), + ))) + } + }, + None => { + warn!("project id '{}' does not exist", repository.project_id); + Err(warp::reject::custom(RepositoryError::ProjectNotFound( + repository.project_id, + ))) + } + }, + Err(_) => Err(warp::reject::custom(RepositoryError::CannotCreate( + "cannot check if the repository exists".to_owned(), + ))), + }, + } +} +pub async fn update_handler( + id: i32, + repo: UpdateRepository, + db_access: impl DBRepository, +) -> Result { + match db_access.by_id(id)? { + Some(p) => Ok(with_status( + json(&db_access.update(p.id, &repo)?), + StatusCode::OK, + )), + None => Err(warp::reject::custom(RepositoryError::NotFound(id))), + } +} +pub async fn delete_handler( + id: i32, + db_access: impl DBRepository, +) -> Result { + match db_access.by_id(id)? { + Some(p) => Ok(with_status( + json(&db_access.delete(p.id)?), + StatusCode::NO_CONTENT, + )), + None => Err(warp::reject::custom(RepositoryError::NotFound(id))), + } +} + +pub async fn get_languages_handler(db_access: impl DBRepository) -> Result { + let languages = db_access.aggregate_languages()?; + Ok(json(&languages)) +} diff --git a/src/api/repositories/mod.rs b/src/api/repositories/mod.rs new file mode 100644 index 0000000..93246e5 --- /dev/null +++ b/src/api/repositories/mod.rs @@ -0,0 +1,5 @@ +pub mod db; +pub mod errors; +pub mod handlers; +pub mod models; +pub mod routes; diff --git a/src/api/repositories/models.rs b/src/api/repositories/models.rs new file mode 100644 index 0000000..fddc19f --- /dev/null +++ b/src/api/repositories/models.rs @@ -0,0 +1,61 @@ +use crate::{api::projects::models::ProjectResponse, schema::repositories}; +use chrono::{DateTime, Utc}; +use diesel::prelude::*; + +use serde_derive::{Deserialize, Serialize}; + +#[derive( + AsChangeset, Queryable, Identifiable, Selectable, Debug, PartialEq, Serialize, Deserialize, +)] +#[diesel(table_name = repositories)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct Repository { + pub id: i32, + pub slug: String, + pub name: String, + pub url: String, + pub language_slug: String, + pub project_id: i32, + pub created_at: DateTime, + pub updated_at: Option>, +} + +#[derive(Deserialize, Debug)] +pub struct QueryParams { + pub slug: Option, + pub name: Option, + pub languages: Option, + pub project_ids: Option, +} + +#[derive(Insertable, Serialize, Deserialize, Debug)] +#[diesel(table_name = repositories)] +pub struct NewRepository { + pub slug: String, + pub name: String, + pub url: String, + pub language_slug: String, + pub project_id: i32, +} + +#[derive(AsChangeset, Serialize, Deserialize, Debug)] +#[diesel(table_name = repositories)] +pub struct UpdateRepository { + pub slug: Option, + pub name: Option, + pub url: Option, + pub language_slug: Option, + pub project_id: Option, +} + +#[derive(Serialize, Debug)] +pub struct RepositoryResponse { + pub id: i32, + pub slug: String, + pub name: String, + pub url: String, + pub language_slug: String, + pub project: ProjectResponse, + pub created_at: DateTime, + pub updated_at: Option>, +} diff --git a/src/api/repositories/routes.rs b/src/api/repositories/routes.rs new file mode 100644 index 0000000..f3896aa --- /dev/null +++ b/src/api/repositories/routes.rs @@ -0,0 +1,70 @@ +use std::convert::Infallible; + +use warp::filters::BoxedFilter; +use warp::{Filter, Reply}; + +use crate::api::projects::db::DBProject; +use crate::auth::with_auth; +use crate::types::PaginationParams; + +use super::db::DBRepository; +use super::handlers; +use super::models::QueryParams; +// use crate::pagination::GetPagination; +// use crate::pagination::GetSort; + +fn with_db( + db_pool: impl DBRepository + DBProject, +) -> impl Filter + Clone { + warp::any().map(move || db_pool.clone()) +} + +pub fn routes(db_access: impl DBRepository + DBProject) -> BoxedFilter<(impl Reply,)> { + let repository = warp::path!("repositories"); + let repository_id = warp::path!("repositories" / i32); + + let all_route = repository + .and(warp::get()) + .and(with_db(db_access.clone())) + .and(warp::query::()) + .and(warp::query::()) + .and_then(handlers::all_handler); + + let by_id_route = repository_id + .and(warp::get()) + .and(with_db(db_access.clone())) + .and_then(handlers::by_id); + + let create_route = repository + .and(with_auth()) + .and(warp::post()) + .and(warp::body::aggregate()) + .and(with_db(db_access.clone())) + .and_then(handlers::create_handler); + + let update_route = repository_id + .and(with_auth()) + .and(warp::patch()) + .and(warp::body::json()) + .and(with_db(db_access.clone())) + .and_then(handlers::update_handler); + + let delete_route = repository_id + .and(with_auth()) + .and(warp::delete()) + .and(with_db(db_access.clone())) + .and_then(handlers::delete_handler); + + let languages_route = warp::path!("languages") + .and(warp::get()) + .and(with_db(db_access.clone())) + .and_then(handlers::get_languages_handler); + + all_route + .or(by_id_route) + .or(create_route) + .or(update_route) + .or(delete_route) + .or(languages_route) + .boxed() +} diff --git a/src/api/users/db.rs b/src/api/users/db.rs new file mode 100644 index 0000000..d1e6bc1 --- /dev/null +++ b/src/api/users/db.rs @@ -0,0 +1,92 @@ +use diesel::dsl::now; +use diesel::prelude::*; + +use super::models::{NewUser, UpdateUser, User}; +use crate::schema::users::dsl as users_dsl; + +use crate::db::{ + errors::DBError, + pool::{DBAccess, DBAccessor}, +}; +use crate::types::PaginationParams; + +pub trait DBUser: Send + Sync + Clone + 'static { + fn by_id(&self, id: i32) -> Result, DBError>; + fn by_username(&self, username: &str) -> Result, DBError>; + fn all(&self, pagination: PaginationParams) -> Result, DBError>; + fn create(&self, user: &NewUser) -> Result; + fn update(&self, id: i32, user: &UpdateUser) -> Result; + fn delete(&self, id: i32) -> Result<(), DBError>; +} + +impl DBUser for DBAccess { + fn by_id(&self, id: i32) -> Result, DBError> { + let conn = &mut self.get_db_conn(); + + let result = users_dsl::users + .find(id) + .first::(conn) + .optional() + .map_err(DBError::from)?; + + Ok(result) + } + fn by_username(&self, username: &str) -> Result, DBError> { + let conn = &mut self.get_db_conn(); + let mut query = users_dsl::users.into_boxed(); + query = query.filter(users_dsl::username.eq(username)); + query = query.limit(1); + let result: Vec = query.load::(conn)?; + if result.is_empty() { + Ok(None) + } else { + Ok(Some(User { + id: result[0].id, + username: result[0].username.clone(), + created_at: result[0].created_at, + updated_at: result[0].updated_at, + })) + } + } + + fn all(&self, pagination: PaginationParams) -> Result, DBError> { + let conn = &mut self.get_db_conn(); + let mut query = users_dsl::users.into_boxed(); + + query = query.offset(pagination.offset).limit(pagination.limit); + + let result = query.load::(conn)?; + Ok(result) + } + + fn create(&self, user: &NewUser) -> Result { + let conn = &mut self.get_db_conn(); + + let user = diesel::insert_into(users_dsl::users) + .values(user) + .get_result(conn) + .map_err(DBError::from)?; + + Ok(user) + } + + fn update(&self, id: i32, form: &UpdateUser) -> Result { + let conn = &mut self.get_db_conn(); + + let user = diesel::update(users_dsl::users.filter(users_dsl::id.eq(id))) + .set((form, users_dsl::updated_at.eq(now))) + .get_result::(conn) + .map_err(DBError::from)?; + + Ok(user) + } + + fn delete(&self, id: i32) -> Result<(), DBError> { + let conn = &mut self.get_db_conn(); + diesel::delete(users_dsl::users.filter(users_dsl::id.eq(id))) + .execute(conn) + .map_err(DBError::from)?; + + Ok(()) + } +} diff --git a/src/api/users/errors.rs b/src/api/users/errors.rs new file mode 100644 index 0000000..f6c7e35 --- /dev/null +++ b/src/api/users/errors.rs @@ -0,0 +1,56 @@ +use std::fmt; + +use serde_derive::Deserialize; +use thiserror::Error; +use warp::{ + http::StatusCode, + reject::Reject, + reply::{Reply, Response}, +}; + +use crate::errors::ErrorResponse; + +#[derive(Clone, Error, Debug, Deserialize, PartialEq)] +pub enum UserError { + AlreadyExists(i32), + NotFound(i32), + NotFoundByName(String), + CannotCreate(String), + CannotUpdate(i32, String), + InvalidPayload(String), +} + +impl fmt::Display for UserError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + UserError::AlreadyExists(id) => write!(f, "User #{id} already exists"), + UserError::NotFound(id) => write!(f, "User #{id} not found"), + UserError::NotFoundByName(name) => write!(f, "User {name} not found"), + UserError::CannotCreate(error) => write!(f, "User cannot be created: {error}"), + UserError::CannotUpdate(id, error) => { + write!(f, "User #{id} cannot be updated: {error}") + } + UserError::InvalidPayload(error) => write!(f, "Cannot create the user: {error}"), + } + } +} + +impl Reject for UserError {} + +impl Reply for UserError { + fn into_response(self) -> Response { + let code = match self { + UserError::AlreadyExists(_) => StatusCode::BAD_REQUEST, + UserError::NotFound(_) => StatusCode::NOT_FOUND, + UserError::NotFoundByName(_) => StatusCode::NOT_FOUND, + UserError::CannotCreate(_) => StatusCode::UNPROCESSABLE_ENTITY, + UserError::CannotUpdate(_, _) => StatusCode::UNPROCESSABLE_ENTITY, + UserError::InvalidPayload(_) => StatusCode::UNPROCESSABLE_ENTITY, + }; + let message = self.to_string(); + + let json = warp::reply::json(&ErrorResponse { message }); + + warp::reply::with_status(json, code).into_response() + } +} diff --git a/src/api/users/handlers.rs b/src/api/users/handlers.rs new file mode 100644 index 0000000..21ac3e2 --- /dev/null +++ b/src/api/users/handlers.rs @@ -0,0 +1,83 @@ +use bytes::Buf; +use warp::{ + http::StatusCode, + reject, + reject::Rejection, + reply::{json, with_status, Reply}, +}; + +use crate::types::PaginationParams; +use log::{error, info, warn}; + +use super::{ + db::DBUser, + errors::UserError, + models::{NewUser, UpdateUser}, +}; + +pub async fn by_id(id: i32, db_access: impl DBUser) -> Result { + match db_access.by_id(id)? { + None => Err(warp::reject::custom(UserError::NotFound(id)))?, + Some(user) => Ok(json(&user)), + } +} + +pub async fn all_handler( + db_access: impl DBUser, + pagination: PaginationParams, +) -> Result { + let users = db_access.all(pagination)?; + Ok(json::>(&users)) +} + +pub async fn create_handler( + buf: impl Buf, + db_access: impl DBUser, +) -> Result { + let des = &mut serde_json::Deserializer::from_reader(buf.reader()); + let user: NewUser = serde_path_to_error::deserialize(des).map_err(|e| { + let e = e.to_string(); + warn!("invalid user '{e}'",); + reject::custom(UserError::InvalidPayload(e)) + })?; + match db_access.by_username(&user.username)? { + Some(user) => { + warn!("user already exists '{:?}'", user); + Err(warp::reject::custom(UserError::AlreadyExists(user.id))) + } + None => match db_access.create(&user) { + Ok(user) => { + info!("user '{}' created", user.username); + Ok(with_status(json(&user), StatusCode::CREATED)) + } + Err(error) => { + error!("error creating the user '{:?}': {}", user, error); + Err(warp::reject::custom(UserError::CannotCreate( + "error creating the user".to_string(), + ))) + } + }, + } +} +pub async fn update_handler( + id: i32, + repo: UpdateUser, + db_access: impl DBUser, +) -> Result { + match db_access.by_id(id)? { + Some(p) => Ok(with_status( + json(&db_access.update(p.id, &repo)?), + StatusCode::OK, + )), + None => Err(warp::reject::custom(UserError::NotFound(id))), + } +} +pub async fn delete_handler(id: i32, db_access: impl DBUser) -> Result { + match db_access.by_id(id)? { + Some(p) => Ok(with_status( + json(&db_access.delete(p.id)?), + StatusCode::NO_CONTENT, + )), + None => Err(warp::reject::custom(UserError::NotFound(id))), + } +} diff --git a/src/api/users/mod.rs b/src/api/users/mod.rs new file mode 100644 index 0000000..93246e5 --- /dev/null +++ b/src/api/users/mod.rs @@ -0,0 +1,5 @@ +pub mod db; +pub mod errors; +pub mod handlers; +pub mod models; +pub mod routes; diff --git a/src/api/users/models.rs b/src/api/users/models.rs new file mode 100644 index 0000000..482178a --- /dev/null +++ b/src/api/users/models.rs @@ -0,0 +1,37 @@ +use crate::schema::users; +use chrono::{DateTime, Utc}; +use diesel::prelude::*; + +use serde_derive::{Deserialize, Serialize}; + +#[derive( + AsChangeset, + Queryable, + Identifiable, + Selectable, + Debug, + PartialEq, + Serialize, + Deserialize, + Clone, +)] +#[diesel(table_name = users)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct User { + pub id: i32, + pub username: String, + pub created_at: DateTime, + pub updated_at: Option>, +} + +#[derive(Insertable, Serialize, Deserialize, Debug)] +#[diesel(table_name = users)] +pub struct NewUser { + pub username: String, +} + +#[derive(AsChangeset, Serialize, Deserialize, Debug)] +#[diesel(table_name = users)] +pub struct UpdateUser { + pub username: Option, +} diff --git a/src/api/users/routes.rs b/src/api/users/routes.rs new file mode 100644 index 0000000..20762a6 --- /dev/null +++ b/src/api/users/routes.rs @@ -0,0 +1,61 @@ +use std::convert::Infallible; + +use warp::filters::BoxedFilter; +use warp::{Filter, Reply}; + +use crate::auth::with_auth; +use crate::types::PaginationParams; + +use super::db::DBUser; +use super::handlers; + +fn with_db( + db_pool: impl DBUser, +) -> impl Filter + Clone { + warp::any().map(move || db_pool.clone()) +} + +pub fn routes(db_access: impl DBUser) -> BoxedFilter<(impl Reply,)> { + let user = warp::path!("users"); + let user_id = warp::path!("users" / i32); + let user_maintainer = warp::path!("users" / i32 / "maintainers"); + + let get_users = user + .and(warp::get()) + .and(with_db(db_access.clone())) + .and(warp::query::()) + .and_then(handlers::all_handler); + + let get_user = user_id + .and(warp::get()) + .and(with_db(db_access.clone())) + .and_then(handlers::by_id); + + let create_user = user + .and(with_auth()) + .and(warp::post()) + .and(warp::body::aggregate()) + .and(with_db(db_access.clone())) + .and_then(handlers::create_handler); + + let update_user = user_maintainer + .and(with_auth()) + .and(warp::patch()) + .and(warp::body::json()) + .and(with_db(db_access.clone())) + .and_then(handlers::update_handler); + + let delete_user = user_id + .and(with_auth()) + .and(warp::delete()) + .and(with_db(db_access.clone())) + .and_then(handlers::delete_handler); + + let route = get_users + .or(create_user) + .or(get_user) + .or(delete_user) + .or(update_user); + + route.boxed() +} diff --git a/src/auth/errors.rs b/src/auth/errors.rs new file mode 100644 index 0000000..1a82ac8 --- /dev/null +++ b/src/auth/errors.rs @@ -0,0 +1,44 @@ +use serde::Deserialize; +use std::fmt; +use thiserror::Error; +use warp::{http::StatusCode, reject::Reject, reply::Response, Reply}; + +use crate::errors::ErrorResponse; + +#[derive(Clone, Error, Debug, Deserialize, PartialEq)] +pub enum AuthenticationError { + WrongCredentials, + BasicToken, + NoAuthHeader, + InvalidAuthHeader, +} + +impl fmt::Display for AuthenticationError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + AuthenticationError::WrongCredentials => { + write!(f, "Wrong credentials") + } + AuthenticationError::BasicToken => { + write!(f, "Basic Token Error") + } + AuthenticationError::NoAuthHeader => write!(f, "No Authorization Header"), + AuthenticationError::InvalidAuthHeader => { + write!(f, "Invalid Authorization Header") + } + } + } +} + +impl Reject for AuthenticationError {} + +impl Reply for AuthenticationError { + fn into_response(self) -> Response { + let code = StatusCode::UNAUTHORIZED; + let message = self.to_string(); + + let json = warp::reply::json(&ErrorResponse { message }); + + warp::reply::with_status(json, code).into_response() + } +} diff --git a/src/auth/mod.rs b/src/auth/mod.rs new file mode 100644 index 0000000..ee3cab2 --- /dev/null +++ b/src/auth/mod.rs @@ -0,0 +1,56 @@ +use base64::Engine; +use std::env; +use warp::{ + http::header::{HeaderMap, HeaderValue, AUTHORIZATION}, + reject, Filter, Rejection, +}; + +use self::errors::AuthenticationError; + +pub mod errors; + +const BASIC: &str = "Basic "; + +pub fn with_auth() -> impl Filter + Clone { + warp::filters::header::headers_cloned() + .and_then(authorize) + .untuple_one() +} +async fn authorize(headers: HeaderMap) -> Result<(), Rejection> { + match token_from_header(&headers) { + Ok(token) => { + let credentials = base64::prelude::BASE64_STANDARD + .decode(token) + .map_err(|_| reject::custom(AuthenticationError::WrongCredentials))?; + let credentials_str = String::from_utf8(credentials) + .map_err(|_| reject::custom(AuthenticationError::WrongCredentials))?; + let credentials: Vec<&str> = credentials_str.split(':').collect(); + + if credentials.len() == 2 { + let expected_username = env::var("USERNAME").unwrap_or("test".to_string()); + let expected_password = env::var("PASSWORD").unwrap_or("test".to_string()); + if credentials[0] == expected_username && credentials[1] == expected_password { + Ok(()) + } else { + Err(reject::custom(AuthenticationError::WrongCredentials)) + } + } else { + Err(reject::custom(AuthenticationError::WrongCredentials)) + } + } + Err(e) => Err(reject::custom(e)), + } +} + +fn token_from_header(headers: &HeaderMap) -> Result { + let header = headers + .get(AUTHORIZATION) + .ok_or(AuthenticationError::NoAuthHeader)?; + let auth_header = std::str::from_utf8(header.as_bytes()) + .map_err(|_| AuthenticationError::InvalidAuthHeader)?; + + if !auth_header.starts_with(BASIC) { + return Err(AuthenticationError::InvalidAuthHeader); + } + Ok(auth_header.trim_start_matches(BASIC).to_owned()) +} diff --git a/src/contributions/db.rs b/src/contributions/db.rs deleted file mode 100644 index c381f83..0000000 --- a/src/contributions/db.rs +++ /dev/null @@ -1,65 +0,0 @@ -use mobc::async_trait; -use mobc_postgres::tokio_postgres::Row; -use warp::reject; - -use crate::db::{ - pool::DBAccess, - utils::{ - execute_query_with_timeout, query_one_timeout, query_opt_timeout, query_with_timeout, - DB_QUERY_TIMEOUT, - }, -}; - -use super::models::{Contribution, ContributionRequest}; - -const TABLE: &str = "contribution"; - -#[async_trait] -pub trait DBContribution: Send + Sync + Clone + 'static { - async fn get_contribution(&self, id: i64) -> Result, reject::Rejection>; - async fn get_contributions(&self) -> Result, reject::Rejection>; - async fn create_contribution( - &self, - contribution: ContributionRequest, - ) -> Result; - async fn delete_contribution(&self, id: i64) -> Result<(), reject::Rejection>; -} - -#[async_trait] -impl DBContribution for DBAccess { - async fn get_contribution(&self, id: i64) -> Result, reject::Rejection> { - let query = format!( - "SELECT id FROM {} WHERE id = $1 ORDER BY created_at DESC", - TABLE - ); - match query_opt_timeout(self, query.as_str(), &[&id], DB_QUERY_TIMEOUT).await? { - Some(contribution) => Ok(Some(row_to_contribution(&contribution))), - None => Ok(None), - } - } - - async fn get_contributions(&self) -> Result, reject::Rejection> { - let query = format!("SELECT id FROM {} ORDER BY created_at DESC", TABLE); - let rows = query_with_timeout(self, query.as_str(), &[], DB_QUERY_TIMEOUT).await?; - Ok(rows.iter().map(row_to_contribution).collect()) - } - - async fn create_contribution( - &self, - contribution: ContributionRequest, - ) -> Result { - let query = format!("INSERT INTO {} (id) VALUES ($1) RETURNING *", TABLE); - let row = query_one_timeout(self, &query, &[&contribution.id], DB_QUERY_TIMEOUT).await?; - Ok(row_to_contribution(&row)) - } - - async fn delete_contribution(&self, id: i64) -> Result<(), reject::Rejection> { - let query = format!("DELETE FROM {} WHERE id = $1", TABLE); - execute_query_with_timeout(self, &query, &[&id], DB_QUERY_TIMEOUT).await - } -} - -fn row_to_contribution(row: &Row) -> Contribution { - let id: i64 = row.get(0); - Contribution { id } -} diff --git a/src/contributions/errors.rs b/src/contributions/errors.rs deleted file mode 100644 index daa9021..0000000 --- a/src/contributions/errors.rs +++ /dev/null @@ -1,46 +0,0 @@ -use std::fmt; - -use serde_derive::Deserialize; -use thiserror::Error; -use warp::{ - http::StatusCode, - reject::Reject, - reply::{Reply, Response}, -}; - -use crate::handlers::ErrorResponse; - -#[derive(Clone, Error, Debug, Deserialize, PartialEq)] -pub enum ContributionError { - ContributionExists(i64), - ContributionNotFound(i64), -} - -impl fmt::Display for ContributionError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - ContributionError::ContributionExists(id) => { - write!(f, "Contribution #{} already exists", id) - } - ContributionError::ContributionNotFound(id) => { - write!(f, "Contribution #{} not found", id) - } - } - } -} - -impl Reject for ContributionError {} - -impl Reply for ContributionError { - fn into_response(self) -> Response { - let code = match self { - ContributionError::ContributionExists(_) => StatusCode::BAD_REQUEST, - ContributionError::ContributionNotFound(_) => StatusCode::NOT_FOUND, - }; - let message = self.to_string(); - - let json = warp::reply::json(&ErrorResponse { message }); - - warp::reply::with_status(json, code).into_response() - } -} diff --git a/src/contributions/handlers.rs b/src/contributions/handlers.rs deleted file mode 100644 index c9293b5..0000000 --- a/src/contributions/handlers.rs +++ /dev/null @@ -1,64 +0,0 @@ -use warp::{ - http::StatusCode, - reject::Rejection, - reply::{json, Reply}, -}; - -use super::{ - db::DBContribution, - errors::ContributionError, - models::{ContributionRequest, ContributionResponse}, -}; - -pub async fn create_contribution_handler( - body: ContributionRequest, - db_access: impl DBContribution, -) -> Result { - match db_access.get_contribution(body.id).await? { - Some(_) => Err(warp::reject::custom(ContributionError::ContributionExists( - body.id, - )))?, - None => Ok(json(&ContributionResponse::of( - db_access.create_contribution(body).await?, - ))), - } -} - -pub async fn get_contribution_handler( - id: i64, - db_access: impl DBContribution, -) -> Result { - match db_access.get_contribution(id).await? { - None => Err(warp::reject::custom( - ContributionError::ContributionNotFound(id), - ))?, - Some(contribution) => Ok(json(&ContributionResponse::of(contribution))), - } -} - -pub async fn get_contributions_handler( - db_access: impl DBContribution, -) -> Result { - let contributions = db_access.get_contributions().await?; - Ok(json::>( - &contributions - .into_iter() - .map(ContributionResponse::of) - .collect(), - )) -} - -pub async fn delete_contribution_handler( - id: i64, - db_access: impl DBContribution, -) -> Result { - match db_access.get_contribution(id).await? { - Some(_) => { - let _ = &db_access.delete_contribution(id).await?; - Ok(StatusCode::OK) - } - None => Err(warp::reject::custom( - ContributionError::ContributionNotFound(id), - ))?, - } -} diff --git a/src/contributions/models.rs b/src/contributions/models.rs deleted file mode 100644 index 146756c..0000000 --- a/src/contributions/models.rs +++ /dev/null @@ -1,24 +0,0 @@ -use serde_derive::{Deserialize, Serialize}; - -#[derive(Deserialize)] -pub struct Contribution { - pub id: i64, -} - -#[derive(Serialize, Deserialize)] -pub struct ContributionRequest { - pub id: i64, -} - -#[derive(Serialize, Deserialize, PartialEq, Debug)] -pub struct ContributionResponse { - pub id: i64, -} - -impl ContributionResponse { - pub fn of(contribution: Contribution) -> ContributionResponse { - ContributionResponse { - id: contribution.id, - } - } -} diff --git a/src/contributions/routes.rs b/src/contributions/routes.rs deleted file mode 100644 index 672d245..0000000 --- a/src/contributions/routes.rs +++ /dev/null @@ -1,47 +0,0 @@ -use std::convert::Infallible; - -use warp::filters::BoxedFilter; -use warp::{Filter, Reply}; - -use super::db::DBContribution; -use super::handlers; - -fn with_db( - db_pool: impl DBContribution, -) -> impl Filter + Clone { - warp::any().map(move || db_pool.clone()) -} - -pub fn routes(db_access: impl DBContribution) -> BoxedFilter<(impl Reply,)> { - let contribution = warp::path!("contribution"); - let contribution_id = warp::path!("contribution" / i64); - - let get_contributions = contribution - .and(warp::get()) - .and(with_db(db_access.clone())) - .and_then(handlers::get_contributions_handler); - - let get_contribution = contribution_id - .and(warp::get()) - // .and(warp::path::param()) - .and(with_db(db_access.clone())) - .and_then(handlers::get_contribution_handler); - - let create_contribution = contribution - .and(warp::post()) - .and(warp::body::json()) - .and(with_db(db_access.clone())) - .and_then(handlers::create_contribution_handler); - - let delete_contribution = contribution_id - .and(warp::delete()) - .and(with_db(db_access.clone())) - .and_then(handlers::delete_contribution_handler); - - let route = get_contributions - .or(get_contribution) - .or(create_contribution) - .or(delete_contribution); - - route.boxed() -} diff --git a/src/db/errors.rs b/src/db/errors.rs index 75a27e5..43633b3 100644 --- a/src/db/errors.rs +++ b/src/db/errors.rs @@ -1,15 +1,14 @@ -use mobc_postgres::tokio_postgres; +use diesel::r2d2::PoolError; +use diesel::result::Error as DieselError; use thiserror::Error; use tokio::time::error::Elapsed; #[derive(Error, Debug)] pub enum DBError { #[error("error getting connection from DB pool: {0}")] - DBPoolConnection(mobc::Error), + DBPoolConnection(PoolError), #[error("error executing DB query: {0}")] - DBQuery(#[from] tokio_postgres::Error), - #[error("error creating table: {0}")] - DBInit(tokio_postgres::Error), + DBQuery(#[from] DieselError), #[error("error reading file: {0}")] ReadFile(#[from] std::io::Error), #[error("database operation timed out: {0}")] diff --git a/src/db/mod.rs b/src/db/mod.rs index f98c1d7..53c4b60 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,4 +1,3 @@ pub mod errors; pub mod pool; pub mod types; -pub mod utils; diff --git a/src/db/pool.rs b/src/db/pool.rs index 64bc691..08a3a4c 100644 --- a/src/db/pool.rs +++ b/src/db/pool.rs @@ -1,55 +1,41 @@ -use mobc::{async_trait, Pool}; -use mobc_postgres::{tokio_postgres, PgConnectionManager}; -use std::fs; -use std::str::FromStr; +use diesel::prelude::*; +use diesel::r2d2::{self, ConnectionManager, PoolError}; +use std::sync::Arc; use std::time::Duration; -use tokio_postgres::{Config, Error, NoTls}; -use crate::db::errors::DBError; -use crate::db::types::{DBCon, DBPool}; +use crate::db::types::{DBConn, DBPool}; -const DB_POOL_MAX_OPEN: u64 = 32; // TODO: move to config -const DB_POOL_MAX_IDLE: u64 = 8; // TODO: move to config +const DB_POOL_MAX_OPEN: u32 = 32; // TODO: move to config +const DB_POOL_MIN_IDLE: u32 = 8; // TODO: move to config const DB_POOL_TIMEOUT_SECONDS: u64 = 15; // TODO: move to config -pub fn create_pool(database_url: &str) -> std::result::Result> { - let config = Config::from_str(database_url)?; - - let manager = PgConnectionManager::new(config, NoTls); - Ok(Pool::builder() - .max_open(DB_POOL_MAX_OPEN) - .max_idle(DB_POOL_MAX_IDLE) - .get_timeout(Some(Duration::from_secs(DB_POOL_TIMEOUT_SECONDS))) - .build(manager)) +pub fn create_db_pool(database_url: &str) -> Result { + let manager = ConnectionManager::::new(database_url); + r2d2::Pool::builder() + .max_size(DB_POOL_MAX_OPEN) + .min_idle(Some(DB_POOL_MIN_IDLE)) + .connection_timeout(Duration::from_secs(DB_POOL_TIMEOUT_SECONDS)) + .build(manager) } -#[async_trait] pub trait DBAccessor: Send + Sync + Clone + 'static { fn new(db_pool: DBPool) -> Self; - async fn get_db_con(&self) -> Result; - async fn init_db(&self, sql_file: &str) -> Result<(), DBError>; + fn get_db_conn(&self) -> DBConn; } #[derive(Clone)] pub struct DBAccess { - pub db_pool: DBPool, + pub db_pool: Arc, } -#[async_trait] + impl DBAccessor for DBAccess { fn new(db_pool: DBPool) -> Self { - Self { db_pool } - } - - async fn get_db_con(&self) -> Result { - self.db_pool.get().await.map_err(DBError::DBPoolConnection) + Self { + db_pool: Arc::new(db_pool), + } } - async fn init_db(&self, sql_file: &str) -> Result<(), DBError> { - let init_file = fs::read_to_string(sql_file)?; - let con = self.get_db_con().await?; - con.batch_execute(init_file.as_str()) - .await - .map_err(DBError::DBInit)?; - Ok(()) + fn get_db_conn(&self) -> DBConn { + self.db_pool.get().expect("Failed to get db connection") } } diff --git a/src/db/types.rs b/src/db/types.rs index 65343fd..1f18739 100644 --- a/src/db/types.rs +++ b/src/db/types.rs @@ -1,6 +1,6 @@ -use mobc::{Connection, Pool}; -use mobc_postgres::{tokio_postgres, PgConnectionManager}; -use tokio_postgres::NoTls; +use diesel::r2d2::PooledConnection; +use diesel::r2d2::{self, ConnectionManager}; +use diesel::PgConnection; -pub type DBCon = Connection>; -pub type DBPool = Pool>; +pub type DBConn = PooledConnection>; +pub type DBPool = r2d2::Pool>; diff --git a/src/db/utils.rs b/src/db/utils.rs deleted file mode 100644 index cdf004e..0000000 --- a/src/db/utils.rs +++ /dev/null @@ -1,77 +0,0 @@ -use super::{ - errors::DBError, - pool::{self, DBAccess, DBAccessor}, -}; -use mobc_postgres::tokio_postgres::{types::ToSql, Row}; -use tokio::time::{timeout, Duration}; -use warp::reject; - -pub const DB_QUERY_TIMEOUT: Duration = Duration::from_secs(5); - -pub async fn query_with_timeout( - db_access: &impl DBAccessor, - query: &str, - params: &[&(dyn ToSql + Sync)], - timeout_duration: Duration, -) -> Result, reject::Rejection> { - let db_conn = db_access.get_db_con().await.map_err(reject::custom)?; - - timeout(timeout_duration, db_conn.query(query, params)) - .await - .map_err(|err| reject::custom(DBError::DBTimeout(err)))? - .map_err(|err| reject::custom(DBError::DBQuery(err))) -} - -pub async fn execute_query_with_timeout( - db_access: &impl DBAccessor, - query: &str, - params: &[&(dyn ToSql + Sync)], - timeout_duration: Duration, -) -> Result<(), reject::Rejection> { - let db_conn = db_access.get_db_con().await.map_err(reject::custom)?; - - timeout(timeout_duration, db_conn.execute(query, params)) - .await - .map_err(|err| reject::custom(DBError::DBTimeout(err)))? - .map_err(|err| reject::custom(DBError::DBQuery(err)))?; - Ok(()) -} - -pub async fn query_opt_timeout( - db_access: &impl DBAccessor, - query: &str, - params: &[&(dyn ToSql + Sync)], - timeout_duration: Duration, -) -> Result, reject::Rejection> { - let db_conn = db_access.get_db_con().await.map_err(reject::custom)?; - - timeout(timeout_duration, db_conn.query_opt(query, params)) - .await - .map_err(|err| reject::custom(DBError::DBTimeout(err)))? - .map_err(|err| reject::custom(DBError::DBQuery(err))) -} - -pub async fn query_one_timeout( - db_access: &impl DBAccessor, - query: &str, - params: &[&(dyn ToSql + Sync)], - timeout_duration: Duration, -) -> Result { - let db_conn = db_access.get_db_con().await.map_err(reject::custom)?; - - timeout(timeout_duration, db_conn.query_one(query, params)) - .await - .map_err(|err| reject::custom(DBError::DBTimeout(err)))? - .map_err(|err| reject::custom(DBError::DBQuery(err))) -} - -pub async fn init_db( - database_url: String, - database_init_file: String, -) -> Result { - let db_pool = pool::create_pool(&database_url).map_err(DBError::DBPoolConnection)?; - let db = DBAccess::new(db_pool); - // TODO: use migrations - db.init_db(&database_init_file).await?; - Ok(db) -} diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..61ea21f --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,73 @@ +use serde::Serialize; +use serde_derive::Deserialize; +use std::convert::Infallible; +use warp::{hyper::StatusCode, Rejection, Reply}; + +use crate::{ + api::issues::errors::IssueError, api::projects::errors::ProjectError, + api::repositories::errors::RepositoryError, api::users::errors::UserError, + auth::errors::AuthenticationError, db::errors::DBError, +}; + +#[derive(Serialize, Deserialize, PartialEq, Debug)] +pub struct ErrorResponse { + pub message: String, +} + +pub async fn error_handler(err: Rejection) -> std::result::Result { + if let Some(e) = err.find::() { + return Ok(e.clone().into_response()); + } else if let Some(e) = err.find::() { + return Ok(e.clone().into_response()); + } else if let Some(e) = err.find::() { + return Ok(e.clone().into_response()); + } else if let Some(e) = err.find::() { + return Ok(e.clone().into_response()); + } + // TODO: add more errors + + let (status, message) = if err.is_not_found() { + (StatusCode::NOT_FOUND, "Resource not found".to_string()) + } else if let Some(e) = err.find::() { + eprintln!("BodyDeserializeError error: {:?}", e); + (StatusCode::BAD_REQUEST, "Invalid request body".to_string()) + } else if let Some(e) = err.find::() { + eprintln!("InvalidQuery error: {:?}", e); + ( + StatusCode::BAD_REQUEST, + "Invalid query parameters".to_string(), + ) + } else if let Some(e) = err.find::() { + eprintln!("AuthenticationError: {e}"); + ( + StatusCode::UNAUTHORIZED, + format!("AuthenticationError - {e}"), + ) + } else if let Some(db_error) = err.find::() { + match db_error { + DBError::DBPoolConnection(_) => ( + StatusCode::INTERNAL_SERVER_ERROR, + "Database connection error".to_string(), + ), + DBError::DBQuery(_) => (StatusCode::BAD_REQUEST, "Database query failed".to_string()), + DBError::ReadFile(_) => ( + StatusCode::INTERNAL_SERVER_ERROR, + "File read error".to_string(), + ), + DBError::DBTimeout(_) => ( + StatusCode::REQUEST_TIMEOUT, + "Database operation timed out".to_string(), + ), + } + } else { + eprintln!("Unhandled error: {:?}", err); // Ensure all unexpected errors are logged. + ( + StatusCode::INTERNAL_SERVER_ERROR, + "Internal server error".to_string(), + ) + }; + + let json = warp::reply::json(&ErrorResponse { message }); + let response = warp::reply::with_status(json, status).into_response(); + Ok(response) +} diff --git a/src/handlers.rs b/src/handlers.rs deleted file mode 100644 index f29f91e..0000000 --- a/src/handlers.rs +++ /dev/null @@ -1,68 +0,0 @@ -use serde::Serialize; -use serde_derive::Deserialize; -use std::convert::Infallible; -use warp::{hyper::StatusCode, Rejection, Reply}; - -use crate::{contributions::errors::ContributionError, db::errors::DBError}; - -#[derive(Serialize, Deserialize, PartialEq, Debug)] -pub struct ErrorResponse { - pub message: String, -} - -pub async fn error_handler(err: Rejection) -> std::result::Result { - if let Some(e) = err.find::() { - Ok(e.clone().into_response()) - } else if let Some(e) = err.find::() { - let (code, message) = match e { - DBError::DBPoolConnection(_) => ( - StatusCode::INTERNAL_SERVER_ERROR, - "Database connection error", - ), - DBError::DBQuery(_) => (StatusCode::BAD_REQUEST, "Database query failed"), - DBError::DBInit(_) => ( - StatusCode::INTERNAL_SERVER_ERROR, - "Error initializing database", - ), - DBError::ReadFile(_) => (StatusCode::INTERNAL_SERVER_ERROR, "File read error"), - DBError::DBTimeout(_) => (StatusCode::REQUEST_TIMEOUT, "Database operation timed out"), - }; - - let json = warp::reply::json(&ErrorResponse { - message: message.to_string(), - }); - - Ok(warp::reply::with_status(json, code).into_response()) - } else { - let code; - let message; - - if err.is_not_found() { - // Handle not found errors - code = StatusCode::NOT_FOUND; - message = "Not Found"; - } else if err - .find::() - .is_some() - { - // Handle invalid body errors - code = StatusCode::BAD_REQUEST; - message = "Invalid Body"; - } else if err.find::().is_some() { - // Handle method not allowed errors - code = StatusCode::METHOD_NOT_ALLOWED; - message = "Method Not Allowed"; - } else { - // Handle all other errors - eprintln!("Unhandled error: {:?}", err); - code = StatusCode::INTERNAL_SERVER_ERROR; - message = "Internal Server Error"; - } - - let json = warp::reply::json(&ErrorResponse { - message: message.into(), - }); - - Ok(warp::reply::with_status(json, code).into_response()) - } -} diff --git a/src/health/db.rs b/src/health/db.rs deleted file mode 100644 index 83a192e..0000000 --- a/src/health/db.rs +++ /dev/null @@ -1,20 +0,0 @@ -use mobc::async_trait; -use warp::reject; - -use crate::db::{ - pool::DBAccess, - utils::{execute_query_with_timeout, DB_QUERY_TIMEOUT}, -}; - -#[async_trait] -pub trait DBHealth: Send + Sync + Clone + 'static { - async fn health(&self) -> Result<(), reject::Rejection>; -} - -#[async_trait] -impl DBHealth for DBAccess { - async fn health(&self) -> Result<(), reject::Rejection> { - execute_query_with_timeout(self, "SELECT 1", &[], DB_QUERY_TIMEOUT).await?; - Ok(()) - } -} diff --git a/src/main.rs b/src/main.rs index 7ff3875..636ce9c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,14 @@ mod types; - -use db::utils::init_db; -use warp::Filter; +use log::{info, warn}; use crate::types::ApiConfig; -mod contributions; +mod api; +mod auth; mod db; -mod handlers; -mod health; +mod errors; +pub mod schema; +mod utils; #[cfg(test)] mod tests; @@ -23,27 +23,19 @@ async fn run() { http_server_host: host, http_server_port: port, database_url, - database_init_file, } = ApiConfig::new(); - // init db - let db = init_db(database_url, database_init_file).await.unwrap(); //If there's an error the api should panic - - let health_route = health::routes::routes(db.clone()); - let contribution_route = contributions::routes::routes(db); - let error_handler = handlers::error_handler; + env_logger::init(); - // string all the routes together - let routes = health_route - .or(contribution_route) - .with(warp::cors().allow_any_origin()) - .recover(error_handler); + let db = utils::setup_db(&database_url).await; + let app_filters = utils::setup_filters(db); let addr = format!("{}:{}", host, port) .parse::() .expect("Invalid server address"); - println!("listening on {}", addr); + info!("listening on {}", addr); + warn!("listening on {}", addr); - warp::serve(routes).run(addr).await; + warp::serve(app_filters).run(addr).await; } diff --git a/src/schema.rs b/src/schema.rs new file mode 100644 index 0000000..405c90f --- /dev/null +++ b/src/schema.rs @@ -0,0 +1,75 @@ +// @generated automatically by Diesel CLI. + +diesel::table! { + issues (id) { + id -> Int4, + number -> Int4, + title -> Text, + labels -> Nullable>>, + open -> Bool, + certified -> Nullable, + assignee_id -> Nullable, + repository_id -> Int4, + issue_created_at -> Timestamptz, + issue_closed_at -> Nullable, + created_at -> Timestamptz, + updated_at -> Nullable, + } +} + +diesel::table! { + languages (id) { + id -> Int4, + slug -> Text, + created_at -> Timestamptz, + updated_at -> Nullable, + } +} + +diesel::table! { + projects (id) { + id -> Int4, + name -> Text, + slug -> Text, + types -> Nullable>>, + purposes -> Nullable>>, + stack_levels -> Nullable>>, + technologies -> Nullable>>, + created_at -> Timestamptz, + updated_at -> Nullable, + } +} + +diesel::table! { + repositories (id) { + id -> Int4, + slug -> Text, + name -> Text, + url -> Text, + language_slug -> Text, + project_id -> Int4, + created_at -> Timestamptz, + updated_at -> Nullable, + } +} + +diesel::table! { + users (id) { + id -> Int4, + username -> Text, + created_at -> Timestamptz, + updated_at -> Nullable, + } +} + +diesel::joinable!(issues -> repositories (repository_id)); +diesel::joinable!(issues -> users (assignee_id)); +diesel::joinable!(repositories -> projects (project_id)); + +diesel::allow_tables_to_appear_in_same_query!( + issues, + languages, + projects, + repositories, + users, +); diff --git a/src/tests/contribution.rs b/src/tests/contribution.rs deleted file mode 100644 index 9eb7012..0000000 --- a/src/tests/contribution.rs +++ /dev/null @@ -1,443 +0,0 @@ -#[cfg(test)] -mod tests { - use crate::{ - contributions::routes::routes, - contributions::{ - db::DBContribution, - models::{Contribution, ContributionRequest, ContributionResponse}, - }, - handlers::{error_handler, ErrorResponse}, - init_db, - }; - use mobc::async_trait; - use warp::{reject, test::request, Filter}; - - #[derive(Clone)] - pub struct DBMockValues {} - #[derive(Clone)] - pub struct DBMockEmpty {} - - #[async_trait] - impl DBContribution for DBMockValues { - async fn get_contribution( - &self, - id: i64, - ) -> Result, reject::Rejection> { - Ok(Some(Contribution { id })) - } - async fn get_contributions(&self) -> Result, reject::Rejection> { - Ok(vec![Contribution { id: 1 }]) - } - async fn create_contribution( - &self, - contribution: ContributionRequest, - ) -> Result { - Ok(Contribution { - id: contribution.id, - }) - } - async fn delete_contribution(&self, _: i64) -> Result<(), reject::Rejection> { - Ok(()) - } - } - #[async_trait] - impl DBContribution for DBMockEmpty { - async fn get_contribution( - &self, - _: i64, - ) -> Result, reject::Rejection> { - Ok(None) - } - async fn get_contributions(&self) -> Result, reject::Rejection> { - Ok(vec![]) - } - async fn create_contribution( - &self, - contribution: ContributionRequest, - ) -> Result { - Ok(Contribution { - id: contribution.id, - }) - } - async fn delete_contribution(&self, _: i64) -> Result<(), reject::Rejection> { - Ok(()) - } - } - - #[tokio::test] - async fn test_get_contribution_mock_db() { - let id = 1; - let r = routes(DBMockValues {}); - let resp = request() - .path(&format!("/contribution/{id}")) - .reply(&r) - .await; - assert_eq!(resp.status(), 200); - let body = resp.into_body(); - assert!(!body.is_empty()); - let expected_response = ContributionResponse { id }; - let response: ContributionResponse = serde_json::from_slice(&body).unwrap(); - assert_eq!(response, expected_response) - } - #[tokio::test] - async fn test_get_contribution_not_found_mock_db() { - let id = 1; - let r = routes(DBMockEmpty {}).recover(error_handler); - let resp = request() - .path(&format!("/contribution/{id}")) - .reply(&r) - .await; - - assert_eq!(resp.status(), 404); - let body = resp.into_body(); - assert!(!body.is_empty()); - - let expected_response = ErrorResponse { - message: format!("Contribution #{} not found", id), - }; - let response: ErrorResponse = serde_json::from_slice(&body).unwrap(); - - assert_eq!(response, expected_response) - } - - #[tokio::test] - async fn test_get_contributions_mock_db() { - let r = routes(DBMockValues {}); - let resp = request().path(&format!("/contribution")).reply(&r).await; - - assert_eq!(resp.status(), 200); - let body = resp.into_body(); - assert!(!body.is_empty()); - let expected_response = vec![ContributionResponse { id: 1 }]; - let response: Vec = serde_json::from_slice(&body).unwrap(); - assert_eq!(response, expected_response) - } - - #[tokio::test] - async fn test_get_contributions_empty_mock_db() { - let r = routes(DBMockEmpty {}); - let resp = request().path(&format!("/contribution")).reply(&r).await; - assert_eq!(resp.status(), 200); - - let body = resp.into_body(); - let response: Vec = serde_json::from_slice(&body).unwrap(); - let expected_response: Vec = vec![]; - assert_eq!(response, expected_response); - } - - #[tokio::test] - async fn test_create_contribution_mock_db() { - let id = 1; - let new_contribution = serde_json::to_vec(&ContributionRequest { id }).unwrap(); - let r = routes(DBMockEmpty {}); - let resp = request() - .body(new_contribution) - .path(&"/contribution") - .method("POST") - .reply(&r) - .await; - assert_eq!(resp.status(), 200); - let body = resp.into_body(); - assert!(!body.is_empty()); - - let expected_response = ContributionResponse { id }; - let response: ContributionResponse = serde_json::from_slice(&body).unwrap(); - assert_eq!(response, expected_response) - } - - #[tokio::test] - async fn test_create_contribution_already_exists_mock_db() { - let id = 1; - let new_contribution = serde_json::to_vec(&ContributionRequest { id }).unwrap(); - let r = routes(DBMockValues {}).recover(error_handler); - let resp = request() - .body(new_contribution) - .path(&"/contribution") - .method("POST") - .reply(&r) - .await; - assert_eq!(resp.status(), 400); - let body = resp.into_body(); - assert!(!body.is_empty()); - - let expected_response = ErrorResponse { - message: format!("Contribution #{} already exists", id), - }; - - let response: ErrorResponse = serde_json::from_slice(&body).unwrap(); - assert_eq!(response, expected_response) - } - - #[tokio::test] - async fn test_delete_contribution_mock_db() { - let id = 1; - let r = routes(DBMockValues {}); - let resp = request() - .path(&format!("/contribution/{id}")) - .method("DELETE") - .reply(&r) - .await; - assert_eq!(resp.status(), 200); - let body = resp.into_body(); - assert!(body.is_empty()); - } - - #[tokio::test] - async fn test_delete_contribution_does_not_exist_mock_db() { - let id = 1; - let new_contribution = serde_json::to_vec(&ContributionRequest { id }).unwrap(); - let r = routes(DBMockEmpty {}).recover(error_handler); - let resp = request() - .body(new_contribution) - .path(&format!("/contribution/{id}")) - .method("DELETE") - .reply(&r) - .await; - - assert_eq!(resp.status(), 404); - let body = resp.into_body(); - assert!(!body.is_empty()); - - let expected_response = ErrorResponse { - message: format!("Contribution #{} not found", id), - }; - let response: ErrorResponse = serde_json::from_slice(&body).unwrap(); - assert_eq!(response, expected_response) - } - - #[tokio::test] - #[ignore] - async fn test_create_contribution_db() { - let id = 1; - let new_contribution = serde_json::to_vec(&ContributionRequest { id }).unwrap(); - let db = init_db( - "postgres://postgres:password@localhost:5432/database".to_string(), - "db_test.sql".to_string(), - ) - .await - .unwrap(); - let r = routes(db); - let resp = request() - .body(new_contribution) - .path(&"/contribution") - .method("POST") - .reply(&r) - .await; - assert_eq!(resp.status(), 200); - let body = resp.into_body(); - assert!(!body.is_empty()); - - let expected_response = ContributionResponse { id }; - let response: ContributionResponse = serde_json::from_slice(&body).unwrap(); - assert_eq!(response, expected_response) - } - - #[tokio::test] - #[ignore] - async fn test_create_contribution_already_exists_db() { - let id = 1; - let new_contribution = serde_json::to_vec(&ContributionRequest { id }).unwrap(); - let db = init_db( - "postgres://postgres:password@localhost:5432/database".to_string(), - "db_test.sql".to_string(), - ) - .await - .unwrap(); - let r = routes(db).recover(error_handler); - let _ = request() - .body(new_contribution.clone()) - .path(&"/contribution") - .method("POST") - .reply(&r) - .await; - let resp = request() - .body(new_contribution) - .path(&"/contribution") - .method("POST") - .reply(&r) - .await; - assert_eq!(resp.status(), 400); - let body = resp.into_body(); - assert!(!body.is_empty()); - - let expected_response = ErrorResponse { - message: format!("Contribution #{} already exists", id), - }; - - let response: ErrorResponse = serde_json::from_slice(&body).unwrap(); - assert_eq!(response, expected_response) - } - - #[tokio::test] - #[ignore] - async fn test_get_contribution_not_found_db() { - let id = 1; - let db = init_db( - "postgres://postgres:password@localhost:5432/database".to_string(), - "db_test.sql".to_string(), - ) - .await - .unwrap(); - let r = routes(db).recover(error_handler); - let resp = request() - .path(&format!("/contribution/{id}")) - .reply(&r) - .await; - - assert_eq!(resp.status(), 404); - let body = resp.into_body(); - assert!(!body.is_empty()); - - let expected_response = ErrorResponse { - message: "Contribution #1 not found".to_string(), - }; - let response: ErrorResponse = serde_json::from_slice(&body).unwrap(); - - assert_eq!(response, expected_response) - } - - #[tokio::test] - #[ignore] - async fn test_get_contribution_db() { - let id = 1; - let new_contribution = serde_json::to_vec(&ContributionRequest { id }).unwrap(); - let db = init_db( - "postgres://postgres:password@localhost:5432/database".to_string(), - "db_test.sql".to_string(), - ) - .await - .unwrap(); - let r = routes(db); - let _ = request() - .body(new_contribution) - .path(&"/contribution") - .method("POST") - .reply(&r) - .await; - let resp = request() - .path(&format!("/contribution/{id}")) - .reply(&r) - .await; - // assert_eq!(resp.status(), 200); - let body = resp.into_body(); - assert!(!body.is_empty()); - let expected_response = ContributionResponse { id }; - let response: ContributionResponse = serde_json::from_slice(&body).unwrap(); - assert_eq!(response, expected_response) - } - - #[tokio::test] - #[ignore] - async fn test_get_contributions_empty_db() { - let db = init_db( - "postgres://postgres:password@localhost:5432/database".to_string(), - "db_test.sql".to_string(), - ) - .await - .unwrap(); - let r = routes(db).recover(error_handler); - let resp = request().path(&format!("/contribution")).reply(&r).await; - - assert_eq!(resp.status(), 200); - let body = resp.into_body(); - let response: Vec = serde_json::from_slice(&body).unwrap(); - let expected_response: Vec = vec![]; - assert_eq!(response, expected_response); - } - #[tokio::test] - #[ignore] - async fn test_get_contributions_db() { - let id = 1; - let new_contribution = serde_json::to_vec(&ContributionRequest { id }).unwrap(); - let db = init_db( - "postgres://postgres:password@localhost:5432/database".to_string(), - "db_test.sql".to_string(), - ) - .await - .unwrap(); - let r = routes(db); - let _ = request() - .body(new_contribution) - .path(&"/contribution") - .method("POST") - .reply(&r) - .await; - let resp = request().path(&format!("/contribution")).reply(&r).await; - assert_eq!(resp.status(), 200); - - let body = resp.into_body(); - assert!(!body.is_empty()); - let expected_response = vec![ContributionResponse { id: 1 }]; - let response: Vec = serde_json::from_slice(&body).unwrap(); - assert_eq!(response, expected_response) - } - - #[tokio::test] - #[ignore] - async fn test_delete_contribution_db() { - let id = 1; - let new_contribution = serde_json::to_vec(&ContributionRequest { id }).unwrap(); - let db = init_db( - "postgres://postgres:password@localhost:5432/database".to_string(), - "db_test.sql".to_string(), - ) - .await - .unwrap(); - let r = routes(db).recover(error_handler); - let resp = request() - .body(new_contribution) - .path(&"/contribution") - .method("POST") - .reply(&r) - .await; - assert_eq!(resp.status(), 200); - - let resp = request() - .path(&format!("/contribution/{id}")) - .method("DELETE") - .reply(&r) - .await; - assert_eq!(resp.status(), 200); - let body = resp.into_body(); - assert!(body.is_empty()); - - let resp = request() - .path(&format!("/contribution/{id}")) - .reply(&r) - .await; - - assert_eq!(resp.status(), 404); - let body = resp.into_body(); - assert!(!body.is_empty()); - } - - #[tokio::test] - #[ignore] - async fn test_delete_contribution_does_not_exist_db() { - let id = 1; - let db = init_db( - "postgres://postgres:password@localhost:5432/database".to_string(), - "db_test.sql".to_string(), - ) - .await - .unwrap(); - let new_contribution = serde_json::to_vec(&ContributionRequest { id }).unwrap(); - let r = routes(db).recover(error_handler); - let resp = request() - .body(new_contribution) - .path(&format!("/contribution/{id}")) - .method("DELETE") - .reply(&r) - .await; - - assert_eq!(resp.status(), 404); - let body = resp.into_body(); - assert!(!body.is_empty()); - - let expected_response = ErrorResponse { - message: "Contribution #1 not found".to_string(), - }; - let response: ErrorResponse = serde_json::from_slice(&body).unwrap(); - assert_eq!(response, expected_response) - } -} diff --git a/src/tests/health.rs b/src/tests/health.rs index 2b57128..e882745 100644 --- a/src/tests/health.rs +++ b/src/tests/health.rs @@ -1,18 +1,17 @@ #[cfg(test)] mod tests { use crate::{ - health::{db::DBHealth, routes::routes}, - init_db, + api::health::{db::DBHealth, routes::routes}, + db::errors::DBError, + tests::utils::generate_test_database, }; - use mobc::async_trait; - use warp::{reject, test::request}; + use warp::test::request; #[derive(Clone)] pub struct DBMock {} - #[async_trait] impl DBHealth for DBMock { - async fn health(&self) -> Result<(), reject::Rejection> { + fn health(&self) -> Result<(), DBError> { Ok(()) } } @@ -28,19 +27,10 @@ mod tests { #[tokio::test] #[ignore] async fn test_health_db() { - let db = init_db( - "postgres://postgres:password@localhost:5432/database".to_string(), - "db.sql".to_string(), - ) - .await - .unwrap(); - + let db = generate_test_database().await; let r = routes(db); let resp = request().path("/health").reply(&r).await; assert_eq!(resp.status(), 200); assert!(resp.body().is_empty()); } } - -// TODO: add e2e test using a real http server. -// hyper can be used for that diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 99d31de..b26c5df 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -1,2 +1,2 @@ -pub mod contribution; pub mod health; +pub mod utils; diff --git a/src/tests/repositories.rs b/src/tests/repositories.rs new file mode 100644 index 0000000..97afef7 --- /dev/null +++ b/src/tests/repositories.rs @@ -0,0 +1,385 @@ +#[cfg(test)] +pub mod tests { + use crate::error_handler::{error_handler, ErrorResponse}; + use crate::organization::db::DBOrganization; + use crate::organization::models::{Organization, OrganizationRequest}; + use crate::pagination::GetPagination; + use crate::repository::models::{ + NewRepository, RepositoriesRelations, RepositoryResponse, RepositorySort, + }; + use crate::repository::routes::routes; + use crate::repository::{db::DBRepository, models::Repository}; + use mobc::async_trait; + use warp::test::request; + use warp::{reject, Filter}; + + #[derive(Clone)] + pub struct RepositoriesDBMock {} + + #[async_trait] + impl DBRepository for RepositoriesDBMock { + async fn get_repository( + &self, + id: i32, + relations: RepositoriesRelations, + ) -> Result, reject::Rejection> { + if id == 1 { + Ok(Some(Repository { + id, + name: "repo".to_owned(), + organization_id: 1, + icon: "icon".to_string(), + url: "url".to_string(), + e_tag: "e_tag".to_string(), + })) + } else { + Ok(None) + } + } + async fn get_repository_by_name( + &self, + name: &str, + relations: RepositoriesRelations, + ) -> Result, reject::Rejection> { + if name == "not_found" || name == "new" { + Ok(None) + } else { + Ok(Some(Repository { + id: 1, + name: "repo".to_owned(), + organization_id: 1, + icon: "icon".to_string(), + url: "url".to_string(), + e_tag: "e_tag".to_string(), + })) + } + } + async fn get_repositories( + &self, + relations: RepositoriesRelations, + pagination: GetPagination, + sort: RepositorySort, + ) -> Result, reject::Rejection> { + Ok(vec![]) + } + async fn create_repository( + &self, + repository: NewRepository, + ) -> Result { + Ok(Repository { + name: repository.name.to_string(), + id: 1, + organization_id: repository.organization_id, + icon: repository.icon, + url: repository.url, + e_tag: repository.e_tag, + }) + } + async fn delete_repository(&self, id: i32) -> Result<(), reject::Rejection> { + Ok(()) + } + } + + #[async_trait] + impl DBOrganization for RepositoriesDBMock { + async fn get_organization( + &self, + id: i32, + ) -> Result, reject::Rejection> { + Ok(Some(Organization { + id, + name: "ok".to_string(), + })) + } + async fn get_organization_by_name( + &self, + name: &str, + ) -> Result, reject::Rejection> { + Ok(Some(Organization { + id: 1, + name: name.to_string(), + })) + } + async fn get_organizations(&self) -> Result, reject::Rejection> { + Ok(vec![ + (Organization { + id: 1, + name: "name".to_string(), + }), + ]) + } + async fn create_organization( + &self, + organization: OrganizationRequest, + ) -> Result { + Ok(Organization { + id: 1, + name: organization.name, + }) + } + async fn delete_organization(&self, id: i32) -> Result<(), reject::Rejection> { + Ok(()) + } + } + #[tokio::test] + async fn test_get_repo_by_id_not_found() { + let id = 2; + let r = routes(RepositoriesDBMock {}).recover(error_handler); + let resp = request() + .path(&format!("/repositories/{id}")) + .reply(&r) + .await; + assert_eq!(resp.status(), 404); + let body = resp.into_body(); + assert!(!body.is_empty()); + + let response: ErrorResponse = serde_json::from_slice(&body).unwrap(); + + assert_eq!( + response, + ErrorResponse { + message: format!("Repository #{id} not found",), + } + ) + } + #[tokio::test] + async fn test_get_repository_by_id_exists() { + let id = 1; + let r = routes(RepositoriesDBMock {}).recover(error_handler); + let resp = request() + .path(&format!("/repositories/{id}")) + .reply(&r) + .await; + assert_eq!(resp.status(), 200); + let body = resp.into_body(); + let response: RepositoryResponse = serde_json::from_slice(&body).unwrap(); + assert_eq!( + response, + RepositoryResponse { + id, + name: "repo".to_owned(), + organization_id: 1, + icon: "icon".to_string(), + url: "url".to_string(), + e_tag: "e_tag".to_string(), + } + ) + } + #[tokio::test] + async fn test_get_repository_by_name_not_found() { + let name = "not_found"; + let r = routes(RepositoriesDBMock {}).recover(error_handler); + let resp = request() + .path(&format!("/repositories/name/{name}")) + .reply(&r) + .await; + assert_eq!(resp.status(), 404); + let body = resp.into_body(); + assert!(!body.is_empty()); + + let response: ErrorResponse = serde_json::from_slice(&body).unwrap(); + assert_eq!( + response, + ErrorResponse { + message: format!("Repository {name} not found",), + } + ) + } + #[tokio::test] + async fn test_get_repository_by_name_exists() { + let name = "repo"; + let r = routes(RepositoriesDBMock {}).recover(error_handler); + let resp = request() + .path(&format!("/repositories/name/{name}")) + .reply(&r) + .await; + assert_eq!(resp.status(), 200); + let body = resp.into_body(); + let expected_response = RepositoryResponse { + id: 1, + name: "repo".to_owned(), + organization_id: 1, + icon: "icon".to_string(), + url: "url".to_string(), + e_tag: "e_tag".to_string(), + }; + let response: RepositoryResponse = serde_json::from_slice(&body).unwrap(); + assert_eq!(response, expected_response) + } + #[tokio::test] + async fn test_get_repositories() { + let r = routes(RepositoriesDBMock {}).recover(error_handler); + let resp = request().path(&format!("/repositories")).reply(&r).await; + assert_eq!(resp.status(), 200); + let body = resp.into_body(); + let response: Vec = serde_json::from_slice(&body).unwrap(); + assert_eq!(response, vec![]); + } + #[tokio::test] + async fn test_get_repositories_valid_query_params() { + let r = routes(RepositoriesDBMock {}).recover(error_handler); + let resp = request().path(&format!("/repositories?languages=false&tips=false&maintainers=false&issues=false&limit=1&offset=10&sort_by=id&descending=false")).reply(&r).await; + assert_eq!(resp.status(), 200); + let body = resp.into_body(); + let response: Vec = serde_json::from_slice(&body).unwrap(); + assert_eq!(response, vec![]); + } + #[tokio::test] + async fn test_get_repositories_invalid_query_params() { + let r = routes(RepositoriesDBMock {}).recover(error_handler); + let resp = request() + .path(&format!("/repositories?languages=fal1se")) + .reply(&r) + .await; + assert_eq!(resp.status(), 401); // TODO: check why it's a 401 + let resp = request() + .path(&format!("/repositories?maintainers=fal1se")) + .reply(&r) + .await; + assert_eq!(resp.status(), 401); // TODO: check why it's a 401 + let resp = request() + .path(&format!("/repositories?issues=123")) + .reply(&r) + .await; + assert_eq!(resp.status(), 401); // TODO: check why it's a 401 + let resp = request() + .path(&format!("/repositories?tips=123")) + .reply(&r) + .await; + assert_eq!(resp.status(), 401); + let resp = request() + .path(&format!("/repositories?limit=asd")) + .reply(&r) + .await; + assert_eq!(resp.status(), 401); + let resp = request() + .path(&format!("/repositories?offset=asd")) + .reply(&r) + .await; + assert_eq!(resp.status(), 401); + let resp = request() + .path(&format!("/repositories?sort_by=invalid")) + .reply(&r) + .await; + assert_eq!(resp.status(), 400); + let resp = request() + .path(&format!("/repositories?descending=sadf")) + .reply(&r) + .await; + assert_eq!(resp.status(), 401); // TODO: check why it's a 401 + } + + #[tokio::test] + async fn test_create_repository_ok() { + let id = 1; + let name = "new".to_string(); + let icon = "icon".to_string(); + let e_tag = "e_tag".to_string(); + let url = "url".to_string(); + + let new_repository: Vec = serde_json::to_vec(&NewRepository { + name: name.clone(), + icon: icon.clone(), + organization_id: 1, + url: url.clone(), + e_tag: e_tag.clone(), + }) + .unwrap(); + let r = routes(RepositoriesDBMock {}).recover(error_handler); + let resp = request() + .body(new_repository) + .path(&"/repositories") + .header("Authorization", "Basic dGVzdDp0ZXN0") // test:test + .method("POST") + .reply(&r) + .await; + assert_eq!(resp.status(), 201); + let body = resp.into_body(); + assert!(!body.is_empty()); + + let expected_response = RepositoryResponse { + id, + name, + organization_id: 1, + icon, + url, + e_tag, + }; + let response: RepositoryResponse = serde_json::from_slice(&body).unwrap(); + assert_eq!(response, expected_response) + } + + #[tokio::test] + async fn test_create_repository_already_exists() { + let name = "name".to_string(); + let icon = "icon".to_string(); + let e_tag = "e_tag".to_string(); + let url = "url".to_string(); + + let new_repository: Vec = serde_json::to_vec(&NewRepository { + name: name.clone(), + icon: icon.clone(), + organization_id: 1, + url: url.clone(), + e_tag: e_tag.clone(), + }) + .unwrap(); + let r = routes(RepositoriesDBMock {}).recover(error_handler); + let resp = request() + .body(new_repository) + .path(&"/repositories") + .header("Authorization", "Basic dGVzdDp0ZXN0") // test:test + .method("POST") + .reply(&r) + .await; + assert_eq!(resp.status(), 400); + let body = resp.into_body(); + assert!(!body.is_empty()); + + let response: ErrorResponse = serde_json::from_slice(&body).unwrap(); + assert_eq!( + response, + ErrorResponse { + message: format!("Repository #1 already exists",), + } + ) + } + + #[tokio::test] + async fn test_delete_repository() { + let id = 1; + let r = routes(RepositoriesDBMock {}).recover(error_handler); + let resp = request() + .path(&format!("/repositories/{id}")) + .method("DELETE") + .header("Authorization", "Basic dGVzdDp0ZXN0") // test:test + .reply(&r) + .await; + assert_eq!(resp.status(), 204); + let body = resp.into_body(); + assert!(body.is_empty()); + } + + #[tokio::test] + async fn test_delete_repository_does_not_exist_mock_db() { + let id = 4; + + let r = routes(RepositoriesDBMock {}).recover(error_handler); + let resp = request() + .path(&format!("/repositories/4")) + .method("DELETE") + .header("Authorization", "Basic dGVzdDp0ZXN0") // test:test + .reply(&r) + .await; + + assert_eq!(resp.status(), 404); + let body = resp.into_body(); + assert!(!body.is_empty()); + + let expected_response = ErrorResponse { + message: format!("Repository #{id} not found"), + }; + let response: ErrorResponse = serde_json::from_slice(&body).unwrap(); + assert_eq!(response, expected_response) + } +} diff --git a/src/tests/users.rs b/src/tests/users.rs new file mode 100644 index 0000000..db1761c --- /dev/null +++ b/src/tests/users.rs @@ -0,0 +1,368 @@ +#[cfg(test)] +mod tests { + use crate::{ + errors::{self, ErrorResponse}, + pagination::GetPagination, + repository::{ + db::DBRepository, + models::{NewRepository, RepositoriesRelations, Repository, RepositorySort}, + }, + user::{ + db::DBUser, + models::{NewUser, PatchUser, User, UserResponse, UserSort, UsersRelations}, + routes::routes, + }, + }; + use mobc::async_trait; + use warp::{reject, test::request, Filter}; + + #[derive(Clone)] + pub struct UsersDBMock {} + + #[async_trait] + impl DBUser for UsersDBMock { + async fn get_user( + &self, + id: i32, + _: UsersRelations, + ) -> Result, reject::Rejection> { + if id == 1 { + Ok(Some(User { + id: 1, + username: "username".to_string(), + })) + } else { + Ok(None) + } + } + async fn get_users( + &self, + _: UsersRelations, + _: GetPagination, + _: UserSort, + ) -> Result, reject::Rejection> { + Ok(vec![]) + } + async fn get_user_by_username( + &self, + username: &str, + _: UsersRelations, + ) -> Result, reject::Rejection> { + if username == "username" { + Ok(Some(User { + id: 1, + username: username.to_string(), + })) + } else { + Ok(None) + } + } + async fn create_user(&self, user: NewUser) -> Result { + Ok(User { + id: 1, + username: user.username, + }) + } + async fn delete_user(&self, _: i32) -> Result<(), reject::Rejection> { + Ok(()) + } + async fn update_user_maintainers( + &self, + _: i32, + _: PatchUser, + ) -> Result { + Ok(User { + id: 1, + username: "username".to_string(), + }) + } + } + + #[async_trait] + impl DBRepository for UsersDBMock { + async fn get_repository( + &self, + _: i32, + _: RepositoriesRelations, + ) -> Result, reject::Rejection> { + Ok(None) + } + async fn get_repository_by_name( + &self, + _: &str, + _: RepositoriesRelations, + ) -> Result, reject::Rejection> { + Ok(None) + } + async fn get_repositories( + &self, + _: RepositoriesRelations, + _: GetPagination, + _: RepositorySort, + ) -> Result, reject::Rejection> { + Ok(vec![]) + } + async fn create_repository( + &self, + _: NewRepository, + ) -> Result { + Ok(Repository { + id: 1, + name: "repo".to_owned(), + organization_id: 1, + icon: "icon".to_string(), + url: "url".to_string(), + e_tag: "e_tag".to_string(), + }) + } + async fn delete_repository(&self, _: i32) -> Result<(), reject::Rejection> { + Ok(()) + } + } + #[tokio::test] + async fn test_get_user_by_id_not_found() { + let id = 2; + let r = routes(UsersDBMock {}).recover(errors::error_handler); + let resp = request().path(&format!("/users/{id}")).reply(&r).await; + assert_eq!(resp.status(), 404); + let body = resp.into_body(); + assert!(!body.is_empty()); + + let response: ErrorResponse = serde_json::from_slice(&body).unwrap(); + + assert_eq!( + response, + ErrorResponse { + message: format!("User #{id} not found",), + } + ) + } + #[tokio::test] + async fn test_get_user_by_id_exists() { + let id = 1; + let r = routes(UsersDBMock {}).recover(errors::error_handler); + let resp = request().path(&format!("/users/{id}")).reply(&r).await; + assert_eq!(resp.status(), 200); + let body = resp.into_body(); + let response: UserResponse = serde_json::from_slice(&body).unwrap(); + assert_eq!( + response, + UserResponse { + id, + username: "username".to_string(), + } + ) + } + #[tokio::test] + async fn test_get_user_by_name_not_found() { + let name = "not_found"; + let r = routes(UsersDBMock {}).recover(errors::error_handler); + let resp = request() + .path(&format!("/users/username/{name}")) + .reply(&r) + .await; + assert_eq!(resp.status(), 404); + let body = resp.into_body(); + assert!(!body.is_empty()); + + let response: ErrorResponse = serde_json::from_slice(&body).unwrap(); + assert_eq!( + response, + ErrorResponse { + message: format!("User {name} not found",), + } + ) + } + #[tokio::test] + async fn test_get_user_by_name_exists() { + let name = "username"; + let r = routes(UsersDBMock {}).recover(errors::error_handler); + let resp = request() + .path(&format!("/users/username/{name}")) + .reply(&r) + .await; + assert_eq!(resp.status(), 200); + let body = resp.into_body(); + let expected_response = UserResponse { + id: 1, + username: name.to_string(), + }; + let response: UserResponse = serde_json::from_slice(&body).unwrap(); + assert_eq!(response, expected_response) + } + #[tokio::test] + async fn test_get_users() { + let r = routes(UsersDBMock {}).recover(errors::error_handler); + let resp = request().path(&format!("/users")).reply(&r).await; + assert_eq!(resp.status(), 200); + let body = resp.into_body(); + let response: Vec = serde_json::from_slice(&body).unwrap(); + assert_eq!(response, vec![]); + } + #[tokio::test] + async fn test_get_users_valid_query_params() { + let r = routes(UsersDBMock {}).recover(errors::error_handler); + let resp = request().path(&format!("/users?wishes=false&tips=false&maintainers=false&issues=false&limit=1&offset=10&sort_by=id&descending=false")).reply(&r).await; + assert_eq!(resp.status(), 200); + let body = resp.into_body(); + let response: Vec = serde_json::from_slice(&body).unwrap(); + assert_eq!(response, vec![]); + } + #[tokio::test] + async fn test_get_users_invalid_query_params() { + let r = routes(UsersDBMock {}).recover(errors::error_handler); + let resp = request() + .path(&format!("/users?wishes=fal1se")) + .reply(&r) + .await; + assert_eq!(resp.status(), 401); // TODO: check why it's a 401 + let resp = request() + .path(&format!("/users?maintainers=fal1se")) + .reply(&r) + .await; + assert_eq!(resp.status(), 401); // TODO: check why it's a 401 + let resp = request() + .path(&format!("/users?issues=123")) + .reply(&r) + .await; + assert_eq!(resp.status(), 401); // TODO: check why it's a 401 + let resp = request().path(&format!("/users?tips=123")).reply(&r).await; + assert_eq!(resp.status(), 401); + let resp = request().path(&format!("/users?limit=asd")).reply(&r).await; + assert_eq!(resp.status(), 401); + let resp = request() + .path(&format!("/users?offset=asd")) + .reply(&r) + .await; + assert_eq!(resp.status(), 401); + let resp = request() + .path(&format!("/users?sort_by=invalid")) + .reply(&r) + .await; + assert_eq!(resp.status(), 400); + let resp = request() + .path(&format!("/users?descending=sadf")) + .reply(&r) + .await; + assert_eq!(resp.status(), 401); + } + + #[tokio::test] + async fn test_create_user_ok() { + let id = 1; + let username = "new".to_string(); + let new_user: Vec = serde_json::to_vec(&NewUser { + username: username.clone(), + repositories: None, + }) + .unwrap(); + let r = routes(UsersDBMock {}).recover(errors::error_handler); + let resp = request() + .body(new_user) + .path(&"/users") + .header("Authorization", "Basic dGVzdDp0ZXN0") // test:test + .method("POST") + .reply(&r) + .await; + assert_eq!(resp.status(), 201); + let body = resp.into_body(); + assert!(!body.is_empty()); + + let expected_response = UserResponse { id, username }; + let response: UserResponse = serde_json::from_slice(&body).unwrap(); + assert_eq!(response, expected_response) + } + + #[tokio::test] + async fn test_patch_user_ok() { + let new_user: Vec = serde_json::to_vec(&PatchUser { + repositories: vec![1], + }) + .unwrap(); + let r = routes(UsersDBMock {}).recover(errors::error_handler); + let resp = request() + .body(new_user) + .path(&"/users/1/maintainers") + .header("Authorization", "Basic dGVzdDp0ZXN0") // test:test + .method("PATCH") + .reply(&r) + .await; + assert_eq!(resp.status(), 422); + let body = resp.into_body(); + assert!(!body.is_empty()); + + let response: ErrorResponse = serde_json::from_slice(&body).unwrap(); + assert_eq!( + response, + ErrorResponse { + message: format!("User #1 cannot be updated: repository 1 does not exist",), + } + ) + } + #[tokio::test] + async fn test_create_user_already_exists() { + let username = "username".to_string(); + let new_user: Vec = serde_json::to_vec(&NewUser { + username: username.clone(), + repositories: None, + }) + .unwrap(); + let r = routes(UsersDBMock {}).recover(errors::error_handler); + let resp = request() + .body(new_user) + .path(&"/users") + .header("Authorization", "Basic dGVzdDp0ZXN0") // test:test + .method("POST") + .reply(&r) + .await; + assert_eq!(resp.status(), 400); + let body = resp.into_body(); + assert!(!body.is_empty()); + + let response: ErrorResponse = serde_json::from_slice(&body).unwrap(); + assert_eq!( + response, + ErrorResponse { + message: format!("User #1 already exists",), + } + ) + } + + #[tokio::test] + async fn test_delete_user() { + let id = 1; + let r = routes(UsersDBMock {}).recover(errors::error_handler); + let resp = request() + .path(&format!("/users/{id}")) + .method("DELETE") + .header("Authorization", "Basic dGVzdDp0ZXN0") // test:test + .reply(&r) + .await; + assert_eq!(resp.status(), 204); + let body = resp.into_body(); + assert!(body.is_empty()); + } + + #[tokio::test] + async fn test_delete_user_does_not_exist_mock_db() { + let id = 4; + + let r = routes(UsersDBMock {}).recover(errors::error_handler); + let resp = request() + .path(&format!("/users/4")) + .method("DELETE") + .header("Authorization", "Basic dGVzdDp0ZXN0") // test:test + .reply(&r) + .await; + + assert_eq!(resp.status(), 404); + let body = resp.into_body(); + assert!(!body.is_empty()); + + let expected_response = ErrorResponse { + message: format!("User #{id} not found"), + }; + let response: ErrorResponse = serde_json::from_slice(&body).unwrap(); + assert_eq!(response, expected_response) + } +} diff --git a/src/tests/utils.rs b/src/tests/utils.rs new file mode 100644 index 0000000..6e41f91 --- /dev/null +++ b/src/tests/utils.rs @@ -0,0 +1,35 @@ +use std::env; + +use crate::{ + db::pool::{DBAccess, DBAccessor}, + utils::setup_db, +}; +use diesel::RunQueryDsl; +use diesel_migrations::MigrationHarness; +use diesel_migrations::{embed_migrations, EmbeddedMigrations}; +use rand::{distributions::Alphanumeric, thread_rng, Rng}; + +pub fn generate_random_database_name() -> String { + let rng = thread_rng(); + let random_string: String = rng + .sample_iter(&Alphanumeric) + .map(char::from) + .take(10) + .collect(); + format!("test_db_{}", random_string) +} + +pub async fn generate_test_database() -> DBAccess { + const MIGRATIONS: EmbeddedMigrations = embed_migrations!(); + let database_url = env::var("DATABASE_URL").expect("missing DATABASE"); + let database_name = generate_random_database_name(); + let db = setup_db(&database_url).await; + let conn = &mut db.get_db_conn(); + diesel::sql_query(format!("CREATE DATABASE {}", database_name)) + .execute(conn) + .expect("Failed to create database"); + db.get_db_conn() + .run_pending_migrations(MIGRATIONS) + .expect("Could not run migrations"); + db +} diff --git a/src/types.rs b/src/types.rs index 6601f27..81a0652 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,4 +1,5 @@ use dotenv::dotenv; +use serde_derive::{Deserialize, Serialize}; use std::env; /// Configuration used by this API. @@ -10,23 +11,42 @@ pub struct ApiConfig { pub http_server_port: u16, /// Database URL. pub database_url: String, - /// Database init file. - pub database_init_file: String, } impl ApiConfig { pub fn new() -> Self { dotenv().ok(); Self { - http_server_host: env::var("HTTP_SERVER_HOST") - .unwrap_or_else(|_| "127.0.0.1".to_owned()), - http_server_port: env::var("HTTP_SERVER_PORT") + http_server_host: env::var("HOST").unwrap_or_else(|_| "127.0.0.1".to_owned()), + http_server_port: env::var("PORT") .unwrap_or_else(|_| "8000".to_owned()) .parse() - .expect("Invalid HTTP_SERVER_PORT"), + .expect("Invalid PORT"), database_url: env::var("DATABASE_URL").expect("DATABASE_URL must be set"), - database_init_file: env::var("DATABASE_INIT_FILE") - .expect("DATABASE_INIT_FILE must be set"), } } } + +#[derive(Deserialize, Debug, Clone)] +pub struct PaginationParams { + #[serde(default = "default_limit")] + pub limit: i64, + #[serde(default = "default_offset")] + pub offset: i64, +} + +fn default_limit() -> i64 { + 100 // Default limit +} + +fn default_offset() -> i64 { + 0 // Default offset +} + +#[derive(Serialize)] +pub struct PaginatedResponse { + pub total_count: Option, + pub has_next_page: bool, + pub has_previous_page: bool, + pub data: Vec, +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..2d49c14 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,53 @@ +use crate::{ + api::{health, issues, projects, repositories, users}, + db::{ + self, + errors::DBError, + pool::{DBAccess, DBAccessor}, + }, + errors::error_handler, +}; +use ::warp::Reply; +use warp::{filters::BoxedFilter, Filter}; + +pub async fn setup_db(url: &str) -> DBAccess { + let db_pool = db::pool::create_db_pool(url) + .map_err(DBError::DBPoolConnection) + .expect("Failed to create DB pool"); + DBAccess::new(db_pool) +} + +pub fn setup_filters(db: DBAccess) -> BoxedFilter<(impl Reply,)> { + let health_route = health::routes::routes(db.clone()); + let projects_route = projects::routes::routes(db.clone()); + let repositories_route = repositories::routes::routes(db.clone()); + let issues_route = issues::routes::routes(db.clone()); + let users_route = users::routes::routes(db.clone()); + + health_route + .or(projects_route) + .or(repositories_route) + .or(issues_route) + .or(users_route) + .with( + warp::cors() + .allow_any_origin() + .allow_header("Authorization"), + ) //TODO: restrict url + .recover(error_handler) + .boxed() +} + +pub fn parse_ids(s: &str) -> Vec { + let mut ids = Vec::new(); + for token in s.split_whitespace() { + if let Ok(id) = token.parse::() { + ids.push(id); + } + } + ids +} + +pub fn parse_comma_values(s: &str) -> Vec { + s.split(',').map(|el: &str| el.to_string()).collect() +}