[server] More server functions

This commit is contained in:
aNNiMON 2023-11-10 23:41:06 +02:00 committed by Victor Melnik
parent e59e09aeb8
commit f575b43b51
7 changed files with 275 additions and 31 deletions

View File

@ -0,0 +1,107 @@
package com.annimon.ownlang.modules.server;
import com.annimon.ownlang.lib.ArrayValue;
import com.annimon.ownlang.lib.NumberValue;
import com.annimon.ownlang.lib.Types;
import com.annimon.ownlang.lib.Value;
import io.javalin.config.JavalinConfig;
import io.javalin.http.staticfiles.Location;
import java.util.Map;
import java.util.function.Consumer;
/*
* Sample config:
* {
* "webjars": true,
* "classpathDirs": ["dir1", "dir2"],
* "externalDirs": ["dir1", "dir2"],
*
* "asyncTimeout": 6_000,
* "defaultContentType": "text/plain",
* "etags": true,
* "maxRequestSize": 1_000_000,
*
* "caseInsensitiveRoutes": true,
* "ignoreTrailingSlashes": true,
* "multipleSlashesAsSingle": true,
* "contextPath": "/",
*
* "basicAuth": ["user", "password"],
* "dev": true,
* "showBanner": false,
* "sslRedirects": true
* }
*/
class Config {
private final Map<String, Value> map;
public Config(Map<String, Value> map) {
this.map = map;
}
public void setup(JavalinConfig config) {
// staticFiles
ifTrue("webjars", config.staticFiles::enableWebjars);
ifArray("classpathDirs", directories -> {
for (Value directory : directories) {
config.staticFiles.add(directory.asString(), Location.CLASSPATH);
}
});
ifArray("externalDirs", directories -> {
for (Value directory : directories) {
config.staticFiles.add(directory.asString(), Location.EXTERNAL);
}
});
// http
ifNumber("asyncTimeout", value -> config.http.asyncTimeout = value.asLong());
ifString("defaultContentType", value -> config.http.defaultContentType = value);
ifBoolean("etags", flag -> config.http.generateEtags = flag);
ifNumber("maxRequestSize", value -> config.http.maxRequestSize = value.asLong());
// routing
ifBoolean("caseInsensitiveRoutes", flag -> config.routing.caseInsensitiveRoutes = flag);
ifBoolean("ignoreTrailingSlashes", flag -> config.routing.ignoreTrailingSlashes = flag);
ifBoolean("multipleSlashesAsSingle", flag -> config.routing.treatMultipleSlashesAsSingleSlash = flag);
ifString("contextPath", path -> config.routing.contextPath = path);
// other
ifArray("basicAuth", arr -> config.plugins.enableBasicAuth(arr.get(0).asString(), arr.get(1).asString()));
ifBoolean("showBanner", flag -> config.showJavalinBanner = flag);
ifTrue("dev", config.plugins::enableDevLogging);
ifTrue("sslRedirects", config.plugins::enableSslRedirects);
}
private void ifTrue(String key, Runnable action) {
if (map.containsKey(key) && map.get(key).asInt() != 0) {
action.run();
}
}
private void ifBoolean(String key, Consumer<Boolean> consumer) {
if (!map.containsKey(key)) return;
consumer.accept(map.get(key).asInt() != 0);
}
private void ifNumber(String key, Consumer<NumberValue> consumer) {
if (!map.containsKey(key)) return;
final Value value = map.get(key);
if (value.type() == Types.NUMBER) {
consumer.accept((NumberValue) value);
}
}
private void ifString(String key, Consumer<String> consumer) {
if (!map.containsKey(key)) return;
consumer.accept(map.get(key).asString());
}
private void ifArray(String key, Consumer<ArrayValue> consumer) {
if (!map.containsKey(key)) return;
final Value value = map.get(key);
if (value.type() == Types.ARRAY) {
consumer.accept((ArrayValue) value);
}
}
}

View File

@ -4,45 +4,108 @@ import com.annimon.ownlang.lib.*;
import io.javalin.http.Context; import io.javalin.http.Context;
import io.javalin.http.HttpStatus; import io.javalin.http.HttpStatus;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.function.Consumer; import java.util.function.Consumer;
public class ContextValue extends MapValue { class ContextValue extends MapValue {
private final Context ctx; private final Context ctx;
public ContextValue(@NotNull Context ctx) { public ContextValue(@NotNull Context ctx) {
super(10); super(32);
this.ctx = ctx; this.ctx = ctx;
init(); init();
} }
private void init() { private void init() {
set("attribute", this::attribute);
set("basicAuthCredentials", this::basicAuthCredentials);
set("body", Converters.voidToString(ctx::body)); set("body", Converters.voidToString(ctx::body));
set("bodyAsBytes", this::bodyAsBytes);
set("characterEncoding", Converters.voidToString(ctx::characterEncoding)); set("characterEncoding", Converters.voidToString(ctx::characterEncoding));
set("contentType", Converters.voidToString(ctx::contentType)); set("cookie", this::cookie);
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("contentLength", Converters.voidToInt(ctx::contentLength));
set("port", Converters.voidToInt(ctx::port)); set("contentType", this::contentType);
set("statusCode", Converters.voidToInt(ctx::statusCode)); set("contextPath", Converters.voidToString(ctx::contextPath));
set("endpointHandlerPath", Converters.voidToString(ctx::endpointHandlerPath));
set("formParam", Converters.stringToString(ctx::formParam));
set("fullUrl", Converters.voidToString(ctx::fullUrl));
set("handlerType", Converters.voidToString(() -> ctx.handlerType().name()));
set("header", this::header);
set("host", Converters.voidToString(ctx::host));
set("html", stringToContext(ctx::html));
set("ip", Converters.voidToString(ctx::ip));
set("json", objectToContext(ctx::json)); set("json", objectToContext(ctx::json));
set("jsonStream", objectToContext(ctx::jsonStream)); set("jsonStream", objectToContext(ctx::jsonStream));
set("matchedPath", Converters.voidToString(ctx::matchedPath));
set("path", Converters.voidToString(ctx::path));
set("port", Converters.voidToInt(ctx::port));
set("protocol", Converters.voidToString(ctx::protocol));
set("queryString", Converters.voidToString(ctx::queryString));
set("redirect", this::redirect);
set("removeCookie", this::removeCookie);
set("render", this::render); set("render", this::render);
set("result", this::result); set("result", this::result);
set("redirect", this::redirect); set("statusCode", Converters.voidToInt(ctx::statusCode));
set("scheme", Converters.voidToString(ctx::scheme));
set("url", Converters.voidToString(ctx::url));
set("userAgent", Converters.voidToString(ctx::userAgent));
}
private Value attribute(Value[] args) {
Arguments.checkOrOr(1, 2, args.length);
String key = args[0].asString();
if (args.length == 1) {
return ctx.attribute(key);
} else {
ctx.attribute(key, args[1]);
}
return this;
}
private Value basicAuthCredentials(Value[] args) {
Arguments.check(0, args.length);
final var cred = ctx.basicAuthCredentials();
return ArrayValue.of(new String[] {
cred.getUsername(),
cred.getPassword()
});
}
private Value bodyAsBytes(Value[] args) {
Arguments.check(0, args.length);
return ArrayValue.of(ctx.bodyAsBytes());
}
private Value cookie(Value[] args) {
Arguments.checkRange(1, 3, args.length);
if (args.length == 1) {
return new StringValue(ctx.cookie(args[0].asString()));
}
int maxAge = args.length == 3 ? args[2].asInt() : -1;
ctx.cookie(args[0].asString(), args[1].asString(), maxAge);
return this;
}
private Value contentType(Value[] args) {
Arguments.checkOrOr(0, 1, args.length);
if (args.length == 0) {
return new StringValue(ctx.contentType());
} else {
ctx.contentType(args[0].asString());
return this;
}
}
private Value header(Value[] args) {
Arguments.checkOrOr(1, 2, args.length);
String name = args[0].asString();
if (args.length == 1) {
return new StringValue(ctx.header(name));
} else {
ctx.header(name, args[1].asString());
return this;
}
} }
private Value render(Value[] args) { private Value render(Value[] args) {
@ -51,9 +114,8 @@ public class ContextValue extends MapValue {
if (args.length == 1) { if (args.length == 1) {
ctx.render(filePath); ctx.render(filePath);
} else { } else {
MapValue map = (MapValue) args[1]; MapValue map = ValueUtils.consumeMap(args[1], 1);
Map<String, Object> data = new HashMap<>(map.size()); Map<String, Object> data = map.convertMap(Value::asString, Value::asJavaObject);
map.getMap().forEach((k, v) -> data.put(k.asString(), v.asJavaObject()));
ctx.render(filePath, data); ctx.render(filePath, data);
} }
return this; return this;
@ -66,6 +128,14 @@ public class ContextValue extends MapValue {
return this; return this;
} }
private Value removeCookie(Value[] args) {
Arguments.checkOrOr(1, 2, args.length);
String name = args[0].asString();
String path = args.length == 2 ? args[1].asString() : "/";
ctx.removeCookie(name, path);
return this;
}
private Value result(Value[] args) { private Value result(Value[] args) {
Arguments.checkOrOr(0, 1, args.length); Arguments.checkOrOr(0, 1, args.length);
if (args.length == 0) { if (args.length == 0) {
@ -81,6 +151,14 @@ public class ContextValue extends MapValue {
} }
} }
private Value stringToContext(Consumer<String> consumer) {
return new FunctionValue(args -> {
Arguments.check(1, args.length);
consumer.accept(args[0].asString());
return this;
});
}
private Value objectToContext(Consumer<Object> consumer) { private Value objectToContext(Consumer<Object> consumer) {
return new FunctionValue(args -> { return new FunctionValue(args -> {
Arguments.check(1, args.length); Arguments.check(1, args.length);

View File

@ -1,17 +1,18 @@
package com.annimon.ownlang.modules.server; package com.annimon.ownlang.modules.server;
import com.annimon.ownlang.exceptions.OwnLangRuntimeException;
import com.annimon.ownlang.lib.*; import com.annimon.ownlang.lib.*;
import io.javalin.Javalin; import io.javalin.Javalin;
import io.javalin.http.Handler; import io.javalin.http.Handler;
import io.javalin.security.RouteRole; import io.javalin.security.RouteRole;
import java.util.Arrays; import java.util.Arrays;
public class ServerValue extends MapValue { class ServerValue extends MapValue {
private final Javalin server; private final Javalin server;
public ServerValue(Javalin server) { public ServerValue(Javalin server) {
super(10); super(12);
this.server = server; this.server = server;
init(); init();
} }
@ -25,7 +26,9 @@ public class ServerValue extends MapValue {
set("delete", httpMethod(server::delete)); set("delete", httpMethod(server::delete));
set("options", httpMethod(server::options)); set("options", httpMethod(server::options));
set("error", this::error); set("error", this::error);
set("exception", this::exception);
set("start", this::start); set("start", this::start);
set("stop", this::stop);
} }
private Value error(Value[] args) { private Value error(Value[] args) {
@ -45,6 +48,23 @@ public class ServerValue extends MapValue {
return this; return this;
} }
@SuppressWarnings("unchecked")
private Value exception(Value[] args) {
Arguments.check(2, args.length);
try {
String className = args[0].asString();
final Class<?> clazz = Class.forName(className);
final Function handler = ValueUtils.consumeFunction(args[1], 1);
server.exception((Class<? extends Exception>) clazz, (exc, ctx) -> {
Value exceptionType = new StringValue(exc.getClass().getName());
handler.execute(exceptionType, new ContextValue(ctx));
});
} catch (ClassNotFoundException e) {
throw new OwnLangRuntimeException(e);
}
return this;
}
private Value start(Value[] args) { private Value start(Value[] args) {
Arguments.checkRange(0, 2, args.length); Arguments.checkRange(0, 2, args.length);
switch (args.length) { switch (args.length) {
@ -55,6 +75,12 @@ public class ServerValue extends MapValue {
return this; return this;
} }
private Value stop(Value[] args) {
Arguments.check(0, args.length);
server.stop();
return this;
}
private FunctionValue httpMethod(HttpMethodHandler httpHandler) { private FunctionValue httpMethod(HttpMethodHandler httpHandler) {
return new FunctionValue(args -> { return new FunctionValue(args -> {
Arguments.checkAtLeast(2, args.length); Arguments.checkAtLeast(2, args.length);

View File

@ -3,6 +3,7 @@ package com.annimon.ownlang.modules.server;
import com.annimon.ownlang.lib.*; import com.annimon.ownlang.lib.*;
import com.annimon.ownlang.modules.Module; import com.annimon.ownlang.modules.Module;
import io.javalin.Javalin; import io.javalin.Javalin;
import io.javalin.http.staticfiles.Location;
import java.util.Map; import java.util.Map;
import static java.util.Map.entry; import static java.util.Map.entry;
@ -26,15 +27,19 @@ public final class server implements Module {
if (args.length == 0) { if (args.length == 0) {
return new ServerValue(Javalin.create()); return new ServerValue(Javalin.create());
} else { } else {
final Function configConsumer = ValueUtils.consumeFunction(args[0], 0); final Map<String, Value> map = ValueUtils.consumeMap(args[0], 0).getMapStringKeys();
return new ServerValue(Javalin.create(config -> configConsumer.execute())); final Config config = new Config(map);
return new ServerValue(Javalin.create(config::setup));
} }
} }
private Value serve(Value[] args) { private Value serve(Value[] args) {
// get path, port Arguments.checkRange(0, 2, args.length);
// javalin start() int port = args.length >= 1 ? args[0].asInt() : 8080;
return NumberValue.ZERO; String dir = args.length >= 2 ? args[1].asString() : ".";
return new ServerValue(Javalin.create(config -> {
config.staticFiles.add(dir, Location.EXTERNAL);
config.showJavalinBanner = false;
}).start(port));
} }
} }

View File

@ -1,6 +1,7 @@
package com.annimon.ownlang.lib; package com.annimon.ownlang.lib;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.function.UnaryOperator;
import static com.annimon.ownlang.lib.ValueUtils.getFloatNumber; import static com.annimon.ownlang.lib.ValueUtils.getFloatNumber;
/** /**
@ -273,4 +274,11 @@ public final class Converters {
return NumberValue.fromBoolean(f.test(args[0].asString())); return NumberValue.fromBoolean(f.test(args[0].asString()));
}); });
} }
public static FunctionValue stringToString(UnaryOperator<String> f) {
return new FunctionValue(args -> {
Arguments.check(1, args.length);
return new StringValue(f.apply(args[0].asString()));
});
}
} }

View File

@ -85,6 +85,17 @@ public class MapValue implements Value, Iterable<Map.Entry<Value, Value>> {
public Map<Value, Value> getMap() { public Map<Value, Value> getMap() {
return map; return map;
} }
public Map<String, Value> getMapStringKeys() {
return convertMap(Value::asString, java.util.function.Function.identity());
}
public <K, V> Map<K, V> convertMap(java.util.function.Function<? super Value, ? extends K> keyMapper,
java.util.function.Function<? super Value, ? extends V> valueMapper) {
final Map<K, V> result = new LinkedHashMap<>(map.size());
map.forEach((key, value) -> result.put(keyMapper.apply(key), valueMapper.apply(value)));
return result;
}
@Override @Override
public Object raw() { public Object raw() {

View File

@ -101,6 +101,15 @@ public final class ValueUtils {
return result; return result;
} }
public static MapValue consumeMap(Value value, int argumentNumber) {
final int type = value.type();
if (type != Types.MAP) {
throw new TypeException("Map expected at argument " + (argumentNumber + 1)
+ ", but found " + Types.typeToString(type));
}
return (MapValue) value;
}
public static Function consumeFunction(Value value, int argumentNumber) { public static Function consumeFunction(Value value, int argumentNumber) {
return consumeFunction(value, " at argument " + (argumentNumber + 1)); return consumeFunction(value, " at argument " + (argumentNumber + 1));
} }