PageRenderTime 47ms CodeModel.GetById 19ms RepoModel.GetById 1ms app.codeStats 0ms

/cl-terrace/cl-terrace/core.lisp

https://github.com/nallen05/old-cl-terrace
Lisp | 419 lines | 101 code | 29 blank | 289 comment | 3 complexity | 5b9ddeefeea740fe209b61dba999ce34 MD5 | raw file
  1. ;
  2. ; OVERVIEW
  3. ;
  4. ; Simply put: `Cl-Terrace' is the VC part of a hypothetical MVC framework written in
  5. ; Common Lisp. It uses `Hunchentoot' for a webserver, `Djula' for HTML templating, and
  6. ; a few abstractions over the url-as-file-in-the-filesystem metaphor for deciding how
  7. ; to reply to HTTP requests
  8. ;
  9. ; TERRACE PROJECTS
  10. ;
  11. ; a terrace project is represented within a lisp image as a symbol. the symbol becomes
  12. ; a terrace project once it has been linked to a "site/" folder using the DEF-TERRACE
  13. ; function
  14. ;
  15. ; a "sight/" folder must contain at least 1 of the following 3 subfolders:
  16. ;
  17. ; 1. "static/" -- the "static/" folder holds static content used by the project
  18. ; 2. "template/" -- the "template/" folder holds Djula templates [and dictionaries]
  19. ; used by the project
  20. ; 3. "terrace/" -- the "terrace/" folder holds the project's ``terrace files''. the
  21. ; contents of the "terrace/" directory control how the project
  22. ; dispatches and handles individual HTTP requests. see
  23. ; TERRACE FILES
  24. ;
  25. ; [note: running DEF-TERRACE hijacks the SYMBOL-FUNCTION and SYMBOL-VALUE slots of the
  26. ; symbol. the symbol value is a Hunchentoot dispatcher]
  27. ;
  28. ; once you have turned a symbol into a terrace project you can to do any of the
  29. ; following 3 things with it:
  30. ;
  31. ; 1. start a view server using the !START-TERRACE-SERVER/VIEW-ONLY function
  32. ;
  33. ; you can use a view server to preview interactive mock-ups of your website
  34. ;
  35. ; [see Djula's {% devel-dictionary %}, {% devel-value %}, and {% include %} tags
  36. ; for ideas].
  37. ;
  38. ; After starting the server go to the root url (/) in your browser and it will
  39. ; guide you from there.
  40. ;
  41. ; 2. publish the project with the function !START-TERRACE-SERVER
  42. ;
  43. ; once this is done your site is up and running and viewable in the browser!
  44. ;
  45. ; 3. push the project to HUNCHENTOOT:*DISPATCH-TABLE* and start Hunchentoot yourself
  46. ;
  47. ; publishing the project manually makes it easier for multiple terrace projects
  48. ; to coexist behind the same Hunchentoot instance or for terrace projects to
  49. ; coexist with other lisp code replying to HTTP requests behind the same
  50. ; Hunchentoot image
  51. ;
  52. ; note: remember that you need to sync the project with the SYNC-TERRACE function
  53. ; before the dispatcher will do anything
  54. ;
  55. ; SYNCING PROJECTS
  56. ;
  57. ; calling the function SYNC-TERRACE on a project makes its Hunchentoot dispatcher aware
  58. ; of any new changes or additions to the "terrace/" or "template/" directories. this
  59. ; has to be done at least once before the project's Hunchentoot dispatcher will do
  60. ; anything. this is done automatically by !START-TERRACE-SERVER
  61. ;
  62. ; TERRACE FILES
  63. ;
  64. ; the "terrace/" folder contains the project's terrace files. terrace files control
  65. ; how the project handles a particular set of possible HTTP requests. the positions of
  66. ; terrace files within the "terrace/" folder [and how they relate to the url path of
  67. ; the HTTP request] dictates which terrace file will be responsible for replying to a
  68. ; request
  69. ;
  70. ; terrace files are like normal lisp source files except their last form should return
  71. ; the body of the reply as a string or array of bytes [like a Hunchentoot dispatcher],
  72. ; or NIL, in which case the server will return a 404.
  73. ;
  74. ; in simple cases this works something like PHP:
  75. ;
  76. ; this url:
  77. ;
  78. ; http://localhost:8282/hello-world
  79. ;
  80. ; matches this file:
  81. ;
  82. ; /hello-world.lisp
  83. ;
  84. ; [note: the root path in these examples is assumed to start at the "terrace/" folder]
  85. ;
  86. ; [".lisp" and ".cl" extensions are trimmed from the filename during this process]
  87. ;
  88. ; there is, however, much more you can do with terrace files. for instance if you have
  89. ; the following file
  90. ;
  91. ; /user/v.username.lisp
  92. ;
  93. ; containing the following lisp code:
  94. ;
  95. ; (in-package :cl-terrace-user)
  96. ; (format nil "the username is ~A" (v "username"))
  97. ;
  98. ; then the following url
  99. ;
  100. ; http://localhost:8282/user/bob
  101. ;
  102. ; with show up in the browser as
  103. ;
  104. ; "the username is bob"
  105. ;
  106. ; see TERRACE FILE MATCHING RULES for more info on terrace files
  107. ;
  108. ; FUNCTIONS FOR USE INSIDE TERRACE FILES
  109. ;
  110. ; the following functions are exported from the CL-TERRACE package specifically for use
  111. ; within terrace files
  112. ;
  113. ; RENDER (template-path &rest kwd-args &key &allow-other-keys)
  114. ;
  115. ; -- renders and returns the Djula template pointed by `TEMPLATE-PATH'. the root
  116. ; path (/) is the "template/" folder
  117. ;
  118. ; `KWD-ARGS' is a plist of keyword arguments that maps to variables used within
  119. ; the template. RENDER accepts :. "splices" like the function returned by
  120. ; DJULA:COMPILE-TEMPLATE
  121. ;
  122. ; STATIC (static-path &optional content-type)
  123. ;
  124. ; -- handles the file pointed to by `STATIC-PATH'. the root path (/) is the
  125. ; "static/" folder
  126. ;
  127. ; FUNCALL-FILE (terrace-path &rest v-plist &key &allow-other-keys)
  128. ;
  129. ; -- calls the terrace file pointed to by `TERRACE-PATH' [returning its value as if
  130. ; it were a normal lisp function]. the root path (/) is the "terrace/" folder
  131. ;
  132. ; `V-PLIST' should be a plist of keys/values that will be visible to the V
  133. ; function within the body of the terrace file pointed to by `TERRACE-PATH'
  134. ;
  135. ; D (variable dictionary-path &rest variable-plist &key &allow-other-keys)
  136. ;
  137. ; -- returns the value of the variable `VARIABLE' in the dictionary file pointed to
  138. ; by `TEMPLATE-PATH' in the language DJULA:*LANGUAGE*. `VARIABLE-PLIST' should be
  139. ; a plist of template variable name/value pairs like the ones given to RENDER
  140. ; [it takes :. "splices"]
  141. ;
  142. ; also returns a second value that is non-NULL if the value of `VARIABLE'
  143. ; is actually found in the language `LANGUAGE' in the dictionary [like GETHASH's
  144. ; second return value]
  145. ;
  146. ; V (key)
  147. ;
  148. ; -- returns the value associated with the string `KEY'. V will look for the key in
  149. ; the following places:
  150. ;
  151. ; 1. the request url and how it relates to any "v." files or folders
  152. ; encountered while dispatching the request [see TERRACE FILE MATCHING RULES]
  153. ; 2. any "special." files that are ancestors or siblings of the top-level
  154. ; terrace file responding to the request [see TERRACE FILE MATCHING RULES]
  155. ; 3. the `v-plist' argument to FUNCALL-FILE [assuming there has been a call to
  156. ; FUNCALL-FILE somewhere up the stack]
  157. ;
  158. ; G (key)
  159. ;
  160. ; -- returns the HTTP GET parameter named `key'.
  161. ;
  162. ; G behaves differently from HUNCHENTOOT:GET-PARAMETER in that a GET parameter
  163. ; that has been supplied but has no value is represented as NIL [instead of an
  164. ; empty string]. there is a second return value that indicates whether the HTTP
  165. ; GET parameter was supplied or not
  166. ;
  167. ; P (key)
  168. ;
  169. ; -- like G, but for POST parameters instead of GET parameters
  170. ;
  171. ; RETURNING FROM TERRACE FILES
  172. ;
  173. ; You can escape from a terrace file at any time by calling RETURN-FROM with the block
  174. ; TERRACE-FILE. eg:
  175. ;
  176. ; (return-from terrace-file "my webpage")
  177. ;
  178. ; note: there is no way if a terrace file can know if it's being called directly by
  179. ; Hunchentoot to handle a request or from within another terrace file by FUNCALL-FILE.
  180. ; if you want to simulate returning from the toplevel terrace file [the one handling
  181. ; the request] throw a HUNCHENTOOT:HANDLER-DONE. eg:
  182. ;
  183. ; (throw 'hunchentoot:handler-done "my webpage")
  184. ;
  185. ; TERRACE FILES AND COMMON LISP PACKAGES
  186. ;
  187. ; the default package of all terrace files is the :CL-USER package. use top-level
  188. ; IN-PACKAGE forms to change *PACKAGE* while the file is being read [like you would do
  189. ; with a normal lisp source file]
  190. ;
  191. ; `cl-terrace' comes with a :CL-TERRACE-USER package that uses the following packages:
  192. ; :COMMON-LISP
  193. ; :CL-TERRACE
  194. ; :DJULA
  195. ; :HUNCHENTOOT
  196. ; :LOGV
  197. ;
  198. ; MIME-TYPE / ENCODING
  199. ;
  200. ; cl-terrace is utf-8 "out of the box". terrace files and Djula templates are always
  201. ; assumed to be encoded in UTF-8.
  202. ;
  203. ; HUNCHENTOOT:CONTENT-TYPE is set from the content type derived from the toplevel
  204. ; terrace file replying to the request after shaving off the boring ".lisp" or ".cl"
  205. ; extention. so the terrace file
  206. ;
  207. ; /foo.html.lisp
  208. ;
  209. ; will send out MIME type
  210. ;
  211. ; text/html; charset=utf-8
  212. ;
  213. ; in the headers before computing the response.
  214. ;
  215. ; note that the contenty type is derived by first looking in
  216. ; CL-TERRACE:*OVERRULE-MIME-TYPE-ALIST* then calling HUNCHENTOOT:MIME-TYPE. this means
  217. ; that things like .txt, .html, .htm, .shtml and "clean urls", etc., will be explicitly
  218. ; given UTF-8 charset
  219. ;
  220. ; <<<DANGER!!! SIDE EFFECTS!!!>>>
  221. ;
  222. ; when a cl-terrace project considers replying to a request, it sets
  223. ; HUNCHENTOOT:REPLY-EXTERNAL-FORMAT to CL-TERRACE::*DEFAULT-REPLY-EXTERNAL-FORMAT*
  224. ; [which by default is a :UTF-8 flexi-streams external format]. it also recomputes the
  225. ; request parameters using this external format. HUNCHENTOOT:REPLY-EXTERNAL-FORMAT can
  226. ; be set from within the terrace file generating the reply to the request if the
  227. ; multibyte UTF-8 encoding screws up generating binary requests...
  228. ;
  229. ; TERRACE FILE MATCHING RULES
  230. ;
  231. ; URL: /foo/bar/baz
  232. ; FILE: /foo/bar/baz.lisp
  233. ;
  234. ; -> serves result of executing the lisp code in "/foo/bar/baz.lisp"
  235. ; [note .cl files work just the same as .lisp]
  236. ;
  237. ; URL: /foo/bar
  238. ; FILE: /foo/bar.i/i.baz.lisp
  239. ;
  240. ; -> serves result of running the lisp code in "/foo/bar.i/i.baz.lisp"
  241. ;
  242. ; "i.baz.lisp" is the ``index-file'' of the folder "bar.i". Any folder that has an
  243. ; index-file should have the suffix ".i". the obvious exception is the root
  244. ; "terrace/" folder
  245. ;
  246. ; URL: /foo/bar/anything
  247. ; FILE: /foo/bar/v.baz.lisp
  248. ;
  249. ; -> serves result of running the lisp code in "/foo/bar/v.baz.lisp"
  250. ;
  251. ; Within this code:
  252. ;
  253. ; (v "baz") -> "anything"
  254. ;
  255. ; URL: /more/other/stuff
  256. ; FILE: /v.foo/v.bar/v.baz.lisp
  257. ;
  258. ; -> serves result of running the lisp code in "/v.foo/v.bar/v.vaz.lisp"
  259. ;
  260. ; Within this code:
  261. ;
  262. ; (v "foo") -> "more"
  263. ; (v "bar") -> "other"
  264. ; (v "baz") -> "stuff"
  265. ;
  266. ; URL: /foo/bar
  267. ; FILE: /foo/bar.lisp
  268. ; ALSO SEEN: /special.a.lisp -- contains the code (+ 1 2)
  269. ; /foo/special.b.lisp -- contains the code: (1+ (v "a"))
  270. ;
  271. ; -> serves result of running the lisp code in "/foo/bar.lisp".
  272. ;
  273. ; Within this code:
  274. ;
  275. ; (v "a") -> 3
  276. ; (v "b") -> 4
  277. ;
  278. (in-package :cl-terrace)
  279. (defun .overruled-mime-type (pathspec)
  280. "
  281. like HUNCHENTOOT:MIME-TYPE but first checks *OVERRULE-MIME-TYPE* then
  282. *OVERRULE-MIME-TYPE-LIST*
  283. (.overruled-mime-type \"/foo.html\")
  284. -> \"text/html; charset=utf-8\"
  285. (.overruled-mime-type \"/foo\")
  286. -> \"text/html; charset=utf-8\"
  287. (.overruled-mime-type \"/foo.txt\")
  288. -> \"text/plain; charset=utf-8\"
  289. (let ((*overrule-mime-type[ \"image/jpeg\"))
  290. (.overruled-mime-type \"/foo.txt\"))
  291. -> \"image/jpeg\"
  292. "
  293. (or (rest (assoc (pathname-type pathspec)
  294. *overrule-mime-type-alist*
  295. :test 'equalp))
  296. (hunchentoot:mime-type pathspec)))
  297. ; projects
  298. (defun def-terrace (project-name
  299. site-folder
  300. &key (publish-static-p t))
  301. "Creates a Terrace project named `PROJECT-NAME' with a \"site/\" folder `SITE-FOLDER'
  302. [note: this creates a Hunchentoot dispatcher named `PROJECT-NAME', so you can't ever
  303. define a function named `PROJECT-NAME' afterwords].
  304. `SITE-FOLDER' should contain 3 subfolders:
  305. 1. terrace/ -- holds Terrace files [\".lisp\" or \".cl\" files] that are used to
  306. dispatch HTTP requests. Make sure to read about the rules for matching
  307. url-paths to Terrace Files at the top of this source file.
  308. 2. template/ -- holds Djula templates. templates in this folder can be rendered with
  309. the function TEMPLATE
  310. 3. static/ -- holds static files used to dispatch an HTTP request
  311. if `PUBLISH-STATIC-P' is non-NULL then the `PROJECT-NAME' Hunchentoot dispatcher
  312. publishes the contents of the \"static/\" folder. Otherwise the \"static/\" folder is
  313. ignored. [It's much faster to have some other webserver such as nginx or Apache sitting
  314. in front of Hunchentoot serving your static files]
  315. "
  316. (let* ((terrace-folder (merge-pathnames "terrace/" site-folder))
  317. (template-folder (merge-pathnames "template/" site-folder))
  318. (static-folder (merge-pathnames "static/" site-folder))
  319. (static-dispatcher (if publish-static-p
  320. (hunchentoot:create-folder-dispatcher-and-handler "/" static-folder))))
  321. ;; create a "terrace/" folder dispatcher named "project-name
  322. (setf (symbol-function project-name)
  323. (lambda (hunchentoot:*request*)
  324. (let ((*terrace-project* project-name))
  325. ;; expect everything to come in encoded in utf-8
  326. (hunchentoot:recompute-request-parameters :request hunchentoot:*request*
  327. :external-format *default-reply-external-format*)
  328. (or ;; maybe dispatch terrace file
  329. (aif (.dispatch-url-path (hunchentoot:script-name hunchentoot:*request*))
  330. (f0 ;; set hunchentoot MIME and encoding stuff
  331. (setf (hunchentoot:reply-external-format) *default-reply-external-format*
  332. (hunchentoot:content-type) (let ((% (.trim-boring-extentions (hunchentoot:script-name hunchentoot:*request*))))
  333. ;; this bit of uglyness is because the "i."/ "v."
  334. ;; suffix of index files and variable files
  335. ;; confuses poor .OVERRULED-MIME-TYPE
  336. (.overruled-mime-type (cond ((eql (mismatch % "i.") 2)
  337. (subseq % 2))
  338. ((eql (mismatch % "v.") 2)
  339. (subseq % 2))
  340. (t %)))))
  341. (funcall it)))
  342. ;; maybe dispatch static file
  343. (if static-dispatcher
  344. (funcall static-dispatcher hunchentoot:*request*))))))
  345. ;; set symbol-plist
  346. (setf ;; save `SITE-FOLDER' and subfolders
  347. (get project-name 'site-folder) site-folder
  348. (get project-name 'terrace-folder) terrace-folder
  349. (get project-name 'template-folder) template-folder
  350. (get project-name 'dictionary-folder) template-folder
  351. (get project-name 'static-folder) static-folder
  352. ;; save keyword options
  353. (get project-name 'publish-static-p) publish-static-p
  354. ;; create project caches
  355. (get project-name 'terrace-ffc) (djula-util:ffc-init '((identity . .compile-terrace-file)))
  356. (get project-name 'template-ffc) (djula-util:ffc-init '((djula:dictionary-p . .compile-dictionary-file)
  357. (djula:devel-dictionary-p . .compile-devel-dictionary-file)
  358. (identity . .compile-template-file)))
  359. ;; finalize
  360. (get project-name 'terrace-project-p) t)
  361. ;; make `PROJECT-NAME' evaluate to itself
  362. (setf (symbol-value project-name)
  363. project-name)
  364. project-name))
  365. (defun sync-terrace (project &key (sync-template-folder t) (sync-terrace-folder t))
  366. "makes sure all the functions compiled from files in `PROJECT's \"terrace/\" and
  367. \"template/\" folders are up to date, recompiling them if the're not"
  368. (if (not (get project 'terrace-project-p))
  369. (error "<<<~S does not name a terrace project [see DEF-TERRACE]>>>" project)
  370. (flet ((syncit (folder-name cache-name)
  371. (aif (get project folder-name)
  372. (if (cl-fad:directory-exists-p it)
  373. (let ((*terrace-project* project))
  374. (logv:format-log "Syncing ~A" folder-name)
  375. (djula-util:ffc-sync (get project cache-name) it)
  376. (djula-util:ffc-uncache-deleted (get project cache-name)
  377. :hook (f_ (logv:format-log "Deleting ~A from ~A"
  378. _
  379. cache-name))))
  380. (logv:format-log "<<<There is no ~A folder in \"site/\">>>"
  381. folder-name))
  382. (logv:format-log "<<<~A does not have a ~A folder>>>"
  383. project
  384. folder-name))))
  385. (logv:format-log "\"site/\": ~A" (get project 'site-folder))
  386. (if sync-template-folder
  387. (syncit 'template-folder 'template-ffc))
  388. (if sync-terrace-folder
  389. (syncit 'terrace-folder 'terrace-ffc))))
  390. project)