commit 5674c1e95c46789f28bff767ae1675e24ad07959 Author: Jonathan Bernard Date: Sat Jul 26 19:30:54 2025 -0500 Initial version. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6045317 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.*.sw? +/short_url diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e69de29 diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000..ee6a6d9 --- /dev/null +++ b/mise.toml @@ -0,0 +1,2 @@ +[tools] +nim = "latest" diff --git a/short-url.cfg.example.json b/short-url.cfg.example.json new file mode 100644 index 0000000..b68979c --- /dev/null +++ b/short-url.cfg.example.json @@ -0,0 +1,7 @@ +{ + "mappings": { + "/hn": "https://news.ycombinator.com", + "/([^/]+)/latest": "https://app.abbey.com/discussions/$1?order=timestamp,desc" + }, + "port": "8081" +} diff --git a/short_url.nimble b/short_url.nimble new file mode 100644 index 0000000..77180d9 --- /dev/null +++ b/short_url.nimble @@ -0,0 +1,17 @@ +# Package + +version = "1.0.0" +author = "Jonathan Bernard" +description = "URL shortener/expander for Probatem at https://pbtm.us" +license = "GPL3" +srcDir = "src" +bin = @["short_url"] + + +# Dependencies + +requires "nim >= 2.2.4" +requires @["cliutils", "mummy", "namespaced_logging >= 2.1.2", "webby"] + +task updateVersion, "Update the version of this package.": + exec "update_version interactive src/short_url.nim" diff --git a/src/short_url.nim b/src/short_url.nim new file mode 100644 index 0000000..8f99a13 --- /dev/null +++ b/src/short_url.nim @@ -0,0 +1,100 @@ +import std/[json, nre, sequtils, strutils] +import cliutils, docopt, mummy, namespaced_logging +import webby/httpheaders + +const USAGE = """ +Usage: + short_url serve [options] + +Options: + + -c, --config Use the given config file (defaults to ./short-url.cfg.json) + --debug Enable debug-level logging. + --trace Enable trace-level logging (takes precedence over --debug). +""" + +const VERSION = "1.0.0" +const CT_TXT = "text/plain" + +type + ShortUrlsConfig = object + mappings: seq[tuple[src, dst: string]] + port: int + cfg: CombinedConfig + + Context = object + mappings: seq[tuple[src:string, regex: Regex, tmpl: string]] + logger: Logger + +proc loadConfig(args: Table[string, Value]): ShortUrlsConfig = + let cfgFile = + if args["--config"]: $args["--config"] + else: findConfigFile("short-url.cfg.json") + + result.cfg = initCombinedConfig(filename = cfgFile, docopt = args) + result.port = parseInt(result.cfg.getVal("port", "80")) + + result.mappings = @[] + for (k, v) in pairs(result.cfg.getJson("mappings")): + result.mappings.add((k, v.getStr)) + +proc initMappings(m: seq[tuple[src, dst: string]]): + seq[tuple[src: string, regex: Regex, tmpl: string]] = + result = m.mapIt((it.src, re(it.src), it.dst)) + +proc makeHandler(mappings: seq[tuple[src,dst: string]], logSvc: LogService): RequestHandler = + let ctx = Context( + mappings: initMappings(mappings), + logger: threadLocalRef(logSvc).getLogger("pbm/short_url/response")) + + return proc(req: Request) {.gcsafe.} = + + if req.path == "/internal/health": + ctx.logger.info("Successful health check") + req.respond(200, @[("Content-Type", CT_TXT)], "healthy and happy") + return + + for (src, regex, tmpl) in ctx.mappings: + if match(req.uri, regex).isSome: + ctx.logger.debug(%*{ + "msg": "found match", + "matching_pattern": %src, + "uri": %req.uri, + "tmpl": %tmpl }) + + req.respond(302, + toWebby(@[("Location", replace(req.uri, regex, tmpl))]), + "") + return + + ctx.logger.debug(%*{ + "msg": "no match found", + "uri": %req.uri, + "patterns": %mapIt(mappings, it.src) }) + + req.respond(404, @[("Content-Type", CT_TXT)], "not found") + +when isMainModule: + var logSvc = initLogService() + try: + + let llsvc = threadLocalRef(logSvc) + llsvc.addAppender(initConsoleLogAppender(formatter = formatJsonStructuredLog)) + llsvc.setRootThreshold(lvlInfo) + let logger = llsvc.getLogger("pbm/short_url") + + let args = docopt(USAGE, version = VERSION) + let cfg = loadConfig(args) + + if args["--trace"]: logSvc.setRootThreshold(lvlAll) + elif args["--debug"]: logSvc.setRootThreshold(lvlDebug) + + let server = newServer(makeHandler(cfg.mappings, logSvc)) + logger.info("short_url v$# listening for requests on $#" % + [ VERSION, $cfg.port ]) + server.serve(Port(cfg.port), address = "0.0.0.0") + + except Exception: + let ex = getCurrentException() + threadLocalRef(logSvc).getLogger("pbm/short_url") + .fatal(msg = ex.msg, err = ex)