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
67 changes: 62 additions & 5 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,25 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::path::Path;

use config::{Config, ConfigError, File};
use serde::Deserialize;
use serde::{Deserialize, Deserializer};
use url::Url;

const DEFAULT_PORT: u16 = 3000;

#[derive(Deserialize, Debug, Clone)]
pub struct GlazedConfig {
#[serde(default = "default_bind_address")]
pub bind_address: SocketAddr,
pub public_address: Option<Url>,
#[serde(
default = "default_public_address",
deserialize_with = "validate_public_address"
)]
pub public_address: Url,
pub tiled_client: TiledClientConfig,
#[serde(default)]
pub log_level: LogLevel,
}

impl GlazedConfig {
pub fn from_file(path: &Path) -> Result<Self, ConfigError> {
let config = Config::builder().add_source(File::from(path)).build()?;
Expand All @@ -21,14 +29,22 @@ impl GlazedConfig {

pub fn default() -> Self {
GlazedConfig {
bind_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 3000),
public_address: None,
bind_address: default_bind_address(),
public_address: default_public_address(),
tiled_client: TiledClientConfig {
address: Url::parse("http://localhost:8000").expect("Static URL is valid"),
},
log_level: LogLevel::Info,
}
}
pub fn endpoint(&self, endpoint: &str) -> String {
let mut addr = self.public_address.clone();
addr.path_segments_mut()
.expect("base address can be a base")
.pop_if_empty()
.push(endpoint);
addr.to_string()
}
}

#[derive(Deserialize, Debug, Clone)]
Expand Down Expand Up @@ -57,16 +73,57 @@ impl From<LogLevel> for tracing::level_filters::LevelFilter {
}
}

fn default_bind_address() -> SocketAddr {
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), DEFAULT_PORT)
}

fn default_public_address() -> Url {
let mut addr = Url::parse("http://localhost").expect("Static URL is valid");
addr.set_port(Some(DEFAULT_PORT)).unwrap();
addr
}

fn validate_public_address<'de, D: Deserializer<'de>>(des: D) -> Result<Url, D::Error> {
let url = Url::deserialize(des)?;
if url.cannot_be_a_base() {
Err(serde::de::Error::custom("URL cannot be a base"))
} else {
Ok(url)
}
}

#[cfg(test)]
mod tests {
use tracing::level_filters::LevelFilter;
use url::Url;

use crate::config::LogLevel;
use crate::config::{GlazedConfig, LogLevel, default_bind_address, default_public_address};

#[test]
fn level_conversion() {
assert_eq!(LevelFilter::from(LogLevel::Info), LevelFilter::INFO);
assert_eq!(LevelFilter::from(LogLevel::Debug), LevelFilter::DEBUG);
assert_eq!(LevelFilter::from(LogLevel::Trace), LevelFilter::TRACE);
}

#[rstest::rstest]
#[case::no_trailing_slash("http://example.com", "http://example.com/graphql")]
#[case::trailing_slash("http://example.com/", "http://example.com/graphql")]
#[case::non_empty_path("http://example.com/extra", "http://example.com/extra/graphql")]
#[case::path_and_trailing("http://example.com/extra/", "http://example.com/extra/graphql")]
fn graphql_endpoint(#[case] base: &str, #[case] complete: &str) {
let mut config = GlazedConfig::default();
config.public_address = Url::parse(base).unwrap();
assert_eq!(config.endpoint("graphql"), complete)
}

#[test]
fn default_addresses() {
assert_eq!(default_bind_address().to_string(), "127.0.0.1:3000");
assert!(!default_public_address().cannot_be_a_base());
assert_eq!(
default_public_address().to_string(),
"http://localhost:3000/"
);
}
}
8 changes: 4 additions & 4 deletions src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,13 @@ pub async fn graphql_ws_handler(
}

pub async fn graphiql_handler(
graphql_endpoint: Option<String>,
subscription_endpoint: Option<String>,
graphql_endpoint: String,
subscription_endpoint: String,
) -> impl IntoResponse {
Html(
GraphiQLSource::build()
.endpoint(graphql_endpoint.as_deref().unwrap_or("/graphql"))
.subscription_endpoint(subscription_endpoint.as_deref().unwrap_or("/ws"))
.endpoint(&graphql_endpoint)
.subscription_endpoint(&subscription_endpoint)
.finish(),
)
}
Expand Down
44 changes: 21 additions & 23 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,25 +57,26 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
pub struct RootAddress(Url);

async fn serve(config: GlazedConfig) -> Result<(), Box<dyn std::error::Error>> {
let client = TiledClient::new(config.tiled_client.address);
let public_address = config
.public_address
.clone()
.unwrap_or_else(|| Url::parse(&format!("http://{}", config.bind_address)).unwrap());
let client = TiledClient::new(config.tiled_client.address.clone());
let schema = Schema::build(TiledQuery, EmptyMutation, TiledSubscription)
.data(RootAddress(public_address.clone()))
.data(RootAddress(config.public_address.clone()))
.data(client.clone())
.finish();

let graphql_endpoint = config
.public_address
.as_ref()
.map(|u| u.join("graphql").unwrap().to_string());
let graphql_endpoint = config.endpoint("graphql");
let graphiql_endpoint = config.endpoint("graphiql");
let subscription_endpoint = config.endpoint("subscribe");
info!(
"Redirecting traffic to public address: {}",
config.public_address
);
info!("Public graphql endpoint available at {}", graphql_endpoint);

let page_not_found = (
StatusCode::NOT_FOUND,
not_found_page(&graphql_endpoint, &graphiql_endpoint),
);

let subscription_endpoint = config
.public_address
.as_ref()
.map(|u| u.join("subscribe").unwrap().to_string());
let app = Router::new()
.route("/graphql", post(graphql_handler).get(graphql_get_warning))
.route("/subscribe", get(graphql_ws_handler))
Expand All @@ -89,7 +90,7 @@ async fn serve(config: GlazedConfig) -> Result<(), Box<dyn std::error::Error>> {
)
.route("/asset/{run}/{stream}/{det}/{id}", get(download_handler))
.with_state(client)
.fallback((StatusCode::NOT_FOUND, not_found_page(&public_address)))
.fallback(page_not_found)
.layer(Extension(schema));

let listener = tokio::net::TcpListener::bind(config.bind_address).await?;
Expand All @@ -100,9 +101,7 @@ async fn serve(config: GlazedConfig) -> Result<(), Box<dyn std::error::Error>> {
.await?)
}

fn not_found_page(public_address: &Url) -> Html<String> {
let graphql = public_address.join("graphql").unwrap();
let graphiql = public_address.join("graphiql").unwrap();
fn not_found_page(graphql: &str, graphiql: &str) -> Html<String> {
Html(format!(
include_str!("../templates/404.html"),
graphql_address = graphql,
Expand Down Expand Up @@ -132,15 +131,14 @@ async fn signal_handler() {

#[cfg(test)]
mod tests {
use url::Url;

use super::not_found_page;

#[test]
fn test_404() {
let public_address = Url::parse("http://example.com/glazed/").unwrap();

let response = not_found_page(&public_address);
let response = not_found_page(
"http://example.com/glazed/graphql",
"http://example.com/glazed/graphiql",
);

assert_eq!(
response.0,
Expand Down
Loading