(require hyrule.destructure [defn+]) (require hyrule.argmove [doto]) (require hyrule.oop [meth]) (import hyrule.collections [assoc]) (import content.file-io [parse-html-file send-raw-file]) (import content.comments [create-comment]) (import re) (import functools [lru-cache]) (import os.path [isdir :as dir? isfile :as file? abspath]) (defmacro unless [test #* body] `(when (not ~test) (do ~@body))) (defn error [code message] (return (dict :code code :body (parse-html-file "./www/site/html/error.html" :code code :message message)))) (defclass always [] (meth __init__ [@value]) (meth __getitem__ [_] @value)) (defclass method-map [] (meth __init__ [] (setv @methods {})) (meth __getattr__ [attr] (if (in attr @methods) (get @methods attr) (fn [value] (setv (get @methods attr) value)))) (meth __getitem__ [item] (setv item (.get @methods item None)) (if item item (fn [#* _] (error 405 "method not allowed"))))) (defclass router [] (meth __init__ [] (setv @route-map {}) (setv @route-cache {})) (meth add-route [route] (when (not-in route @route-map) (setv (get @route-map route) (method-map))) (return (get @route-map route))) (meth [lru-cache] get-route-by-path [path] (cond (in path @route-map) (do (get @route-map path)) True (do (while (> (.count path "/") 1) (setv path (.join "/" (get (.split path "/") (slice 0 -1)))) (when (in (+ path "/*") @route-map) (setv handler (get @route-map (+ path "/*"))) (return handler))) (always (fn [#* _] (error 404 "not-found)"))))))) (setv routes (router)) (defmacro route [route method] `(. (routes.add-route ~route) ~(hy.models.Symbol method))) (defreader route-args (.slurp-space &reader) (if (= (.peekc &reader) "[") `[method path request ~@(.parse-one-form &reader)] '[method path request])) (defn if-file-exists [* base-path otherwise] (fn [f] (fn [method path request] (if (file? f"{base-path}{path}") (f method path request) otherwise)))) (defn forward-params [#* params] (fn [f] (fn [method path request] (f method path request #** (dict (lfor param params :if (setx value (. request (get "route") (get "parameters") (get param None))) #(param value))))))) (defn shtml-file-response [file [code 200] [template-params {}]] (dict :code code :body (parse-html-file f"./www/site/html/{file}" #** template-params))) (defn raw-file-response [file [code 200]] (dict :code code #** (dict (zip ["headers" "body"] (send-raw-file f"./www/site/{file}"))))) (defn [(route "/" "GET")] /home #route-args (shtml-file-response "home.html")) (defn [(route "/html/*" "GET") (if-file-exists :base-path "./www/site/html" :otherwise (error 404 "not found"))] /html/* #route-args (shtml-file-response path)) (defn [(route "/assets/*" "GET") (if-file-exists :base-path "./www/site/" :otherwise (error 404 "not found"))] /assets/* #route-args (raw-file-response path)) (defn [(route "/html/view-thought.html" "GET") (forward-params "thought" "filter-tag")] /html/view-thought #route-args [#** template-args] (shtml-file-response "/html/view-thought.html" :template-params template-args)) (defn [(route "/comment" "POST")] /comments #route-args (create-comment request)) (defn [(route "/robots.txt" "GET") ] /robots #route-args (dict :code 200 :headers {"Content-Type" "text/plain"} :body "User-agent *\nDisallow: /\n")) (defn+ [lru-cache] match-request [{method "method" {path "path"} "route" :as request}] ((get (routes.get-route-by-path path) method) method path request))