Skip to content

Commit

Permalink
feat: split CLI and library into separate crates (#166)
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 authored Jan 6, 2024
1 parent 162ef19 commit c09efd0
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 c09efd0

Please sign in to comment.