Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
765 changes: 722 additions & 43 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion examples/config-file/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,3 @@ edition = "2021"
[dependencies]
sword = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
4 changes: 4 additions & 0 deletions examples/dependency-injection/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
POSTGRES_USER=user
POSTGRES_PASSWORD=password
POSTGRES_DB=postgres_db
POSTGRES_DATABASE_URL="postgres://user:password@localhost:5432/postgres_db"
4 changes: 2 additions & 2 deletions examples/dependency-injection/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ path = "src/main.rs"
[dependencies]
sword = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true }
sqlx = { version = "0.8.6", features = ["postgres", "runtime-tokio"] }
dotenv = "0.15.0"
39 changes: 39 additions & 0 deletions examples/dependency-injection/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@

# Dependency injection example with SQLx

To run this example, make sure you have Docker installed and running on your machine.

## Setup

1. Clone the Sword repository if you haven't already:

```bash
git clone https://github.com/sword-web/sword.git
cd sword/examples/dependency-injection
```

2. Run the PostgreSQL database using Docker Compose:

```bash
docker-compose up -d
```

3. Run the Sword application:

```bash
cargo run
```

## Endpoints

### List tasks

```bash
curl http://localhost:8080/tasks
```

### Create a new task (with default values)

```bash
curl -X POST http://localhost:8080/tasks
```
20 changes: 20 additions & 0 deletions examples/dependency-injection/compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
version: '3.8'

services:
postgres:
image: postgres:15
env_file:
- "./.env"
container_name: sword_postgres_example
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data

volumes:
postgres_data:
5 changes: 3 additions & 2 deletions examples/dependency-injection/config/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ port = 8080
body_limit = "10MB"
request_timeout_seconds = 15
graceful_shutdown = false
name = "MySwordApp"
name = "Dependency Injection Example"

[db-config]
collection_name = "tasks"
uri = "${POSTGRES_DATABASE_URL}"
migrations_path = "config/migrations"
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-- Add migration script here

CREATE TABLE tasks(
id INT PRIMARY KEY,
title TEXT NOT NULL
)
41 changes: 20 additions & 21 deletions examples/dependency-injection/src/database.rs
Original file line number Diff line number Diff line change
@@ -1,43 +1,42 @@
use std::{collections::HashMap, sync::Arc};
use std::{path::Path, sync::Arc};

use serde::Deserialize;
use serde_json::Value;
use sqlx::{migrate::Migrator, PgPool};
use sword::prelude::*;
use tokio::sync::RwLock;

pub type Store = Arc<RwLock<HashMap<String, Vec<Value>>>>;

#[derive(Clone, Deserialize)]
#[config(key = "db-config")]
pub struct DatabaseConfig {
collection_name: String,
uri: String,
migrations_path: String,
}

#[injectable(kind = "provider")]
#[injectable(provider)]
pub struct Database {
db: Store,
pool: Arc<PgPool>,
}

impl Database {
pub async fn new(db_conf: DatabaseConfig) -> Self {
let db = Arc::new(RwLock::new(HashMap::new()));

db.write().await.insert(db_conf.collection_name, Vec::new());
let pool = PgPool::connect(&db_conf.uri)
.await
.expect("Failed to create Postgres connection pool");

Self { db }
}
let migrator = Migrator::new(Path::new(&db_conf.migrations_path))
.await
.unwrap();

pub async fn insert(&self, table: &'static str, record: Value) {
let mut db = self.db.write().await;
migrator
.run(&pool)
.await
.expect("Failed to run database migrations");

if let Some(table_data) = db.get_mut(table) {
table_data.push(record);
Self {
pool: Arc::new(pool),
}
}

pub async fn get_all(&self, table: &'static str) -> Option<Vec<Value>> {
let db = self.db.read().await;

db.get(table).cloned()
pub fn get_pool(&self) -> &PgPool {
&self.pool
}
}
23 changes: 12 additions & 11 deletions examples/dependency-injection/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
mod database;
mod middleware;
mod repository;
mod service;

use std::sync::Arc;

use dotenv::dotenv;
pub use middleware::MyMiddleware;
pub use repository::TaskRepository;

use serde_json::json;
use sword::{core::DependencyContainer, prelude::*};

use crate::{
database::{Database, DatabaseConfig},
service::TasksService,
repository::Task,
};

#[controller("/tasks", version = "v1")]
#[controller("/tasks")]
struct TasksController {
tasks: Arc<TasksService>,
tasks: Arc<TaskRepository>,
}

#[routes]
Expand All @@ -33,12 +32,13 @@ impl TasksController {

#[post("/")]
async fn create_task(&self) -> HttpResponse {
let total_task = self.tasks.find_all().await.len();
let tasks = self.tasks.find_all().await;
let total_count = tasks.len() as i32 + 1;

let task = json!({
"id": total_task + 1,
"title": format!("Task {}", total_task + 1),
});
let task = Task {
id: total_count,
title: format!("Task {total_count}"),
};

self.tasks.create(task.clone()).await;

Expand All @@ -48,6 +48,8 @@ impl TasksController {

#[sword::main]
async fn main() {
dotenv().ok();

let app = Application::builder();
let db_config = app.config::<DatabaseConfig>().unwrap();

Expand All @@ -56,7 +58,6 @@ async fn main() {
let container = DependencyContainer::builder()
.register_provider(db)
.register_component::<TaskRepository>()
.register_component::<TasksService>()
.build();

let app = app
Expand Down
10 changes: 7 additions & 3 deletions examples/dependency-injection/src/middleware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,15 @@ impl OnRequest for MyMiddleware {
async fn on_request(&self, req: Request, next: Next) -> MiddlewareResult {
let tasks = self.tasks_repository.find_all().await;

println!();
println!("Current tasks:");

match tasks {
Some(tasks) => println!("{tasks:?}"),
None => println!("There's no tasks"),
if tasks.is_empty() {
println!("There's no tasks");
}

for task in tasks {
println!(" - [{}] {}", task.id, task.title);
}

next!(req, next)
Expand Down
31 changes: 22 additions & 9 deletions examples/dependency-injection/src/repository.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,35 @@
use std::sync::Arc;

use crate::database::{Database, DatabaseConfig};
use serde_json::Value;
use crate::database::Database;

use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use std::sync::Arc;
use sword::core::injectable;

#[derive(Clone, Serialize, Deserialize, FromRow)]
pub struct Task {
pub id: i32,
pub title: String,
}

#[injectable]
pub struct TaskRepository {
db_conf: DatabaseConfig,
db: Arc<Database>,
}

impl TaskRepository {
pub async fn create(&self, task: Value) {
self.db.insert("tasks", task).await;
pub async fn find_all(&self) -> Vec<Task> {
sqlx::query_as::<_, Task>("SELECT id, title FROM tasks")
.fetch_all(self.db.get_pool())
.await
.expect("Failed to fetch tasks")
}

pub async fn find_all(&self) -> Option<Vec<Value>> {
self.db.get_all("tasks").await
pub async fn create(&self, task: Task) {
sqlx::query("INSERT INTO tasks (id, title) VALUES ($1, $2)")
.bind(task.id)
.bind(task.title)
.execute(self.db.get_pool())
.await
.expect("Failed to insert task");
}
}
20 changes: 0 additions & 20 deletions examples/dependency-injection/src/service.rs

This file was deleted.

1 change: 0 additions & 1 deletion examples/middlewares/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,3 @@ edition = "2021"
sword = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
axum = { workspace = true }
2 changes: 1 addition & 1 deletion examples/middlewares/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ impl TestController {
}
}

#[injectable(kind = "provider")]
#[injectable(provider)]
pub struct Database {
pub connection_string: String,
}
Expand Down
25 changes: 9 additions & 16 deletions sword-macros/src/injectable/parse.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::shared::collect_struct_fields;
use proc_macro::TokenStream;
use syn::parse::{ParseStream, Result as ParseResult};
use syn::{Ident, ItemStruct, LitStr, Token, Type, parse::Parse};
use syn::{Ident, ItemStruct, Token, Type, parse::Parse};

pub enum InjectableKind {
Provider,
Expand Down Expand Up @@ -33,22 +33,15 @@ impl Parse for InjectableArgs {
let arg: Ident = input.parse()?;

match arg.to_string().as_str() {
"kind" => {
input.parse::<Token![=]>()?;
let val: LitStr = input.parse()?;
kind = match val.value().as_str() {
"provider" => InjectableKind::Provider,
"component" => InjectableKind::Component,
_ => {
return Err(syn::Error::new_spanned(
val,
"Expected 'provider' or 'component'",
));
}
};
}
"provider" => kind = InjectableKind::Provider,
"component" => kind = InjectableKind::Component,
"no_derive_clone" => derive_clone = false,
_ => return Err(syn::Error::new_spanned(arg, "Unknown attribute")),
_ => {
return Err(syn::Error::new_spanned(
arg,
"Unknown attribute. Use 'provider', 'component', or 'no_derive_clone'",
));
}
}

if !input.is_empty() {
Expand Down
6 changes: 3 additions & 3 deletions sword-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ pub fn config(attr: TokenStream, item: TokenStream) -> TokenStream {
/// - `no_derive_clone`: (Optional) If provided, the struct will not derive the `Clone` automatically.
/// By default, the struct will derive `Clone` if all its fields implement `Clone`.
///
/// ### Usage of `#[injectable]` without parameters
/// ### Usage of `#[injectable]` without parameters (same as #[injectable(component)])
///
/// ```rust,ignore
/// #[injectable]
Expand All @@ -329,10 +329,10 @@ pub fn config(attr: TokenStream, item: TokenStream) -> TokenStream {
/// }
/// ```
///
/// ### Usage of `#[injectable(instance)]` with parameters
/// ### Usage of `#[injectable(provider)]` with parameters
///
/// ```rust,ignore
/// #[injectable(kind = "provider")]
/// #[injectable(provider)]
/// pub struct Database {
/// db: Store,
/// }
Expand Down
Loading