├── LICENSE ├── README.md ├── app.rkt ├── index.html ├── info.rkt └── main.rkt /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2014 Daniel MacDougall 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spin 2 | 3 | Write RESTful web apps in Racket. 4 | 5 | Spin layers some convenience functions on top of Racket's built-in web server to simplify defining routes and route handlers. 6 | 7 | ## Installation 8 | From the command line, run `raco pkg install https://github.com/dmac/spin.git` to install the package. 9 | 10 | ## Overview 11 | 12 | Define routes with one of `get`, `post`, `put`, `patch`, `delete` and pass it the route string and a handler function. 13 | 14 | ```scheme 15 | #lang racket 16 | 17 | (require (planet dmac/spin)) 18 | 19 | (get "/" 20 | (lambda () "Hello!")) 21 | 22 | (run) 23 | ``` 24 | 25 | ## Params 26 | 27 | Your handler function will be passed the request object if an argument is specified. 28 | 29 | It can be given to the `params` function along with a key to search for values in the query-string, post-body, or url. 30 | 31 | ```scheme 32 | (get "/hi" (lambda (req) 33 | (string-append "Hello, " (params req 'name) "!"))) 34 | ``` 35 | 36 | ``` 37 | $ curl "http://localhost:8000/hi?name=Charlotte" 38 | Hello, Charlotte! 39 | ``` 40 | 41 | ``` 42 | $ curl "http://localhost:8000/hi" -X POST -d "name=Anansi" 43 | Hello, Anansi! 44 | ``` 45 | 46 | Retrieve params from the url string itself: 47 | 48 | ```scheme 49 | (get "/hi/:name" (lambda (req) 50 | (string-append "Hello, " (params req 'name) "!"))) 51 | ``` 52 | 53 | ``` 54 | $ curl "http://localhost:8000/hi/Peter" 55 | Hello, Peter! 56 | ``` 57 | 58 | ## Templating 59 | 60 | Your handler function need only return a string to render. You can easily use existing templating libraries with Spin. 61 | 62 | **app.rkt** 63 | 64 | ```scheme 65 | (require web-server/templates) 66 | 67 | (get "/template" (lambda (req) 68 | (define name (params req 'name)) 69 | (include-template "index.html"))) 70 | 71 | (run) 72 | ``` 73 | 74 | **index.html** 75 | 76 | ```html 77 | 78 |
79 |Hello, @|name|!
80 | 81 | 82 | ``` 83 | 84 | ``` 85 | $ curl "http://localhost:8000/template?name=Aragog" 86 | 87 | 88 |Hello, Aragog!
89 | 90 | 91 | ``` 92 | 93 | ## Advanced Responses 94 | 95 | In addition to the response body, you can specify response status and custom headers if you return a list instead of a string from your handler: 96 | 97 | ```scheme 98 | (get "/headers" (lambda () 99 | (define h (header #"Custom-Header" #"Itsy bitsy")) 100 | `(201 (,h) "Look for the custom header!"))) 101 | ``` 102 | 103 | ## Response Makers 104 | 105 | Response makers are middleware that transform a response before it is sent to the client. 106 | 107 | A global default response maker can be defined by passing it to the run function: 108 | 109 | ```scheme 110 | (define (json-404-response-maker status headers body) 111 | (response status 112 | (status->message status) 113 | (current-seconds) 114 | #"application/json; charset=utf-8" 115 | headers 116 | (let ([jsexpr-body (case status 117 | [(404) (string->jsexpr 118 | "{\"error\": 404, \"message\": \"Not Found\"}")] 119 | [else body])]) 120 | (lambda (op) (write-json (force jsexpr-body) op))))) 121 | 122 | (run #:response-maker json-404-response-maker) 123 | ``` 124 | 125 | It is also possible to define new handler types that use different response makers: 126 | 127 | ```scheme 128 | (define (json-response-maker status headers body) 129 | (response status 130 | (status->message status) 131 | (current-seconds) 132 | #"application/json; charset=utf-8" 133 | headers 134 | (let ([jsexpr-body (string->jsexpr body)]) 135 | (lambda (op) (write-json (force jsexpr-body) op))))) 136 | 137 | (define (json-get path handler) 138 | (define-handler "GET" path handler json-response-maker)) 139 | 140 | (json-get "/json" (lambda (req) 141 | "{\"body\":\"JSON GET\"}")) 142 | ``` 143 | 144 | ## Contributors 145 | 146 | - Felipe Oliveira Carvalho ([@philix](https://github.com/philix)) 147 | - Jordan Johnson ([@RenaissanceBug](https://github.com/RenaissanceBug)) 148 | -------------------------------------------------------------------------------- /app.rkt: -------------------------------------------------------------------------------- 1 | #lang racket 2 | 3 | (require "main.rkt" 4 | web-server/servlet 5 | web-server/templates 6 | json) 7 | 8 | (get "/" (lambda () "GET request")) 9 | 10 | (post "/" (lambda () "POST request")) 11 | 12 | (put "/" (lambda () "PUT request")) 13 | 14 | (delete "/" (lambda () "DELETE request")) 15 | 16 | (get "/hi/:name" (lambda (req) 17 | (string-append "Hi, " (params req 'name)))) 18 | 19 | (get "/hi/:first_name/:last_name" (lambda (req) 20 | (define first_name (params req 'first_name)) 21 | (define last_name (params req 'last_name)) 22 | (include-template "index.html"))) 23 | 24 | (get "/headers" (lambda (req) 25 | (define h (header #"Custom-Header" #"This is a custom header")) 26 | `(200 (,h) "Check out the custom header"))) 27 | 28 | (post "/payload" (lambda (req) 29 | (string-append "POSTed payload: " (params req 'payload)))) 30 | 31 | 32 | ;; Examples using response makers. A global default response maker can be defined by passing it to the run 33 | ;; function, and new handler types can be defined with different response makers. 34 | 35 | (define (json-404-response-maker status headers body) 36 | (response status 37 | (status->message status) 38 | (current-seconds) 39 | #"application/json; charset=utf-8" 40 | headers 41 | (let ([jsexpr-body (case status 42 | [(404) (string->jsexpr 43 | "{\"error\": 404, \"message\": \"Not Found\"}")] 44 | [else body])]) 45 | (lambda (op) (write-json (force jsexpr-body) op))))) 46 | 47 | (define (json-response-maker status headers body) 48 | (response status 49 | (status->message status) 50 | (current-seconds) 51 | #"application/json; charset=utf-8" 52 | headers 53 | (let ([jsexpr-body (string->jsexpr body)]) 54 | (lambda (op) (write-json (force jsexpr-body) op))))) 55 | 56 | (define (json-get path handler) 57 | (define-handler "GET" path handler json-response-maker)) 58 | 59 | (json-get "/json" (lambda (req) 60 | "{\"body\":\"JSON GET\"}")) 61 | 62 | (run #:response-maker json-404-response-maker) 63 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |Hello, @|first_name| @|last_name|
5 | 6 | 7 | -------------------------------------------------------------------------------- /info.rkt: -------------------------------------------------------------------------------- 1 | #lang setup/infotab 2 | (define name "spin") 3 | (define blurb 4 | (list '(p "Write RESTful web apps in Racket."))) 5 | (define release-notes 6 | (list '(ul (li "Support for serve/servlet args") 7 | (li "Customizable response handlers")))) 8 | (define primary-file "main.rkt") 9 | (define categories '(net)) 10 | (define homepage "https://github.com/dmacdougall/spin") 11 | (define version "1.3") 12 | (define required-core-version "5.2") 13 | (define repositories 14 | (list "4.x")) 15 | -------------------------------------------------------------------------------- /main.rkt: -------------------------------------------------------------------------------- 1 | #lang racket 2 | 3 | ; TODO 4 | ; * tests 5 | ; * look into ORMs 6 | 7 | (require web-server/servlet 8 | web-server/servlet-env 9 | web-server/http/bindings 10 | web-server/http/request-structs 11 | net/url-structs) 12 | 13 | (provide get post put patch delete 14 | default-response-maker 15 | status->message 16 | define-handler 17 | params 18 | header 19 | run) 20 | 21 | (define (get path handler) (define-handler "GET" path handler)) 22 | (define (post path handler) (define-handler "POST" path handler)) 23 | (define (put path handler) (define-handler "PUT" path handler)) 24 | (define (patch path handler) (define-handler "PATCH" path handler)) 25 | (define (delete path handler) (define-handler "DELETE" path handler)) 26 | 27 | (define (default-response-maker status headers body) 28 | (response/full status 29 | (status->message status) 30 | (current-seconds) 31 | TEXT/HTML-MIME-TYPE 32 | headers 33 | (list (string->bytes/utf-8 body)))) 34 | 35 | (define run 36 | (make-keyword-procedure 37 | (lambda (kws kw-args . etc) 38 | (cond 39 | [(not (empty? etc)) 40 | (error 'run 41 | "expected kw args (for serve/servlet) only; found ~a non-kw args" 42 | (length etc))] 43 | [(ormap (curryr memq '(#:servlet-regexp #:command-line?)) kws) 44 | (error 'run 45 | "kw args may not include #:servlet-regexp or #:command-line?")] 46 | [else 47 | (let* ([kw-pairs (append '((#:servlet-regexp #rx"") 48 | (#:command-line? #t)) 49 | (filter (lambda (kw-pair) 50 | (not (eq? '#:response-maker (car kw-pair)))) 51 | (map list kws kw-args)))] 52 | [sorted-pairs (sort kw-pairs keyword #:key first)] 53 | [response-maker (let ([response-maker-pair 54 | (findf (lambda (p) (eq? (car p) '#:response-maker)) 55 | (map list kws kw-args))]) 56 | (if response-maker-pair 57 | (cadr response-maker-pair) 58 | default-response-maker))]) 59 | (keyword-apply serve/servlet 60 | (map first sorted-pairs) 61 | (map second sorted-pairs) 62 | (list (lambda (req) 63 | (request->handler req response-maker)))))])))) 64 | 65 | (define (params request key) 66 | (define query-pairs (url-query (request-uri request))) 67 | (define body-pairs 68 | (match (request-post-data/raw request) 69 | [#f empty] 70 | [body (url-query (string->url (string-append "?" (bytes->string/utf-8 body))))])) 71 | (define url-pairs 72 | (let ([keys (cadr (request->handler/keys/response-maker request))]) 73 | (request->key-bindings request keys))) 74 | (hash-ref (make-hash (append query-pairs body-pairs url-pairs)) key "")) 75 | 76 | (define request-handlers (make-hash)) 77 | 78 | (define (define-handler method path handler [response-maker default-response-maker]) 79 | (define keys (path->keys path)) 80 | (define path-regexp (compile-path path)) 81 | (define handler/keys/response-maker (list handler keys response-maker)) 82 | (hash-set! request-handlers 83 | (string-append method " " path-regexp) 84 | handler/keys/response-maker)) 85 | 86 | (define (path->keys path) 87 | (map (lambda (match) (string->symbol (substring match 2))) 88 | (regexp-match* #rx"/:([^\\/]+)" path))) 89 | 90 | (define (compile-path path) 91 | (string-append 92 | "^" 93 | (regexp-replace* #rx":[^\\/]+" path "([^/?]+)") 94 | "(?:$|\\?)")) 95 | 96 | (define (request->handler request 97 | response-maker) 98 | (define handler/keys/response-maker (request->handler/keys/response-maker request)) 99 | (begin 100 | (cond 101 | [handler/keys/response-maker (render/handler (car handler/keys/response-maker) 102 | request 103 | (caddr handler/keys/response-maker))] 104 | [else (render/404 response-maker)]))) 105 | 106 | (define (request->handler/keys/response-maker request) 107 | (define handler-key (request->matching-key request)) 108 | (case handler-key 109 | [(#f) #f] 110 | [else (hash-ref request-handlers handler-key #f)])) 111 | 112 | (define (request->key-bindings request keys) 113 | (define path-regexp 114 | (second (regexp-split #rx" " (request->matching-key request)))) 115 | (define bindings (cdr (regexp-match path-regexp (url->string (request-uri request))))) 116 | (for/list ([key keys] [binding bindings]) 117 | (cons key binding))) 118 | 119 | (define (request->matching-key request) 120 | (define (key-matches-route? key) 121 | (match-define (list _ method path-regexp) 122 | (regexp-match #rx"([^ ]+) ([^ ]+)" key)) 123 | (and (equal? (request-method request) (string->bytes/utf-8 method)) 124 | (regexp-match (regexp path-regexp) 125 | (url->string (request-uri request))))) 126 | (findf key-matches-route? (hash-keys request-handlers))) 127 | 128 | (define (render/handler handler request response-maker) 129 | (define content 130 | (case (procedure-arity handler) 131 | [(1) (handler request)] 132 | [else (handler)])) 133 | (define status 134 | (cond [(list? content) (first content)] 135 | [else 200])) 136 | (define headers 137 | (cond [(list? content) (second content)] 138 | [else '()])) 139 | (define body 140 | (cond [(list? content) (third content)] 141 | [else content])) 142 | 143 | (response-maker status headers body)) 144 | 145 | (define (render/404 response-maker) 146 | (response-maker 404 147 | '() 148 | "Not Found")) 149 | 150 | (define (status->message status) 151 | (case status 152 | [(100) #"Continue"] 153 | [(101) #"Switching Protocols"] 154 | [(200) #"OK"] 155 | [(201) #"Created"] 156 | [(202) #"Accepted"] 157 | [(203) #"Non-Authoritative Information"] 158 | [(204) #"No Content"] 159 | [(205) #"Reset Content"] 160 | [(206) #"Partial Content"] 161 | [(300) #"Multiple Choices"] 162 | [(301) #"Moved Permanently"] 163 | [(302) #"Found"] 164 | [(303) #"See Other"] 165 | [(304) #"Not Modified"] 166 | [(305) #"Use Proxy"] 167 | [(307) #"Temporary Redirect"] 168 | [(400) #"Bad Request"] 169 | [(401) #"Unauthorized"] 170 | [(402) #"Payment Required"] 171 | [(403) #"Forbidden"] 172 | [(404) #"Not Found"] 173 | [(405) #"Method Not Allowed"] 174 | [(406) #"Not Acceptable"] 175 | [(407) #"Proxy Authentication Required"] 176 | [(408) #"Request Timeout"] 177 | [(409) #"Conflict"] 178 | [(410) #"Gone"] 179 | [(411) #"Length Required"] 180 | [(412) #"Precondition Failed"] 181 | [(413) #"Request Entity Too Large"] 182 | [(414) #"Request-URI Too Long"] 183 | [(415) #"Unsupported Media Type"] 184 | [(416) #"Requested Range Not Satisfiable"] 185 | [(417) #"Expectation Failed"] 186 | [(500) #"Internal Server Error"] 187 | [(501) #"Not Implemented"] 188 | [(502) #"Bad Gateway"] 189 | [(503) #"Service Unavailable"] 190 | [(504) #"Gateway Timeout"] 191 | [(505) #"HTTP Version Not Supported"] 192 | [else #""])) 193 | --------------------------------------------------------------------------------