Skip to content

Commit 95b02e0

Browse files
authored
Merge pull request #8 from dimacurrentai/htmljson
Using HTML template for JSONs
2 parents 800da1e + 457a418 commit 95b02e0

File tree

5 files changed

+266
-1
lines changed

5 files changed

+266
-1
lines changed

step02_httpserver/code/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ edition = "2021"
66
axum = "0.7"
77
hyper = { version = "1", features = ["server", "http1"] }
88
tokio = { version = "1", features = ["full"] }
9+
tower = "0.4"
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
4+
<head>
5+
<meta charset="UTF-8">
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
7+
8+
<!-- Can be changed with `._title`. -->
9+
<title>JSON</title>
10+
11+
<!-- Can be changed with `._favicon`, orange 1x1 by default. -->
12+
<link href="data:image/x-icon;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4v5QBAARLAaU/Dnq2AAAAAElFTkSuQmCC" rel="icon" type="image/x-icon" />
13+
14+
<style>
15+
body {
16+
font-family: Arial, sans-serif;
17+
background-color: #282c34;
18+
color: white;
19+
padding: 1px;
20+
}
21+
pre {
22+
white-space: pre;
23+
overflow-x: auto;
24+
background: #1e1e1e;
25+
padding: 15px;
26+
border-radius: 5px;
27+
overflow-x: auto;
28+
}
29+
.string {
30+
color: #ce9178;
31+
}
32+
.number {
33+
color: #b5cea8;
34+
}
35+
.boolean {
36+
color: #569cd6;
37+
}
38+
.null {
39+
color: #569cd6;
40+
}
41+
.key {
42+
color: #9cdcfe;
43+
}
44+
a {
45+
color: #9cdcfe;
46+
}
47+
a:hover {
48+
text-decoration: underline;
49+
}
50+
</style>
51+
</head>
52+
53+
<body>
54+
<script>
55+
const escapeHtml = (str) => {
56+
return str
57+
.replace(/&/g, "&amp;")
58+
.replace(/</g, "&lt;")
59+
.replace(/>/g, "&gt;")
60+
.replace(/"/g, "&quot;")
61+
.replace(/'/g, "&#039;");
62+
};
63+
64+
const innerLinks = (value, links, i) => {
65+
// NOTE(dkorolev): Support both top-level `_links` and inner-level `._links`, see the example.
66+
if (value && value._links) {
67+
return value._links;
68+
} else if (links !== undefined && i in links) {
69+
return links[i];
70+
} else {
71+
return undefined;
72+
}
73+
};
74+
75+
const createJson = (value, links, indent) => {
76+
if (Array.isArray(value)) {
77+
let s = `[\n`;
78+
for (let i = 0; i < value.length; ++i) {
79+
s += `${indent} ${createJson(value[i], innerLinks(value, links, i), indent + ' ')}${i + 1 == value.length ? '' : ','}\n`;
80+
}
81+
s += `${indent}]`;
82+
return s;
83+
} else if (value === null) {
84+
return `<span class="null">null</span>`;
85+
} else {
86+
const type = typeof value;
87+
if (type === 'string') {
88+
return `<span class="string">"${escapeHtml(value)}"</span>`;
89+
} else if (type === 'boolean') {
90+
return `<span class="boolean">${type ? 'true' : 'false'}</span>`;
91+
} else if (type === 'number') {
92+
return `<span class="number">${value}</span>`;
93+
} else if (type === 'object') {
94+
let s = `{\n`;
95+
let k = Object.keys(value).filter(e => e != '_links');
96+
for (let i = 0; i < k.length; ++i) {
97+
if (links && k[i] in links && typeof links[k[i]] === 'string') {
98+
s += `${indent} <a href='${links[k[i]]}'><span class="key">${escapeHtml(k[i])}</span></a>: ${createJson(value[k[i]], innerLinks(value[k[i]], links, k[i]), indent + ' ')}${i + 1 == k.length ? '' : ','}\n`;
99+
} else {
100+
s += `${indent} <span class="key">${escapeHtml(k[i])}</span>: ${createJson(value[k[i]], innerLinks(value[k[i]], links, k[i]), indent + ' ')}${i + 1 == k.length ? '' : ','}\n`;
101+
}
102+
}
103+
s += `${indent}}`;
104+
return s;
105+
}
106+
}
107+
};
108+
const data = {};
109+
</script>
110+
<pre id="json-output"></pre>
111+
<script>
112+
let favico = null;
113+
if ('_title' in data && typeof data._title === 'string') {
114+
document.title = data._title;
115+
delete data._title;
116+
}
117+
if ('_favico' in data && typeof data._favico === 'string') {
118+
favico = data._favico;
119+
delete data._favico;
120+
}
121+
document.getElementById("json-output").innerHTML = createJson(data, '_links' in data ? data._links : null, '');
122+
if (favico) {
123+
document.querySelector("link[rel~='icon']").href = favico;
124+
}
125+
</script>
126+
</body>
127+
128+
</html>

step02_httpserver/code/src/main.rs

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,90 @@
1+
use axum::body::Body;
12
use axum::{routing::get, serve, Router};
3+
use hyper::header::CONTENT_TYPE;
4+
use hyper::StatusCode;
5+
use std::sync::Arc;
6+
7+
use tower::ServiceBuilder;
8+
9+
use hyper::header::ACCEPT;
10+
use hyper::Request;
211
use std::net::SocketAddr;
312
use tokio::signal::unix::{signal, SignalKind};
413
use tokio::{net::TcpListener, sync::mpsc};
514

15+
use axum::middleware::{from_fn, Next};
16+
use axum::response::{IntoResponse, Response};
17+
18+
#[derive(Clone)]
19+
struct BrowserFriendlyJson {
20+
data: String,
21+
}
22+
23+
struct JsonHtmlTemplate<'a> {
24+
pre: &'a str,
25+
post: &'a str,
26+
}
27+
28+
impl IntoResponse for BrowserFriendlyJson {
29+
fn into_response(self) -> Response {
30+
let mut response = StatusCode::NOT_IMPLEMENTED.into_response();
31+
response.extensions_mut().insert(self);
32+
response
33+
}
34+
}
35+
36+
const SAMPLE_JSON: &str = include_str!("sample.json");
37+
38+
const fn find_split_position(bytes: &[u8]) -> usize {
39+
let mut i = 0;
40+
while i < bytes.len() && (bytes[i] != b'{' || bytes[i + 1] != b'}') {
41+
i += 1;
42+
}
43+
i
44+
// TODO(dkorolev): Panic if did not find `{}` or if found more than one `{}`.
45+
// NOTE(dkorolev): Why not create the split `str` slice at compile time, huh?
46+
}
47+
48+
static JSON_TEMPLATE_HTML: &[u8] = include_bytes!("jsontemplate.html");
49+
static JSON_TEMPLATE_HTML_SPLIT_IDX: usize = find_split_position(&JSON_TEMPLATE_HTML);
50+
51+
fn create_response<S: Into<String>>(content_type: &str, body: S) -> Response<Body> {
52+
Response::builder().status(StatusCode::OK).header(CONTENT_TYPE, content_type).body(Body::from(body.into())).unwrap()
53+
}
54+
55+
async fn browser_json_renderer(request: Request<Body>, next: Next, tmpl: Arc<JsonHtmlTemplate<'_>>) -> Response {
56+
// TODO(dkorolev): Can this be more Rusty?
57+
let mut accept_html = false;
58+
request.headers().get(&ACCEPT).map(|value| {
59+
let s = std::str::from_utf8(value.as_ref()).unwrap();
60+
s.split(',').for_each(|value| {
61+
if value == "text/html" || value == "html" {
62+
accept_html = true;
63+
}
64+
})
65+
});
66+
67+
// NOTE(dkorolev): I could not put the above logic to inside after `if let`, although, clearly it should be there.
68+
let mut response = next.run(request).await;
69+
if let Some(my_data) = response.extensions_mut().remove::<BrowserFriendlyJson>() {
70+
if accept_html {
71+
return create_response("text/html", format!("{}{}{}", tmpl.pre, my_data.data, tmpl.post));
72+
} else {
73+
return create_response("application/json", my_data.data);
74+
}
75+
}
76+
77+
response
78+
}
79+
680
#[tokio::main]
781
async fn main() {
82+
// NOTE(dkorolev): Can this be done at compile time?
83+
let html_template = Arc::new(JsonHtmlTemplate {
84+
pre: std::str::from_utf8(&JSON_TEMPLATE_HTML[0..JSON_TEMPLATE_HTML_SPLIT_IDX]).expect("NON-UTF8 TEMPLATE"),
85+
post: std::str::from_utf8(&JSON_TEMPLATE_HTML[(JSON_TEMPLATE_HTML_SPLIT_IDX + 2)..]).expect("NON-UTF8 TEMPLATE"),
86+
});
87+
888
let (shutdown_tx, mut shutdown_rx) = mpsc::channel::<()>(1);
989

1090
let app = Router::new()
@@ -19,7 +99,13 @@ async fn main() {
1999
"yes i am shutting down\n"
20100
}
21101
}),
22-
);
102+
)
103+
.route("/json", get(|| async { BrowserFriendlyJson { data: SAMPLE_JSON.to_string() } }))
104+
.layer(ServiceBuilder::new().layer(from_fn({
105+
// TODO(dkorolev): Can I just move the `html_template` into `browser_json_renderer`?
106+
let html_template = Arc::clone(&html_template);
107+
move |req, next| browser_json_renderer(req, next, Arc::clone(&html_template))
108+
})));
23109

24110
let addr = SocketAddr::from(([0, 0, 0, 0], 3000));
25111
let listener = TcpListener::bind(addr).await.unwrap();
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{
2+
"hello": "world",
3+
"the_answer": 42,
4+
"list1": {
5+
"orwell": 1984,
6+
"adams": 42
7+
},
8+
"list2": {
9+
"foo": true,
10+
"bar": null,
11+
"_links": {
12+
"bar": "http://dima.ai?null"
13+
}
14+
},
15+
"_links": {
16+
"list1": {
17+
"orwell": "http://dima.ai?1984",
18+
"adams": "http://dima.ai?42",
19+
}
20+
},
21+
"inner": {
22+
"list3": {
23+
"orwell": 1984,
24+
"adams": 42
25+
},
26+
"list4": {
27+
"foo": true,
28+
"bar": null,
29+
"_links": {
30+
"bar": "http://dima.ai?inner_null"
31+
}
32+
},
33+
},
34+
"_links": {
35+
"list1": {
36+
"orwell": "http://dima.ai?1984",
37+
"adams": "http://dima.ai?42",
38+
},
39+
"inner": {
40+
"list3": {
41+
"orwell": "http://dima.ai?inner_1984",
42+
"adams": "http://dima.ai?inner_42",
43+
}
44+
}
45+
},
46+
"_title": "Rust JSON",
47+
"_favico": "data:image/x-icon;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGNgaGgAAAGEAQFWjyAjAAAAAElFTkSuQmCC"
48+
}

step02_httpserver/run.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ done
1919

2020
curl -s localhost:3000
2121

22+
curl -s localhost:3000/json
23+
2224
curl -s localhost:3000/quit
2325

2426
wait $PID

0 commit comments

Comments
 (0)