Skip to content

Commit

Permalink
feat: split CLI and library into separate crates
Browse files Browse the repository at this point in the history
To allow for using the core business logic here outside of the CLI (eg.
integration with `-Zscript`).
  • Loading branch information
johnallen3d committed Jan 6, 2024
1 parent 162ef19 commit 6d1662e
Show file tree
Hide file tree
Showing 21 changed files with 881 additions and 677 deletions.
739 changes: 305 additions & 434 deletions Cargo.lock

Large diffs are not rendered by default.

34 changes: 13 additions & 21 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,29 +1,21 @@
[package]
name = "conditions"
[workspace]
resolver = "2"

members = ["cli", "lib"]

[workspace.package]
version = "0.2.0"
edition = "2021"
authors = ["John Allen <[email protected]>"]
repository = "https://github.com/johnallen3d/conditions"
authors = ["John Allen <[email protected]"]
homepage = "https://github.com/johnallen3d/conditions"
repository = "https://github.com/johnallen3d/mp-cli.git"
description = "Fetch basic weather conditions for current or specified location"
keywords = ["weather"]
categories = ["command-line-utilities"]
readme = "README.md"

license = "MIT"

[dependencies]
clap = { version = "4.4.13", features = ["derive"] }
confy = "0.5.1"
env_logger = "0.10.1"
eyre = "0.6.11"
lazy_static = "1.4.0"
log = "0.4.20"
rustls = "0.21.9"
[workspace.dependencies]
eyre = "0.6.8"
serde = { version = "1", features = ["derive"] }
thiserror = "1.0.56"
ureq = { version = "2.9.1", features = ["json"] }
sqlx = { version = "0.7", features = ["macros", "runtime-tokio", "sqlite"] }
tokio = { version = "1.34", features = ["macros"] }

[dev-dependencies]
tempfile = "3.8.1"
thiserror = "1.0.50"
tokio = { version = "1.33", features = ["macros"] }
25 changes: 25 additions & 0 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[package]
name = "conditions-cli"
version = { workspace = true }
edition = { workspace = true }
authors = { workspace = true }
homepage = { workspace = true }
repository = { workspace = true }
description = "CLI tootl to fetch basic weather conditions for current or specified location"
keywords = { workspace = true }
categories = ["command-line-utilities", "weather"]
readme = { workspace = true }
license = { workspace = true }

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
clap = { version = "4.4.8", features = ["derive"] }
conditions = { path = "../lib" }
env_logger = "0.10.0"
eyre = { workspace = true }
log = "0.4.20"
serde = { workspace = true }
serde_json = "1.0.111"
thiserror = { workspace = true }
tokio = { workspace = true }
16 changes: 3 additions & 13 deletions src/args.rs → cli/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,20 +111,10 @@ pub enum Unit {
}

impl Unit {
#[must_use]
pub fn from_char(unit: char) -> Option<Self> {
match unit {
'c' => Some(Self::C),
'f' => Some(Self::F),
_ => None,
}
}

#[must_use]
pub fn as_char(&self) -> char {
pub fn to(self) -> conditions::Unit {
match self {
Unit::C => 'c',
Unit::F => 'f',
Unit::C => conditions::Unit::C,
Unit::F => conditions::Unit::F,
}
}
}
Expand Down
37 changes: 23 additions & 14 deletions src/lib.rs → cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,35 @@

use clap::Parser;

mod args;

use args::{
Command, Conditions, ConfigSubcommand, LocationSubcommand, UnitSubcommand,
WeatherApiKeySubcommand,
};
use conditions::{cache::Cache, config::Config};

use cache::Cache;
use config::Config;
#[tokio::main(flavor = "current_thread")]
async fn main() {
env_logger::init();

pub(crate) mod api;
pub mod args;
mod cache;
mod conditions;
mod config;
pub mod icons;
mod location;
mod weather;
match run().await {
Ok(result) => println!("{result}"),
Err(err) => {
// for user
eprintln!("{err}");
// for development
log::error!("{:?}", err);
}
}
}

/// Entry point for running the application logic based on parsed CLI arguments.
///
/// # Returns
///
/// - `Ok(String)`: A success message or relevant output for the executed command
/// - `Ok(String)`: A success message or relevant output for the executed
/// command
/// - `Err(eyre::Error)`: An error if any step in the execution fails.
///
/// # Errors
Expand All @@ -44,9 +51,11 @@ pub async fn run() -> eyre::Result<String> {
Command::Current { region } => {
let (config, mut cache) = init().await?;

conditions::Conditions::new(config, region.clone())
let output = conditions::Conditions::new(config, region.clone())
.fetch(&mut cache)
.await?
.await?;

serde_json::to_string(&output)?
}
Command::Location(cmd) => match &cmd.command {
LocationSubcommand::Set(input) => {
Expand All @@ -73,7 +82,7 @@ pub async fn run() -> eyre::Result<String> {
WeatherApiKeySubcommand::Unset => Config::unset_weatherapi_token()?,
},
Command::Unit(cmd) => match &cmd.command {
UnitSubcommand::Set(unit) => Config::set_unit(unit.unit)?,
UnitSubcommand::Set(unit) => Config::set_unit(unit.unit.to())?,
UnitSubcommand::View => {
format!("unit stored as: {}", Config::load()?.unit)
}
Expand Down
28 changes: 28 additions & 0 deletions lib/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
[package]
name = "conditions"
version = { workspace = true }
edition = { workspace = true }
authors = { workspace = true }
homepage = { workspace = true }
repository = { workspace = true }
description = "Fetch basic weather conditions for current or specified location"
keywords = { workspace = true }
categories = ["weather"]
readme = { workspace = true }
license = { workspace = true }

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
confy = "0.5.1"
eyre = { workspace = true }
lazy_static = "1.4.0"
rustls = "0.22.1"
sqlx = { version = "0.7", features = ["macros", "runtime-tokio", "sqlite"] }
thiserror = { workspace = true }
ureq = { version = "2.8.0", features = ["json"] }
serde = { workspace = true }
tokio = { workspace = true }

[dev-dependencies]
tempfile = "3.8.1"
4 changes: 2 additions & 2 deletions src/api.rs → lib/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ use serde::Deserialize;
/// The `Fetchable` trait provides a generalized interface for making basic HTTP
/// requests.
///
/// Implementations are expected to specify the `URL` constant and can optionally
/// provide custom query parameters.
/// Implementations are expected to specify the `URL` constant and can
/// optionally provide custom query parameters.
///
/// Types `T` and `U` are involved in deserialization and conversion.
/// - `T` is the type that the HTTP response can be deserialized into.
Expand Down
65 changes: 65 additions & 0 deletions src/cache.rs → lib/src/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,29 @@ pub struct Cache {
}

impl Cache {
/// Creates a new instance of the `Cache` struct.
///
/// # Arguments
///
/// * `path` - An optional `String` representing the path to the `SQLite`
/// database file. If `Some`, the provided path will be used. If `None`, the
/// `DATABASE_URL` environment variable will be used as the path. If the
/// `DATABASE_URL` environment variable is not set, an `eyre::Report` error
/// will be returned.
///
/// # Returns
///
/// Returns a `Result` containing the newly created `Cache` instance on
/// success, or an `eyre::Report` error on failure.
///
/// # Errors
///
/// This function can return the following errors:
///
/// * `eyre::Report` - If the `DATABASE_URL` environment variable is not set
/// and no `path` is provided.
/// * `sqlx::Error` - If there is an error connecting to the `SQLite`
/// database or executing the SQL query to create the `cache` table.
pub async fn new(path: Option<String>) -> eyre::Result<Self> {
let path = match path {
Some(path) => path,
Expand Down Expand Up @@ -40,6 +63,31 @@ impl Cache {
Ok(Self { connection })
}

/// Caches the provided location.
///
/// This function takes a mutable reference to `self` (the cache) and a
/// reference to a `Location` struct.
/// It inserts or updates the location in the cache table based on the
/// postal code.
///
/// # Arguments
///
/// * `location` - A reference to a `Location` struct containing the details
/// of the location to be inserted or updated.
///
/// # Returns
///
/// This function returns a `Result` indicating success or failure. If the
/// operation is successful, it returns `Ok(())`.
/// If an error occurs during the execution of the SQL query or the database
/// connection, it returns an `eyre::Result` with the error details.
///
/// # Errors
///
/// This function can raise an error if there is an issue with the SQL query
/// execution or the database connection.
/// The specific error type is `eyre::Result`, which provides a flexible way
/// to handle and propagate errors.
pub async fn set(&mut self, location: &Location) -> eyre::Result<()> {
let query = r"
INSERT INTO cache (
Expand Down Expand Up @@ -73,6 +121,23 @@ impl Cache {
Ok(())
}

/// Retrieves a location from the cache based on the provided postal code.
///
/// # Arguments
///
/// * `postal_code` - A string representing the postal code of the location
/// to retrieve.
///
/// # Returns
///
/// * `Result<Option<Location>, eyre::Report>` - A result that contains an
/// optional `Location` if found in the cache, or an `eyre::Report` if an
/// error occurred.
///
/// # Errors
///
/// This function can return an `eyre::Report` if there was an error
/// executing the database query or fetching the location from the cache.
pub async fn get(
&mut self,
postal_code: &str,
Expand Down
41 changes: 29 additions & 12 deletions src/conditions.rs → lib/src/conditions.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
use serde::Serialize;

use crate::{
args::Unit, cache::Cache, config::Config, location,
weather::CurrentConditions,
cache::Cache, config::Config, location, weather::CurrentConditions,
};

use crate::Unit;

#[derive(Debug, Serialize)]
struct Output {
temp: i32,
icon: String,
pub struct Output {
pub temp: i32,
pub icon: String,
}

pub struct Conditions {
Expand All @@ -17,17 +18,33 @@ pub struct Conditions {
}

impl Conditions {
#[must_use]
pub fn new(config: Config, region: Option<String>) -> Self {
Self { config, region }
}

/// Fetch the current weather conditions given supplied configuration.
/// Fetches current weather conditions based on the provided configuration
/// and location.
///
/// # Arguments
///
/// * `cache` - A mutable reference to the cache object used for storing
/// location data.
///
/// # Returns
///
/// Returns a `Result` containing the fetched weather conditions as an
/// `Output` struct on success, or an `eyre::Error` on failure.
///
/// # Errors
///
/// This function can return an `eyre::Error` if any of the following
/// conditions are met:
///
/// - use configured location or infer location via IP
/// - retrieve wather conditions from weather provider(s) for location
/// - compose output structure
/// - convert output to JSON and return
pub async fn fetch(&mut self, cache: &mut Cache) -> eyre::Result<String> {
/// * The location retrieval from the cache fails.
/// * The location retrieval from the configuration fails.
/// * The retrieval of current weather conditions fails.
pub async fn fetch(&mut self, cache: &mut Cache) -> eyre::Result<Output> {
let location = if let Some(region) = &self.region {
location::get(cache, Some(region)).await?
} else {
Expand All @@ -37,7 +54,7 @@ impl Conditions {
let conditions = CurrentConditions::get(&self.config, &location)?;
let output = self.to_output(conditions);

Ok(ureq::serde_json::to_string(&output)?)
Ok(output)
}

fn to_output(&self, conditions: CurrentConditions) -> Output {
Expand Down
Loading

0 comments on commit 6d1662e

Please sign in to comment.