Echo Writes Code

gitten.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
use axum::{ Router, serve };
use axum::extract::{ State };
use axum::http::{ StatusCode };
use axum::response::{ Html, IntoResponse, Response };
use axum::routing::{ get };
use config::{ Config };
use serde::{ Serialize };
use tera::{ Context, Tera };
use tracing_subscriber::layer::{ SubscriberExt };
use tracing_subscriber::util::{ SubscriberInitExt };
use tokio::net::{ TcpListener };
use tower_http::services::{ ServeDir };
use tower_http::trace::{ TraceLayer };

use std::fmt;
use std::io;
use std::sync::{ Arc };

// -------------------------------------------------------------------------------------------------
// Error handling ----------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------------------

type Result<T> = ::std::result::Result<T, Error>;

#[derive(Debug)]
enum Error {
	ConfigError(config::ConfigError),
	IoError(io::Error),
	TeraError(tera::Error),
}

impl fmt::Display for Error {
	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
		use Error::*;

		match *self {
			ConfigError(ref e) => write!(f, "Config error: {}", e),
			IoError(ref e) => write!(f, "IO error: {}", e),
			TeraError(ref e) => write!(f, "Tera error: {}", e),
		}
	}
}

impl From<config::ConfigError> for Error {
	fn from(e: config::ConfigError) -> Error {
		Error::ConfigError(e)
	}
}

impl From<io::Error> for Error {
	fn from(e: io::Error) -> Error {
		Error::IoError(e)
	}
}

impl From<tera::Error> for Error {
	fn from(e: tera::Error) -> Error {
		Error::TeraError(e)
	}
}

impl IntoResponse for Error {
	fn into_response(self) -> Response {
		use Error::*;

		tracing::error!("Error when building response: {}", &self);

		let body = match self {
			ConfigError(..) => "The server encountered an unexpected error",
			IoError(..) => "The server encountered an unexpected error",
			TeraError(..) => "The server encountered an unexpected error",
		};

		(StatusCode::INTERNAL_SERVER_ERROR, body).into_response()
	}
}

// -------------------------------------------------------------------------------------------------
// Server ------------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------------------

struct ServerState {
	application_context: ApplicationContext,
	tera: Tera,
}

impl ServerState {
	fn new(application_context: ApplicationContext, tera: Tera) -> ServerState {
		ServerState { application_context, tera }
	}
}

#[derive(Serialize)]
struct ApplicationContext {
	name: String,
}

#[tokio::main]
async fn main() -> Result<()> {
	// Read configuration file
	let config = Config::builder()
		.add_source(config::File::with_name("server/gitten.toml"))
		.build()?;

	// Configure the application context (static state visible to every template)
	let application_name = config.get_string("application.name")?;
	let application_context = ApplicationContext {
		name: application_name.clone(),
	};

	// Configure tracing
	tracing_subscriber::registry()
		.with(tracing_subscriber::fmt::layer())
		.init();

	// Configure Tera (the templating engine)
	let tera = Tera::new("templates/**/*.tera.html")?;

	// Configure the shared server state object
	let server_state = ServerState::new(application_context, tera);

	// Configure the router
	let router = Router::new()
		.layer(TraceLayer::new_for_http())
		.route("/", get(index))
		.nest_service("/static", ServeDir::new("static"))
		.with_state(Arc::new(server_state));

	// Bind to an address and start the server
	let host = config.get_string("network.host")?;
	let port = config.get_int("network.port")?;
	let listener = TcpListener::bind(format!("{}:{}", host, port)).await?;
	tracing::debug!("{} v0.1.0 now listening on {}", application_name, listener.local_addr()?);

	serve(listener, router).await?;

	Ok(())
}

async fn index(State(server_state): State<Arc<ServerState>>) -> Result<Html<String>> {
	let context = page_context(&server_state)?;
	let html = server_state.tera.render("index.tera.html", &context)?;
	Ok(Html(html))
}

fn page_context(server_state: &ServerState) -> Result<Context> {
	let mut context = Context::new();
	context.insert("application", &server_state.application_context);
	Ok(context)
}