diff --git a/.gitignore b/.gitignore index 7544000d9a..713f4165b6 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ node_modules ignored-assets main.wasm .parcel-cache +build/ diff --git a/crates/config/src/provider/env.rs b/crates/config/src/provider/env.rs index 3475f3e212..ba8f48a0bf 100644 --- a/crates/config/src/provider/env.rs +++ b/crates/config/src/provider/env.rs @@ -42,7 +42,6 @@ impl Provider for EnvProvider { #[cfg(test)] mod test { use super::*; - #[test] fn provider_get() { std::env::set_var("TESTING_SPIN_ENV_KEY1", "val"); diff --git a/examples/http-assemblyscript/index.ts b/examples/http-assemblyscript/index.ts new file mode 100644 index 0000000000..326d2c02df --- /dev/null +++ b/examples/http-assemblyscript/index.ts @@ -0,0 +1,15 @@ +import { Console } from 'as-wasi'; +import { handleRequest, Request, Response, ResponseBuilder, StatusCode } from '../../sdk/assemblyscript'; + +export function _start(): void { + handleRequest((request: Request): Response => { + for (var i = 0; i < request.headers.size; i++) { + Console.error("Key: " + request.headers.keys()[i]); + Console.error("Value: " + request.headers.values()[i] + "\n"); + } + return new ResponseBuilder(StatusCode.FORBIDDEN) + .header("content-type", "text/plain") + .header("foo", "bar") + .body(String.UTF8.encode("Hello, Fermyon!\n")); + }); +} diff --git a/examples/http-assemblyscript/package-lock.json b/examples/http-assemblyscript/package-lock.json new file mode 100644 index 0000000000..fc252998f2 --- /dev/null +++ b/examples/http-assemblyscript/package-lock.json @@ -0,0 +1,78 @@ +{ + "name": "@fermyon/spin-assemblyscript-sample", + "version": "0.1.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "@fermyon/spin-assemblyscript-sample", + "version": "0.1.0", + "license": "Apache V2", + "dependencies": { + "as-wasi": "0.4.4", + "assemblyscript": "^0.18.16" + } + }, + "node_modules/as-wasi": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/as-wasi/-/as-wasi-0.4.4.tgz", + "integrity": "sha512-CNeZ3AjKSjrFXRNDRzX1VzxsWz3Fwksn4g0J7tZv5RKz4agKhVlcl0QeMIOOkJms7DujCBCpbelGxNDtvlFKmw==" + }, + "node_modules/assemblyscript": { + "version": "0.18.32", + "resolved": "https://registry.npmjs.org/assemblyscript/-/assemblyscript-0.18.32.tgz", + "integrity": "sha512-Py6zremwGhO3nSoI/VxyVUzTZfNhTjzNzFDaUdG4JhPJHeG+FzVlEoNCrw4bE5nPc7F+P2DJ8tZQCqIt15ceKw==", + "dependencies": { + "binaryen": "100.0.0-nightly.20210413", + "long": "^4.0.0" + }, + "bin": { + "asc": "bin/asc", + "asinit": "bin/asinit" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/assemblyscript" + } + }, + "node_modules/binaryen": { + "version": "100.0.0-nightly.20210413", + "resolved": "https://registry.npmjs.org/binaryen/-/binaryen-100.0.0-nightly.20210413.tgz", + "integrity": "sha512-EeGLIxQmJS0xnYl+SH34mNBqVMoixKd9nsE7S7z+CtS9A4eoWn3Qjav+XElgunUgXIHAI5yLnYT2TUGnLX2f1w==", + "bin": { + "wasm-opt": "bin/wasm-opt" + } + }, + "node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + } + }, + "dependencies": { + "as-wasi": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/as-wasi/-/as-wasi-0.4.4.tgz", + "integrity": "sha512-CNeZ3AjKSjrFXRNDRzX1VzxsWz3Fwksn4g0J7tZv5RKz4agKhVlcl0QeMIOOkJms7DujCBCpbelGxNDtvlFKmw==" + }, + "assemblyscript": { + "version": "0.18.32", + "resolved": "https://registry.npmjs.org/assemblyscript/-/assemblyscript-0.18.32.tgz", + "integrity": "sha512-Py6zremwGhO3nSoI/VxyVUzTZfNhTjzNzFDaUdG4JhPJHeG+FzVlEoNCrw4bE5nPc7F+P2DJ8tZQCqIt15ceKw==", + "requires": { + "binaryen": "100.0.0-nightly.20210413", + "long": "^4.0.0" + } + }, + "binaryen": { + "version": "100.0.0-nightly.20210413", + "resolved": "https://registry.npmjs.org/binaryen/-/binaryen-100.0.0-nightly.20210413.tgz", + "integrity": "sha512-EeGLIxQmJS0xnYl+SH34mNBqVMoixKd9nsE7S7z+CtS9A4eoWn3Qjav+XElgunUgXIHAI5yLnYT2TUGnLX2f1w==" + }, + "long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + } + } +} diff --git a/examples/http-assemblyscript/package.json b/examples/http-assemblyscript/package.json new file mode 100644 index 0000000000..49c691c119 --- /dev/null +++ b/examples/http-assemblyscript/package.json @@ -0,0 +1,20 @@ +{ + "name": "@fermyon/spin-assemblyscript-sample", + "main": "index.ts", + "ascMain": "index.ts", + "types": "index.ts", + "version": "0.1.0", + "description": "A lightweight Spin HTTP handler in AssemblyScript", + "author": { + "name": "Fermyon Engineering", + "email": "engineering@fermyon.com" + }, + "license": "Apache V2", + "scripts": { + "asbuild": "asc index.ts -b build/optimized.wasm --use abort=wasi_abort --debug" + }, + "dependencies": { + "as-wasi": "0.4.4", + "assemblyscript": "^0.18.16" + } +} diff --git a/examples/http-assemblyscript/spin.toml b/examples/http-assemblyscript/spin.toml new file mode 100644 index 0000000000..fe495735cb --- /dev/null +++ b/examples/http-assemblyscript/spin.toml @@ -0,0 +1,13 @@ +spin_version = "1" +authors = ["Fermyon Engineering "] +description = "A simple Spin application written in AssemblyScript" +name = "spinass" +trigger = { type = "http", base = "/" } +version = "1.0.0" + +[[component]] +id = "as" +source = "build/optimized.wasm" +[component.trigger] +route = "/hello" +executor = { type = "wagi" } diff --git a/examples/http-assemblyscript/tsconfig.json b/examples/http-assemblyscript/tsconfig.json new file mode 100644 index 0000000000..09e68496d7 --- /dev/null +++ b/examples/http-assemblyscript/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "./node_modules/assemblyscript/std/assembly.json", + "include": [ + "./**/*.ts" + ] +} diff --git a/sdk/assemblyscript/http/cgi.ts b/sdk/assemblyscript/http/cgi.ts new file mode 100644 index 0000000000..8eaed47e40 --- /dev/null +++ b/sdk/assemblyscript/http/cgi.ts @@ -0,0 +1,85 @@ +import * as wasi from 'as-wasi'; +import { Method, Request, Response } from "./http"; + +export function requestFromCgi(): Request { + let uri = requestUriFromEnv(); + let method = requestMethodFromEnv(); + let headers = requestHeadersFromEnv(); + // let body = requestBodyFromEnv(); + + return new Request(uri, method, headers); +} + +export function sendResponse(response: Response): void { + printHeaders(response); + // wasi.Descriptor.Stdout.write(changetype(response.body)); +} + +function requestHeadersFromEnv(): Map { + let res = new Map(); + let env = new wasi.Environ().all(); + + for (var i = 0; i < env.length; i++) { + // only set as request headers the environment variables that start with HTTP_ + if (env[i].key.startsWith("HTTP_")) { + res.set(env[i].key.replace("HTTP_", "").toLowerCase(), env[i].value.toLowerCase()); + } + } + + return res; +} + +/** Return the request body from the standard input. */ +function requestBodyFromEnv(): ArrayBuffer { + let bytes = wasi.Descriptor.Stdin.readAll() || []; + if (bytes !== null) { + wasi.Console.error("Body size: " + bytes.length.toString()); + } + return changetype(bytes); +} + +function requestUriFromEnv(): string { + let url = new wasi.Environ().get("X_FULL_URL"); + if (url !== null) { + return url; + } else { + return ""; + } +} + +function requestMethodFromEnv(): Method { + let method = new wasi.Environ().get("REQUEST_METHOD"); + if (method !== null) { + return Method.parse(method); + } else { + return Method.GET; + } +} + + +function printHeaders(response: Response): void { + let location = searchCaseInsensitive("location", response.headers); + if (location !== null) { + wasi.Console.write("Location: " + location, true); + } + let contentType = searchCaseInsensitive("content-type", response.headers); + if (contentType !== null) { + wasi.Console.write("Content-Type: " + contentType, true); + } + + for (var i = 0; i < response.headers.size; i++) { + wasi.Console.write(response.headers.keys()[i] + ": " + response.headers.values()[i]); + } + wasi.Console.write("Status: " + response.status.toString()); + wasi.Console.write("\n"); +} + +function searchCaseInsensitive(key: string, map: Map): string | null { + for (var i = 0; i < map.size; i++) { + if (map.keys()[i].toLowerCase() === key.toLowerCase()) { + return map.values()[i].toLowerCase(); + } + } + + return null; +} diff --git a/sdk/assemblyscript/http/http.ts b/sdk/assemblyscript/http/http.ts new file mode 100644 index 0000000000..4ea8211e83 --- /dev/null +++ b/sdk/assemblyscript/http/http.ts @@ -0,0 +1,238 @@ +export { requestFromCgi, sendResponse } from "./cgi"; + +export type Handler = (request: Request) => Response; + + +/** An HTTP request. */ +export class Request { + /** The URL of the request. */ + public url: string; + /** The HTTP method of the request. */ + public method: Method; + /** The request headers. */ + public headers: Map; + /** The request body as bytes. */ + public body: ArrayBuffer; + + constructor( + url: string, + method: Method = Method.GET, + headers: Map = new Map(), + body: ArrayBuffer = new ArrayBuffer(0) + ) { + this.url = url; + this.method = method; + this.headers = headers; + this.body = body; + } +} + +/** An HTTP request builder. */ +export class RequestBuilder { + private request: Request; + + constructor(url: string) { + this.request = new Request(url); + } + + /** Set the request's HTTP method. */ + public method(m: Method): RequestBuilder { + this.request.method = m; + return this; + } + + /** Add a new pair of header key and header value to the request. */ + public header(key: string, value: string): RequestBuilder { + this.request.headers.set(key, value); + return this; + } + + /** Set the request's body. */ + public body(b: ArrayBuffer): RequestBuilder { + this.request.body = b; + return this; + } + + /** Send the request and return an HTTP response. */ + public send(): Response { + return new Response(StatusCode.OK); + } +} + +/** An HTTP response. */ +export class Response { + /** The HTTP response status code. */ + public status: StatusCode; + /** The response headers. */ + public headers: Map; + /** The response body */ + public body: ArrayBuffer; + + public constructor( + status: StatusCode, + headers: Map = new Map(), + body: ArrayBuffer = new ArrayBuffer(0) + ) { + this.status = status; + this.headers = headers; + this.body = body; + } +} + +/** An HTTP response builder. */ +export class ResponseBuilder { + private response: Response; + + constructor(status: StatusCode) { + this.response = new Response(status); + } + + /** Add a new pair of header key and header value to the response. */ + public header(key: string, value: string): ResponseBuilder { + this.response.headers.set(key, value); + return this; + } + + /** Set the response body and get the actual response. */ + public body(body: ArrayBuffer = new ArrayBuffer(0)): Response { + this.response.body = changetype(body); + return this.response; + } +} + +/** The standard HTTP methods. */ +export enum Method { + GET, + HEAD, + POST, + PUT, + DELETE, + CONNECT, + OPTIONS, + TRACE, + PATCH, +} + +export namespace Method { + /** Parse a string into an HTTP method. */ + export function parse(m: string): Method { + if (m == "GET" || m == "get") { + return Method.GET; + } else if (m == "HEAD" || m == "head") { + return Method.HEAD; + } else if (m == "POST" || m == "post") { + return Method.POST; + } else if (m == "PUT" || m == "put") { + return Method.PUT; + } else if (m == "DELETE" || m == "delete") { + return Method.DELETE; + } else if (m == "CONNECT" || m == "connect") { + return Method.CONNECT; + } else if (m == "OPTIONS" || m == "options") { + return Method.OPTIONS; + } else if (m == "TRACE" || m == "trace") { + return Method.TRACE; + } else if (m == "PATCH" || m == "patch") { + return Method.PATCH; + } else { + return Method.GET; + } + } + + /** Convert an HTTP method into a string. */ + export function from(m: Method): string { + switch (m) { + case Method.GET: + return "GET"; + case Method.HEAD: + return "HEAD"; + case Method.POST: + return "POST"; + case Method.PUT: + return "PUT"; + case Method.DELETE: + return "DELET"; + case Method.CONNECT: + return "CONNECT"; + case Method.OPTIONS: + return "OPTIONS"; + case Method.TRACE: + return "TRACE"; + case Method.PATCH: + return "PATCH"; + default: + return ""; + } + } +} + +/** The standard HTTP status codes. */ +export enum StatusCode { + CONTINUE = 100, + SWITCHING_PROTOCOL = 101, + PROCESSING = 102, + EARLY_HINTS = 103, + + OK = 200, + CREATED = 201, + ACCEPTED = 202, + NON_AUTHORITATIVE_INFORMATION = 203, + NO_CONTENT = 204, + RESET_CONTENT = 205, + PARTIAL_CONTENT = 206, + MULTI_STATUS = 207, + ALREADY_REPORTED = 208, + IM_USED = 226, + + MULTIPLE_CHOICE = 300, + MOVED_PERMANENTLY = 301, + FOUND = 302, + SEE_OTHER = 303, + NOT_MODIFIED = 304, + USE_PROXY = 305, + UNUSED = 306, + TEMPORARY_REDIRECT = 307, + PERMANENT_REDIRECT = 308, + + BAD_REQUEST = 400, + UNAUTHORIZED = 401, + PAYMENT_REQUIRED = 402, + FORBIDDEN = 403, + NOT_FOUND = 404, + METHOD_NOT_ALLOWED = 405, + NOT_ACCEPTABLE = 406, + PROXY_AUTHENTICATION_REQUIRED = 407, + REQUEST_TIMEOUT = 408, + CONFLICT = 409, + GONE = 410, + LENGTH_REQUIRED = 411, + PRECONDITION_FAILED = 412, + PAYLOAD_TOO_LARGE = 413, + URI_TOO_LONG = 414, + UNSUPPORTED_MEDIA_TYPE = 415, + RANGE_NOT_SATISFIABLE = 416, + EXPECTATION_FAILED = 417, + IM_A_TEAPOT = 418, + MISDIRECTED_REQUEST = 421, + UNPROCESSABLE_ENTITY = 422, + LOCKED = 423, + FAILED_DEPENDENCY = 424, + TOO_EARLY = 425, + UPGRADE_REQUIRED = 426, + PRECONDITION_REQURIED = 428, + TOO_MANY_REQUESTS = 429, + REQUEST_HEADER_FIELDS_TOO_LARGE = 431, + UNAVAILABLE_FOR_LEGAL_REASONS = 451, + + INTERNAL_SERVER_ERROR = 500, + NOT_IMPLELENTED = 501, + BAD_GATEWAY = 502, + SERVICE_UNAVAILABLE = 503, + GATEWAY_TIMEOUT = 504, + HTTP_VERSION_NOT_SUPPORTED = 505, + VARIANT_ALSO_NEGOTIATES = 506, + INSUFFICIENT_STORAGE = 507, + LOOP_DETECTED = 508, + NOT_EXTENDED = 510, + NETWORK_AUTHENTICATION_REQUIRED = 511, +} diff --git a/sdk/assemblyscript/index.ts b/sdk/assemblyscript/index.ts new file mode 100644 index 0000000000..b4bcdb7769 --- /dev/null +++ b/sdk/assemblyscript/index.ts @@ -0,0 +1,9 @@ +import { Console } from 'as-wasi'; +import * as http from './http/http'; + +export * from './http/http'; + +export function handleRequest(handler: http.Handler): void { + let response = handler(http.requestFromCgi()); + http.sendResponse(response); +} diff --git a/sdk/assemblyscript/package-lock.json b/sdk/assemblyscript/package-lock.json new file mode 100644 index 0000000000..a36cc544d9 --- /dev/null +++ b/sdk/assemblyscript/package-lock.json @@ -0,0 +1,78 @@ +{ + "name": "@fermyon/spin-sdk-as", + "version": "0.9.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "@fermyon/spin-sdk-as", + "version": "0.9.0", + "license": "MIT", + "dependencies": { + "as-wasi": "0.4.4", + "assemblyscript": "^0.18.16" + } + }, + "node_modules/as-wasi": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/as-wasi/-/as-wasi-0.4.4.tgz", + "integrity": "sha512-CNeZ3AjKSjrFXRNDRzX1VzxsWz3Fwksn4g0J7tZv5RKz4agKhVlcl0QeMIOOkJms7DujCBCpbelGxNDtvlFKmw==" + }, + "node_modules/assemblyscript": { + "version": "0.18.32", + "resolved": "https://registry.npmjs.org/assemblyscript/-/assemblyscript-0.18.32.tgz", + "integrity": "sha512-Py6zremwGhO3nSoI/VxyVUzTZfNhTjzNzFDaUdG4JhPJHeG+FzVlEoNCrw4bE5nPc7F+P2DJ8tZQCqIt15ceKw==", + "dependencies": { + "binaryen": "100.0.0-nightly.20210413", + "long": "^4.0.0" + }, + "bin": { + "asc": "bin/asc", + "asinit": "bin/asinit" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/assemblyscript" + } + }, + "node_modules/binaryen": { + "version": "100.0.0-nightly.20210413", + "resolved": "https://registry.npmjs.org/binaryen/-/binaryen-100.0.0-nightly.20210413.tgz", + "integrity": "sha512-EeGLIxQmJS0xnYl+SH34mNBqVMoixKd9nsE7S7z+CtS9A4eoWn3Qjav+XElgunUgXIHAI5yLnYT2TUGnLX2f1w==", + "bin": { + "wasm-opt": "bin/wasm-opt" + } + }, + "node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + } + }, + "dependencies": { + "as-wasi": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/as-wasi/-/as-wasi-0.4.4.tgz", + "integrity": "sha512-CNeZ3AjKSjrFXRNDRzX1VzxsWz3Fwksn4g0J7tZv5RKz4agKhVlcl0QeMIOOkJms7DujCBCpbelGxNDtvlFKmw==" + }, + "assemblyscript": { + "version": "0.18.32", + "resolved": "https://registry.npmjs.org/assemblyscript/-/assemblyscript-0.18.32.tgz", + "integrity": "sha512-Py6zremwGhO3nSoI/VxyVUzTZfNhTjzNzFDaUdG4JhPJHeG+FzVlEoNCrw4bE5nPc7F+P2DJ8tZQCqIt15ceKw==", + "requires": { + "binaryen": "100.0.0-nightly.20210413", + "long": "^4.0.0" + } + }, + "binaryen": { + "version": "100.0.0-nightly.20210413", + "resolved": "https://registry.npmjs.org/binaryen/-/binaryen-100.0.0-nightly.20210413.tgz", + "integrity": "sha512-EeGLIxQmJS0xnYl+SH34mNBqVMoixKd9nsE7S7z+CtS9A4eoWn3Qjav+XElgunUgXIHAI5yLnYT2TUGnLX2f1w==" + }, + "long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + } + } +} diff --git a/sdk/assemblyscript/package.json b/sdk/assemblyscript/package.json new file mode 100644 index 0000000000..d1efb6fb71 --- /dev/null +++ b/sdk/assemblyscript/package.json @@ -0,0 +1,28 @@ +{ + "name": "@fermyon/spin-sdk-as", + "main": "index.ts", + "ascMain": "index.ts", + "types": "index.ts", + "version": "0.1.0", + "description": "A lightweight Spin SDK for AssemblyScript", + "author": { + "name": "Fermyon Engineering", + "email": "engineering@fermyon.com" + }, + "homepage": "https://github.com/fermyon/spin", + "repository": { + "type": "git", + "url": "https://github.com/fermyon/spin" + }, + "bugs": { + "url": "https://github.com/fermyon/spin/issues" + }, + "license": "Apache V2", + "scripts": { + "asbuild": "asc index.ts -b build/optimized.wasm --use abort=wasi_abort --debug" + }, + "dependencies": { + "as-wasi": "0.4.4", + "assemblyscript": "^0.18.16" + } +} diff --git a/sdk/assemblyscript/tsconfig.json b/sdk/assemblyscript/tsconfig.json new file mode 100644 index 0000000000..09e68496d7 --- /dev/null +++ b/sdk/assemblyscript/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "./node_modules/assemblyscript/std/assembly.json", + "include": [ + "./**/*.ts" + ] +}