First import
This commit is contained in:
32
.github/workflows/ci.yml
vendored
Normal file
32
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
name: Build Template
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
PROJECT_NAME: testgen
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Run cargo generate
|
||||||
|
uses: cargo-generate/cargo-generate-action@v0.13.0
|
||||||
|
with:
|
||||||
|
name: ${{ env.PROJECT_NAME }}
|
||||||
|
subfolder: template
|
||||||
|
template_values_file: .github/workflows/template_values.toml
|
||||||
|
- name: Install Rust toolchain
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
profile: minimal
|
||||||
|
override: true
|
||||||
|
- name: Cargo check
|
||||||
|
run: |
|
||||||
|
mv $PROJECT_NAME ${{ runner.temp }}/
|
||||||
|
cd ${{ runner.temp }}/$PROJECT_NAME
|
||||||
|
cargo check --tests --all-features --workspace
|
||||||
2
.github/workflows/template_values.toml
vendored
Normal file
2
.github/workflows/template_values.toml
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[values]
|
||||||
|
gh-username = "dax"
|
||||||
201
LICENSE
Normal file
201
LICENSE
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright 2022 David Rousselie
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
8
README.md
Normal file
8
README.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Actix API + Yew application Template
|
||||||
|
|
||||||
|
[](https://mit-license.org/)
|
||||||
|
[](https://github.com/dax/actix-yew-app-template/actions)
|
||||||
|
|
||||||
|
A [cargo generate](https://github.com/cargo-generate/cargo-generate) template for web application:
|
||||||
|
- backend is based on [Actix](https://actix.rs)
|
||||||
|
- frontend use [Yew](https://yew.rs)
|
||||||
7
template/.dockerignore
Normal file
7
template/.dockerignore
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
.git
|
||||||
|
.github
|
||||||
|
target/
|
||||||
|
tests/
|
||||||
|
Dockerfile
|
||||||
|
web/dist
|
||||||
|
api/config/local.*
|
||||||
24
template/.github/workflows/audit.yml
vendored
Normal file
24
template/.github/workflows/audit.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
name: Security audit
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- '**/Cargo.toml'
|
||||||
|
- '**/Cargo.lock'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
security_audit:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Install Rust
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
profile: minimal
|
||||||
|
override: true
|
||||||
|
- uses: Swatinem/rust-cache@v1
|
||||||
|
- uses: actions-rs/audit-check@v1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
118
template/.github/workflows/ci.yml
vendored
Normal file
118
template/.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
rustfmt:
|
||||||
|
name: Rustfmt
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Install Rust toolchain
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
profile: minimal
|
||||||
|
components: rustfmt
|
||||||
|
- name: Check formatting
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: fmt
|
||||||
|
args: --all -- --check
|
||||||
|
|
||||||
|
build:
|
||||||
|
name: Build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Install Rust toolchain
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
profile: minimal
|
||||||
|
- name: Install Wasm Rust toolchain
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
profile: minimal
|
||||||
|
target: wasm32-unknown-unknown
|
||||||
|
- uses: Swatinem/rust-cache@v1
|
||||||
|
with:
|
||||||
|
sharedKey: ci
|
||||||
|
- uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: build
|
||||||
|
args: --all-features --workspace
|
||||||
|
|
||||||
|
test:
|
||||||
|
needs: build
|
||||||
|
name: Test Suite
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Install Rust toolchain
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
profile: minimal
|
||||||
|
- uses: Swatinem/rust-cache@v1
|
||||||
|
with:
|
||||||
|
sharedKey: ci
|
||||||
|
- uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: test
|
||||||
|
args: --all-features --workspace
|
||||||
|
|
||||||
|
clippy:
|
||||||
|
needs: build
|
||||||
|
name: Clippy
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Install Rust toolchain
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
profile: minimal
|
||||||
|
components: clippy
|
||||||
|
- uses: Swatinem/rust-cache@v1
|
||||||
|
with:
|
||||||
|
sharedKey: ci
|
||||||
|
- name: Clippy check
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: clippy
|
||||||
|
args: --all-targets --all-features --workspace -- -D warnings
|
||||||
|
|
||||||
|
coverage:
|
||||||
|
name: Code coverage
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Install Rust toolchain
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
profile: minimal
|
||||||
|
- uses: Swatinem/rust-cache@v1
|
||||||
|
- name: Run cargo-tarpaulin
|
||||||
|
uses: actions-rs/tarpaulin@v0.1
|
||||||
|
with:
|
||||||
|
args: '--all-features --workspace --ignore-tests --out Lcov'
|
||||||
|
- name: Upload to Coveralls
|
||||||
|
# upload only if push
|
||||||
|
if: ${{ github.event_name == 'push' }}
|
||||||
|
uses: coverallsapp/github-action@master
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
path-to-lcov: './lcov.info'
|
||||||
3
template/.gitignore
vendored
Normal file
3
template/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
/target
|
||||||
|
api/config/local.*
|
||||||
|
web/dist
|
||||||
23
template/.pre-commit-config.yaml
Normal file
23
template/.pre-commit-config.yaml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
repos:
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
- id: format
|
||||||
|
name: format
|
||||||
|
language: system
|
||||||
|
pass_filenames: false
|
||||||
|
entry: cargo make format-flow
|
||||||
|
- id: format-toml
|
||||||
|
name: format-toml
|
||||||
|
language: system
|
||||||
|
pass_filenames: false
|
||||||
|
entry: cargo make format-toml-flow
|
||||||
|
- id: check
|
||||||
|
name: check
|
||||||
|
language: system
|
||||||
|
pass_filenames: false
|
||||||
|
entry: cargo make check-tests
|
||||||
|
- id: clippy
|
||||||
|
name: clippy
|
||||||
|
language: system
|
||||||
|
pass_filenames: false
|
||||||
|
entry: cargo make clippy-flow
|
||||||
17
template/Cargo.toml
Normal file
17
template/Cargo.toml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
[package]
|
||||||
|
name = "{{project-name}}"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
authors = ["{{authors}}"]
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
members = ["api", "web"]
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
uuid = { version = "0.8.0", features = ["serde"] }
|
||||||
|
chrono = { version = "0.4.0", features = ["serde"] }
|
||||||
31
template/Dockerfile
Normal file
31
template/Dockerfile
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
FROM lukemathwalker/cargo-chef:latest-rust-1.57.0 as chef
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
FROM chef as planner
|
||||||
|
COPY . .
|
||||||
|
RUN cargo chef prepare --recipe-path recipe.json
|
||||||
|
|
||||||
|
|
||||||
|
FROM chef as dep-builder
|
||||||
|
RUN cargo install cargo-make
|
||||||
|
RUN cargo install trunk
|
||||||
|
RUN rustup target add wasm32-unknown-unknown
|
||||||
|
COPY --from=planner /app/recipe.json recipe.json
|
||||||
|
RUN cargo chef cook -p {{project-name}} --release --recipe-path recipe.json
|
||||||
|
RUN cargo chef cook -p {{project-name}}-api --release --recipe-path recipe.json
|
||||||
|
RUN cargo chef cook -p {{project-name}}-web --release --recipe-path recipe.json --target wasm32-unknown-unknown
|
||||||
|
|
||||||
|
FROM dep-builder as release-builder
|
||||||
|
COPY . .
|
||||||
|
RUN cargo make build-release
|
||||||
|
RUN sed -i 's#http://localhost:8000/api#/api#' web/dist/snippets/{{project-name}}-web-*/js/api.js
|
||||||
|
|
||||||
|
FROM debian:bullseye-slim AS runtime
|
||||||
|
WORKDIR /app
|
||||||
|
RUN mkdir /data
|
||||||
|
COPY --from=release-builder /app/target/release/{{project-name}}-api {{project-name}}
|
||||||
|
COPY --from=release-builder /app/api/config/default.toml config/default.toml
|
||||||
|
COPY --from=release-builder /app/web/dist/ .
|
||||||
|
ENV {{crate_name | upcase}}_APPLICATION.API_PATH /api
|
||||||
|
ENV {{crate_name | upcase}}_APPLICATION.STATIC_PATH /
|
||||||
|
CMD ["/app/{{project-name}}"]
|
||||||
201
template/LICENSE
Normal file
201
template/LICENSE
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright 2022 David Rousselie
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
57
template/Makefile.toml
Normal file
57
template/Makefile.toml
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
[env]
|
||||||
|
CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = "true"
|
||||||
|
CARGO_MAKE_COVERAGE_PROVIDER = "tarpaulin"
|
||||||
|
CARGO_MAKE_CLIPPY_ARGS = "--tests -- -D warnings"
|
||||||
|
|
||||||
|
[tasks.default]
|
||||||
|
clear = true
|
||||||
|
alias = "watch"
|
||||||
|
|
||||||
|
[tasks.test]
|
||||||
|
install_crate = "cargo-nextest"
|
||||||
|
args = [
|
||||||
|
"nextest",
|
||||||
|
"run",
|
||||||
|
"@@remove-empty(CARGO_MAKE_CARGO_VERBOSE_FLAGS)",
|
||||||
|
"@@split(CARGO_MAKE_CARGO_BUILD_TEST_FLAGS, )",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tasks.audit]
|
||||||
|
condition = {}
|
||||||
|
workspace = false
|
||||||
|
|
||||||
|
[tasks.run-api]
|
||||||
|
command = "bash"
|
||||||
|
args = ["-c", "cd api; cargo make run"]
|
||||||
|
workspace = false
|
||||||
|
watch = { watch = ["./api/"], no_git_ignore = true }
|
||||||
|
|
||||||
|
[tasks.run-web]
|
||||||
|
command = "bash"
|
||||||
|
args = ["-c", "cd web; cargo make run"]
|
||||||
|
workspace = false
|
||||||
|
|
||||||
|
[tasks.run]
|
||||||
|
run_task = { name = ["run-api", "run-web"], parallel = true, fork = true }
|
||||||
|
workspace = false
|
||||||
|
|
||||||
|
[tasks.watch-api]
|
||||||
|
command = "bash"
|
||||||
|
args = ["-c", "cd api; cargo make dev-test-flow"]
|
||||||
|
workspace = false
|
||||||
|
watch = { watch = ["./api/"], no_git_ignore = true }
|
||||||
|
|
||||||
|
[tasks.watch-web]
|
||||||
|
command = "bash"
|
||||||
|
args = ["-c", "cd web; cargo make dev-test-flow"]
|
||||||
|
workspace = false
|
||||||
|
watch = { watch = ["./web/"], no_git_ignore = true }
|
||||||
|
|
||||||
|
[tasks.watch-root]
|
||||||
|
watch = { watch = ["./src/"], no_git_ignore = true }
|
||||||
|
run_task = "dev-test-flow"
|
||||||
|
workspace = false
|
||||||
|
|
||||||
|
[tasks.watch]
|
||||||
|
run_task = { name = ["watch-api", "watch-web", "watch-root"], parallel = true, fork = true }
|
||||||
|
workspace = false
|
||||||
73
template/README.md
Normal file
73
template/README.md
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# {{project-name | capitalize}}
|
||||||
|
|
||||||
|
[](http://www.gnu.org/licenses/agpl-3.0)
|
||||||
|
[](https://coveralls.io/github/{{gh-username}}/{{project-name}}?branch=main)
|
||||||
|
[](https://github.com/{{gh-username}}/{{project-name}}/actions)
|
||||||
|
|
||||||
|
{{project-name}} is ...
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- [ ]
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Using cargo (for development)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo make run
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual
|
||||||
|
|
||||||
|
1. Get the code
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/{{gh-username}}/{{project-name}}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Build api and web release assets
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo make build-release
|
||||||
|
```
|
||||||
|
|
||||||
|
It will produce a `target/release/{{project-name}}-api` backend binary and frontend assets in the `web/dist` directory.
|
||||||
|
|
||||||
|
3. Deploy assets
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p $DEPLOY_DIR/config
|
||||||
|
cp -a target/release/{{project-name}}-api $DEPLOY_DIR
|
||||||
|
cp -a web/dist/* $DEPLOY_DIR
|
||||||
|
cp -a api/config/{default.toml, prod.toml} $DEPLOY_DIR/config
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Run server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd $DEPLOY_DIR
|
||||||
|
env CONFIG_FILE=$DEPLOY_DIR/config/prod.toml ./{{project-name}}-api
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Docker
|
||||||
|
|
||||||
|
#### Build Docker image
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t {{project-name}} .
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Run {{project-name | capitalize}} using Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm -ti -p 8000:8000 project-name
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Access {{project-name | capitalize}} using [http://localhost:8000](http://localhost:8000)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[AGPL](LICENSE)
|
||||||
46
template/api/Cargo.toml
Normal file
46
template/api/Cargo.toml
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
[package]
|
||||||
|
name = "{{project-name}}-api"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
authors = ["{{authors}}"]
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
path = "src/main.rs"
|
||||||
|
name = "{{project-name}}-api"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
{{project-name}} = { path = ".." }
|
||||||
|
actix-web = "4.0.0"
|
||||||
|
actix-http = "3.0.0"
|
||||||
|
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
|
||||||
|
serde = { version = "1.0.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
uuid = { version = "0.8.0", features = ["serde"] }
|
||||||
|
chrono = { version = "0.4.0", features = ["serde"] }
|
||||||
|
mktemp = "0.4.0"
|
||||||
|
configparser = "3.0.0"
|
||||||
|
tracing = { version = "0.1.0", features = ["log"] }
|
||||||
|
tracing-subscriber = { version = "0.3.0", features = [
|
||||||
|
"std",
|
||||||
|
"env-filter",
|
||||||
|
"fmt",
|
||||||
|
"json",
|
||||||
|
] }
|
||||||
|
tracing-log = "0.1.0"
|
||||||
|
tracing-actix-web = "0.5.0"
|
||||||
|
regex = "1.5.0"
|
||||||
|
lazy_static = "1.4.0"
|
||||||
|
tracing-bunyan-formatter = "0.3.0"
|
||||||
|
thiserror = "1.0"
|
||||||
|
anyhow = "1.0"
|
||||||
|
http = "0.2.0"
|
||||||
|
config = "0.12.0"
|
||||||
|
actix-files = "0.6.0"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
proptest = "1.0.0"
|
||||||
|
reqwest = { version = "0.11.0", features = ["json"] }
|
||||||
|
rstest = "0.12.0"
|
||||||
8
template/api/Makefile.toml
Normal file
8
template/api/Makefile.toml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
extend = "../Makefile.toml"
|
||||||
|
|
||||||
|
[tasks.run]
|
||||||
|
clear = true
|
||||||
|
install_crate = { crate_name = "bunyan", binary = "bunyan" }
|
||||||
|
env = { "TASKRC" = "$PWD/taskrc" }
|
||||||
|
command = "bash"
|
||||||
|
args = ["-c", "cargo run | bunyan"]
|
||||||
6
template/api/config/default.toml
Normal file
6
template/api/config/default.toml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[application]
|
||||||
|
port = 8000
|
||||||
|
# See https://docs.rs/tracing-subscriber/latest/tracing_subscriber/struct.EnvFilter.html
|
||||||
|
log_directive = "info"
|
||||||
|
api_path = ""
|
||||||
|
front_base_url = "http://localhost:8080"
|
||||||
5
template/api/config/dev.toml
Normal file
5
template/api/config/dev.toml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
[application]
|
||||||
|
log_directive = "debug"
|
||||||
|
static_dir = "../web/dist"
|
||||||
|
api_path = "/api"
|
||||||
|
static_path = ""
|
||||||
4
template/api/config/prod.toml
Normal file
4
template/api/config/prod.toml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
[application]
|
||||||
|
static_dir = "."
|
||||||
|
api_path = "/api"
|
||||||
|
static_path = ""
|
||||||
2
template/api/config/test.toml
Normal file
2
template/api/config/test.toml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[application]
|
||||||
|
log_directive = "debug"
|
||||||
48
template/api/src/configuration.rs
Normal file
48
template/api/src/configuration.rs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
use config::{Config, ConfigError, Environment, File};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::env;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct Settings {
|
||||||
|
pub application: ApplicationSettings,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct ApplicationSettings {
|
||||||
|
pub port: u16,
|
||||||
|
pub log_directive: String,
|
||||||
|
pub front_base_url: String,
|
||||||
|
pub api_path: String,
|
||||||
|
pub static_path: Option<String>,
|
||||||
|
pub static_dir: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Settings {
|
||||||
|
pub fn new_from_file(file: Option<String>) -> Result<Self, ConfigError> {
|
||||||
|
let config_file_required = file.is_some();
|
||||||
|
let config_path = env::var("CONFIG_PATH").unwrap_or_else(|_| "config".into());
|
||||||
|
let config_file = file.unwrap_or_else(|| {
|
||||||
|
env::var("CONFIG_FILE").unwrap_or_else(|_| format!("{}/dev", &config_path))
|
||||||
|
});
|
||||||
|
|
||||||
|
let default_config_file = format!("{}/default", config_path);
|
||||||
|
let local_config_file = format!("{}/local", config_path);
|
||||||
|
println!(
|
||||||
|
"Trying to load {:?} config files",
|
||||||
|
vec![&default_config_file, &local_config_file, &config_file]
|
||||||
|
);
|
||||||
|
|
||||||
|
let config = Config::builder()
|
||||||
|
.add_source(File::with_name(&default_config_file))
|
||||||
|
.add_source(File::with_name(&local_config_file).required(false))
|
||||||
|
.add_source(File::with_name(&config_file).required(config_file_required))
|
||||||
|
.add_source(Environment::with_prefix("{{crate_name}}"))
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
config.try_deserialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new() -> Result<Self, ConfigError> {
|
||||||
|
Settings::new_from_file(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
67
template/api/src/lib.rs
Normal file
67
template/api/src/lib.rs
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
use actix_files as fs;
|
||||||
|
use actix_web::{dev::Server, http, middleware, web, App, HttpServer};
|
||||||
|
use configuration::Settings;
|
||||||
|
use core::time::Duration;
|
||||||
|
use std::net::TcpListener;
|
||||||
|
use tracing::info;
|
||||||
|
use tracing_actix_web::TracingLogger;
|
||||||
|
|
||||||
|
pub mod configuration;
|
||||||
|
pub mod {{crate_name}};
|
||||||
|
pub mod observability;
|
||||||
|
pub mod routes;
|
||||||
|
|
||||||
|
pub fn run(listener: TcpListener, settings: &Settings) -> Result<Server, std::io::Error> {
|
||||||
|
let api_path = settings.application.api_path.clone();
|
||||||
|
let front_base_url = settings.application.front_base_url.clone();
|
||||||
|
let static_path = settings.application.static_path.clone();
|
||||||
|
let static_dir = settings
|
||||||
|
.application
|
||||||
|
.static_dir
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| ".".to_string());
|
||||||
|
|
||||||
|
let server = HttpServer::new(move || {
|
||||||
|
info!(
|
||||||
|
"Mounting API on {}",
|
||||||
|
if api_path.is_empty() { "/" } else { &api_path }
|
||||||
|
);
|
||||||
|
let api_scope = web::scope(&api_path)
|
||||||
|
.wrap(
|
||||||
|
middleware::DefaultHeaders::new()
|
||||||
|
.add(("Access-Control-Allow-Origin", front_base_url.as_bytes()))
|
||||||
|
.add((
|
||||||
|
"Access-Control-Allow-Methods",
|
||||||
|
"POST, GET, OPTIONS".as_bytes(),
|
||||||
|
))
|
||||||
|
.add(("Access-Control-Allow-Headers", "content-type".as_bytes())),
|
||||||
|
)
|
||||||
|
.route("/hello", web::get().to(routes::hello))
|
||||||
|
.route(
|
||||||
|
"/TODO*",
|
||||||
|
web::method(http::Method::OPTIONS).to(routes::option_wildcard),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut app = App::new()
|
||||||
|
.wrap(TracingLogger::default())
|
||||||
|
.wrap(middleware::Compress::default())
|
||||||
|
.route("/ping", web::get().to(routes::ping))
|
||||||
|
.service(api_scope);
|
||||||
|
if let Some(path) = &static_path {
|
||||||
|
info!(
|
||||||
|
"Mounting static files on {}",
|
||||||
|
if path.is_empty() { "/" } else { &path }
|
||||||
|
);
|
||||||
|
let static_scope = fs::Files::new(path, &static_dir)
|
||||||
|
.use_last_modified(true)
|
||||||
|
.index_file("index.html");
|
||||||
|
app = app.service(static_scope);
|
||||||
|
}
|
||||||
|
app
|
||||||
|
})
|
||||||
|
.keep_alive(http::KeepAlive::Timeout(Duration::from_secs(60)))
|
||||||
|
.shutdown_timeout(60)
|
||||||
|
.listen(listener)?;
|
||||||
|
|
||||||
|
Ok(server.run())
|
||||||
|
}
|
||||||
15
template/api/src/main.rs
Normal file
15
template/api/src/main.rs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
use {{crate_name}}_api::configuration::Settings;
|
||||||
|
use {{crate_name}}_api::observability::{get_subscriber, init_subscriber};
|
||||||
|
use {{crate_name}}_api::run;
|
||||||
|
use std::net::TcpListener;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> std::io::Result<()> {
|
||||||
|
let settings = Settings::new().expect("Cannot load {{project-name | capitalize}} configuration");
|
||||||
|
let subscriber = get_subscriber(&settings.application.log_directive);
|
||||||
|
init_subscriber(subscriber);
|
||||||
|
|
||||||
|
let listener = TcpListener::bind(format!("0.0.0.0:{}", settings.application.port))
|
||||||
|
.expect("Failed to bind port");
|
||||||
|
run(listener, &settings)?.await
|
||||||
|
}
|
||||||
23
template/api/src/observability.rs
Normal file
23
template/api/src/observability.rs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
use tracing::{subscriber::set_global_default, Subscriber};
|
||||||
|
use tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer};
|
||||||
|
use tracing_log::LogTracer;
|
||||||
|
use tracing_subscriber::fmt::TestWriter;
|
||||||
|
use tracing_subscriber::{layer::SubscriberExt, EnvFilter};
|
||||||
|
|
||||||
|
pub fn get_subscriber(env_filter_str: &str) -> impl Subscriber + Send + Sync {
|
||||||
|
let formatting_layer =
|
||||||
|
BunyanFormattingLayer::new("{{project-name}}-api".into(), TestWriter::new);
|
||||||
|
|
||||||
|
let env_filter =
|
||||||
|
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(env_filter_str));
|
||||||
|
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(env_filter)
|
||||||
|
.with(JsonStorageLayer)
|
||||||
|
.with(formatting_layer)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init_subscriber(subscriber: impl Subscriber + Send + Sync) {
|
||||||
|
LogTracer::init().expect("Failed to set logger");
|
||||||
|
set_global_default(subscriber).expect("Failed to set subscriber");
|
||||||
|
}
|
||||||
28
template/api/src/routes/default.rs
Normal file
28
template/api/src/routes/default.rs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
use crate::{{crate_name}};
|
||||||
|
use actix_web::{http::StatusCode, HttpResponse, ResponseError};
|
||||||
|
use anyhow::Context;
|
||||||
|
use ::{{crate_name}}::Object;
|
||||||
|
|
||||||
|
impl ResponseError for {{crate_name}}::{{project-name | capitalize}}Error {
|
||||||
|
fn status_code(&self) -> StatusCode {
|
||||||
|
match self {
|
||||||
|
{{crate_name}}::{{project-name | capitalize}}Error::InvalidDataError { .. } => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
{{crate_name}}::{{project-name | capitalize}}Error::UnexpectedError(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "debug")]
|
||||||
|
pub async fn hello() -> Result<HttpResponse, {{crate_name}}::{{project-name | capitalize}}Error> {
|
||||||
|
let object: Object = {{crate_name}}::build_object()?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok()
|
||||||
|
.content_type("application/json")
|
||||||
|
.body(serde_json::to_string(&object).context("Cannot serialize object")?))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "debug")]
|
||||||
|
pub async fn option_wildcard() -> HttpResponse {
|
||||||
|
HttpResponse::Ok().finish()
|
||||||
|
}
|
||||||
5
template/api/src/routes/health_check.rs
Normal file
5
template/api/src/routes/health_check.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
use actix_web::HttpResponse;
|
||||||
|
|
||||||
|
pub async fn ping() -> HttpResponse {
|
||||||
|
HttpResponse::Ok().finish()
|
||||||
|
}
|
||||||
5
template/api/src/routes/mod.rs
Normal file
5
template/api/src/routes/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
mod default;
|
||||||
|
mod health_check;
|
||||||
|
|
||||||
|
pub use default::*;
|
||||||
|
pub use health_check::*;
|
||||||
40
template/api/src/{{project-name}}/api.rs
Normal file
40
template/api/src/{{project-name}}/api.rs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
use chrono::Utc;
|
||||||
|
use serde_json;
|
||||||
|
use {{crate_name}}::Object;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
fn error_chain_fmt(
|
||||||
|
e: &impl std::error::Error,
|
||||||
|
f: &mut std::fmt::Formatter<'_>,
|
||||||
|
) -> std::fmt::Result {
|
||||||
|
writeln!(f, "{}\n", e)?;
|
||||||
|
let mut current = e.source();
|
||||||
|
while let Some(cause) = current {
|
||||||
|
writeln!(f, "Caused by:\n\t{}", cause)?;
|
||||||
|
current = cause.source();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for {{project-name | capitalize}}Error {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
error_chain_fmt(self, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(thiserror::Error)]
|
||||||
|
pub enum {{project-name | capitalize}}Error {
|
||||||
|
#[error("Invalid {{project-name | capitalize}} data")]
|
||||||
|
InvalidDataError(#[from] serde_json::Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
UnexpectedError(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "debug")]
|
||||||
|
pub fn build_object() -> Result<Object, {{project-name | capitalize}}Error> {
|
||||||
|
Ok(Object {
|
||||||
|
name: "test".to_string(),
|
||||||
|
id: Uuid::new_v4(),
|
||||||
|
modified: Utc::now(),
|
||||||
|
})
|
||||||
|
}
|
||||||
3
template/api/src/{{project-name}}/mod.rs
Normal file
3
template/api/src/{{project-name}}/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
mod api;
|
||||||
|
|
||||||
|
pub use api::*;
|
||||||
22
template/api/tests/api/default.rs
Normal file
22
template/api/tests/api/default.rs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
use crate::helpers::app_address;
|
||||||
|
use ::{{crate_name}}::Object;
|
||||||
|
use rstest::*;
|
||||||
|
|
||||||
|
mod get_object {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_hello(app_address: &str) {
|
||||||
|
let object: Object = reqwest::Client::new()
|
||||||
|
.get(&format!("{}/hello", &app_address))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to execute request")
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.expect("Cannot parse JSON result");
|
||||||
|
|
||||||
|
assert_eq!(object.name, "test");
|
||||||
|
}
|
||||||
|
}
|
||||||
15
template/api/tests/api/health_check.rs
Normal file
15
template/api/tests/api/health_check.rs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
use crate::helpers::app_address;
|
||||||
|
use rstest::*;
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn health_check_works(app_address: &str) {
|
||||||
|
let response = reqwest::Client::new()
|
||||||
|
.get(&format!("{}/ping", &app_address))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to execute request.");
|
||||||
|
|
||||||
|
assert!(response.status().is_success());
|
||||||
|
assert_eq!(Some(0), response.content_length());
|
||||||
|
}
|
||||||
31
template/api/tests/api/helpers.rs
Normal file
31
template/api/tests/api/helpers.rs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
use {{crate_name}}_api::configuration::Settings;
|
||||||
|
use {{crate_name}}_api::observability::{get_subscriber, init_subscriber};
|
||||||
|
use rstest::*;
|
||||||
|
use std::net::TcpListener;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
fn setup_tracing(settings: &Settings) {
|
||||||
|
info!("Setting up tracing");
|
||||||
|
let subscriber = get_subscriber(&settings.application.log_directive);
|
||||||
|
init_subscriber(subscriber);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_server(settings: &Settings) -> String {
|
||||||
|
info!("Setting up server");
|
||||||
|
let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind random port");
|
||||||
|
let port = listener.local_addr().unwrap().port();
|
||||||
|
|
||||||
|
let server = {{crate_name}}_api::run(listener, settings).expect("Failed to bind address");
|
||||||
|
let _ = tokio::spawn(server);
|
||||||
|
format!("http://127.0.0.1:{}", port)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[fixture]
|
||||||
|
#[once]
|
||||||
|
pub fn app_address() -> String {
|
||||||
|
let settings = Settings::new_from_file(Some("config/test".to_string()))
|
||||||
|
.expect("Cannot load test configuration");
|
||||||
|
setup_tracing(&settings);
|
||||||
|
let address = setup_server(&settings);
|
||||||
|
address
|
||||||
|
}
|
||||||
3
template/api/tests/api/main.rs
Normal file
3
template/api/tests/api/main.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
mod default;
|
||||||
|
mod health_check;
|
||||||
|
mod helpers;
|
||||||
6
template/cargo-generate.toml
Normal file
6
template/cargo-generate.toml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[template]
|
||||||
|
cargo_generate_version = ">=0.13.0"
|
||||||
|
|
||||||
|
[placeholders.gh-username]
|
||||||
|
type = "string"
|
||||||
|
prompt = "GitHub username"
|
||||||
35
template/src/lib.rs
Normal file
35
template/src/lib.rs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Eq)]
|
||||||
|
pub struct Object {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub name: String,
|
||||||
|
#[serde(with = "tw_date_format")]
|
||||||
|
pub modified: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod tw_date_format {
|
||||||
|
use chrono::{DateTime, TimeZone, Utc};
|
||||||
|
use serde::{self, Deserialize, Deserializer, Serializer};
|
||||||
|
|
||||||
|
const FORMAT: &str = "%Y%m%dT%H%M%SZ";
|
||||||
|
|
||||||
|
pub fn serialize<S>(date: &DateTime<Utc>, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
let s = format!("{}", date.format(FORMAT));
|
||||||
|
serializer.serialize_str(&s)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deserialize<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let s = String::deserialize(deserializer)?;
|
||||||
|
Utc.datetime_from_str(&s, FORMAT)
|
||||||
|
.map_err(serde::de::Error::custom)
|
||||||
|
}
|
||||||
|
}
|
||||||
21
template/web/Cargo.toml
Normal file
21
template/web/Cargo.toml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
[package]
|
||||||
|
name = "{{project-name}}-web"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
authors = ["{{authors}}"]
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
path = "src/main.rs"
|
||||||
|
name = "{{project-name}}-web"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
{{project-name}} = { path = ".." }
|
||||||
|
yew = "0.19"
|
||||||
|
reqwasm = "0.5"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
wasm-bindgen-futures = "0.4"
|
||||||
|
uikit-rs = { git = "https://github.com/dax/uikit-rs.git" }
|
||||||
|
wasm-bindgen = "0.2.79"
|
||||||
12
template/web/Makefile.toml
Normal file
12
template/web/Makefile.toml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
extend = "../Makefile.toml"
|
||||||
|
|
||||||
|
[tasks.build-release]
|
||||||
|
install_crate = { crate_name = "trunk", binary = "trunk" }
|
||||||
|
command = "trunk"
|
||||||
|
args = ["build", "--release"]
|
||||||
|
|
||||||
|
[tasks.run]
|
||||||
|
clear = true
|
||||||
|
install_crate = { crate_name = "trunk", binary = "trunk" }
|
||||||
|
command = "trunk"
|
||||||
|
args = ["serve"]
|
||||||
1
template/web/css/uikit.min.css
vendored
Normal file
1
template/web/css/uikit.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
13
template/web/index.html
Normal file
13
template/web/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>{{project-name | capitalize}}</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="stylesheet" href="css/uikit.min.css" />
|
||||||
|
<script src="js/uikit.min.js"></script>
|
||||||
|
<script src="js/uikit-icons.min.js"></script>
|
||||||
|
<link data-trunk rel="copy-dir" href="css" />
|
||||||
|
<link data-trunk rel="copy-dir" href="js" />
|
||||||
|
</head>
|
||||||
|
</html>
|
||||||
3
template/web/js/api.js
Normal file
3
template/web/js/api.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export function get_api_base_url() {
|
||||||
|
return "http://localhost:8000/api";
|
||||||
|
}
|
||||||
1
template/web/js/uikit-icons.min.js
vendored
Normal file
1
template/web/js/uikit-icons.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
template/web/js/uikit.min.js
vendored
Normal file
1
template/web/js/uikit.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
template/web/src/components/mod.rs
Normal file
1
template/web/src/components/mod.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod object;
|
||||||
23
template/web/src/components/object.rs
Normal file
23
template/web/src/components/object.rs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
use {{crate_name}};
|
||||||
|
use uikit_rs as uk;
|
||||||
|
use yew::{classes, function_component, html, Properties};
|
||||||
|
|
||||||
|
#[derive(Properties, PartialEq)]
|
||||||
|
pub struct ObjectProps {
|
||||||
|
#[prop_or_default]
|
||||||
|
pub object: Option<{{crate_name}}::Object>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component(Object)]
|
||||||
|
pub fn object(ObjectProps { object }: &ObjectProps) -> Html {
|
||||||
|
html! {
|
||||||
|
<span class={classes!(uk::Text::Meta)}>
|
||||||
|
{
|
||||||
|
match object {
|
||||||
|
Some(obj) => format!("object: {}", obj.name ),
|
||||||
|
None => "No object".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
46
template/web/src/lib.rs
Normal file
46
template/web/src/lib.rs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
use components::object;
|
||||||
|
use ::{{crate_name}}::Object;
|
||||||
|
use reqwasm::http::Request;
|
||||||
|
use uikit_rs as uk;
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
use yew::prelude::*;
|
||||||
|
|
||||||
|
mod components;
|
||||||
|
|
||||||
|
#[wasm_bindgen(module = "/js/api.js")]
|
||||||
|
extern "C" {
|
||||||
|
fn get_api_base_url() -> String;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component(App)]
|
||||||
|
pub fn app() -> Html {
|
||||||
|
let object = use_state(|| None);
|
||||||
|
{
|
||||||
|
let object = object.clone();
|
||||||
|
use_effect_with_deps(
|
||||||
|
move |_| {
|
||||||
|
wasm_bindgen_futures::spawn_local(async move {
|
||||||
|
let fetched_object: Object =
|
||||||
|
Request::get(&format!("{}/hello", get_api_base_url()))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap() // TODO
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.unwrap(); // TODO
|
||||||
|
object.set(Some(fetched_object));
|
||||||
|
});
|
||||||
|
|| ()
|
||||||
|
},
|
||||||
|
(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<uk::Section style={uk::SectionStyle::Default}>
|
||||||
|
<uk::Container size={uk::ContainerSize::Small}>
|
||||||
|
<object::Object object={(*object).clone()} />
|
||||||
|
</uk::Container>
|
||||||
|
</uk::Section>
|
||||||
|
}
|
||||||
|
}
|
||||||
5
template/web/src/main.rs
Normal file
5
template/web/src/main.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
use {{crate_name}}_web::App;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
yew::start_app::<App>();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user