Initial version.

This commit is contained in:
Jonathan Bernard
2025-07-26 19:30:54 -05:00
commit 5674c1e95c
6 changed files with 128 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
.*.sw?
/short_url
View File
+2
View File
@@ -0,0 +1,2 @@
[tools]
nim = "latest"
+7
View File
@@ -0,0 +1,7 @@
{
"mappings": {
"/hn": "https://news.ycombinator.com",
"/([^/]+)/latest": "https://app.abbey.com/discussions/$1?order=timestamp,desc"
},
"port": "8081"
}
+17
View File
@@ -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"
+100
View File
@@ -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 <cfgFile> 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)