Add "Hello, world" examples

This commit is contained in:
Konrad Borowski 2019-11-24 12:43:02 +01:00
parent b791aba860
commit adeb13189b
13 changed files with 168 additions and 37 deletions

View File

@ -34,7 +34,7 @@ walkdir = "2.2.9"
[dev-dependencies]
scraper = "0.10.1"
serde_json = "1.0.40"
[features]
database_tests = []
sandbox_tests = []

View File

@ -11,6 +11,7 @@ class Editor {
output: Output
autodeleteText: HTMLSpanElement
autodeleteCheckbox: HTMLLabelElement
helloWorldLink: HTMLSpanElement
submit: HTMLInputElement
detailsElement: HTMLDetailsElement
stdinElement: HTMLTextAreaElement
@ -31,11 +32,12 @@ class Editor {
this.output.display({}, {
stdout: stdout.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.autodeleteCheckbox = form.querySelector('#automatically-hidden-label')
this.helloWorldLink = form.querySelector('#hello-world')
this.submit = form.querySelector('[type=submit]')
this.submit.disabled = true
form.addEventListener('submit', () => {
@ -58,7 +60,7 @@ class Editor {
this.stdinElement.name = 'stdin'
this.stdinElement.addEventListener('change', () => this.changeToLookLikeNewPaste())
this.detailsElement.append(summary, this.stdinElement)
const dbStdin = document.querySelector<HTMLInputElement>('#dbstdin')?.value
const dbStdin = document.querySelector<HTMLInputElement>('#dbstdin') ?.value
if (dbStdin) {
this.stdinElement.value = dbStdin
this.detailsElement.open = true
@ -112,12 +114,19 @@ class Editor {
async updateLanguage() {
this.wrapperButtons.clear()
this.helloWorldLink.textContent = ''
const identifier = this.getLanguageIdentifier()
this.setLanguage(identifier)
const isStillValid = () => identifier === this.getLanguageIdentifier()
const language = await getLanguage(identifier, isStillValid)
// This deals with user changing the language after asynchronous event
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.wrapperButtons.update(language.implementations)
}

View File

@ -6,6 +6,7 @@
{
"identifier": "c",
"name": "C",
"helloworld": "#include <stdio.h>\n\nint main(void) {\n puts(\"Hello, world!\");\n return 0;\n}",
"implementations": [
{
"label": "Clang",
@ -34,6 +35,7 @@
{
"identifier": "cpp",
"name": "C++",
"helloworld": "#include <iostream>\n\nint main(void) {\n std::cout << \"Hello, world!\\n\";\n}",
"implementations": [
{
"label": "Clang",
@ -62,6 +64,7 @@
{
"identifier": "csharp",
"name": "C#",
"helloworld": "using System;\n\nclass Program {\n static void Main(string[] args) {\n Console.WriteLine(\"Hello, world!\");\n }\n}",
"implementations": [
{
"label": ".NET Core",
@ -79,6 +82,7 @@
{
"identifier": "haskell",
"name": "Haskell",
"helloworld": "main = putStrLn \"Hello, world!\"",
"implementations": [
{
"label": "GHC",
@ -100,6 +104,7 @@
{
"identifier": "java",
"name": "Java",
"helloworld": "class HelloWorld {\n public static void main(String[] args) {\n System.out.println(\"Hello, world!\");\n }\n}",
"implementations": [
{
"label": "OpenJDK 8",
@ -128,6 +133,7 @@
{
"identifier": "javascript",
"name": "JavaScript",
"helloworld": "console.log(\"Hello, world!\");",
"implementations": [
{
"label": "Node.js",
@ -157,6 +163,7 @@
{
"identifier": "perl",
"name": "Perl",
"helloworld": "use v5.12;\nuse warnings;\n\nsay \"Hello, world!\";",
"implementations": [
{
"label": "Perl",
@ -174,6 +181,7 @@
{
"identifier": "php",
"name": "PHP",
"helloworld": "<?php\necho \"Hello, world!\\n\";",
"implementations": [
{
"label": "PHP",
@ -199,6 +207,7 @@
{
"identifier": "python",
"name": "Python 3",
"helloworld": "print(\"Hello, world!\")",
"implementations": [
{
"label": "CPython",
@ -222,6 +231,7 @@
{
"identifier": "raku",
"name": "Raku",
"helloworld": "say \"Hello, world!\"",
"implementations": [
{
"label": "Rakudo",
@ -239,6 +249,7 @@
{
"identifier": "rust",
"name": "Rust",
"helloworld": "fn main() {\n println!(\"Hello, world!\");\n}",
"implementations": [
{
"label": "Stable",
@ -268,6 +279,7 @@
{
"identifier": "sh",
"name": "Sh",
"helloworld": "echo Hello, world!",
"implementations": [
{
"label": "sh",
@ -289,6 +301,7 @@
{
"identifier": "sqlite",
"name": "SQLite",
"helloworld": "SELECT 'Hello, world!'",
"implementations": [
{
"label": "SQLite",
@ -306,6 +319,7 @@
{
"identifier": "typescript",
"name": "TypeScript",
"helloworld": "console.log(\"Hello, world!\");",
"implementations": [
{
"label": "Node.js",

View File

@ -0,0 +1 @@
ALTER TABLE languages DROP COLUMN hello_world_paste_id;

View File

@ -0,0 +1 @@
ALTER TABLE languages ADD COLUMN hello_world_paste_id int REFERENCES pastes;

View File

@ -19,7 +19,7 @@ fn main() -> Result<(), Box<dyn Error>> {
let pool = Pool::new(ConnectionManager::new(database_url))
.expect("Couldn't create a connection connection");
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));
Ok(())
}

View File

@ -1,5 +1,6 @@
use crate::schema::{implementation_wrappers, implementations, languages};
use diesel::pg::Pg;
use crate::models::paste;
use crate::schema::{implementation_wrappers, implementations, languages, pastes};
use crate::Connection;
use diesel::prelude::*;
use diesel::sql_types::{Bool, Integer, Text};
use serde::Deserialize;
@ -9,6 +10,7 @@ use std::fs;
#[derive(Deserialize)]
struct Language {
identifier: String,
helloworld: Option<String>,
#[serde(default)]
implementations: Vec<Implementation>,
}
@ -32,13 +34,42 @@ struct Wrapper {
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")?)?;
for Language {
identifier: languages_identifier,
helloworld,
implementations,
} 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 {
label,
identifier: implementation_identifier,
@ -61,7 +92,7 @@ pub fn run(connection: impl Connection<Backend = Pg>) -> Result<(), Box<dyn Erro
.on_conflict((implementations::language_id, implementations::identifier))
.do_update()
.set(implementations::label.eq(&label))
.execute(&connection)?;
.execute(connection)?;
for (
i,
Wrapper {
@ -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::ordering.eq(i),
))
.execute(&connection)?;
.execute(connection)?;
}
}
}

View File

@ -1,4 +1,4 @@
use crate::schema::{implementation_wrappers, implementations, languages};
use crate::schema::{implementation_wrappers, implementations, languages, pastes};
use crate::Connection;
use diesel::prelude::*;
use futures::Future;
@ -8,6 +8,12 @@ use tokio_executor::blocking;
use warp::http::header::CACHE_CONTROL;
use warp::{Rejection, Reply};
#[derive(Queryable)]
struct Language {
id: i32,
paste_identifier: Option<String>,
}
#[derive(Serialize, Queryable)]
#[serde(rename_all = "camelCase")]
struct Wrapper {
@ -37,6 +43,7 @@ struct ImplementationWrapper {
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct JsonLanguage {
hello_world_paste: Option<String>,
implementations: Vec<JsonImplementation>,
}
@ -51,16 +58,22 @@ pub fn api_language(
identifier: String,
) -> impl Future<Item = impl Reply, Error = Rejection> {
blocking::run(move || {
let id: i32 = languages::table
let language: Language = languages::table
.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)
.optional()
.map_err(warp::reject::custom)?
.ok_or_else(warp::reject::not_found)?;
let implementations = implementations::table
.select((implementations::implementation_id, implementations::label))
.filter(implementations::language_id.eq(id))
.filter(implementations::language_id.eq(language.id))
.load(&connection)
.map_err(warp::reject::custom)?;
let implementation_wrappers = ImplementationWrapper::belonging_to(&implementations)
@ -103,7 +116,10 @@ pub fn api_language(
})
.collect();
Ok(warp::reply::with_header(
warp::reply::json(&JsonLanguage { implementations }),
warp::reply::json(&JsonLanguage {
implementations,
hello_world_paste: language.paste_identifier,
}),
CACHE_CONTROL,
"max-age=14400",
))

View File

@ -18,7 +18,7 @@ pub fn display_paste(
Paste::delete_old(connection)?;
let languages = Language::fetch(connection)?;
let paste: Paste = pastes::table
.inner_join(languages::table)
.inner_join(languages::table.on(pastes::language_id.eq(languages::language_id)))
.select((
pastes::paste,
pastes::language_id,

View File

@ -237,7 +237,7 @@ mod test {
)))
.expect("Couldn't create a connection connection");
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()
};
}
@ -268,6 +268,22 @@ mod test {
.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]
#[cfg_attr(not(feature = "database_tests"), ignore)]
fn test_language_api() {
@ -276,23 +292,6 @@ mod test {
#[serde(borrow)]
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()
.path(&format!("/api/v0/language/{}", get_sh_id()))
.reply(&*ROUTES);
@ -331,4 +330,62 @@ mod test {
"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,
);
}
}
}
}
}

View File

@ -26,6 +26,7 @@ table! {
priority -> Int4,
name -> Text,
identifier -> Text,
hello_world_paste_id -> Nullable<Int4>,
}
}
@ -46,6 +47,5 @@ table! {
joinable!(implementation_wrappers -> implementations (implementation_id));
joinable!(implementations -> languages (language_id));
joinable!(pastes -> languages (language_id));
allow_tables_to_appear_in_same_query!(implementations, implementation_wrappers, languages, pastes,);

View File

@ -15,9 +15,10 @@
This paste will be automatically deleted on @delete_at.format("%Y-%m-%d %H:%M") UTC.
</span>
}
<label id="automatically-hidden-label">
<input type=checkbox name=autodelete checked> Automatically delete after 24 hours
</label>
<span id="automatically-hidden-label">
<label><input type=checkbox name=autodelete checked> Automatically delete after 24 hours</label>
<span id=hello-world></span>
</span>
</p>
<p><textarea id=code name=code>@('\n')@paste.paste</textarea></p>
@:buttons()

View File

@ -11,6 +11,7 @@
<label>
<input type=checkbox name=autodelete checked> Automatically delete after 24 hours
</label>
<span id=hello-world></span>
</p>
<p><textarea id=code name=code></textarea></p>
@:buttons()