feat: add example scripts for argparse, bytes, dataset, html, markdown, uuid, wdantic, and web chat modules
Add comprehensive demo scripts showcasing the usage of multiple Wren modules including argument parsing, byte manipulation, in-memory dataset operations, HTML encoding/slugification, markdown-to-HTML conversion, UUID generation/validation, schema validation with wdantic, and a full web chat application with REST endpoints.
This commit is contained in:
@@ -3,15 +3,20 @@
|
||||
|
||||
#include "modules.h"
|
||||
|
||||
#include "argparse.wren.inc"
|
||||
#include "base64.wren.inc"
|
||||
#include "bytes.wren.inc"
|
||||
#include "crypto.wren.inc"
|
||||
#include "dataset.wren.inc"
|
||||
#include "datetime.wren.inc"
|
||||
#include "dns.wren.inc"
|
||||
#include "env.wren.inc"
|
||||
#include "html.wren.inc"
|
||||
#include "http.wren.inc"
|
||||
#include "io.wren.inc"
|
||||
#include "jinja.wren.inc"
|
||||
#include "json.wren.inc"
|
||||
#include "markdown.wren.inc"
|
||||
#include "math.wren.inc"
|
||||
#include "net.wren.inc"
|
||||
#include "os.wren.inc"
|
||||
@@ -23,10 +28,19 @@
|
||||
#include "subprocess.wren.inc"
|
||||
#include "timer.wren.inc"
|
||||
#include "tls.wren.inc"
|
||||
#include "uuid.wren.inc"
|
||||
#include "wdantic.wren.inc"
|
||||
#include "web.wren.inc"
|
||||
#include "websocket.wren.inc"
|
||||
|
||||
extern void base64Encode(WrenVM* vm);
|
||||
extern void base64Decode(WrenVM* vm);
|
||||
extern void bytesToList(WrenVM* vm);
|
||||
extern void bytesFromList(WrenVM* vm);
|
||||
extern void bytesXorMask(WrenVM* vm);
|
||||
extern void bytesConcat(WrenVM* vm);
|
||||
extern void bytesSlice(WrenVM* vm);
|
||||
extern void bytesLength(WrenVM* vm);
|
||||
extern void cryptoRandomBytes(WrenVM* vm);
|
||||
extern void cryptoMd5(WrenVM* vm);
|
||||
extern void cryptoSha1(WrenVM* vm);
|
||||
@@ -218,12 +232,24 @@ typedef struct
|
||||
// The array of built-in modules.
|
||||
static ModuleRegistry modules[] =
|
||||
{
|
||||
MODULE(argparse)
|
||||
END_MODULE
|
||||
MODULE(base64)
|
||||
CLASS(Base64)
|
||||
STATIC_METHOD("encode(_)", base64Encode)
|
||||
STATIC_METHOD("decode(_)", base64Decode)
|
||||
END_CLASS
|
||||
END_MODULE
|
||||
MODULE(bytes)
|
||||
CLASS(Bytes)
|
||||
STATIC_METHOD("toList(_)", bytesToList)
|
||||
STATIC_METHOD("fromList(_)", bytesFromList)
|
||||
STATIC_METHOD("xorMask(_,_)", bytesXorMask)
|
||||
STATIC_METHOD("concat(_,_)", bytesConcat)
|
||||
STATIC_METHOD("slice(_,_,_)", bytesSlice)
|
||||
STATIC_METHOD("length(_)", bytesLength)
|
||||
END_CLASS
|
||||
END_MODULE
|
||||
MODULE(crypto)
|
||||
CLASS(Crypto)
|
||||
STATIC_METHOD("randomBytes_(_,_)", cryptoRandomBytes)
|
||||
@@ -234,6 +260,8 @@ static ModuleRegistry modules[] =
|
||||
STATIC_METHOD("sha256_(_)", cryptoSha256)
|
||||
END_CLASS
|
||||
END_MODULE
|
||||
MODULE(dataset)
|
||||
END_MODULE
|
||||
MODULE(datetime)
|
||||
CLASS(DateTime)
|
||||
STATIC_METHOD("now_()", datetimeNow)
|
||||
@@ -255,6 +283,8 @@ static ModuleRegistry modules[] =
|
||||
STATIC_METHOD("all", envAll)
|
||||
END_CLASS
|
||||
END_MODULE
|
||||
MODULE(html)
|
||||
END_MODULE
|
||||
MODULE(http)
|
||||
END_MODULE
|
||||
MODULE(io)
|
||||
@@ -313,6 +343,8 @@ static ModuleRegistry modules[] =
|
||||
STATIC_METHOD("parse(_)", jsonParse)
|
||||
END_CLASS
|
||||
END_MODULE
|
||||
MODULE(markdown)
|
||||
END_MODULE
|
||||
MODULE(math)
|
||||
CLASS(Math)
|
||||
STATIC_METHOD("sin(_)", mathSin)
|
||||
@@ -431,6 +463,12 @@ static ModuleRegistry modules[] =
|
||||
METHOD("close_()", tlsSocketClose)
|
||||
END_CLASS
|
||||
END_MODULE
|
||||
MODULE(uuid)
|
||||
END_MODULE
|
||||
MODULE(wdantic)
|
||||
END_MODULE
|
||||
MODULE(web)
|
||||
END_MODULE
|
||||
MODULE(websocket)
|
||||
END_MODULE
|
||||
|
||||
|
||||
Vendored
+277
@@ -0,0 +1,277 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
|
||||
import "os" for Process
|
||||
|
||||
class ArgumentParser {
|
||||
construct new() {
|
||||
_description = ""
|
||||
_positional = []
|
||||
_optional = []
|
||||
_prog = null
|
||||
}
|
||||
|
||||
construct new(description) {
|
||||
_description = description
|
||||
_positional = []
|
||||
_optional = []
|
||||
_prog = null
|
||||
}
|
||||
|
||||
prog { _prog }
|
||||
prog=(value) { _prog = value }
|
||||
description { _description }
|
||||
description=(value) { _description = value }
|
||||
|
||||
addArgument(name) { addArgument(name, {}) }
|
||||
|
||||
addArgument(name, options) {
|
||||
if (name.startsWith("-")) {
|
||||
var arg = {
|
||||
"name": name,
|
||||
"long": options.containsKey("long") ? options["long"] : null,
|
||||
"type": options.containsKey("type") ? options["type"] : "string",
|
||||
"default": options.containsKey("default") ? options["default"] : null,
|
||||
"required": options.containsKey("required") ? options["required"] : false,
|
||||
"help": options.containsKey("help") ? options["help"] : "",
|
||||
"choices": options.containsKey("choices") ? options["choices"] : null,
|
||||
"action": options.containsKey("action") ? options["action"] : "store",
|
||||
"nargs": options.containsKey("nargs") ? options["nargs"] : null,
|
||||
"dest": options.containsKey("dest") ? options["dest"] : null
|
||||
}
|
||||
_optional.add(arg)
|
||||
} else {
|
||||
var arg = {
|
||||
"name": name,
|
||||
"type": options.containsKey("type") ? options["type"] : "string",
|
||||
"default": options.containsKey("default") ? options["default"] : null,
|
||||
"required": options.containsKey("required") ? options["required"] : true,
|
||||
"help": options.containsKey("help") ? options["help"] : "",
|
||||
"choices": options.containsKey("choices") ? options["choices"] : null,
|
||||
"nargs": options.containsKey("nargs") ? options["nargs"] : null
|
||||
}
|
||||
_positional.add(arg)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
destName_(arg) {
|
||||
if (arg["dest"]) return arg["dest"]
|
||||
var name = arg["long"] ? arg["long"] : arg["name"]
|
||||
while (name.startsWith("-")) name = name[1..-1]
|
||||
return name.replace("-", "_")
|
||||
}
|
||||
|
||||
parseArgs() { parseArgs(Process.arguments) }
|
||||
|
||||
parseArgs(args) {
|
||||
var result = {}
|
||||
|
||||
for (arg in _optional) {
|
||||
var dest = destName_(arg)
|
||||
if (arg["action"] == "storeTrue") {
|
||||
result[dest] = false
|
||||
} else if (arg["action"] == "storeFalse") {
|
||||
result[dest] = true
|
||||
} else if (arg["action"] == "count") {
|
||||
result[dest] = 0
|
||||
} else if (arg["action"] == "append") {
|
||||
result[dest] = []
|
||||
} else if (arg["default"] != null) {
|
||||
result[dest] = arg["default"]
|
||||
} else {
|
||||
result[dest] = null
|
||||
}
|
||||
}
|
||||
|
||||
for (arg in _positional) {
|
||||
if (arg["default"] != null) {
|
||||
result[arg["name"]] = arg["default"]
|
||||
} else {
|
||||
result[arg["name"]] = null
|
||||
}
|
||||
}
|
||||
|
||||
var posIdx = 0
|
||||
var i = 0
|
||||
while (i < args.count) {
|
||||
var token = args[i]
|
||||
|
||||
if (token.startsWith("-")) {
|
||||
var matchedArg = null
|
||||
for (arg in _optional) {
|
||||
if (token == arg["name"] || token == arg["long"]) {
|
||||
matchedArg = arg
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedArg == null) {
|
||||
Fiber.abort("Unknown option: %(token)")
|
||||
}
|
||||
|
||||
var dest = destName_(matchedArg)
|
||||
if (matchedArg["action"] == "storeTrue") {
|
||||
result[dest] = true
|
||||
i = i + 1
|
||||
} else if (matchedArg["action"] == "storeFalse") {
|
||||
result[dest] = false
|
||||
i = i + 1
|
||||
} else if (matchedArg["action"] == "count") {
|
||||
result[dest] = result[dest] + 1
|
||||
i = i + 1
|
||||
} else if (matchedArg["action"] == "append") {
|
||||
if (i + 1 >= args.count) {
|
||||
Fiber.abort("Option %(token) requires a value")
|
||||
}
|
||||
i = i + 1
|
||||
result[dest].add(convertValue_(args[i], matchedArg["type"]))
|
||||
i = i + 1
|
||||
} else {
|
||||
if (matchedArg["nargs"] == "*") {
|
||||
var values = []
|
||||
i = i + 1
|
||||
while (i < args.count && !args[i].startsWith("-")) {
|
||||
values.add(convertValue_(args[i], matchedArg["type"]))
|
||||
i = i + 1
|
||||
}
|
||||
result[dest] = values
|
||||
} else if (matchedArg["nargs"] == "+") {
|
||||
var values = []
|
||||
i = i + 1
|
||||
while (i < args.count && !args[i].startsWith("-")) {
|
||||
values.add(convertValue_(args[i], matchedArg["type"]))
|
||||
i = i + 1
|
||||
}
|
||||
if (values.count == 0) {
|
||||
Fiber.abort("Option %(token) requires at least one value")
|
||||
}
|
||||
result[dest] = values
|
||||
} else if (matchedArg["nargs"] is Num) {
|
||||
var values = []
|
||||
var n = matchedArg["nargs"]
|
||||
i = i + 1
|
||||
for (j in 0...n) {
|
||||
if (i >= args.count) {
|
||||
Fiber.abort("Option %(token) requires %(n) values")
|
||||
}
|
||||
values.add(convertValue_(args[i], matchedArg["type"]))
|
||||
i = i + 1
|
||||
}
|
||||
result[dest] = values
|
||||
} else {
|
||||
if (i + 1 >= args.count) {
|
||||
Fiber.abort("Option %(token) requires a value")
|
||||
}
|
||||
i = i + 1
|
||||
var value = convertValue_(args[i], matchedArg["type"])
|
||||
if (matchedArg["choices"] && !matchedArg["choices"].contains(value)) {
|
||||
Fiber.abort("Invalid choice '%(value)' for %(token)")
|
||||
}
|
||||
result[dest] = value
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (posIdx < _positional.count) {
|
||||
var arg = _positional[posIdx]
|
||||
if (arg["nargs"] == "*" || arg["nargs"] == "+") {
|
||||
var values = []
|
||||
while (i < args.count && !args[i].startsWith("-")) {
|
||||
values.add(convertValue_(args[i], arg["type"]))
|
||||
i = i + 1
|
||||
}
|
||||
if (arg["nargs"] == "+" && values.count == 0) {
|
||||
Fiber.abort("Argument %(arg["name"]) requires at least one value")
|
||||
}
|
||||
result[arg["name"]] = values
|
||||
} else {
|
||||
var value = convertValue_(token, arg["type"])
|
||||
if (arg["choices"] && !arg["choices"].contains(value)) {
|
||||
Fiber.abort("Invalid choice '%(value)' for %(arg["name"])")
|
||||
}
|
||||
result[arg["name"]] = value
|
||||
i = i + 1
|
||||
}
|
||||
posIdx = posIdx + 1
|
||||
} else {
|
||||
Fiber.abort("Unexpected argument: %(token)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (arg in _positional) {
|
||||
if (arg["required"] && result[arg["name"]] == null) {
|
||||
Fiber.abort("Missing required argument: %(arg["name"])")
|
||||
}
|
||||
}
|
||||
|
||||
for (arg in _optional) {
|
||||
var dest = destName_(arg)
|
||||
if (arg["required"] && result[dest] == null) {
|
||||
Fiber.abort("Missing required option: %(arg["name"])")
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
convertValue_(value, type) {
|
||||
if (type == "int") {
|
||||
return Num.fromString(value)
|
||||
} else if (type == "float") {
|
||||
return Num.fromString(value)
|
||||
} else if (type == "bool") {
|
||||
if (value == "true" || value == "1" || value == "yes") return true
|
||||
if (value == "false" || value == "0" || value == "no") return false
|
||||
Fiber.abort("Invalid boolean value: %(value)")
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
printHelp() {
|
||||
var prog = _prog ? _prog : "program"
|
||||
System.print("usage: %(prog) [options]%(positionalUsage_())")
|
||||
if (_description.count > 0) {
|
||||
System.print("\n%(description)")
|
||||
}
|
||||
if (_positional.count > 0) {
|
||||
System.print("\npositional arguments:")
|
||||
for (arg in _positional) {
|
||||
var helpText = arg["help"].count > 0 ? arg["help"] : ""
|
||||
System.print(" %(pad_(arg["name"], 20)) %(helpText)")
|
||||
}
|
||||
}
|
||||
if (_optional.count > 0) {
|
||||
System.print("\noptional arguments:")
|
||||
for (arg in _optional) {
|
||||
var names = arg["name"]
|
||||
if (arg["long"]) names = names + ", " + arg["long"]
|
||||
var helpText = arg["help"].count > 0 ? arg["help"] : ""
|
||||
System.print(" %(pad_(names, 20)) %(helpText)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
positionalUsage_() {
|
||||
if (_positional.count == 0) return ""
|
||||
var parts = []
|
||||
for (arg in _positional) {
|
||||
if (arg["nargs"] == "*") {
|
||||
parts.add("[%(arg["name"]) ...]")
|
||||
} else if (arg["nargs"] == "+") {
|
||||
parts.add("%(arg["name"]) [...]")
|
||||
} else if (arg["required"]) {
|
||||
parts.add(arg["name"])
|
||||
} else {
|
||||
parts.add("[%(arg["name"])]")
|
||||
}
|
||||
}
|
||||
return " " + parts.join(" ")
|
||||
}
|
||||
|
||||
pad_(str, width) {
|
||||
var result = str
|
||||
while (result.count < width) result = result + " "
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
// Please do not edit this file. It has been generated automatically
|
||||
// from `src/module/argparse.wren` using `util/wren_to_c_string.py`
|
||||
|
||||
static const char* argparseModuleSource =
|
||||
"// retoor <retoor@molodetz.nl>\n"
|
||||
"\n"
|
||||
"import \"os\" for Process\n"
|
||||
"\n"
|
||||
"class ArgumentParser {\n"
|
||||
" construct new() {\n"
|
||||
" _description = \"\"\n"
|
||||
" _positional = []\n"
|
||||
" _optional = []\n"
|
||||
" _prog = null\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" construct new(description) {\n"
|
||||
" _description = description\n"
|
||||
" _positional = []\n"
|
||||
" _optional = []\n"
|
||||
" _prog = null\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" prog { _prog }\n"
|
||||
" prog=(value) { _prog = value }\n"
|
||||
" description { _description }\n"
|
||||
" description=(value) { _description = value }\n"
|
||||
"\n"
|
||||
" addArgument(name) { addArgument(name, {}) }\n"
|
||||
"\n"
|
||||
" addArgument(name, options) {\n"
|
||||
" if (name.startsWith(\"-\")) {\n"
|
||||
" var arg = {\n"
|
||||
" \"name\": name,\n"
|
||||
" \"long\": options.containsKey(\"long\") ? options[\"long\"] : null,\n"
|
||||
" \"type\": options.containsKey(\"type\") ? options[\"type\"] : \"string\",\n"
|
||||
" \"default\": options.containsKey(\"default\") ? options[\"default\"] : null,\n"
|
||||
" \"required\": options.containsKey(\"required\") ? options[\"required\"] : false,\n"
|
||||
" \"help\": options.containsKey(\"help\") ? options[\"help\"] : \"\",\n"
|
||||
" \"choices\": options.containsKey(\"choices\") ? options[\"choices\"] : null,\n"
|
||||
" \"action\": options.containsKey(\"action\") ? options[\"action\"] : \"store\",\n"
|
||||
" \"nargs\": options.containsKey(\"nargs\") ? options[\"nargs\"] : null,\n"
|
||||
" \"dest\": options.containsKey(\"dest\") ? options[\"dest\"] : null\n"
|
||||
" }\n"
|
||||
" _optional.add(arg)\n"
|
||||
" } else {\n"
|
||||
" var arg = {\n"
|
||||
" \"name\": name,\n"
|
||||
" \"type\": options.containsKey(\"type\") ? options[\"type\"] : \"string\",\n"
|
||||
" \"default\": options.containsKey(\"default\") ? options[\"default\"] : null,\n"
|
||||
" \"required\": options.containsKey(\"required\") ? options[\"required\"] : true,\n"
|
||||
" \"help\": options.containsKey(\"help\") ? options[\"help\"] : \"\",\n"
|
||||
" \"choices\": options.containsKey(\"choices\") ? options[\"choices\"] : null,\n"
|
||||
" \"nargs\": options.containsKey(\"nargs\") ? options[\"nargs\"] : null\n"
|
||||
" }\n"
|
||||
" _positional.add(arg)\n"
|
||||
" }\n"
|
||||
" return this\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" destName_(arg) {\n"
|
||||
" if (arg[\"dest\"]) return arg[\"dest\"]\n"
|
||||
" var name = arg[\"long\"] ? arg[\"long\"] : arg[\"name\"]\n"
|
||||
" while (name.startsWith(\"-\")) name = name[1..-1]\n"
|
||||
" return name.replace(\"-\", \"_\")\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" parseArgs() { parseArgs(Process.arguments) }\n"
|
||||
"\n"
|
||||
" parseArgs(args) {\n"
|
||||
" var result = {}\n"
|
||||
"\n"
|
||||
" for (arg in _optional) {\n"
|
||||
" var dest = destName_(arg)\n"
|
||||
" if (arg[\"action\"] == \"storeTrue\") {\n"
|
||||
" result[dest] = false\n"
|
||||
" } else if (arg[\"action\"] == \"storeFalse\") {\n"
|
||||
" result[dest] = true\n"
|
||||
" } else if (arg[\"action\"] == \"count\") {\n"
|
||||
" result[dest] = 0\n"
|
||||
" } else if (arg[\"action\"] == \"append\") {\n"
|
||||
" result[dest] = []\n"
|
||||
" } else if (arg[\"default\"] != null) {\n"
|
||||
" result[dest] = arg[\"default\"]\n"
|
||||
" } else {\n"
|
||||
" result[dest] = null\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" for (arg in _positional) {\n"
|
||||
" if (arg[\"default\"] != null) {\n"
|
||||
" result[arg[\"name\"]] = arg[\"default\"]\n"
|
||||
" } else {\n"
|
||||
" result[arg[\"name\"]] = null\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" var posIdx = 0\n"
|
||||
" var i = 0\n"
|
||||
" while (i < args.count) {\n"
|
||||
" var token = args[i]\n"
|
||||
"\n"
|
||||
" if (token.startsWith(\"-\")) {\n"
|
||||
" var matchedArg = null\n"
|
||||
" for (arg in _optional) {\n"
|
||||
" if (token == arg[\"name\"] || token == arg[\"long\"]) {\n"
|
||||
" matchedArg = arg\n"
|
||||
" break\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" if (matchedArg == null) {\n"
|
||||
" Fiber.abort(\"Unknown option: %(token)\")\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" var dest = destName_(matchedArg)\n"
|
||||
" if (matchedArg[\"action\"] == \"storeTrue\") {\n"
|
||||
" result[dest] = true\n"
|
||||
" i = i + 1\n"
|
||||
" } else if (matchedArg[\"action\"] == \"storeFalse\") {\n"
|
||||
" result[dest] = false\n"
|
||||
" i = i + 1\n"
|
||||
" } else if (matchedArg[\"action\"] == \"count\") {\n"
|
||||
" result[dest] = result[dest] + 1\n"
|
||||
" i = i + 1\n"
|
||||
" } else if (matchedArg[\"action\"] == \"append\") {\n"
|
||||
" if (i + 1 >= args.count) {\n"
|
||||
" Fiber.abort(\"Option %(token) requires a value\")\n"
|
||||
" }\n"
|
||||
" i = i + 1\n"
|
||||
" result[dest].add(convertValue_(args[i], matchedArg[\"type\"]))\n"
|
||||
" i = i + 1\n"
|
||||
" } else {\n"
|
||||
" if (matchedArg[\"nargs\"] == \"*\") {\n"
|
||||
" var values = []\n"
|
||||
" i = i + 1\n"
|
||||
" while (i < args.count && !args[i].startsWith(\"-\")) {\n"
|
||||
" values.add(convertValue_(args[i], matchedArg[\"type\"]))\n"
|
||||
" i = i + 1\n"
|
||||
" }\n"
|
||||
" result[dest] = values\n"
|
||||
" } else if (matchedArg[\"nargs\"] == \"+\") {\n"
|
||||
" var values = []\n"
|
||||
" i = i + 1\n"
|
||||
" while (i < args.count && !args[i].startsWith(\"-\")) {\n"
|
||||
" values.add(convertValue_(args[i], matchedArg[\"type\"]))\n"
|
||||
" i = i + 1\n"
|
||||
" }\n"
|
||||
" if (values.count == 0) {\n"
|
||||
" Fiber.abort(\"Option %(token) requires at least one value\")\n"
|
||||
" }\n"
|
||||
" result[dest] = values\n"
|
||||
" } else if (matchedArg[\"nargs\"] is Num) {\n"
|
||||
" var values = []\n"
|
||||
" var n = matchedArg[\"nargs\"]\n"
|
||||
" i = i + 1\n"
|
||||
" for (j in 0...n) {\n"
|
||||
" if (i >= args.count) {\n"
|
||||
" Fiber.abort(\"Option %(token) requires %(n) values\")\n"
|
||||
" }\n"
|
||||
" values.add(convertValue_(args[i], matchedArg[\"type\"]))\n"
|
||||
" i = i + 1\n"
|
||||
" }\n"
|
||||
" result[dest] = values\n"
|
||||
" } else {\n"
|
||||
" if (i + 1 >= args.count) {\n"
|
||||
" Fiber.abort(\"Option %(token) requires a value\")\n"
|
||||
" }\n"
|
||||
" i = i + 1\n"
|
||||
" var value = convertValue_(args[i], matchedArg[\"type\"])\n"
|
||||
" if (matchedArg[\"choices\"] && !matchedArg[\"choices\"].contains(value)) {\n"
|
||||
" Fiber.abort(\"Invalid choice '%(value)' for %(token)\")\n"
|
||||
" }\n"
|
||||
" result[dest] = value\n"
|
||||
" i = i + 1\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" } else {\n"
|
||||
" if (posIdx < _positional.count) {\n"
|
||||
" var arg = _positional[posIdx]\n"
|
||||
" if (arg[\"nargs\"] == \"*\" || arg[\"nargs\"] == \"+\") {\n"
|
||||
" var values = []\n"
|
||||
" while (i < args.count && !args[i].startsWith(\"-\")) {\n"
|
||||
" values.add(convertValue_(args[i], arg[\"type\"]))\n"
|
||||
" i = i + 1\n"
|
||||
" }\n"
|
||||
" if (arg[\"nargs\"] == \"+\" && values.count == 0) {\n"
|
||||
" Fiber.abort(\"Argument %(arg[\"name\"]) requires at least one value\")\n"
|
||||
" }\n"
|
||||
" result[arg[\"name\"]] = values\n"
|
||||
" } else {\n"
|
||||
" var value = convertValue_(token, arg[\"type\"])\n"
|
||||
" if (arg[\"choices\"] && !arg[\"choices\"].contains(value)) {\n"
|
||||
" Fiber.abort(\"Invalid choice '%(value)' for %(arg[\"name\"])\")\n"
|
||||
" }\n"
|
||||
" result[arg[\"name\"]] = value\n"
|
||||
" i = i + 1\n"
|
||||
" }\n"
|
||||
" posIdx = posIdx + 1\n"
|
||||
" } else {\n"
|
||||
" Fiber.abort(\"Unexpected argument: %(token)\")\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" for (arg in _positional) {\n"
|
||||
" if (arg[\"required\"] && result[arg[\"name\"]] == null) {\n"
|
||||
" Fiber.abort(\"Missing required argument: %(arg[\"name\"])\")\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" for (arg in _optional) {\n"
|
||||
" var dest = destName_(arg)\n"
|
||||
" if (arg[\"required\"] && result[dest] == null) {\n"
|
||||
" Fiber.abort(\"Missing required option: %(arg[\"name\"])\")\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" return result\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" convertValue_(value, type) {\n"
|
||||
" if (type == \"int\") {\n"
|
||||
" return Num.fromString(value)\n"
|
||||
" } else if (type == \"float\") {\n"
|
||||
" return Num.fromString(value)\n"
|
||||
" } else if (type == \"bool\") {\n"
|
||||
" if (value == \"true\" || value == \"1\" || value == \"yes\") return true\n"
|
||||
" if (value == \"false\" || value == \"0\" || value == \"no\") return false\n"
|
||||
" Fiber.abort(\"Invalid boolean value: %(value)\")\n"
|
||||
" }\n"
|
||||
" return value\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" printHelp() {\n"
|
||||
" var prog = _prog ? _prog : \"program\"\n"
|
||||
" System.print(\"usage: %(prog) [options]%(positionalUsage_())\")\n"
|
||||
" if (_description.count > 0) {\n"
|
||||
" System.print(\"\\n%(description)\")\n"
|
||||
" }\n"
|
||||
" if (_positional.count > 0) {\n"
|
||||
" System.print(\"\\npositional arguments:\")\n"
|
||||
" for (arg in _positional) {\n"
|
||||
" var helpText = arg[\"help\"].count > 0 ? arg[\"help\"] : \"\"\n"
|
||||
" System.print(\" %(pad_(arg[\"name\"], 20)) %(helpText)\")\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" if (_optional.count > 0) {\n"
|
||||
" System.print(\"\\noptional arguments:\")\n"
|
||||
" for (arg in _optional) {\n"
|
||||
" var names = arg[\"name\"]\n"
|
||||
" if (arg[\"long\"]) names = names + \", \" + arg[\"long\"]\n"
|
||||
" var helpText = arg[\"help\"].count > 0 ? arg[\"help\"] : \"\"\n"
|
||||
" System.print(\" %(pad_(names, 20)) %(helpText)\")\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" positionalUsage_() {\n"
|
||||
" if (_positional.count == 0) return \"\"\n"
|
||||
" var parts = []\n"
|
||||
" for (arg in _positional) {\n"
|
||||
" if (arg[\"nargs\"] == \"*\") {\n"
|
||||
" parts.add(\"[%(arg[\"name\"]) ...]\")\n"
|
||||
" } else if (arg[\"nargs\"] == \"+\") {\n"
|
||||
" parts.add(\"%(arg[\"name\"]) [...]\")\n"
|
||||
" } else if (arg[\"required\"]) {\n"
|
||||
" parts.add(arg[\"name\"])\n"
|
||||
" } else {\n"
|
||||
" parts.add(\"[%(arg[\"name\"])]\")\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" return \" \" + parts.join(\" \")\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" pad_(str, width) {\n"
|
||||
" var result = str\n"
|
||||
" while (result.count < width) result = result + \" \"\n"
|
||||
" return result\n"
|
||||
" }\n"
|
||||
"}\n";
|
||||
@@ -0,0 +1,180 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include "bytes.h"
|
||||
#include "wren.h"
|
||||
|
||||
void bytesToList(WrenVM* vm) {
|
||||
int length = 0;
|
||||
const char* data = wrenGetSlotBytes(vm, 1, &length);
|
||||
|
||||
if (data == NULL || length == 0) {
|
||||
wrenSetSlotNewList(vm, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
wrenEnsureSlots(vm, 3);
|
||||
wrenSetSlotNewList(vm, 0);
|
||||
|
||||
const uint8_t* bytes = (const uint8_t*)data;
|
||||
for (int i = 0; i < length; i++) {
|
||||
wrenSetSlotDouble(vm, 2, (double)bytes[i]);
|
||||
wrenInsertInList(vm, 0, -1, 2);
|
||||
}
|
||||
}
|
||||
|
||||
void bytesFromList(WrenVM* vm) {
|
||||
if (wrenGetSlotType(vm, 1) != WREN_TYPE_LIST) {
|
||||
wrenSetSlotString(vm, 0, "Argument must be a list.");
|
||||
wrenAbortFiber(vm, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
int count = wrenGetListCount(vm, 1);
|
||||
if (count == 0) {
|
||||
wrenSetSlotBytes(vm, 0, "", 0);
|
||||
return;
|
||||
}
|
||||
|
||||
char* buffer = (char*)malloc(count);
|
||||
if (buffer == NULL) {
|
||||
wrenSetSlotString(vm, 0, "Memory allocation failed.");
|
||||
wrenAbortFiber(vm, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
wrenEnsureSlots(vm, 3);
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
wrenGetListElement(vm, 1, i, 2);
|
||||
if (wrenGetSlotType(vm, 2) != WREN_TYPE_NUM) {
|
||||
free(buffer);
|
||||
wrenSetSlotString(vm, 0, "List must contain only numbers.");
|
||||
wrenAbortFiber(vm, 0);
|
||||
return;
|
||||
}
|
||||
double value = wrenGetSlotDouble(vm, 2);
|
||||
int byteVal = (int)value;
|
||||
if (byteVal < 0 || byteVal > 255 || value != (double)byteVal) {
|
||||
free(buffer);
|
||||
wrenSetSlotString(vm, 0, "Byte values must be integers 0-255.");
|
||||
wrenAbortFiber(vm, 0);
|
||||
return;
|
||||
}
|
||||
buffer[i] = (char)(uint8_t)byteVal;
|
||||
}
|
||||
|
||||
wrenSetSlotBytes(vm, 0, buffer, count);
|
||||
free(buffer);
|
||||
}
|
||||
|
||||
void bytesXorMask(WrenVM* vm) {
|
||||
int dataLen = 0;
|
||||
const char* data = wrenGetSlotBytes(vm, 1, &dataLen);
|
||||
int maskLen = 0;
|
||||
const char* mask = wrenGetSlotBytes(vm, 2, &maskLen);
|
||||
|
||||
if (mask == NULL || maskLen == 0) {
|
||||
wrenSetSlotString(vm, 0, "Mask cannot be empty.");
|
||||
wrenAbortFiber(vm, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data == NULL || dataLen == 0) {
|
||||
wrenSetSlotBytes(vm, 0, "", 0);
|
||||
return;
|
||||
}
|
||||
|
||||
char* result = (char*)malloc(dataLen);
|
||||
if (result == NULL) {
|
||||
wrenSetSlotString(vm, 0, "Memory allocation failed.");
|
||||
wrenAbortFiber(vm, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t* src = (const uint8_t*)data;
|
||||
const uint8_t* maskBytes = (const uint8_t*)mask;
|
||||
uint8_t* dst = (uint8_t*)result;
|
||||
|
||||
if (maskLen == 4) {
|
||||
uint8_t m0 = maskBytes[0], m1 = maskBytes[1];
|
||||
uint8_t m2 = maskBytes[2], m3 = maskBytes[3];
|
||||
int i = 0;
|
||||
int fullBlocks = dataLen / 4;
|
||||
for (int block = 0; block < fullBlocks; block++) {
|
||||
dst[i] = src[i] ^ m0;
|
||||
dst[i+1] = src[i+1] ^ m1;
|
||||
dst[i+2] = src[i+2] ^ m2;
|
||||
dst[i+3] = src[i+3] ^ m3;
|
||||
i += 4;
|
||||
}
|
||||
int rem = dataLen % 4;
|
||||
if (rem >= 1) dst[i] = src[i] ^ m0;
|
||||
if (rem >= 2) dst[i+1] = src[i+1] ^ m1;
|
||||
if (rem >= 3) dst[i+2] = src[i+2] ^ m2;
|
||||
} else {
|
||||
for (int i = 0; i < dataLen; i++) {
|
||||
dst[i] = src[i] ^ maskBytes[i % maskLen];
|
||||
}
|
||||
}
|
||||
|
||||
wrenSetSlotBytes(vm, 0, result, dataLen);
|
||||
free(result);
|
||||
}
|
||||
|
||||
void bytesConcat(WrenVM* vm) {
|
||||
int lenA = 0;
|
||||
const char* a = wrenGetSlotBytes(vm, 1, &lenA);
|
||||
int lenB = 0;
|
||||
const char* b = wrenGetSlotBytes(vm, 2, &lenB);
|
||||
|
||||
if (lenA == 0 && lenB == 0) {
|
||||
wrenSetSlotBytes(vm, 0, "", 0);
|
||||
return;
|
||||
}
|
||||
if (lenA == 0) {
|
||||
wrenSetSlotBytes(vm, 0, b, lenB);
|
||||
return;
|
||||
}
|
||||
if (lenB == 0) {
|
||||
wrenSetSlotBytes(vm, 0, a, lenA);
|
||||
return;
|
||||
}
|
||||
|
||||
size_t totalLen = (size_t)lenA + (size_t)lenB;
|
||||
char* result = (char*)malloc(totalLen);
|
||||
if (result == NULL) {
|
||||
wrenSetSlotString(vm, 0, "Memory allocation failed.");
|
||||
wrenAbortFiber(vm, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
memcpy(result, a, lenA);
|
||||
memcpy(result + lenA, b, lenB);
|
||||
wrenSetSlotBytes(vm, 0, result, (int)totalLen);
|
||||
free(result);
|
||||
}
|
||||
|
||||
void bytesSlice(WrenVM* vm) {
|
||||
int length = 0;
|
||||
const char* data = wrenGetSlotBytes(vm, 1, &length);
|
||||
int start = (int)wrenGetSlotDouble(vm, 2);
|
||||
int end = (int)wrenGetSlotDouble(vm, 3);
|
||||
|
||||
if (start < 0) start = 0;
|
||||
if (end > length) end = length;
|
||||
if (start >= end || start >= length) {
|
||||
wrenSetSlotBytes(vm, 0, "", 0);
|
||||
return;
|
||||
}
|
||||
|
||||
wrenSetSlotBytes(vm, 0, data + start, end - start);
|
||||
}
|
||||
|
||||
void bytesLength(WrenVM* vm) {
|
||||
int length = 0;
|
||||
wrenGetSlotBytes(vm, 1, &length);
|
||||
wrenSetSlotDouble(vm, 0, (double)length);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
|
||||
#ifndef bytes_h
|
||||
#define bytes_h
|
||||
|
||||
#include "wren.h"
|
||||
|
||||
void bytesToList(WrenVM* vm);
|
||||
void bytesFromList(WrenVM* vm);
|
||||
void bytesXorMask(WrenVM* vm);
|
||||
void bytesConcat(WrenVM* vm);
|
||||
void bytesSlice(WrenVM* vm);
|
||||
void bytesLength(WrenVM* vm);
|
||||
|
||||
#endif
|
||||
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
|
||||
class Bytes {
|
||||
foreign static toList(data)
|
||||
foreign static fromList(list)
|
||||
foreign static xorMask(data, mask)
|
||||
foreign static concat(a, b)
|
||||
foreign static slice(data, start, end)
|
||||
foreign static length(data)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Please do not edit this file. It has been generated automatically
|
||||
// from `src/module/bytes.wren` using `util/wren_to_c_string.py`
|
||||
|
||||
static const char* bytesModuleSource =
|
||||
"// retoor <retoor@molodetz.nl>\n"
|
||||
"\n"
|
||||
"class Bytes {\n"
|
||||
" foreign static toList(data)\n"
|
||||
" foreign static fromList(list)\n"
|
||||
" foreign static xorMask(data, mask)\n"
|
||||
" foreign static concat(a, b)\n"
|
||||
" foreign static slice(data, start, end)\n"
|
||||
" foreign static length(data)\n"
|
||||
"}\n";
|
||||
Vendored
+263
@@ -0,0 +1,263 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
|
||||
import "sqlite" for Database
|
||||
import "uuid" for Uuid
|
||||
import "datetime" for DateTime
|
||||
import "json" for Json
|
||||
|
||||
class Dataset {
|
||||
construct open(path) {
|
||||
_db = Database.open(path)
|
||||
_tables = {}
|
||||
}
|
||||
|
||||
construct memory() {
|
||||
_db = Database.memory()
|
||||
_tables = {}
|
||||
}
|
||||
|
||||
db { _db }
|
||||
|
||||
[tableName] {
|
||||
if (!_tables.containsKey(tableName)) {
|
||||
_tables[tableName] = Table.new_(this, tableName)
|
||||
}
|
||||
return _tables[tableName]
|
||||
}
|
||||
|
||||
tables {
|
||||
var rows = _db.query("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
|
||||
var result = []
|
||||
for (row in rows) {
|
||||
result.add(row["name"])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
close() { _db.close() }
|
||||
}
|
||||
|
||||
class Table {
|
||||
construct new_(dataset, name) {
|
||||
_dataset = dataset
|
||||
_name = name
|
||||
_columns = null
|
||||
}
|
||||
|
||||
name { _name }
|
||||
db { _dataset.db }
|
||||
|
||||
columns {
|
||||
if (_columns == null) {
|
||||
_columns = {}
|
||||
var rows = db.query("PRAGMA table_info(" + _name + ")")
|
||||
for (row in rows) {
|
||||
_columns[row["name"]] = row["type"]
|
||||
}
|
||||
}
|
||||
return _columns
|
||||
}
|
||||
|
||||
ensureTable_() {
|
||||
var exists = db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?", [_name])
|
||||
if (exists.count == 0) {
|
||||
db.execute("CREATE TABLE " + _name + " (uid TEXT PRIMARY KEY, created_at TEXT, deleted_at TEXT)")
|
||||
_columns = null
|
||||
}
|
||||
}
|
||||
|
||||
ensureColumn_(colName, value) {
|
||||
ensureTable_()
|
||||
var cols = columns
|
||||
if (!cols.containsKey(colName)) {
|
||||
var sqlType = getSqlType_(value)
|
||||
db.execute("ALTER TABLE " + _name + " ADD COLUMN " + colName + " " + sqlType)
|
||||
_columns = null
|
||||
}
|
||||
}
|
||||
|
||||
getSqlType_(value) {
|
||||
if (value is Num) {
|
||||
if (value == value.floor) return "INTEGER"
|
||||
return "REAL"
|
||||
}
|
||||
if (value is Bool) return "INTEGER"
|
||||
if (value is Map || value is List) return "TEXT"
|
||||
return "TEXT"
|
||||
}
|
||||
|
||||
serializeValue_(value) {
|
||||
if (value is Bool) return value ? 1 : 0
|
||||
if (value is Map || value is List) return Json.stringify(value)
|
||||
return value
|
||||
}
|
||||
|
||||
deserializeRow_(row) {
|
||||
var result = {}
|
||||
for (key in row.keys) {
|
||||
var value = row[key]
|
||||
if (value is String && (value.startsWith("{") || value.startsWith("["))) {
|
||||
var fiber = Fiber.new { Json.parse(value) }
|
||||
var parsed = fiber.try()
|
||||
if (!fiber.error) {
|
||||
result[key] = parsed
|
||||
continue
|
||||
}
|
||||
}
|
||||
result[key] = value
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
insert(record) {
|
||||
ensureTable_()
|
||||
var uid = record.containsKey("uid") ? record["uid"] : Uuid.v4()
|
||||
var createdAt = record.containsKey("created_at") ? record["created_at"] : DateTime.now().toString
|
||||
|
||||
var colNames = ["uid", "created_at"]
|
||||
var placeholders = ["?", "?"]
|
||||
var values = [uid, createdAt]
|
||||
|
||||
for (key in record.keys) {
|
||||
if (key == "uid" || key == "created_at" || key == "deleted_at") continue
|
||||
ensureColumn_(key, record[key])
|
||||
colNames.add(key)
|
||||
placeholders.add("?")
|
||||
values.add(serializeValue_(record[key]))
|
||||
}
|
||||
|
||||
var sql = "INSERT INTO " + _name + " (" + colNames.join(", ") + ") VALUES (" + placeholders.join(", ") + ")"
|
||||
db.execute(sql, values)
|
||||
|
||||
var result = {}
|
||||
for (key in record.keys) {
|
||||
result[key] = record[key]
|
||||
}
|
||||
result["uid"] = uid
|
||||
result["created_at"] = createdAt
|
||||
return result
|
||||
}
|
||||
|
||||
update(record) {
|
||||
if (!record.containsKey("uid")) {
|
||||
Fiber.abort("Record must have a uid for update")
|
||||
}
|
||||
var uid = record["uid"]
|
||||
|
||||
var setParts = []
|
||||
var values = []
|
||||
for (key in record.keys) {
|
||||
if (key == "uid") continue
|
||||
ensureColumn_(key, record[key])
|
||||
setParts.add(key + " = ?")
|
||||
values.add(serializeValue_(record[key]))
|
||||
}
|
||||
values.add(uid)
|
||||
|
||||
var sql = "UPDATE " + _name + " SET " + setParts.join(", ") + " WHERE uid = ? AND deleted_at IS NULL"
|
||||
db.execute(sql, values)
|
||||
return db.changes
|
||||
}
|
||||
|
||||
delete(uid) {
|
||||
var sql = "UPDATE " + _name + " SET deleted_at = ? WHERE uid = ? AND deleted_at IS NULL"
|
||||
db.execute(sql, [DateTime.now().toString, uid])
|
||||
return db.changes > 0
|
||||
}
|
||||
|
||||
hardDelete(uid) {
|
||||
var sql = "DELETE FROM " + _name + " WHERE uid = ?"
|
||||
db.execute(sql, [uid])
|
||||
return db.changes > 0
|
||||
}
|
||||
|
||||
find(conditions) {
|
||||
ensureTable_()
|
||||
var where = buildWhere_(conditions)
|
||||
var sql = "SELECT * FROM " + _name + " WHERE deleted_at IS NULL" + where["clause"]
|
||||
var rows = db.query(sql, where["values"])
|
||||
var result = []
|
||||
for (row in rows) {
|
||||
result.add(deserializeRow_(row))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
findOne(conditions) {
|
||||
var results = find(conditions)
|
||||
return results.count > 0 ? results[0] : null
|
||||
}
|
||||
|
||||
all() {
|
||||
ensureTable_()
|
||||
var rows = db.query("SELECT * FROM " + _name + " WHERE deleted_at IS NULL")
|
||||
var result = []
|
||||
for (row in rows) {
|
||||
result.add(deserializeRow_(row))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
count() {
|
||||
ensureTable_()
|
||||
var rows = db.query("SELECT COUNT(*) as cnt FROM " + _name + " WHERE deleted_at IS NULL")
|
||||
return rows[0]["cnt"]
|
||||
}
|
||||
|
||||
buildWhere_(conditions) {
|
||||
var parts = []
|
||||
var values = []
|
||||
|
||||
for (key in conditions.keys) {
|
||||
var value = conditions[key]
|
||||
var op = "="
|
||||
var col = key
|
||||
|
||||
if (key.contains("__")) {
|
||||
var split = key.split("__")
|
||||
col = split[0]
|
||||
var suffix = split[1]
|
||||
if (suffix == "gt") {
|
||||
op = ">"
|
||||
} else if (suffix == "lt") {
|
||||
op = "<"
|
||||
} else if (suffix == "gte") {
|
||||
op = ">="
|
||||
} else if (suffix == "lte") {
|
||||
op = "<="
|
||||
} else if (suffix == "ne") {
|
||||
op = "!="
|
||||
} else if (suffix == "like") {
|
||||
op = "LIKE"
|
||||
} else if (suffix == "in") {
|
||||
if (value is List) {
|
||||
var placeholders = []
|
||||
for (v in value) {
|
||||
placeholders.add("?")
|
||||
values.add(v)
|
||||
}
|
||||
parts.add(col + " IN (" + placeholders.join(", ") + ")")
|
||||
continue
|
||||
}
|
||||
} else if (suffix == "null") {
|
||||
if (value) {
|
||||
parts.add(col + " IS NULL")
|
||||
} else {
|
||||
parts.add(col + " IS NOT NULL")
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
parts.add(col + " " + op + " ?")
|
||||
values.add(serializeValue_(value))
|
||||
}
|
||||
|
||||
var clause = ""
|
||||
if (parts.count > 0) {
|
||||
clause = " AND " + parts.join(" AND ")
|
||||
}
|
||||
|
||||
return {"clause": clause, "values": values}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
// Please do not edit this file. It has been generated automatically
|
||||
// from `src/module/dataset.wren` using `util/wren_to_c_string.py`
|
||||
|
||||
static const char* datasetModuleSource =
|
||||
"// retoor <retoor@molodetz.nl>\n"
|
||||
"\n"
|
||||
"import \"sqlite\" for Database\n"
|
||||
"import \"uuid\" for Uuid\n"
|
||||
"import \"datetime\" for DateTime\n"
|
||||
"import \"json\" for Json\n"
|
||||
"\n"
|
||||
"class Dataset {\n"
|
||||
" construct open(path) {\n"
|
||||
" _db = Database.open(path)\n"
|
||||
" _tables = {}\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" construct memory() {\n"
|
||||
" _db = Database.memory()\n"
|
||||
" _tables = {}\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" db { _db }\n"
|
||||
"\n"
|
||||
" [tableName] {\n"
|
||||
" if (!_tables.containsKey(tableName)) {\n"
|
||||
" _tables[tableName] = Table.new_(this, tableName)\n"
|
||||
" }\n"
|
||||
" return _tables[tableName]\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" tables {\n"
|
||||
" var rows = _db.query(\"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name\")\n"
|
||||
" var result = []\n"
|
||||
" for (row in rows) {\n"
|
||||
" result.add(row[\"name\"])\n"
|
||||
" }\n"
|
||||
" return result\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" close() { _db.close() }\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"class Table {\n"
|
||||
" construct new_(dataset, name) {\n"
|
||||
" _dataset = dataset\n"
|
||||
" _name = name\n"
|
||||
" _columns = null\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" name { _name }\n"
|
||||
" db { _dataset.db }\n"
|
||||
"\n"
|
||||
" columns {\n"
|
||||
" if (_columns == null) {\n"
|
||||
" _columns = {}\n"
|
||||
" var rows = db.query(\"PRAGMA table_info(\" + _name + \")\")\n"
|
||||
" for (row in rows) {\n"
|
||||
" _columns[row[\"name\"]] = row[\"type\"]\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" return _columns\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" ensureTable_() {\n"
|
||||
" var exists = db.query(\"SELECT name FROM sqlite_master WHERE type='table' AND name=?\", [_name])\n"
|
||||
" if (exists.count == 0) {\n"
|
||||
" db.execute(\"CREATE TABLE \" + _name + \" (uid TEXT PRIMARY KEY, created_at TEXT, deleted_at TEXT)\")\n"
|
||||
" _columns = null\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" ensureColumn_(colName, value) {\n"
|
||||
" ensureTable_()\n"
|
||||
" var cols = columns\n"
|
||||
" if (!cols.containsKey(colName)) {\n"
|
||||
" var sqlType = getSqlType_(value)\n"
|
||||
" db.execute(\"ALTER TABLE \" + _name + \" ADD COLUMN \" + colName + \" \" + sqlType)\n"
|
||||
" _columns = null\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" getSqlType_(value) {\n"
|
||||
" if (value is Num) {\n"
|
||||
" if (value == value.floor) return \"INTEGER\"\n"
|
||||
" return \"REAL\"\n"
|
||||
" }\n"
|
||||
" if (value is Bool) return \"INTEGER\"\n"
|
||||
" if (value is Map || value is List) return \"TEXT\"\n"
|
||||
" return \"TEXT\"\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" serializeValue_(value) {\n"
|
||||
" if (value is Bool) return value ? 1 : 0\n"
|
||||
" if (value is Map || value is List) return Json.stringify(value)\n"
|
||||
" return value\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" deserializeRow_(row) {\n"
|
||||
" var result = {}\n"
|
||||
" for (key in row.keys) {\n"
|
||||
" var value = row[key]\n"
|
||||
" if (value is String && (value.startsWith(\"{\") || value.startsWith(\"[\"))) {\n"
|
||||
" var fiber = Fiber.new { Json.parse(value) }\n"
|
||||
" var parsed = fiber.try()\n"
|
||||
" if (!fiber.error) {\n"
|
||||
" result[key] = parsed\n"
|
||||
" continue\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" result[key] = value\n"
|
||||
" }\n"
|
||||
" return result\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" insert(record) {\n"
|
||||
" ensureTable_()\n"
|
||||
" var uid = record.containsKey(\"uid\") ? record[\"uid\"] : Uuid.v4()\n"
|
||||
" var createdAt = record.containsKey(\"created_at\") ? record[\"created_at\"] : DateTime.now().toString\n"
|
||||
"\n"
|
||||
" var colNames = [\"uid\", \"created_at\"]\n"
|
||||
" var placeholders = [\"?\", \"?\"]\n"
|
||||
" var values = [uid, createdAt]\n"
|
||||
"\n"
|
||||
" for (key in record.keys) {\n"
|
||||
" if (key == \"uid\" || key == \"created_at\" || key == \"deleted_at\") continue\n"
|
||||
" ensureColumn_(key, record[key])\n"
|
||||
" colNames.add(key)\n"
|
||||
" placeholders.add(\"?\")\n"
|
||||
" values.add(serializeValue_(record[key]))\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" var sql = \"INSERT INTO \" + _name + \" (\" + colNames.join(\", \") + \") VALUES (\" + placeholders.join(\", \") + \")\"\n"
|
||||
" db.execute(sql, values)\n"
|
||||
"\n"
|
||||
" var result = {}\n"
|
||||
" for (key in record.keys) {\n"
|
||||
" result[key] = record[key]\n"
|
||||
" }\n"
|
||||
" result[\"uid\"] = uid\n"
|
||||
" result[\"created_at\"] = createdAt\n"
|
||||
" return result\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" update(record) {\n"
|
||||
" if (!record.containsKey(\"uid\")) {\n"
|
||||
" Fiber.abort(\"Record must have a uid for update\")\n"
|
||||
" }\n"
|
||||
" var uid = record[\"uid\"]\n"
|
||||
"\n"
|
||||
" var setParts = []\n"
|
||||
" var values = []\n"
|
||||
" for (key in record.keys) {\n"
|
||||
" if (key == \"uid\") continue\n"
|
||||
" ensureColumn_(key, record[key])\n"
|
||||
" setParts.add(key + \" = ?\")\n"
|
||||
" values.add(serializeValue_(record[key]))\n"
|
||||
" }\n"
|
||||
" values.add(uid)\n"
|
||||
"\n"
|
||||
" var sql = \"UPDATE \" + _name + \" SET \" + setParts.join(\", \") + \" WHERE uid = ? AND deleted_at IS NULL\"\n"
|
||||
" db.execute(sql, values)\n"
|
||||
" return db.changes\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" delete(uid) {\n"
|
||||
" var sql = \"UPDATE \" + _name + \" SET deleted_at = ? WHERE uid = ? AND deleted_at IS NULL\"\n"
|
||||
" db.execute(sql, [DateTime.now().toString, uid])\n"
|
||||
" return db.changes > 0\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" hardDelete(uid) {\n"
|
||||
" var sql = \"DELETE FROM \" + _name + \" WHERE uid = ?\"\n"
|
||||
" db.execute(sql, [uid])\n"
|
||||
" return db.changes > 0\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" find(conditions) {\n"
|
||||
" ensureTable_()\n"
|
||||
" var where = buildWhere_(conditions)\n"
|
||||
" var sql = \"SELECT * FROM \" + _name + \" WHERE deleted_at IS NULL\" + where[\"clause\"]\n"
|
||||
" var rows = db.query(sql, where[\"values\"])\n"
|
||||
" var result = []\n"
|
||||
" for (row in rows) {\n"
|
||||
" result.add(deserializeRow_(row))\n"
|
||||
" }\n"
|
||||
" return result\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" findOne(conditions) {\n"
|
||||
" var results = find(conditions)\n"
|
||||
" return results.count > 0 ? results[0] : null\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" all() {\n"
|
||||
" ensureTable_()\n"
|
||||
" var rows = db.query(\"SELECT * FROM \" + _name + \" WHERE deleted_at IS NULL\")\n"
|
||||
" var result = []\n"
|
||||
" for (row in rows) {\n"
|
||||
" result.add(deserializeRow_(row))\n"
|
||||
" }\n"
|
||||
" return result\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" count() {\n"
|
||||
" ensureTable_()\n"
|
||||
" var rows = db.query(\"SELECT COUNT(*) as cnt FROM \" + _name + \" WHERE deleted_at IS NULL\")\n"
|
||||
" return rows[0][\"cnt\"]\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" buildWhere_(conditions) {\n"
|
||||
" var parts = []\n"
|
||||
" var values = []\n"
|
||||
"\n"
|
||||
" for (key in conditions.keys) {\n"
|
||||
" var value = conditions[key]\n"
|
||||
" var op = \"=\"\n"
|
||||
" var col = key\n"
|
||||
"\n"
|
||||
" if (key.contains(\"__\")) {\n"
|
||||
" var split = key.split(\"__\")\n"
|
||||
" col = split[0]\n"
|
||||
" var suffix = split[1]\n"
|
||||
" if (suffix == \"gt\") {\n"
|
||||
" op = \">\"\n"
|
||||
" } else if (suffix == \"lt\") {\n"
|
||||
" op = \"<\"\n"
|
||||
" } else if (suffix == \"gte\") {\n"
|
||||
" op = \">=\"\n"
|
||||
" } else if (suffix == \"lte\") {\n"
|
||||
" op = \"<=\"\n"
|
||||
" } else if (suffix == \"ne\") {\n"
|
||||
" op = \"!=\"\n"
|
||||
" } else if (suffix == \"like\") {\n"
|
||||
" op = \"LIKE\"\n"
|
||||
" } else if (suffix == \"in\") {\n"
|
||||
" if (value is List) {\n"
|
||||
" var placeholders = []\n"
|
||||
" for (v in value) {\n"
|
||||
" placeholders.add(\"?\")\n"
|
||||
" values.add(v)\n"
|
||||
" }\n"
|
||||
" parts.add(col + \" IN (\" + placeholders.join(\", \") + \")\")\n"
|
||||
" continue\n"
|
||||
" }\n"
|
||||
" } else if (suffix == \"null\") {\n"
|
||||
" if (value) {\n"
|
||||
" parts.add(col + \" IS NULL\")\n"
|
||||
" } else {\n"
|
||||
" parts.add(col + \" IS NOT NULL\")\n"
|
||||
" }\n"
|
||||
" continue\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" parts.add(col + \" \" + op + \" ?\")\n"
|
||||
" values.add(serializeValue_(value))\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" var clause = \"\"\n"
|
||||
" if (parts.count > 0) {\n"
|
||||
" clause = \" AND \" + parts.join(\" AND \")\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" return {\"clause\": clause, \"values\": values}\n"
|
||||
" }\n"
|
||||
"}\n";
|
||||
Vendored
+177
@@ -0,0 +1,177 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
|
||||
class Html {
|
||||
static isUnreserved_(c) {
|
||||
var code = c.codePoints.toList[0]
|
||||
if (code >= 65 && code <= 90) return true
|
||||
if (code >= 97 && code <= 122) return true
|
||||
if (code >= 48 && code <= 57) return true
|
||||
if (c == "-" || c == "_" || c == "." || c == "~") return true
|
||||
return false
|
||||
}
|
||||
|
||||
static hexDigit_(n) {
|
||||
if (n < 10) return String.fromCodePoint(48 + n)
|
||||
return String.fromCodePoint(65 + n - 10)
|
||||
}
|
||||
|
||||
static hexValue_(c) {
|
||||
var code = c.codePoints.toList[0]
|
||||
if (code >= 48 && code <= 57) return code - 48
|
||||
if (code >= 65 && code <= 70) return code - 55
|
||||
if (code >= 97 && code <= 102) return code - 87
|
||||
return -1
|
||||
}
|
||||
|
||||
static urlencode(string) {
|
||||
if (!(string is String)) Fiber.abort("Argument must be a string.")
|
||||
var result = ""
|
||||
for (c in string) {
|
||||
if (isUnreserved_(c)) {
|
||||
result = result + c
|
||||
} else if (c == " ") {
|
||||
result = result + "+"
|
||||
} else {
|
||||
for (b in c.bytes) {
|
||||
result = result + "\%" + hexDigit_((b >> 4) & 0x0F) + hexDigit_(b & 0x0F)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
static decodeUtf8_(bytes) {
|
||||
var result = ""
|
||||
var i = 0
|
||||
while (i < bytes.count) {
|
||||
var b = bytes[i]
|
||||
if (b < 128) {
|
||||
result = result + String.fromCodePoint(b)
|
||||
i = i + 1
|
||||
} else if ((b & 0xE0) == 0xC0 && i + 1 < bytes.count) {
|
||||
var cp = ((b & 0x1F) << 6) | (bytes[i + 1] & 0x3F)
|
||||
result = result + String.fromCodePoint(cp)
|
||||
i = i + 2
|
||||
} else if ((b & 0xF0) == 0xE0 && i + 2 < bytes.count) {
|
||||
var cp = ((b & 0x0F) << 12) | ((bytes[i + 1] & 0x3F) << 6) | (bytes[i + 2] & 0x3F)
|
||||
result = result + String.fromCodePoint(cp)
|
||||
i = i + 3
|
||||
} else if ((b & 0xF8) == 0xF0 && i + 3 < bytes.count) {
|
||||
var cp = ((b & 0x07) << 18) | ((bytes[i + 1] & 0x3F) << 12) | ((bytes[i + 2] & 0x3F) << 6) | (bytes[i + 3] & 0x3F)
|
||||
result = result + String.fromCodePoint(cp)
|
||||
i = i + 4
|
||||
} else {
|
||||
result = result + String.fromCodePoint(0xFFFD)
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
static urldecode(string) {
|
||||
if (!(string is String)) Fiber.abort("Argument must be a string.")
|
||||
var bytes = []
|
||||
var i = 0
|
||||
var chars = string.toList
|
||||
while (i < chars.count) {
|
||||
if (chars[i] == "+") {
|
||||
bytes.add(32)
|
||||
i = i + 1
|
||||
} else if (chars[i] == "\%" && i + 2 < chars.count) {
|
||||
var hi = hexValue_(chars[i + 1])
|
||||
var lo = hexValue_(chars[i + 2])
|
||||
if (hi >= 0 && lo >= 0) {
|
||||
bytes.add((hi << 4) | lo)
|
||||
i = i + 3
|
||||
} else {
|
||||
for (b in chars[i].bytes) bytes.add(b)
|
||||
i = i + 1
|
||||
}
|
||||
} else {
|
||||
for (b in chars[i].bytes) bytes.add(b)
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
return decodeUtf8_(bytes)
|
||||
}
|
||||
|
||||
static slugify(string) {
|
||||
if (!(string is String)) Fiber.abort("Argument must be a string.")
|
||||
var result = ""
|
||||
var prevHyphen = true
|
||||
for (c in string.bytes) {
|
||||
if ((c >= 65 && c <= 90)) {
|
||||
result = result + String.fromCodePoint(c + 32)
|
||||
prevHyphen = false
|
||||
} else if ((c >= 97 && c <= 122) || (c >= 48 && c <= 57)) {
|
||||
result = result + String.fromCodePoint(c)
|
||||
prevHyphen = false
|
||||
} else if (!prevHyphen) {
|
||||
result = result + "-"
|
||||
prevHyphen = true
|
||||
}
|
||||
}
|
||||
if (result.count > 0 && result[-1] == "-") {
|
||||
result = result[0..-2]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
static quote(string) {
|
||||
if (!(string is String)) Fiber.abort("Argument must be a string.")
|
||||
var result = ""
|
||||
for (c in string) {
|
||||
if (c == "&") {
|
||||
result = result + "&"
|
||||
} else if (c == "<") {
|
||||
result = result + "<"
|
||||
} else if (c == ">") {
|
||||
result = result + ">"
|
||||
} else if (c == "\"") {
|
||||
result = result + """
|
||||
} else if (c == "'") {
|
||||
result = result + "'"
|
||||
} else {
|
||||
result = result + c
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
static unquote(string) {
|
||||
if (!(string is String)) Fiber.abort("Argument must be a string.")
|
||||
return string
|
||||
.replace("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace(""", "\"")
|
||||
.replace("'", "'")
|
||||
}
|
||||
|
||||
static encodeParams(params) {
|
||||
if (!(params is Map)) Fiber.abort("Argument must be a map.")
|
||||
var parts = []
|
||||
for (key in params.keys) {
|
||||
parts.add(urlencode(key.toString) + "=" + urlencode(params[key].toString))
|
||||
}
|
||||
return parts.join("&")
|
||||
}
|
||||
|
||||
static decodeParams(string) {
|
||||
if (!(string is String)) Fiber.abort("Argument must be a string.")
|
||||
var params = {}
|
||||
if (string.count == 0) return params
|
||||
var pairs = string.split("&")
|
||||
for (pair in pairs) {
|
||||
var idx = pair.indexOf("=")
|
||||
if (idx > 0) {
|
||||
var key = urldecode(pair[0...idx])
|
||||
var value = urldecode(pair[idx + 1..-1])
|
||||
params[key] = value
|
||||
} else if (pair.count > 0) {
|
||||
params[urldecode(pair)] = ""
|
||||
}
|
||||
}
|
||||
return params
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
// Please do not edit this file. It has been generated automatically
|
||||
// from `src/module/html.wren` using `util/wren_to_c_string.py`
|
||||
|
||||
static const char* htmlModuleSource =
|
||||
"// retoor <retoor@molodetz.nl>\n"
|
||||
"\n"
|
||||
"class Html {\n"
|
||||
" static isUnreserved_(c) {\n"
|
||||
" var code = c.codePoints.toList[0]\n"
|
||||
" if (code >= 65 && code <= 90) return true\n"
|
||||
" if (code >= 97 && code <= 122) return true\n"
|
||||
" if (code >= 48 && code <= 57) return true\n"
|
||||
" if (c == \"-\" || c == \"_\" || c == \".\" || c == \"~\") return true\n"
|
||||
" return false\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static hexDigit_(n) {\n"
|
||||
" if (n < 10) return String.fromCodePoint(48 + n)\n"
|
||||
" return String.fromCodePoint(65 + n - 10)\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static hexValue_(c) {\n"
|
||||
" var code = c.codePoints.toList[0]\n"
|
||||
" if (code >= 48 && code <= 57) return code - 48\n"
|
||||
" if (code >= 65 && code <= 70) return code - 55\n"
|
||||
" if (code >= 97 && code <= 102) return code - 87\n"
|
||||
" return -1\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static urlencode(string) {\n"
|
||||
" if (!(string is String)) Fiber.abort(\"Argument must be a string.\")\n"
|
||||
" var result = \"\"\n"
|
||||
" for (c in string) {\n"
|
||||
" if (isUnreserved_(c)) {\n"
|
||||
" result = result + c\n"
|
||||
" } else if (c == \" \") {\n"
|
||||
" result = result + \"+\"\n"
|
||||
" } else {\n"
|
||||
" for (b in c.bytes) {\n"
|
||||
" result = result + \"\\%\" + hexDigit_((b >> 4) & 0x0F) + hexDigit_(b & 0x0F)\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" return result\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static decodeUtf8_(bytes) {\n"
|
||||
" var result = \"\"\n"
|
||||
" var i = 0\n"
|
||||
" while (i < bytes.count) {\n"
|
||||
" var b = bytes[i]\n"
|
||||
" if (b < 128) {\n"
|
||||
" result = result + String.fromCodePoint(b)\n"
|
||||
" i = i + 1\n"
|
||||
" } else if ((b & 0xE0) == 0xC0 && i + 1 < bytes.count) {\n"
|
||||
" var cp = ((b & 0x1F) << 6) | (bytes[i + 1] & 0x3F)\n"
|
||||
" result = result + String.fromCodePoint(cp)\n"
|
||||
" i = i + 2\n"
|
||||
" } else if ((b & 0xF0) == 0xE0 && i + 2 < bytes.count) {\n"
|
||||
" var cp = ((b & 0x0F) << 12) | ((bytes[i + 1] & 0x3F) << 6) | (bytes[i + 2] & 0x3F)\n"
|
||||
" result = result + String.fromCodePoint(cp)\n"
|
||||
" i = i + 3\n"
|
||||
" } else if ((b & 0xF8) == 0xF0 && i + 3 < bytes.count) {\n"
|
||||
" var cp = ((b & 0x07) << 18) | ((bytes[i + 1] & 0x3F) << 12) | ((bytes[i + 2] & 0x3F) << 6) | (bytes[i + 3] & 0x3F)\n"
|
||||
" result = result + String.fromCodePoint(cp)\n"
|
||||
" i = i + 4\n"
|
||||
" } else {\n"
|
||||
" result = result + String.fromCodePoint(0xFFFD)\n"
|
||||
" i = i + 1\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" return result\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static urldecode(string) {\n"
|
||||
" if (!(string is String)) Fiber.abort(\"Argument must be a string.\")\n"
|
||||
" var bytes = []\n"
|
||||
" var i = 0\n"
|
||||
" var chars = string.toList\n"
|
||||
" while (i < chars.count) {\n"
|
||||
" if (chars[i] == \"+\") {\n"
|
||||
" bytes.add(32)\n"
|
||||
" i = i + 1\n"
|
||||
" } else if (chars[i] == \"\\%\" && i + 2 < chars.count) {\n"
|
||||
" var hi = hexValue_(chars[i + 1])\n"
|
||||
" var lo = hexValue_(chars[i + 2])\n"
|
||||
" if (hi >= 0 && lo >= 0) {\n"
|
||||
" bytes.add((hi << 4) | lo)\n"
|
||||
" i = i + 3\n"
|
||||
" } else {\n"
|
||||
" for (b in chars[i].bytes) bytes.add(b)\n"
|
||||
" i = i + 1\n"
|
||||
" }\n"
|
||||
" } else {\n"
|
||||
" for (b in chars[i].bytes) bytes.add(b)\n"
|
||||
" i = i + 1\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" return decodeUtf8_(bytes)\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static slugify(string) {\n"
|
||||
" if (!(string is String)) Fiber.abort(\"Argument must be a string.\")\n"
|
||||
" var result = \"\"\n"
|
||||
" var prevHyphen = true\n"
|
||||
" for (c in string.bytes) {\n"
|
||||
" if ((c >= 65 && c <= 90)) {\n"
|
||||
" result = result + String.fromCodePoint(c + 32)\n"
|
||||
" prevHyphen = false\n"
|
||||
" } else if ((c >= 97 && c <= 122) || (c >= 48 && c <= 57)) {\n"
|
||||
" result = result + String.fromCodePoint(c)\n"
|
||||
" prevHyphen = false\n"
|
||||
" } else if (!prevHyphen) {\n"
|
||||
" result = result + \"-\"\n"
|
||||
" prevHyphen = true\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" if (result.count > 0 && result[-1] == \"-\") {\n"
|
||||
" result = result[0..-2]\n"
|
||||
" }\n"
|
||||
" return result\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static quote(string) {\n"
|
||||
" if (!(string is String)) Fiber.abort(\"Argument must be a string.\")\n"
|
||||
" var result = \"\"\n"
|
||||
" for (c in string) {\n"
|
||||
" if (c == \"&\") {\n"
|
||||
" result = result + \"&\"\n"
|
||||
" } else if (c == \"<\") {\n"
|
||||
" result = result + \"<\"\n"
|
||||
" } else if (c == \">\") {\n"
|
||||
" result = result + \">\"\n"
|
||||
" } else if (c == \"\\\"\") {\n"
|
||||
" result = result + \""\"\n"
|
||||
" } else if (c == \"'\") {\n"
|
||||
" result = result + \"'\"\n"
|
||||
" } else {\n"
|
||||
" result = result + c\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" return result\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static unquote(string) {\n"
|
||||
" if (!(string is String)) Fiber.abort(\"Argument must be a string.\")\n"
|
||||
" return string\n"
|
||||
" .replace(\"&\", \"&\")\n"
|
||||
" .replace(\"<\", \"<\")\n"
|
||||
" .replace(\">\", \">\")\n"
|
||||
" .replace(\""\", \"\\\"\")\n"
|
||||
" .replace(\"'\", \"'\")\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static encodeParams(params) {\n"
|
||||
" if (!(params is Map)) Fiber.abort(\"Argument must be a map.\")\n"
|
||||
" var parts = []\n"
|
||||
" for (key in params.keys) {\n"
|
||||
" parts.add(urlencode(key.toString) + \"=\" + urlencode(params[key].toString))\n"
|
||||
" }\n"
|
||||
" return parts.join(\"&\")\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static decodeParams(string) {\n"
|
||||
" if (!(string is String)) Fiber.abort(\"Argument must be a string.\")\n"
|
||||
" var params = {}\n"
|
||||
" if (string.count == 0) return params\n"
|
||||
" var pairs = string.split(\"&\")\n"
|
||||
" for (pair in pairs) {\n"
|
||||
" var idx = pair.indexOf(\"=\")\n"
|
||||
" if (idx > 0) {\n"
|
||||
" var key = urldecode(pair[0...idx])\n"
|
||||
" var value = urldecode(pair[idx + 1..-1])\n"
|
||||
" params[key] = value\n"
|
||||
" } else if (pair.count > 0) {\n"
|
||||
" params[urldecode(pair)] = \"\"\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" return params\n"
|
||||
" }\n"
|
||||
"}\n";
|
||||
Vendored
+328
@@ -0,0 +1,328 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
|
||||
import "html" for Html
|
||||
|
||||
class Markdown {
|
||||
static toHtml(text) { toHtml(text, {}) }
|
||||
|
||||
static toHtml(text, options) {
|
||||
var safeMode = options.containsKey("safeMode") ? options["safeMode"] : false
|
||||
var lines = text.split("\n")
|
||||
var result = []
|
||||
var inCodeBlock = false
|
||||
var codeBlockContent = []
|
||||
var inList = false
|
||||
var listType = null
|
||||
var inBlockquote = false
|
||||
|
||||
for (i in 0...lines.count) {
|
||||
var line = lines[i]
|
||||
|
||||
if (line.startsWith("```")) {
|
||||
if (inCodeBlock) {
|
||||
result.add("<pre><code>" + (safeMode ? Html.quote(codeBlockContent.join("\n")) : codeBlockContent.join("\n")) + "</code></pre>")
|
||||
codeBlockContent = []
|
||||
inCodeBlock = false
|
||||
} else {
|
||||
closeList_(result, inList, listType)
|
||||
inList = false
|
||||
inCodeBlock = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (inCodeBlock) {
|
||||
codeBlockContent.add(line)
|
||||
continue
|
||||
}
|
||||
|
||||
if (line.count == 0) {
|
||||
closeList_(result, inList, listType)
|
||||
inList = false
|
||||
if (inBlockquote) {
|
||||
result.add("</blockquote>")
|
||||
inBlockquote = false
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (line.startsWith("> ")) {
|
||||
closeList_(result, inList, listType)
|
||||
inList = false
|
||||
if (!inBlockquote) {
|
||||
result.add("<blockquote>")
|
||||
inBlockquote = true
|
||||
}
|
||||
result.add("<p>" + processInline_(line[2..-1], safeMode) + "</p>")
|
||||
continue
|
||||
}
|
||||
|
||||
if (line.startsWith("---") || line.startsWith("***") || line.startsWith("___")) {
|
||||
var isHr = true
|
||||
for (c in line) {
|
||||
if (c != "-" && c != "*" && c != "_" && c != " ") {
|
||||
isHr = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if (isHr && line.count >= 3) {
|
||||
closeList_(result, inList, listType)
|
||||
inList = false
|
||||
result.add("<hr>")
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
var heading = parseHeading_(line)
|
||||
if (heading) {
|
||||
closeList_(result, inList, listType)
|
||||
inList = false
|
||||
result.add("<h%(heading["level"])>" + processInline_(heading["text"], safeMode) + "</h%(heading["level"])>")
|
||||
continue
|
||||
}
|
||||
|
||||
var listItem = parseListItem_(line)
|
||||
if (listItem) {
|
||||
var newType = listItem["type"]
|
||||
if (!inList || listType != newType) {
|
||||
closeList_(result, inList, listType)
|
||||
result.add(newType == "ul" ? "<ul>" : "<ol>")
|
||||
inList = true
|
||||
listType = newType
|
||||
}
|
||||
result.add("<li>" + processInline_(listItem["text"], safeMode) + "</li>")
|
||||
continue
|
||||
}
|
||||
|
||||
closeList_(result, inList, listType)
|
||||
inList = false
|
||||
result.add("<p>" + processInline_(line, safeMode) + "</p>")
|
||||
}
|
||||
|
||||
closeList_(result, inList, listType)
|
||||
if (inBlockquote) {
|
||||
result.add("</blockquote>")
|
||||
}
|
||||
if (inCodeBlock) {
|
||||
result.add("<pre><code>" + (safeMode ? Html.quote(codeBlockContent.join("\n")) : codeBlockContent.join("\n")) + "</code></pre>")
|
||||
}
|
||||
|
||||
return result.join("\n")
|
||||
}
|
||||
|
||||
static closeList_(result, inList, listType) {
|
||||
if (inList) {
|
||||
result.add(listType == "ul" ? "</ul>" : "</ol>")
|
||||
}
|
||||
}
|
||||
|
||||
static parseHeading_(line) {
|
||||
var level = 0
|
||||
for (c in line) {
|
||||
if (c == "#") {
|
||||
level = level + 1
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
if (level > 0 && level <= 6 && line.count > level && line[level] == " ") {
|
||||
return {"level": level, "text": line[level + 1..-1]}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
static parseListItem_(line) {
|
||||
var trimmed = line
|
||||
var indent = 0
|
||||
while (trimmed.count > 0 && trimmed[0] == " ") {
|
||||
trimmed = trimmed[1..-1]
|
||||
indent = indent + 1
|
||||
}
|
||||
|
||||
if (trimmed.count >= 2 && (trimmed[0] == "-" || trimmed[0] == "*" || trimmed[0] == "+") && trimmed[1] == " ") {
|
||||
return {"type": "ul", "text": trimmed[2..-1]}
|
||||
}
|
||||
|
||||
var i = 0
|
||||
while (i < trimmed.count) {
|
||||
var c = trimmed[i].codePoints.toList[0]
|
||||
if (c < 48 || c > 57) break
|
||||
i = i + 1
|
||||
}
|
||||
if (i > 0 && i < trimmed.count - 1 && trimmed[i] == "." && trimmed[i + 1] == " ") {
|
||||
return {"type": "ol", "text": trimmed[i + 2..-1]}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
static processInline_(text, safeMode) {
|
||||
if (safeMode) text = Html.quote(text)
|
||||
text = processCode_(text)
|
||||
text = processBold_(text)
|
||||
text = processItalic_(text)
|
||||
text = processStrikethrough_(text)
|
||||
text = processImages_(text)
|
||||
text = processLinks_(text)
|
||||
return text
|
||||
}
|
||||
|
||||
static processCode_(text) {
|
||||
var result = ""
|
||||
var i = 0
|
||||
var chars = text.toList
|
||||
while (i < chars.count) {
|
||||
if (chars[i] == "`") {
|
||||
var end = i + 1
|
||||
while (end < chars.count && chars[end] != "`") {
|
||||
end = end + 1
|
||||
}
|
||||
if (end < chars.count) {
|
||||
var code = ""
|
||||
for (j in (i + 1)...end) {
|
||||
code = code + chars[j]
|
||||
}
|
||||
result = result + "<code>" + code + "</code>"
|
||||
i = end + 1
|
||||
} else {
|
||||
result = result + chars[i]
|
||||
i = i + 1
|
||||
}
|
||||
} else {
|
||||
result = result + chars[i]
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
static processBold_(text) {
|
||||
var result = text
|
||||
while (result.contains("**")) {
|
||||
var start = result.indexOf("**")
|
||||
var rest = result[start + 2..-1]
|
||||
var end = rest.indexOf("**")
|
||||
if (end >= 0) {
|
||||
var before = result[0...start]
|
||||
var content = rest[0...end]
|
||||
var after = rest[end + 2..-1]
|
||||
result = before + "<strong>" + content + "</strong>" + after
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
while (result.contains("__")) {
|
||||
var start = result.indexOf("__")
|
||||
var rest = result[start + 2..-1]
|
||||
var end = rest.indexOf("__")
|
||||
if (end >= 0) {
|
||||
var before = result[0...start]
|
||||
var content = rest[0...end]
|
||||
var after = rest[end + 2..-1]
|
||||
result = before + "<strong>" + content + "</strong>" + after
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
static processItalic_(text) {
|
||||
var result = text
|
||||
while (result.contains("*")) {
|
||||
var start = result.indexOf("*")
|
||||
if (start > 0 && result[start - 1] == "<") {
|
||||
break
|
||||
}
|
||||
var rest = result[start + 1..-1]
|
||||
var end = rest.indexOf("*")
|
||||
if (end >= 0 && end > 0) {
|
||||
var before = result[0...start]
|
||||
var content = rest[0...end]
|
||||
var after = rest[end + 1..-1]
|
||||
result = before + "<em>" + content + "</em>" + after
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
static processStrikethrough_(text) {
|
||||
var result = text
|
||||
while (result.contains("~~")) {
|
||||
var start = result.indexOf("~~")
|
||||
var rest = result[start + 2..-1]
|
||||
var end = rest.indexOf("~~")
|
||||
if (end >= 0) {
|
||||
var before = result[0...start]
|
||||
var content = rest[0...end]
|
||||
var after = rest[end + 2..-1]
|
||||
result = before + "<del>" + content + "</del>" + after
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
static processLinks_(text) {
|
||||
var result = text
|
||||
while (result.contains("](")) {
|
||||
var linkEnd = result.indexOf("](")
|
||||
var textStart = linkEnd
|
||||
while (textStart > 0 && result[textStart - 1] != "[") {
|
||||
textStart = textStart - 1
|
||||
}
|
||||
if (textStart > 0 && result[textStart - 1] == "[") {
|
||||
var urlStart = linkEnd + 2
|
||||
var urlEnd = urlStart
|
||||
while (urlEnd < result.count && result[urlEnd] != ")") {
|
||||
urlEnd = urlEnd + 1
|
||||
}
|
||||
if (urlEnd < result.count) {
|
||||
var before = result[0...textStart - 1]
|
||||
var linkText = result[textStart...linkEnd]
|
||||
var url = result[urlStart...urlEnd]
|
||||
var after = result[urlEnd + 1..-1]
|
||||
result = before + "<a href=\"" + url + "\">" + linkText + "</a>" + after
|
||||
} else {
|
||||
break
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
static processImages_(text) {
|
||||
var result = text
|
||||
while (result.contains("![")) {
|
||||
var start = result.indexOf("![")
|
||||
var altEnd = start + 2
|
||||
while (altEnd < result.count && result[altEnd] != "]") {
|
||||
altEnd = altEnd + 1
|
||||
}
|
||||
if (altEnd < result.count && altEnd + 1 < result.count && result[altEnd + 1] == "(") {
|
||||
var urlStart = altEnd + 2
|
||||
var urlEnd = urlStart
|
||||
while (urlEnd < result.count && result[urlEnd] != ")") {
|
||||
urlEnd = urlEnd + 1
|
||||
}
|
||||
if (urlEnd < result.count) {
|
||||
var before = result[0...start]
|
||||
var alt = result[start + 2...altEnd]
|
||||
var url = result[urlStart...urlEnd]
|
||||
var after = result[urlEnd + 1..-1]
|
||||
result = before + "<img src=\"" + url + "\" alt=\"" + alt + "\">" + after
|
||||
} else {
|
||||
break
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
// Please do not edit this file. It has been generated automatically
|
||||
// from `src/module/markdown.wren` using `util/wren_to_c_string.py`
|
||||
|
||||
static const char* markdownModuleSource =
|
||||
"// retoor <retoor@molodetz.nl>\n"
|
||||
"\n"
|
||||
"import \"html\" for Html\n"
|
||||
"\n"
|
||||
"class Markdown {\n"
|
||||
" static toHtml(text) { toHtml(text, {}) }\n"
|
||||
"\n"
|
||||
" static toHtml(text, options) {\n"
|
||||
" var safeMode = options.containsKey(\"safeMode\") ? options[\"safeMode\"] : false\n"
|
||||
" var lines = text.split(\"\\n\")\n"
|
||||
" var result = []\n"
|
||||
" var inCodeBlock = false\n"
|
||||
" var codeBlockContent = []\n"
|
||||
" var inList = false\n"
|
||||
" var listType = null\n"
|
||||
" var inBlockquote = false\n"
|
||||
"\n"
|
||||
" for (i in 0...lines.count) {\n"
|
||||
" var line = lines[i]\n"
|
||||
"\n"
|
||||
" if (line.startsWith(\"```\")) {\n"
|
||||
" if (inCodeBlock) {\n"
|
||||
" result.add(\"<pre><code>\" + (safeMode ? Html.quote(codeBlockContent.join(\"\\n\")) : codeBlockContent.join(\"\\n\")) + \"</code></pre>\")\n"
|
||||
" codeBlockContent = []\n"
|
||||
" inCodeBlock = false\n"
|
||||
" } else {\n"
|
||||
" closeList_(result, inList, listType)\n"
|
||||
" inList = false\n"
|
||||
" inCodeBlock = true\n"
|
||||
" }\n"
|
||||
" continue\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" if (inCodeBlock) {\n"
|
||||
" codeBlockContent.add(line)\n"
|
||||
" continue\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" if (line.count == 0) {\n"
|
||||
" closeList_(result, inList, listType)\n"
|
||||
" inList = false\n"
|
||||
" if (inBlockquote) {\n"
|
||||
" result.add(\"</blockquote>\")\n"
|
||||
" inBlockquote = false\n"
|
||||
" }\n"
|
||||
" continue\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" if (line.startsWith(\"> \")) {\n"
|
||||
" closeList_(result, inList, listType)\n"
|
||||
" inList = false\n"
|
||||
" if (!inBlockquote) {\n"
|
||||
" result.add(\"<blockquote>\")\n"
|
||||
" inBlockquote = true\n"
|
||||
" }\n"
|
||||
" result.add(\"<p>\" + processInline_(line[2..-1], safeMode) + \"</p>\")\n"
|
||||
" continue\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" if (line.startsWith(\"---\") || line.startsWith(\"***\") || line.startsWith(\"___\")) {\n"
|
||||
" var isHr = true\n"
|
||||
" for (c in line) {\n"
|
||||
" if (c != \"-\" && c != \"*\" && c != \"_\" && c != \" \") {\n"
|
||||
" isHr = false\n"
|
||||
" break\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" if (isHr && line.count >= 3) {\n"
|
||||
" closeList_(result, inList, listType)\n"
|
||||
" inList = false\n"
|
||||
" result.add(\"<hr>\")\n"
|
||||
" continue\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" var heading = parseHeading_(line)\n"
|
||||
" if (heading) {\n"
|
||||
" closeList_(result, inList, listType)\n"
|
||||
" inList = false\n"
|
||||
" result.add(\"<h%(heading[\"level\"])>\" + processInline_(heading[\"text\"], safeMode) + \"</h%(heading[\"level\"])>\")\n"
|
||||
" continue\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" var listItem = parseListItem_(line)\n"
|
||||
" if (listItem) {\n"
|
||||
" var newType = listItem[\"type\"]\n"
|
||||
" if (!inList || listType != newType) {\n"
|
||||
" closeList_(result, inList, listType)\n"
|
||||
" result.add(newType == \"ul\" ? \"<ul>\" : \"<ol>\")\n"
|
||||
" inList = true\n"
|
||||
" listType = newType\n"
|
||||
" }\n"
|
||||
" result.add(\"<li>\" + processInline_(listItem[\"text\"], safeMode) + \"</li>\")\n"
|
||||
" continue\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" closeList_(result, inList, listType)\n"
|
||||
" inList = false\n"
|
||||
" result.add(\"<p>\" + processInline_(line, safeMode) + \"</p>\")\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" closeList_(result, inList, listType)\n"
|
||||
" if (inBlockquote) {\n"
|
||||
" result.add(\"</blockquote>\")\n"
|
||||
" }\n"
|
||||
" if (inCodeBlock) {\n"
|
||||
" result.add(\"<pre><code>\" + (safeMode ? Html.quote(codeBlockContent.join(\"\\n\")) : codeBlockContent.join(\"\\n\")) + \"</code></pre>\")\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" return result.join(\"\\n\")\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static closeList_(result, inList, listType) {\n"
|
||||
" if (inList) {\n"
|
||||
" result.add(listType == \"ul\" ? \"</ul>\" : \"</ol>\")\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static parseHeading_(line) {\n"
|
||||
" var level = 0\n"
|
||||
" for (c in line) {\n"
|
||||
" if (c == \"#\") {\n"
|
||||
" level = level + 1\n"
|
||||
" } else {\n"
|
||||
" break\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" if (level > 0 && level <= 6 && line.count > level && line[level] == \" \") {\n"
|
||||
" return {\"level\": level, \"text\": line[level + 1..-1]}\n"
|
||||
" }\n"
|
||||
" return null\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static parseListItem_(line) {\n"
|
||||
" var trimmed = line\n"
|
||||
" var indent = 0\n"
|
||||
" while (trimmed.count > 0 && trimmed[0] == \" \") {\n"
|
||||
" trimmed = trimmed[1..-1]\n"
|
||||
" indent = indent + 1\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" if (trimmed.count >= 2 && (trimmed[0] == \"-\" || trimmed[0] == \"*\" || trimmed[0] == \"+\") && trimmed[1] == \" \") {\n"
|
||||
" return {\"type\": \"ul\", \"text\": trimmed[2..-1]}\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" var i = 0\n"
|
||||
" while (i < trimmed.count) {\n"
|
||||
" var c = trimmed[i].codePoints.toList[0]\n"
|
||||
" if (c < 48 || c > 57) break\n"
|
||||
" i = i + 1\n"
|
||||
" }\n"
|
||||
" if (i > 0 && i < trimmed.count - 1 && trimmed[i] == \".\" && trimmed[i + 1] == \" \") {\n"
|
||||
" return {\"type\": \"ol\", \"text\": trimmed[i + 2..-1]}\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" return null\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static processInline_(text, safeMode) {\n"
|
||||
" if (safeMode) text = Html.quote(text)\n"
|
||||
" text = processCode_(text)\n"
|
||||
" text = processBold_(text)\n"
|
||||
" text = processItalic_(text)\n"
|
||||
" text = processStrikethrough_(text)\n"
|
||||
" text = processImages_(text)\n"
|
||||
" text = processLinks_(text)\n"
|
||||
" return text\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static processCode_(text) {\n"
|
||||
" var result = \"\"\n"
|
||||
" var i = 0\n"
|
||||
" var chars = text.toList\n"
|
||||
" while (i < chars.count) {\n"
|
||||
" if (chars[i] == \"`\") {\n"
|
||||
" var end = i + 1\n"
|
||||
" while (end < chars.count && chars[end] != \"`\") {\n"
|
||||
" end = end + 1\n"
|
||||
" }\n"
|
||||
" if (end < chars.count) {\n"
|
||||
" var code = \"\"\n"
|
||||
" for (j in (i + 1)...end) {\n"
|
||||
" code = code + chars[j]\n"
|
||||
" }\n"
|
||||
" result = result + \"<code>\" + code + \"</code>\"\n"
|
||||
" i = end + 1\n"
|
||||
" } else {\n"
|
||||
" result = result + chars[i]\n"
|
||||
" i = i + 1\n"
|
||||
" }\n"
|
||||
" } else {\n"
|
||||
" result = result + chars[i]\n"
|
||||
" i = i + 1\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" return result\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static processBold_(text) {\n"
|
||||
" var result = text\n"
|
||||
" while (result.contains(\"**\")) {\n"
|
||||
" var start = result.indexOf(\"**\")\n"
|
||||
" var rest = result[start + 2..-1]\n"
|
||||
" var end = rest.indexOf(\"**\")\n"
|
||||
" if (end >= 0) {\n"
|
||||
" var before = result[0...start]\n"
|
||||
" var content = rest[0...end]\n"
|
||||
" var after = rest[end + 2..-1]\n"
|
||||
" result = before + \"<strong>\" + content + \"</strong>\" + after\n"
|
||||
" } else {\n"
|
||||
" break\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" while (result.contains(\"__\")) {\n"
|
||||
" var start = result.indexOf(\"__\")\n"
|
||||
" var rest = result[start + 2..-1]\n"
|
||||
" var end = rest.indexOf(\"__\")\n"
|
||||
" if (end >= 0) {\n"
|
||||
" var before = result[0...start]\n"
|
||||
" var content = rest[0...end]\n"
|
||||
" var after = rest[end + 2..-1]\n"
|
||||
" result = before + \"<strong>\" + content + \"</strong>\" + after\n"
|
||||
" } else {\n"
|
||||
" break\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" return result\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static processItalic_(text) {\n"
|
||||
" var result = text\n"
|
||||
" while (result.contains(\"*\")) {\n"
|
||||
" var start = result.indexOf(\"*\")\n"
|
||||
" if (start > 0 && result[start - 1] == \"<\") {\n"
|
||||
" break\n"
|
||||
" }\n"
|
||||
" var rest = result[start + 1..-1]\n"
|
||||
" var end = rest.indexOf(\"*\")\n"
|
||||
" if (end >= 0 && end > 0) {\n"
|
||||
" var before = result[0...start]\n"
|
||||
" var content = rest[0...end]\n"
|
||||
" var after = rest[end + 1..-1]\n"
|
||||
" result = before + \"<em>\" + content + \"</em>\" + after\n"
|
||||
" } else {\n"
|
||||
" break\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" return result\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static processStrikethrough_(text) {\n"
|
||||
" var result = text\n"
|
||||
" while (result.contains(\"~~\")) {\n"
|
||||
" var start = result.indexOf(\"~~\")\n"
|
||||
" var rest = result[start + 2..-1]\n"
|
||||
" var end = rest.indexOf(\"~~\")\n"
|
||||
" if (end >= 0) {\n"
|
||||
" var before = result[0...start]\n"
|
||||
" var content = rest[0...end]\n"
|
||||
" var after = rest[end + 2..-1]\n"
|
||||
" result = before + \"<del>\" + content + \"</del>\" + after\n"
|
||||
" } else {\n"
|
||||
" break\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" return result\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static processLinks_(text) {\n"
|
||||
" var result = text\n"
|
||||
" while (result.contains(\"](\")) {\n"
|
||||
" var linkEnd = result.indexOf(\"](\")\n"
|
||||
" var textStart = linkEnd\n"
|
||||
" while (textStart > 0 && result[textStart - 1] != \"[\") {\n"
|
||||
" textStart = textStart - 1\n"
|
||||
" }\n"
|
||||
" if (textStart > 0 && result[textStart - 1] == \"[\") {\n"
|
||||
" var urlStart = linkEnd + 2\n"
|
||||
" var urlEnd = urlStart\n"
|
||||
" while (urlEnd < result.count && result[urlEnd] != \")\") {\n"
|
||||
" urlEnd = urlEnd + 1\n"
|
||||
" }\n"
|
||||
" if (urlEnd < result.count) {\n"
|
||||
" var before = result[0...textStart - 1]\n"
|
||||
" var linkText = result[textStart...linkEnd]\n"
|
||||
" var url = result[urlStart...urlEnd]\n"
|
||||
" var after = result[urlEnd + 1..-1]\n"
|
||||
" result = before + \"<a href=\\\"\" + url + \"\\\">\" + linkText + \"</a>\" + after\n"
|
||||
" } else {\n"
|
||||
" break\n"
|
||||
" }\n"
|
||||
" } else {\n"
|
||||
" break\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" return result\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static processImages_(text) {\n"
|
||||
" var result = text\n"
|
||||
" while (result.contains(\"![\")) {\n"
|
||||
" var start = result.indexOf(\"![\")\n"
|
||||
" var altEnd = start + 2\n"
|
||||
" while (altEnd < result.count && result[altEnd] != \"]\") {\n"
|
||||
" altEnd = altEnd + 1\n"
|
||||
" }\n"
|
||||
" if (altEnd < result.count && altEnd + 1 < result.count && result[altEnd + 1] == \"(\") {\n"
|
||||
" var urlStart = altEnd + 2\n"
|
||||
" var urlEnd = urlStart\n"
|
||||
" while (urlEnd < result.count && result[urlEnd] != \")\") {\n"
|
||||
" urlEnd = urlEnd + 1\n"
|
||||
" }\n"
|
||||
" if (urlEnd < result.count) {\n"
|
||||
" var before = result[0...start]\n"
|
||||
" var alt = result[start + 2...altEnd]\n"
|
||||
" var url = result[urlStart...urlEnd]\n"
|
||||
" var after = result[urlEnd + 1..-1]\n"
|
||||
" result = before + \"<img src=\\\"\" + url + \"\\\" alt=\\\"\" + alt + \"\\\">\" + after\n"
|
||||
" } else {\n"
|
||||
" break\n"
|
||||
" }\n"
|
||||
" } else {\n"
|
||||
" break\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" return result\n"
|
||||
" }\n"
|
||||
"}\n";
|
||||
Vendored
+49
@@ -0,0 +1,49 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
|
||||
import "crypto" for Crypto
|
||||
|
||||
class Uuid {
|
||||
static hexDigit_(n) {
|
||||
if (n < 10) return String.fromCodePoint(48 + n)
|
||||
return String.fromCodePoint(97 + n - 10)
|
||||
}
|
||||
|
||||
static toHex_(bytes) {
|
||||
var hex = ""
|
||||
for (b in bytes) {
|
||||
var hi = (b >> 4) & 0x0F
|
||||
var lo = b & 0x0F
|
||||
hex = hex + hexDigit_(hi) + hexDigit_(lo)
|
||||
}
|
||||
return hex
|
||||
}
|
||||
|
||||
static v4() {
|
||||
var bytes = Crypto.randomBytes(16)
|
||||
bytes[6] = (bytes[6] & 0x0F) | 0x40
|
||||
bytes[8] = (bytes[8] & 0x3F) | 0x80
|
||||
var hex = toHex_(bytes)
|
||||
return hex[0..7] + "-" + hex[8..11] + "-" + hex[12..15] + "-" + hex[16..19] + "-" + hex[20..31]
|
||||
}
|
||||
|
||||
static isValid(string) {
|
||||
if (!(string is String)) return false
|
||||
if (string.count != 36) return false
|
||||
if (string[8] != "-" || string[13] != "-" || string[18] != "-" || string[23] != "-") return false
|
||||
var hexChars = "0123456789abcdefABCDEF"
|
||||
var positions = [0, 1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 14, 15, 16, 17, 19, 20, 21, 22, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35]
|
||||
for (pos in positions) {
|
||||
if (!hexChars.contains(string[pos])) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
static isV4(string) {
|
||||
if (!isValid(string)) return false
|
||||
var version = string[14]
|
||||
if (version != "4") return false
|
||||
var variant = string[19]
|
||||
if (variant != "8" && variant != "9" && variant != "a" && variant != "b" && variant != "A" && variant != "B") return false
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Please do not edit this file. It has been generated automatically
|
||||
// from `src/module/uuid.wren` using `util/wren_to_c_string.py`
|
||||
|
||||
static const char* uuidModuleSource =
|
||||
"// retoor <retoor@molodetz.nl>\n"
|
||||
"\n"
|
||||
"import \"crypto\" for Crypto\n"
|
||||
"\n"
|
||||
"class Uuid {\n"
|
||||
" static hexDigit_(n) {\n"
|
||||
" if (n < 10) return String.fromCodePoint(48 + n)\n"
|
||||
" return String.fromCodePoint(97 + n - 10)\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static toHex_(bytes) {\n"
|
||||
" var hex = \"\"\n"
|
||||
" for (b in bytes) {\n"
|
||||
" var hi = (b >> 4) & 0x0F\n"
|
||||
" var lo = b & 0x0F\n"
|
||||
" hex = hex + hexDigit_(hi) + hexDigit_(lo)\n"
|
||||
" }\n"
|
||||
" return hex\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static v4() {\n"
|
||||
" var bytes = Crypto.randomBytes(16)\n"
|
||||
" bytes[6] = (bytes[6] & 0x0F) | 0x40\n"
|
||||
" bytes[8] = (bytes[8] & 0x3F) | 0x80\n"
|
||||
" var hex = toHex_(bytes)\n"
|
||||
" return hex[0..7] + \"-\" + hex[8..11] + \"-\" + hex[12..15] + \"-\" + hex[16..19] + \"-\" + hex[20..31]\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static isValid(string) {\n"
|
||||
" if (!(string is String)) return false\n"
|
||||
" if (string.count != 36) return false\n"
|
||||
" if (string[8] != \"-\" || string[13] != \"-\" || string[18] != \"-\" || string[23] != \"-\") return false\n"
|
||||
" var hexChars = \"0123456789abcdefABCDEF\"\n"
|
||||
" var positions = [0, 1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 14, 15, 16, 17, 19, 20, 21, 22, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35]\n"
|
||||
" for (pos in positions) {\n"
|
||||
" if (!hexChars.contains(string[pos])) return false\n"
|
||||
" }\n"
|
||||
" return true\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static isV4(string) {\n"
|
||||
" if (!isValid(string)) return false\n"
|
||||
" var version = string[14]\n"
|
||||
" if (version != \"4\") return false\n"
|
||||
" var variant = string[19]\n"
|
||||
" if (variant != \"8\" && variant != \"9\" && variant != \"a\" && variant != \"b\" && variant != \"A\" && variant != \"B\") return false\n"
|
||||
" return true\n"
|
||||
" }\n"
|
||||
"}\n";
|
||||
Vendored
+453
@@ -0,0 +1,453 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
|
||||
import "regex" for Regex
|
||||
import "base64" for Base64
|
||||
import "json" for Json
|
||||
import "uuid" for Uuid
|
||||
|
||||
class Validator {
|
||||
static email(value) {
|
||||
if (!(value is String)) return false
|
||||
var at = value.indexOf("@")
|
||||
if (at < 1) return false
|
||||
var dot = value.indexOf(".", at)
|
||||
if (dot < at + 2) return false
|
||||
if (dot >= value.count - 1) return false
|
||||
for (c in value) {
|
||||
var code = c.codePoints.toList[0]
|
||||
if (c == "@" || c == "." || c == "_" || c == "-" || c == "+") continue
|
||||
if (code >= 48 && code <= 57) continue
|
||||
if (code >= 65 && code <= 90) continue
|
||||
if (code >= 97 && code <= 122) continue
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
static domain(value) {
|
||||
if (!(value is String)) return false
|
||||
if (value.count == 0) return false
|
||||
if (value[0] == "-" || value[-1] == "-") return false
|
||||
var parts = value.split(".")
|
||||
if (parts.count < 2) return false
|
||||
for (part in parts) {
|
||||
if (part.count == 0 || part.count > 63) return false
|
||||
if (part[0] == "-" || part[-1] == "-") return false
|
||||
for (c in part) {
|
||||
var code = c.codePoints.toList[0]
|
||||
if (c == "-") continue
|
||||
if (code >= 48 && code <= 57) continue
|
||||
if (code >= 65 && code <= 90) continue
|
||||
if (code >= 97 && code <= 122) continue
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
static safeStr(value) {
|
||||
if (!(value is String)) return false
|
||||
for (b in value.bytes) {
|
||||
if (b < 32 || b > 126) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
static httpEncoded(value) {
|
||||
if (!(value is String)) return false
|
||||
for (c in value) {
|
||||
var code = c.codePoints.toList[0]
|
||||
if (c == "_" || c == "." || c == "~" || c == "-" || c == "+") continue
|
||||
if (c == "\%") continue
|
||||
if (code >= 48 && code <= 57) continue
|
||||
if (code >= 65 && code <= 90) continue
|
||||
if (code >= 97 && code <= 122) continue
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
static base64(value) {
|
||||
if (!(value is String)) return false
|
||||
if (value.count % 4 != 0) return false
|
||||
var padCount = 0
|
||||
for (i in 0...value.count) {
|
||||
var c = value[i]
|
||||
if (c == "=") {
|
||||
padCount = padCount + 1
|
||||
if (i < value.count - 2) return false
|
||||
} else if (padCount > 0) {
|
||||
return false
|
||||
} else {
|
||||
var code = c.codePoints.toList[0]
|
||||
if (c == "+" || c == "/") continue
|
||||
if (code >= 48 && code <= 57) continue
|
||||
if (code >= 65 && code <= 90) continue
|
||||
if (code >= 97 && code <= 122) continue
|
||||
return false
|
||||
}
|
||||
}
|
||||
return padCount <= 2
|
||||
}
|
||||
|
||||
static json(value) {
|
||||
if (!(value is String)) return false
|
||||
var fiber = Fiber.new {
|
||||
Json.parse(value)
|
||||
}
|
||||
var result = fiber.try()
|
||||
return !fiber.error
|
||||
}
|
||||
|
||||
static regex(value, pattern) {
|
||||
if (!(value is String)) return false
|
||||
var re = Regex.new(pattern)
|
||||
return re.test(value)
|
||||
}
|
||||
|
||||
static url(value) {
|
||||
if (!(value is String)) return false
|
||||
if (!value.startsWith("http://") && !value.startsWith("https://")) return false
|
||||
var rest = value.startsWith("https://") ? value[8..-1] : value[7..-1]
|
||||
if (rest.count == 0) return false
|
||||
return true
|
||||
}
|
||||
|
||||
static uuid(value) {
|
||||
return Uuid.isValid(value)
|
||||
}
|
||||
|
||||
static minLength(value, min) {
|
||||
if (value is String) return value.count >= min
|
||||
if (value is List) return value.count >= min
|
||||
return false
|
||||
}
|
||||
|
||||
static maxLength(value, max) {
|
||||
if (value is String) return value.count <= max
|
||||
if (value is List) return value.count <= max
|
||||
return false
|
||||
}
|
||||
|
||||
static range(value, min, max) {
|
||||
if (!(value is Num)) return false
|
||||
return value >= min && value <= max
|
||||
}
|
||||
|
||||
static positive(value) {
|
||||
if (!(value is Num)) return false
|
||||
return value > 0
|
||||
}
|
||||
|
||||
static negative(value) {
|
||||
if (!(value is Num)) return false
|
||||
return value < 0
|
||||
}
|
||||
|
||||
static integer(value) {
|
||||
if (!(value is Num)) return false
|
||||
return value == value.floor
|
||||
}
|
||||
|
||||
static ipv4(value) {
|
||||
if (!(value is String)) return false
|
||||
var parts = value.split(".")
|
||||
if (parts.count != 4) return false
|
||||
for (part in parts) {
|
||||
if (part.count == 0 || part.count > 3) return false
|
||||
var n = Num.fromString(part)
|
||||
if (n == null || n < 0 || n > 255) return false
|
||||
if (part.count > 1 && part[0] == "0") return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
class Field {
|
||||
static string() { StringField.new() }
|
||||
static string(options) { StringField.new(options) }
|
||||
static integer() { IntegerField.new() }
|
||||
static integer(options) { IntegerField.new(options) }
|
||||
static number() { NumberField.new() }
|
||||
static number(options) { NumberField.new(options) }
|
||||
static email() { EmailField.new() }
|
||||
static boolean() { BooleanField.new() }
|
||||
static list(itemType) { ListField.new(itemType) }
|
||||
static map() { MapField.new() }
|
||||
static optional(fieldDef) { OptionalField.new(fieldDef) }
|
||||
static required(fieldDef) {
|
||||
fieldDef.required = true
|
||||
return fieldDef
|
||||
}
|
||||
}
|
||||
|
||||
class BaseField {
|
||||
construct new() {
|
||||
_required = true
|
||||
_default = null
|
||||
_validators = []
|
||||
}
|
||||
|
||||
construct new(options) {
|
||||
_required = options.containsKey("required") ? options["required"] : true
|
||||
_default = options.containsKey("default") ? options["default"] : null
|
||||
_validators = options.containsKey("validators") ? options["validators"] : []
|
||||
}
|
||||
|
||||
required { _required }
|
||||
required=(value) { _required = value }
|
||||
default { _default }
|
||||
validators { _validators }
|
||||
|
||||
addValidator(fn) {
|
||||
_validators.add(fn)
|
||||
return this
|
||||
}
|
||||
|
||||
validate(value, path) {
|
||||
if (value == null) {
|
||||
if (_required) return ValidationError.new(path, "Field is required")
|
||||
return null
|
||||
}
|
||||
var typeError = validateType_(value, path)
|
||||
if (typeError) return typeError
|
||||
for (validator in _validators) {
|
||||
if (!validator.call(value)) {
|
||||
return ValidationError.new(path, "Custom validation failed")
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
validateType_(value, path) { null }
|
||||
|
||||
coerce(value) { value }
|
||||
}
|
||||
|
||||
class StringField is BaseField {
|
||||
construct new() {
|
||||
super()
|
||||
_minLength = null
|
||||
_maxLength = null
|
||||
_pattern = null
|
||||
}
|
||||
construct new(options) {
|
||||
super(options)
|
||||
_minLength = options.containsKey("minLength") ? options["minLength"] : null
|
||||
_maxLength = options.containsKey("maxLength") ? options["maxLength"] : null
|
||||
_pattern = options.containsKey("pattern") ? options["pattern"] : null
|
||||
}
|
||||
|
||||
validateType_(value, path) {
|
||||
if (!(value is String)) return ValidationError.new(path, "Expected string")
|
||||
if (_minLength && value.count < _minLength) {
|
||||
return ValidationError.new(path, "String too short")
|
||||
}
|
||||
if (_maxLength && value.count > _maxLength) {
|
||||
return ValidationError.new(path, "String too long")
|
||||
}
|
||||
if (_pattern) {
|
||||
var re = Regex.new(_pattern)
|
||||
if (!re.test(value)) {
|
||||
return ValidationError.new(path, "String does not match pattern")
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
minLength { _minLength }
|
||||
maxLength { _maxLength }
|
||||
}
|
||||
|
||||
class IntegerField is BaseField {
|
||||
construct new() {
|
||||
super()
|
||||
_min = null
|
||||
_max = null
|
||||
}
|
||||
construct new(options) {
|
||||
super(options)
|
||||
_min = options.containsKey("min") ? options["min"] : null
|
||||
_max = options.containsKey("max") ? options["max"] : null
|
||||
}
|
||||
|
||||
validateType_(value, path) {
|
||||
if (!(value is Num)) return ValidationError.new(path, "Expected integer")
|
||||
if (value != value.floor) return ValidationError.new(path, "Expected integer, got float")
|
||||
if (_min && value < _min) return ValidationError.new(path, "Value below minimum")
|
||||
if (_max && value > _max) return ValidationError.new(path, "Value above maximum")
|
||||
return null
|
||||
}
|
||||
|
||||
min { _min }
|
||||
max { _max }
|
||||
|
||||
coerce(value) {
|
||||
if (value is String) return Num.fromString(value)
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
class NumberField is BaseField {
|
||||
construct new() {
|
||||
super()
|
||||
_min = null
|
||||
_max = null
|
||||
}
|
||||
construct new(options) {
|
||||
super(options)
|
||||
_min = options.containsKey("min") ? options["min"] : null
|
||||
_max = options.containsKey("max") ? options["max"] : null
|
||||
}
|
||||
|
||||
validateType_(value, path) {
|
||||
if (!(value is Num)) return ValidationError.new(path, "Expected number")
|
||||
if (_min && value < _min) return ValidationError.new(path, "Value below minimum")
|
||||
if (_max && value > _max) return ValidationError.new(path, "Value above maximum")
|
||||
return null
|
||||
}
|
||||
|
||||
min { _min }
|
||||
max { _max }
|
||||
|
||||
coerce(value) {
|
||||
if (value is String) return Num.fromString(value)
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
class EmailField is BaseField {
|
||||
construct new() { super() }
|
||||
|
||||
validateType_(value, path) {
|
||||
if (!(value is String)) return ValidationError.new(path, "Expected string")
|
||||
if (!Validator.email(value)) return ValidationError.new(path, "Invalid email format")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
class BooleanField is BaseField {
|
||||
construct new() { super() }
|
||||
|
||||
validateType_(value, path) {
|
||||
if (!(value is Bool)) return ValidationError.new(path, "Expected boolean")
|
||||
return null
|
||||
}
|
||||
|
||||
coerce(value) {
|
||||
if (value is String) {
|
||||
if (value == "true" || value == "1") return true
|
||||
if (value == "false" || value == "0") return false
|
||||
}
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
class ListField is BaseField {
|
||||
construct new(itemType) {
|
||||
super()
|
||||
_itemType = itemType
|
||||
}
|
||||
|
||||
validateType_(value, path) {
|
||||
if (!(value is List)) return ValidationError.new(path, "Expected list")
|
||||
for (i in 0...value.count) {
|
||||
var itemPath = path + "[" + i.toString + "]"
|
||||
var error = _itemType.validate(value[i], itemPath)
|
||||
if (error) return error
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
itemType { _itemType }
|
||||
}
|
||||
|
||||
class MapField is BaseField {
|
||||
construct new() { super() }
|
||||
|
||||
validateType_(value, path) {
|
||||
if (!(value is Map)) return ValidationError.new(path, "Expected map")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
class OptionalField is BaseField {
|
||||
construct new(fieldDef) {
|
||||
super()
|
||||
_field = fieldDef
|
||||
_required = false
|
||||
}
|
||||
|
||||
validate(value, path) {
|
||||
if (value == null) return null
|
||||
return _field.validate(value, path)
|
||||
}
|
||||
}
|
||||
|
||||
class ValidationError {
|
||||
construct new(path, message) {
|
||||
_path = path
|
||||
_message = message
|
||||
}
|
||||
|
||||
path { _path }
|
||||
message { _message }
|
||||
toString { _path + ": " + _message }
|
||||
}
|
||||
|
||||
class ValidationResult {
|
||||
construct new(isValid, errors, data) {
|
||||
_isValid = isValid
|
||||
_errors = errors
|
||||
_data = data
|
||||
}
|
||||
|
||||
isValid { _isValid }
|
||||
errors { _errors }
|
||||
data { _data }
|
||||
}
|
||||
|
||||
class Schema {
|
||||
construct new(definition) {
|
||||
_definition = definition
|
||||
}
|
||||
|
||||
validate(data) {
|
||||
if (!(data is Map)) {
|
||||
return ValidationResult.new(false, [ValidationError.new("", "Expected object")], null)
|
||||
}
|
||||
|
||||
var errors = []
|
||||
var validData = {}
|
||||
|
||||
for (key in _definition.keys) {
|
||||
var field = _definition[key]
|
||||
var value = data.containsKey(key) ? data[key] : null
|
||||
|
||||
if (value != null) {
|
||||
value = field.coerce(value)
|
||||
}
|
||||
|
||||
var error = field.validate(value, key)
|
||||
if (error) {
|
||||
errors.add(error)
|
||||
} else {
|
||||
validData[key] = value != null ? value : field.default
|
||||
}
|
||||
}
|
||||
|
||||
return ValidationResult.new(errors.count == 0, errors, errors.count == 0 ? validData : null)
|
||||
}
|
||||
|
||||
validateOrAbort(data) {
|
||||
var result = validate(data)
|
||||
if (!result.isValid) {
|
||||
var messages = []
|
||||
for (error in result.errors) {
|
||||
messages.add(error.toString)
|
||||
}
|
||||
Fiber.abort("Validation failed: " + messages.join(", "))
|
||||
}
|
||||
return result.data
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,457 @@
|
||||
// Please do not edit this file. It has been generated automatically
|
||||
// from `src/module/wdantic.wren` using `util/wren_to_c_string.py`
|
||||
|
||||
static const char* wdanticModuleSource =
|
||||
"// retoor <retoor@molodetz.nl>\n"
|
||||
"\n"
|
||||
"import \"regex\" for Regex\n"
|
||||
"import \"base64\" for Base64\n"
|
||||
"import \"json\" for Json\n"
|
||||
"import \"uuid\" for Uuid\n"
|
||||
"\n"
|
||||
"class Validator {\n"
|
||||
" static email(value) {\n"
|
||||
" if (!(value is String)) return false\n"
|
||||
" var at = value.indexOf(\"@\")\n"
|
||||
" if (at < 1) return false\n"
|
||||
" var dot = value.indexOf(\".\", at)\n"
|
||||
" if (dot < at + 2) return false\n"
|
||||
" if (dot >= value.count - 1) return false\n"
|
||||
" for (c in value) {\n"
|
||||
" var code = c.codePoints.toList[0]\n"
|
||||
" if (c == \"@\" || c == \".\" || c == \"_\" || c == \"-\" || c == \"+\") continue\n"
|
||||
" if (code >= 48 && code <= 57) continue\n"
|
||||
" if (code >= 65 && code <= 90) continue\n"
|
||||
" if (code >= 97 && code <= 122) continue\n"
|
||||
" return false\n"
|
||||
" }\n"
|
||||
" return true\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static domain(value) {\n"
|
||||
" if (!(value is String)) return false\n"
|
||||
" if (value.count == 0) return false\n"
|
||||
" if (value[0] == \"-\" || value[-1] == \"-\") return false\n"
|
||||
" var parts = value.split(\".\")\n"
|
||||
" if (parts.count < 2) return false\n"
|
||||
" for (part in parts) {\n"
|
||||
" if (part.count == 0 || part.count > 63) return false\n"
|
||||
" if (part[0] == \"-\" || part[-1] == \"-\") return false\n"
|
||||
" for (c in part) {\n"
|
||||
" var code = c.codePoints.toList[0]\n"
|
||||
" if (c == \"-\") continue\n"
|
||||
" if (code >= 48 && code <= 57) continue\n"
|
||||
" if (code >= 65 && code <= 90) continue\n"
|
||||
" if (code >= 97 && code <= 122) continue\n"
|
||||
" return false\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" return true\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static safeStr(value) {\n"
|
||||
" if (!(value is String)) return false\n"
|
||||
" for (b in value.bytes) {\n"
|
||||
" if (b < 32 || b > 126) return false\n"
|
||||
" }\n"
|
||||
" return true\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static httpEncoded(value) {\n"
|
||||
" if (!(value is String)) return false\n"
|
||||
" for (c in value) {\n"
|
||||
" var code = c.codePoints.toList[0]\n"
|
||||
" if (c == \"_\" || c == \".\" || c == \"~\" || c == \"-\" || c == \"+\") continue\n"
|
||||
" if (c == \"\\%\") continue\n"
|
||||
" if (code >= 48 && code <= 57) continue\n"
|
||||
" if (code >= 65 && code <= 90) continue\n"
|
||||
" if (code >= 97 && code <= 122) continue\n"
|
||||
" return false\n"
|
||||
" }\n"
|
||||
" return true\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static base64(value) {\n"
|
||||
" if (!(value is String)) return false\n"
|
||||
" if (value.count % 4 != 0) return false\n"
|
||||
" var padCount = 0\n"
|
||||
" for (i in 0...value.count) {\n"
|
||||
" var c = value[i]\n"
|
||||
" if (c == \"=\") {\n"
|
||||
" padCount = padCount + 1\n"
|
||||
" if (i < value.count - 2) return false\n"
|
||||
" } else if (padCount > 0) {\n"
|
||||
" return false\n"
|
||||
" } else {\n"
|
||||
" var code = c.codePoints.toList[0]\n"
|
||||
" if (c == \"+\" || c == \"/\") continue\n"
|
||||
" if (code >= 48 && code <= 57) continue\n"
|
||||
" if (code >= 65 && code <= 90) continue\n"
|
||||
" if (code >= 97 && code <= 122) continue\n"
|
||||
" return false\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" return padCount <= 2\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static json(value) {\n"
|
||||
" if (!(value is String)) return false\n"
|
||||
" var fiber = Fiber.new {\n"
|
||||
" Json.parse(value)\n"
|
||||
" }\n"
|
||||
" var result = fiber.try()\n"
|
||||
" return !fiber.error\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static regex(value, pattern) {\n"
|
||||
" if (!(value is String)) return false\n"
|
||||
" var re = Regex.new(pattern)\n"
|
||||
" return re.test(value)\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static url(value) {\n"
|
||||
" if (!(value is String)) return false\n"
|
||||
" if (!value.startsWith(\"http://\") && !value.startsWith(\"https://\")) return false\n"
|
||||
" var rest = value.startsWith(\"https://\") ? value[8..-1] : value[7..-1]\n"
|
||||
" if (rest.count == 0) return false\n"
|
||||
" return true\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static uuid(value) {\n"
|
||||
" return Uuid.isValid(value)\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static minLength(value, min) {\n"
|
||||
" if (value is String) return value.count >= min\n"
|
||||
" if (value is List) return value.count >= min\n"
|
||||
" return false\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static maxLength(value, max) {\n"
|
||||
" if (value is String) return value.count <= max\n"
|
||||
" if (value is List) return value.count <= max\n"
|
||||
" return false\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static range(value, min, max) {\n"
|
||||
" if (!(value is Num)) return false\n"
|
||||
" return value >= min && value <= max\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static positive(value) {\n"
|
||||
" if (!(value is Num)) return false\n"
|
||||
" return value > 0\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static negative(value) {\n"
|
||||
" if (!(value is Num)) return false\n"
|
||||
" return value < 0\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static integer(value) {\n"
|
||||
" if (!(value is Num)) return false\n"
|
||||
" return value == value.floor\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static ipv4(value) {\n"
|
||||
" if (!(value is String)) return false\n"
|
||||
" var parts = value.split(\".\")\n"
|
||||
" if (parts.count != 4) return false\n"
|
||||
" for (part in parts) {\n"
|
||||
" if (part.count == 0 || part.count > 3) return false\n"
|
||||
" var n = Num.fromString(part)\n"
|
||||
" if (n == null || n < 0 || n > 255) return false\n"
|
||||
" if (part.count > 1 && part[0] == \"0\") return false\n"
|
||||
" }\n"
|
||||
" return true\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"class Field {\n"
|
||||
" static string() { StringField.new() }\n"
|
||||
" static string(options) { StringField.new(options) }\n"
|
||||
" static integer() { IntegerField.new() }\n"
|
||||
" static integer(options) { IntegerField.new(options) }\n"
|
||||
" static number() { NumberField.new() }\n"
|
||||
" static number(options) { NumberField.new(options) }\n"
|
||||
" static email() { EmailField.new() }\n"
|
||||
" static boolean() { BooleanField.new() }\n"
|
||||
" static list(itemType) { ListField.new(itemType) }\n"
|
||||
" static map() { MapField.new() }\n"
|
||||
" static optional(fieldDef) { OptionalField.new(fieldDef) }\n"
|
||||
" static required(fieldDef) {\n"
|
||||
" fieldDef.required = true\n"
|
||||
" return fieldDef\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"class BaseField {\n"
|
||||
" construct new() {\n"
|
||||
" _required = true\n"
|
||||
" _default = null\n"
|
||||
" _validators = []\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" construct new(options) {\n"
|
||||
" _required = options.containsKey(\"required\") ? options[\"required\"] : true\n"
|
||||
" _default = options.containsKey(\"default\") ? options[\"default\"] : null\n"
|
||||
" _validators = options.containsKey(\"validators\") ? options[\"validators\"] : []\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" required { _required }\n"
|
||||
" required=(value) { _required = value }\n"
|
||||
" default { _default }\n"
|
||||
" validators { _validators }\n"
|
||||
"\n"
|
||||
" addValidator(fn) {\n"
|
||||
" _validators.add(fn)\n"
|
||||
" return this\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" validate(value, path) {\n"
|
||||
" if (value == null) {\n"
|
||||
" if (_required) return ValidationError.new(path, \"Field is required\")\n"
|
||||
" return null\n"
|
||||
" }\n"
|
||||
" var typeError = validateType_(value, path)\n"
|
||||
" if (typeError) return typeError\n"
|
||||
" for (validator in _validators) {\n"
|
||||
" if (!validator.call(value)) {\n"
|
||||
" return ValidationError.new(path, \"Custom validation failed\")\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" return null\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" validateType_(value, path) { null }\n"
|
||||
"\n"
|
||||
" coerce(value) { value }\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"class StringField is BaseField {\n"
|
||||
" construct new() {\n"
|
||||
" super()\n"
|
||||
" _minLength = null\n"
|
||||
" _maxLength = null\n"
|
||||
" _pattern = null\n"
|
||||
" }\n"
|
||||
" construct new(options) {\n"
|
||||
" super(options)\n"
|
||||
" _minLength = options.containsKey(\"minLength\") ? options[\"minLength\"] : null\n"
|
||||
" _maxLength = options.containsKey(\"maxLength\") ? options[\"maxLength\"] : null\n"
|
||||
" _pattern = options.containsKey(\"pattern\") ? options[\"pattern\"] : null\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" validateType_(value, path) {\n"
|
||||
" if (!(value is String)) return ValidationError.new(path, \"Expected string\")\n"
|
||||
" if (_minLength && value.count < _minLength) {\n"
|
||||
" return ValidationError.new(path, \"String too short\")\n"
|
||||
" }\n"
|
||||
" if (_maxLength && value.count > _maxLength) {\n"
|
||||
" return ValidationError.new(path, \"String too long\")\n"
|
||||
" }\n"
|
||||
" if (_pattern) {\n"
|
||||
" var re = Regex.new(_pattern)\n"
|
||||
" if (!re.test(value)) {\n"
|
||||
" return ValidationError.new(path, \"String does not match pattern\")\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" return null\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" minLength { _minLength }\n"
|
||||
" maxLength { _maxLength }\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"class IntegerField is BaseField {\n"
|
||||
" construct new() {\n"
|
||||
" super()\n"
|
||||
" _min = null\n"
|
||||
" _max = null\n"
|
||||
" }\n"
|
||||
" construct new(options) {\n"
|
||||
" super(options)\n"
|
||||
" _min = options.containsKey(\"min\") ? options[\"min\"] : null\n"
|
||||
" _max = options.containsKey(\"max\") ? options[\"max\"] : null\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" validateType_(value, path) {\n"
|
||||
" if (!(value is Num)) return ValidationError.new(path, \"Expected integer\")\n"
|
||||
" if (value != value.floor) return ValidationError.new(path, \"Expected integer, got float\")\n"
|
||||
" if (_min && value < _min) return ValidationError.new(path, \"Value below minimum\")\n"
|
||||
" if (_max && value > _max) return ValidationError.new(path, \"Value above maximum\")\n"
|
||||
" return null\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" min { _min }\n"
|
||||
" max { _max }\n"
|
||||
"\n"
|
||||
" coerce(value) {\n"
|
||||
" if (value is String) return Num.fromString(value)\n"
|
||||
" return value\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"class NumberField is BaseField {\n"
|
||||
" construct new() {\n"
|
||||
" super()\n"
|
||||
" _min = null\n"
|
||||
" _max = null\n"
|
||||
" }\n"
|
||||
" construct new(options) {\n"
|
||||
" super(options)\n"
|
||||
" _min = options.containsKey(\"min\") ? options[\"min\"] : null\n"
|
||||
" _max = options.containsKey(\"max\") ? options[\"max\"] : null\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" validateType_(value, path) {\n"
|
||||
" if (!(value is Num)) return ValidationError.new(path, \"Expected number\")\n"
|
||||
" if (_min && value < _min) return ValidationError.new(path, \"Value below minimum\")\n"
|
||||
" if (_max && value > _max) return ValidationError.new(path, \"Value above maximum\")\n"
|
||||
" return null\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" min { _min }\n"
|
||||
" max { _max }\n"
|
||||
"\n"
|
||||
" coerce(value) {\n"
|
||||
" if (value is String) return Num.fromString(value)\n"
|
||||
" return value\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"class EmailField is BaseField {\n"
|
||||
" construct new() { super() }\n"
|
||||
"\n"
|
||||
" validateType_(value, path) {\n"
|
||||
" if (!(value is String)) return ValidationError.new(path, \"Expected string\")\n"
|
||||
" if (!Validator.email(value)) return ValidationError.new(path, \"Invalid email format\")\n"
|
||||
" return null\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"class BooleanField is BaseField {\n"
|
||||
" construct new() { super() }\n"
|
||||
"\n"
|
||||
" validateType_(value, path) {\n"
|
||||
" if (!(value is Bool)) return ValidationError.new(path, \"Expected boolean\")\n"
|
||||
" return null\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" coerce(value) {\n"
|
||||
" if (value is String) {\n"
|
||||
" if (value == \"true\" || value == \"1\") return true\n"
|
||||
" if (value == \"false\" || value == \"0\") return false\n"
|
||||
" }\n"
|
||||
" return value\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"class ListField is BaseField {\n"
|
||||
" construct new(itemType) {\n"
|
||||
" super()\n"
|
||||
" _itemType = itemType\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" validateType_(value, path) {\n"
|
||||
" if (!(value is List)) return ValidationError.new(path, \"Expected list\")\n"
|
||||
" for (i in 0...value.count) {\n"
|
||||
" var itemPath = path + \"[\" + i.toString + \"]\"\n"
|
||||
" var error = _itemType.validate(value[i], itemPath)\n"
|
||||
" if (error) return error\n"
|
||||
" }\n"
|
||||
" return null\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" itemType { _itemType }\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"class MapField is BaseField {\n"
|
||||
" construct new() { super() }\n"
|
||||
"\n"
|
||||
" validateType_(value, path) {\n"
|
||||
" if (!(value is Map)) return ValidationError.new(path, \"Expected map\")\n"
|
||||
" return null\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"class OptionalField is BaseField {\n"
|
||||
" construct new(fieldDef) {\n"
|
||||
" super()\n"
|
||||
" _field = fieldDef\n"
|
||||
" _required = false\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" validate(value, path) {\n"
|
||||
" if (value == null) return null\n"
|
||||
" return _field.validate(value, path)\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"class ValidationError {\n"
|
||||
" construct new(path, message) {\n"
|
||||
" _path = path\n"
|
||||
" _message = message\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" path { _path }\n"
|
||||
" message { _message }\n"
|
||||
" toString { _path + \": \" + _message }\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"class ValidationResult {\n"
|
||||
" construct new(isValid, errors, data) {\n"
|
||||
" _isValid = isValid\n"
|
||||
" _errors = errors\n"
|
||||
" _data = data\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" isValid { _isValid }\n"
|
||||
" errors { _errors }\n"
|
||||
" data { _data }\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"class Schema {\n"
|
||||
" construct new(definition) {\n"
|
||||
" _definition = definition\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" validate(data) {\n"
|
||||
" if (!(data is Map)) {\n"
|
||||
" return ValidationResult.new(false, [ValidationError.new(\"\", \"Expected object\")], null)\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" var errors = []\n"
|
||||
" var validData = {}\n"
|
||||
"\n"
|
||||
" for (key in _definition.keys) {\n"
|
||||
" var field = _definition[key]\n"
|
||||
" var value = data.containsKey(key) ? data[key] : null\n"
|
||||
"\n"
|
||||
" if (value != null) {\n"
|
||||
" value = field.coerce(value)\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" var error = field.validate(value, key)\n"
|
||||
" if (error) {\n"
|
||||
" errors.add(error)\n"
|
||||
" } else {\n"
|
||||
" validData[key] = value != null ? value : field.default\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" return ValidationResult.new(errors.count == 0, errors, errors.count == 0 ? validData : null)\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" validateOrAbort(data) {\n"
|
||||
" var result = validate(data)\n"
|
||||
" if (!result.isValid) {\n"
|
||||
" var messages = []\n"
|
||||
" for (error in result.errors) {\n"
|
||||
" messages.add(error.toString)\n"
|
||||
" }\n"
|
||||
" Fiber.abort(\"Validation failed: \" + messages.join(\", \"))\n"
|
||||
" }\n"
|
||||
" return result.data\n"
|
||||
" }\n"
|
||||
"}\n";
|
||||
Vendored
+707
@@ -0,0 +1,707 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
|
||||
import "net" for Socket, Server
|
||||
import "tls" for TlsSocket
|
||||
import "dns" for Dns
|
||||
import "websocket" for WebSocket, WebSocketServer, WebSocketMessage
|
||||
import "json" for Json
|
||||
import "html" for Html
|
||||
import "uuid" for Uuid
|
||||
import "datetime" for DateTime
|
||||
import "io" for File
|
||||
|
||||
class Request {
|
||||
construct new_(method, path, query, headers, body, params, socket) {
|
||||
_method = method
|
||||
_path = path
|
||||
_query = query
|
||||
_headers = headers
|
||||
_body = body
|
||||
_params = params
|
||||
_socket = socket
|
||||
_parsedJson = null
|
||||
_parsedForm = null
|
||||
_cookies = null
|
||||
_session = null
|
||||
}
|
||||
|
||||
method { _method }
|
||||
path { _path }
|
||||
query { _query }
|
||||
headers { _headers }
|
||||
body { _body }
|
||||
params { _params }
|
||||
socket { _socket }
|
||||
|
||||
header(name) {
|
||||
var lower = toLower_(name)
|
||||
for (key in _headers.keys) {
|
||||
if (toLower_(key) == lower) return _headers[key]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
json {
|
||||
if (_parsedJson == null && _body.count > 0) {
|
||||
_parsedJson = Json.parse(_body)
|
||||
}
|
||||
return _parsedJson
|
||||
}
|
||||
|
||||
form {
|
||||
if (_parsedForm == null && _body.count > 0) {
|
||||
_parsedForm = Html.decodeParams(_body)
|
||||
}
|
||||
return _parsedForm
|
||||
}
|
||||
|
||||
cookies {
|
||||
if (_cookies == null) {
|
||||
_cookies = {}
|
||||
var cookieHeader = header("Cookie")
|
||||
if (cookieHeader != null) {
|
||||
var pairs = cookieHeader.split("; ")
|
||||
for (pair in pairs) {
|
||||
var eq = pair.indexOf("=")
|
||||
if (eq > 0) {
|
||||
var name = pair[0...eq].trim()
|
||||
var value = pair[eq + 1..-1].trim()
|
||||
_cookies[name] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return _cookies
|
||||
}
|
||||
|
||||
session { _session }
|
||||
session=(s) { _session = s }
|
||||
|
||||
toLower_(str) {
|
||||
var result = ""
|
||||
for (c in str) {
|
||||
var cp = c.codePoints.toList[0]
|
||||
if (cp >= 65 && cp <= 90) {
|
||||
result = result + String.fromCodePoint(cp + 32)
|
||||
} else {
|
||||
result = result + c
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
class Response {
|
||||
construct new() {
|
||||
_status = 200
|
||||
_statusText = "OK"
|
||||
_headers = {}
|
||||
_body = ""
|
||||
_cookies = []
|
||||
}
|
||||
|
||||
status { _status }
|
||||
status=(s) {
|
||||
_status = s
|
||||
_statusText = statusText_(s)
|
||||
}
|
||||
|
||||
headers { _headers }
|
||||
body { _body }
|
||||
body=(b) { _body = b }
|
||||
|
||||
header(name, value) {
|
||||
_headers[name] = value
|
||||
return this
|
||||
}
|
||||
|
||||
cookie(name, value) { cookie(name, value, {}) }
|
||||
|
||||
cookie(name, value, options) {
|
||||
var cookie = name + "=" + value
|
||||
if (options.containsKey("path")) cookie = cookie + "; Path=" + options["path"]
|
||||
if (options.containsKey("maxAge")) cookie = cookie + "; Max-Age=" + options["maxAge"].toString
|
||||
if (options.containsKey("httpOnly") && options["httpOnly"]) cookie = cookie + "; HttpOnly"
|
||||
if (options.containsKey("secure") && options["secure"]) cookie = cookie + "; Secure"
|
||||
_cookies.add(cookie)
|
||||
return this
|
||||
}
|
||||
|
||||
static text(content) {
|
||||
var r = Response.new()
|
||||
r.header("Content-Type", "text/plain; charset=utf-8")
|
||||
r.body = content
|
||||
return r
|
||||
}
|
||||
|
||||
static html(content) {
|
||||
var r = Response.new()
|
||||
r.header("Content-Type", "text/html; charset=utf-8")
|
||||
r.body = content
|
||||
return r
|
||||
}
|
||||
|
||||
static json(data) {
|
||||
var r = Response.new()
|
||||
r.header("Content-Type", "application/json; charset=utf-8")
|
||||
r.body = Json.stringify(data)
|
||||
return r
|
||||
}
|
||||
|
||||
static redirect(url) { redirect(url, 302) }
|
||||
|
||||
static redirect(url, status) {
|
||||
var r = Response.new()
|
||||
r.status = status
|
||||
r.header("Location", url)
|
||||
return r
|
||||
}
|
||||
|
||||
static file(path) { file(path, null) }
|
||||
|
||||
static file(path, contentType) {
|
||||
if (!File.exists(path)) {
|
||||
var r = Response.new()
|
||||
r.status = 404
|
||||
r.body = "Not Found"
|
||||
return r
|
||||
}
|
||||
var r = Response.new()
|
||||
var ct = contentType
|
||||
if (ct == null) ct = guessContentType_(path)
|
||||
r.header("Content-Type", ct)
|
||||
r.body = File.read(path)
|
||||
return r
|
||||
}
|
||||
|
||||
static guessContentType_(path) {
|
||||
if (path.endsWith(".html") || path.endsWith(".htm")) return "text/html; charset=utf-8"
|
||||
if (path.endsWith(".css")) return "text/css; charset=utf-8"
|
||||
if (path.endsWith(".js")) return "application/javascript; charset=utf-8"
|
||||
if (path.endsWith(".json")) return "application/json; charset=utf-8"
|
||||
if (path.endsWith(".png")) return "image/png"
|
||||
if (path.endsWith(".jpg") || path.endsWith(".jpeg")) return "image/jpeg"
|
||||
if (path.endsWith(".gif")) return "image/gif"
|
||||
if (path.endsWith(".svg")) return "image/svg+xml"
|
||||
if (path.endsWith(".ico")) return "image/x-icon"
|
||||
if (path.endsWith(".txt")) return "text/plain; charset=utf-8"
|
||||
if (path.endsWith(".xml")) return "application/xml; charset=utf-8"
|
||||
if (path.endsWith(".pdf")) return "application/pdf"
|
||||
if (path.endsWith(".woff")) return "font/woff"
|
||||
if (path.endsWith(".woff2")) return "font/woff2"
|
||||
if (path.endsWith(".ttf")) return "font/ttf"
|
||||
return "application/octet-stream"
|
||||
}
|
||||
|
||||
statusText_(code) {
|
||||
if (code == 200) return "OK"
|
||||
if (code == 201) return "Created"
|
||||
if (code == 204) return "No Content"
|
||||
if (code == 301) return "Moved Permanently"
|
||||
if (code == 302) return "Found"
|
||||
if (code == 304) return "Not Modified"
|
||||
if (code == 400) return "Bad Request"
|
||||
if (code == 401) return "Unauthorized"
|
||||
if (code == 403) return "Forbidden"
|
||||
if (code == 404) return "Not Found"
|
||||
if (code == 405) return "Method Not Allowed"
|
||||
if (code == 500) return "Internal Server Error"
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
build() {
|
||||
var response = "HTTP/1.1 %(_status) %(_statusText)\r\n"
|
||||
_headers["Content-Length"] = _body.bytes.count.toString
|
||||
for (name in _headers.keys) {
|
||||
response = response + name + ": " + _headers[name] + "\r\n"
|
||||
}
|
||||
for (cookie in _cookies) {
|
||||
response = response + "Set-Cookie: " + cookie + "\r\n"
|
||||
}
|
||||
response = response + "\r\n" + _body
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
class Session {
|
||||
construct new_(id, data) {
|
||||
_id = id
|
||||
_data = data
|
||||
_modified = false
|
||||
}
|
||||
|
||||
id { _id }
|
||||
|
||||
[key] { _data.containsKey(key) ? _data[key] : null }
|
||||
|
||||
[key]=(value) {
|
||||
_data[key] = value
|
||||
_modified = true
|
||||
}
|
||||
|
||||
remove(key) {
|
||||
if (_data.containsKey(key)) {
|
||||
_data.remove(key)
|
||||
_modified = true
|
||||
}
|
||||
}
|
||||
|
||||
data { _data }
|
||||
isModified { _modified }
|
||||
}
|
||||
|
||||
class SessionStore {
|
||||
construct new() {
|
||||
_sessions = {}
|
||||
}
|
||||
|
||||
get(id) {
|
||||
if (_sessions.containsKey(id)) {
|
||||
return _sessions[id]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
create() {
|
||||
var id = Uuid.v4()
|
||||
_sessions[id] = {}
|
||||
return Session.new_(id, _sessions[id])
|
||||
}
|
||||
|
||||
save(session) {
|
||||
_sessions[session.id] = session.data
|
||||
}
|
||||
|
||||
destroy(id) {
|
||||
if (_sessions.containsKey(id)) {
|
||||
_sessions.remove(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Router {
|
||||
construct new() {
|
||||
_routes = []
|
||||
}
|
||||
|
||||
addRoute(method, path, handler) {
|
||||
_routes.add({"method": method, "path": path, "handler": handler, "pattern": compilePattern_(path)})
|
||||
}
|
||||
|
||||
get(path, handler) { addRoute("GET", path, handler) }
|
||||
post(path, handler) { addRoute("POST", path, handler) }
|
||||
put(path, handler) { addRoute("PUT", path, handler) }
|
||||
delete(path, handler) { addRoute("DELETE", path, handler) }
|
||||
patch(path, handler) { addRoute("PATCH", path, handler) }
|
||||
|
||||
match(method, path) {
|
||||
for (route in _routes) {
|
||||
if (route["method"] != method && route["method"] != "*") continue
|
||||
var match = matchPattern_(route["pattern"], path)
|
||||
if (match != null) {
|
||||
return {"handler": route["handler"], "params": match}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
compilePattern_(path) {
|
||||
var parts = path.split("/")
|
||||
var pattern = []
|
||||
for (part in parts) {
|
||||
if (part.count == 0) continue
|
||||
if (part.startsWith(":")) {
|
||||
pattern.add({"type": "param", "name": part[1..-1]})
|
||||
} else if (part == "*") {
|
||||
pattern.add({"type": "wildcard"})
|
||||
} else {
|
||||
pattern.add({"type": "literal", "value": part})
|
||||
}
|
||||
}
|
||||
return pattern
|
||||
}
|
||||
|
||||
matchPattern_(pattern, path) {
|
||||
var parts = path.split("/").where { |p| p.count > 0 }.toList
|
||||
if (parts.count != pattern.count) {
|
||||
var hasWildcard = pattern.any { |p| p["type"] == "wildcard" }
|
||||
if (!hasWildcard) return null
|
||||
}
|
||||
|
||||
var params = {}
|
||||
var pi = 0
|
||||
for (i in 0...pattern.count) {
|
||||
var p = pattern[i]
|
||||
if (p["type"] == "wildcard") {
|
||||
break
|
||||
}
|
||||
if (pi >= parts.count) return null
|
||||
if (p["type"] == "literal") {
|
||||
if (parts[pi] != p["value"]) return null
|
||||
} else if (p["type"] == "param") {
|
||||
params[p["name"]] = parts[pi]
|
||||
}
|
||||
pi = pi + 1
|
||||
}
|
||||
return params
|
||||
}
|
||||
}
|
||||
|
||||
class View {
|
||||
get(request) { methodNotAllowed_() }
|
||||
post(request) { methodNotAllowed_() }
|
||||
put(request) { methodNotAllowed_() }
|
||||
delete(request) { methodNotAllowed_() }
|
||||
patch(request) { methodNotAllowed_() }
|
||||
|
||||
methodNotAllowed_() {
|
||||
var r = Response.new()
|
||||
r.status = 405
|
||||
r.body = "Method Not Allowed"
|
||||
return r
|
||||
}
|
||||
|
||||
dispatch(request) {
|
||||
if (request.method == "GET") return get(request)
|
||||
if (request.method == "POST") return post(request)
|
||||
if (request.method == "PUT") return put(request)
|
||||
if (request.method == "DELETE") return delete(request)
|
||||
if (request.method == "PATCH") return patch(request)
|
||||
return methodNotAllowed_()
|
||||
}
|
||||
}
|
||||
|
||||
class Application {
|
||||
construct new() {
|
||||
_router = Router.new()
|
||||
_staticPrefixes = []
|
||||
_sessionStore = SessionStore.new()
|
||||
_sessionCookieName = "session_id"
|
||||
_middleware = []
|
||||
_views = {}
|
||||
_wsHandlers = {}
|
||||
}
|
||||
|
||||
router { _router }
|
||||
sessionStore { _sessionStore }
|
||||
|
||||
addRoute(method, path, handler) {
|
||||
_router.addRoute(method, path, handler)
|
||||
return this
|
||||
}
|
||||
|
||||
get(path, handler) { addRoute("GET", path, handler) }
|
||||
post(path, handler) { addRoute("POST", path, handler) }
|
||||
put(path, handler) { addRoute("PUT", path, handler) }
|
||||
delete(path, handler) { addRoute("DELETE", path, handler) }
|
||||
patch(path, handler) { addRoute("PATCH", path, handler) }
|
||||
|
||||
addView(path, viewClass) {
|
||||
_views[path] = viewClass
|
||||
_router.addRoute("*", path, Fn.new { |req| handleView_(viewClass, req) })
|
||||
return this
|
||||
}
|
||||
|
||||
handleView_(viewClass, request) {
|
||||
var view = viewClass.new()
|
||||
return view.dispatch(request)
|
||||
}
|
||||
|
||||
static_(prefix, directory) {
|
||||
_staticPrefixes.add({"prefix": prefix, "directory": directory})
|
||||
return this
|
||||
}
|
||||
|
||||
websocket(path, handler) {
|
||||
_wsHandlers[path] = handler
|
||||
return this
|
||||
}
|
||||
|
||||
use(middleware) {
|
||||
_middleware.add(middleware)
|
||||
return this
|
||||
}
|
||||
|
||||
run(host, port) {
|
||||
var server = Server.bind(host, port)
|
||||
System.print("Server running on http://%(host):%(port)")
|
||||
|
||||
while (true) {
|
||||
var socket = server.accept()
|
||||
if (socket == null) continue
|
||||
handleConnection_(socket)
|
||||
}
|
||||
}
|
||||
|
||||
handleConnection_(socket) {
|
||||
var requestData = ""
|
||||
var contentLength = 0
|
||||
var headersComplete = false
|
||||
var body = ""
|
||||
|
||||
while (true) {
|
||||
var chunk = socket.read()
|
||||
if (chunk == null || chunk.count == 0) {
|
||||
socket.close()
|
||||
return
|
||||
}
|
||||
requestData = requestData + chunk
|
||||
|
||||
if (!headersComplete && requestData.contains("\r\n\r\n")) {
|
||||
headersComplete = true
|
||||
var headerEnd = requestData.indexOf("\r\n\r\n")
|
||||
var headerPart = requestData[0...headerEnd]
|
||||
body = requestData[headerEnd + 4..-1]
|
||||
|
||||
var headers = parseHeaders_(headerPart)
|
||||
if (headers.containsKey("Content-Length")) {
|
||||
contentLength = Num.fromString(headers["Content-Length"])
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (headersComplete && body.bytes.count >= contentLength) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var headerEnd = requestData.indexOf("\r\n\r\n")
|
||||
var headerPart = requestData[0...headerEnd]
|
||||
body = requestData[headerEnd + 4..-1]
|
||||
if (contentLength > 0 && body.bytes.count > contentLength) {
|
||||
body = body[0...contentLength]
|
||||
}
|
||||
|
||||
var lines = headerPart.split("\r\n")
|
||||
if (lines.count == 0) {
|
||||
socket.close()
|
||||
return
|
||||
}
|
||||
|
||||
var requestLine = lines[0].split(" ")
|
||||
if (requestLine.count < 2) {
|
||||
socket.close()
|
||||
return
|
||||
}
|
||||
|
||||
var method = requestLine[0]
|
||||
var fullPath = requestLine[1]
|
||||
var path = fullPath
|
||||
var query = {}
|
||||
|
||||
var queryStart = fullPath.indexOf("?")
|
||||
if (queryStart >= 0) {
|
||||
path = fullPath[0...queryStart]
|
||||
query = Html.decodeParams(fullPath[queryStart + 1..-1])
|
||||
}
|
||||
|
||||
var headers = {}
|
||||
for (i in 1...lines.count) {
|
||||
var colonPos = lines[i].indexOf(":")
|
||||
if (colonPos > 0) {
|
||||
var name = lines[i][0...colonPos].trim()
|
||||
var value = lines[i][colonPos + 1..-1].trim()
|
||||
headers[name] = value
|
||||
}
|
||||
}
|
||||
|
||||
var request = Request.new_(method, path, query, headers, body, {}, socket)
|
||||
loadSession_(request)
|
||||
|
||||
var response = null
|
||||
|
||||
for (sp in _staticPrefixes) {
|
||||
if (path.startsWith(sp["prefix"])) {
|
||||
var filePath = sp["directory"] + path[sp["prefix"].count..-1]
|
||||
response = Response.file(filePath)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (response == null) {
|
||||
var route = _router.match(method, path)
|
||||
if (route != null) {
|
||||
request = Request.new_(method, path, query, headers, body, route["params"], socket)
|
||||
loadSession_(request)
|
||||
for (mw in _middleware) {
|
||||
var result = mw.call(request)
|
||||
if (result != null) {
|
||||
response = result
|
||||
break
|
||||
}
|
||||
}
|
||||
if (response == null) {
|
||||
response = route["handler"].call(request)
|
||||
}
|
||||
} else {
|
||||
response = Response.new()
|
||||
response.status = 404
|
||||
response.body = "Not Found"
|
||||
}
|
||||
}
|
||||
|
||||
saveSession_(request, response)
|
||||
socket.write(response.build())
|
||||
socket.close()
|
||||
}
|
||||
|
||||
loadSession_(request) {
|
||||
var sessionId = request.cookies[_sessionCookieName]
|
||||
if (sessionId != null) {
|
||||
var data = _sessionStore.get(sessionId)
|
||||
if (data != null) {
|
||||
request.session = Session.new_(sessionId, data)
|
||||
return
|
||||
}
|
||||
}
|
||||
request.session = _sessionStore.create()
|
||||
}
|
||||
|
||||
saveSession_(request, response) {
|
||||
if (request.session != null) {
|
||||
_sessionStore.save(request.session)
|
||||
response.cookie(_sessionCookieName, request.session.id, {"path": "/", "httpOnly": true})
|
||||
}
|
||||
}
|
||||
|
||||
parseHeaders_(headerPart) {
|
||||
var headers = {}
|
||||
var lines = headerPart.split("\r\n")
|
||||
for (i in 1...lines.count) {
|
||||
var colonPos = lines[i].indexOf(":")
|
||||
if (colonPos > 0) {
|
||||
var name = lines[i][0...colonPos].trim()
|
||||
var value = lines[i][colonPos + 1..-1].trim()
|
||||
headers[name] = value
|
||||
}
|
||||
}
|
||||
return headers
|
||||
}
|
||||
}
|
||||
|
||||
class Client {
|
||||
static get(url) { get(url, {}) }
|
||||
|
||||
static get(url, options) {
|
||||
return request_("GET", url, options)
|
||||
}
|
||||
|
||||
static post(url) { post(url, {}) }
|
||||
|
||||
static post(url, options) {
|
||||
return request_("POST", url, options)
|
||||
}
|
||||
|
||||
static put(url, options) {
|
||||
return request_("PUT", url, options)
|
||||
}
|
||||
|
||||
static delete(url, options) {
|
||||
return request_("DELETE", url, options)
|
||||
}
|
||||
|
||||
static request_(method, url, options) {
|
||||
var parsed = parseUrl_(url)
|
||||
var host = parsed["host"]
|
||||
var port = parsed["port"]
|
||||
var path = parsed["path"]
|
||||
var isSecure = parsed["scheme"] == "https"
|
||||
|
||||
var addresses = Dns.lookup(host, 4)
|
||||
if (addresses.count == 0) {
|
||||
Fiber.abort("Could not resolve host: " + host)
|
||||
}
|
||||
|
||||
var socket
|
||||
if (isSecure) {
|
||||
socket = TlsSocket.connect(addresses[0], port, host)
|
||||
} else {
|
||||
socket = Socket.connect(addresses[0], port)
|
||||
}
|
||||
|
||||
var body = options.containsKey("body") ? options["body"] : ""
|
||||
if (options.containsKey("json")) {
|
||||
body = Json.stringify(options["json"])
|
||||
if (!options.containsKey("headers")) options["headers"] = {}
|
||||
options["headers"]["Content-Type"] = "application/json"
|
||||
}
|
||||
|
||||
var request = method + " " + path + " HTTP/1.1\r\n"
|
||||
request = request + "Host: " + host + "\r\n"
|
||||
request = request + "Connection: close\r\n"
|
||||
|
||||
if (options.containsKey("headers")) {
|
||||
for (entry in options["headers"]) {
|
||||
request = request + entry.key + ": " + entry.value + "\r\n"
|
||||
}
|
||||
}
|
||||
|
||||
if (body.count > 0) {
|
||||
request = request + "Content-Length: " + body.bytes.count.toString + "\r\n"
|
||||
}
|
||||
request = request + "\r\n" + body
|
||||
|
||||
socket.write(request)
|
||||
|
||||
var response = ""
|
||||
while (true) {
|
||||
var chunk = socket.read()
|
||||
if (chunk == null || chunk.count == 0) break
|
||||
response = response + chunk
|
||||
}
|
||||
socket.close()
|
||||
|
||||
return parseResponse_(response)
|
||||
}
|
||||
|
||||
static parseUrl_(url) {
|
||||
var result = {"scheme": "http", "host": "", "port": 80, "path": "/"}
|
||||
var rest = url
|
||||
|
||||
var schemeEnd = rest.indexOf("://")
|
||||
if (schemeEnd >= 0) {
|
||||
result["scheme"] = rest[0...schemeEnd]
|
||||
rest = rest[schemeEnd + 3..-1]
|
||||
if (result["scheme"] == "https") result["port"] = 443
|
||||
}
|
||||
|
||||
var pathStart = rest.indexOf("/")
|
||||
var hostPart = pathStart >= 0 ? rest[0...pathStart] : rest
|
||||
result["path"] = pathStart >= 0 ? rest[pathStart..-1] : "/"
|
||||
|
||||
var portStart = hostPart.indexOf(":")
|
||||
if (portStart >= 0) {
|
||||
result["host"] = hostPart[0...portStart]
|
||||
result["port"] = Num.fromString(hostPart[portStart + 1..-1])
|
||||
} else {
|
||||
result["host"] = hostPart
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
static parseResponse_(response) {
|
||||
var headerEnd = response.indexOf("\r\n\r\n")
|
||||
if (headerEnd < 0) return {"status": 0, "headers": {}, "body": response}
|
||||
|
||||
var headerPart = response[0...headerEnd]
|
||||
var body = response[headerEnd + 4..-1]
|
||||
|
||||
var lines = headerPart.split("\r\n")
|
||||
var statusLine = lines[0].split(" ")
|
||||
var status = statusLine.count > 1 ? Num.fromString(statusLine[1]) : 0
|
||||
|
||||
var headers = {}
|
||||
for (i in 1...lines.count) {
|
||||
var colonPos = lines[i].indexOf(":")
|
||||
if (colonPos > 0) {
|
||||
var name = lines[i][0...colonPos].trim()
|
||||
var value = lines[i][colonPos + 1..-1].trim()
|
||||
headers[name] = value
|
||||
}
|
||||
}
|
||||
|
||||
return {"status": status, "headers": headers, "body": body}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,711 @@
|
||||
// Please do not edit this file. It has been generated automatically
|
||||
// from `src/module/web.wren` using `util/wren_to_c_string.py`
|
||||
|
||||
static const char* webModuleSource =
|
||||
"// retoor <retoor@molodetz.nl>\n"
|
||||
"\n"
|
||||
"import \"net\" for Socket, Server\n"
|
||||
"import \"tls\" for TlsSocket\n"
|
||||
"import \"dns\" for Dns\n"
|
||||
"import \"websocket\" for WebSocket, WebSocketServer, WebSocketMessage\n"
|
||||
"import \"json\" for Json\n"
|
||||
"import \"html\" for Html\n"
|
||||
"import \"uuid\" for Uuid\n"
|
||||
"import \"datetime\" for DateTime\n"
|
||||
"import \"io\" for File\n"
|
||||
"\n"
|
||||
"class Request {\n"
|
||||
" construct new_(method, path, query, headers, body, params, socket) {\n"
|
||||
" _method = method\n"
|
||||
" _path = path\n"
|
||||
" _query = query\n"
|
||||
" _headers = headers\n"
|
||||
" _body = body\n"
|
||||
" _params = params\n"
|
||||
" _socket = socket\n"
|
||||
" _parsedJson = null\n"
|
||||
" _parsedForm = null\n"
|
||||
" _cookies = null\n"
|
||||
" _session = null\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" method { _method }\n"
|
||||
" path { _path }\n"
|
||||
" query { _query }\n"
|
||||
" headers { _headers }\n"
|
||||
" body { _body }\n"
|
||||
" params { _params }\n"
|
||||
" socket { _socket }\n"
|
||||
"\n"
|
||||
" header(name) {\n"
|
||||
" var lower = toLower_(name)\n"
|
||||
" for (key in _headers.keys) {\n"
|
||||
" if (toLower_(key) == lower) return _headers[key]\n"
|
||||
" }\n"
|
||||
" return null\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" json {\n"
|
||||
" if (_parsedJson == null && _body.count > 0) {\n"
|
||||
" _parsedJson = Json.parse(_body)\n"
|
||||
" }\n"
|
||||
" return _parsedJson\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" form {\n"
|
||||
" if (_parsedForm == null && _body.count > 0) {\n"
|
||||
" _parsedForm = Html.decodeParams(_body)\n"
|
||||
" }\n"
|
||||
" return _parsedForm\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" cookies {\n"
|
||||
" if (_cookies == null) {\n"
|
||||
" _cookies = {}\n"
|
||||
" var cookieHeader = header(\"Cookie\")\n"
|
||||
" if (cookieHeader != null) {\n"
|
||||
" var pairs = cookieHeader.split(\"; \")\n"
|
||||
" for (pair in pairs) {\n"
|
||||
" var eq = pair.indexOf(\"=\")\n"
|
||||
" if (eq > 0) {\n"
|
||||
" var name = pair[0...eq].trim()\n"
|
||||
" var value = pair[eq + 1..-1].trim()\n"
|
||||
" _cookies[name] = value\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" return _cookies\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" session { _session }\n"
|
||||
" session=(s) { _session = s }\n"
|
||||
"\n"
|
||||
" toLower_(str) {\n"
|
||||
" var result = \"\"\n"
|
||||
" for (c in str) {\n"
|
||||
" var cp = c.codePoints.toList[0]\n"
|
||||
" if (cp >= 65 && cp <= 90) {\n"
|
||||
" result = result + String.fromCodePoint(cp + 32)\n"
|
||||
" } else {\n"
|
||||
" result = result + c\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" return result\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"class Response {\n"
|
||||
" construct new() {\n"
|
||||
" _status = 200\n"
|
||||
" _statusText = \"OK\"\n"
|
||||
" _headers = {}\n"
|
||||
" _body = \"\"\n"
|
||||
" _cookies = []\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" status { _status }\n"
|
||||
" status=(s) {\n"
|
||||
" _status = s\n"
|
||||
" _statusText = statusText_(s)\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" headers { _headers }\n"
|
||||
" body { _body }\n"
|
||||
" body=(b) { _body = b }\n"
|
||||
"\n"
|
||||
" header(name, value) {\n"
|
||||
" _headers[name] = value\n"
|
||||
" return this\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" cookie(name, value) { cookie(name, value, {}) }\n"
|
||||
"\n"
|
||||
" cookie(name, value, options) {\n"
|
||||
" var cookie = name + \"=\" + value\n"
|
||||
" if (options.containsKey(\"path\")) cookie = cookie + \"; Path=\" + options[\"path\"]\n"
|
||||
" if (options.containsKey(\"maxAge\")) cookie = cookie + \"; Max-Age=\" + options[\"maxAge\"].toString\n"
|
||||
" if (options.containsKey(\"httpOnly\") && options[\"httpOnly\"]) cookie = cookie + \"; HttpOnly\"\n"
|
||||
" if (options.containsKey(\"secure\") && options[\"secure\"]) cookie = cookie + \"; Secure\"\n"
|
||||
" _cookies.add(cookie)\n"
|
||||
" return this\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static text(content) {\n"
|
||||
" var r = Response.new()\n"
|
||||
" r.header(\"Content-Type\", \"text/plain; charset=utf-8\")\n"
|
||||
" r.body = content\n"
|
||||
" return r\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static html(content) {\n"
|
||||
" var r = Response.new()\n"
|
||||
" r.header(\"Content-Type\", \"text/html; charset=utf-8\")\n"
|
||||
" r.body = content\n"
|
||||
" return r\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static json(data) {\n"
|
||||
" var r = Response.new()\n"
|
||||
" r.header(\"Content-Type\", \"application/json; charset=utf-8\")\n"
|
||||
" r.body = Json.stringify(data)\n"
|
||||
" return r\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static redirect(url) { redirect(url, 302) }\n"
|
||||
"\n"
|
||||
" static redirect(url, status) {\n"
|
||||
" var r = Response.new()\n"
|
||||
" r.status = status\n"
|
||||
" r.header(\"Location\", url)\n"
|
||||
" return r\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static file(path) { file(path, null) }\n"
|
||||
"\n"
|
||||
" static file(path, contentType) {\n"
|
||||
" if (!File.exists(path)) {\n"
|
||||
" var r = Response.new()\n"
|
||||
" r.status = 404\n"
|
||||
" r.body = \"Not Found\"\n"
|
||||
" return r\n"
|
||||
" }\n"
|
||||
" var r = Response.new()\n"
|
||||
" var ct = contentType\n"
|
||||
" if (ct == null) ct = guessContentType_(path)\n"
|
||||
" r.header(\"Content-Type\", ct)\n"
|
||||
" r.body = File.read(path)\n"
|
||||
" return r\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static guessContentType_(path) {\n"
|
||||
" if (path.endsWith(\".html\") || path.endsWith(\".htm\")) return \"text/html; charset=utf-8\"\n"
|
||||
" if (path.endsWith(\".css\")) return \"text/css; charset=utf-8\"\n"
|
||||
" if (path.endsWith(\".js\")) return \"application/javascript; charset=utf-8\"\n"
|
||||
" if (path.endsWith(\".json\")) return \"application/json; charset=utf-8\"\n"
|
||||
" if (path.endsWith(\".png\")) return \"image/png\"\n"
|
||||
" if (path.endsWith(\".jpg\") || path.endsWith(\".jpeg\")) return \"image/jpeg\"\n"
|
||||
" if (path.endsWith(\".gif\")) return \"image/gif\"\n"
|
||||
" if (path.endsWith(\".svg\")) return \"image/svg+xml\"\n"
|
||||
" if (path.endsWith(\".ico\")) return \"image/x-icon\"\n"
|
||||
" if (path.endsWith(\".txt\")) return \"text/plain; charset=utf-8\"\n"
|
||||
" if (path.endsWith(\".xml\")) return \"application/xml; charset=utf-8\"\n"
|
||||
" if (path.endsWith(\".pdf\")) return \"application/pdf\"\n"
|
||||
" if (path.endsWith(\".woff\")) return \"font/woff\"\n"
|
||||
" if (path.endsWith(\".woff2\")) return \"font/woff2\"\n"
|
||||
" if (path.endsWith(\".ttf\")) return \"font/ttf\"\n"
|
||||
" return \"application/octet-stream\"\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" statusText_(code) {\n"
|
||||
" if (code == 200) return \"OK\"\n"
|
||||
" if (code == 201) return \"Created\"\n"
|
||||
" if (code == 204) return \"No Content\"\n"
|
||||
" if (code == 301) return \"Moved Permanently\"\n"
|
||||
" if (code == 302) return \"Found\"\n"
|
||||
" if (code == 304) return \"Not Modified\"\n"
|
||||
" if (code == 400) return \"Bad Request\"\n"
|
||||
" if (code == 401) return \"Unauthorized\"\n"
|
||||
" if (code == 403) return \"Forbidden\"\n"
|
||||
" if (code == 404) return \"Not Found\"\n"
|
||||
" if (code == 405) return \"Method Not Allowed\"\n"
|
||||
" if (code == 500) return \"Internal Server Error\"\n"
|
||||
" return \"Unknown\"\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" build() {\n"
|
||||
" var response = \"HTTP/1.1 %(_status) %(_statusText)\\r\\n\"\n"
|
||||
" _headers[\"Content-Length\"] = _body.bytes.count.toString\n"
|
||||
" for (name in _headers.keys) {\n"
|
||||
" response = response + name + \": \" + _headers[name] + \"\\r\\n\"\n"
|
||||
" }\n"
|
||||
" for (cookie in _cookies) {\n"
|
||||
" response = response + \"Set-Cookie: \" + cookie + \"\\r\\n\"\n"
|
||||
" }\n"
|
||||
" response = response + \"\\r\\n\" + _body\n"
|
||||
" return response\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"class Session {\n"
|
||||
" construct new_(id, data) {\n"
|
||||
" _id = id\n"
|
||||
" _data = data\n"
|
||||
" _modified = false\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" id { _id }\n"
|
||||
"\n"
|
||||
" [key] { _data.containsKey(key) ? _data[key] : null }\n"
|
||||
"\n"
|
||||
" [key]=(value) {\n"
|
||||
" _data[key] = value\n"
|
||||
" _modified = true\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" remove(key) {\n"
|
||||
" if (_data.containsKey(key)) {\n"
|
||||
" _data.remove(key)\n"
|
||||
" _modified = true\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" data { _data }\n"
|
||||
" isModified { _modified }\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"class SessionStore {\n"
|
||||
" construct new() {\n"
|
||||
" _sessions = {}\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" get(id) {\n"
|
||||
" if (_sessions.containsKey(id)) {\n"
|
||||
" return _sessions[id]\n"
|
||||
" }\n"
|
||||
" return null\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" create() {\n"
|
||||
" var id = Uuid.v4()\n"
|
||||
" _sessions[id] = {}\n"
|
||||
" return Session.new_(id, _sessions[id])\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" save(session) {\n"
|
||||
" _sessions[session.id] = session.data\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" destroy(id) {\n"
|
||||
" if (_sessions.containsKey(id)) {\n"
|
||||
" _sessions.remove(id)\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"class Router {\n"
|
||||
" construct new() {\n"
|
||||
" _routes = []\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" addRoute(method, path, handler) {\n"
|
||||
" _routes.add({\"method\": method, \"path\": path, \"handler\": handler, \"pattern\": compilePattern_(path)})\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" get(path, handler) { addRoute(\"GET\", path, handler) }\n"
|
||||
" post(path, handler) { addRoute(\"POST\", path, handler) }\n"
|
||||
" put(path, handler) { addRoute(\"PUT\", path, handler) }\n"
|
||||
" delete(path, handler) { addRoute(\"DELETE\", path, handler) }\n"
|
||||
" patch(path, handler) { addRoute(\"PATCH\", path, handler) }\n"
|
||||
"\n"
|
||||
" match(method, path) {\n"
|
||||
" for (route in _routes) {\n"
|
||||
" if (route[\"method\"] != method && route[\"method\"] != \"*\") continue\n"
|
||||
" var match = matchPattern_(route[\"pattern\"], path)\n"
|
||||
" if (match != null) {\n"
|
||||
" return {\"handler\": route[\"handler\"], \"params\": match}\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" return null\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" compilePattern_(path) {\n"
|
||||
" var parts = path.split(\"/\")\n"
|
||||
" var pattern = []\n"
|
||||
" for (part in parts) {\n"
|
||||
" if (part.count == 0) continue\n"
|
||||
" if (part.startsWith(\":\")) {\n"
|
||||
" pattern.add({\"type\": \"param\", \"name\": part[1..-1]})\n"
|
||||
" } else if (part == \"*\") {\n"
|
||||
" pattern.add({\"type\": \"wildcard\"})\n"
|
||||
" } else {\n"
|
||||
" pattern.add({\"type\": \"literal\", \"value\": part})\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" return pattern\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" matchPattern_(pattern, path) {\n"
|
||||
" var parts = path.split(\"/\").where { |p| p.count > 0 }.toList\n"
|
||||
" if (parts.count != pattern.count) {\n"
|
||||
" var hasWildcard = pattern.any { |p| p[\"type\"] == \"wildcard\" }\n"
|
||||
" if (!hasWildcard) return null\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" var params = {}\n"
|
||||
" var pi = 0\n"
|
||||
" for (i in 0...pattern.count) {\n"
|
||||
" var p = pattern[i]\n"
|
||||
" if (p[\"type\"] == \"wildcard\") {\n"
|
||||
" break\n"
|
||||
" }\n"
|
||||
" if (pi >= parts.count) return null\n"
|
||||
" if (p[\"type\"] == \"literal\") {\n"
|
||||
" if (parts[pi] != p[\"value\"]) return null\n"
|
||||
" } else if (p[\"type\"] == \"param\") {\n"
|
||||
" params[p[\"name\"]] = parts[pi]\n"
|
||||
" }\n"
|
||||
" pi = pi + 1\n"
|
||||
" }\n"
|
||||
" return params\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"class View {\n"
|
||||
" get(request) { methodNotAllowed_() }\n"
|
||||
" post(request) { methodNotAllowed_() }\n"
|
||||
" put(request) { methodNotAllowed_() }\n"
|
||||
" delete(request) { methodNotAllowed_() }\n"
|
||||
" patch(request) { methodNotAllowed_() }\n"
|
||||
"\n"
|
||||
" methodNotAllowed_() {\n"
|
||||
" var r = Response.new()\n"
|
||||
" r.status = 405\n"
|
||||
" r.body = \"Method Not Allowed\"\n"
|
||||
" return r\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" dispatch(request) {\n"
|
||||
" if (request.method == \"GET\") return get(request)\n"
|
||||
" if (request.method == \"POST\") return post(request)\n"
|
||||
" if (request.method == \"PUT\") return put(request)\n"
|
||||
" if (request.method == \"DELETE\") return delete(request)\n"
|
||||
" if (request.method == \"PATCH\") return patch(request)\n"
|
||||
" return methodNotAllowed_()\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"class Application {\n"
|
||||
" construct new() {\n"
|
||||
" _router = Router.new()\n"
|
||||
" _staticPrefixes = []\n"
|
||||
" _sessionStore = SessionStore.new()\n"
|
||||
" _sessionCookieName = \"session_id\"\n"
|
||||
" _middleware = []\n"
|
||||
" _views = {}\n"
|
||||
" _wsHandlers = {}\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" router { _router }\n"
|
||||
" sessionStore { _sessionStore }\n"
|
||||
"\n"
|
||||
" addRoute(method, path, handler) {\n"
|
||||
" _router.addRoute(method, path, handler)\n"
|
||||
" return this\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" get(path, handler) { addRoute(\"GET\", path, handler) }\n"
|
||||
" post(path, handler) { addRoute(\"POST\", path, handler) }\n"
|
||||
" put(path, handler) { addRoute(\"PUT\", path, handler) }\n"
|
||||
" delete(path, handler) { addRoute(\"DELETE\", path, handler) }\n"
|
||||
" patch(path, handler) { addRoute(\"PATCH\", path, handler) }\n"
|
||||
"\n"
|
||||
" addView(path, viewClass) {\n"
|
||||
" _views[path] = viewClass\n"
|
||||
" _router.addRoute(\"*\", path, Fn.new { |req| handleView_(viewClass, req) })\n"
|
||||
" return this\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" handleView_(viewClass, request) {\n"
|
||||
" var view = viewClass.new()\n"
|
||||
" return view.dispatch(request)\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static_(prefix, directory) {\n"
|
||||
" _staticPrefixes.add({\"prefix\": prefix, \"directory\": directory})\n"
|
||||
" return this\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" websocket(path, handler) {\n"
|
||||
" _wsHandlers[path] = handler\n"
|
||||
" return this\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" use(middleware) {\n"
|
||||
" _middleware.add(middleware)\n"
|
||||
" return this\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" run(host, port) {\n"
|
||||
" var server = Server.bind(host, port)\n"
|
||||
" System.print(\"Server running on http://%(host):%(port)\")\n"
|
||||
"\n"
|
||||
" while (true) {\n"
|
||||
" var socket = server.accept()\n"
|
||||
" if (socket == null) continue\n"
|
||||
" handleConnection_(socket)\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" handleConnection_(socket) {\n"
|
||||
" var requestData = \"\"\n"
|
||||
" var contentLength = 0\n"
|
||||
" var headersComplete = false\n"
|
||||
" var body = \"\"\n"
|
||||
"\n"
|
||||
" while (true) {\n"
|
||||
" var chunk = socket.read()\n"
|
||||
" if (chunk == null || chunk.count == 0) {\n"
|
||||
" socket.close()\n"
|
||||
" return\n"
|
||||
" }\n"
|
||||
" requestData = requestData + chunk\n"
|
||||
"\n"
|
||||
" if (!headersComplete && requestData.contains(\"\\r\\n\\r\\n\")) {\n"
|
||||
" headersComplete = true\n"
|
||||
" var headerEnd = requestData.indexOf(\"\\r\\n\\r\\n\")\n"
|
||||
" var headerPart = requestData[0...headerEnd]\n"
|
||||
" body = requestData[headerEnd + 4..-1]\n"
|
||||
"\n"
|
||||
" var headers = parseHeaders_(headerPart)\n"
|
||||
" if (headers.containsKey(\"Content-Length\")) {\n"
|
||||
" contentLength = Num.fromString(headers[\"Content-Length\"])\n"
|
||||
" } else {\n"
|
||||
" break\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" if (headersComplete && body.bytes.count >= contentLength) {\n"
|
||||
" break\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" var headerEnd = requestData.indexOf(\"\\r\\n\\r\\n\")\n"
|
||||
" var headerPart = requestData[0...headerEnd]\n"
|
||||
" body = requestData[headerEnd + 4..-1]\n"
|
||||
" if (contentLength > 0 && body.bytes.count > contentLength) {\n"
|
||||
" body = body[0...contentLength]\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" var lines = headerPart.split(\"\\r\\n\")\n"
|
||||
" if (lines.count == 0) {\n"
|
||||
" socket.close()\n"
|
||||
" return\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" var requestLine = lines[0].split(\" \")\n"
|
||||
" if (requestLine.count < 2) {\n"
|
||||
" socket.close()\n"
|
||||
" return\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" var method = requestLine[0]\n"
|
||||
" var fullPath = requestLine[1]\n"
|
||||
" var path = fullPath\n"
|
||||
" var query = {}\n"
|
||||
"\n"
|
||||
" var queryStart = fullPath.indexOf(\"?\")\n"
|
||||
" if (queryStart >= 0) {\n"
|
||||
" path = fullPath[0...queryStart]\n"
|
||||
" query = Html.decodeParams(fullPath[queryStart + 1..-1])\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" var headers = {}\n"
|
||||
" for (i in 1...lines.count) {\n"
|
||||
" var colonPos = lines[i].indexOf(\":\")\n"
|
||||
" if (colonPos > 0) {\n"
|
||||
" var name = lines[i][0...colonPos].trim()\n"
|
||||
" var value = lines[i][colonPos + 1..-1].trim()\n"
|
||||
" headers[name] = value\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" var request = Request.new_(method, path, query, headers, body, {}, socket)\n"
|
||||
" loadSession_(request)\n"
|
||||
"\n"
|
||||
" var response = null\n"
|
||||
"\n"
|
||||
" for (sp in _staticPrefixes) {\n"
|
||||
" if (path.startsWith(sp[\"prefix\"])) {\n"
|
||||
" var filePath = sp[\"directory\"] + path[sp[\"prefix\"].count..-1]\n"
|
||||
" response = Response.file(filePath)\n"
|
||||
" break\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" if (response == null) {\n"
|
||||
" var route = _router.match(method, path)\n"
|
||||
" if (route != null) {\n"
|
||||
" request = Request.new_(method, path, query, headers, body, route[\"params\"], socket)\n"
|
||||
" loadSession_(request)\n"
|
||||
" for (mw in _middleware) {\n"
|
||||
" var result = mw.call(request)\n"
|
||||
" if (result != null) {\n"
|
||||
" response = result\n"
|
||||
" break\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" if (response == null) {\n"
|
||||
" response = route[\"handler\"].call(request)\n"
|
||||
" }\n"
|
||||
" } else {\n"
|
||||
" response = Response.new()\n"
|
||||
" response.status = 404\n"
|
||||
" response.body = \"Not Found\"\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" saveSession_(request, response)\n"
|
||||
" socket.write(response.build())\n"
|
||||
" socket.close()\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" loadSession_(request) {\n"
|
||||
" var sessionId = request.cookies[_sessionCookieName]\n"
|
||||
" if (sessionId != null) {\n"
|
||||
" var data = _sessionStore.get(sessionId)\n"
|
||||
" if (data != null) {\n"
|
||||
" request.session = Session.new_(sessionId, data)\n"
|
||||
" return\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" request.session = _sessionStore.create()\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" saveSession_(request, response) {\n"
|
||||
" if (request.session != null) {\n"
|
||||
" _sessionStore.save(request.session)\n"
|
||||
" response.cookie(_sessionCookieName, request.session.id, {\"path\": \"/\", \"httpOnly\": true})\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" parseHeaders_(headerPart) {\n"
|
||||
" var headers = {}\n"
|
||||
" var lines = headerPart.split(\"\\r\\n\")\n"
|
||||
" for (i in 1...lines.count) {\n"
|
||||
" var colonPos = lines[i].indexOf(\":\")\n"
|
||||
" if (colonPos > 0) {\n"
|
||||
" var name = lines[i][0...colonPos].trim()\n"
|
||||
" var value = lines[i][colonPos + 1..-1].trim()\n"
|
||||
" headers[name] = value\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" return headers\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"class Client {\n"
|
||||
" static get(url) { get(url, {}) }\n"
|
||||
"\n"
|
||||
" static get(url, options) {\n"
|
||||
" return request_(\"GET\", url, options)\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static post(url) { post(url, {}) }\n"
|
||||
"\n"
|
||||
" static post(url, options) {\n"
|
||||
" return request_(\"POST\", url, options)\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static put(url, options) {\n"
|
||||
" return request_(\"PUT\", url, options)\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static delete(url, options) {\n"
|
||||
" return request_(\"DELETE\", url, options)\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static request_(method, url, options) {\n"
|
||||
" var parsed = parseUrl_(url)\n"
|
||||
" var host = parsed[\"host\"]\n"
|
||||
" var port = parsed[\"port\"]\n"
|
||||
" var path = parsed[\"path\"]\n"
|
||||
" var isSecure = parsed[\"scheme\"] == \"https\"\n"
|
||||
"\n"
|
||||
" var addresses = Dns.lookup(host, 4)\n"
|
||||
" if (addresses.count == 0) {\n"
|
||||
" Fiber.abort(\"Could not resolve host: \" + host)\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" var socket\n"
|
||||
" if (isSecure) {\n"
|
||||
" socket = TlsSocket.connect(addresses[0], port, host)\n"
|
||||
" } else {\n"
|
||||
" socket = Socket.connect(addresses[0], port)\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" var body = options.containsKey(\"body\") ? options[\"body\"] : \"\"\n"
|
||||
" if (options.containsKey(\"json\")) {\n"
|
||||
" body = Json.stringify(options[\"json\"])\n"
|
||||
" if (!options.containsKey(\"headers\")) options[\"headers\"] = {}\n"
|
||||
" options[\"headers\"][\"Content-Type\"] = \"application/json\"\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" var request = method + \" \" + path + \" HTTP/1.1\\r\\n\"\n"
|
||||
" request = request + \"Host: \" + host + \"\\r\\n\"\n"
|
||||
" request = request + \"Connection: close\\r\\n\"\n"
|
||||
"\n"
|
||||
" if (options.containsKey(\"headers\")) {\n"
|
||||
" for (entry in options[\"headers\"]) {\n"
|
||||
" request = request + entry.key + \": \" + entry.value + \"\\r\\n\"\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" if (body.count > 0) {\n"
|
||||
" request = request + \"Content-Length: \" + body.bytes.count.toString + \"\\r\\n\"\n"
|
||||
" }\n"
|
||||
" request = request + \"\\r\\n\" + body\n"
|
||||
"\n"
|
||||
" socket.write(request)\n"
|
||||
"\n"
|
||||
" var response = \"\"\n"
|
||||
" while (true) {\n"
|
||||
" var chunk = socket.read()\n"
|
||||
" if (chunk == null || chunk.count == 0) break\n"
|
||||
" response = response + chunk\n"
|
||||
" }\n"
|
||||
" socket.close()\n"
|
||||
"\n"
|
||||
" return parseResponse_(response)\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static parseUrl_(url) {\n"
|
||||
" var result = {\"scheme\": \"http\", \"host\": \"\", \"port\": 80, \"path\": \"/\"}\n"
|
||||
" var rest = url\n"
|
||||
"\n"
|
||||
" var schemeEnd = rest.indexOf(\"://\")\n"
|
||||
" if (schemeEnd >= 0) {\n"
|
||||
" result[\"scheme\"] = rest[0...schemeEnd]\n"
|
||||
" rest = rest[schemeEnd + 3..-1]\n"
|
||||
" if (result[\"scheme\"] == \"https\") result[\"port\"] = 443\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" var pathStart = rest.indexOf(\"/\")\n"
|
||||
" var hostPart = pathStart >= 0 ? rest[0...pathStart] : rest\n"
|
||||
" result[\"path\"] = pathStart >= 0 ? rest[pathStart..-1] : \"/\"\n"
|
||||
"\n"
|
||||
" var portStart = hostPart.indexOf(\":\")\n"
|
||||
" if (portStart >= 0) {\n"
|
||||
" result[\"host\"] = hostPart[0...portStart]\n"
|
||||
" result[\"port\"] = Num.fromString(hostPart[portStart + 1..-1])\n"
|
||||
" } else {\n"
|
||||
" result[\"host\"] = hostPart\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" return result\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static parseResponse_(response) {\n"
|
||||
" var headerEnd = response.indexOf(\"\\r\\n\\r\\n\")\n"
|
||||
" if (headerEnd < 0) return {\"status\": 0, \"headers\": {}, \"body\": response}\n"
|
||||
"\n"
|
||||
" var headerPart = response[0...headerEnd]\n"
|
||||
" var body = response[headerEnd + 4..-1]\n"
|
||||
"\n"
|
||||
" var lines = headerPart.split(\"\\r\\n\")\n"
|
||||
" var statusLine = lines[0].split(\" \")\n"
|
||||
" var status = statusLine.count > 1 ? Num.fromString(statusLine[1]) : 0\n"
|
||||
"\n"
|
||||
" var headers = {}\n"
|
||||
" for (i in 1...lines.count) {\n"
|
||||
" var colonPos = lines[i].indexOf(\":\")\n"
|
||||
" if (colonPos > 0) {\n"
|
||||
" var name = lines[i][0...colonPos].trim()\n"
|
||||
" var value = lines[i][colonPos + 1..-1].trim()\n"
|
||||
" headers[name] = value\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" return {\"status\": status, \"headers\": headers, \"body\": body}\n"
|
||||
" }\n"
|
||||
"}\n";
|
||||
Vendored
+37
-47
@@ -5,6 +5,7 @@ import "tls" for TlsSocket
|
||||
import "dns" for Dns
|
||||
import "crypto" for Crypto, Hash
|
||||
import "base64" for Base64
|
||||
import "bytes" for Bytes
|
||||
|
||||
class WebSocketMessage {
|
||||
construct new_(opcode, payload, fin) {
|
||||
@@ -43,11 +44,11 @@ class WebSocketMessage {
|
||||
}
|
||||
|
||||
static bytesToString_(bytes) {
|
||||
var str = ""
|
||||
var parts = []
|
||||
for (b in bytes) {
|
||||
str = str + String.fromCodePoint(b)
|
||||
parts.add(String.fromCodePoint(b))
|
||||
}
|
||||
return str
|
||||
return parts.join("")
|
||||
}
|
||||
|
||||
bytesToString_(bytes) { WebSocketMessage.bytesToString_(bytes) }
|
||||
@@ -61,7 +62,7 @@ class WebSocket {
|
||||
_isOpen = true
|
||||
_fragmentBuffer = []
|
||||
_fragmentOpcode = null
|
||||
_readBuffer = []
|
||||
_readBuffer = ""
|
||||
}
|
||||
|
||||
static connect(url) { connect(url, {}) }
|
||||
@@ -226,11 +227,11 @@ class WebSocket {
|
||||
}
|
||||
|
||||
static bytesToString_(bytes) {
|
||||
var str = ""
|
||||
var parts = []
|
||||
for (b in bytes) {
|
||||
str = str + String.fromByte(b)
|
||||
parts.add(String.fromByte(b))
|
||||
}
|
||||
return str
|
||||
return parts.join("")
|
||||
}
|
||||
|
||||
url { _url }
|
||||
@@ -349,11 +350,7 @@ class WebSocket {
|
||||
|
||||
sendFrame_(opcode, payload) {
|
||||
var frame = encodeFrame_(opcode, payload, _isClient)
|
||||
var data = ""
|
||||
for (b in frame) {
|
||||
data = data + String.fromByte(b)
|
||||
}
|
||||
_socket.write(data)
|
||||
_socket.write(Bytes.fromList(frame))
|
||||
}
|
||||
|
||||
encodeFrame_(opcode, payload, masked) {
|
||||
@@ -382,9 +379,11 @@ class WebSocket {
|
||||
if (masked) {
|
||||
var mask = Crypto.randomBytes(4)
|
||||
for (b in mask) frame.add(b)
|
||||
for (i in 0...len) {
|
||||
frame.add(payload[i] ^ mask[i % 4])
|
||||
}
|
||||
var payloadBytes = Bytes.fromList(payload)
|
||||
var maskBytes = Bytes.fromList(mask)
|
||||
var maskedPayload = Bytes.xorMask(payloadBytes, maskBytes)
|
||||
var maskedList = Bytes.toList(maskedPayload)
|
||||
for (b in maskedList) frame.add(b)
|
||||
} else {
|
||||
for (b in payload) frame.add(b)
|
||||
}
|
||||
@@ -394,70 +393,61 @@ class WebSocket {
|
||||
|
||||
readFrame_() {
|
||||
var header = readBytes_(2)
|
||||
if (header == null || header.count < 2) return null
|
||||
if (header == null || Bytes.length(header) < 2) return null
|
||||
|
||||
var fin = (header[0] & 0x80) != 0
|
||||
var opcode = header[0] & 0x0F
|
||||
var masked = (header[1] & 0x80) != 0
|
||||
var len = header[1] & 0x7F
|
||||
var headerList = Bytes.toList(header)
|
||||
var fin = (headerList[0] & 0x80) != 0
|
||||
var opcode = headerList[0] & 0x0F
|
||||
var masked = (headerList[1] & 0x80) != 0
|
||||
var len = headerList[1] & 0x7F
|
||||
|
||||
if (len == 126) {
|
||||
var ext = readBytes_(2)
|
||||
if (ext == null || ext.count < 2) return null
|
||||
len = (ext[0] << 8) | ext[1]
|
||||
if (ext == null || Bytes.length(ext) < 2) return null
|
||||
var extList = Bytes.toList(ext)
|
||||
len = (extList[0] << 8) | extList[1]
|
||||
} else if (len == 127) {
|
||||
var ext = readBytes_(8)
|
||||
if (ext == null || ext.count < 8) return null
|
||||
if (ext == null || Bytes.length(ext) < 8) return null
|
||||
var extList = Bytes.toList(ext)
|
||||
len = 0
|
||||
for (i in 4...8) {
|
||||
len = (len << 8) | ext[i]
|
||||
len = (len << 8) | extList[i]
|
||||
}
|
||||
}
|
||||
|
||||
var mask = null
|
||||
if (masked) {
|
||||
mask = readBytes_(4)
|
||||
if (mask == null || mask.count < 4) return null
|
||||
if (mask == null || Bytes.length(mask) < 4) return null
|
||||
}
|
||||
|
||||
var payload = []
|
||||
var payload = ""
|
||||
if (len > 0) {
|
||||
payload = readBytes_(len)
|
||||
if (payload == null) return null
|
||||
}
|
||||
|
||||
if (masked && mask != null) {
|
||||
for (i in 0...payload.count) {
|
||||
payload[i] = payload[i] ^ mask[i % 4]
|
||||
}
|
||||
payload = Bytes.xorMask(payload, mask)
|
||||
}
|
||||
|
||||
return WebSocketMessage.new_(opcode, payload, fin)
|
||||
return WebSocketMessage.new_(opcode, Bytes.toList(payload), fin)
|
||||
}
|
||||
|
||||
readBytes_(count) {
|
||||
while (_readBuffer.count < count) {
|
||||
while (Bytes.length(_readBuffer) < count) {
|
||||
var chunk = _socket.read()
|
||||
if (chunk == null || chunk.count == 0) {
|
||||
if (_readBuffer.count == 0) return null
|
||||
var result = []
|
||||
for (b in _readBuffer) result.add(b)
|
||||
_readBuffer = []
|
||||
if (Bytes.length(_readBuffer) == 0) return null
|
||||
var result = _readBuffer
|
||||
_readBuffer = ""
|
||||
return result
|
||||
}
|
||||
for (b in chunk.bytes) {
|
||||
_readBuffer.add(b)
|
||||
}
|
||||
_readBuffer = Bytes.concat(_readBuffer, chunk)
|
||||
}
|
||||
var result = []
|
||||
for (i in 0...count) {
|
||||
result.add(_readBuffer[i])
|
||||
}
|
||||
var remaining = []
|
||||
for (i in count..._readBuffer.count) {
|
||||
remaining.add(_readBuffer[i])
|
||||
}
|
||||
_readBuffer = remaining
|
||||
var result = Bytes.slice(_readBuffer, 0, count)
|
||||
_readBuffer = Bytes.slice(_readBuffer, count, Bytes.length(_readBuffer))
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ static const char* websocketModuleSource =
|
||||
"import \"dns\" for Dns\n"
|
||||
"import \"crypto\" for Crypto, Hash\n"
|
||||
"import \"base64\" for Base64\n"
|
||||
"import \"bytes\" for Bytes\n"
|
||||
"\n"
|
||||
"class WebSocketMessage {\n"
|
||||
" construct new_(opcode, payload, fin) {\n"
|
||||
@@ -47,11 +48,11 @@ static const char* websocketModuleSource =
|
||||
" }\n"
|
||||
"\n"
|
||||
" static bytesToString_(bytes) {\n"
|
||||
" var str = \"\"\n"
|
||||
" var parts = []\n"
|
||||
" for (b in bytes) {\n"
|
||||
" str = str + String.fromCodePoint(b)\n"
|
||||
" parts.add(String.fromCodePoint(b))\n"
|
||||
" }\n"
|
||||
" return str\n"
|
||||
" return parts.join(\"\")\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" bytesToString_(bytes) { WebSocketMessage.bytesToString_(bytes) }\n"
|
||||
@@ -65,7 +66,7 @@ static const char* websocketModuleSource =
|
||||
" _isOpen = true\n"
|
||||
" _fragmentBuffer = []\n"
|
||||
" _fragmentOpcode = null\n"
|
||||
" _readBuffer = []\n"
|
||||
" _readBuffer = \"\"\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static connect(url) { connect(url, {}) }\n"
|
||||
@@ -230,11 +231,11 @@ static const char* websocketModuleSource =
|
||||
" }\n"
|
||||
"\n"
|
||||
" static bytesToString_(bytes) {\n"
|
||||
" var str = \"\"\n"
|
||||
" var parts = []\n"
|
||||
" for (b in bytes) {\n"
|
||||
" str = str + String.fromByte(b)\n"
|
||||
" parts.add(String.fromByte(b))\n"
|
||||
" }\n"
|
||||
" return str\n"
|
||||
" return parts.join(\"\")\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" url { _url }\n"
|
||||
@@ -353,11 +354,7 @@ static const char* websocketModuleSource =
|
||||
"\n"
|
||||
" sendFrame_(opcode, payload) {\n"
|
||||
" var frame = encodeFrame_(opcode, payload, _isClient)\n"
|
||||
" var data = \"\"\n"
|
||||
" for (b in frame) {\n"
|
||||
" data = data + String.fromByte(b)\n"
|
||||
" }\n"
|
||||
" _socket.write(data)\n"
|
||||
" _socket.write(Bytes.fromList(frame))\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" encodeFrame_(opcode, payload, masked) {\n"
|
||||
@@ -386,9 +383,11 @@ static const char* websocketModuleSource =
|
||||
" if (masked) {\n"
|
||||
" var mask = Crypto.randomBytes(4)\n"
|
||||
" for (b in mask) frame.add(b)\n"
|
||||
" for (i in 0...len) {\n"
|
||||
" frame.add(payload[i] ^ mask[i % 4])\n"
|
||||
" }\n"
|
||||
" var payloadBytes = Bytes.fromList(payload)\n"
|
||||
" var maskBytes = Bytes.fromList(mask)\n"
|
||||
" var maskedPayload = Bytes.xorMask(payloadBytes, maskBytes)\n"
|
||||
" var maskedList = Bytes.toList(maskedPayload)\n"
|
||||
" for (b in maskedList) frame.add(b)\n"
|
||||
" } else {\n"
|
||||
" for (b in payload) frame.add(b)\n"
|
||||
" }\n"
|
||||
@@ -398,70 +397,61 @@ static const char* websocketModuleSource =
|
||||
"\n"
|
||||
" readFrame_() {\n"
|
||||
" var header = readBytes_(2)\n"
|
||||
" if (header == null || header.count < 2) return null\n"
|
||||
" if (header == null || Bytes.length(header) < 2) return null\n"
|
||||
"\n"
|
||||
" var fin = (header[0] & 0x80) != 0\n"
|
||||
" var opcode = header[0] & 0x0F\n"
|
||||
" var masked = (header[1] & 0x80) != 0\n"
|
||||
" var len = header[1] & 0x7F\n"
|
||||
" var headerList = Bytes.toList(header)\n"
|
||||
" var fin = (headerList[0] & 0x80) != 0\n"
|
||||
" var opcode = headerList[0] & 0x0F\n"
|
||||
" var masked = (headerList[1] & 0x80) != 0\n"
|
||||
" var len = headerList[1] & 0x7F\n"
|
||||
"\n"
|
||||
" if (len == 126) {\n"
|
||||
" var ext = readBytes_(2)\n"
|
||||
" if (ext == null || ext.count < 2) return null\n"
|
||||
" len = (ext[0] << 8) | ext[1]\n"
|
||||
" if (ext == null || Bytes.length(ext) < 2) return null\n"
|
||||
" var extList = Bytes.toList(ext)\n"
|
||||
" len = (extList[0] << 8) | extList[1]\n"
|
||||
" } else if (len == 127) {\n"
|
||||
" var ext = readBytes_(8)\n"
|
||||
" if (ext == null || ext.count < 8) return null\n"
|
||||
" if (ext == null || Bytes.length(ext) < 8) return null\n"
|
||||
" var extList = Bytes.toList(ext)\n"
|
||||
" len = 0\n"
|
||||
" for (i in 4...8) {\n"
|
||||
" len = (len << 8) | ext[i]\n"
|
||||
" len = (len << 8) | extList[i]\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" var mask = null\n"
|
||||
" if (masked) {\n"
|
||||
" mask = readBytes_(4)\n"
|
||||
" if (mask == null || mask.count < 4) return null\n"
|
||||
" if (mask == null || Bytes.length(mask) < 4) return null\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" var payload = []\n"
|
||||
" var payload = \"\"\n"
|
||||
" if (len > 0) {\n"
|
||||
" payload = readBytes_(len)\n"
|
||||
" if (payload == null) return null\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" if (masked && mask != null) {\n"
|
||||
" for (i in 0...payload.count) {\n"
|
||||
" payload[i] = payload[i] ^ mask[i % 4]\n"
|
||||
" }\n"
|
||||
" payload = Bytes.xorMask(payload, mask)\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" return WebSocketMessage.new_(opcode, payload, fin)\n"
|
||||
" return WebSocketMessage.new_(opcode, Bytes.toList(payload), fin)\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" readBytes_(count) {\n"
|
||||
" while (_readBuffer.count < count) {\n"
|
||||
" while (Bytes.length(_readBuffer) < count) {\n"
|
||||
" var chunk = _socket.read()\n"
|
||||
" if (chunk == null || chunk.count == 0) {\n"
|
||||
" if (_readBuffer.count == 0) return null\n"
|
||||
" var result = []\n"
|
||||
" for (b in _readBuffer) result.add(b)\n"
|
||||
" _readBuffer = []\n"
|
||||
" if (Bytes.length(_readBuffer) == 0) return null\n"
|
||||
" var result = _readBuffer\n"
|
||||
" _readBuffer = \"\"\n"
|
||||
" return result\n"
|
||||
" }\n"
|
||||
" for (b in chunk.bytes) {\n"
|
||||
" _readBuffer.add(b)\n"
|
||||
" }\n"
|
||||
" _readBuffer = Bytes.concat(_readBuffer, chunk)\n"
|
||||
" }\n"
|
||||
" var result = []\n"
|
||||
" for (i in 0...count) {\n"
|
||||
" result.add(_readBuffer[i])\n"
|
||||
" }\n"
|
||||
" var remaining = []\n"
|
||||
" for (i in count..._readBuffer.count) {\n"
|
||||
" remaining.add(_readBuffer[i])\n"
|
||||
" }\n"
|
||||
" _readBuffer = remaining\n"
|
||||
" var result = Bytes.slice(_readBuffer, 0, count)\n"
|
||||
" _readBuffer = Bytes.slice(_readBuffer, count, Bytes.length(_readBuffer))\n"
|
||||
" return result\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
|
||||
Reference in New Issue
Block a user