diff --git a/.sqlx/query-4bd6bbade521cd577279e91d8a8b978748046beff031d153699b351089c3bf9b.json b/.sqlx/query-4bd6bbade521cd577279e91d8a8b978748046beff031d153699b351089c3bf9b.json new file mode 100644 index 0000000..e3d16e3 --- /dev/null +++ b/.sqlx/query-4bd6bbade521cd577279e91d8a8b978748046beff031d153699b351089c3bf9b.json @@ -0,0 +1,17 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO subscriptions (id, email, name, subscribed_at)\n VALUES ($1, $2, $3, $4)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Text", + "Text", + "Timestamptz" + ] + }, + "nullable": [] + }, + "hash": "4bd6bbade521cd577279e91d8a8b978748046beff031d153699b351089c3bf9b" +} diff --git a/Dockerfile.production b/Dockerfile.production new file mode 100644 index 0000000..1484b84 --- /dev/null +++ b/Dockerfile.production @@ -0,0 +1,20 @@ +# We use the latest Rust stable release as base image +FROM rust:1.78.0 +# Let's switch our working directory to `app` (equivalent to `cd app`) +# The `app` folder will be created for us by Docker in case it does not +# exist already. +WORKDIR /app +# Install the required system dependencies for our linking configuration +RUN apt update && apt install lld clang -y + +# Copy all files from our working environment to our Docker image +COPY . . +# Let's build our binary! +# We'll use the release profile to make it faaaast +ENV SQLX_OFFLINE true +RUN cargo build --release + +ENV APP_ENVIRONMENT production + +# When `docker run` is executed, launch the binary! +ENTRYPOINT ["./target/release/email_newsletter_api"] diff --git a/README.md b/README.md index b6e141e..7f048bf 100644 --- a/README.md +++ b/README.md @@ -9,4 +9,12 @@ - Run `cargo watch -x check -x test -x run` to lint, test and run the binary as soon as you make a change to the file. - Bonus: install and use `mold`, a very fast linker that can link your Rust binary _blazingly fast_. +## Notable Dependencies + +- `actix-web`: Most popular Rust web framework +- `serde`: Data structure serialization/deserialization +- `tokio`: Async Runtime +- `tracing`: Alternative to traditional logging +- `sqlx`: SQL toolkit for Rust. Offers compile-time SQL checked queries + ## [Technical Write Up](./docs/technical_write_up.md) diff --git a/configuration.yaml b/configuration/base.yaml similarity index 61% rename from configuration.yaml rename to configuration/base.yaml index 85a79ac..62d213c 100644 --- a/configuration.yaml +++ b/configuration/base.yaml @@ -1,6 +1,8 @@ -application_port: 8000 +application: + port: 8000 + host: 0.0.0.0 database: - host: "127.0.0.1" + host: "localhost" port: 5432 username: "postgres" password: "password" diff --git a/configuration/local.yaml b/configuration/local.yaml new file mode 100644 index 0000000..c464c2f --- /dev/null +++ b/configuration/local.yaml @@ -0,0 +1,2 @@ +application: + host: 127.0.0.1 diff --git a/configuration/production.yaml b/configuration/production.yaml new file mode 100644 index 0000000..b936a88 --- /dev/null +++ b/configuration/production.yaml @@ -0,0 +1,2 @@ +application: + host: 0.0.0.0 diff --git a/docs/database.md b/docs/database.md index 3eaf362..176071d 100644 --- a/docs/database.md +++ b/docs/database.md @@ -3,3 +3,8 @@ ## SQLx The SQLx library will run compile time checks to make sure our SQL queries are valid. This is done by running PostgreSQL queries during compile time. Therefore, it is important that DATABASE_URL must be properly set. + +### Offline mode vs Online mode + +- Online mode is when the database is up and running and therefore, `SQLx` can perform compile time SQL queries check against it. +- Offline mode is when the database is NOT up and running. But we can save query metadata for offline usage and build to let the app run without SQLx complaining. diff --git a/src/configuration.rs b/src/configuration.rs index 1d76ee8..907ead9 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -3,7 +3,13 @@ use secrecy::{ExposeSecret, Secret}; #[derive(serde::Deserialize)] pub struct Settings { pub database: DatabaseSettings, - pub application_port: u16, + pub application: ApplicationSettings, +} + +#[derive(serde::Deserialize)] +pub struct ApplicationSettings { + pub port: u16, + pub host: String, } #[derive(serde::Deserialize)] @@ -16,16 +22,56 @@ pub struct DatabaseSettings { } pub fn get_configuration() -> Result { + let base_path = std::env::current_dir().expect("Failed to determine the current directory"); + let configuration_directory = base_path.join("configuration"); + + let environment: Environment = std::env::var("APP_ENVIRONMENT") + .unwrap_or_else(|_| "local".into()) + .try_into() + .expect("Failed to parse APP_ENVIRONMENT."); + + let environment_filename = format!("{}.yaml", environment.as_str()); + let settings = config::Config::builder() - .add_source(config::File::new( - "configuration.yaml", - config::FileFormat::Yaml, + .add_source(config::File::from( + configuration_directory.join("base.yaml"), + )) + .add_source(config::File::from( + configuration_directory.join(environment_filename), )) .build()?; settings.try_deserialize::() } +/// The possible runtime environment for our application. +pub enum Environment { + Local, + Production, +} +impl Environment { + pub fn as_str(&self) -> &'static str { + match self { + Environment::Local => "local", + Environment::Production => "production", + } + } +} +impl TryFrom for Environment { + type Error = String; + fn try_from(s: String) -> Result { + match s.to_lowercase().as_str() { + "local" => Ok(Self::Local), + "production" => Ok(Self::Production), + other => Err(format!( + "{} is not a supported environment. \ +Use either `local` or `production`.", + other + )), + } + } +} + impl DatabaseSettings { pub fn connection_string(&self) -> Secret { Secret::new(format!( diff --git a/src/main.rs b/src/main.rs index 90c4bd4..f033756 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,14 +16,19 @@ async fn main() -> Result<(), std::io::Error> { ); init_subscriber(subscriber); - let db_conn = PgPool::connect(configuration.database.connection_string().expose_secret()) - .await + let db_conn = PgPool::connect_lazy(configuration.database.connection_string().expose_secret()) .expect("Failed to connect to PostgreSQL"); - let port_number = configuration.application_port; - - let listener = TcpListener::bind(format!("127.0.0.1:{}", port_number)) - .unwrap_or_else(|_| panic!("Can't bind to port {} at localhost", port_number)); + let listener = TcpListener::bind(format!( + "{}:{}", + configuration.application.host, configuration.application.port + )) + .unwrap_or_else(|_| { + panic!( + "Can't bind to port {} at localhost", + configuration.application.port + ) + }); // Move the error up the call stack // otherwise await for the HttpServer