Commit adeb1318 authored by Konrad Borowski's avatar Konrad Borowski

Add "Hello, world" examples

parent b791aba8
Pipeline #98149183 passed with stages
in 15 minutes and 22 seconds
...@@ -34,7 +34,7 @@ walkdir = "2.2.9" ...@@ -34,7 +34,7 @@ walkdir = "2.2.9"
[dev-dependencies] [dev-dependencies]
scraper = "0.10.1" scraper = "0.10.1"
serde_json = "1.0.40"
[features] [features]
database_tests = [] database_tests = []
sandbox_tests = []
...@@ -11,6 +11,7 @@ class Editor { ...@@ -11,6 +11,7 @@ class Editor {
output: Output output: Output
autodeleteText: HTMLSpanElement autodeleteText: HTMLSpanElement
autodeleteCheckbox: HTMLLabelElement autodeleteCheckbox: HTMLLabelElement
helloWorldLink: HTMLSpanElement
submit: HTMLInputElement submit: HTMLInputElement
detailsElement: HTMLDetailsElement detailsElement: HTMLDetailsElement
stdinElement: HTMLTextAreaElement stdinElement: HTMLTextAreaElement
...@@ -31,11 +32,12 @@ class Editor { ...@@ -31,11 +32,12 @@ class Editor {
this.output.display({}, { this.output.display({}, {
stdout: stdout.value, stdout: stdout.value,
stderr: document.querySelector<HTMLInputElement>('#dbstderr').value, stderr: document.querySelector<HTMLInputElement>('#dbstderr').value,
status: +document.querySelector<HTMLInputElement>('#dbstatus')?.value, status: +document.querySelector<HTMLInputElement>('#dbstatus') ?.value,
}) })
} }
this.autodeleteText = form.querySelector('#autodelete-text') this.autodeleteText = form.querySelector('#autodelete-text')
this.autodeleteCheckbox = form.querySelector('#automatically-hidden-label') this.autodeleteCheckbox = form.querySelector('#automatically-hidden-label')
this.helloWorldLink = form.querySelector('#hello-world')
this.submit = form.querySelector('[type=submit]') this.submit = form.querySelector('[type=submit]')
this.submit.disabled = true this.submit.disabled = true
form.addEventListener('submit', () => { form.addEventListener('submit', () => {
...@@ -58,7 +60,7 @@ class Editor { ...@@ -58,7 +60,7 @@ class Editor {
this.stdinElement.name = 'stdin' this.stdinElement.name = 'stdin'
this.stdinElement.addEventListener('change', () => this.changeToLookLikeNewPaste()) this.stdinElement.addEventListener('change', () => this.changeToLookLikeNewPaste())
this.detailsElement.append(summary, this.stdinElement) this.detailsElement.append(summary, this.stdinElement)
const dbStdin = document.querySelector<HTMLInputElement>('#dbstdin')?.value const dbStdin = document.querySelector<HTMLInputElement>('#dbstdin') ?.value
if (dbStdin) { if (dbStdin) {
this.stdinElement.value = dbStdin this.stdinElement.value = dbStdin
this.detailsElement.open = true this.detailsElement.open = true
...@@ -112,12 +114,19 @@ class Editor { ...@@ -112,12 +114,19 @@ class Editor {
async updateLanguage() { async updateLanguage() {
this.wrapperButtons.clear() this.wrapperButtons.clear()
this.helloWorldLink.textContent = ''
const identifier = this.getLanguageIdentifier() const identifier = this.getLanguageIdentifier()
this.setLanguage(identifier) this.setLanguage(identifier)
const isStillValid = () => identifier === this.getLanguageIdentifier() const isStillValid = () => identifier === this.getLanguageIdentifier()
const language = await getLanguage(identifier, isStillValid) const language = await getLanguage(identifier, isStillValid)
// This deals with user changing the language after asynchronous event // This deals with user changing the language after asynchronous event
if (isStillValid()) { if (isStillValid()) {
if (language.helloWorldPaste) {
const anchor = document.createElement('a')
anchor.href = '/' + language.helloWorldPaste
anchor.textContent = 'Hello world program'
this.helloWorldLink.append(' | ', anchor)
}
this.detailsElement.style.display = language.implementations.length ? 'block' : 'none' this.detailsElement.style.display = language.implementations.length ? 'block' : 'none'
this.wrapperButtons.update(language.implementations) this.wrapperButtons.update(language.implementations)
} }
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
{ {
"identifier": "c", "identifier": "c",
"name": "C", "name": "C",
"helloworld": "#include <stdio.h>\n\nint main(void) {\n puts(\"Hello, world!\");\n return 0;\n}",
"implementations": [ "implementations": [
{ {
"label": "Clang", "label": "Clang",
...@@ -34,6 +35,7 @@ ...@@ -34,6 +35,7 @@
{ {
"identifier": "cpp", "identifier": "cpp",
"name": "C++", "name": "C++",
"helloworld": "#include <iostream>\n\nint main(void) {\n std::cout << \"Hello, world!\\n\";\n}",
"implementations": [ "implementations": [
{ {
"label": "Clang", "label": "Clang",
...@@ -62,6 +64,7 @@ ...@@ -62,6 +64,7 @@
{ {
"identifier": "csharp", "identifier": "csharp",
"name": "C#", "name": "C#",
"helloworld": "using System;\n\nclass Program {\n static void Main(string[] args) {\n Console.WriteLine(\"Hello, world!\");\n }\n}",
"implementations": [ "implementations": [
{ {
"label": ".NET Core", "label": ".NET Core",
...@@ -79,6 +82,7 @@ ...@@ -79,6 +82,7 @@
{ {
"identifier": "haskell", "identifier": "haskell",
"name": "Haskell", "name": "Haskell",
"helloworld": "main = putStrLn \"Hello, world!\"",
"implementations": [ "implementations": [
{ {
"label": "GHC", "label": "GHC",
...@@ -100,6 +104,7 @@ ...@@ -100,6 +104,7 @@
{ {
"identifier": "java", "identifier": "java",
"name": "Java", "name": "Java",
"helloworld": "class HelloWorld {\n public static void main(String[] args) {\n System.out.println(\"Hello, world!\");\n }\n}",
"implementations": [ "implementations": [
{ {
"label": "OpenJDK 8", "label": "OpenJDK 8",
...@@ -128,6 +133,7 @@ ...@@ -128,6 +133,7 @@
{ {
"identifier": "javascript", "identifier": "javascript",
"name": "JavaScript", "name": "JavaScript",
"helloworld": "console.log(\"Hello, world!\");",
"implementations": [ "implementations": [
{ {
"label": "Node.js", "label": "Node.js",
...@@ -157,6 +163,7 @@ ...@@ -157,6 +163,7 @@
{ {
"identifier": "perl", "identifier": "perl",
"name": "Perl", "name": "Perl",
"helloworld": "use v5.12;\nuse warnings;\n\nsay \"Hello, world!\";",
"implementations": [ "implementations": [
{ {
"label": "Perl", "label": "Perl",
...@@ -174,6 +181,7 @@ ...@@ -174,6 +181,7 @@
{ {
"identifier": "php", "identifier": "php",
"name": "PHP", "name": "PHP",
"helloworld": "<?php\necho \"Hello, world!\\n\";",
"implementations": [ "implementations": [
{ {
"label": "PHP", "label": "PHP",
...@@ -199,6 +207,7 @@ ...@@ -199,6 +207,7 @@
{ {
"identifier": "python", "identifier": "python",
"name": "Python 3", "name": "Python 3",
"helloworld": "print(\"Hello, world!\")",
"implementations": [ "implementations": [
{ {
"label": "CPython", "label": "CPython",
...@@ -222,6 +231,7 @@ ...@@ -222,6 +231,7 @@
{ {
"identifier": "raku", "identifier": "raku",
"name": "Raku", "name": "Raku",
"helloworld": "say \"Hello, world!\"",
"implementations": [ "implementations": [
{ {
"label": "Rakudo", "label": "Rakudo",
...@@ -239,6 +249,7 @@ ...@@ -239,6 +249,7 @@
{ {
"identifier": "rust", "identifier": "rust",
"name": "Rust", "name": "Rust",
"helloworld": "fn main() {\n println!(\"Hello, world!\");\n}",
"implementations": [ "implementations": [
{ {
"label": "Stable", "label": "Stable",
...@@ -268,6 +279,7 @@ ...@@ -268,6 +279,7 @@
{ {
"identifier": "sh", "identifier": "sh",
"name": "Sh", "name": "Sh",
"helloworld": "echo Hello, world!",
"implementations": [ "implementations": [
{ {
"label": "sh", "label": "sh",
...@@ -289,6 +301,7 @@ ...@@ -289,6 +301,7 @@
{ {
"identifier": "sqlite", "identifier": "sqlite",
"name": "SQLite", "name": "SQLite",
"helloworld": "SELECT 'Hello, world!'",
"implementations": [ "implementations": [
{ {
"label": "SQLite", "label": "SQLite",
...@@ -306,6 +319,7 @@ ...@@ -306,6 +319,7 @@
{ {
"identifier": "typescript", "identifier": "typescript",
"name": "TypeScript", "name": "TypeScript",
"helloworld": "console.log(\"Hello, world!\");",
"implementations": [ "implementations": [
{ {
"label": "Node.js", "label": "Node.js",
......
ALTER TABLE languages DROP COLUMN hello_world_paste_id;
ALTER TABLE languages ADD COLUMN hello_world_paste_id int REFERENCES pastes;
...@@ -19,7 +19,7 @@ fn main() -> Result<(), Box<dyn Error>> { ...@@ -19,7 +19,7 @@ fn main() -> Result<(), Box<dyn Error>> {
let pool = Pool::new(ConnectionManager::new(database_url)) let pool = Pool::new(ConnectionManager::new(database_url))
.expect("Couldn't create a connection connection"); .expect("Couldn't create a connection connection");
diesel_migrations::run_pending_migrations(&pool.get()?)?; diesel_migrations::run_pending_migrations(&pool.get()?)?;
migration::run(pool.get()?)?; migration::run(&pool.get()?)?;
warp::serve(routes::routes(pool)).run(([127, 0, 0, 1], 8080)); warp::serve(routes::routes(pool)).run(([127, 0, 0, 1], 8080));
Ok(()) Ok(())
} }
......
use crate::schema::{implementation_wrappers, implementations, languages}; use crate::models::paste;
use diesel::pg::Pg; use crate::schema::{implementation_wrappers, implementations, languages, pastes};
use crate::Connection;
use diesel::prelude::*; use diesel::prelude::*;
use diesel::sql_types::{Bool, Integer, Text}; use diesel::sql_types::{Bool, Integer, Text};
use serde::Deserialize; use serde::Deserialize;
...@@ -9,6 +10,7 @@ use std::fs; ...@@ -9,6 +10,7 @@ use std::fs;
#[derive(Deserialize)] #[derive(Deserialize)]
struct Language { struct Language {
identifier: String, identifier: String,
helloworld: Option<String>,
#[serde(default)] #[serde(default)]
implementations: Vec<Implementation>, implementations: Vec<Implementation>,
} }
...@@ -32,13 +34,42 @@ struct Wrapper { ...@@ -32,13 +34,42 @@ struct Wrapper {
is_formatter: bool, is_formatter: bool,
} }
pub fn run(connection: impl Connection<Backend = Pg>) -> Result<(), Box<dyn Error>> { pub fn run(connection: &Connection) -> Result<(), Box<dyn Error>> {
let languages: Vec<Language> = serde_json::from_slice(&fs::read("languages.json")?)?; let languages: Vec<Language> = serde_json::from_slice(&fs::read("languages.json")?)?;
for Language { for Language {
identifier: languages_identifier, identifier: languages_identifier,
helloworld,
implementations, implementations,
} in languages } in languages
{ {
if let Some(hello_world) = helloworld {
let paste_id: Option<i32> = languages::table
.filter(languages::identifier.eq(&languages_identifier))
.select(languages::hello_world_paste_id)
.get_result(connection)?;
if paste_id.is_none() {
let identifier = paste::insert(
connection,
None,
&languages_identifier,
hello_world,
"".into(),
None,
None,
None,
)
.unwrap();
diesel::update(languages::table)
.set(
languages::hello_world_paste_id.eq(pastes::table
.select(pastes::paste_id)
.filter(pastes::identifier.eq(identifier))
.single_value()),
)
.filter(languages::identifier.eq(&languages_identifier))
.execute(connection)?;
}
}
for Implementation { for Implementation {
label, label,
identifier: implementation_identifier, identifier: implementation_identifier,
...@@ -61,7 +92,7 @@ pub fn run(connection: impl Connection<Backend = Pg>) -> Result<(), Box<dyn Erro ...@@ -61,7 +92,7 @@ pub fn run(connection: impl Connection<Backend = Pg>) -> Result<(), Box<dyn Erro
.on_conflict((implementations::language_id, implementations::identifier)) .on_conflict((implementations::language_id, implementations::identifier))
.do_update() .do_update()
.set(implementations::label.eq(&label)) .set(implementations::label.eq(&label))
.execute(&connection)?; .execute(connection)?;
for ( for (
i, i,
Wrapper { Wrapper {
...@@ -105,7 +136,7 @@ pub fn run(connection: impl Connection<Backend = Pg>) -> Result<(), Box<dyn Erro ...@@ -105,7 +136,7 @@ pub fn run(connection: impl Connection<Backend = Pg>) -> Result<(), Box<dyn Erro
implementation_wrappers::is_formatter.eq(is_formatter), implementation_wrappers::is_formatter.eq(is_formatter),
implementation_wrappers::ordering.eq(i), implementation_wrappers::ordering.eq(i),
)) ))
.execute(&connection)?; .execute(connection)?;
} }
} }
} }
......
use crate::schema::{implementation_wrappers, implementations, languages}; use crate::schema::{implementation_wrappers, implementations, languages, pastes};
use crate::Connection; use crate::Connection;
use diesel::prelude::*; use diesel::prelude::*;
use futures::Future; use futures::Future;
...@@ -8,6 +8,12 @@ use tokio_executor::blocking; ...@@ -8,6 +8,12 @@ use tokio_executor::blocking;
use warp::http::header::CACHE_CONTROL; use warp::http::header::CACHE_CONTROL;
use warp::{Rejection, Reply}; use warp::{Rejection, Reply};
#[derive(Queryable)]
struct Language {
id: i32,
paste_identifier: Option<String>,
}
#[derive(Serialize, Queryable)] #[derive(Serialize, Queryable)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct Wrapper { struct Wrapper {
...@@ -37,6 +43,7 @@ struct ImplementationWrapper { ...@@ -37,6 +43,7 @@ struct ImplementationWrapper {
#[derive(Serialize)] #[derive(Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct JsonLanguage { struct JsonLanguage {
hello_world_paste: Option<String>,
implementations: Vec<JsonImplementation>, implementations: Vec<JsonImplementation>,
} }
...@@ -51,16 +58,22 @@ pub fn api_language( ...@@ -51,16 +58,22 @@ pub fn api_language(
identifier: String, identifier: String,
) -> impl Future<Item = impl Reply, Error = Rejection> { ) -> impl Future<Item = impl Reply, Error = Rejection> {
blocking::run(move || { blocking::run(move || {
let id: i32 = languages::table let language: Language = languages::table
.filter(languages::identifier.eq(identifier)) .filter(languages::identifier.eq(identifier))
.select(languages::language_id) .select((
languages::language_id,
pastes::table
.select(pastes::identifier)
.filter(languages::hello_world_paste_id.eq(pastes::paste_id.nullable()))
.single_value(),
))
.get_result(&connection) .get_result(&connection)
.optional() .optional()
.map_err(warp::reject::custom)? .map_err(warp::reject::custom)?
.ok_or_else(warp::reject::not_found)?; .ok_or_else(warp::reject::not_found)?;
let implementations = implementations::table let implementations = implementations::table
.select((implementations::implementation_id, implementations::label)) .select((implementations::implementation_id, implementations::label))
.filter(implementations::language_id.eq(id)) .filter(implementations::language_id.eq(language.id))
.load(&connection) .load(&connection)
.map_err(warp::reject::custom)?; .map_err(warp::reject::custom)?;
let implementation_wrappers = ImplementationWrapper::belonging_to(&implementations) let implementation_wrappers = ImplementationWrapper::belonging_to(&implementations)
...@@ -103,7 +116,10 @@ pub fn api_language( ...@@ -103,7 +116,10 @@ pub fn api_language(
}) })
.collect(); .collect();
Ok(warp::reply::with_header( Ok(warp::reply::with_header(
warp::reply::json(&JsonLanguage { implementations }), warp::reply::json(&JsonLanguage {
implementations,
hello_world_paste: language.paste_identifier,
}),
CACHE_CONTROL, CACHE_CONTROL,
"max-age=14400", "max-age=14400",
)) ))
......
...@@ -18,7 +18,7 @@ pub fn display_paste( ...@@ -18,7 +18,7 @@ pub fn display_paste(
Paste::delete_old(connection)?; Paste::delete_old(connection)?;
let languages = Language::fetch(connection)?; let languages = Language::fetch(connection)?;
let paste: Paste = pastes::table let paste: Paste = pastes::table
.inner_join(languages::table) .inner_join(languages::table.on(pastes::language_id.eq(languages::language_id)))
.select(( .select((
pastes::paste, pastes::paste,
pastes::language_id, pastes::language_id,
......
...@@ -237,7 +237,7 @@ mod test { ...@@ -237,7 +237,7 @@ mod test {
))) )))
.expect("Couldn't create a connection connection"); .expect("Couldn't create a connection connection");
diesel_migrations::run_pending_migrations(&pool.get().unwrap()).unwrap(); diesel_migrations::run_pending_migrations(&pool.get().unwrap()).unwrap();
migration::run(pool.get().unwrap()).unwrap(); migration::run(&pool.get().unwrap()).unwrap();
routes(pool).map(Reply::into_response).boxed() routes(pool).map(Reply::into_response).boxed()
}; };
} }
...@@ -268,6 +268,22 @@ mod test { ...@@ -268,6 +268,22 @@ mod test {
.to_string() .to_string()
} }
#[derive(Debug, Deserialize, PartialEq)]
pub struct Implementation<'a> {
label: &'a str,
#[serde(borrow)]
wrappers: Vec<Wrapper<'a>>,
}
#[derive(Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Wrapper<'a> {
identifier: &'a str,
label: &'a str,
is_asm: bool,
is_formatter: bool,
}
#[test] #[test]
#[cfg_attr(not(feature = "database_tests"), ignore)] #[cfg_attr(not(feature = "database_tests"), ignore)]
fn test_language_api() { fn test_language_api() {
...@@ -276,23 +292,6 @@ mod test { ...@@ -276,23 +292,6 @@ mod test {
#[serde(borrow)] #[serde(borrow)]
implementations: Vec<Implementation<'a>>, implementations: Vec<Implementation<'a>>,
} }
#[derive(Debug, Deserialize, PartialEq)]
pub struct Implementation<'a> {
label: &'a str,
#[serde(borrow)]
wrappers: Vec<Wrapper<'a>>,
}
#[derive(Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Wrapper<'a> {
identifier: &'a str,
label: &'a str,
is_asm: bool,
is_formatter: bool,
}
let response = warp::test::request() let response = warp::test::request()
.path(&format!("/api/v0/language/{}", get_sh_id())) .path(&format!("/api/v0/language/{}", get_sh_id()))
.reply(&*ROUTES); .reply(&*ROUTES);
...@@ -331,4 +330,62 @@ mod test { ...@@ -331,4 +330,62 @@ mod test {
"abc" "abc"
); );
} }
#[test]
#[cfg_attr(not(feature = "sandbox_tests"), ignore)]
fn test_sandbox() {
#[derive(Deserialize)]
struct LanguageIdentifier<'a> {
identifier: &'a str,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ApiLanguage<'a> {
hello_world_paste: Option<String>,
#[serde(borrow)]
implementations: Vec<Implementation<'a>>,
}
let languages = warp::test::request()
.path("/api/v1/languages")
.reply(&*ROUTES);
let languages =
serde_json::from_slice::<Vec<LanguageIdentifier>>(languages.body()).unwrap();
for LanguageIdentifier { identifier } in languages {
let language = warp::test::request()
.path(&format!("/api/v0/language/{}", identifier))
.reply(&*ROUTES);
if let ApiLanguage {
hello_world_paste: Some(hello_world_paste),
implementations,
} = serde_json::from_slice(language.body()).unwrap()
{
let code = warp::test::request()
.path(&format!("/{}.txt", hello_world_paste))
.reply(&*ROUTES);
let wrappers = implementations
.into_iter()
.flat_map(|i| i.wrappers)
.filter(|w| w.label == "Run");
for Wrapper { identifier, .. } in wrappers {
let body = format!(
"code={}&compilerOptions=&stdin=",
str::from_utf8(code.body()).unwrap()
);
let out = warp::test::request()
.path(&format!("/api/v0/run/{}", identifier))
.method("POST")
.header(CONTENT_LENGTH, body.len())
.body(body)
.reply(&*ROUTES);
let body = str::from_utf8(out.body()).unwrap();
assert!(
body.contains(r#"Hello, world!\n""#),
"{}: {}",
identifier,
body,
);
}
}
}
}
} }
...@@ -26,6 +26,7 @@ table! { ...@@ -26,6 +26,7 @@ table! {
priority -> Int4, priority -> Int4,
name -> Text, name -> Text,
identifier -> Text, identifier -> Text,
hello_world_paste_id -> Nullable<Int4>,
} }
} }
...@@ -46,6 +47,5 @@ table! { ...@@ -46,6 +47,5 @@ table! {
joinable!(implementation_wrappers -> implementations (implementation_id)); joinable!(implementation_wrappers -> implementations (implementation_id));
joinable!(implementations -> languages (language_id)); joinable!(implementations -> languages (language_id));
joinable!(pastes -> languages (language_id));
allow_tables_to_appear_in_same_query!(implementations, implementation_wrappers, languages, pastes,); allow_tables_to_appear_in_same_query!(implementations, implementation_wrappers, languages, pastes,);
...@@ -15,9 +15,10 @@ ...@@ -15,9 +15,10 @@
This paste will be automatically deleted on @delete_at.format("%Y-%m-%d %H:%M") UTC. This paste will be automatically deleted on @delete_at.format("%Y-%m-%d %H:%M") UTC.
</span> </span>
} }
<label id="automatically-hidden-label"> <span id="automatically-hidden-label">
<input type=checkbox name=autodelete checked> Automatically delete after 24 hours <label><input type=checkbox name=autodelete checked> Automatically delete after 24 hours</label>
</label> <span id=hello-world></span>
</span>
</p> </p>
<p><textarea id=code name=code>@('\n')@paste.paste</textarea></p> <p><textarea id=code name=code>@('\n')@paste.paste</textarea></p>
@:buttons() @:buttons()
......
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
<label> <label>
<input type=checkbox name=autodelete checked> Automatically delete after 24 hours <input type=checkbox name=autodelete checked> Automatically delete after 24 hours
</label> </label>
<span id=hello-world></span>
</p> </p>
<p><textarea id=code name=code></textarea></p> <p><textarea id=code name=code></textarea></p>
@:buttons() @:buttons()
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment