From d6d29b656800d90942b0dd37c8e9ce1165a04007 Mon Sep 17 00:00:00 2001 From: Lucien Cartier-Tilet Date: Fri, 9 Aug 2024 09:05:42 +0200 Subject: [PATCH] initial commit --- .cargo/audit.toml | 18 + .dir-locals.el | 15 + .env.example | 6 + .envrc | 2 + .gitea/ISSUE_TEMPLATE/BUG-REPORT.yml | 62 ++ .gitea/ISSUE_TEMPLATE/FEATURE-REQUEST.yml | 33 ++ .gitea/template | 10 + .gitea/workflows/ci.yaml | 71 +++ .gitea/workflows/publish.yaml | 33 ++ .gitignore | 5 + .tarpaulin.ci.toml | 5 + .tarpaulin.local.toml | 6 + CODE_OF_CONDUCT.md | 127 +++++ CONTRIBUTING.md | 314 ++++++++++ Cargo.toml | 61 ++ LICENSE.md | 660 ++++++++++++++++++++++ README.md | 186 ++++++ bacon.toml | 84 +++ docker/compose.dev.yml | 55 ++ flake.nix | 61 ++ justfile | 72 +++ rust-toolchain.toml | 4 + settings/base.yaml | 18 + settings/development.yaml | 7 + settings/production.yaml | 7 + src/lib.rs | 63 +++ src/main.rs | 5 + src/route/health.rs | 29 + src/route/mod.rs | 18 + src/route/version.rs | 46 ++ src/settings.rs | 246 ++++++++ src/startup.rs | 142 +++++ src/telemetry.rs | 28 + 33 files changed, 2499 insertions(+) create mode 100644 .cargo/audit.toml create mode 100644 .dir-locals.el create mode 100644 .env.example create mode 100644 .envrc create mode 100644 .gitea/ISSUE_TEMPLATE/BUG-REPORT.yml create mode 100644 .gitea/ISSUE_TEMPLATE/FEATURE-REQUEST.yml create mode 100644 .gitea/template create mode 100644 .gitea/workflows/ci.yaml create mode 100644 .gitea/workflows/publish.yaml create mode 100644 .gitignore create mode 100644 .tarpaulin.ci.toml create mode 100644 .tarpaulin.local.toml create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 Cargo.toml create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 bacon.toml create mode 100644 docker/compose.dev.yml create mode 100644 flake.nix create mode 100644 justfile create mode 100644 rust-toolchain.toml create mode 100644 settings/base.yaml create mode 100644 settings/development.yaml create mode 100644 settings/production.yaml create mode 100644 src/lib.rs create mode 100644 src/main.rs create mode 100644 src/route/health.rs create mode 100644 src/route/mod.rs create mode 100644 src/route/version.rs create mode 100644 src/settings.rs create mode 100644 src/startup.rs create mode 100644 src/telemetry.rs diff --git a/.cargo/audit.toml b/.cargo/audit.toml new file mode 100644 index 0000000..b1f8e11 --- /dev/null +++ b/.cargo/audit.toml @@ -0,0 +1,18 @@ +[advisories] +ignore = ["RUSTSEC-2023-0071"] +informational_warnings = ["unmaintained"] +severity_threshold = "low" + +[output] +deny = [] +format = "terminal" +quiet = false +show_tree = true + +[target] +arch = "x86_64" +os = "linux" + +[yanked] +enabled = true +update_index = true \ No newline at end of file diff --git a/.dir-locals.el b/.dir-locals.el new file mode 100644 index 0000000..0181ae0 --- /dev/null +++ b/.dir-locals.el @@ -0,0 +1,15 @@ +;;; Directory Local Variables -*- no-byte-compile: t -*- +;;; For more information see (info "(emacs) Directory Variables") + +((sql-mode + . + ((eval . (progn + (setq-local lsp-sqls-connections + `(((driver . "postgresql") + (dataSourceName . + ,(format "host=%s port=%s user=%s password=%s dbname=%s sslmode=disable" + (getenv "DB_HOST") + (getenv "DB_PORT") + (getenv "DB_USER") + (getenv "DB_PASSWORD") + (getenv "DB_NAME"))))))))))) diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..9d9f328 --- /dev/null +++ b/.env.example @@ -0,0 +1,6 @@ +DB_HOST=localhost +DB_PORT=5432 +DB_NAME=${REPO_NAME_KEBAB} +DB_USER=dev +DB_PASSWORD=password +DATABASE_URL=postgresql://$${DB_USER}:$${DB_PASSWORD}@$${DB_HOST}:$${DB_PORT}/$${DB_NAME} diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..437a1ae --- /dev/null +++ b/.envrc @@ -0,0 +1,2 @@ +use flake +dotenv \ No newline at end of file diff --git a/.gitea/ISSUE_TEMPLATE/BUG-REPORT.yml b/.gitea/ISSUE_TEMPLATE/BUG-REPORT.yml new file mode 100644 index 0000000..156bd20 --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/BUG-REPORT.yml @@ -0,0 +1,62 @@ +name: Bug Report +description: File a bug report +title: "[Bug]: " +labels: ["bug/unconfirmed"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + - type: textarea + id: expected-behaviour + attributes: + label: Expected behaviour + description: How do you expect ${REPO_NAME} to behave? + value: "Something should happen" + validations: + required: true + - type: textarea + id: what-happened + attributes: + label: Actual behaviour + description: How does the actual behaviour differ from the expected behaviour? + value: "Something else happened" + validations: + required: true + - type: dropdown + id: package-version + attributes: + label: ${REPO_NAME} version + description: What version of ${REPO_NAME} are you using? + options: + - main + - develop + - something else (please specify) + - type: dropdown + id: source + attributes: + label: Source of backend + description: From which source did you get the backend? + options: + - Compiled yourself (Nix development shell) + - Compiled yourself (non-Nix development shell) + - Release binary + - Docker image + - something else (please specify) + - type: textarea + id: rust-version + attributes: + label: Rust version + description: If you compiled the binary yourself, which version of Rust did you use? + value: "Rust 1.y.z" + - type: textarea + id: logs + attributes: + label: Relevant code or log output + description: Please copy and pase any relevant code or log output. This will be automatically formatted into code, so no need for backticks + render: text + - type: textarea + id: other-info + attributes: + label: Other relevant information + description: Please provide any other information which could be relevant to the issue (PostgreSQL version? Upstream bug? diff --git a/.gitea/ISSUE_TEMPLATE/FEATURE-REQUEST.yml b/.gitea/ISSUE_TEMPLATE/FEATURE-REQUEST.yml new file mode 100644 index 0000000..0e56b61 --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/FEATURE-REQUEST.yml @@ -0,0 +1,33 @@ +name: Feature Request +description: Request a new feature +title: "[Feature Request]: " +labels: ["enhancement"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to request a new feature! + - type: textarea + id: feature-description + attributes: + label: New feature + description: Description of the new feature + value: "New API endpoint should do thing" + validations: + required: true + - type: textarea + id: feature-reason + attributes: + label: Why this new feature + description: Describe why this new feature should be added to ${REPO_NAME} + value: "New API endpoint would simplify doing thing often done by people using ${REPO_NAME}" + validations: + required: true + - type: textarea + id: ideas-implementation + attributes: + label: Implementation ideas and additional thoughts + description: Do you have an idea on how to implement it? + value: "It could be implemented doing foo, bar, and baz" + validations: + required: false diff --git a/.gitea/template b/.gitea/template new file mode 100644 index 0000000..899edfd --- /dev/null +++ b/.gitea/template @@ -0,0 +1,10 @@ +*.md +LICENSE +Cargo.toml +flake.nix +justfile +.env.example +docker/*.yml +settings/*.yaml +**/*.rs +.gitea/**/*.yml diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml new file mode 100644 index 0000000..8ceb586 --- /dev/null +++ b/.gitea/workflows/ci.yaml @@ -0,0 +1,71 @@ +name: CI +on: + pull_request: + push: +env: + DATABASE_URL: ${{ vars.DATABASE_URL }} + +concurrency: + group: ${{ gitea.workflow }}-${{ gitea.ref }} + cancel-in-progress: ${{ gitea.ref != 'ref/heads/master' }} + +jobs: + tests: + runs-on: ubuntu-latest + container: + image: catthehacker/ubuntu:js-latest + options: --security-opt seccomp=unconfined + permissions: + pull-requests: write + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_PASSWORD: ${{ vars.DB_PASSWORD }} + POSTGRES_USER: ${{ vars.DB_USER }} + POSTGRES_DB: ${{ vars.DB_NAME }} + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 10s + --health-retries 5 + ports: + - 5432:5432 + steps: + - uses: actions/checkout@v4 + - name: Install Nix + uses: cachix/install-nix-action@v27 + with: + nix_path: nixpkgs=channel:nixos-unstable + - name: Migrate database + run: nix develop --command -- just migrate + - name: Formatting check + run: nix develop --command -- just format-check + - name: Lint + run: nix develop --command -- just lint + - name: Audit + run: nix develop --command -- just audit + - name: Minimum supported Rust version check + run: nix develop --command -- just msrv + - name: Tests + run: nix develop --command -- just test + - name: Coverage + run: nix develop --command -- just coverage-ci + - name: Code Coverage Report + uses: irongut/CodeCoverageSummary@v1.3.0 + with: + filename: coverage/cobertura.xml + badge: true + fail_below_min: true + format: markdown + hide_branch_rate: false + hide_complexity: false + indicators: true + output: both + thresholds: '60 80' + - name: Add Coverage PR Comment + uses: mshick/add-pr-comment@v2 + if: gitea.event_name == 'pull_request' + with: + recreate: true + message-path: code-coverage-results.md diff --git a/.gitea/workflows/publish.yaml b/.gitea/workflows/publish.yaml new file mode 100644 index 0000000..79b1afa --- /dev/null +++ b/.gitea/workflows/publish.yaml @@ -0,0 +1,33 @@ +name: Publish Docker image +on: + push: + branches: + - 'main' + - 'develop' + tags: + - 'v*' + pull_request: + branches: + - 'main' + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Log in to Docker registry + uses: docker/login-action@v3.3.0 + with: + username: ${{ secrets.DOCKER_REGISTRY_USERNAME }} + password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }} + - uses: cachix/install-nix-action@v27 + with: + nix_path: nixpkgs=channel:nixos-unstable + - name: Build Docker image + run: nix develop --command -- just docker-build + - name: Load Docker image + run: docker load < result + - name: Docker Metadata action + uses: docker/metadata-action@v5.5.1 + with: + image: tal-backend:latest diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cd67dd7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/target +.direnv/ +.env +/result +/coverage/ diff --git a/.tarpaulin.ci.toml b/.tarpaulin.ci.toml new file mode 100644 index 0000000..0ebe9ee --- /dev/null +++ b/.tarpaulin.ci.toml @@ -0,0 +1,5 @@ +[all] +out = ["Xml"] +target-dir = "coverage" +output-dir = "coverage" +fail-under = 60 diff --git a/.tarpaulin.local.toml b/.tarpaulin.local.toml new file mode 100644 index 0000000..2bf0d3c --- /dev/null +++ b/.tarpaulin.local.toml @@ -0,0 +1,6 @@ +[all] +out = ["Html", "Lcov"] +skip-clean = true +target-dir = "coverage" +output-dir = "coverage" +fail-under = 60 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..e491cf5 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,127 @@ +# Code of Conduct - ${REPO_NAME} + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to make participation in our +project and our community a harassment-free experience for everyone, +regardless of age, body size, disability, ethnicity, sex +characteristics, gender identity and expression, level of experience, +education, socio-economic status, nationality, personal appearance, +race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to a positive environment for +our community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our + mistakes, and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances +* Trolling, insulting or derogatory comments, and personal or + political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in + a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying and enforcing our +standards of acceptable behavior and will take appropriate and fair +corrective action in response to any behavior that they deem +inappropriate, threatening, offensive, or harmful. + +Project maintainers have the right and responsibility to remove, edit, +or reject comments, commits, code, wiki edits, issues, and other +contributions that are not aligned to this Code of Conduct, and will +communicate reasons for moderation decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also +applies when an individual is officially representing the community in +public spaces. Examples of representing our community include using an +official e-mail address, posting via an official social media account, +or acting as an appointed representative at an online or offline +event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior +may be reported to the community leaders responsible for enforcement +at <${REPO_OWNER}>. All complaints will be reviewed and investigated +promptly and fairly. + +All community leaders are obligated to respect the privacy and +security of the reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in +determining the consequences for any action they deem in violation of +this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior +deemed unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, +providing clarity around the nature of the violation and an +explanation of why the behavior was inappropriate. A public apology +may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. +No interaction with the people involved, including unsolicited +interaction with those enforcing the Code of Conduct, for a specified +period of time. This includes avoiding interactions in community +spaces as well as external channels like social media. Violating these +terms may lead to a temporary or permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, +including sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or +public communication with the community for a specified period of +time. No public or private interaction with the people involved, +including unsolicited interaction with those enforcing the Code of +Conduct, is allowed during this period. Violating these terms may lead +to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of +community standards, including sustained inappropriate behavior, +harassment of an individual, or aggression toward or disparagement of +classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction +within the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor +Covenant](https://contributor-covenant.org/), version +[1.4](https://www.contributor-covenant.org/version/1/4/code-of-conduct/code_of_conduct.md) +and +[2.0](https://www.contributor-covenant.org/version/2/0/code_of_conduct/code_of_conduct.md), +and was generated by +[contributing-gen](https://github.com/bttger/contributing-gen). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..5002705 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,314 @@ + +# Contributing to ${REPO_NAME} + +First off, thanks for taking the time to contribute! ❤️ + +All types of contributions are encouraged and valued. See the [Table +of Contents](#table-of-contents) for different ways to help and +details about how this project handles them. Please make sure to read +the relevant section before making your contribution. It will make it +a lot easier for us maintainers and smooth out the experience for all +involved. The community looks forward to your contributions. 🎉 + +> And if you like the project, but just don't have time to contribute, +> that's fine. There are other easy ways to support the project and +> show your appreciation, which we would also be very happy about: +> - Star the project +> - Tweet about it +> - Refer this project in your project's readme +> - Mention the project at local meetups and tell your +> friends/colleagues + + +## Table of Contents + +- [Code of Conduct](#code-of-conduct) +- [I Have a Question](#i-have-a-question) +- [I Want To Contribute](#i-want-to-contribute) + - [Reporting Bugs](#reporting-bugs) + - [Suggesting Enhancements](#suggesting-enhancements) + - [Your First Code Contribution](#your-first-code-contribution) + - [Improving The Documentation](#improving-the-documentation) +- [Styleguides](#styleguides) + - [Commit Messages](#commit-messages) +- [Join The Project Team](#join-the-project-team) + + +## Code of Conduct + +This project and everyone participating in it is governed by the +[${REPO_NAME} Code of Conduct](CODE_OF_CONDUCT.md). By participating, +you are expected to uphold this code. Please report unacceptable +behavior to <${REPO_OWNER}>. + + +## I Have a Question + +> If you want to ask a question, we assume that you have read the +> available [Documentation](${REPO_LINK}/wiki). + +Before you ask a question, it is best to search for existing +[Issues](${REPO_LINK}/issues) that might help you. In case you have +found a suitable issue and still need clarification, you can write +your question in this issue. It is also advisable to search the +internet for answers first. + +If you then still feel the need to ask a question and need +clarification, we recommend the following: + +- Open an [Issue](${REPO_LINK}/issues/new). +- Provide as much context as you can about what you're running into. +- Provide project and platform versions (cargo, rustc, etc), depending + on what seems relevant. + +We will then take care of the issue as soon as possible. + +## I Want To Contribute + +> ### Legal Notice +> When contributing to this project, you must agree that you have +> authored 100% of the content, that you have the necessary rights to +> the content and that the content you contribute may be provided +> under the project license. + +### Reporting Bugs + + +#### Before Submitting a Bug Report + +A good bug report shouldn't leave others needing to chase you up for +more information. Therefore, we ask you to investigate carefully, +collect information and describe the issue in detail in your report. +Please complete the following steps in advance to help us fix any +potential bug as fast as possible. + +- Make sure that you are using the latest version. +- Determine if your bug is really a bug and not an error on your side + e.g. using incompatible environment components/versions (Make sure + that you have read the [documentation](${REPO_LINK}/wiki). If you + are looking for support, you might want to check [this + section](#i-have-a-question)). +- To see if other users have experienced (and potentially already + solved) the same issue you are having, check if there is not already + a bug report existing for your bug or error in the [bug + tracker](${REPO_LINK}issues?q=label%3Abug). +- Also make sure to search the internet (including Stack Overflow) to + see if users outside of the PhundrakLabs community have discussed + the issue. +- Collect information about the bug: + - Stack trace (Traceback) + - OS, Platform and Version (Windows, Linux, macOS, x86, ARM) + - Version of the interpreter, compiler, SDK, runtime environment, + package manager, depending on what seems relevant. + - Possibly your input and the output + - Can you reliably reproduce the issue? And can you also reproduce + it with older versions? + + +#### How Do I Submit a Good Bug Report? + +> You must never report security related issues, vulnerabilities or +> bugs including sensitive information to the issue tracker, or +> elsewhere in public. Instead sensitive bugs must be sent by email to +> <${REPO_OWNER}>. + + +We use PhundrakLabs issues to track bugs and errors. If you run into +an issue with the project: + +- Open an [Issue](${REPO_LINK}/issues/new). (Since we can't be sure at + this point whether it is a bug or not, we ask you not to talk about + a bug yet and not to label the issue.) +- Explain the behavior you would expect and the actual behavior. +- Please provide as much context as possible and describe the + *reproduction steps* that someone else can follow to recreate the + issue on their own. This usually includes your code. For good bug + reports you should isolate the problem and create a reduced test + case. +- Provide the information you collected in the previous section. + +Once it's filed: + +- The project team will label the issue accordingly. +- A team member will try to reproduce the issue with your provided + steps. If there are no reproduction steps or no obvious way to + reproduce the issue, the team will ask you for those steps and mark + the issue as `Status/Need More Info`. Bugs with the `Status/Need + More Info` tag will not be addressed until they are reproduced. +- If the team is able to reproduce the issue, it will be marked + `Reviewed/Confirmed`, as well as possibly other tags (such as + `Priority/Medium`), and the issue will be left to be [implemented by + someone](#your-first-code-contribution). + + + + +### Suggesting Enhancements + +This section guides you through submitting an enhancement suggestion for ${REPO_NAME}, **including completely new features and minor improvements to existing functionality**. Following these guidelines will help maintainers and the community to understand your suggestion and find related suggestions. + + +#### Before Submitting an Enhancement + +- Make sure that you are using the latest version. +- Read the [documentation](${REPO_LINK}/wiki) carefully and find out + if the functionality is already covered, maybe by an individual + configuration. +- Perform a [search](${REPO_LINK}/issues) to see if the enhancement + has already been suggested. If it has, add a comment to the existing + issue instead of opening a new one. +- Find out whether your idea fits with the scope and aims of the + project. It's up to you to make a strong case to convince the + project's developers of the merits of this feature. Keep in mind + that we want features that will be useful to the majority of our + users and not just a small subset. If you're just targeting a + minority of users, consider writing an add-on/plugin library. + + +#### How Do I Submit a Good Enhancement Suggestion? + +Enhancement suggestions are tracked as [Gitea +issues](${REPO_LINK}/issues). + +- Use a **clear and descriptive title** for the issue to identify the + suggestion. +- Provide a **step-by-step description of the suggested enhancement** + in as many details as possible. +- **Describe the current behavior** and **explain which behavior you + expected to see instead** and why. At this point you can also tell + which alternatives do not work for you. +- **Explain why this enhancement would be useful** to most + ${REPO_NAME} users. You may also want to point out the other + projects that solved it better and which could serve as inspiration. + + + +### Your First Code Contribution +#### Setting Up Your Development Environment +Code contributions are most welcome! To contribute to the project, you +will need to the README and install the +[prerequisites](${REPO_LINK}#prerequisites) and [setup your +development environment](${REPO_LINK}#installing). + +You can use the IDE of your choice, popular options for Rust projects +are [VSCode](https://code.visualstudio.com/) or +[RustRover](https://www.jetbrains.com/rust/), but plenty of other code +editors are available such as: +- Emacs (we recommend [rustic](https://github.com/rustic-rs/rustic) + over [rust-mode](https://github.com/rust-lang/rust-mode) +- [Vim/NeoVim](https://github.com/rust-lang/rust.vim) +- [Sublime Text](https://github.com/rust-lang/rust-enhanced) +- [Helix](https://rust-analyzer.github.io/manual.html#helix) +- [Visual Studio](https://rust-analyzer.github.io/manual.html#visual-studio-2022) +- [Eclipse](https://projects.eclipse.org/projects/tools.corrosion) +- And plenty other text editors! + +Depending on your choice, you may need to install an LSP server and an +LSP client on your text editor, such as with Emacs and Vim/NeoVim. + +#### Where Should You Start? +If you want to participate to ${REPO_NAME}, but you’re not sure what +to do, take a look at the [opened issues](${REPO_LINK}/issues). You +way find issues with the `help wanted` tag where you could weigh in +for the resolution of the issue or for decision-making. You may also +find issues tagged as `good first issue` which should be relatively +approachable for first time contributors. + +#### Writing Your First Code Contribution +Take your time when reading the code. The existing documentation can +help you better understand how the project is built and how the code +behaves. If you still have some questions, don’t hesitate to reach out +to maintainers. + +When you start writing your code, only modify what needs to be +modified. Each contribution should do one thing and one thing only. Do +not, for instance, refactor some code that is unrelated to the main +topic of your contribution. + +Check often the output of clippy by running `just lint`, and check if +existing tests still pass with `just test`. Ideally, start by writing +new tests that describe the intended behaviour of your contribution +with functions that will purposefully fail these tests, then iterate +over these functions until they finally pass all tests. + +Check also that your code is properly formatted with +`just format-check`. You can format it automatically with +`just format`. + +Finally, check if the code coverage of ${REPO_NAME}. Ideally, try to +stay within the initial percentage of code coverage of the project, +and try to stay above 75% of code coverage. If it drops below 60%, +your contribution will be rejected automatically until you add more +test covering more code. + +For writing tests, don’t hesitate to take a look at existing tests. +You can also read on how to write tests with SQLx [in their +documentation](https://docs.rs/sqlx/latest/sqlx/attr.test.html), as +well as some examples of poem tests in the [documentation of its +`test` module](https://docs.rs/poem/latest/poem/test/index.html). + +### Improving the Documentation +To improve the documentation of ${REPO_NAME}, you have two choices: +- Improve the [wiki](${REPO_LINK}/wiki) of the project with + high-level, functional documentation +- Improve the code documentation by adding some + [rustdoc](https://doc.rust-lang.org/rustdoc/how-to-write-documentation.html) + within the code. You can also take the opportunity to add new tests + through code examples in the rustdock; who knows, maybe you will + discover a bug writing these tests, which will help improve the code + itself! + +## New Pull Requests +### Commit Messages +When creating a new commit, try to follow as closely as possible the +[Conventional Commits 1.0.0](https://www.conventionalcommits.org/) +standard. Each line should not exceed 72 characters in length. Commits +shall also be written in the present tense. Use the imperative mood as +much as possible when explaining what this commit does. + +> Instead of *Fixed #42* or *Fixes #42*, write *Fix #42* + +**DO NOT** increase the project version yourself. This will be up for +the maintainers to do so. + +### Creating the Pull Request +Submit your pull requests to the `develop` branch. Pull requests to +other branches will be refused, unless there is a very specific reason +to do so explained in the pull request. + +Note: *PR* means *Pull Request*. + +**All PRs** must: +- Branch from `develop` +- Target the `develop` branch, unless specific cases. Maintainers are + the only contributors that can create a PR targeting `main` +- Live on their own branch, prefixed by `feature/` or `fix/` (other + prefixes can be accepted in specific cases) with the name of the + feature or the issue fixed in `kebab-case` +- Be rebased on `develop` if the PR is no longer up to date +- Pass the CI pipeline (a failed CI pipeline will prevent any merge) + +PRs coming from a `main`, `master`, `develop`, `release/`, `hotfix/`, +or `support/` branch will be rejected. PRs not up to date with +`develop` will not be merged. + +**Simple PRs** shall: +- Have only one topic +- Have only one commit +- Have all their commits squashed into one if it contains several commits + +If you open a PR whose scope are multiple topics, it will be rejected. +Open as many PRs as necessary, one for each topic. + +**Complex PRs** shall: +- squash uninteresting commits (fixes to earlier commits, typos, + syntax, etc…) together +- keep the major steps into individual commits + + +## Attribution +This guide is based on +[**contributing-gen**](https://github.com/bttger/contributing-gen). +The Pull Request part is heavily based on the corresponding part of +Spacemacs’ +[CONTRIBUTING.md](https://github.com/syl20bnr/spacemacs/blob/develop/CONTRIBUTING.org#pull-request). diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..7f292ee --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,61 @@ +[package] +name = "${REPO_NAME_KEBAB}" +version = "0.1.0" +edition = "2021" +publish = false +authors = ["${REPO_OWNER}"] +rust-version = "1.78" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +path = "src/lib.rs" + +[[bin]] +path = "src/main.rs" +name = "${REPO_NAME_KEBAB}" + +[dependencies] +chrono = { version = "0.4.38", features = ["serde"] } +config = { version = "0.14.0", features = ["yaml"] } +dotenvy = "0.15.7" +serde = "1.0.204" +serde_json = "1.0.120" +thiserror = "1.0.63" +tokio = { version = "1.39.2", features = ["macros", "rt-multi-thread"] } +tracing = "0.1.40" +tracing-subscriber = { version = "0.3.18", features = ["fmt", "std", "env-filter", "registry", "json", "tracing-log"] } +uuid = { version = "1.10.0", features = ["v4", "serde"] } + +[dependencies.lettre] +version = "0.11.7" +default-features = false +features = [ + "builder", + "hostname", + "pool", + "rustls-tls", + "tokio1", + "tokio1-rustls-tls", + "smtp-transport" +] + + +[dependencies.poem] +version = "3.0.4" +default-features = false +features = [ + "csrf", + "rustls", + "cookie", + "test" +] + +[dependencies.poem-openapi] +version = "5.0.3" +features = ["chrono", "swagger-ui", "uuid"] + +[dependencies.sqlx] +version = "0.8.0" +default-features = false +features = ["postgres", "uuid", "chrono", "migrate", "runtime-tokio", "macros"] diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..c6f01c6 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,660 @@ +# GNU AFFERO GENERAL PUBLIC LICENSE + +Version 3, 19 November 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. + + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + +## Preamble + +The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + +The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains +free software for all its users. + +When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + +Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + +A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + +The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + +An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing +under this license. + +The precise terms and conditions for copying, distribution and +modification follow. + +## TERMS AND CONDITIONS + +### 0. Definitions. + +"This License" refers to version 3 of the GNU Affero General Public +License. + +"Copyright" also means copyright-like laws that apply to other kinds +of works, such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + +To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of +an exact copy. The resulting work is called a "modified version" of +the earlier work or a work "based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based +on the Program. + +To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + +To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user +through a computer network, with no transfer of a copy, is not +conveying. + +An interactive user interface displays "Appropriate Legal Notices" to +the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + +### 1. Source Code. + +The "source code" for a work means the preferred form of the work for +making modifications to it. "Object code" means any non-source form of +a work. + +A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + +The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + +The Corresponding Source need not include anything that users can +regenerate automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same +work. + +### 2. Basic Permissions. + +All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not convey, +without conditions so long as your license otherwise remains in force. +You may convey covered works to others for the sole purpose of having +them make modifications exclusively for you, or provide you with +facilities for running those works, provided that you comply with the +terms of this License in conveying all material for which you do not +control copyright. Those thus making or running the covered works for +you must do so exclusively on your behalf, under your direction and +control, on terms that prohibit them from making any copies of your +copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the +conditions stated below. Sublicensing is not allowed; section 10 makes +it unnecessary. + +### 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + +No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + +When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such +circumvention is effected by exercising rights under this License with +respect to the covered work, and you disclaim any intention to limit +operation or modification of the work as a means of enforcing, against +the work's users, your or third parties' legal rights to forbid +circumvention of technological measures. + +### 4. Conveying Verbatim Copies. + +You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + +### 5. Conveying Modified Source Versions. + +You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these +conditions: + +- a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. +- b) The work must carry prominent notices stating that it is + released under this License and any conditions added under + section 7. This requirement modifies the requirement in section 4 + to "keep intact all notices". +- c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. +- d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + +A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + +### 6. Conveying Non-Source Forms. + +You may convey a covered work in object code form under the terms of +sections 4 and 5, provided that you also convey the machine-readable +Corresponding Source under the terms of this License, in one of these +ways: + +- a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. +- b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the Corresponding + Source from a network server at no charge. +- c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. +- d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. +- e) Convey the object code using peer-to-peer transmission, + provided you inform other peers where the object code and + Corresponding Source of the work are being offered to the general + public at no charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + +A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, +family, or household purposes, or (2) anything designed or sold for +incorporation into a dwelling. In determining whether a product is a +consumer product, doubtful cases shall be resolved in favor of +coverage. For a particular product received by a particular user, +"normally used" refers to a typical or common use of that class of +product, regardless of the status of the particular user or of the way +in which the particular user actually uses, or expects or is expected +to use, the product. A product is a consumer product regardless of +whether the product has substantial commercial, industrial or +non-consumer uses, unless such uses represent the only significant +mode of use of the product. + +"Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to +install and execute modified versions of a covered work in that User +Product from a modified version of its Corresponding Source. The +information must suffice to ensure that the continued functioning of +the modified object code is in no case prevented or interfered with +solely because modification has been made. + +If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + +The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or +updates for a work that has been modified or installed by the +recipient, or for the User Product in which it has been modified or +installed. Access to a network may be denied when the modification +itself materially and adversely affects the operation of the network +or violates the rules and protocols for communication across the +network. + +Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + +### 7. Additional Terms. + +"Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders +of that material) supplement the terms of this License with terms: + +- a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or +- b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or +- c) Prohibiting misrepresentation of the origin of that material, + or requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or +- d) Limiting the use for publicity purposes of names of licensors + or authors of the material; or +- e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or +- f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions + of it) with contractual assumptions of liability to the recipient, + for any liability that these contractual assumptions directly + impose on those licensors and authors. + +All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; the +above requirements apply either way. + +### 8. Termination. + +You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + +However, if you cease all violation of this License, then your license +from a particular copyright holder is reinstated (a) provisionally, +unless and until the copyright holder explicitly and finally +terminates your license, and (b) permanently, if the copyright holder +fails to notify you of the violation by some reasonable means prior to +60 days after the cessation. + +Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + +Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + +### 9. Acceptance Not Required for Having Copies. + +You are not required to accept this License in order to receive or run +a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + +### 10. Automatic Licensing of Downstream Recipients. + +Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + +An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + +### 11. Patents. + +A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + +A contributor's "essential patent claims" are all patent claims owned +or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + +In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + +If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + +A patent license is "discriminatory" if it does not include within the +scope of its coverage, prohibits the exercise of, or is conditioned on +the non-exercise of one or more of the rights that are specifically +granted under this License. You may not convey a covered work if you +are a party to an arrangement with a third party that is in the +business of distributing software, under which you make payment to the +third party based on the extent of your activity of conveying the +work, and under which the third party grants, to any of the parties +who would receive the covered work from you, a discriminatory patent +license (a) in connection with copies of the covered work conveyed by +you (or copies made from those copies), or (b) primarily for and in +connection with specific products or compilations that contain the +covered work, unless you entered into that arrangement, or that patent +license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + +### 12. No Surrender of Others' Freedom. + +If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under +this License and any other pertinent obligations, then as a +consequence you may not convey it at all. For example, if you agree to +terms that obligate you to collect a royalty for further conveying +from those to whom you convey the Program, the only way you could +satisfy both those terms and this License would be to refrain entirely +from conveying the Program. + +### 13. Remote Network Interaction; Use with the GNU General Public License. + +Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your +version supports such interaction) an opportunity to receive the +Corresponding Source of your version by providing access to the +Corresponding Source from a network server at no charge, through some +standard or customary means of facilitating copying of software. This +Corresponding Source shall include the Corresponding Source for any +work covered by version 3 of the GNU General Public License that is +incorporated pursuant to the following paragraph. + +Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + +### 14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions +of the GNU Affero General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever +published by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future versions +of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + +Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + +### 15. Disclaimer of Warranty. + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT +WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND +PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE +DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR +CORRECTION. + +### 16. Limitation of Liability. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR +CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT +NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR +LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM +TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER +PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +### 17. Interpretation of Sections 15 and 16. + +If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + +END OF TERMS AND CONDITIONS + +## How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these +terms. + +To do so, attach the following notices to the program. It is safest to +attach them to the start of each source file to most effectively state +the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as + published by the Free Software Foundation, either version 3 of the + License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper +mail. + +If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for +the specific requirements. + +You should also get your employer (if you work as a programmer) or +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. For more information on this, and how to apply and follow +the GNU AGPL, see . diff --git a/README.md b/README.md new file mode 100644 index 0000000..ff0a389 --- /dev/null +++ b/README.md @@ -0,0 +1,186 @@ +# ${REPO_NAME} + + +## Getting Started + +These instructions will give you a copy of the project up and running +on your local machine for development and testing purposes. See +deployment for notes on deploying the project on a live system. + +### Prerequisites + +You have two options for getting started: installing all the +requirements locally, or using the included Nix development shell with +[Nix flakes](https://nixos.wiki/wiki/flakes). + +#### Nix development shell +If you have [`direnv`](https://direnv.net/) installed on your machine, +run `direnv allow .` at the root of this project to automatically +enter the Nix development shell for ${REPO_NAME}. + +Otherwise, you can simply run `nix develop` at the root of the project. + +All the necessary tools will be installed automatically, except for +Docker (see below). + +#### Manually installing the prerequisites +If you decide to install everything manually, you will need the +following tools: +- [Rust](https://www.rust-lang.org/), in particular `cargo`. This + project uses a precise version of Rust, check the + [rust-toolchain.toml](./rust-toolchain.toml) file for more + information; +- [sqlx-cli](https://github.com/launchbadge/sqlx/tree/main/sqlx-cli), + necessary for creating the database used by the backend and running + its migrations; +- [just](https://just.systems/), an alternative to Makefiles, for + running commands more easily; +- [cargo-audit](https://github.com/rustsec/rustsec/tree/main/cargo-audit), + to verify whether the project contains known vulnerabilities; +- [cargo-auditable](https://github.com/rust-secure-code/cargo-auditable), + to audit the compiled binaries; +- [cargo-tarpaulin](https://github.com/xd009642/tarpaulin), a code + coverage tool for Rust; +- [cargo-msrv](https://crates.io/crates/cargo-msrv), to check whether + your code respects the minimum supported Rust version used by this + project. + +It is also recommended, though not necessary, to have the following +tools installed: +- [bacon](https://dystroy.org/bacon/), to automatically run cargo + commands on code changes; +- [Docker](https://www.docker.com/) to easily spin up a PostgreSQL + database required by the backend, as well as Mailpit (see below) +- [Mailpit](https://mailpit.axllent.org/) to easily test sending + emails without the need of a real SMTP server (included in the + default dev Docker Compose file); +- [rust-analyzer](https://rust-analyzer.github.io/), the *de facto* + standard LSP server for Rust; +- [sqls](https://github.com/sqls-server/sqls), an SQL LSP server, + useful when editing SQL files for migrations or tests. + +### Installing + +Start by cloning the repository to your local machine: + +```sh +$ git clone ${REPO_HTTPS_URL} +$ cd ${REPO_NAME} +``` + +Then, copy the `.env.example` file to a `.env` file and adjust it as +you see fit. + +As mentioned above, if you have [Nix +flakes](https://nixos.wiki/wiki/flakes) and +[direnv](https://direnv.net/) installed, you can simply run +`direnv allow .` to automatically set up your development environment. +Otherwise, you should install every required tool yourself (including, +if you want, the optional tools). + +You should then spin up a PostgreSQL database ${REPO_NAME} will be +using. You can do it manually, or you can spin up the Docker Compose +file included with this repository. + +```sh +just docker-start +``` + +This will launch a PostgreSQL 16 container based on the content of the +`.env` file you created earlier. + +To stop the Docker containers, simply run the following command: + +```sh +just docker-stop +``` + +## Running the project +To run the project, you can run one of the two following commands: + +```sh +just run # starts the Docker containers automatically before + # launching the project +just run-no-docker # runs the project without the Docker containers +``` + +## Running the tests + +There are two ways to run the tests of ${REPO_NAME}. The first one is +the usual `cargo run`, which you can run with `just run`. It will +simply run the tests and fail if one of them or more fails. + +There is a second option: + +```sh +just coverage +``` + +This command will run all the tests of the project and generate a code +coverage report for ${REPO_NAME} in the `coverage/` directory. You +will find both the `lcov` coverage file for the project, as well as an +HTML file you can open in your browser to explore which line of code +is covered or not. And, as with `just test`, if at least one of these +tests fails, the code coverage will fail too and will display the same +information. + +**NOTE**: The CI pipeline will run `just coverage` and will fail if +any of the tests fails or if the code coverage drops below 60% of code +covered by tests. + +### Linting + +This project uses [clippy](https://github.com/rust-lang/rust-clippy) +for linting the project. You can run it with the following command: + +```sh +just lint +``` + +**NOTE*: The CI pipeline will run `just lint` and will fail if the +command fails too. It is strongly recommended to fix any issue from +clippy before opening a pull request. + +### Style test + +This project uses [rustfmt](https://github.com/rust-lang/rustfmt) to format its code. You can format your code with this command: +```sh +just format +``` + +You can also check if your code is properly formatted with: +```sh +just format-check +``` + +## Contributing + +Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code +of conduct, and the process for submitting pull requests to us. + +## Versioning + +We use [Semantic Versioning](http://semver.org/) for versioning. For +the versions available, see the [tags on this +repository](${REPO_LINK}/tags). + +## Authors + + - **${REPO_OWNER}** - *Author and maintainer* - + [${REPO_OWNER}](https://labs.phundrak.com/${REPO_OWNER}) + + + + +## License + +This project is licensed under the [GNU AFFERO GENERAL PUBLIC +LICENSE](LICENSE.md) (`AGPL-3.0-or-later`) + - see the [LICENSE.md](LICENSE.md) file for details + +## Acknowledgments + + - The `README` file is based on the templates you can find over at + [github.com/PurpleBooth/a-good-readme-template](https://github.com/PurpleBooth/a-good-readme-template#readme); + - The `CONTRIBUTING` and `CODE_OF_CONDUCT` files are based on the + files generated over at . diff --git a/bacon.toml b/bacon.toml new file mode 100644 index 0000000..31c1d7a --- /dev/null +++ b/bacon.toml @@ -0,0 +1,84 @@ +# This is a configuration file for the bacon tool +# +# Bacon repository: https://github.com/Canop/bacon +# Complete help on configuration: https://dystroy.org/bacon/config/ +# You can also check bacon's own bacon.toml file +# as an example: https://github.com/Canop/bacon/blob/main/bacon.toml + +default_job = "clippy-all" + +[jobs.check] +command = ["cargo", "check", "--color", "always"] +need_stdout = false + +[jobs.check-all] +command = ["cargo", "check", "--all-targets", "--color", "always"] +need_stdout = false + +# Run clippy on the default target +[jobs.clippy] +command = [ + "cargo", "clippy", + "--color", "always", +] +need_stdout = false + +[jobs.clippy-all] +command = [ + "cargo", "clippy", + "--all-targets", + "--color", "always", +] +need_stdout = false + +[jobs.test] +command = [ + "cargo", "test", "--color", "always", + "--", "--color", "always", # see https://github.com/Canop/bacon/issues/124 +] +need_stdout = true + +[jobs.doc] +command = ["cargo", "doc", "--color", "always", "--no-deps"] +need_stdout = false + +# If the doc compiles, then it opens in your browser and bacon switches +# to the previous job +[jobs.doc-open] +command = ["cargo", "doc", "--color", "always", "--no-deps", "--open"] +need_stdout = false +on_success = "back" # so that we don't open the browser at each change + +# You can run your application and have the result displayed in bacon, +# *if* it makes sense for this crate. +# Don't forget the `--color always` part or the errors won't be +# properly parsed. +# If your program never stops (eg a server), you may set `background` +# to false to have the cargo run output immediately displayed instead +# of waiting for program's end. +[jobs.run] +command = [ + "cargo", "run", + "--color", "always", + # put launch parameters for your program behind a `--` separator +] +need_stdout = true +allow_warnings = true +background = true + +# This parameterized job runs the example of your choice, as soon +# as the code compiles. +# Call it as +# bacon ex -- my-example +[jobs.ex] +command = ["cargo", "run", "--color", "always", "--example"] +need_stdout = true +allow_warnings = true + +# You may define here keybindings that would be specific to +# a project, for example a shortcut to launch a specific job. +# Shortcuts to internal functions (scrolling, toggling, etc.) +# should go in your personal global prefs.toml file instead. +[keybindings] +# alt-m = "job:my-job" +c = "job:clippy-all" # comment this to have 'c' run clippy on only the default target diff --git a/docker/compose.dev.yml b/docker/compose.dev.yml new file mode 100644 index 0000000..c864c59 --- /dev/null +++ b/docker/compose.dev.yml @@ -0,0 +1,55 @@ +services: + db: + image: postgres:16-alpine + restart: unless-stopped + container_name: ${REPO_NAME_KEBAB}-db + environment: + POSTGRES_PASSWORD: $${DB_PASSWORD} + POSTGRES_USER: $${DB_USER} + POSTGRES_DB: $${DB_NAME} + ports: + - 127.0.0.1:5432:5432 + volumes: + - ${REPO_NAME_SNAKE}_db_data:/var/lib/postgresql/data + + # If you run ${REPO_NAME_PASCAL} in production, I recommend you to + # not run PgAdmin with it. + pgadmin: + image: dpage/pgadmin4:8 + restart: unless-stopped + container_name: ${REPO_NAME_KEBAB}-pgadmin + environment: + PGADMIN_DEFAULT_EMAIL: admin@example.com + PGADMIN_DEFAULT_PASSWORD: password + # Disables sending emails. Disable it if you decide to run + # PgAdmin in production + PGADMIN_DISABLE_POSTFIX: true + ports: + - 127.0.0.1:8080:80 + volumes: + - ${REPO_NAME_SNAKE}_pgadmin_data:/var/lib/pgadmin + depends_on: + - db + + # If you run ${REPO_NAME_PASCAL} in production, DO NOT use mailpit. + # This tool is for testing only. Instead, you should use a real SMTP + # provider, such as Mailgun, Mailwhale, or Postal. + mailpit: + image: axllent/mailpit:latest + restart: unless-stopped + container_name: ${REPO_NAME_KEBAB}-mailpit + ports: + - 127.0.0.1:8025:8025 # WebUI + - 127.0.0.1:1025:1025 # SMTP + volumes: + - ${REPO_NAME_SNAKE}_mailpit:/data + environment: + MP_MAX_MESSAGES: 5000 + MP_DATABASE: /data/mailpit.db + MP_SMTP_AUTH_ACCEPT_ANY: 1 + MP_SMTP_AUTH_ALLOW_INSECURE: 1 + +volumes: + ${REPO_NAME_SNAKE}_db_data: + ${REPO_NAME_SNAKE}_pgadmin_data: + ${REPO_NAME_SNAKE}_mailpit: diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..6a0d9c5 --- /dev/null +++ b/flake.nix @@ -0,0 +1,61 @@ +{ + description = "${REPO_DESCRIPTION}"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + rust-overlay.url = "github:oxalica/rust-overlay"; + }; + + outputs = { self, nixpkgs, flake-utils, rust-overlay }: + flake-utils.lib.eachSystem ["x86_64-linux"] (system: + let + overlays = [ (import rust-overlay) ]; + pkgs = import nixpkgs { inherit system overlays; }; + rustVersion = (pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml); + rustPlatform = pkgs.makeRustPlatform { + cargo = rustVersion; + rustc = rustVersion; + }; + + appName = "${REPO_NAME_KEBAB}"; + + appRustBuild = rustPlatform.buildRustPackage { + pname = appName; + version = "0.1.0"; + src = ./.; + cargoLock.lockFile = ./Cargo.lock; + }; + + dockerImage = pkgs.dockerTools.buildLayeredImage { + name = appName; + config = { + Entrypoint = [ "$${appRustBuild}/bin/$${appName}" ]; + Env = [ "SSL_CERT_FILE=$${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt" ]; + Tag = "latest"; + }; + contents = [ appRustBuild pkgs.cacert ]; + }; + in { + packages = { + rustPackage = appRustBuild; + docker = dockerImage; + }; + defaultPackage = dockerImage; + devShell = with pkgs; mkShell { + buildInputs = [ + bacon + cargo + cargo-audit + cargo-auditable + cargo-tarpaulin + cargo-msrv + just + rust-analyzer + (rustVersion.override { extensions = [ "rust-src" ]; }) + sqls + sqlx-cli + ]; + }; + }); +} diff --git a/justfile b/justfile new file mode 100644 index 0000000..b8ef7ae --- /dev/null +++ b/justfile @@ -0,0 +1,72 @@ +default: run + +prepare: + cargo sqlx prepare + +migrate: + sqlx migrate run + +format: + cargo fmt --all + +format-check: + cargo fmt --check --all + +build: + cargo auditable build + +build-release: + cargo auditable build --release + +run: docker-start + cargo auditable run + +run-no-docker: + cargo auditable run + +lint: + cargo clippy --all-targets + +msrv: + cargo msrv verify + +release-build: + cargo auditable build --release + +release-run: + cargo auditable run --release + +audit: build + cargo audit bin target/debug/${REPO_NAME_KEBAB} + +audit-release: build-release + cargo audit bin target/release/${REPO_NAME_KEBAB} + +test: + cargo test + +coverage: + mkdir -p coverage + cargo tarpaulin --config .tarpaulin.local.toml + +coverage-ci: + mkdir -p coverage + cargo tarpaulin --config .tarpaulin.ci.toml + +check-all: format-check lint msrv coverage audit + +docker-build: + nix build .#docker + +docker-start: + docker compose -f docker/compose.dev.yml up -d + +docker-stop: + docker compose -f docker/compose.dev.yml down + +docker-logs: + docker compose -f docker/compose.dev.yml logs -f + +## Local Variables: +## mode: makefile +## End: diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..d4fc4a3 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "1.78.0" +components = [ "rustfmt", "rust-src", "clippy", "rust-analyzer" ] +profile = "default" diff --git a/settings/base.yaml b/settings/base.yaml new file mode 100644 index 0000000..88493d2 --- /dev/null +++ b/settings/base.yaml @@ -0,0 +1,18 @@ +application: + port: 3000 + name: "${REPO_NAME_PASCAL}" + version: 0.1.0 + +database: + host: localhost + port: 5432 + name: ${REPO_NAME_KEBAB} + user: dev + password: password + require_ssl: false + +email: + host: smtp.${REPO_NAME_KEBAB}.example + user: user@${REPO_NAME_KEBAB}.example + from: ${REPO_NAME_PASCAL} + password: hunter2 diff --git a/settings/development.yaml b/settings/development.yaml new file mode 100644 index 0000000..bf09150 --- /dev/null +++ b/settings/development.yaml @@ -0,0 +1,7 @@ +frontend_url: http://localhost:5173 +debug: true + +application: + protocol: http + host: 127.0.0.1 + base_url: http://127.0.0.1:3000 diff --git a/settings/production.yaml b/settings/production.yaml new file mode 100644 index 0000000..37161ea --- /dev/null +++ b/settings/production.yaml @@ -0,0 +1,7 @@ +debug: false +frontend_url: "" + +application: + protocol: https + host: 0.0.0.0 + base_url: "" diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..0cabf4c --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,63 @@ +#![deny(clippy::all)] +#![deny(clippy::pedantic)] +#![deny(clippy::nursery)] +#![allow(clippy::module_name_repetitions)] +#![allow(clippy::unused_async)] +#![allow(clippy::useless_let_if_seq)] // Reason: prevents some OpenApi structs from compiling + +pub mod route; +pub mod settings; +pub mod startup; +pub mod telemetry; + +type MaybeListener = Option>; + +async fn prepare(listener: MaybeListener, test_db: Option) -> startup::Application { + dotenvy::dotenv().ok(); + let settings = settings::Settings::new().expect("Failed to read settings"); + if !cfg!(test) { + let subscriber = telemetry::get_subscriber(settings.clone().debug); + telemetry::init_subscriber(subscriber); + } + tracing::event!( + target: "${REPO_NAME_KEBAB}", + tracing::Level::DEBUG, + "Using these settings: {:?}", + settings.clone() + ); + let application = startup::Application::build(settings.clone(), test_db, listener).await; + tracing::event!( + target: "${REPO_NAME_KEBAB}", + tracing::Level::INFO, + "Listening on http://127.0.0.1:{}/", + application.port() + ); + application +} + +/// # Errors +/// +/// May return an error if the server encounters an error it cannot +/// recover from. +#[cfg(not(tarpaulin_include))] +pub async fn run(listener: MaybeListener) -> Result<(), std::io::Error> { + let application = prepare(listener, None).await; + application.make_app().run().await +} + +#[cfg(test)] +async fn make_random_tcp_listener() -> poem::listener::TcpListener { + let tcp_listener = + std::net::TcpListener::bind("127.0.0.1:0").expect("Failed to bind a random TCP listener"); + let port = tcp_listener.local_addr().unwrap().port(); + poem::listener::TcpListener::bind(format!("127.0.0.1:{port}")) +} + +#[cfg(test)] +async fn get_test_app(test_db: Option) -> startup::App { + let tcp_listener = crate::make_random_tcp_listener().await; + crate::prepare(Some(tcp_listener), test_db) + .await + .make_app() + .into() +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..be64d08 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,5 @@ +#[cfg(not(tarpaulin_include))] +#[tokio::main] +async fn main() -> Result<(), std::io::Error> { + ${REPO_NAME_SNAKE}::run(None).await +} diff --git a/src/route/health.rs b/src/route/health.rs new file mode 100644 index 0000000..6d7862a --- /dev/null +++ b/src/route/health.rs @@ -0,0 +1,29 @@ +use poem_openapi::{ApiResponse, OpenApi}; + +use super::ApiCategory; + +#[derive(ApiResponse)] +enum HealthResponse { + #[oai(status = 200)] + Ok, +} + +pub struct HealthApi; + +#[OpenApi(prefix_path = "/v1/health-check", tag = "ApiCategory::Health")] +impl HealthApi { + #[oai(path = "/", method = "get")] + async fn health_check(&self) -> HealthResponse { + tracing::event!(target: "${REPO_NAME_KEBAB}", tracing::Level::DEBUG, "Accessing health-check endpoint."); + HealthResponse::Ok + } +} + +#[tokio::test] +async fn health_check_works() { + let app = crate::get_test_app(None).await; + let cli = poem::test::TestClient::new(app); + let resp = cli.get("/v1/health-check").send().await; + resp.assert_status_is_ok(); + resp.assert_text("").await; +} diff --git a/src/route/mod.rs b/src/route/mod.rs new file mode 100644 index 0000000..f2c3ca4 --- /dev/null +++ b/src/route/mod.rs @@ -0,0 +1,18 @@ +use poem_openapi::{OpenApi, Tags}; + +mod health; +pub use health::HealthApi; + +mod version; +pub use version::VersionApi; + +#[derive(Tags)] +enum ApiCategory { + Health, + Version, +} + +pub(crate) struct Api; + +#[OpenApi] +impl Api {} diff --git a/src/route/version.rs b/src/route/version.rs new file mode 100644 index 0000000..abaf6e7 --- /dev/null +++ b/src/route/version.rs @@ -0,0 +1,46 @@ +use poem::Result; +use poem_openapi::{payload::Json, ApiResponse, Object, OpenApi}; + +use crate::settings::Settings; + +use super::ApiCategory; + +#[derive(Object, Debug, Clone, serde::Serialize, serde::Deserialize)] +struct Meta { + version: String, +} + +impl From> for Meta { + fn from(value: poem::web::Data<&Settings>) -> Self { + let version = value.application.version.clone(); + Self { version } + } +} + +#[derive(ApiResponse)] +enum VersionResponse { + #[oai(status = 200)] + Version(Json), +} + +pub struct VersionApi; + +#[OpenApi(prefix_path = "/v1/version", tag = "ApiCategory::Version")] +impl VersionApi { + #[oai(path = "/", method = "get")] + async fn version(&self, settings: poem::web::Data<&Settings>) -> Result { + tracing::event!(target: "${REPO_NAME_KEBAB}", tracing::Level::DEBUG, "Accessing version endpoint."); + Ok(VersionResponse::Version(Json(settings.into()))) + } +} + +#[tokio::test] +async fn version_works() { + let app = crate::get_test_app(None).await; + let cli = poem::test::TestClient::new(app); + let resp = cli.get("/v1/version").send().await; + resp.assert_status_is_ok(); + let json = resp.json().await; + let json_value = json.value(); + json_value.object().get("version").assert_not_null(); +} diff --git a/src/settings.rs b/src/settings.rs new file mode 100644 index 0000000..97e9d52 --- /dev/null +++ b/src/settings.rs @@ -0,0 +1,246 @@ +use sqlx::ConnectOptions; + +#[derive(Debug, serde::Deserialize, Clone, Default)] +pub struct Settings { + pub application: ApplicationSettings, + pub database: Database, + pub debug: bool, + pub email: EmailSettings, + pub frontend_url: String, +} + +impl Settings { + #[must_use] + pub fn web_address(&self) -> String { + if self.debug { + format!( + "{}:{}", + self.application.base_url.clone(), + self.application.port + ) + } else { + self.application.base_url.clone() + } + } + + /// Multipurpose function that helps detect the current + /// environment the application is running in using the + /// `APP_ENVIRONMENT` environment variable. + /// + /// ```text + /// APP_ENVIRONMENT = development | dev | production | prod + /// ``` + /// + /// After detection, it loads the appropriate `.yaml` file. It + /// then loads the environment variables that overrides whatever + /// is set in the `.yaml` files. For this to work, the environment + /// variable MUST be in uppercase and start with `APP`, a `_` + /// separator, then the category of settings, followed by a `__` + /// separator, and finally the variable itself. For instance, + /// `APP__APPLICATION_PORT=3001` for `port` to be set as `3001`. + /// + /// # Errors + /// + /// Function may return an error if it fails to parse its config + /// files. + /// + /// # Panics + /// + /// Panics if the program fails to detect the directory it is + /// running in. Can also panic if it fails to parse the + /// environment variable `APP_ENVIRONMENT` and it fails to fall + /// back to its default value. + pub fn new() -> Result { + let base_path = std::env::current_dir().expect("Failed to determine the current directory"); + let settings_directory = base_path.join("settings"); + let environment: Environment = std::env::var("APP_ENVIRONMENT") + .unwrap_or_else(|_| "development".into()) + .try_into() + .expect("Failed to parse APP_ENVIRONMENT"); + let environment_filename = format!("{environment}.yaml"); + let settings = config::Config::builder() + .add_source(config::File::from(settings_directory.join("base.yaml"))) + .add_source(config::File::from( + settings_directory.join(environment_filename), + )) + .add_source( + config::Environment::with_prefix("APP") + .prefix_separator("_") + .separator("__"), + ) + .build()?; + settings.try_deserialize::() + } +} + +#[derive(Debug, serde::Deserialize, Clone, Default)] +pub struct ApplicationSettings { + pub name: String, + pub version: String, + pub port: u16, + pub host: String, + pub base_url: String, + pub protocol: String, +} + +#[derive(Debug, serde::Deserialize, Clone, Default)] +pub struct Database { + pub host: String, + pub port: u16, + pub name: String, + pub user: String, + pub password: String, + pub require_ssl: bool, +} + +impl Database { + #[must_use] + pub fn get_connect_options(&self) -> sqlx::postgres::PgConnectOptions { + let ssl_mode = if self.require_ssl { + sqlx::postgres::PgSslMode::Require + } else { + sqlx::postgres::PgSslMode::Prefer + }; + sqlx::postgres::PgConnectOptions::new() + .host(&self.host) + .username(&self.user) + .password(&self.password) + .port(self.port) + .ssl_mode(ssl_mode) + .database(&self.name) + .log_statements(tracing::log::LevelFilter::Trace) + } +} + +#[derive(Debug, PartialEq, Eq)] +pub enum Environment { + Development, + Production, +} + +impl Default for Environment { + fn default() -> Self { + Self::Development + } +} + +impl std::fmt::Display for Environment { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let self_str = match self { + Self::Development => "development", + Self::Production => "production", + }; + write!(f, "{self_str}") + } +} + +impl TryFrom for Environment { + type Error = String; + + fn try_from(value: String) -> Result { + match value.to_lowercase().as_str() { + "development" | "dev" => Ok(Self::Development), + "production" | "prod" => Ok(Self::Production), + other => Err(format!( + "{other} is not a supported environment. Use either `development` or `production`" + )), + } + } +} + +impl TryFrom<&str> for Environment { + type Error = String; + + fn try_from(value: &str) -> Result { + Self::try_from(value.to_string()) + } +} + +#[derive(serde::Deserialize, Clone, Debug, Default)] +pub struct EmailSettings { + pub host: String, + pub user: String, + pub password: String, + pub from: String, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn default_environment_works() { + let default_environment = Environment::default(); + assert_eq!(Environment::Development, default_environment); + } + + #[test] + fn display_environment_works() { + let expected_prod = "production".to_string(); + let expected_dev = "development".to_string(); + let prod = Environment::Production.to_string(); + let dev = Environment::Development.to_string(); + assert_eq!(expected_prod, prod); + assert_eq!(expected_dev, dev); + } + + #[test] + fn try_from_works() { + [ + "DEVELOPMENT", + "DEVEloPmENT", + "Development", + "DEV", + "Dev", + "dev", + ] + .iter() + .map(|v| (*v).to_string()) + .for_each(|v| { + let environment = Environment::try_from(v); + assert!(environment.is_ok()); + assert_eq!(Environment::Development, environment.unwrap()); + }); + [ + "PRODUCTION", + "Production", + "PRODuction", + "production", + "PROD", + "Prod", + "prod", + ] + .iter() + .map(|v| (*v).to_string()) + .for_each(|v| { + let environment = Environment::try_from(v); + assert!(environment.is_ok()); + assert_eq!(Environment::Production, environment.unwrap()); + }); + let environment = Environment::try_from("invalid"); + assert!(environment.is_err()); + assert_eq!( + "invalid is not a supported environment. Use either `development` or `production`" + .to_string(), + environment.err().unwrap() + ); + } + + #[test] + fn web_address_works() { + let mut settings = Settings { + debug: false, + application: ApplicationSettings { + base_url: "127.0.0.1".to_string(), + port: 3000, + ..Default::default() + }, + ..Default::default() + }; + let expected_no_debug = "127.0.0.1".to_string(); + let expected_debug = "127.0.0.1:3000".to_string(); + assert_eq!(expected_no_debug, settings.web_address()); + settings.debug = true; + assert_eq!(expected_debug, settings.web_address()); + } +} diff --git a/src/startup.rs b/src/startup.rs new file mode 100644 index 0000000..211c7b1 --- /dev/null +++ b/src/startup.rs @@ -0,0 +1,142 @@ +use poem::middleware::Cors; +use poem::middleware::{AddDataEndpoint, CorsEndpoint}; +use poem::{EndpointExt, Route}; +use poem_openapi::OpenApiService; + +use crate::{ + route::{Api, HealthApi, VersionApi}, + settings::Settings, +}; + +#[must_use] +pub fn get_connection_pool(settings: &crate::settings::Database) -> sqlx::postgres::PgPool { + tracing::event!( + target: "startup", + tracing::Level::INFO, + "connecting to database with configuration {:?}", + settings.clone() + ); + sqlx::postgres::PgPoolOptions::new() + .acquire_timeout(std::time::Duration::from_secs(2)) + .connect_lazy_with(settings.get_connect_options()) +} + +type Server = poem::Server, std::convert::Infallible>; +pub type App = AddDataEndpoint, sqlx::PgPool>, Settings>; + +pub struct Application { + server: Server, + app: poem::Route, + port: u16, + database: sqlx::postgres::PgPool, + settings: Settings, +} + +pub struct RunnableApplication { + server: Server, + app: App, +} + +impl RunnableApplication { + /// Runs the application until it decides to stop by itself. + /// + /// # Errors + /// + /// If the server encounters an internal error it cannot recover + /// from, it will forward it to this function which will forward + /// it to its caller. + pub async fn run(self) -> Result<(), std::io::Error> { + self.server.run(self.app).await + } +} + +impl From for App { + fn from(value: RunnableApplication) -> Self { + value.app + } +} + +impl From for RunnableApplication { + fn from(val: Application) -> Self { + let app = val + .app + .with(Cors::new()) + .data(val.database) + .data(val.settings); + let server = val.server; + Self { server, app } + } +} + +impl Application { + async fn setup_db( + settings: &Settings, + test_pool: Option, + ) -> sqlx::postgres::PgPool { + let database_pool = + test_pool.map_or_else(|| get_connection_pool(&settings.database), |pool| pool); + if !cfg!(test) { + migrate_database(&database_pool).await; + } + database_pool + } + + fn setup_app(settings: &Settings) -> poem::Route { + let api_service = OpenApiService::new( + (Api, HealthApi, VersionApi), + settings.application.clone().name, + settings.application.clone().version, + ); + let ui = api_service.swagger_ui(); + poem::Route::new().nest("/", api_service).nest("/docs", ui) + } + + fn setup_server( + settings: &Settings, + tcp_listener: Option>, + ) -> Server { + let tcp_listener = tcp_listener.unwrap_or_else(|| { + let address = format!( + "{}:{}", + settings.application.host, settings.application.port + ); + poem::listener::TcpListener::bind(address) + }); + poem::Server::new(tcp_listener) + } + pub async fn build( + settings: Settings, + test_pool: Option, + tcp_listener: Option>, + ) -> Self { + let database_pool = Self::setup_db(&settings, test_pool).await; + let port = settings.application.port; + let app = Self::setup_app(&settings); + let server = Self::setup_server(&settings, tcp_listener); + Self { + server, + app, + port, + database: database_pool, + settings, + } + } + + /// Make the app runnable. + #[must_use] + pub fn make_app(self) -> RunnableApplication { + self.into() + } + + #[must_use] + pub const fn port(&self) -> u16 { + self.port + } +} + +async fn migrate_database(pool: &sqlx::postgres::PgPool) { + sqlx::migrate!() + .run(pool) + .await + .expect("Failed to migrate the database"); +} diff --git a/src/telemetry.rs b/src/telemetry.rs new file mode 100644 index 0000000..7dfafed --- /dev/null +++ b/src/telemetry.rs @@ -0,0 +1,28 @@ +use tracing_subscriber::layer::SubscriberExt; + +#[must_use] +pub fn get_subscriber(debug: bool) -> impl tracing::Subscriber + Send + Sync { + let env_filter = if debug { "debug" } else { "info" }.to_string(); + let env_filter = tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(env_filter)); + let stdout_log = tracing_subscriber::fmt::layer().pretty(); + let subscriber = tracing_subscriber::Registry::default() + .with(env_filter) + .with(stdout_log); + let json_log = if debug { + None + } else { + Some(tracing_subscriber::fmt::layer().json()) + }; + subscriber.with(json_log) +} + +/// Initialize the global tracing subscriber. +/// +/// # Panics +/// +/// May panic if the function fails to set `subscriber` as the default +/// global subscriber. +pub fn init_subscriber(subscriber: impl tracing::Subscriber + Send + Sync) { + tracing::subscriber::set_global_default(subscriber).expect("Failed to set subscriber"); +}