diff --git a/build.gradle b/build.gradle index 8c65cb3..386aa77 100644 --- a/build.gradle +++ b/build.gradle @@ -6,6 +6,10 @@ ext { socket: '1.0.2', // io.socket:socket.io-client jline: '2.14.5', // jline:jline + javalin: '5.6.3', // io.javalin:javalin + slf4j: '2.0.9', // org.slf4j:slf4j-simple + jackson: '2.15.3', // com.fasterxml.jackson.core:jackson-databind + junit: '5.9.2', // org.junit:junit-bom jmh: '1.37', // org.openjdk.jmh:jmh-core assertj: '3.24.2' // org.assertj:assertj-core diff --git a/modules/server/build.gradle b/modules/server/build.gradle new file mode 100644 index 0000000..80f0570 --- /dev/null +++ b/modules/server/build.gradle @@ -0,0 +1,21 @@ +plugins { + id 'java-library' + id 'com.github.johnrengelman.shadow' version '8.1.1' +} + +group = 'com.annimon.module' +version = '2.0-SNAPSHOT' + +dependencies { + compileOnlyApi project(":ownlang-core") + implementation "io.javalin:javalin:${versions.javalin}" + implementation "org.slf4j:slf4j-simple:${versions.slf4j}" + implementation "com.fasterxml.jackson.core:jackson-databind:${versions.jackson}" + + testImplementation platform("org.junit:junit-bom:") + testImplementation 'org.junit.jupiter:junit-jupiter' +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/modules/server/src/main/java/com/annimon/ownlang/modules/server/ContextValue.java b/modules/server/src/main/java/com/annimon/ownlang/modules/server/ContextValue.java new file mode 100644 index 0000000..6d8477c --- /dev/null +++ b/modules/server/src/main/java/com/annimon/ownlang/modules/server/ContextValue.java @@ -0,0 +1,91 @@ +package com.annimon.ownlang.modules.server; + +import com.annimon.ownlang.lib.*; +import io.javalin.http.Context; +import io.javalin.http.HttpStatus; +import org.jetbrains.annotations.NotNull; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; + +public class ContextValue extends MapValue { + + private final Context ctx; + + public ContextValue(@NotNull Context ctx) { + super(10); + this.ctx = ctx; + init(); + } + + private void init() { + set("body", Converters.voidToString(ctx::body)); + set("characterEncoding", Converters.voidToString(ctx::characterEncoding)); + set("contentType", Converters.voidToString(ctx::contentType)); + set("contextPath", Converters.voidToString(ctx::contextPath)); + set("fullUrl", Converters.voidToString(ctx::fullUrl)); + set("host", Converters.voidToString(ctx::host)); + set("ip", Converters.voidToString(ctx::ip)); + set("matchedPath", Converters.voidToString(ctx::matchedPath)); + set("path", Converters.voidToString(ctx::path)); + set("protocol", Converters.voidToString(ctx::protocol)); + set("queryString", Converters.voidToString(ctx::queryString)); + set("url", Converters.voidToString(ctx::url)); + set("userAgent", Converters.voidToString(ctx::userAgent)); + + set("contentLength", Converters.voidToInt(ctx::contentLength)); + set("port", Converters.voidToInt(ctx::port)); + set("statusCode", Converters.voidToInt(ctx::statusCode)); + + set("json", objectToContext(ctx::json)); + set("jsonStream", objectToContext(ctx::jsonStream)); + + set("render", this::render); + set("result", this::result); + set("redirect", this::redirect); + } + + private Value render(Value[] args) { + Arguments.checkOrOr(1, 2, args.length); + String filePath = args[0].asString(); + if (args.length == 1) { + ctx.render(filePath); + } else { + MapValue map = (MapValue) args[1]; + Map data = new HashMap<>(map.size()); + map.getMap().forEach((k, v) -> data.put(k.asString(), v.asJavaObject())); + ctx.render(filePath, data); + } + return this; + } + + private Value redirect(Value[] args) { + Arguments.checkOrOr(1, 2, args.length); + HttpStatus status = args.length == 1 ? HttpStatus.FOUND : HttpStatus.forStatus(args[1].asInt()); + ctx.redirect(args[0].asString(), status); + return this; + } + + private Value result(Value[] args) { + Arguments.checkOrOr(0, 1, args.length); + if (args.length == 0) { + return new StringValue(ctx.result()); + } else { + final var arg = args[0]; + if (arg.type() == Types.ARRAY) { + ctx.result(ValueUtils.toByteArray((ArrayValue) arg)); + } else { + ctx.result(arg.asString()); + } + return this; + } + } + + private Value objectToContext(Consumer consumer) { + return new FunctionValue(args -> { + Arguments.check(1, args.length); + consumer.accept(args[0].asJavaObject()); + return this; + }); + } +} diff --git a/modules/server/src/main/java/com/annimon/ownlang/modules/server/ServerValue.java b/modules/server/src/main/java/com/annimon/ownlang/modules/server/ServerValue.java new file mode 100644 index 0000000..b9b7a47 --- /dev/null +++ b/modules/server/src/main/java/com/annimon/ownlang/modules/server/ServerValue.java @@ -0,0 +1,86 @@ +package com.annimon.ownlang.modules.server; + +import com.annimon.ownlang.lib.*; +import io.javalin.Javalin; +import io.javalin.http.Handler; +import io.javalin.security.RouteRole; +import java.util.Arrays; + +public class ServerValue extends MapValue { + + private final Javalin server; + + public ServerValue(Javalin server) { + super(10); + this.server = server; + init(); + } + + private void init() { + set("get", httpMethod(server::get)); + set("post", httpMethod(server::post)); + set("put", httpMethod(server::put)); + set("patch", httpMethod(server::patch)); + set("head", httpMethod(server::head)); + set("delete", httpMethod(server::delete)); + set("options", httpMethod(server::options)); + set("error", this::error); + set("start", this::start); + } + + private Value error(Value[] args) { + Arguments.checkOrOr(2, 3, args.length); + final int handlerIndex; + final String contentType; + if (args.length == 2) { + contentType = "*"; + handlerIndex = 1; + } else { + contentType = args[1].asString(); + handlerIndex = 2; + } + int status = args[0].asInt(); + final Handler handler = toHandler(ValueUtils.consumeFunction(args[handlerIndex], handlerIndex)); + server.error(status, contentType, handler); + return this; + } + + private Value start(Value[] args) { + Arguments.checkRange(0, 2, args.length); + switch (args.length) { + case 0 -> server.start(); + case 1 -> server.start(args[0].asInt()); + case 2 -> server.start(args[0].asString(), args[1].asInt()); + } + return this; + } + + private FunctionValue httpMethod(HttpMethodHandler httpHandler) { + return new FunctionValue(args -> { + Arguments.checkAtLeast(2, args.length); + final String path = args[0].asString(); + final Handler handler = toHandler(ValueUtils.consumeFunction(args[1], 1)); + final Role[] roles; + if (args.length == 2) { + roles = new Role[0]; + } else { + roles = Arrays.stream(args) + .skip(2) + .map(Role::new) + .toArray(Role[]::new); + } + httpHandler.handle(path, handler, roles); + return this; + }); + } + + private Handler toHandler(Function function) { + return ctx -> function.execute(new ContextValue(ctx)); + } + + private interface HttpMethodHandler { + void handle(String path, Handler handler, RouteRole[] roles); + } + + private record Role(Value value) implements RouteRole { } +} diff --git a/modules/server/src/main/java/com/annimon/ownlang/modules/server/server.java b/modules/server/src/main/java/com/annimon/ownlang/modules/server/server.java new file mode 100644 index 0000000..fd508b9 --- /dev/null +++ b/modules/server/src/main/java/com/annimon/ownlang/modules/server/server.java @@ -0,0 +1,40 @@ +package com.annimon.ownlang.modules.server; + +import com.annimon.ownlang.lib.*; +import com.annimon.ownlang.modules.Module; +import io.javalin.Javalin; +import java.util.Map; +import static java.util.Map.entry; + +public final class server implements Module { + + @Override + public Map constants() { + return Map.of(); + } + + @Override + public Map functions() { + return Map.ofEntries( + entry("newServer", this::newServer), + entry("serve", this::serve) + ); + } + + private Value newServer(Value[] args) { + Arguments.checkOrOr(0, 1, args.length); + if (args.length == 0) { + return new ServerValue(Javalin.create()); + } else { + final Function configConsumer = ValueUtils.consumeFunction(args[0], 0); + return new ServerValue(Javalin.create(config -> configConsumer.execute())); + } + } + + private Value serve(Value[] args) { + // get path, port + // javalin start() + return NumberValue.ZERO; + + } +} diff --git a/settings.gradle b/settings.gradle index 28d1286..72a528c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -5,7 +5,9 @@ include 'ownlang-parser' include 'ownlang-desktop' include 'ownlang-utils' -include 'modules:main' -findProject(':modules:main')?.name = 'main' -include 'modules:canvasfx' -findProject(':modules:canvasfx')?.name = 'canvasfx' \ No newline at end of file +final def modules = ['main', 'canvasfx', 'server'] + +for (final def module in modules) { + include "modules:$module" + findProject(":modules:$module")?.name = module +}