Thursday, August 13, 2009

Intermission - Refactoring Path and Dispatch Related Procedures

At this time we have multiple procedures that overlap in terms of functionality for path and dispatching, and it is probably a good time to take a look at how to refactor them.

The following are path related procedures:
  • normalize-path: convert a path to the underlying system path, depends on path->segments and shp-handler-path
  • url->path-segments: converts the url into path segments; overlapping partly with path->segments
  • normalize-partial-path: takes the segments and convert into an underlying path; depends on shp-handler-path
  • segments->path: converts the segments back to the underling path; calls normalize-partial-path
  • url->shp-path: wraps around url-path->segments and segments->path
And dispatch related procedures:
  • eval-script-if-changed: evaluate the script if it is changed; takes in script struct
  • shp-handler: evaluate the script from top level, takes in nothing
  • envoke-topfilter-if-available: call the top filter if available - takes in a procedure
  • include! - calls the code, takes in a path
The dispatching procedures appear even more overlapped than the path procedures. Let's start somewhere to refactor them.

First - url-path->segments can be refactored to:

;; url->path-segments - returns an underlying path based on input url (duplicate with normalize-path)
(define (url->path-segments url (default "index.shp"))
(filter (lambda (path)
(not (equal? path "")))
(map path/param-path (url-path url))))

But now url->path-segments looks a lot like path->segments, so the two can be refactored:

(define (path->segments path)
(filter (lambda (path)
(not (equal? path "")))
(if (url? path)
(map path/param-path (url-path path))
(regexp-split #px"\\/" path))))

(define (url->shp-path url)
(segments->path (path->segments url) #t))

Change the name of normalize-partial-path to segments->partial-path to more correctly state its purpose.

The difference between include!, shp-handler, eval-script-if-changed, and envoke-topfilter-if-available are basically their signatures, one takes in a path, the other takes a request, then one takes a script structure, and the last takes a procedure. It would be great to "normalize" them.

Note shp-handler and envoke-top-filter-if-available depend on include!, so include! should be the base of our refactoring. Let's see if we can push redundant features into include!.

Since include! takes a path into path->segments, which now also takes an url, we can reduce the code in shp-handler if we can fold envoke-topfilter-if-available in as well.

(define (include! path #:topfilter (topfilter #f) . args)
(define (helper topfilter)
(let ((proc (evaluate-script (segments->path (path->segments path) #f))))
(if topfilter
(topfilter (lambda () (apply proc args)))
(apply proc args))))
(helper (if topfilter
(evaluate-script (segments->path (path->segments topfilter) #f))
topfilter)))

So we can now get rid of envoke-topfilter-if-available, and modify shp-handler as such:

(define (handle-request server request)
(parameterize (($pathinfo ($pathinfo))
($server server)
($request request))
(eval-script-if-changed! (shp-handler-required server))
(make-response (include! (request-uri request)
#:topfilter (shp-handler-topfilter server)
#:partial? #t))))

handle-request is extracted out of the struct, which now becomes:

(define-struct shp-handler (path default not-found required topfilter)
#:property prop:procedure
(lambda ($struct request)
(handle-request $struct request)))

You'll notice that handler-request calls include! with an extra partial? parameter, which will allow a partial path match, so include! now looks like:

(define (include! path
#:topfilter (topfilter #f)
#:partial? (partial? #f)
. args)
(define (helper topfilter)
(let ((proc (evaluate-script (segments->path (path->segments path) #f))))
(if topfilter
(topfilter (lambda () (apply proc args)))
(apply proc args))))
(helper (if topfilter
(evaluate-script (segments->path (path->segments topfilter) partial?))
topfilter)))

Notice that partial? is only applied to the script and not to the toplevel filter, which needs a full path match.

At this time only eval-script-if-changed has not been combined. A simple way of handling the issue is to allow include! to take in script struct as well:

(define (include! path
#:topfilter (topfilter #f)
#:partial? (partial? #f)
. args)
(define (helper topfilter)
(let ((proc (evaluate-script
(if (script? path)
(script-path path)
(segments->path (path->segments path) partial?)))))
(if topfilter
(topfilter (lambda () (apply proc args)))
(apply proc args))))
(helper (if topfilter
(evaluate-script (segments->path (path->segments topfilter) #f))
topfilter)))

(define (eval-script-if-changed! script)
(unless (not (file-exists? (script-path script)))
(let ((timestamp (file-or-directory-modify-seconds (script-path script))))
(when (> timestamp (script-timestamp script))
(set-script-timestamp! script timestamp)
(include! script)))))

Finally - we can abstract (segments->path (path->segments ...)) into its own procedure:

(define (resolve-path path (partial? #t))
(segments->path (path->segments path) partial?))

(define (include! path
#:topfilter (topfilter #f)
#:partial? (partial? #f)
. args)
(define (make-script path partial?)
(evaluate-script (if (script? path)
(script-path path)
(resolve-path path partial?))))
(define (helper topfilter)
(let ((proc (make-script path partial?)))
(if topfilter
(topfilter (lambda () (apply proc args)))
(apply proc args))))
(helper (if topfilter (make-script topfilter #f) topfilter)))


Allright! Now the code looks a lot better! We can move onto adding more features!

No comments:

Post a Comment