Skip to content
Browse files

Initial import from http://github.com/mmcgrana/clj-garden

  • Loading branch information...
0 parents commit 68e08fb6a8a09a5027459d383bdce70cfbe40367 @mmcgrana committed
1 .gitignore
@@ -0,0 +1 @@
+bundle
22 LICENSE
@@ -0,0 +1,22 @@
+Copyright (c) 2009 Mark McGranaghan
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
60 README.textile
@@ -0,0 +1,60 @@
+h1. Ring
+
+Ring is a library inspired by Python's WSGI and Ruby's Rack that
+enables modular and concise Clojure web applications. Ring abstracts the details
+of HTTP and defines a simple API by which web application components can
+cleanly interact.
+
+h2. Quick Setup
+
+All jars needed to load any of the @ring.*@ libs in this distribution are
+included in the jars/ directory. To see a live Ring app, run:
+
+ java -Djava.ext.dirs=jars clojure.main src/ring/examples/hello_world.clj
+ curl localhost:8080/
+
+You can find additional example Ring apps in the @src/examples@ directory of this distribution, including a "basic stack" example that shows a typical Ring setup with wrapping middleware.
+
+h2. Spec
+
+Please consult the @SPEC@ file at the root of this distribution for a complete
+description of the Ring interface.
+
+h2. Available Libs
+
+ * @ring.jetty@: Handler for the Jetty webserver.
+ * @ring.file@: Middleware that serves static files out of a public directory.
+ * @ring.file-info@: Middleware that augments response headers with info about File responses.
+ * @ring.dump@: Endpoint that dumps the Ring requests as HTML responses for debugging.
+ * @ring.show-exceptions@: Middleware that catches exceptions and displays readable stack traces for debugging.
+ * @ring.reloading@: Middleware to automatically reload selected libs before each requests, minimizing server restarts.
+ * @ring.builder@: Helpers for combining Ring endpoints and middleware into Ring apps.
+ * @ring.lint@: Linter for the Ring interface, ensures compliance with the @SPEC@.
+ * @ring.examples.*@: Various example Ring apps.
+
+h2. Development
+
+Ring is being actively developed; you can track its progress and contribute at: "github.com/mmcgrana/ring":http://github.com/mmcgrana/ring.
+
+To run all of the @ring.*@ unit tests:
+
+ java -Djava.ext.dirs=jars clojure.main test/ring/run.clj
+
+You can learn more about Ring's dependencies at the following sites:
+
+ * @clojure.jar@, @clojure-contrib.jar@: "clojure.org":http://clojure.org
+ * @jetty-6.1.14.jar@, @jetty-util-6.1.14.jar@, @servlet-api-2.5-6.1.14.jar@: "mortbay.org/jetty":http://www.mortbay.org/jetty
+ * @commons-io-1.4.jar@: "commons.apache.org/io":http://commons.apache.org/io/
+ * @clj-html.jar@, @clj-html-helpers.jar@, @clj-backtrace.jar@, @clj-unit@ (testing): "github.com/mmcgrana":http://github.com/mmcgrana
+
+---
+
+<pre><code>
+ +--->HTTP Request--->[Handler]--->Req1---->[Middleware]--->Req2----+
+ | |
+ [Client] [Endpoint]
+ | |
+ +----HTTP Response<--[Handler]<---Resp2<---[Middleware]<---Resp1<--+
+</code></pre>
+
+Copyright (c) 2009 Mark McGranaghan and released under an MIT license.
124 SPEC
@@ -0,0 +1,124 @@
+=== Ring Spec (Draft Version)
+Ring is defined in terms of Components, Handlers, Requests, and Responses,
+each of which are described below.
+
+
+== Components
+Ring components constitute the logic of the web application and are abstracted
+form the details of the HTTP protocol. They are implemented as Clojure
+functions that process a given request to generate and return a response.
+
+A component is either an 'endpoint' or 'middleware'. An endpoint component is
+used at the leaf of a component tree, i.e. it does not call any other components
+to generate it's response. A 'middleware' component in contrast may invoke
+other components to generate its response. We can combine 1 or more endpoints
+with 0 or more middleware components to build a complete Ring 'app'. Such an
+app can then be run by a Ring handler.
+
+== Handlers
+A Ring handler is a server implementation than can 'run' apps. Handlers are
+responsible for implementing the HTTP protocol and abstracting the apps that
+they run from the details of the protocol.
+
+Handlers are implemented as functions of two arguments, an options map and a
+Ring app. The options map provides any needed configuration to the handler, such
+as the port on which to run.
+
+Once initialized, handlers receive HTTP requests, parse them to construct an
+Ring request, and then invoke their Ring app with this request as an
+argument. Once the app returns a Ring response, the handler uses it to construct
+and send an HTTP response to the client.
+
+
+== Ring Requests
+A Ring request is a Clojure map containing at least the following keys and
+corresponding values:
+
+:server-port
+ (Required, Integer)
+ The port on which the request is being handled.
+
+:server-name
+ (Required, String)
+ The resolved server name, or the server IP address.
+
+:remote-addr
+ (Required, String)
+ The IP address of the client or the last proxy that sent the request.
+
+:uri
+ (Required, String)
+ The request URI. Must starts with "/".
+
+:query-string
+ (Optional, String)
+ The query string, if present.
+
+:scheme
+ (Required, Keyword)
+ The transport protocol, must be one of :http or :https.
+
+:request-method
+ (Required, Keyword)
+ The HTTP request method, must be one of :get, :head, :options, :put, :post, or
+ :delete.
+
+:content-type
+ (Optional, String)
+ The MIME type of the request body, if known.
+
+:content-length
+ (Optional, Integer)
+ The number of bytes in the request body, if known.
+
+:character-encoding
+ (Optional, String)
+ The name of the character encoding used in the request body, if known.
+
+:headers
+ (Required, IPersistentMap)
+ A Clojure map of downcased header name Strings to corresponding header value
+ Strings.
+
+:body
+ (Optional, InputStream)
+ An InputStream for the request body, if present.
+
+If a component invokes another component with an environment containing
+additional keys, these keys must be namespaced using the Clojure
+:name.space/key-name convention. The ring.* namespaces are reserved.
+
+
+== Ring Responses
+A Ring response is a Clojure map containing at least the following keys and corresponding values:
+
+:status
+ (Required, Integer)
+ The HTTP status code, must be greater than or equal to 100.
+
+:headers
+ (Required, IPersistentMap)
+ A Clojure map of HTTP header names to header values. These values may be
+ either Strings, in which case one name/value header will be sent in the
+ HTTP response, or a seq of Strings, in which case a name/value header will be
+ sent for each such String value.
+
+:body
+ (Optional, {String, File, InputStream})
+ A representation of the response body, if a response body is appropriate for
+ the response's status code. The respond body is handled according to its type:
+ String:
+ Contents are sent to the client as-is.
+ File:
+ Contents at the specified location are sent to the client. The server may
+ use an optimized method to send the file if such a method is available.
+ InputStream:
+ Contents are consumed from the stream and sent to the client. When the
+ stream is exhausted, it is .close'd.
+
+As with environments, keys other than those listed above should be appropriately
+namespaced.
+
+
+== Credit
+This project borrows heavily from Ruby's Rack and Python's WSGI, and I thank the communities developing and supporting those projects.
17 build.xml
@@ -0,0 +1,17 @@
+<project name="ring" default="jar">
+ <description>Pack all sources into a JAR.</description>
+
+ <property name="jarfile" location="ring.jar"/>
+
+ <target name="clean" description="Remove generated files and directories.">
+ <delete file="${jarfile}"/>
+ </target>
+
+ <target name="jar" description="Create jar file.">
+ <jar jarfile="${jarfile}">
+ <path location="LICENSE"/>
+ <fileset dir="./src" includes="ring/**/*.clj"/>
+ <fileset dir="./test" includes="ring/**/*.clj"/>
+ </jar>
+ </target>
+</project>
BIN jars/clj-backtrace.jar
Binary file not shown.
BIN jars/clj-html-helpers.jar
Binary file not shown.
BIN jars/clj-html.jar
Binary file not shown.
BIN jars/clj-unit.jar
Binary file not shown.
BIN jars/clojure-contrib.jar
Binary file not shown.
BIN jars/clojure.jar
Binary file not shown.
BIN jars/commons-io-1.4.jar
Binary file not shown.
BIN jars/jetty-6.1.14.jar
Binary file not shown.
BIN jars/jetty-util-6.1.14.jar
Binary file not shown.
BIN jars/ring.jar
Binary file not shown.
BIN jars/servlet-api-2.5-6.1.14.jar
Binary file not shown.
8 src/ring/builder.clj
@@ -0,0 +1,8 @@
+(ns ring.builder)
+
+(defn wrap-if
+ "If test is logically true, returns the result of invoking the wrapper on the
+ core app, i.e. a wrapped app; if test is logically false, returns the core
+ app."
+ [test wrapper-fn core-app]
+ (if test (wrapper-fn core-app) core-app))
81 src/ring/dump.clj
@@ -0,0 +1,81 @@
+(ns ring.dump
+ (:use (clj-html core utils helpers)
+ clojure.contrib.def
+ clojure.set)
+ (:import (org.apache.commons.io IOUtils)))
+
+(def css "
+/*
+Copyright (c) 2008, Yahoo! Inc. All rights reserved.
+Code licensed under the BSD License:
+http://developer.yahoo.net/yui/license.txt
+version: 2.6.0
+*/
+html{color:#000;background:#FFF;}body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,textarea,p,blockquote,th,td{margin:0;padding:0;}table{border-collapse:collapse;border-spacing:0;}fieldset,img{border:0;}address,caption,cite,code,dfn,em,strong,th,var{font-style:normal;font-weight:normal;}li{list-style:none;}caption,th{text-align:left;}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal;}q:before,q:after{content:'';}abbr,acronym{border:0;font-variant:normal;}sup{vertical-align:text-top;}sub{vertical-align:text-bottom;}input,textarea,select{font-family:inherit;font-size:inherit;font-weight:inherit;}input,textarea,select{*font-size:100%;}legend{color:#000;}del,ins{text-decoration:none;}
+
+
+h3.info {
+ font-size: 1.5em;
+ margin-left: 1em;
+ padding-top: .5em;
+ padding-bottom: .5em;
+}
+
+table.trace {
+ margin-left: 1em;
+ background: lightgrey;
+}
+
+table.trace tr {
+ line-height: 1.4em;
+}
+
+table.trace td.method {
+ padding-left: .5em;
+ text-aligh: left;
+}
+
+table.trace td.source {
+ text-align: left;
+}")
+
+(def ring-keys
+ '(:server-port :server-name :remote-addr :uri :query-string :scheme
+ :request-method :content-type :content-length :character-encoding
+ :headers :body))
+
+(defhtml req-pair
+ [key req]
+ [:tr [:td.source (h (str key))]
+ [:td.method (h (pr-str (key req)))]])
+
+(defhtml template
+ [req]
+ (doctype :xhtml-transitional)
+ [:html {:xmlns "http://www.w3.org/1999/xhtml"}
+ [:head
+ [:meta {:http-equiv "Content-Type" :content "text/html"}]
+ [:title "Ring: Environment Dump"]]
+ [:style {:type "text/css"} css]
+ [:body
+ [:div#content
+ [:h3.info "Ring Env. Values"]
+ [:table.trace
+ [:tbody
+ (domap-str [key ring-keys]
+ (req-pair key req))]]
+ (if-let [user-keys (difference (set (keys req)) (set ring-keys))]
+ (html
+ [:br]
+ [:table.trace
+ [:tbody [:tr
+ (domap-str [key (sort user-keys)]
+ (req-pair key req))]]]))]]])
+
+(defn app
+ "Returns a response tuple corresponding to an HTML dump of the request
+ req as it was recieved by this app."
+ [req]
+ {:status 200
+ :headers {"Content-Type" "text/html"}
+ :body (template req)})
14 src/ring/examples/basic_stack.clj
@@ -0,0 +1,14 @@
+; A failry typical middleware configuration wrapping the dump endpoint.
+
+(ns ring.examples.basic-stack
+ (:require (ring show-exceptions file-info file reloading dump jetty))
+ (:import (java.io File)))
+
+(def app
+ (ring.show-exceptions/wrap
+ (ring.file-info/wrap
+ (ring.file/wrap (File. "src/ring/examples/public")
+ (ring.reloading/wrap '(ring.dump)
+ ring.dump/app)))))
+
+(ring.jetty/run {:port 8080} app)
16 src/ring/examples/hello_world.clj
@@ -0,0 +1,16 @@
+; A very simple Ring application.
+
+(ns ring.examples.hello_world
+ (:require ring.jetty)
+ (:import java.util.Date (java.text DateFormat SimpleDateFormat)))
+
+(def formatter (SimpleDateFormat. "HH:mm:ss"));
+
+(defn app
+ [req]
+ {:status 200
+ :headers {"Content-Type" "text/html"}
+ :body (str "<h3>Hello World from Ring</h3>"
+ "<p>The current time is " (.format formatter (Date.)) ".</p>")})
+
+(ring.jetty/run {:port 8080} app)
18 src/ring/examples/linted.clj
@@ -0,0 +1,18 @@
+; Like basic_stack.clj, but with the linter inserted between each component.
+
+(ns ring.examples.linted
+ (:require (ring show-exceptions file file-info reloading lint dump jetty))
+ (:import (java.io File)))
+
+(def app
+ (ring.lint/wrap
+ (ring.show-exceptions/wrap
+ (ring.lint/wrap
+ (ring.file-info/wrap
+ (ring.lint/wrap
+ (ring.file/wrap (File. "src/ring/examples/public")
+ (ring.lint/wrap
+ (ring.reloading/wrap '(ring.dump)
+ ring.dump/app)))))))))
+
+(ring.jetty/run {:port 8080} app)
BIN src/ring/examples/public/clojure.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
49 src/ring/file.clj
@@ -0,0 +1,49 @@
+(ns ring.file
+ (:use clojure.contrib.except
+ ring.utils)
+ (:import (java.io File)))
+
+(defn- ensure-dir
+ "Ensures that the given directory exists."
+ [dir]
+ (throw-if-not (.exists dir)
+ "Directory does not exist: %s" dir))
+
+(defn- forbidden
+ []
+ {:status 403
+ :headers {"Content-Type" "text/html"}
+ :body "<html><body><h1>403 Forbidden</h1></body></html>"})
+
+(defn- success
+ [file]
+ {:status 200 :headers {} :body file})
+
+(defn- maybe-file
+ "Returns the File corresponding to the given relative path within the given
+ dir if it exists, or nil if no such file exists."
+ [dir path]
+ (let [file (File. dir path)]
+ (and (.exists file) (.canRead file) file)))
+
+(defn wrap
+ "Wrap an app such that a given directory is checked for a static file
+ with which to respond to the request, proxying the request to the
+ wrapped app if such a file does not exist."
+ ([dir app]
+ (ensure-dir dir)
+ (fn [req]
+ (if (#{:get :head} (:request-method req))
+ (let [uri (url-decode (:uri req))]
+ (if (str-includes? ".." uri)
+ (forbidden)
+ (let [path (cond
+ (.endsWith "/" uri) (str uri "index.html")
+ (re-match? #"\.[a-z]+$" uri) uri
+ :else (str uri ".html"))]
+ (if-let [file (maybe-file dir path)]
+ (success file)
+ (app req)))))
+ (app req))))
+ ([dir]
+ (partial wrap dir)))
79 src/ring/file_info.clj
@@ -0,0 +1,79 @@
+(ns ring.file-info
+ (:use clojure.contrib.def)
+ (:import org.apache.commons.io.FilenameUtils
+ java.io.File))
+
+(defvar- mime-type-map
+ {"ai" "application/postscript"
+ "asc" "text/plain"
+ "avi" "video/x-msvideo"
+ "bin" "application/octet-stream"
+ "bmp" "image/bmp"
+ "class" "application/octet-stream"
+ "cer" "application/pkix-cert"
+ "crl" "application/pkix-crl"
+ "crt" "application/x-x509-ca-cert"
+ "css" "text/css"
+ "dms" "application/octet-stream"
+ "doc" "application/msword"
+ "dvi" "application/x-dvi"
+ "eps" "application/postscript"
+ "etx" "text/x-setext"
+ "exe" "application/octet-stream"
+ "gif" "image/gif"
+ "htm" "text/html"
+ "html" "text/html"
+ "jpe" "image/jpeg"
+ "jpeg" "image/jpeg"
+ "jpg" "image/jpeg"
+ "js" "text/javascript"
+ "lha" "application/octet-stream"
+ "lzh" "application/octet-stream"
+ "mov" "video/quicktime"
+ "mpe" "video/mpeg"
+ "mpeg" "video/mpeg"
+ "mpg" "video/mpeg"
+ "pbm" "image/x-portable-bitmap"
+ "pdf" "application/pdf"
+ "pgm" "image/x-portable-graymap"
+ "png" "image/png"
+ "pnm" "image/x-portable-anymap"
+ "ppm" "image/x-portable-pixmap"
+ "ppt" "application/vnd.ms-powerpoint"
+ "ps" "application/postscript"
+ "qt" "video/quicktime"
+ "ras" "image/x-cmu-raster"
+ "rb" "text/plain"
+ "rd" "text/plain"
+ "rtf" "application/rtf"
+ "sgm" "text/sgml"
+ "sgml" "text/sgml"
+ "tif" "image/tiff"
+ "tiff" "image/tiff"
+ "txt" "text/plain"
+ "xbm" "image/x-xbitmap"
+ "xls" "application/vnd.ms-excel"
+ "xml" "text/xml"
+ "xpm" "image/x-xpixmap"
+ "xwd" "image/x-xwindowdump"
+ "zip" "application/zip"})
+
+(defn- guess-mime-type
+ "Returns a String corresponding to the guessed mime type for the given file,
+ or application/octet-stream if a type cannot be guessed."
+ [#^File file]
+ (get mime-type-map (FilenameUtils/getExtension (.getPath file))
+ "application/octet-stream"))
+
+(defn wrap
+ "Wrap an app such that responses with a file a body will have
+ corresponding Content-Type and Content-Length headers added if they are not
+ allready present and can be determined from the file."
+ [app]
+ (fn [req]
+ (let [{:keys [headers body] :as response} (app req)]
+ (if (instance? File body)
+ (assoc response :headers
+ (assoc headers "Content-Length" (str (.length body))
+ "Content-Type" (guess-mime-type body)))
+ response))))
87 src/ring/jetty.clj
@@ -0,0 +1,87 @@
+(ns ring.jetty
+ (:import (javax.servlet.http HttpServletRequest HttpServletResponse)
+ (org.mortbay.jetty.handler AbstractHandler)
+ (org.mortbay.jetty Server)
+ (java.io File FileInputStream InputStream OutputStream)
+ (org.apache.commons.io IOUtils))
+ (:use (clojure.contrib fcase except)))
+
+(defn- req-map
+ "Returns a map representing the given Servlet request, to be passed as the
+ Ring request to an app."
+ [#^HttpServletRequest request]
+ {:server-port (.getServerPort request)
+ :server-name (.getServerName request)
+ :remote-addr (.getRemoteAddr request)
+ :uri (.getRequestURI request)
+ :query-string (.getQueryString request)
+ :scheme (keyword (.getScheme request))
+ :request-method (keyword (.toLowerCase (.getMethod request)))
+ :headers (reduce
+ (fn [header-map #^String header-name]
+ (assoc header-map
+ (.toLowerCase header-name)
+ (.getHeader request header-name)))
+ {}
+ (enumeration-seq (.getHeaderNames request)))
+ :content-type (.getContentType request)
+ :content-length (let [len (.getContentLength request)]
+ (if (>= len 0) len))
+ :character-encoding (.getCharacterEncoding request)
+ :body (.getInputStream request)})
+
+(defn- apply-response-map
+ "Apply the given response map to the servlet response, therby completing
+ the HTTP response."
+ [#^HttpServletResponse response {:keys [status headers body]}]
+ ; Apply the status.
+ (.setStatus response status)
+ ; Apply the headers.
+ (doseq [[key val-or-vals] headers]
+ (if (string? val-or-vals)
+ (.setHeader response key val-or-vals)
+ (doseq [val val-or-vals]
+ (.addHeader response key val))))
+ ; Apply the body - the method depends on the given body type.
+ (cond
+ (string? body)
+ (with-open [writer (.getWriter response)]
+ (.println writer body))
+ (instance? InputStream body)
+ (let [#^InputStream in body]
+ (with-open [out (.getOutputStream response)]
+ (IOUtils/copy in out)
+ (.close in)
+ (.flush out)))
+ (instance? File body)
+ (let [#^File f body]
+ (with-open [fin (FileInputStream. f)]
+ (with-open [out (.getOutputStream response)]
+ (IOUtils/copy fin out)
+ (.flush out))))
+ (nil? body)
+ nil
+ :else
+ (throwf "Unreceognized body: %s" body)))
+
+(defn- proxy-handler
+ "Returns an Handler implementation for the given app."
+ [app]
+ (proxy [AbstractHandler] []
+ (handle [target request response dispatch]
+ (let [req (req-map request)
+ tuple (app req)]
+ (apply-response-map response tuple)
+ (.setHandled request true)))))
+
+(defn run
+ "Serve the given app according to the options.
+ Options:
+ :port, an Integer."
+ [options app]
+ (let [port (or (:port options) (throwf ":port missing from options"))
+ server (doto (Server. port) (.setSendDateHeader true))
+ handler (proxy-handler app)]
+ (.setHandler server handler)
+ (.start server)
+ (.join server)))
108 src/ring/lint.clj
@@ -0,0 +1,108 @@
+(ns ring.lint
+ (:use clojure.set clojure.contrib.except)
+ (:import (java.io File InputStream)))
+
+(defn lint
+ "Asserts that spec applied to val returns logical truth, otherwise raises
+ an exception with a message produced by applying format to the message-pattern
+ argument and a printing of an invalid val."
+ [val spec message]
+ (try
+ (if-not (spec val)
+ (throwf "Ring lint error: specified %s, but %s was not" message (pr-str val)))
+ (catch Exception e
+ (if-not (re-find #"^Ring lint error: " (.getMessage e))
+ (throwf
+ "Ring lint error: exception occured when checking that %s on %s: %s"
+ message (pr-str val) (.getMessage e))
+ (throw e)))))
+
+(defn lint-namespacing
+ "Asserts that all keys are namespaces other than those included in a
+ specified set of permitted unnamspaced keys"
+ [map map-name no-namespace-needed]
+ (let [must-namespace (difference (set (keys map)) no-namespace-needed)]
+ (doseq [k must-namespace]
+ (lint k namespace
+ (format "user keys in the %s map must be namespaced" map-name)))))
+
+(defn check-req
+ "Validates the request, throwing an exception on violations of the spec"
+ [req]
+ (lint req map?
+ "Ring request must a Clojure map")
+
+ (lint (:server-port req) integer?
+ ":server-port must be an Integer")
+ (lint (:server-name req) string?
+ ":server-name must be a String")
+ (lint (:remote-addr req) string?
+ ":remote-addr must be a String")
+ (lint (:uri req) #(and (string? %) (.startsWith % "/"))
+ ":uri must be a String starting with \"/\"")
+ (lint (:query-string req) #(or (nil? %) (string? %))
+ ":query-string must be nil or a non-blank String")
+ (lint (:scheme req) #{:http :https}
+ ":scheme must be one of :http or :https")
+ (lint (:request-method req) #{:get :head :options :put :post :delete}
+ ":request-method must be one of :get, :head, :options, :put, :post, or :delete")
+ (lint (:content-type req) #(or (nil? %) (string? %))
+ ":content-type must be nil or a String")
+ (lint (:content-length req) #(or (nil? %) (integer? %))
+ ":content-length must be nil or an Integer")
+ (lint (:character-encoding req) #(or (nil? %) (string? %))
+ ":character-encoding must be nil or a String")
+
+ (let [headers (:headers req)]
+ (lint headers map?
+ ":headers must be a Clojure map")
+ (doseq [[hname hval] headers]
+ (lint hname string?
+ "header names must be Strings")
+ (lint hname #(= % (.toLowerCase %))
+ "header names must be in lower case")
+ (lint hval string?
+ "header values must be strings")))
+
+ (lint (:body req) #(or (nil? %) (instance? InputStream %))
+ ":body must be nil or an InputStream")
+
+ (lint-namespacing req "request"
+ #{:server-port :server-name :remote-addr :uri :query-string :scheme
+ :request-method :content-type :content-length :character-encoding
+ :headers :body}))
+
+(defn check-resp
+ "Validates the response, throwing an exception on violations of the spec"
+ [resp]
+ (lint resp map?
+ "Ring response must be a Clojure map")
+
+ (lint (:status resp) #(and (integer? %) (>= % 100))
+ ":status must be an Intger greater than or equal to 100")
+
+ (let [headers (:headers resp)]
+ (lint headers map?
+ ":headers must be a Clojure map")
+ (doseq [[hname hval] headers]
+ (lint hname string?
+ "header names must Strings")
+ (lint hval #(or (string? %) (every? string? %))
+ "header values must be Strings or colls of Strings")))
+
+ (lint (:body resp) #(or (nil? %) (string? %) (instance? File %)
+ (instance? InputStream %))
+ ":body must a String, File, or InputStream")
+
+ (lint-namespacing resp "response"
+ #{:status :headers :body}))
+
+(defn wrap
+ "Wrap an app to validate incoming requests and outgoing responses
+ according to the Ring spec."
+ [app]
+ (fn [req]
+ (check-req req)
+ (let [resp (app req)]
+ (check-resp resp)
+ resp)))
10 src/ring/reloading.clj
@@ -0,0 +1,10 @@
+(ns ring.reloading)
+
+(defn wrap
+ "Wrap an app such that before a request is passed to the app, each namespace
+ identified by syms in reloadables is reloaded."
+ [reloadables app]
+ (fn [req]
+ (doseq [ns-sym reloadables]
+ (require ns-sym :reload))
+ (app req)))
93 src/ring/show_exceptions.clj
@@ -0,0 +1,93 @@
+(ns ring.show-exceptions
+ (:use (clj-html core utils helpers)
+ clojure.contrib.str-utils
+ (clj-backtrace core repl)
+ ring.utils))
+
+(def css "
+/*
+Copyright (c) 2008, Yahoo! Inc. All rights reserved.
+Code licensed under the BSD License:
+http://developer.yahoo.net/yui/license.txt
+version: 2.6.0
+*/
+html{color:#000;background:#FFF;}body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,textarea,p,blockquote,th,td{margin:0;padding:0;}table{border-collapse:collapse;border-spacing:0;}fieldset,img{border:0;}address,caption,cite,code,dfn,em,strong,th,var{font-style:normal;font-weight:normal;}li{list-style:none;}caption,th{text-align:left;}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal;}q:before,q:after{content:'';}abbr,acronym{border:0;font-variant:normal;}sup{vertical-align:text-top;}sub{vertical-align:text-bottom;}input,textarea,select{font-family:inherit;font-size:inherit;font-weight:inherit;}input,textarea,select{*font-size:100%;}legend{color:#000;}del,ins{text-decoration:none;}
+
+
+h3.info {
+ font-size: 1.5em;
+ margin-left: 1em;
+ padding-top: .5em;
+ padding-bottom: .5em;
+}
+
+table.trace {
+ margin-left: 1em;
+ background: lightgrey;
+}
+
+table.trace tr {
+ line-height: 1.4em;
+}
+
+table.trace td.method {
+ padding-left: .5em;
+ text-aligh: left;
+}
+
+table.trace td.source {
+ text-align: right;
+}")
+
+(defn- js-response [req e]
+ {:status 500
+ :headers {"Content-Type" "text/javascript"}
+ :body (pst-str e)})
+
+(defn- html-reponse [req e]
+ (let [excp (parse-exception e)]
+ {:status 500
+ :headers {"Content-Type" "text/html"}
+ :body
+ (html
+ (doctype :xhtml-transitional)
+ [:html {:xmlns "http://www.w3.org/1999/xhtml"}
+ [:head
+ [:meta {:http-equiv "Content-Type" :content "text/html;charset=utf-8"}]
+ [:title "Ring: Show Exceptions"]
+ [:style {:type "text/css"} css]
+ [:body
+ [:div#content
+ [:h3.info (h (str e))]
+ [:table.trace
+ (domap-str [parsed (:trace-elems excp)]
+ (html
+ [:tbody
+ (if (:clojure parsed)
+ (html
+ [:tr
+ [:td.source (h (source-str parsed))]
+ [:td.method (h (clojure-method-str parsed))]])
+ (html
+ [:tr
+ [:td.source (h (source-str parsed))]
+ [:td.method (h (java-method-str parsed))]]))]))]]]]])}))
+
+(defn- response
+ "Returns a response showing debugging information about the exception.
+ Currently supports delegation to either js or html exception views."
+ [req e]
+ (let [accept (get-in req [:headers "accept"])]
+ (if (and accept (re-match? #"^text/javascript" accept))
+ (js-response req e)
+ (html-reponse req e))))
+
+(defn wrap
+ "Wrap an app such that exceptions thrown within the wrapped app are caught
+ and a helpful debugging response is returned."
+ [app]
+ (fn [req]
+ (try
+ (app req)
+ (catch Exception e
+ (response req e)))))
25 src/ring/utils.clj
@@ -0,0 +1,25 @@
+(ns ring.utils
+ (:use clojure.contrib.str-utils))
+
+(defn url-decode
+ "Returns the form-url-decoded version of the given string."
+ [encoded]
+ (java.net.URLDecoder/decode encoded "UTF-8"))
+
+(defn str-includes?
+ "Returns logical truth iff the given target appears in the given string"
+ [target string]
+ (<= 0 (.indexOf string target)))
+
+(defn re-match?
+ "Returns true iff the given string contains a match for the given pattern."
+ [#^java.util.regex.Pattern pattern string]
+ (.find (.matcher pattern string)))
+
+(defn re-get
+ "Returns the nth captured group resulting from matching the given pattern
+ against the given string, or nil if no match is found."
+ [re n s]
+ (let [m (re-matcher re s)]
+ (if (.find m)
+ (.group m n))))
1 test/ring/assets/foo.html
@@ -0,0 +1 @@
+foo
1 test/ring/assets/index.html
@@ -0,0 +1 @@
+index
1 test/ring/assets/plain.txt
@@ -0,0 +1 @@
+plain
1 test/ring/assets/random.xyz
@@ -0,0 +1 @@
+random
10 test/ring/builder_test.clj
@@ -0,0 +1,10 @@
+(ns ring.builder-test
+ (:use clj-unit.core ring.builder))
+
+(deftest "wrap-if"
+ (let [core-app inc
+ wrapper-fn (fn [app] (fn [req] (+ 2 (app req))))
+ unwrapped (wrap-if false wrapper-fn core-app)
+ wrapped (wrap-if true wrapper-fn core-app)]
+ (assert= 1 (unwrapped 0))
+ (assert= 3 (wrapped 0))))
18 test/ring/dump_test.clj
@@ -0,0 +1,18 @@
+(ns ring.dump-test
+ (:use clj-unit.core ring.dump)
+ (:import java.io.ByteArrayInputStream))
+
+(def post-req
+ {:uri "/foo/bar"
+ :request-method :post
+ :body (ByteArrayInputStream. (.getBytes "post body"))})
+
+(def get-req
+ {:uri "/foo/bar"
+ :request-method :get})
+
+(deftest "app"
+ (let [{:keys [status]} (app post-req)]
+ (assert= 200 status))
+ (let [{:keys [status]} (app get-req)]
+ (assert= 200 status)))
26 test/ring/file_info_test.clj
@@ -0,0 +1,26 @@
+(ns ring.file-info-test
+ (:use clj-unit.core ring.file-info)
+ (:import java.io.File))
+
+(def non-file-app (wrap (constantly {:headers {} :body "body"})))
+
+(def known-file (File. "test/ring/assets/plain.txt"))
+(def known-file-app (wrap (constantly {:headers {} :body known-file})))
+
+(def unknown-file (File. "test/ring/assets/random.xyz"))
+(def unknown-file-app (wrap (constantly {:headers {} :body unknown-file})))
+
+(deftest "wrap: non-file response"
+ (assert= {:headers {} :body "body"} (non-file-app {})))
+
+(deftest "wrap: known file response"
+ (assert=
+ {:headers {"Content-Type" "text/plain" "Content-Length" "6"}
+ :body known-file}
+ (known-file-app {})))
+
+(deftest "wrap: unknown file resonse"
+ (assert=
+ {:headers {"Content-Type" "application/octet-stream" "Content-Length" "7"}
+ :body unknown-file}
+ (unknown-file-app {})))
42 test/ring/file_test.clj
@@ -0,0 +1,42 @@
+(ns ring.file-test
+ (:use clj-unit.core ring.file)
+ (:import java.io.File))
+
+(deftest "wrap: no directory"
+ (assert-throws #"Directory does not exist"
+ (wrap (File. "not_here") (constantly :response))))
+
+(def public-dir (File. "test/ring/assets"))
+(def index-html (File. public-dir "index.html"))
+(def foo-html (File. public-dir "foo.html"))
+
+(def app (wrap public-dir (constantly :response)))
+
+(deftest "wrap: unsafe method"
+ (assert= :response (app {:request-method :post :uri "/foo"})))
+
+(deftest "wrap: forbidden url"
+ (let [{:keys [status body]} (app {:request-method :get :uri "/../foo"})]
+ (assert= 403 status)
+ (assert-match #"Forbidden" body)))
+
+(deftest "wrap: directory"
+ (let [{:keys [status headers body]} (app {:request-method :get :uri "/"})]
+ (assert= 200 status)
+ (assert= {} headers)
+ (assert= index-html body)))
+
+(deftest "wrap: file without extension"
+ (let [{:keys [status headers body]} (app {:request-method :get :uri "/foo"})]
+ (assert= 200 status)
+ (assert= {} headers)
+ (assert= foo-html body)))
+
+(deftest "wrap: file with extension"
+ (let [{:keys [status headers body]} (app {:request-method :get :uri "/foo.html"})]
+ (assert= 200 status)
+ (assert= {} headers)
+ (assert= foo-html body)))
+
+(deftest "wrap: no file"
+ (assert= :response (app {:request-method :get :uri "/dynamic"})))
129 test/ring/lint_test.clj
@@ -0,0 +1,129 @@
+(ns ring.lint-test
+ (:use clj-unit.core ring.lint)
+ (:import (java.io File InputStream ByteArrayInputStream)))
+
+(defn str-input-stream
+ "Returns a ByteArrayInputStream for the given String."
+ [string]
+ (ByteArrayInputStream. (.getBytes string)))
+
+(def valid-request
+ {:server-port 80
+ :server-name "localhost"
+ :remote-addr "192.0.2.235"
+ :uri "/"
+ :query-string nil
+ :scheme :http
+ :request-method :get
+ :headers {}
+ :content-type nil
+ :content-length nil
+ :character-encoding nil
+ :body nil})
+
+(def valid-response
+ {:status 200 :headers {} :body "valid"})
+
+(def valid-response-app
+ (wrap (fn [req] valid-response)))
+
+(defn constant-app
+ [constant-response]
+ (wrap (fn [req] constant-response)))
+
+
+(defmacro lints-req
+ [key goods bads]
+ (let [good-name (str "request " key " valid values")
+ bad-name (str "request " key " invalid values")]
+ `(do
+ (deftest ~good-name
+ (doseq [good# ~goods]
+ (assert-truth (= valid-response
+ (valid-response-app (assoc valid-request ~key good#)))
+ (format "%s is a valid value for request key %s"
+ (pr-str good#) ~key))))
+ (deftest ~bad-name
+ (doseq [bad# ~bads]
+ (assert-throws #"Ring lint error:"
+ (valid-response-app (assoc valid-request ~key bad#))))))))
+
+(defmacro lints-resp
+ [key goods bads]
+ (let [good-name (str "response " key " valid values")
+ bad-name (str "response " key " invalid values")]
+ `(do
+ (deftest ~good-name
+ (doseq [good# ~goods]
+ (let [response# (assoc valid-response ~key good#)
+ app# (constant-app response#)]
+ (assert-truth (= response# (app# valid-request))
+ (format "%s is a valid value for response key %s"
+ (pr-str good#) ~key)))))
+ (deftest ~bad-name
+ (doseq [bad# ~bads]
+ (let [response# (assoc valid-response ~key bad#)
+ app# (constant-app response#)]
+ (assert-throws #"Ring lint error:"
+ (app# valid-request))))))))
+
+(lints-req :server-port
+ [80 8080]
+ [nil "80"])
+
+(lints-req :server-name
+ ["localhost" "www.amazon.com" "192.0.2.235"]
+ [nil 123])
+
+(lints-req :remote-addr
+ ["192.0.2.235" "0:0:0:0:0:0:0:1%0"]
+ [nil 123])
+
+(lints-req :uri
+ ["/" "/foo" "/foo/bar"]
+ [nil ""])
+
+(lints-req :query-string
+ [nil "" "foo=bar" "foo=bar&biz=bat"]
+ [:foo])
+
+(lints-req :scheme
+ [:http :https]
+ [nil "http"])
+
+(lints-req :request-method
+ [:get :head :options :put :post :delete]
+ [nil :foobar "get"])
+
+(lints-req :content-type
+ [nil "text/html"]
+ [:text/html])
+
+(lints-req :content-length
+ [nil 123]
+ ["123"])
+
+(lints-req :character-encoding
+ [nil "UTF-8"]
+ [:utf-8])
+
+(lints-req :headers
+ [{"foo" "bar"} {"bat" "Biz"} {"whiz-bang" "high-low"}]
+ [nil {:foo "bar"} {"bar" :foo} {"Bar" "foo"}])
+
+(lints-req :body
+ [nil (str-input-stream "thebody")]
+ ["thebody" :thebody])
+
+
+(lints-resp :status
+ [100 301 500]
+ [nil "100" 99])
+
+(lints-resp :headers
+ [{} {"foo" "bar"} {"Biz" "Bat"} {"vert" ["high" "low"]} {"horz" #{"left right"}}]
+ [nil {:foo "bar"} {"foo" :bar} {"dir" 123}])
+
+(lints-resp :body
+ [nil "thebody" (str-input-stream "thebody") (File. "test/ring/assets/foo.html")]
+ [123 :thebody])
8 test/ring/reloading_test.clj
@@ -0,0 +1,8 @@
+(ns ring.reloading-test
+ (:use clj-unit.core ring.reloading))
+
+(def app (constantly :response))
+
+(deftest "wrap"
+ (let [wrapped (wrap '(ring.reloading) app)]
+ (assert= :response (wrapped :request))))
15 test/ring/show_exceptions_test.clj
@@ -0,0 +1,15 @@
+(ns ring.show-exceptions-test
+ (:use clj-unit.core ring.show-exceptions))
+
+(def app (wrap #(throw (Exception. "fail"))))
+
+(def html-req {})
+(def js-req {:headers {"accept" "text/javascript"}})
+
+(deftest "wrap"
+ (let [{:keys [status headers] :as response} (app html-req)]
+ (assert= 500 status)
+ (assert= {"Content-Type" "text/html"} headers))
+ (let [{:keys [status headers]} (app js-req)]
+ (assert= 500 status)
+ (assert= {"Content-Type" "text/javascript"} headers)))
9 test/run.clj
@@ -0,0 +1,9 @@
+(use 'clj-unit.core)
+(require-and-run-tests
+ 'ring.builder-test
+ 'ring.dump-test
+ 'ring.lint-test
+ 'ring.file-test
+ 'ring.file-info-test
+ 'ring.reloading-test
+ 'ring.show-exceptions-test)

0 comments on commit 68e08fb

Please sign in to comment.
Something went wrong with that request. Please try again.