chore: bootstrap rust service
This commit is contained in:
47
.github/workflows/rust.yml
vendored
Normal file
47
.github/workflows/rust.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
name: Rust
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_PROFILE_DEV_DEBUG: line-tables-only
|
||||||
|
CARGO_PROFILE_TEST_DEBUG: line-tables-only
|
||||||
|
CARGO_INCREMENTAL: 0
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||||
|
|
||||||
|
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
|
||||||
|
with:
|
||||||
|
components: clippy, rustfmt
|
||||||
|
|
||||||
|
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
|
||||||
|
|
||||||
|
- name: cargo fmt
|
||||||
|
run: cargo fmt --all --check
|
||||||
|
|
||||||
|
- name: cargo clippy
|
||||||
|
run: cargo clippy --locked --all-targets -- -D warnings
|
||||||
|
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||||
|
|
||||||
|
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
|
||||||
|
|
||||||
|
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
|
||||||
|
|
||||||
|
- name: cargo test
|
||||||
|
run: cargo test --locked
|
||||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
/target/
|
||||||
|
.env
|
||||||
|
.DS_Store
|
||||||
|
.coding-challenge-plan.md
|
||||||
2111
Cargo.lock
generated
Normal file
2111
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
36
Cargo.toml
Normal file
36
Cargo.toml
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
[package]
|
||||||
|
name = "pokedex-api"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
license = "MIT"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
axum = { version = "0.8", features = ["json", "macros"] }
|
||||||
|
opentelemetry = "0.31"
|
||||||
|
opentelemetry-prometheus-text-exporter = "0.2"
|
||||||
|
opentelemetry_sdk = { version = "0.31", features = ["metrics"] }
|
||||||
|
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
thiserror = "2"
|
||||||
|
tokio = { version = "1", features = ["macros", "net", "rt-multi-thread", "signal"] }
|
||||||
|
tower = { version = "0.5", features = ["util"] }
|
||||||
|
tower-http = { version = "0.6", features = ["request-id", "trace"] }
|
||||||
|
tracing = "0.1"
|
||||||
|
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
|
||||||
|
uuid = { version = "1", features = ["v4"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
http-body-util = "0.1"
|
||||||
|
wiremock = "0.6"
|
||||||
|
|
||||||
|
[lints.rust]
|
||||||
|
unsafe_code = "forbid"
|
||||||
|
|
||||||
|
[lints.clippy]
|
||||||
|
all = { level = "deny", priority = -1 }
|
||||||
|
pedantic = { level = "deny", priority = -1 }
|
||||||
|
nursery = { level = "deny", priority = -1 }
|
||||||
|
cargo = { level = "deny", priority = -1 }
|
||||||
|
multiple_crate_versions = { level = "allow", priority = 1 }
|
||||||
76
src/config.rs
Normal file
76
src/config.rs
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
use std::{env, net::SocketAddr, time::Duration};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct AppConfig {
|
||||||
|
pub bind_addr: SocketAddr,
|
||||||
|
pub pokeapi_base_url: String,
|
||||||
|
pub translations_base_url: String,
|
||||||
|
pub request_timeout: Duration,
|
||||||
|
pub rate_limit_per_second: f64,
|
||||||
|
pub rate_limit_burst: u32,
|
||||||
|
pub service_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppConfig {
|
||||||
|
pub fn from_env() -> Result<Self, ConfigError> {
|
||||||
|
Ok(Self {
|
||||||
|
bind_addr: env_or_parse("BIND_ADDR", default_bind_addr())?,
|
||||||
|
pokeapi_base_url: env_or_string("POKEAPI_BASE_URL", "https://pokeapi.co/api/v2"),
|
||||||
|
translations_base_url: env_or_string(
|
||||||
|
"FUN_TRANSLATIONS_BASE_URL",
|
||||||
|
"https://funtranslations.mercxry.me",
|
||||||
|
),
|
||||||
|
request_timeout: Duration::from_secs(env_or_parse("REQUEST_TIMEOUT_SECONDS", 5)?),
|
||||||
|
rate_limit_per_second: env_or_parse("RATE_LIMIT_PER_SECOND", 20.0)?,
|
||||||
|
rate_limit_burst: env_or_parse("RATE_LIMIT_BURST", 40)?,
|
||||||
|
service_name: env_or_string("OTEL_SERVICE_NAME", env!("CARGO_PKG_NAME")),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum ConfigError {
|
||||||
|
#[error("invalid value for {name}: {source}")]
|
||||||
|
InvalidValue {
|
||||||
|
name: &'static str,
|
||||||
|
source: Box<dyn std::error::Error + Send + Sync>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fn env_or_string(name: &'static str, default: &str) -> String {
|
||||||
|
env::var(name).unwrap_or_else(|_| default.to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_bind_addr() -> SocketAddr {
|
||||||
|
SocketAddr::from(([0, 0, 0, 0], 5000))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn env_or_parse<T>(name: &'static str, default: T) -> Result<T, ConfigError>
|
||||||
|
where
|
||||||
|
T: std::str::FromStr,
|
||||||
|
T::Err: std::error::Error + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
env::var(name).map_or_else(
|
||||||
|
|_| Ok(default),
|
||||||
|
|value| {
|
||||||
|
value.parse().map_err(|source| ConfigError::InvalidValue {
|
||||||
|
name,
|
||||||
|
source: Box::new(source),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::AppConfig;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_config_is_valid_for_local_development() {
|
||||||
|
let config = AppConfig::from_env().expect("default env should parse");
|
||||||
|
|
||||||
|
assert_eq!(config.bind_addr.port(), 5000);
|
||||||
|
assert_eq!(config.pokeapi_base_url, "https://pokeapi.co/api/v2");
|
||||||
|
assert!((config.rate_limit_per_second - 20.0).abs() < f64::EPSILON);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/lib.rs
Normal file
10
src/lib.rs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
#![deny(warnings)]
|
||||||
|
#![deny(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
|
||||||
|
#![allow(clippy::missing_errors_doc, clippy::multiple_crate_versions)]
|
||||||
|
|
||||||
|
pub mod config;
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn crate_name() -> &'static str {
|
||||||
|
env!("CARGO_PKG_NAME")
|
||||||
|
}
|
||||||
11
src/main.rs
Normal file
11
src/main.rs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
#![deny(warnings)]
|
||||||
|
#![deny(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
|
||||||
|
#![allow(clippy::missing_errors_doc, clippy::multiple_crate_versions)]
|
||||||
|
|
||||||
|
use pokedex_api::{config::AppConfig, crate_name};
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let _config = AppConfig::from_env()?;
|
||||||
|
println!("{name} bootstrap complete", name = crate_name());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user