Skip to content

Commit f38735b

Browse files
committed
Update dependencies, enhance API structure, and improve error handling
- Updated various dependencies in Cargo.toml for better performance and security. - Refactored API structure to improve modularity and maintainability, including the addition of new endpoints for Spotify real-time data and enhanced error handling. - Improved README documentation to reflect recent changes and provide clearer usage examples. - Introduced a consistent error handling mechanism across the application. - Enhanced the organization of modules for better clarity and separation of concerns.
1 parent 333fbef commit f38735b

18 files changed

Lines changed: 2795 additions & 1098 deletions

File tree

Cargo.lock

Lines changed: 1858 additions & 790 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,28 @@
22
name = "null-api"
33
version = "0.1.0"
44
edition = "2021"
5-
author = "Stephen F<stephen@thenull.dev>"
5+
authors = ["Stephen F <stephen@thenull.dev>"]
66

77
[dependencies]
88
# Runtime
9-
tokio = { version = "1", features = ["full"] }
9+
tokio = { version = "1.40", features = ["full"] }
1010
# Environment
11-
dotenvy = "0.15.6"
12-
env_logger = "0.10.0"
11+
dotenvy = "0.15.7"
12+
env_logger = "0.11.0"
1313
envconfig = "0.10.0"
14-
log = "0.4"
14+
log = "0.4.21"
1515
# HTTP Libs
16-
actix-web = "4"
17-
reqwest = "0.11.13"
16+
actix-web = "4.5.0"
17+
reqwest = { version = "0.12", features = ["json"] }
1818
gql_client = "1.0.7"
1919
# Json
20-
serde = {version = "1.0.147", features= ["derive"] }
21-
serde_json = "1.0.89"
22-
serde_urlencoded = "0.7"
20+
serde = { version = "1.0.217", features = ["derive"] }
21+
serde_json = "1.0.133"
22+
serde_urlencoded = "0.7.1"
2323

2424
# Database
25-
redis = {version ="0.22.1", features = ["tokio-comp", "connection-manager"]}
26-
sqlx = { version = "0.6", features = [ "runtime-tokio-rustls", "postgres", "chrono" ] }
25+
redis = { version = "0.24", features = ["tokio-comp", "connection-manager"] }
26+
sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "postgres", "chrono"] }
27+
28+
# Date/Time
29+
chrono = { version = "0.4", features = ["serde"] }

README.md

Lines changed: 77 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@
5555
<li>
5656
<a href="#about-the-project">About The Project</a>
5757
<ul>
58+
<li><a href="#project-structure">Project Structure</a></li>
59+
<li><a href="#recent-improvements">Recent Improvements</a></li>
5860
<li><a href="#built-with">Built With</a></li>
5961
</ul>
6062
</li>
@@ -77,16 +79,56 @@
7779
<!-- ABOUT THE PROJECT -->
7880
## About The Project
7981

80-
This is a project to track stats on services I use
82+
This is a Rust-based API for collecting and tracking personal statistics from various services including Spotify, Duolingo, and GitHub. The API provides endpoints to retrieve real-time data and analytics for dashboard visualization.
83+
84+
### Project Structure
85+
86+
The API follows a clean modular architecture with each service having its own dedicated module:
87+
88+
```
89+
src/modules/
90+
├── duolingo/
91+
│ ├── entity.rs # Data structures for Duolingo API responses
92+
│ ├── handler.rs # HTTP request handlers and endpoints
93+
│ ├── manager.rs # Business logic and API interactions
94+
│ └── mod.rs # Module exports and public API
95+
├── spotify/
96+
│ ├── entity.rs # Data structures for Spotify API responses
97+
│ ├── handler.rs # HTTP request handlers and endpoints
98+
│ ├── manager.rs # Business logic and API interactions
99+
│ └── mod.rs # Module exports and public API
100+
└── github/
101+
├── entity.rs # Data structures for GitHub API responses
102+
├── handler.rs # HTTP request handlers and endpoints
103+
├── manager.rs # Business logic and API interactions
104+
└── mod.rs # Module exports and public API
105+
```
106+
107+
Each module follows a consistent pattern:
108+
- **Entity**: Defines data structures and types for API responses
109+
- **Handler**: Contains HTTP route handlers and request/response logic
110+
- **Manager**: Implements business logic, API calls, and data management
111+
- **Mod**: Provides clean module exports and public API surface
112+
113+
### Recent Improvements
114+
115+
The project has been recently refactored to improve code organization and maintainability:
116+
117+
- **Separated Manager Logic**: Manager implementations have been moved from `mod.rs` files into dedicated `manager.rs` files
118+
- **Cleaner Module Structure**: Each `mod.rs` now only contains module declarations and re-exports, making them much more readable
119+
- **Better Separation of Concerns**: Business logic is now clearly separated from module organization
120+
- **Improved Maintainability**: Each manager's implementation is now in its own focused file, making it easier to maintain and extend
81121

82122
<p align="right">(<a href="#readme-top">back to top</a>)</p>
83123

84124
### Built With
85125

86-
* Actix-Web
87-
* SQLx
88-
* Redis
89-
* Reqwest
126+
* **[Rust](https://www.rust-lang.org/)** - Systems programming language for performance and safety
127+
* **[Actix-Web](https://actix.rs/)** - High-performance web framework for Rust
128+
* **[SQLx](https://github.com/launchbadge/sqlx)** - Async SQL toolkit with compile-time checked queries
129+
* **[Redis](https://redis.io/)** - In-memory data structure store for caching
130+
* **[Reqwest](https://github.com/seanmonstar/reqwest)** - HTTP client library for making API requests
131+
* **[Serde](https://serde.rs/)** - Serialization framework for converting data structures
90132

91133
<p align="right">(<a href="#readme-top">back to top</a>)</p>
92134

@@ -139,8 +181,34 @@ This is an example of how to list things you need to use the software and how to
139181
<!-- USAGE EXAMPLES -->
140182
## Usage
141183

142-
This is mainly used to track my personal stats to display on a dashboard and for learning.
184+
This API is designed to collect and aggregate personal statistics from various services for dashboard visualization and analytics. Each module provides specific functionality:
185+
186+
### Available Modules
187+
188+
- **Duolingo Module**: Tracks language learning progress, streak data, and user statistics
189+
- **Spotify Module**: Retrieves currently playing tracks, user playlists, top artists/tracks, and listening analytics
190+
- **GitHub Module**: Monitors repository activity, GitHub Actions runners, and organization statistics
191+
192+
### API Endpoints
193+
194+
The API provides RESTful endpoints for each service module, allowing you to:
195+
- Retrieve real-time data from external APIs
196+
- Cache frequently accessed data using Redis
197+
- Aggregate statistics for dashboard visualization
198+
- Monitor service health and availability
199+
200+
### Example Usage
201+
202+
```bash
203+
# Get Spotify currently playing track
204+
GET /spotify/current
205+
206+
# Get Duolingo user stats
207+
GET /duolingo/stats/{username}
143208
209+
# Get GitHub runners status
210+
GET /github/runners
211+
```
144212

145213
<p align="right">(<a href="#readme-top">back to top</a>)</p>
146214

@@ -150,7 +218,10 @@ This is mainly used to track my personal stats to display on a dashboard and for
150218
- [x] Spotify Stats
151219
- [x] Duolingo Stats
152220
- [x] Github Stats
221+
- [x] Modular Architecture Refactoring
153222
- [ ] Waka Stats
223+
- [ ] Enhanced Error Handling
224+
- [ ] API Documentation with OpenAPI/Swagger
154225

155226
See the [open issues](https://github.com/thenulldev/api/issues) for a full list of proposed features (and known issues).
156227

src/client.rs

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,16 @@ use tokio::sync::Mutex;
66
use crate::{
77
config::Config,
88
db::{postgres::PostgresManager, redis::RedisManager},
9+
error::AppError,
910
modules::{
1011
default,
1112
duolingo::handler::get_duo_user,
1213
github::handler::{repos, runners},
1314
health, index,
14-
spotify::handler::{authorize, callback, current},
15+
spotify::handler::{
16+
authorize, callback, current, devices, playlists, queue,
17+
realtime_info, recently_played, top_artists, top_tracks
18+
},
1519
},
1620
};
1721

@@ -22,16 +26,17 @@ pub struct NullClient {
2226

2327
impl NullClient {
2428
// Start the Client
25-
pub async fn start() -> std::io::Result<()> {
29+
pub async fn start() -> Result<(), AppError> {
2630
// Init Redis
27-
let redis = RedisManager::new().await;
31+
let redis = RedisManager::new().await?;
2832
// Init Postgres
29-
let postgres = PostgresManager::new().await;
33+
let postgres = PostgresManager::new().await?;
3034

3135
// Store data in State
3236
let data = web::Data::new(Mutex::new(Self { postgres, redis }));
3337
// Load Client Config
34-
let config = Config::init_from_env().unwrap();
38+
let config = Config::init_from_env()?;
39+
3540
// Start HTTP Server
3641
let server = HttpServer::new(move || {
3742
let logger = Logger::default();
@@ -41,13 +46,14 @@ impl NullClient {
4146
.configure(Self::init)
4247
.app_data(web::Data::clone(&data))
4348
})
44-
.bind(((config.listen_host).to_owned(), config.listen_port))?;
49+
.bind((config.listen_host.clone(), config.listen_port))?;
50+
4551
info!(
46-
"Connected and listening on http://{}:{}",
47-
(config.listen_host).to_owned(),
48-
config.listen_port
52+
"Server started and listening on http://{}:{}",
53+
config.listen_host, config.listen_port
4954
);
50-
server.run().await
55+
56+
server.run().await.map_err(AppError::from)
5157
}
5258

5359
// Initialize Services
@@ -59,6 +65,13 @@ impl NullClient {
5965
cfg.service(current);
6066
cfg.service(authorize);
6167
cfg.service(callback);
68+
cfg.service(realtime_info);
69+
cfg.service(devices);
70+
cfg.service(queue);
71+
cfg.service(top_tracks);
72+
cfg.service(top_artists);
73+
cfg.service(recently_played);
74+
cfg.service(playlists);
6275
// Github
6376
cfg.service(runners);
6477
cfg.service(repos);

src/db/postgres.rs

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,23 @@
1-
// let conn = Database::connect(&db_url).await.unwrap();
2-
31
use envconfig::Envconfig;
42
use log::info;
53
use sqlx::{postgres::PgPoolOptions, Pool, Postgres};
64

7-
use crate::config::Config;
5+
use crate::{config::Config, error::AppError};
86

97
pub struct PostgresManager {
10-
pub pm: Pool<Postgres>,
8+
pub pool: Pool<Postgres>,
119
}
1210

1311
impl PostgresManager {
14-
pub async fn new() -> Self {
15-
let config = Config::init_from_env().unwrap();
12+
pub async fn new() -> Result<Self, AppError> {
13+
let config = Config::init_from_env()?;
1614

1715
let pool = PgPoolOptions::new()
1816
.max_connections(5)
1917
.connect(&config.db_url)
20-
.await
21-
.unwrap();
18+
.await?;
2219

23-
info!("Connected to Database");
24-
Self { pm: pool }
20+
info!("Connected to PostgreSQL database");
21+
Ok(Self { pool })
2522
}
2623
}

src/db/redis.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
use envconfig::Envconfig;
22
use log::info;
33
use redis::{aio::ConnectionManager, Client};
4-
extern crate serde_json;
5-
use crate::config::Config;
4+
5+
use crate::{config::Config, error::AppError};
66

77
#[derive(Clone)]
88
pub struct RedisManager {
9-
pub cm: ConnectionManager,
9+
pub connection: ConnectionManager,
1010
}
1111

1212
impl RedisManager {
13-
pub async fn new() -> Self {
14-
let config = Config::init_from_env().unwrap();
15-
let client = Client::open(config.redis).unwrap();
16-
let cm = ConnectionManager::new(client).await.unwrap();
13+
pub async fn new() -> Result<Self, AppError> {
14+
let config = Config::init_from_env()?;
15+
let client = Client::open(config.redis)?;
16+
let connection = ConnectionManager::new(client).await?;
1717
info!("Connected to Redis");
18-
Self { cm }
18+
Ok(Self { connection })
1919
}
2020
}

src/error.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
use actix_web::{HttpResponse, ResponseError};
2+
use std::fmt;
3+
4+
#[derive(Debug)]
5+
pub enum AppError {
6+
DatabaseError(sqlx::Error),
7+
RedisError(redis::RedisError),
8+
ConfigError(envconfig::Error),
9+
HttpError(reqwest::Error),
10+
JsonError(serde_json::Error),
11+
UrlEncodedError(serde_urlencoded::ser::Error),
12+
IoError(std::io::Error),
13+
SpotifyError(String),
14+
}
15+
16+
impl fmt::Display for AppError {
17+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18+
match self {
19+
AppError::DatabaseError(err) => write!(f, "Database error: {}", err),
20+
AppError::RedisError(err) => write!(f, "Redis error: {}", err),
21+
AppError::ConfigError(err) => write!(f, "Configuration error: {}", err),
22+
AppError::HttpError(err) => write!(f, "HTTP error: {}", err),
23+
AppError::JsonError(err) => write!(f, "JSON error: {}", err),
24+
AppError::UrlEncodedError(err) => write!(f, "URL encoding error: {}", err),
25+
AppError::IoError(err) => write!(f, "IO error: {}", err),
26+
AppError::SpotifyError(msg) => write!(f, "Spotify error: {}", msg),
27+
}
28+
}
29+
}
30+
31+
impl ResponseError for AppError {
32+
fn error_response(&self) -> HttpResponse {
33+
HttpResponse::InternalServerError().json(serde_json::json!({
34+
"error": self.to_string()
35+
}))
36+
}
37+
}
38+
39+
// Conversion implementations
40+
impl From<sqlx::Error> for AppError {
41+
fn from(err: sqlx::Error) -> Self {
42+
AppError::DatabaseError(err)
43+
}
44+
}
45+
46+
impl From<redis::RedisError> for AppError {
47+
fn from(err: redis::RedisError) -> Self {
48+
AppError::RedisError(err)
49+
}
50+
}
51+
52+
impl From<envconfig::Error> for AppError {
53+
fn from(err: envconfig::Error) -> Self {
54+
AppError::ConfigError(err)
55+
}
56+
}
57+
58+
impl From<reqwest::Error> for AppError {
59+
fn from(err: reqwest::Error) -> Self {
60+
AppError::HttpError(err)
61+
}
62+
}
63+
64+
impl From<serde_json::Error> for AppError {
65+
fn from(err: serde_json::Error) -> Self {
66+
AppError::JsonError(err)
67+
}
68+
}
69+
70+
impl From<serde_urlencoded::ser::Error> for AppError {
71+
fn from(err: serde_urlencoded::ser::Error) -> Self {
72+
AppError::UrlEncodedError(err)
73+
}
74+
}
75+
76+
impl From<std::io::Error> for AppError {
77+
fn from(err: std::io::Error) -> Self {
78+
AppError::IoError(err)
79+
}
80+
}

0 commit comments

Comments
 (0)