dependenciesorg.clojure/clojure |
| 1.6.0 | compojure |
| 1.1.6 | hiccup |
| 1.0.5 | ring-server |
| 0.3.1 | ring/ring-json |
| 0.3.1 | fogus/bacwn |
| 0.4.0 | clj-time |
| 0.8.0 | clojure-csv/clojure-csv |
| 2.0.1 |
|
(this space intentionally left almost blank) |
| |
| |
The App
The application can be split into the following verticals:
- Public Website (
home-routes )
- Public API (
api-routes )
- Private Website (
tbd )
| ( ns brian.handler
( :require [ compojure.core :refer [ defroutes routes ] ]
[ ring.middleware.resource :refer [ wrap-resource ] ]
[ ring.middleware.file-info :refer [ wrap-file-info ] ]
[ ring.middleware.json :refer [ wrap-json-response ] ]
[ hiccup.middleware :refer [ wrap-base-url ] ]
[ compojure.handler :as handler ]
[ compojure.route :as route ]
[ brian.routes.api :refer [ api-routes ] ]
[ brian.routes.home :refer [ home-routes ] ] ) )
|
|
| ( defn init [ ]
( println "brian is starting" ) )
|
|
| ( defn destroy [ ]
( println "brian is shutting down" ) )
|
|
| ( defroutes app-routes
( route/resources "/" )
( route/not-found "Not Found" ) )
|
|
| ( def app
( -> ( routes home-routes api-routes app-routes )
( handler/site )
( wrap-base-url )
( wrap-json-response ) ) )
|
|
| |
| |
The Data
Initialize, load and save the in-memory database.
Why datalog?
- I want to learn datalog.
- I want to learn datomic, which uses datalog.
- Queries are data.
- Bacwn is a small datalog implementation that I can squeeze
into my head.
- I know SQL. I don't know Datalog.
| ( ns brian.models.db
( :require [ clojure-csv.core :as csv ]
[ clojure.string :refer [ blank? ] ]
[ clojure.set :refer [ rename-keys ] ]
[ fogus.datalog.bacwn :refer
[ q build-work-plan run-work-plan ] ]
[ fogus.datalog.bacwn.macros :refer [ <- ?- make-database ] ]
[ fogus.datalog.bacwn.impl.rules :refer [ rules-set ] ]
[ fogus.datalog.bacwn.impl.database :refer [ add-tuples ] ] ) )
|
|
| ( def db-base
( make-database
( relation :book [ :isbn :author :title ] )
( index :book :isbn )
( relation :start [ :isbn :when ] )
( index :start :isbn )
( relation :finish [ :isbn :when ] )
( index :finish :isbn ) ) )
|
|
| ( def rules
( rules-set
( <- ( :attempt :author ?a :title ?t :when ?w )
( :book :isbn ?id :author ?a :title ?t )
( :start :isbn ?id :when ?w ) )
( <- ( :success :author ?a :title ?t :when ?w )
( :book :isbn ?id :author ?a :title ?t )
( :finish :isbn ?id :when ?w ) ) ) )
|
|
| ( def wp-1 ( build-work-plan rules ( ?- :attempt :author ?a
:title ?t :when ?q ) ) )
|
|
Given spreadsheet data, construct a vector of maps
using the first row as keys.
| ( defn get-tabular-data
[ path delim ]
( let [ tsv ( csv/parse-csv ( slurp path ) :delimiter delim )
hdrs ( first tsv )
rows ( rest tsv ) ]
( mapv #( zipmap hdrs % ) rows ) ) )
|
|
Filter map by keys (old-ks ) then swap the old keys (old-ks )
for the new keys (new-ks ).
| ( defn filter-and-swap
[ d old-ks new-ks ]
( rename-keys ( select-keys d old-ks )
( zipmap old-ks new-ks ) ) )
|
|
| ( defn parse-entries [ relation d old-ks new-ks ]
"TODO iterate through the entries once
TODO use a map instead of the two vectors"
( let [ massaged ( mapv #( filter-and-swap % old-ks new-ks ) d )
filtered ( filter #( not-any? blank? ( vals % ) ) massaged ) ]
( mapv #( flatten ( cons relation % ) ) filtered ) ) )
|
|
| ( defn parse-books [ d ]
( parse-entries :book d
[ "Title" "Author" "ISBN" ]
[ :title :author :isbn ] ) )
( defn parse-starts [ d ]
( parse-entries :start d
[ "ISBN" "DateAdded" ]
[ :isbn :when ] ) )
( defn parse-finishes [ d ]
( parse-entries :finish d
[ "ISBN" "DateRead" ]
[ :isbn :when ] ) )
|
|
| ( def db
( let [ raw ( get-tabular-data "resources/shelfari_data.tsv" \tab )
parsed ( concat ( parse-books raw )
( parse-starts raw )
( parse-finishes raw ) ) ]
( apply ( partial add-tuples db-base ) parsed ) ) )
|
|
Query the database for books that have been read.
| ( defn books-read
[ ]
( q ( ?- :attempt :author ?a :title ?t :when ?w )
db rules { } ) )
|
|
| |
| |
| ( ns brian.repl
( :use brian.handler
ring.server.standalone
[ ring.middleware file-info file ] ) )
|
|
| ( defonce server ( atom nil ) )
|
|
#'app expands to (var app) so that when we reload our code,
the server is forced to re-resolve the symbol in the var
rather than having its own copy. When the root binding
changes, the server picks it up without having to restart.
| ( defn get-handler
[ ]
( -> #' app
( wrap-file "resources" )
( wrap-file-info ) ) )
|
|
Start the server in development mode from the REPL.
| ( defn start-server
[ & [ port ] ]
( let [ port ( if port ( Integer/parseInt port ) 8080 ) ]
( reset! server
( serve ( get-handler )
{ :port port
:init init
:auto-reload? true
:destroy destroy
:join true } ) )
|
|
Stop the server from the REPL.
| ( defn stop-server
[ ]
( .stop @ server )
( reset! server nil ) )
|
|
| |
| |
The API
| ( ns brian.routes.api
( :require [ compojure.core :refer :all ]
[ clj-time.core :as t ]
[ clj-time.format :as f ]
[ brian.models.db :refer [ db rules ] ]
[ fogus.datalog.bacwn :refer [ q ] ]
[ fogus.datalog.bacwn.macros :refer [ ?- ] ] ) )
|
|
| ( defn parse-year
( [ x ]
( parse-year "MM/dd/yyyy" x ) )
( [ fmt x ]
( t/year ( f/parse ( f/formatter fmt ) x ) ) ) )
|
|
| ( defn books-read [ ]
( q ( ?- :success :author ?a :title ?t :when ?w ) db rules { } ) )
|
|
Partition books by year read.
| ( defn books-read-by-year
[ ]
( let [ res ( books-read )
parts ( group-by ( comp parse-year :when ) res ) ]
( map #( hash-map ( first % ) ( count ( second % ) ) ) parts ) ) )
|
|
TODO: datetime parsing
| |
TODO: datetime parsing
| |
| ( defroutes api-routes
( GET "/api/books-read" [ ] ( books-read ) )
( GET "/api/books-by-year" [ ] ( books-read-by-year ) ) )
|
|
| |
| |
The Public Website
| ( ns brian.routes.home
( :require [ compojure.core :refer :all ]
[ brian.views.layout :as layout ]
[ hiccup.core :refer :all ] ) )
|
|
Construct markup for icon links.
TODO: make data uris for images
| ( defn icon
[ url img ]
( let [ x [ :a { :href url }
[ :img.icon { :src img } ] ] ]
x ) )
|
|
| ( def twitter-icon ( icon "//twitter.com/brianru" "/img/twitter.png" ) )
( def github-icon ( icon "//github.com/brianru" "/img/github.png" ) )
"/img/linkedin.png" ) )
|
|
Homepage
## Top - first impression
Full (vertical) page with an opaque background image (that I took).
Name in the middle. Most important links directly beneath.
## Middle - depth - data
- reading habits (shelfari)
- fitness (fitbit)
programming (github)
Bottom - tbd
contact information?
| ( defn home
[ ]
( layout/common
[ :div.top-banner
[ :div [ :ul
[ :li [ :span.my-name "Brian James Rubinton" ] ]
[ :li github-icon twitter-icon linkedin-icon ] ] ] ] ) )
|
|
| ( defroutes home-routes
( GET "/" [ ] ( home ) ) )
|
|
| |
| |
| ( ns brian.views.layout
( :require [ hiccup.page :refer [ html5 include-css include-js ] ] ) )
|
|
Template for responsive webpages.
## Includes:
- JQuery
- Bootstrap
| ( defn common
[ & body ]
( html5
[ :head
[ :meta { :name "viewport"
:content "width=device-width, initial-scale=1" } ]
[ :title "BJR" ]
( include-css "/css/screen.css"
"//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css" )
( include-js "//code.jquery.com/jquery-1.11.0.min.js"
"//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js" ) ]
[ :body body ] ) )
|
|
| |