simple_syrup/
lib.rs

1use async_graphql::http::{playground_source, GraphQLPlaygroundConfig};
2use async_graphql::{ObjectType, SubscriptionType};
3use async_graphql_warp::{GraphQLBadRequest, GraphQLResponse};
4
5use dotenv::dotenv;
6use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
7use std::convert::Infallible;
8use std::str::FromStr;
9use warp::{http::Response as HttpResponse, http::StatusCode, Filter, Rejection};
10
11pub use anyhow;
12pub use async_graphql::{
13    self, Context, EmptyMutation, EmptySubscription, Enum, InputObject, Interface, Object, Result,
14    Scalar, Schema, SchemaBuilder, SimpleObject, Union,
15};
16pub use chrono;
17pub use serde::{self, Deserialize, Serialize};
18pub use serde_json;
19pub use sqlx::{self, sqlite::SqlitePool};
20pub use tokio;
21pub use uuid::{self, Uuid};
22
23/// SimpleGraphql
24pub struct SimpleGraphql<
25    Q: ObjectType + 'static,
26    M: ObjectType + 'static,
27    S: SubscriptionType + 'static,
28> {
29    builder: SchemaBuilder<Q, M, S>,
30    sqlite: Option<SimpleSqlite>,
31    spa: Option<SpaConfig>,
32}
33
34pub struct SpaConfig {
35    assets: String,
36    index: String,
37}
38
39impl<Q: ObjectType + 'static, M: ObjectType + 'static, S: SubscriptionType + 'static>
40    SimpleGraphql<Q, M, S>
41{
42    pub fn new(builder: SchemaBuilder<Q, M, S>) -> Self {
43        Self {
44            builder,
45            sqlite: None,
46            spa: None,
47        }
48    }
49
50    pub fn with_sqlite(self, sqlite: SimpleSqlite) -> Self {
51        Self {
52            sqlite: Some(sqlite),
53            ..self
54        }
55    }
56
57    pub fn with_spa(self, assets_dir: &str, index_file: &str) -> Self {
58        Self {
59            spa: Some(SpaConfig {
60                assets: assets_dir.to_string(),
61                index: index_file.to_string(),
62            }),
63            ..self
64        }
65    }
66
67    pub async fn run(self) {
68        dotenv().ok();
69
70        let schema = if let Some(sqlite) = self.sqlite {
71            self.builder.data(sqlite.pool()).finish()
72        } else {
73            self.builder.finish()
74        };
75
76        let graphql_post = warp::path!("graphql")
77            .and(async_graphql_warp::graphql(schema))
78            .and_then(
79                |(schema, request): (Schema<Q, M, S>, async_graphql::Request)| async move {
80                    Ok::<_, Infallible>(GraphQLResponse::from(schema.execute(request).await))
81                },
82            );
83
84        let graphql_playground = warp::path!("playground").and(warp::get()).map(|| {
85            HttpResponse::builder()
86                .header("content-type", "text/html")
87                .body(playground_source(GraphQLPlaygroundConfig::new("/graphql")))
88        });
89
90        let recover = |err: Rejection| async move {
91            if let Some(GraphQLBadRequest(err)) = err.find() {
92                return Ok::<_, Infallible>(warp::reply::with_status(
93                    err.to_string(),
94                    StatusCode::BAD_REQUEST,
95                ));
96            }
97
98            Ok(warp::reply::with_status(
99                "INTERNAL_SERVER_ERROR".to_string(),
100                StatusCode::INTERNAL_SERVER_ERROR,
101            ))
102        };
103
104        // Conditional routes here is not great
105        if let Some(spa) = self.spa {
106            let assets = warp::fs::dir(spa.assets);
107            let spa_index = warp::fs::file(spa.index);
108
109            let routes = graphql_playground
110                .or(graphql_post)
111                .or(assets)
112                .or(spa_index)
113                .recover(recover);
114            print_start();
115            warp::serve(routes).run(([0, 0, 0, 0], 3030)).await;
116        } else {
117            let routes = graphql_playground.or(graphql_post).recover(recover);
118            print_start();
119            warp::serve(routes).run(([0, 0, 0, 0], 3030)).await;
120        }
121    }
122}
123
124fn print_start() {
125    println!("Running at http://0.0.0.0:3030");
126    println!("\nRoutes:");
127    println!("\t/graphql");
128    println!("\t/playground");
129}
130
131/// SimpleSqlite
132pub struct SimpleSqlite {
133    pool: SqlitePool,
134}
135
136impl SimpleSqlite {
137    pub fn new(filename: &str) -> Self {
138        let url = format!("sqlite://{}", filename);
139        let options = SqliteConnectOptions::from_str(&url)
140            .unwrap()
141            .create_if_missing(true);
142        let pool = SqlitePoolOptions::new().connect_lazy_with(options);
143
144        Self { pool }
145    }
146
147    pub fn pool(&self) -> SqlitePool {
148        self.pool.clone()
149    }
150}