noir.cookies documentation
Stateful access to cookie values
3 |
get
(get k)
(get k default)
Get the value of a cookie from the request. k can either be a string or keyword.
4 | If this is a signed cookie, use get-signed, otherwise the signature will not be
5 | checked.
get-signed
(get-signed sign-key k)
(get-signed sign-key k default)
Get the value of a cookie from the request using 'get'. Verifies that a signing
6 | cookie also exists. If not, returns default or nil.
put!
(put! k v)
Add a new cookie whose name is k and has the value v. If v is a string
7 | a cookie map is created with :path '/'. To set custom attributes, such as
8 | "expires", provide a map as v. Stores all keys as strings.
put-signed!
(put-signed! sign-key k v)
Adds a new cookie whose name is k and has the value v. In addition,
9 | adds another cookie that checks the authenticity of 'v'. Sign-key
10 | should be a secret that's user-wide, session-wide or site wide (worst).
--------------------------------------------------------------------------------
/doc/noir.core.html:
--------------------------------------------------------------------------------
1 |
2 | noir.core documentation
Functions to work with partials and pages.
3 |
compojure-route
(compojure-route compojure-func)
Adds a compojure route fn to the end of the route table. These routes are queried after
4 | those created by defpage and before the generic catch-all and resources routes.
5 |
6 | These are primarily used to integrate generated routes from other libs into Noir.
custom-handler
(custom-handler & args)
Adds a handler to the end of the route table. This is equivalent to writing
7 | a compojure route using noir's [:method route] syntax.
8 |
9 | (custom-handler [:post "/login"] {:as req} (println "hello " req))
10 | => (POST "/login" {:as req} (println "hello" req))
11 |
12 | These are primarily used to interface with other handler generating libraries, i.e. async aleph handlers.
custom-handler*
(custom-handler* route func)
Adds a handler to the end of the route table. This is equivalent to writing
13 | a compojure route using noir's [:method route] syntax, but allows functions
14 | to be created dynamically:
15 |
16 | (custom-handler* [:post "/login"] (fn [params] (println req)))
17 |
18 | These are primarily used to interface with other dynamic handler generating libraries
defpage
(defpage & args)
Adds a route to the server whose content is the the result of evaluating the body.
19 | The function created is passed the params of the request and the destruct param allows
20 | you to destructure that meaningfully for use in the body.
21 |
22 | There are several supported forms:
23 |
24 | (defpage "/foo/:id" {id :id}) an unnamed route
25 | (defpage [:post "/foo/:id"] {id :id}) a route that responds to POST
26 | (defpage foo "/foo:id" {id :id}) a named route
27 | (defpage foo [:post "/foo/:id"] {id :id})
28 |
29 | The default method is GET.
defpartial
(defpartial fname params & body)
Create a function that returns html using hiccup. The function is callable with the given name.
30 |
post-route
(post-route & args)
Adds a route to the end of the route table and passes the entire request to
31 | be desctructured and used in the body. These routes are guaranteed to be
32 | evaluated after those created by defpage and before the generic catch-all and
33 | resources routes.
pre-route
(pre-route & args)
Adds a route to the beginning of the route table and passes the entire request
34 | to be destructured and used in the body. These routes are the only ones to make
35 | an ordering gaurantee. They will always be in order of ascending specificity (e.g. /* ,
36 | /admin/* , /admin/user/*) Pre-routes are usually used for filtering, like redirecting
37 | a section based on privileges:
38 |
39 | (pre-route '/admin/*' {} (when-not (is-admin?) (redirect '/login')))
render
(render route & [params])
Renders the content for a route by calling the page like a function
40 | with the given param map. Accepts either '/vals' or [:post '/vals']
url-for
(url-for route & [arg-map])
given a named route, i.e. (defpage foo "/foo/:id"), returns the url for the
41 | route. If the route takes arguments, the second argument must be a
42 | map of route arguments to values
43 |
44 | (url-for foo {:id 3}) => "/foo/3"
--------------------------------------------------------------------------------
/doc/noir.exception.html:
--------------------------------------------------------------------------------
1 |
2 | noir.exception documentation
Functions to handle exceptions within a Noir server gracefully.
3 |
--------------------------------------------------------------------------------
/doc/noir.options.html:
--------------------------------------------------------------------------------
1 |
2 | noir.options documentation
Allows access to Noir's server options
3 |
dev-mode?
(dev-mode?)
Returns if the server is currently in development mode
4 |
get
(get k default)
(get k)
Get an option from the noir options map
5 |
--------------------------------------------------------------------------------
/doc/noir.request.html:
--------------------------------------------------------------------------------
1 |
2 | noir.request documentation
Functions for accessing the original request object from within noir handlers
3 |
ring-request
(ring-request)
Returns back the current ring request map
4 |
--------------------------------------------------------------------------------
/doc/noir.response.html:
--------------------------------------------------------------------------------
1 |
2 | noir.response documentation
Simple response helpers to change the content type, redirect, or return a canned response
3 |
content-type
(content-type ctype content)
Wraps the response with the given content type and sets the body to the content.
4 |
empty
(empty)
Return a successful, but completely empty response
5 |
json
(json content)
Wraps the response in the json content type and generates JSON from the content
6 |
jsonp
(jsonp func-name content)
Generates JSON for the given content and creates a javascript response for calling
7 | func-name with it.
redirect
(redirect url)
A header redirect to a different url
8 |
status
(status code content)
Wraps the content in the given status code
13 |
xml
(xml content)
Wraps the response with the content type for xml and sets the body to the content.
14 |
--------------------------------------------------------------------------------
/doc/noir.server.handler.html:
--------------------------------------------------------------------------------
1 |
2 | noir.server.handler documentation
Handler generation functions used by noir.server and other ring handler libraries.
3 |
add-custom-middleware
(add-custom-middleware func & args)
Add a middleware function to all noir handlers.
4 |
base-handler
(base-handler & [opts])
Get the most basic Noir request handler, only adding wrap-custom-middleware and wrap-request-map.
5 |
wrap-noir-middleware
(wrap-noir-middleware handler opts)
Wrap a base handler in all of noir's middleware
6 |
wrap-spec-routes
(wrap-spec-routes handler opts)
Wrap a handler in noir's resource and catch-all routes.
7 |
--------------------------------------------------------------------------------
/doc/noir.server.html:
--------------------------------------------------------------------------------
1 |
2 | noir.server documentation
A collection of functions to handle Noir's server and add middleware to the stack.
3 |
add-middleware
(add-middleware func & args)
Add a middleware function to the noir server. Func is a standard ring middleware
4 | function, which will be passed the handler. Any extra args to be applied should be
5 | supplied along with the function.
gen-handler
(gen-handler & [opts])
Get a full Noir request handler for use with plugins like lein-ring or lein-beanstalk.
6 | If used in a definition, this must come after views have been loaded to ensure that the
7 | routes have already been added to the route table.
load-views
(load-views & dirs)
Require all the namespaces in the given dir so that the pages are loaded
8 | by the server.
load-views-ns
(load-views-ns & ns-syms)
Require all the namespaces prefixed by the namespace symbol given so that the pages
9 | are loaded by the server.
restart
(restart server)
Restart a noir server
10 |
start
(start port & [opts])
Create a noir server bound to the specified port with a map of options and return it.
11 | The available options are:
12 |
13 | :mode - either :dev or :prod
14 | :ns - the root namepace of your project
15 | :jetty-options - any extra options you want to send to jetty like :ssl?
16 | :base-url - the root url to prepend to generated links and resources
17 | :resource-options - a map of options for the resources route (:root or :mime-types)
18 | :session-store - an alternate store for session handling
19 | :session-cookie-attrs - custom session cookie attributes
stop
(stop server)
Stop a noir server
20 |
wrap-route
(wrap-route route middleware & args)
Add a middleware function to a specific route. Route is a standard route you would
21 | use for defpage, func is a ring middleware function, and args are any additional args
22 | to pass to the middleware function. You can wrap the resources and catch-all routes by
23 | supplying the routes :resources and :catch-all respectively:
24 |
25 | (wrap-route :resources some-caching-middleware)
--------------------------------------------------------------------------------
/doc/noir.session.html:
--------------------------------------------------------------------------------
1 |
2 | noir.session documentation
Stateful session handling functions. Uses a memory-store by
3 | default, but can use a custom store by supplying a :session-store
4 | option to server/start.
clear!
(clear!)
Remove all data from the session and start over cleanly.
5 |
flash-get
(flash-get k)
(flash-get k not-found)
Retrieve the flash stored value.
6 |
flash-put!
(flash-put! k v)
Store a value that will persist for this request and the next.
7 |
get
(get k)
(get k default)
Get the key's value from the session, returns nil if it doesn't exist.
8 |
get!
(get! k)
(get! k default)
Destructive get from the session. This returns the current value of the key
9 | and then removes it from the session.
put!
(put! k v)
Associates the key with the given value in the session
10 |
remove!
(remove! k)
Remove a key from the session
11 |
swap!
(swap! f & args)
Replace the current session's value with the result of executing f with
12 | the current value and args.
--------------------------------------------------------------------------------
/doc/noir.statuses.html:
--------------------------------------------------------------------------------
1 |
2 | noir.statuses documentation
If no pages are defined that match a request, a status page is used based on the
3 | the HTTP status code of the response. This contains the function necessary to get
4 | or set these status pages.
get-page
(get-page code)
Gets the content to display for the given status code
5 |
set-page!
(set-page! code content)
Sets the content to be displayed if there is a response with the given status
6 | code. This is used for custom 404 pages, for example.
--------------------------------------------------------------------------------
/doc/noir.util.crypt.html:
--------------------------------------------------------------------------------
1 |
2 | noir.util.crypt documentation
Simple functions for hashing strings and comparing them. Typically used for storing passwords.
3 |
compare
(compare raw encrypted)
Compare a raw string with an already encrypted string
4 |
encrypt
(encrypt salt raw)
(encrypt raw)
Encrypt the given string with a generated or supplied salt. Uses BCrypt for strong hashing.
5 |
--------------------------------------------------------------------------------
/doc/noir.util.gae.html:
--------------------------------------------------------------------------------
1 |
2 | noir.util.gae documentation
Functions to help run noir on Google App Engine.
3 |
gae-handler
(gae-handler opts)
Create a Google AppEngine friendly handler for Noir. Use this instead
4 | of server/gen-handler for AppEngine projects.
--------------------------------------------------------------------------------
/doc/noir.util.test.html:
--------------------------------------------------------------------------------
1 |
2 | noir.util.test documentation
A set of utilities for testing a Noir project
3 |
has-body
(has-body resp cont)
Asserts that the response has the given body
4 |
has-content-type
(has-content-type resp ct)
Asserts that the response has the given content type
5 |
has-status
(has-status resp stat)
Asserts that the response has the given status
6 |
send-request
(send-request route & [params])
Send a request to the Noir handler. Unlike with-noir, this will run
7 | the request within the context of all middleware.
send-request-map
(send-request-map ring-req)
Send a ring-request map to the noir handler.
8 |
with-noir
(with-noir & body)
Executes the body within the context of Noir's bindings
9 |
--------------------------------------------------------------------------------
/doc/noir.validation.html:
--------------------------------------------------------------------------------
1 |
2 | noir.validation documentation
Functions for validating input and setting string errors on fields.
3 | All fields are simply keys, meaning this can be a general error storage and
4 | retrieval mechanism for the lifetime of a single request. Errors are not
5 | persisted and are cleaned out at the end of the request.
errors?
(errors? & field)
For all fields given return true if any field contains errors. If none of the fields
6 | contain errors, return false. If no fields are supplied return true if any errors exist.
get-errors
(get-errors & [field])
Get the errors for the given field. This will return a vector of all error strings or nil.
7 |
has-value?
(has-value? v)
Returns true if v is truthy and not an empty string.
8 |
has-values?
(has-values? coll)
Returns true if all members of the collection has-value? This works on maps as well.
9 |
is-email?
(is-email? v)
Returns true if v is an email address
10 |
max-length?
(max-length? v len)
Returns true if v is less than or equal to the given len
11 |
min-length?
(min-length? v len)
Returns true if v is greater than or equal to the given len
12 |
not-nil?
(not-nil? v)
Returns true if v is not nil
13 |
on-error
(on-error field func)
If the given field has an error, execute func and return its value
14 |
rule
(rule passed? [field error])
If the passed? condition is not met, add the error text to the given field:
15 | (rule (not-nil? username) [:username "Usernames must have a value."])
set-error
(set-error field error)
Explicitly set an error for the given field. This can be used to
16 | create complex error cases, such as in a multi-step login process.
--------------------------------------------------------------------------------
/history.md:
--------------------------------------------------------------------------------
1 | ##Changes for 1.3.0-beta1
2 | * BREAKING CHANGE: flashes now last the length of one non-resource request
3 | * BREAKING CHANGE: switched to the latest hiccup: form-helpers and page-helpers have been split apart. See http://github.com/weavejester/hiccup for more details.
4 | * BREAKING CHANGE: clj-json has been replaced with cheshire.
5 | * BREAKING CHANGE: noir.util.middleware was removed as wrap-utf8 is done by default in ring.
6 | * BREAKING CHANGE: noir.util.s3 has been removed. See https://github.com/weavejester/clj-aws-s3 for a replacement.
7 | * Added noir.session/get! to have a destructive get like the old flashes
8 | * Added noir.server/wrap-route to wrap middleware around specific routes
9 | * Added noir.core/custom-handler* for adding dynamic route functions to the routing table.
10 | * Added noir.util.test/send-request-map for sending full ring maps
11 | * Refactored noir.server so that the jetty dependency can be excluded.
12 | * Refactored noir.response/* so that all functions compose
13 | * Fixed all generated content-types are utf-8
14 |
15 | ##Changes for 1.2.2
16 | * Added an argless form of (noir.validation/errors?) that returns all errors
17 | * Added the ability to define routes with vars
18 | * Refactored defpage to allow for better errors when a param is passed incorrectly
19 | * Refactored url-for to be more robust
20 | * Fixed s3 var being dynamic
21 | * Fixed an issue with utf-8 routes being encoded incorrectly
22 | * Moved to ring 1.0.1 and compojure 1.0.0
23 | * Fixes issue with no routes being loaded resulting in a 500
24 | * Fixes issue with file names containing spaces being unreachable
25 |
26 | ##Changes for 1.2.1
27 | * BREAKING CHANGE: (url-for) now takes a map of params instead of key-value pairs: (url-for foo {:id 2})
28 | * Changed noir.content.pages to noir.content.getting-started
29 | * Added noir.response/jsonp
30 | * Added :base-url option to noir.server so that you can run noir at different root urls
31 | * Added noir.session/swap! to do atomic updates to the session
32 | * Updated noir.content to be prettier/more informative
33 | * Fixed pre-route to use ANY by default
34 | * Fixed issue that cause complex pre-routes not work
35 | * Fixed a couple of doc strings to be clearer
36 | * Refactored the way noir.core parses urls for routes to be significantly simpler
37 | * Removed cssgen dependency
38 | * Moved to latest Ring
39 |
40 | ##Changes for 1.2.0
41 |
42 | * Refactored for Clojure 1.3.0 support
43 | * Refactored server to enable custom noir handler creation
44 | * Added url decoding for routes. (defpage "/hey how" ...) will work now.
45 | * Added noir.util.gae to get Noir up on Google App Engine
46 | * Added named routes
47 | * Added noir.request/ring-request
48 | * Added url-for to query named routes
49 | * Added noir.server/load-view-ns
50 | * Added a :resource-root option to the server
51 | * Added a :cookie-attrs option to the server
52 | * Added post-route
53 | * Added signed cookies
54 | * Added compojure-route and custom-handler to handle integration with other libs
55 | * Changed noir.validation/errors? will now return if any errors exist if no fields are supplied.
56 | * Fixed noir.validation/is-email? to use a better regex
57 | * Fixed and improved noir.util.s3
58 | * Fixed incorrect header setting for noir.response/xml
59 | * Fixed custom middleware preserves order
60 | * Fixed bugs in cookie handling that would cause incorrect retrieval
61 | * Fixed some issues with exceptions to make the 500 page more resilient
62 | * Moved to latest compojure/ring/hiccup
63 | * Added tons of tests
64 |
65 |
66 | ##Changes for 1.1.0
67 |
68 | * Added session/flash-put! and sesion/flash-get
69 | * Added alternative session storage via the :session-store server option
70 | * Removed dependency on contrib
71 | * Added defaults for session/get and cookies/get
72 | * Added gen-handler for interop with other ring-based libraries
73 | * Added test utilities under noir.util.test
74 | * Added noir.util.middleware
75 | * Moved to latest compojure/ring/hiccup
76 | * Added server/stop server/restart
77 | * Fixed bug where server/start wasn't returning a server object
78 |
--------------------------------------------------------------------------------
/project.clj:
--------------------------------------------------------------------------------
1 | (defproject noir "1.3.0"
2 | :description "Noir - a clojure web framework"
3 | :license {:name "Eclipse Public License - v 1.0"
4 | :url "http://www.eclipse.org/legal/epl-v10.html"}
5 | :url "http://webnoir.org"
6 | :codox {:exclude [noir.exception noir.content.defaults
7 | noir.content.getting-started]}
8 | :dependencies [[org.clojure/clojure "1.4.0"]
9 | [lib-noir "0.2.0"]
10 | [compojure "1.1.3"]
11 | [bultitude "0.2.0"]
12 | [ring "1.1.6"]
13 | [hiccup "1.0.2"]
14 | [clj-stacktrace "0.2.5"]
15 | [org.clojure/tools.macro "0.1.1"]])
16 |
--------------------------------------------------------------------------------
/resources/public/css/noir.css:
--------------------------------------------------------------------------------
1 | body {
2 | background: url(../img/noir-bg.png);
3 | color: #d1d9e1;
4 | padding: 60px 80px;
5 | font-family: 'Helvetica Neue',Helvetica,Verdana;
6 | }
7 | #wrapper {
8 | margin: 0 auto;
9 | width: 1000px;
10 | }
11 | #content {
12 | float: left;
13 | display: inline;
14 | width: 100%;
15 | padding-bottom: 100px;
16 | }
17 | a {
18 | text-decoration: underline;
19 | color: #91979d;
20 | }
21 | a:hover {
22 | color: #6bffbd;
23 | }
24 | h1 {
25 | margin-bottom: 0px;
26 | color: #d1d9e1;
27 | }
28 | h2 {
29 | margin-top: 10px;
30 | margin-left: 60px;
31 | font-size: 18px;
32 | font-weight: normal;
33 | }
34 | code {
35 | float: left;
36 | display: inline;
37 | border-radius: 8px;
38 | padding: 10px;
39 | background: #474949;
40 | border: 2px solid #616363;
41 | font-family: Monaco, Consolas, 'Courier New';
42 | }
43 | .announce {
44 | float: left;
45 | display: inline;
46 | width: 970px;
47 | text-align: center;
48 | font-size: 20px;
49 | margin-top: 20px;
50 | margin-bottom: 110px;
51 | border-radius: 8px;
52 | padding: 10px;
53 | background: #3f634d;
54 | border: 2px solid #3c8455;
55 | padding: 15px;
56 | }
57 | #header {
58 | float: left;
59 | display: inline;
60 | width: 100%;
61 | margin-bottom: 50px;
62 | }
63 | #header h1 {
64 | float: left;
65 | display: inline;
66 | }
67 |
68 | #header ul {
69 | float: right;
70 | display: inline;
71 | list-style: none;
72 | margin-top: 30px;
73 | }
74 | #header ul li {
75 | float: left;
76 | display: inline;
77 | }
78 | #header ul li a {
79 | text-decoration: none;
80 | float: left;
81 | display: inline;
82 | border-radius: 8px;
83 | padding: 10px;
84 | background: #474949;
85 | border: 2px solid #616363;
86 | padding: 8px;
87 | margin-left: 10px;
88 | }
89 | #header ul li a:hover {
90 | background: #3f634d;
91 | border: 2px solid #3c8455;
92 | }
93 | ul.content {
94 | float: left;
95 | display: inline;
96 | }
97 | ul.content li {
98 | float: left;
99 | display: inline;
100 | margin-bottom: 55px;
101 | width: 100%;
102 | }
103 | ul.content li .left {
104 | float: left;
105 | display: inline;
106 | width: 55%;
107 | text-align: left;
108 | }
109 | ul.content li .left p {
110 | padding: 0;
111 | margin: 0;
112 | font-size: 18px;
113 | }
114 |
115 | ul.content li .right {
116 | float: left;
117 | display: inline;
118 | margin-right: 5%;
119 | width: 40%;
120 | }
121 | ul.content li .right code {
122 | width: 100%;
123 | }
124 |
125 | ul.content li .right p {
126 | max-width: 440px;
127 | }
128 | #not-found {
129 | text-align: center;
130 | width: 600px;
131 | margin: 0px auto;
132 | margin-top: 200px;
133 | }
134 | #not-found h1 {
135 | color: #6bffbd;
136 | font-size: 32px;
137 | margin-bottom: 20px;
138 | }
139 | #exception {
140 | max-width: 900px;
141 | }
142 | #exception h1 {
143 | font-size: 24px;
144 | }
145 |
146 | #exception ul {
147 | margin: 0;
148 | padding: 0;
149 | margin-top: 20px;
150 | list-style: none;
151 | }
152 |
153 | #exception table {
154 | width: 100%;
155 | margin-top: 20px;
156 | border-collapse: collapse;
157 | }
158 |
159 | #exception tr {
160 | border-radius: 8px;
161 | padding: 10px;
162 | background: #474949;
163 | border: 2px solid #616363;
164 | margin-bottom: 10px;
165 | width: 100%;
166 | }
167 |
168 | #exception td {
169 | padding: 10px;
170 | }
171 |
172 | #exception .dt {
173 | text-align: right;
174 | }
175 |
176 | #exception .dd {
177 | color: #91979d;
178 | margin: 0;
179 | padding-left: 5%;
180 | }
181 |
182 | #exception h1 span {
183 | font-size: 18px;
184 | font-weight: normal;
185 | color: #91979d;
186 | }
187 |
188 | #exception .mine {
189 | border-radius: 8px;
190 | padding: 10px;
191 | background: #3f634d;
192 | border: 2px solid #3c8455;
193 | }
194 |
--------------------------------------------------------------------------------
/resources/public/css/reset.css:
--------------------------------------------------------------------------------
1 | html {
2 | margin:0;
3 | padding:0;
4 | border:0;
5 | }
6 |
7 | body, div, span, object, iframe,
8 | h1, h2, h3, h4, h5, h6, p, blockquote, pre,
9 | a, abbr, acronym, address, code,
10 | del, dfn, em, img, q, dl, dt, dd, ol, ul, li,
11 | fieldset, form, label, legend,
12 | table, caption, tbody, tfoot, thead, tr, th, td,
13 | article, aside, dialog, figure, footer, header,
14 | hgroup, nav, section {
15 | margin: 0;
16 | padding: 0;
17 | border: 0;
18 | font-weight: inherit;
19 | font-style: inherit;
20 | font-size: 100%;
21 | font-family: inherit;
22 | vertical-align: baseline;
23 | }
24 |
25 | article, aside, dialog, figure, footer, header,
26 | hgroup, nav, section {
27 | display:block;
28 | }
29 |
30 | body {
31 | line-height: 1.5;
32 | background: white;
33 | }
34 |
35 | table {
36 | border-collapse: separate;
37 | border-spacing: 0;
38 | }
39 |
40 | caption, th, td {
41 | text-align: left;
42 | font-weight: normal;
43 | float:none !important;
44 | }
45 | table, th, td {
46 | vertical-align: middle;
47 | }
48 |
49 | blockquote:before, blockquote:after, q:before, q:after { content: ''; }
50 | blockquote, q { quotes: "" ""; }
51 |
52 | a img { border: none; }
53 |
54 | /*:focus { outline: 0; }*/
55 |
56 |
57 |
--------------------------------------------------------------------------------
/resources/public/img/noir-bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/noir-clojure/noir/bd3d13430ab75613b753bbc1e31a1b5f737fb3b6/resources/public/img/noir-bg.png
--------------------------------------------------------------------------------
/resources/public/img/noir-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/noir-clojure/noir/bd3d13430ab75613b753bbc1e31a1b5f737fb3b6/resources/public/img/noir-logo.png
--------------------------------------------------------------------------------
/src/noir/content/defaults.clj:
--------------------------------------------------------------------------------
1 | (ns
2 | #^{:skip-wiki true}
3 | noir.content.defaults
4 | (:use noir.core
5 | hiccup.core
6 | hiccup.page))
7 |
8 | (defpartial noir-layout [& content]
9 | (html5
10 | [:head
11 | [:title "Noir"]
12 | (include-css "/css/reset.css")
13 | (include-css "/css/noir.css")]
14 | [:body
15 | [:div#wrapper
16 | [:div#content
17 | content]]]))
18 |
19 | (defpartial min-noir-layout [& content]
20 | (html5
21 | [:head
22 | [:title "Noir"]
23 | (include-css "/css/reset.css")
24 | (include-css "/css/noir.css")]
25 | [:body
26 | content]))
27 |
28 | (defpartial not-found []
29 | (min-noir-layout
30 | [:div#not-found
31 | [:h1 "We seem to have lost that one."]
32 | [:p "Since we couldn't find the page you were looking for, check to make sure the address is correct."]]))
33 |
34 | (defpartial exception-item [{nams :ns in-ns? :in-ns? fq :fully-qualified f :file line :line :as ex}]
35 | [:tr {:class (when in-ns?
36 | "mine")}
37 | [:td.dt f " :: " line]
38 | [:td.dd fq]])
39 |
40 | (defpartial stack-trace [{exception :exception causes :causes}]
41 | (noir-layout
42 | [:div#exception
43 | [:h1 (or (:message exception) "An exception was thrown") [:span " - (" (:class exception) ")"]]
44 | [:table [:tbody (map exception-item (:trace-elems exception))]]
45 | (for [cause causes :while cause]
46 | [:div.cause
47 | (try
48 | [:h3 "Caused by: " (:class cause) " - " (:message cause)]
49 | [:table (map exception-item (:trimmed-elems cause))]
50 | (catch Throwable e))])]))
51 |
52 | (defpartial internal-error []
53 | (min-noir-layout
54 | [:div#not-found
55 | [:h1 "Something very bad has happened."]
56 | [:p "We've dispatched a team of highly trained gnomes to take
57 | care of the problem."]]))
58 |
--------------------------------------------------------------------------------
/src/noir/content/getting_started.clj:
--------------------------------------------------------------------------------
1 | (ns
2 | #^{:skip-wiki true}
3 | noir.content.getting-started
4 | (:use noir.core
5 | noir.content.defaults
6 | hiccup.element
7 | hiccup.page))
8 |
9 | (def header-links [{:url "http://www.webnoir.org/tutorials" :text "Tutorials"}
10 | {:url "http://groups.google.com/group/clj-noir" :text "Google Group"}
11 | {:url "http://www.webnoir.org/docs/" :text "API"}])
12 |
13 | (defpartial link [{:keys [url text]}]
14 | (link-to url text))
15 |
16 | (defpartial link-item [lnk]
17 | [:li
18 | (link lnk)])
19 |
20 | (defpartial logo []
21 | (link-to "http://www.webnoir.org/" (image "/img/noir-logo.png" "Noir")))
22 |
23 | (defpartial header []
24 | [:div#header
25 | [:h1 (logo)]
26 | [:ul
27 | (map link-item header-links)]])
28 |
29 | (defpage "/" []
30 | (noir-layout
31 | (header)
32 | [:p.announce "Noir is up and running... time to start building some websites!"]
33 | [:ul.content
34 | [:li
35 | [:div.right
36 | [:pre
37 | [:code
38 | "(defpage \"/my-page\" []
39 | (html5
40 | [:h1 \"This is my first page!\"]))"]]]
41 | [:div.left
42 | [:p "Time to get going with our first page. Let's open views/welcome.clj
43 | and use (defpage) to add a new page to our site. With that we can go to "
44 | (link-to "/my-page" "/my-page")
45 | " and see our handiwork."]]]
46 |
47 | [:li
48 | [:div.right
49 | [:pre
50 | [:code
51 | "(defpartial site-layout [& content]
52 | (html5
53 | [:head
54 | [:title \"my site\"]]
55 | [:body
56 | [:div#wrapper
57 | content]]))"]]]
58 | [:div.left
59 | [:p "We really need a layout for all our pages, so let's create a
60 | partial (a function that returns html). We'll do that
61 | in views/common.clj since all your views will use it."]]]
62 |
63 | [:li
64 | [:div.right
65 | [:pre
66 | [:code
67 | "(defpage \"/my-page\" []
68 | (common/site-layout
69 | [:h1 \"Welcome to my site!\"]
70 | [:p \"Hope you like it.\"]))"]]]
71 | [:div.left
72 | [:p "Now we'll update our page to use the layout. Just refresh the browser
73 | and you'll see your change."]]]
74 |
75 | [:li
76 | [:div.right
77 | [:code "[noir.content.getting-started]"]]
78 | [:div.left
79 | [:p "That's it! You've created your own page. Now get rid of this one simply by
80 | removing the require for getting-started at the top."]]]]))
81 |
--------------------------------------------------------------------------------
/src/noir/core.clj:
--------------------------------------------------------------------------------
1 | (ns noir.core
2 | "Functions to work with partials and pages."
3 | (:use hiccup.core
4 | compojure.core)
5 | (:require [clojure.string :as string]
6 | [clojure.tools.macro :as macro]))
7 |
8 | (defonce noir-routes (atom {}))
9 | (defonce route-funcs (atom {}))
10 | (defonce pre-routes (atom (sorted-map)))
11 | (defonce post-routes (atom []))
12 | (defonce compojure-routes (atom []))
13 |
14 | (defn- keyword->symbol [namesp kw]
15 | (symbol namesp (string/upper-case (name kw))))
16 |
17 | (defn- route->key [action rte]
18 | (let [action (string/replace (str action) #".*/" "")]
19 | (str action (-> rte
20 | (string/replace #"\." "!dot!")
21 | (string/replace #"/" "--")
22 | (string/replace #":" ">")
23 | (string/replace #"\*" "<")))))
24 |
25 | (defn- throwf [msg & args]
26 | (throw (Exception. (apply format msg args))))
27 |
28 | (defn- parse-fn-name [[cur :as all]]
29 | (let [[fn-name remaining] (if (and (symbol? cur)
30 | (or (@route-funcs (keyword (name cur)))
31 | (not (resolve cur))))
32 | [cur (rest all)]
33 | [nil all])]
34 | [{:fn-name fn-name} remaining]))
35 |
36 | (defn- parse-route [[{:keys [fn-name] :as result} [cur :as all]] default-action]
37 | (let [cur (if (symbol? cur)
38 | (try
39 | (deref (resolve cur))
40 | (catch Exception e
41 | (throwf "Symbol given for route has no value")))
42 | cur)]
43 | (when-not (or (vector? cur) (string? cur))
44 | (throwf "Routes must either be a string or vector, not a %s" (type cur)))
45 | (let [[action url] (if (vector? cur)
46 | [(keyword->symbol "compojure.core" (first cur)) (second cur)]
47 | [default-action cur])
48 | final (-> result
49 | (assoc :fn-name (if fn-name
50 | fn-name
51 | (symbol (route->key action url))))
52 | (assoc :url url)
53 | (assoc :action action))]
54 | [final (rest all)])))
55 |
56 | (defn- parse-destruct-body [[result [cur :as all]]]
57 | (when-not (some true? (map #(% cur) [vector? map? symbol?]))
58 | (throwf "Invalid destructuring param: %s" cur))
59 | (-> result
60 | (assoc :destruct cur)
61 | (assoc :body (rest all))))
62 |
63 | (defn ^{:skip-wiki true} parse-args
64 | "parses the arguments to defpage. Returns a map containing the keys :fn-name :action :url :destruct :body"
65 | [args & [default-action]]
66 | (-> args
67 | (parse-fn-name)
68 | (parse-route (or default-action 'compojure.core/GET))
69 | (parse-destruct-body)))
70 |
71 | (defn ^{:skip-wiki true} route->name
72 | "Parses a set of route args into the keyword name for the route"
73 | [route]
74 | (cond
75 | (keyword? route) route
76 | (fn? route) (keyword (:name (meta route)))
77 | :else (let [res (first (parse-route [{} [route]] 'compojure.core/GET))]
78 | (keyword (:fn-name res)))))
79 |
80 | (defmacro defpage
81 | "Adds a route to the server whose content is the the result of evaluating the body.
82 | The function created is passed the params of the request and the destruct param allows
83 | you to destructure that meaningfully for use in the body.
84 |
85 | There are several supported forms:
86 |
87 | (defpage \"/foo/:id\" {id :id}) an unnamed route
88 | (defpage [:post \"/foo/:id\"] {id :id}) a route that responds to POST
89 | (defpage foo \"/foo:id\" {id :id}) a named route
90 | (defpage foo [:post \"/foo/:id\"] {id :id})
91 |
92 | The default method is GET."
93 | [& args]
94 | (let [{:keys [fn-name action url destruct body]} (parse-args args)]
95 | `(do
96 | (defn ~fn-name {::url ~url
97 | ::action (quote ~action)
98 | ::args (quote ~destruct)} [~destruct]
99 | ~@body)
100 | (swap! route-funcs assoc ~(keyword fn-name) ~fn-name)
101 | (swap! noir-routes assoc ~(keyword fn-name) (~action ~url {params# :params} (~fn-name params#))))))
102 |
103 | (defmacro defpartial
104 | "Create a function that returns html using hiccup. The function is callable with the given name. Can optionally include a docstring or metadata map, like a normal function declaration."
105 | [fname & args]
106 | (let [[fname args] (macro/name-with-attributes fname args)
107 | [params & body] args]
108 | `(defn ~fname ~params
109 | (html
110 | ~@body))))
111 |
112 | (defn ^{:skip-wiki true} route-arguments
113 | "returns the list of route arguments in a route"
114 | [route]
115 | (let [args (re-seq #"/(:([^\/]+)|\*)" route)]
116 | (set (map #(keyword (or (nth % 2) (second %))) args))))
117 |
118 | (defn url-for* [url route-args]
119 | (let [url (if (vector? url) ;;handle complex routes
120 | (first url)
121 | url)
122 | route-arg-names (route-arguments url)]
123 | (when-not (every? (set (keys route-args)) route-arg-names)
124 | (throwf "Missing route-args %s" (vec (filter #(not (contains? route-args %)) route-arg-names))))
125 | (reduce (fn [path [k v]]
126 | (if (= k :*)
127 | (string/replace path "*" (str v))
128 | (string/replace path (str k) (str v))))
129 | url
130 | route-args)))
131 |
132 | (defn url-for-fn* [route-fn route-args]
133 | (let [url (-> route-fn meta ::url)]
134 | (when-not url
135 | (throwf "No url metadata on %s" route-fn))
136 | (url-for* url route-args)))
137 |
138 | (defmacro url-for
139 | "Given a named route, e.g. (defpage foo \"/foo/:id\"), returns the url for the
140 | route. If the route takes arguments, the second argument must be a
141 | map of route arguments to values
142 |
143 | (url-for foo {:id 3}) => \"/foo/3\" "
144 | ([route & [arg-map]]
145 | (let [cur-ns *ns*
146 | route (if (symbol? route)
147 | `(ns-resolve ~cur-ns (quote ~route))
148 | `(delay ~route))]
149 | `(let [var# ~route]
150 | (cond
151 | (string? @var#) (url-for* @var# ~arg-map)
152 | (vector? @var#) (url-for* (second @var#) ~arg-map)
153 | (fn? @var#) (url-for-fn* var# ~arg-map)
154 | :else (throw (Exception. (str "Unknown route type: " @var#))))))))
155 |
156 | (defn render
157 | "Renders the content for a route by calling the page like a function
158 | with the given param map. Accepts either '/vals' or [:post '/vals']"
159 | [route & [params]]
160 | (if (fn? route)
161 | (route params)
162 | (let [rname (route->name route)
163 | func (get @route-funcs rname)]
164 | (func params))))
165 |
166 | (defmacro pre-route
167 | "Adds a route to the beginning of the route table and passes the entire request
168 | to be destructured and used in the body. These routes are the only ones to make
169 | an ordering guarantee. They will always be in order of ascending specificity (e.g. /* ,
170 | /admin/* , /admin/user/*) Pre-routes are usually used for filtering, like redirecting
171 | a section based on privileges:
172 |
173 | (pre-route '/admin/*' {} (when-not (is-admin?) (redirect '/login')))"
174 | [& args]
175 | (let [{:keys [action destruct url body]} (parse-args args 'compojure.core/ANY)
176 | safe-url (if (vector? url)
177 | (first url)
178 | url)]
179 | `(swap! pre-routes assoc ~safe-url (~action ~url {:as request#} ((fn [~destruct] ~@body) request#)))))
180 |
181 | (defmacro post-route
182 | "Adds a route to the end of the route table and passes the entire request to
183 | be destructured and used in the body. These routes are guaranteed to be
184 | evaluated after those created by defpage and before the generic catch-all and
185 | resources routes."
186 | [& args]
187 | (let [{:keys [action destruct url body fn-name]} (parse-args args)]
188 | `(swap! post-routes conj [~(keyword fn-name) (~action ~url {:as request#} ((fn [~destruct] ~@body) request#))])))
189 |
190 | (defn compojure-route
191 | "Adds a compojure route fn to the end of the route table. These routes are queried after
192 | those created by defpage and before the generic catch-all and resources routes.
193 |
194 | These are primarily used to integrate generated routes from other libs into Noir."
195 | [compojure-func]
196 | (swap! compojure-routes conj compojure-func))
197 |
198 | (defmacro custom-handler
199 | "Adds a handler to the end of the route table. This is equivalent to writing
200 | a compojure route using noir's [:method route] syntax.
201 |
202 | (custom-handler [:post \"/login\"] {:as req} (println \"hello \" req))
203 | => (POST \"/login\" {:as req} (println \"hello\" req))
204 |
205 | These are primarily used to interface with other handler generating libraries, i.e. async aleph handlers."
206 | [& args]
207 | (let [{:keys [action destruct url body]} (parse-args args)]
208 | `(compojure-route (~action ~url ~destruct ~@body))))
209 |
210 | (defn custom-handler*
211 | "Adds a handler to the end of the route table. This is equivalent to writing
212 | a compojure route using noir's [:method route] syntax, but allows functions
213 | to be created dynamically:
214 |
215 | (custom-handler* [:post \"/login\"] (fn [params] (println params)))
216 |
217 | These are primarily used to interface with other dynamic handler generating libraries"
218 | [route func]
219 | (let [[{:keys [action url fn-name]}] (parse-route [{} [route]] 'compojure.core/GET)
220 | fn-key (keyword fn-name)]
221 | (swap! route-funcs assoc fn-key func)
222 | (swap! noir-routes assoc fn-key (eval `(~action ~url {params# :params} (~func params#))))))
223 |
--------------------------------------------------------------------------------
/src/noir/exception.clj:
--------------------------------------------------------------------------------
1 | (ns noir.exception
2 | "Functions to handle exceptions within a Noir server gracefully."
3 | (:use clj-stacktrace.core
4 | clj-stacktrace.repl)
5 | (:require [clojure.string :as string]
6 | [noir.options :as options]
7 | [noir.statuses :as statuses]
8 | [noir.content.defaults :as defaults]))
9 |
10 | (defn- route-fn? [k]
11 | (and k
12 | (re-seq #".*--" k)))
13 |
14 | (defn- key->route-fn [k]
15 | (if (route-fn? k)
16 | (let [with-slahes (-> k
17 | (string/replace #"!dot!" ".")
18 | (string/replace #"--" "/")
19 | (string/replace #">" ":")
20 | (string/replace #"<" "*"))
21 | separated (string/replace with-slahes #"(POST|GET|HEAD|ANY|PUT|DELETE)" #(str (first %1) " :: "))]
22 | separated)
23 | k))
24 |
25 | (defn- ex-item [{anon :annon-fn func :fn nams :ns clj? :clojure f :file line :line :as ex}]
26 | (let [func-name (if (and anon func (re-seq #"eval" func))
27 | "anon [fn]"
28 | (key->route-fn func))
29 | ns-str (if clj?
30 | (if (route-fn? func)
31 | (str nams " :: " func-name)
32 | (str nams "/" func-name))
33 | (str (:method ex) "." (:class ex)))
34 | in-ns? (and nams (re-seq
35 | (re-pattern (str (options/get :ns)))
36 | nams))]
37 | {:fn func-name
38 | :ns nams
39 | :in-ns? in-ns?
40 | :fully-qualified ns-str
41 | :annon? anon
42 | :clj? clj?
43 | :file f
44 | :line line}))
45 |
46 | (defn parse-ex [ex]
47 | (let [clj-parsed (iterate :cause (parse-exception ex))
48 | exception (first clj-parsed)
49 | causes (rest clj-parsed)]
50 | {:exception (assoc exception :trace-elems (map ex-item (:trace-elems exception)))
51 | :causes (for [cause causes :while cause]
52 | (assoc cause :trimmed-elems (map ex-item (:trimmed-elems cause))))}))
53 |
54 | (defn wrap-exceptions [handler]
55 | (fn [request]
56 | (try
57 | (handler request)
58 | (catch Exception e
59 | (.printStackTrace e)
60 | (let [content (if (options/dev-mode?)
61 | (try
62 | (defaults/stack-trace (parse-ex e))
63 | (catch Throwable e
64 | (statuses/get-page 500)))
65 | (statuses/get-page 500))]
66 | {:status 500
67 | :headers {"Content-Type" "text/html"}
68 | :body content})))))
69 |
--------------------------------------------------------------------------------
/src/noir/options.clj:
--------------------------------------------------------------------------------
1 | (ns noir.options
2 | "Allows access to Noir's server options"
3 | (:refer-clojure :exclude [get]))
4 |
5 | (def ^:dynamic *options* nil)
6 | (def default-opts {:ns (gensym)
7 | :mode :dev})
8 |
9 | (defn compile-options
10 | [opts]
11 | (if (map? opts)
12 | (merge default-opts opts)
13 | default-opts))
14 |
15 | (defn get
16 | "Get an option from the noir options map"
17 | ([k default]
18 | (clojure.core/get *options* k default))
19 | ([k]
20 | (clojure.core/get *options* k)))
21 |
22 | (defn resolve-url [url]
23 | (str (get :base-url "") url))
24 |
25 | (defn dev-mode?
26 | "Returns if the server is currently in development mode"
27 | []
28 | (= (get :mode) :dev))
29 |
30 | (defn wrap-options [handler opts]
31 | (let [final-opts (compile-options opts)]
32 | (fn [request]
33 | (binding [*options* final-opts]
34 | (handler request)))))
35 |
--------------------------------------------------------------------------------
/src/noir/request.clj:
--------------------------------------------------------------------------------
1 | (ns noir.request
2 | "Functions for accessing the original request object from within noir handlers")
3 |
4 | (declare ^{:dynamic true} *request*)
5 |
6 | (defn ring-request
7 | "Returns back the current ring request map"
8 | []
9 | *request*)
10 |
11 | (defn wrap-request-map [handler]
12 | (fn [req]
13 | (binding [*request* req]
14 | (handler req))))
15 |
--------------------------------------------------------------------------------
/src/noir/server.clj:
--------------------------------------------------------------------------------
1 | (ns noir.server
2 | "A collection of functions to handle Noir's server and add middleware to the stack."
3 | (:use compojure.core
4 | [clojure.java.io :only [file]]
5 | [clojure.string :only [join]]
6 | [bultitude.core :only [namespaces-on-classpath]]
7 | [ring.middleware.multipart-params])
8 | (:require [compojure.handler :as compojure]
9 | [noir.server.handler :as handler]))
10 |
11 | (defn gen-handler
12 | "Get a full Noir request handler for use with plugins like lein-ring or lein-beanstalk.
13 | If used in a definition, this must come after views have been loaded to ensure that the
14 | routes have already been added to the route table."
15 | [& [opts]]
16 | (-> (handler/base-handler opts)
17 | (handler/wrap-noir-middleware opts)
18 | (handler/wrap-spec-routes opts)
19 | (compojure/api)
20 | (wrap-multipart-params)))
21 |
22 | (defn load-views
23 | "Require all the namespaces in the given dirs so that the pages are loaded
24 | by the server."
25 | [& dirs]
26 | (doseq [f (namespaces-on-classpath :classpath (map file dirs))]
27 | (require f)))
28 |
29 | (defn load-views-ns
30 | "Require all the namespaces prefixed by the namespace symbol given so that the pages
31 | are loaded by the server."
32 | [& ns-syms]
33 | (doseq [sym ns-syms
34 | f (namespaces-on-classpath :prefix (name sym))]
35 | (require f)))
36 |
37 | (defn add-middleware
38 | "Add a middleware function to the noir server. Func is a standard ring middleware
39 | function, which will be passed the handler. Any extra args to be applied should be
40 | supplied along with the function."
41 | [func & args]
42 | (apply handler/add-custom-middleware func args))
43 |
44 | (defn wrap-route
45 | "Add a middleware function to a specific route. Route is a standard route you would
46 | use for defpage, func is a ring middleware function, and args are any additional args
47 | to pass to the middleware function. You can wrap the resources and catch-all routes by
48 | supplying the routes :resources and :catch-all respectively:
49 |
50 | (wrap-route :resources some-caching-middleware)"
51 | [route middleware & args]
52 | (apply handler/wrap-route route middleware args))
53 |
54 | (defn start
55 | "Create a noir server bound to the specified port with a map of options and return it.
56 | The available options are:
57 |
58 | :mode - either :dev or :prod
59 | :ns - the root namepace of your project
60 | :jetty-options - any extra options you want to send to jetty like :ssl?
61 | :base-url - the root url to prepend to generated links and resources
62 | :resource-options - a map of options for the resources route (:root or :mime-types)
63 | :session-store - an alternate store for session handling
64 | :session-cookie-attrs - custom session cookie attributes"
65 | [port & [opts]]
66 | ;; to allow for jetty to be excluded as a dependency, it is included
67 | ;; here inline.
68 | (require 'ring.adapter.jetty)
69 | (println "Starting server...")
70 | (let [run-fn (resolve 'ring.adapter.jetty/run-jetty) ;; force runtime resolution of jetty
71 | jetty-opts (merge {:port port :join? false} (:jetty-options opts))
72 | server (run-fn (gen-handler opts) jetty-opts)]
73 | (println (str "Server started on port [" port "]."))
74 | (println (str "You can view the site at http://localhost:" port))
75 | server))
76 |
77 | (defn stop
78 | "Stop a noir server"
79 | [server]
80 | (.stop server))
81 |
82 | (defn restart
83 | "Restart a noir server"
84 | [server]
85 | (stop server)
86 | (.start server))
87 |
--------------------------------------------------------------------------------
/src/noir/server/handler.clj:
--------------------------------------------------------------------------------
1 | (ns noir.server.handler
2 | "Handler generation functions used by noir.server and other ring handler libraries."
3 | (:use [compojure.core :only [routes ANY]]
4 | ring.middleware.reload
5 | ring.middleware.flash
6 | ring.middleware.session.memory)
7 | (:import java.net.URLDecoder)
8 | (:require [compojure.route :as c-route]
9 | [hiccup.middleware :as hiccup]
10 | [noir.core :as noir]
11 | [noir.content.defaults :as defaults]
12 | [noir.cookies :as cookie]
13 | [noir.exception :as exception]
14 | [noir.request :as request]
15 | [noir.statuses :as statuses]
16 | [noir.options :as options]
17 | [noir.session :as session]
18 | [noir.validation :as validation]
19 | [clojure.string :as string]))
20 |
21 | (defonce middleware (atom []))
22 | (defonce wrappers (atom []))
23 |
24 | ;;***************************************************
25 | ;; Wrappers
26 | ;;***************************************************
27 |
28 | (defn wrappers-for [& urls]
29 | (let [url-set (set urls)]
30 | (group-by :url (filter #(url-set (:url %)) @wrappers))))
31 |
32 | (defn all-wrappers []
33 | (group-by :url @wrappers))
34 |
35 | (defn wrappers->fn [wrapped]
36 | (let [wrapped (if (coll? (first wrapped))
37 | wrapped
38 | [wrapped])]
39 | (apply comp (map :func (reverse wrapped)))))
40 |
41 | (defn try-wrap [ws route]
42 | (if ws
43 | (let [func (wrappers->fn ws)]
44 | (func route))
45 | route))
46 |
47 | (defn add-route-middleware [rts]
48 | (let [ws (all-wrappers)]
49 | (for [[route-name route] rts]
50 | (try-wrap (ws route-name) route))))
51 |
52 | (defn wrap-route [url func & params]
53 | (swap! wrappers conj {:url (noir/route->name url) :func #(apply func % params)}))
54 |
55 | ;;***************************************************
56 | ;; Other middleware
57 | ;;***************************************************
58 |
59 | (defn- wrap-route-updating [handler]
60 | (if (options/dev-mode?)
61 | (wrap-reload handler {:dirs ["src"]})
62 | handler))
63 |
64 | (defn- wrap-custom-middleware [handler]
65 | (reduce (fn [cur [func args]] (apply func cur args))
66 | handler
67 | (seq @middleware)))
68 |
69 | (defn- wrap-base-url-routing [handler]
70 | (fn [req]
71 | (let [path-info (or (:path-info req)
72 | (if-let [base-url (options/get :base-url)]
73 | (string/replace-first (:uri req) base-url "")
74 | (:uri req)))]
75 | (handler (assoc req :path-info path-info)))))
76 |
77 |
78 |
79 | ;;***************************************************
80 | ;; Route packing
81 | ;;***************************************************
82 |
83 | (defn- spec-routes []
84 | (let [ws (wrappers-for :resources :catch-all)
85 | resource-opts (merge {:root "public"} (options/get :resource-options {}))
86 | resources (c-route/resources "/" resource-opts)
87 | catch-all (ANY "*" [] {:status 404 :body nil})]
88 | [(try-wrap (:resources ws) resources)
89 | (try-wrap (:catch-all ws) catch-all)]))
90 |
91 | (defn- pack-routes []
92 | (apply routes (concat (add-route-middleware @noir/pre-routes)
93 | (add-route-middleware @noir/noir-routes)
94 | (add-route-middleware @noir/post-routes)
95 | @noir/compojure-routes)))
96 |
97 | (defn- init-routes [opts]
98 | (binding [options/*options* (options/compile-options opts)]
99 | (->
100 | (if (options/dev-mode?)
101 | (fn [request]
102 | ;; by doing this as a function we can ensure that any routes added as the
103 | ;; result of a modification are evaluated on the first reload.
104 | ((pack-routes) request))
105 | (pack-routes))
106 | (request/wrap-request-map)
107 | (wrap-custom-middleware))))
108 |
109 | (defn add-custom-middleware
110 | "Add a middleware function to all noir handlers."
111 | [func & args]
112 | (swap! middleware conj [func args]))
113 |
114 | (defn ^:private assoc-if [m k v]
115 | (if (not (nil? v))
116 | (assoc m k v)
117 | m))
118 |
119 | (defn wrap-noir-middleware
120 | "Wrap a base handler in all of noir's middleware"
121 | [handler opts]
122 | (binding [options/*options* (options/compile-options opts)]
123 | (-> handler
124 | (hiccup/wrap-base-url (options/get :base-url))
125 | (wrap-base-url-routing)
126 | (session/wrap-noir-flash)
127 | (session/wrap-noir-session
128 | (assoc-if {:store (options/get :session-store (memory-store session/mem))}
129 | :cookie-attrs (options/get :session-cookie-attrs)))
130 | (cookie/wrap-noir-cookies)
131 | (validation/wrap-noir-validation)
132 | (statuses/wrap-status-pages)
133 | (wrap-route-updating)
134 | (exception/wrap-exceptions)
135 | (options/wrap-options opts))))
136 |
137 | ;; We want to not wrap these particular routes in session and flash middleware.
138 | (defn wrap-spec-routes
139 | "Wrap a handler in noir's resource and catch-all routes."
140 | [handler opts]
141 | (routes handler
142 | (-> (apply routes (spec-routes))
143 | (cookie/wrap-noir-cookies)
144 | (validation/wrap-noir-validation)
145 | (hiccup/wrap-base-url (options/get :base-url))
146 | (statuses/wrap-status-pages)
147 | (exception/wrap-exceptions)
148 | (options/wrap-options opts))))
149 |
150 | (defn base-handler
151 | "Get the most basic Noir request handler, only adding wrap-custom-middleware and wrap-request-map."
152 | [& [opts]]
153 | (init-routes opts))
154 |
--------------------------------------------------------------------------------
/src/noir/statuses.clj:
--------------------------------------------------------------------------------
1 | (ns noir.statuses
2 | "If no pages are defined that match a request, a status page is used based on the
3 | the HTTP status code of the response. This contains the function necessary to get
4 | or set these status pages."
5 | (:require [noir.content.defaults :as defaults]
6 | [ring.util.response :as ring-resp]))
7 |
8 | (def status-pages (atom {404 (defaults/not-found)
9 | 500 (defaults/internal-error)}))
10 |
11 | (defn get-page
12 | "Gets the content to display for the given status code"
13 | [code]
14 | (get @status-pages code))
15 |
16 | (defn set-page!
17 | "Sets the content to be displayed if there is a response with the given status
18 | code. This is used for custom 404 pages, for example."
19 | [code content]
20 | (swap! status-pages assoc code content))
21 |
22 | (defn status-response [orig]
23 | (let [{:keys [status headers]} orig
24 | content (or (get-page status) (get-page 404))
25 | headers (merge {"Content-Type" "text/html; charset=utf-8"}
26 | headers)
27 | final (-> orig
28 | (assoc :headers headers)
29 | (assoc :body content))]
30 | final))
31 |
32 | (defn wrap-status-pages [handler]
33 | (fn [request]
34 | (let [{status :status body :body :as resp} (handler request)]
35 | (if (and
36 | resp
37 | (not= status 200)
38 | (not body))
39 | (status-response resp)
40 | resp))))
41 |
--------------------------------------------------------------------------------
/src/noir/util/gae.clj:
--------------------------------------------------------------------------------
1 | (ns noir.util.gae
2 | "Functions to help run noir on Google App Engine."
3 | (:use [ring.middleware params
4 | keyword-params
5 | nested-params])
6 | (:require [noir.server.handler :as handler]))
7 |
8 |
9 | (defn gae-handler
10 | "Create a Google AppEngine friendly handler for Noir. Use this instead
11 | of server/gen-handler for AppEngine projects."
12 | [opts]
13 | (-> (handler/base-handler opts)
14 | (wrap-keyword-params)
15 | (wrap-nested-params)
16 | (wrap-params)
17 | (handler/wrap-noir-middleware opts)
18 | (handler/wrap-spec-routes opts)))
19 |
--------------------------------------------------------------------------------
/src/noir/util/test.clj:
--------------------------------------------------------------------------------
1 | (ns noir.util.test
2 | "A set of utilities for testing a Noir project"
3 | (:use clojure.test
4 | [clojure.pprint :only [pprint]])
5 | (:require [noir.server :as server]
6 | [noir.session :as session]
7 | [noir.validation :as vali]
8 | [noir.cookies :as cookies]
9 | [noir.options :as options]))
10 |
11 | (def content-types {:json "application/json; charset=utf-8"
12 | :html "text/html"})
13 |
14 | (defmacro with-noir
15 | "Executes the body within the context of Noir's bindings"
16 | [& body]
17 | `(binding [options/*options* options/default-opts
18 | vali/*errors* (atom {})
19 | session/*noir-session* (atom {})
20 | session/*noir-flash* (atom {})
21 | cookies/*new-cookies* (atom {})
22 | cookies/*cur-cookies* (atom {})]
23 | ~@body))
24 |
25 | (defn has-content-type
26 | "Asserts that the response has the given content type"
27 | [resp ct]
28 | (is (= ct (get-in resp [:headers "Content-Type"])))
29 | resp)
30 |
31 | (defn has-status
32 | "Asserts that the response has the given status"
33 | [resp stat]
34 | (is (= stat (get resp :status)))
35 | resp)
36 |
37 | (defn has-body
38 | "Asserts that the response has the given body"
39 | [resp cont]
40 | (is (= cont (get resp :body)))
41 | resp)
42 |
43 | (defn- make-request [route & [params]]
44 | (let [[method uri] (if (vector? route)
45 | route
46 | [:get route])]
47 | {:uri uri :request-method method :params params}))
48 |
49 | (defn send-request
50 | "Send a request to the Noir handler. Unlike with-noir, this will run
51 | the request within the context of all middleware."
52 | [route & [params]]
53 | (let [handler (server/gen-handler options/*options*)]
54 | (handler (make-request route params))))
55 |
56 | (defn send-request-map
57 | "Send a ring-request map to the noir handler."
58 | [ring-req]
59 | (let [handler (server/gen-handler options/*options*)]
60 | (handler ring-req)))
61 |
62 |
63 | (defn print-state
64 | "Print the state of the noir server's routes/middleware/wrappers.
65 | If optional details? arg is truthy, show noir-routes, route-funcs,
66 | and status pages too."
67 | [& details?]
68 | (let [print-func (if details? pprint (comp println pprint sort keys))]
69 |
70 | (println "== Pre-Routes ==")
71 | (print-func @noir.core/pre-routes)
72 |
73 | (println "== Routes and Funcs ==")
74 | (print-func (merge-with vector @noir.core/noir-routes @noir.core/route-funcs))
75 |
76 | (println "== Post-Routes ==")
77 | (pprint @noir.core/post-routes)
78 |
79 | (println "== Compojure-Routes ==")
80 | (pprint @noir.core/compojure-routes)
81 |
82 | (println "== Middleware ==")
83 | (pprint @noir.server.handler/middleware)
84 |
85 | (println "== Wrappers ==")
86 | (pprint @noir.server.handler/wrappers)
87 |
88 | (println "== Memory Store ==")
89 | (pprint @noir.session/mem)
90 |
91 | (when details?
92 | (do (println "== Status Pages ==")
93 | (pprint @noir.statuses/status-pages)))))
94 |
95 |
--------------------------------------------------------------------------------
/test/noir/test/core.clj:
--------------------------------------------------------------------------------
1 | (ns noir.test.core
2 | (:use [noir.core]
3 | [compojure.core]
4 | [hiccup.core :only [html]]
5 | [hiccup.element :only [link-to]]
6 | [noir.util.test])
7 | (:use [clojure.test])
8 | (:require [noir.util.crypt :as crypt]
9 | [noir.server :as server]
10 | [noir.session :as session]
11 | [noir.request :as request]
12 | [noir.options :as options]
13 | [noir.response :as resp]
14 | [noir.cookies :as cookies]
15 | [noir.validation :as vali]))
16 |
17 | (deftest hashing
18 | (let [pass (crypt/encrypt "password")]
19 | (is (crypt/compare "password" pass))))
20 |
21 | (deftest session-get-default
22 | (with-noir
23 | (is (nil? (session/get :noir)))
24 | (is (= "noir" (session/get :noir "noir")))))
25 |
26 | (deftest cookies
27 | (with-noir
28 | (cookies/put! :noir2 "woo")
29 | (is (= "woo" (cookies/get :noir2)))
30 | (is (nil? (cookies/get :noir)))
31 | (is (= "noir" (cookies/get :noir "noir")))))
32 |
33 | (deftest cookies-get-signed
34 | (with-noir
35 | (is (nil? (cookies/get :noir3)))
36 | (cookies/put-signed! "s3cr3t-k3y" :noir3 "stored-value")
37 | ;; Check default behavior for bad keys.
38 | (is (nil? (cookies/get-signed "b4d-k3y" :noir3)))
39 | (is (= "noir" (cookies/get-signed "b4d-k3y" :noir3 "noir")))
40 | ;; Check retrieval of good value.
41 | (is (= "stored-value" (cookies/get-signed "s3cr3t-k3y" :noir3)))
42 | ;; Modify value,
43 | (cookies/put! :noir3 "changed-value")
44 | (is (nil? (cookies/get-signed "s3cr3t-k3y" :noir3)))))
45 |
46 | (deftest options-get-default
47 | (with-noir
48 | (is (nil? (options/get :noir)))
49 | (is (= "noir" (options/get :noir "noir")))))
50 |
51 | (deftest json-resp
52 | (with-noir
53 | (-> (resp/json {:noir "web"})
54 | (has-content-type (content-types :json))
55 | (has-body "{\"noir\":\"web\"}"))))
56 |
57 | (deftest flash-lifetime
58 | (with-noir
59 | (session/flash-put! :test "noir")
60 | (is (= "noir" (session/flash-get :test)))))
61 |
62 | (defpage "/test" {:keys [nme]}
63 | (str "Hello " nme))
64 |
65 | (defpage "/request" {}
66 | (let [req (request/ring-request)]
67 | (is req)
68 | (is (map? req))
69 | (is (:uri req))))
70 |
71 | (deftest request-middleware
72 | (send-request "/request"))
73 |
74 | (deftest route-test
75 | (-> (send-request "/test" {"nme" "chris"})
76 | (has-status 200)
77 | (has-body "Hello chris")))
78 |
79 | (deftest route-test
80 | (-> (send-request "/test" {"nme" "chris"})
81 | (has-status 200)
82 | (has-body "Hello chris")))
83 |
84 | (defpage "/test.json" []
85 | (resp/json {:json "text"}))
86 |
87 | (deftest route-dot-test
88 | (-> (send-request "/test.json")
89 | (has-status 200)
90 | (has-content-type "application/json; charset=utf-8")
91 | (has-body "{\"json\":\"text\"}")))
92 |
93 | (deftest parsing-defpage
94 | (is (= (parse-args '[foo "/" [] "hey"])
95 | {:fn-name 'foo
96 | :url "/"
97 | :action 'compojure.core/GET
98 | :destruct []
99 | :body '("hey")}))
100 | (is (= (parse-args '["/" [] "hey"])
101 | {:fn-name 'GET--
102 | :url "/"
103 | :action 'compojure.core/GET
104 | :destruct []
105 | :body '("hey")}))
106 | (is (= (parse-args '[foo [:post "/"] [] "hey"])
107 | {:fn-name 'foo
108 | :url "/"
109 | :action 'compojure.core/POST
110 | :destruct []
111 | :body '("hey")}))
112 | (is (= (parse-args '[[:post "/"] [] "hey" "blah"])
113 | {:fn-name 'POST--
114 | :url "/"
115 | :action 'compojure.core/POST
116 | :destruct []
117 | :body '("hey" "blah")}))
118 | (is (= (parse-args '["/test" {} "hey"])
119 | {:fn-name 'GET--test
120 | :url "/test"
121 | :action 'compojure.core/GET
122 | :destruct {}
123 | :body '("hey")}))
124 | (is (thrown? Exception (parse-args '["/" 3 3])))
125 | (is (thrown? Exception (parse-args '["/" '() 3])))
126 | (is (thrown? Exception (parse-args '[{} '() 3]))))
127 |
128 | (defpage "/utf" [] "ąčęė")
129 |
130 | (deftest url-for-before-def
131 | (is (= "/one-arg/5" (url-for route-one-arg {:id 5}))))
132 |
133 | (defpage foo "/foo" []
134 | "named-route")
135 |
136 | (pre-route "/pre" []
137 | (resp/status 403 "not allowed"))
138 |
139 | (pre-route "/pre_with_args/:id" {{:keys [id]} :params}
140 | (resp/status 403
141 | (str id " not allowed")))
142 |
143 | (post-route "/post-route" []
144 | (resp/status 403 "not allowed"))
145 |
146 | (defpage "/not-post-route" [] "success")
147 | (post-route "/not-post-route" [] "fail")
148 |
149 | (defpage "/pre" [] "you should never see this")
150 |
151 | (compojure-route (ANY "/compojure" [] "compojure-route"))
152 |
153 | (deftest pre-route-test
154 | (-> (send-request [:post "/pre"])
155 | (has-status 403)
156 | (has-body "not allowed"))
157 | (-> (send-request "/pre")
158 | (has-status 403)
159 | (has-body "not allowed"))
160 | (-> (send-request "/pre_with_args/1")
161 | (has-status 403)
162 | (has-body "1 not allowed")))
163 |
164 | (deftest compojure-route-test
165 | (-> (send-request "/compojure")
166 | (has-status 200)
167 | (has-body "compojure-route")))
168 |
169 | (deftest post-route-test
170 | (-> (send-request "/post-route")
171 | (has-status 403)
172 | (has-body "not allowed"))
173 | (-> (send-request "/not-post-route")
174 | (has-status 200)
175 | (has-body "success")))
176 |
177 | (deftest named-route-test
178 | (-> (send-request "/foo")
179 | (has-status 200)
180 | (has-body "named-route")))
181 |
182 | (defpage [:post "/post-route"] {:keys [nme]}
183 | (str "Post " nme))
184 |
185 | (deftest render-test
186 | (is (= "named-route" (render foo)))
187 | (is (= "Hello chris" (render "/test" {:nme "chris"})))
188 | (is (= "Post chris") (render [:post "/post-route"] {:nme "chris"})))
189 |
190 | (deftest route-post-test
191 | (-> (send-request [:post "/post-route"] {"nme" "chris"})
192 | (has-status 200)
193 | (has-body "Post chris")))
194 |
195 | (defpage named-route-with-post [:post "/foo"] []
196 | "named-post")
197 |
198 | (deftest named-route-post-test
199 | (-> (send-request [:post "/post-route"] {"nme" "chris"})
200 | (has-status 200)
201 | (has-body "Post chris")))
202 |
203 | (defpage route-one-arg "/one-arg/:id" {id :id})
204 | (def two-args "/two-args/:arg1/:arg2")
205 |
206 | (deftest url-args
207 | (is (= "/foo" (url-for foo)))
208 | (is (= "/one-arg/cool"(url-for "/one-arg/:blah" {:blah "cool"})))
209 | (is (= "/one-arg/5" (url-for route-one-arg {:id 5})))
210 | (is (= "/star/blah/cool" (url-for "/star/*" {:* "blah/cool"})))
211 | (is (= "/two-args/blah/cool" (url-for two-args {:arg2 "cool" :arg1 "blah"})))
212 | ;; make sure only subset matters
213 | (is (= "/one-arg/5" (url-for route-one-arg {:id 5 :name "chris"})))
214 | ;; make sure order doesn't matter
215 | (is (= "/two-args/blah/cool" (url-for two-args {:arg1 "blah" :arg2 "cool"}))))
216 |
217 | (deftest url-for-throws
218 | (is (thrown? Exception (url-for route-one-arg))))
219 |
220 | (defpage "/base-url" []
221 | (html
222 | (link-to "/hey" "link")))
223 |
224 | (deftest base-url
225 | (binding [options/*options* {:base-url "/woohoo"}]
226 | (-> (send-request "/base-url")
227 | (has-status 200)
228 | (has-body "