Initial project structure

This commit is contained in:
Aaron Fischer 2015-12-05 01:03:29 +01:00
parent dd2040e0b1
commit c3e12f32a3
19 changed files with 538 additions and 0 deletions

1
.lein-env Normal file
View file

@ -0,0 +1 @@
{:dev true, :port 3000, :nrepl-port 7000, :log-level :trace}

1
Procfile Normal file
View file

@ -0,0 +1 @@
web: java $JVM_OPTS -cp target/mailhead.jar clojure.main -m mailhead.core

11
env/dev/clj/mailhead/config.clj vendored Normal file
View file

@ -0,0 +1,11 @@
(ns mailhead.config
(:require [selmer.parser :as parser]
[taoensso.timbre :as timbre]
[mailhead.dev-middleware :refer [wrap-dev]]))
(def defaults
{:init
(fn []
(parser/cache-off!)
(timbre/info "\n-=[mailhead started successfully using the development profile]=-"))
:middleware wrap-dev})

10
env/dev/clj/mailhead/dev_middleware.clj vendored Normal file
View file

@ -0,0 +1,10 @@
(ns mailhead.dev-middleware
(:require [ring.middleware.reload :refer [wrap-reload]]
[selmer.middleware :refer [wrap-error-page]]
[prone.middleware :refer [wrap-exceptions]]))
(defn wrap-dev [handler]
(-> handler
wrap-reload
wrap-error-page
wrap-exceptions))

8
env/prod/clj/mailhead/config.clj vendored Normal file
View file

@ -0,0 +1,8 @@
(ns mailhead.config
(:require [taoensso.timbre :as timbre]))
(def defaults
{:init
(fn []
(timbre/info "\n-=[mailhead started successfully]=-"))
:middleware identity})

2
profiles.clj Normal file
View file

@ -0,0 +1,2 @@
{:profiles/dev {:env {}}
:profiles/test {:env {}}}

60
project.clj Normal file
View file

@ -0,0 +1,60 @@
(defproject mailhead "0.1.0-SNAPSHOT"
:description "FIXME: write description"
:url "http://example.com/FIXME"
:dependencies [[org.clojure/clojure "1.7.0"]
[selmer "0.9.5"]
[markdown-clj "0.9.82"]
[environ "1.0.1"]
[metosin/ring-middleware-format "0.6.0"]
[metosin/ring-http-response "0.6.5"]
[bouncer "0.3.3"]
[org.clojure/tools.nrepl "0.2.12"]
[org.webjars/bootstrap "3.3.6"]
[org.webjars/jquery "2.1.4"]
[com.taoensso/tower "3.0.2"]
[com.taoensso/timbre "4.1.4"]
[com.fzakaria/slf4j-timbre "0.2.1"]
[compojure "1.4.0"]
[ring-webjars "0.1.1"]
[ring/ring-defaults "0.1.5"]
[ring "1.4.0" :exclusions [ring/ring-jetty-adapter]]
[mount "0.1.5"]
[org.immutant/web "2.1.1" :exclusions [ch.qos.logback/logback-classic]]]
:min-lein-version "2.0.0"
:uberjar-name "mailhead.jar"
:jvm-opts ["-server"]
:main mailhead.core
:plugins [[lein-environ "1.0.1"]]
:profiles
{:uberjar {:omit-source true
:env {:production true}
:aot :all
:source-paths ["env/prod/clj"]}
:dev [:project/dev :profiles/dev]
:test [:project/test :profiles/test]
:project/dev {:dependencies [[prone "0.8.2"]
[ring/ring-mock "0.3.0"]
[ring/ring-devel "1.4.0"]
[pjstadig/humane-test-output "0.7.1"]]
:source-paths ["env/dev/clj"]
:repl-options {:init-ns mailhead.core}
:injections [(require 'pjstadig.humane-test-output)
(pjstadig.humane-test-output/activate!)]
;;when :nrepl-port is set the application starts the nREPL server on load
:env {:dev true
:port 3000
:nrepl-port 7000
:log-level :trace}}
:project/test {:env {:test true
:port 3001
:nrepl-port 7001
:log-level :trace}}
:profiles/dev {}
:profiles/test {}})

23
resources/docs/docs.md Normal file
View file

@ -0,0 +1,23 @@
### Managing Your Middleware
Request middleware functions are located under the `mailhead.middleware` namespace.
This namespace is reserved for any custom middleware for the application. Some default middleware is
already defined here. The middleware is assembled in the `wrap-base` function.
Middleware used for development is placed in the `mailhead.dev-middleware` namespace found in
the `env/dev/clj/` source path.
### Here are some links to get started
1. [HTML templating](http://www.luminusweb.net/docs/html_templating.md)
2. [Accessing the database](http://www.luminusweb.net/docs/database.md)
3. [Serving static resources](http://www.luminusweb.net/docs/static_resources.md)
4. [Setting response types](http://www.luminusweb.net/docs/responses.md)
5. [Defining routes](http://www.luminusweb.net/docs/routes.md)
6. [Adding middleware](http://www.luminusweb.net/docs/middleware.md)
7. [Sessions and cookies](http://www.luminusweb.net/docs/sessions_cookies.md)
8. [Security](http://www.luminusweb.net/docs/security.md)
9. [Deploying the application](http://www.luminusweb.net/docs/deployment.md)

View file

@ -0,0 +1,58 @@
html,
body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
height: 100%;
padding-top: 40px;
}
{% if cljs %}
@-moz-keyframes three-quarters-loader {
0% {
-moz-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-moz-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@-webkit-keyframes three-quarters-loader {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes three-quarters-loader {
0% {
-moz-transform: rotate(0deg);
-ms-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-moz-transform: rotate(360deg);
-ms-transform: rotate(360deg);
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
/* :not(:required) hides this rule from IE9 and below */
.three-quarters-loader:not(:required) {
-moz-animation: three-quarters-loader 1250ms infinite linear;
-webkit-animation: three-quarters-loader 1250ms infinite linear;
animation: three-quarters-loader 1250ms infinite linear;
border: 8px solid #38e;
border-right-color: transparent;
border-radius: 16px;
box-sizing: border-box;
display: inline-block;
position: relative;
overflow: hidden;
text-indent: -9999px;
width: 32px;
height: 32px;
}
{% endif %}

View file

View file

@ -0,0 +1,4 @@
{% extends "base.html" %}
{% block content %}
<p>this is the story of mailhead... work in progress</p>
{% endblock %}

View file

@ -0,0 +1,61 @@
<!DOCTYPE html>
<html>
<head>
<META http-equiv="Content-Type" content="text/html; charset=UTF-8">
<META name="viewport" content="width=device-width, initial-scale=1">
<title>Welcome to mailhead</title>
<!-- styles -->
{% style "/assets/bootstrap/css/bootstrap.min.css" %}
{% style "/assets/bootstrap/css/bootstrap-theme.min.css" %}
{% style "/css/screen.css" %}
</head>
<body>
<!-- navbar -->
<div id="navbar">
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button class="navbar-toggle"
data-target="#app-navbar"
data-toggle="collapse"
aria-expanded="false"
aria-controls="navbar">
<span class="sr-only">Toggle Navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a href="{{servlet-context}}/" class="navbar-brand">mailhead</a>
</div>
<div id="app-navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li {% ifequal page "home.html" %} class="active"{%endifequal%}>
<a href="{{servlet-context}}/">Home</a>
</li>
<li {% ifequal page "about.html" %} class="active"{%endifequal%}>
<a href="{{servlet-context}}/about">About</a>
</li>
</ul>
</div>
</div>
</nav>
</div>
<div class="container">
{% block content %}
{% endblock %}
</div>
<!-- scripts -->
{% script "/assets/jquery/jquery.min.js" %}
{% script "/assets/bootstrap/js/bootstrap.min.js" %}
{% script "/assets/bootstrap/js/collapse.js" %}
<script type="text/javascript">
var context = "{{servlet-context}}";
</script>
{% block page-scripts %}
{% endblock %}
</body>
</html>

View file

@ -0,0 +1,56 @@
<!DOCTYPE html>
<html>
<head>
<title>Something bad happened</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{% style "/assets/bootstrap/css/bootstrap.min.css" %}
{% style "/assets/bootstrap/css/bootstrap-theme.min.css" %}
<style type="text/css">
html {
height: 100%;
min-height: 100%;
min-width: 100%;
overflow: hidden;
width: 100%;
}
html body {
height: 100%;
margin: 0;
padding: 0;
width: 100%;
}
html .container-fluid {
display: table;
height: 100%;
padding: 0;
width: 100%;
}
html .row-fluid {
display: table-cell;
height: 100%;
vertical-align: middle;
}
</style>
</head>
<body>
<div class="container-fluid">
<div class="row-fluid">
<div class="col-lg-12">
<div class="centering text-center">
<div class="text-center">
<h1><span class="text-danger">Error: {{status}}</span></h1>
<hr>
{% if title %}
<h2 class="without-margin">{{title}}</h2>
{% endif %}
{% if message %}
<h4 class="text-danger">{{message}}</h4>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,14 @@
{% extends "base.html" %}
{% block content %}
<div class="jumbotron">
<h1>Welcome to mailhead</h1>
<p>Time to start building your site!</p>
<p><a class="btn btn-primary btn-lg" href="http://luminusweb.net">Learn more &raquo;</a></p>
</div>
<div class="row">
<div class="span12">
{{docs|markdown}}
</div>
</div>
{% endblock %}

64
src/mailhead/core.clj Normal file
View file

@ -0,0 +1,64 @@
(ns mailhead.core
(:require [mailhead.handler :refer [app init destroy]]
[immutant.web :as immutant]
[clojure.tools.nrepl.server :as nrepl]
[taoensso.timbre :as timbre]
[environ.core :refer [env]])
(:gen-class))
(defonce nrepl-server (atom nil))
(defn parse-port [port]
(when port
(cond
(string? port) (Integer/parseInt port)
(number? port) port
:else (throw (Exception. (str "invalid port value: " port))))))
(defn stop-nrepl []
(when-let [server @nrepl-server]
(nrepl/stop-server server)))
(defn start-nrepl
"Start a network repl for debugging when the :nrepl-port is set in the environment."
[]
(if @nrepl-server
(timbre/error "nREPL is already running!")
(when-let [port (env :nrepl-port)]
(try
(->> port
(parse-port)
(nrepl/start-server :port)
(reset! nrepl-server))
(timbre/info "nREPL server started on port" port)
(catch Throwable t
(timbre/error t "failed to start nREPL"))))))
(defn http-port [port]
(parse-port (or port (env :port) 3000)))
(defonce http-server (atom nil))
(defn start-http-server [port]
(init)
(reset! http-server (immutant/run app :host "0.0.0.0" :port port)))
(defn stop-http-server []
(when @http-server
(destroy)
(immutant/stop @http-server)
(reset! http-server nil)))
(defn stop-app []
(stop-nrepl)
(stop-http-server)
(shutdown-agents))
(defn start-app [[port]]
(.addShutdownHook (Runtime/getRuntime) (Thread. stop-app))
(start-nrepl)
(start-http-server (http-port port))
(timbre/info "server started on port:" (:port @http-server)))
(defn -main [& args]
(start-app args))

48
src/mailhead/handler.clj Normal file
View file

@ -0,0 +1,48 @@
(ns mailhead.handler
(:require [compojure.core :refer [defroutes routes wrap-routes]]
[mailhead.layout :refer [error-page]]
[mailhead.routes.home :refer [home-routes]]
[mailhead.middleware :as middleware]
[compojure.route :as route]
[taoensso.timbre :as timbre]
[taoensso.timbre.appenders.3rd-party.rotor :as rotor]
[selmer.parser :as parser]
[environ.core :refer [env]]
[mailhead.config :refer [defaults]]
[mount.core :as mount]))
(defn init
"init will be called once when
app is deployed as a servlet on
an app server such as Tomcat
put any initialization code here"
[]
(timbre/merge-config!
{:level ((fnil keyword :info) (env :log-level))
:appenders {:rotor (rotor/rotor-appender
{:path (or (env :log-path) "mailhead.log")
:max-size (* 512 1024)
:backlog 10})}})
(doseq [component (:started (mount/start))]
(timbre/info component "started"))
((:init defaults)))
(defn destroy
"destroy will be called when your application
shuts down, put any clean up code here"
[]
(timbre/info "mailhead is shutting down...")
(doseq [component (:stopped (mount/stop))]
(timbre/info component "stopped"))
(timbre/info "shutdown complete!"))
(def app-routes
(routes
(wrap-routes #'home-routes middleware/wrap-csrf)
(route/not-found
(:body
(error-page {:status 404
:title "page not found"})))))
(def app (middleware/wrap-base #'app-routes))

39
src/mailhead/layout.clj Normal file
View file

@ -0,0 +1,39 @@
(ns mailhead.layout
(:require [selmer.parser :as parser]
[selmer.filters :as filters]
[markdown.core :refer [md-to-html-string]]
[ring.util.http-response :refer [content-type ok]]
[ring.util.anti-forgery :refer [anti-forgery-field]]
[ring.middleware.anti-forgery :refer [*anti-forgery-token*]]))
(declare ^:dynamic *app-context*)
(parser/set-resource-path! (clojure.java.io/resource "templates"))
(parser/add-tag! :csrf-field (fn [_ _] (anti-forgery-field)))
(filters/add-filter! :markdown (fn [content] [:safe (md-to-html-string content)]))
(defn render
"renders the HTML template located relative to resources/templates"
[template & [params]]
(content-type
(ok
(parser/render-file
template
(assoc params
:page template
:csrf-token *anti-forgery-token*
:servlet-context *app-context*)))
"text/html; charset=utf-8"))
(defn error-page
"error-details should be a map containing the following keys:
:status - error status
:title - error title (optional)
:message - detailed error message (optional)
returns a response map with the error page as the body
and the status specified by the status key"
[error-details]
{:status (:status error-details)
:headers {"Content-Type" "text/html; charset=utf-8"}
:body (parser/render-file "error.html" error-details)})

View file

@ -0,0 +1,61 @@
(ns mailhead.middleware
(:require [mailhead.layout :refer [*app-context* error-page]]
[taoensso.timbre :as timbre]
[environ.core :refer [env]]
[ring.middleware.flash :refer [wrap-flash]]
[immutant.web.middleware :refer [wrap-session]]
[ring.middleware.webjars :refer [wrap-webjars]]
[ring.middleware.defaults :refer [site-defaults wrap-defaults]]
[ring.middleware.anti-forgery :refer [wrap-anti-forgery]]
[ring.middleware.format :refer [wrap-restful-format]]
[mailhead.config :refer [defaults]])
(:import [javax.servlet ServletContext]))
(defn wrap-context [handler]
(fn [request]
(binding [*app-context*
(if-let [context (:servlet-context request)]
;; If we're not inside a servlet environment
;; (for example when using mock requests), then
;; .getContextPath might not exist
(try (.getContextPath ^ServletContext context)
(catch IllegalArgumentException _ context))
;; if the context is not specified in the request
;; we check if one has been specified in the environment
;; instead
(:app-context env))]
(handler request))))
(defn wrap-internal-error [handler]
(fn [req]
(try
(handler req)
(catch Throwable t
(timbre/error t)
(error-page {:status 500
:title "Something very bad has happened!"
:message "We've dispatched a team of highly trained gnomes to take care of the problem."})))))
(defn wrap-csrf [handler]
(wrap-anti-forgery
handler
{:error-response
(error-page
{:status 403
:title "Invalid anti-forgery token"})}))
(defn wrap-formats [handler]
(wrap-restful-format handler {:formats [:json-kw :transit-json :transit-msgpack]}))
(defn wrap-base [handler]
(-> ((:middleware defaults) handler)
wrap-formats
wrap-webjars
wrap-flash
(wrap-session {:cookie-attrs {:http-only true}})
(wrap-defaults
(-> site-defaults
(assoc-in [:security :anti-forgery] false)
(dissoc :session)))
wrap-context
wrap-internal-error))

View file

@ -0,0 +1,17 @@
(ns mailhead.routes.home
(:require [mailhead.layout :as layout]
[compojure.core :refer [defroutes GET]]
[ring.util.http-response :refer [ok]]
[clojure.java.io :as io]))
(defn home-page []
(layout/render
"home.html" {:docs (-> "docs/docs.md" io/resource slurp)}))
(defn about-page []
(layout/render "about.html"))
(defroutes home-routes
(GET "/" [] (home-page))
(GET "/about" [] (about-page)))