started the rest api

This commit is contained in:
didier amyot committed 2018-06-16 17:24:30 -04:00
1 parent 516cccf4cd
commit ad924ca5c7
10 files changed
+320 -91

No files matched your search

+17
View File
@@ -15,19 +15,33 @@
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Leiningen: cheshire:5.8.0" level="project" />
<orderEntry type="library" name="Leiningen: clj-time:0.14.2" level="project" />
<orderEntry type="library" name="Leiningen: clj-tuple:0.2.2" level="project" />
<orderEntry type="library" name="Leiningen: clojure-complete:0.2.4" level="project" />
<orderEntry type="library" name="Leiningen: clout:2.2.1" level="project" />
<orderEntry type="library" name="Leiningen: colorize:0.1.1" level="project" />
<orderEntry type="library" name="Leiningen: com.fasterxml.jackson.core/jackson-core:2.9.0" level="project" />
<orderEntry type="library" name="Leiningen: com.fasterxml.jackson.dataformat/jackson-dataformat-cbor:2.9.0" level="project" />
<orderEntry type="library" name="Leiningen: com.fasterxml.jackson.dataformat/jackson-dataformat-smile:2.9.0" level="project" />
<orderEntry type="library" name="Leiningen: com.rpl/specter:1.0.4" level="project" />
<orderEntry type="library" name="Leiningen: commons-codec:1.10" level="project" />
<orderEntry type="library" name="Leiningen: commons-fileupload:1.3.3" level="project" />
<orderEntry type="library" name="Leiningen: commons-io:2.5" level="project" />
<orderEntry type="library" name="Leiningen: compojure:1.6.1" level="project" />
<orderEntry type="library" name="Leiningen: crypto-equality:1.0.0" level="project" />
<orderEntry type="library" name="Leiningen: crypto-random:1.2.0" level="project" />
<orderEntry type="library" name="Leiningen: environ:1.1.0" level="project" />
<orderEntry type="library" name="Leiningen: fipp:0.6.10" level="project" />
<orderEntry type="library" name="Leiningen: flare:0.2.9" level="project" />
<orderEntry type="library" name="Leiningen: http-kit:2.3.0" level="project" />
<orderEntry type="library" name="Leiningen: instaparse:1.4.8" level="project" />
<orderEntry type="library" name="Leiningen: io.aviso/pretty:0.1.34" level="project" />
<orderEntry type="library" name="Leiningen: javax.servlet/javax.servlet-api:3.1.0" level="project" />
<orderEntry type="library" name="Leiningen: joda-time:2.9.7" level="project" />
<orderEntry type="library" name="Leiningen: marick/clojure-commons:3.0.0" level="project" />
<orderEntry type="library" name="Leiningen: marick/suchwow:6.0.0" level="project" />
<orderEntry type="library" name="Leiningen: medley:1.0.0" level="project" />
<orderEntry type="library" name="Leiningen: midje:1.9.1" level="project" />
<orderEntry type="library" name="Leiningen: mvxcvi/arrangement:1.1.1" level="project" />
<orderEntry type="library" name="Leiningen: mvxcvi/puget:1.0.2" level="project" />
@@ -53,7 +67,10 @@
<orderEntry type="library" name="Leiningen: org.tcrawley/dynapath:1.0.0" level="project" />
<orderEntry type="library" name="Leiningen: potemkin:0.4.3" level="project" />
<orderEntry type="library" name="Leiningen: riddley:0.1.12" level="project" />
<orderEntry type="library" name="Leiningen: ring/ring-codec:1.1.0" level="project" />
<orderEntry type="library" name="Leiningen: ring/ring-core:1.6.3" level="project" />
<orderEntry type="library" name="Leiningen: slingshot:0.12.2" level="project" />
<orderEntry type="library" name="Leiningen: swiss-arrows:1.0.0" level="project" />
<orderEntry type="library" name="Leiningen: tigris:0.1.1" level="project" />
</component>
</module>
+9 -1
View File
@@ -4,7 +4,15 @@
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[org.clojure/clojure "1.9.0"]
[org.clojure/core.async "0.4.474"]]
[org.clojure/core.async "0.4.474"]
;; web
[http-kit "2.3.0"]
[compojure "1.6.1"]
[javax.servlet/javax.servlet-api "3.1.0"]
;;utils
[slingshot "0.12.2"]
[cheshire "5.8.0"]]
:java-source-paths ["src/java"]
:main ^:skip-aot book.core
:target-path "target/%s"
+56
View File
@@ -0,0 +1,56 @@
(ns book.cli
(:require [book.match :refer [sell?]]
[book.util :refer :all]
[book.exchange :refer [empty-book process-orders create-exchange-for-security]]
[book.order] ;; for spec side effect
[slingshot.slingshot :refer [try+ throw+]]
[clojure.edn :as edn]
[clojure.core.async :refer [chan go >!! <!!]]
[clojure.spec.alpha :as s]))
(def incoming-order-channel (chan))
;(def trades-channel (chan))
;
;(def new-book-order-channel (chan))
(defn print-chan [prefix c]
(loop [shit (<!! c)]
(println prefix shit)
(recur (<!! c))))
;(def master-book {:sell (create-sell-book)
; :buy (create-buy-book)})
;;; repl things.
(defn read-order []
(try
(print "$ ")
(let [order (edn/read-string (read-line))
valid? (s/valid? :book.order/order order)]
(if valid? order nil))
(catch Exception e nil)))
(def order-ids (atom -1))
(defn add-id-to-order [order]
(assoc order :book.order/id (swap! order-ids inc)))
(defn -main [& args]
(let [initial-book empty-book
incoming-order-channel (chan)
[trades-channel new-book-order-channel] (create-exchange-for-security initial-book incoming-order-channel)]
(go (print-chan "trade > " trades-channel))
(go (print-chan "order > " new-book-order-channel))
(loop [order (read-order)]
(if (some? order)
(do
(let [order-with-id (add-id-to-order order)]
(>!! incoming-order-channel order-with-id)
(println "\n\norder accepted with id: " (:book.order/id order-with-id))))
(println "invalid order"))
(recur (read-order)))))
+7 -77
View File
@@ -1,80 +1,10 @@
(ns book.core
(:require [book.match :refer [sell?
create-buy-book
create-sell-book
match-order-in-book]]
[book.util :refer :all]
[book.order] ;; for spec side effect
[clojure.edn :as edn]
[clojure.core.async :refer [chan go >!! <!!]]
[clojure.spec.alpha :as s])
(:require [book.cli]
[book.web])
(:gen-class))
(def incoming-order-channel (chan))
(def trades-channel (chan))
(def new-book-order-channel (chan))
(defn print-chan [prefix c]
(loop [shit (<!! c)]
(println prefix shit)
(recur (<!! c))))
(def master-book {:sell (create-sell-book)
:buy (create-buy-book)})
;
;(defn process-buy-order [order]
; (let [[x y x] (match-order-in-book @buy-book order)]))
;;; repl things.
(defn read-order []
(try
(print "$ ")
(let [order (edn/read-string (read-line))
valid? (s/valid? :book.order/order order)]
(if valid? order nil))
(catch Exception e nil)))
(def order-ids (atom -1))
(defn add-id-to-order [order]
(assoc order :book.order/id (swap! order-ids inc)))
(defn process-orders [master-book orders-channel trades-channel new-book-order-channel]
(loop [order (<!! orders-channel)
{sell-book :sell buy-book :buy} master-book]
(if (sell? order)
(do
(let [[trades new-buy-book leftover-order] (match-order-in-book buy-book order)
new-sell-book (if leftover-order (conj sell-book) leftover-order)]
(when (seq trades) (>!! trades-channel trades))
(when leftover-order (>!! new-book-order-channel leftover-order))
(recur (<!! orders-channel) {:sell new-sell-book :buy new-buy-book})))
(do
(let [[trades new-sell-book leftover-order] (match-order-in-book sell-book order)
new-buy-book (if leftover-order (conj buy-book) leftover-order)]
(when (seq trades) (>!! trades-channel trades))
(when leftover-order (>!! new-book-order-channel leftover-order))
(recur (<!! orders-channel) {:sell new-sell-book :buy new-buy-book}))))))
(defn -main [& args]
(println "enter orders in edn format.")
(go (print-chan "trade > " trades-channel))
(go (print-chan "order > " new-book-order-channel))
(go (process-orders master-book incoming-order-channel trades-channel new-book-order-channel))
(loop [order (read-order)]
(if (some? order)
(do
(let [order-with-id (add-id-to-order order)]
(>!! incoming-order-channel order-with-id)
(println "\n\norder accepted with id: " (:book.order/id order-with-id))))
(println "invalid order"))
(recur (read-order))))
(defn -main [& argv]
(case (first argv)
"web" (apply book.web/-main (rest argv))
"cli" (apply book.cli/-main (rest argv))
(println "First argument must be web or cli")))
+40
View File
@@ -0,0 +1,40 @@
(ns book.exchange
(:require [book.match :refer [sell?
create-buy-book
create-sell-book
match-order-in-book]]
[book.util :refer :all]
[clojure.core.async :refer [chan go >!! <!!]]))
(def empty-book {:sell (create-sell-book) :buy (create-buy-book)})
(defn process-abstract-sell [order reference-book same-type-book trades-channel new-book-order-channel]
(let [[trades new-reference-book leftover-order] (match-order-in-book reference-book order)
new-same-type-book (if leftover-order (conj same-type-book leftover-order) leftover-order)]
(when (seq trades) (doseq [trade trades] (>!! trades-channel trade)))
(when leftover-order (>!! new-book-order-channel leftover-order))
[new-reference-book new-same-type-book]))
(defn process-sell-order [order {sell-book :sell buy-book :buy} trades-channel new-book-order-channel]
(let [[new-buy-book new-sell-book] (process-abstract-sell order buy-book sell-book trades-channel new-book-order-channel)]
{:sell new-sell-book :buy new-buy-book}))
(defn process-buy-order [order {sell-book :sell buy-book :buy} trades-channel new-book-order-channel]
(let [[new-sell-book new-buy-book] (process-abstract-sell order sell-book buy-book trades-channel new-book-order-channel)]
{:sell new-sell-book :buy new-buy-book}))
(defn process-orders [master-book orders-channel trades-channel new-book-order-channel]
(loop [order (<!! orders-channel)
book master-book]
(let [process-order (if (sell? order) process-sell-order process-buy-order)
new-book (process-order order book trades-channel new-book-order-channel)]
(recur (<!! orders-channel) new-book))))
(defn create-exchange-for-security [initial-book incoming-order-channel]
(let [trades-channel (chan)
new-book-order-channel (chan)]
(go (process-orders initial-book incoming-order-channel trades-channel new-book-order-channel))
[trades-channel new-book-order-channel]))
+43
View File
@@ -0,0 +1,43 @@
(ns book.rest.util
(:require [cheshire.core :refer [generate-string]]
[clojure.string :refer [split starts-with? ends-with?]]))
(def mime-types
[["application/json" generate-string]
["application/edn" str]])
(defn create-mime-type-predicate [accept]
(let [[accept-start accept-end] (split accept #"/")]
(fn [mimetype]
(let [[start end] (split mimetype #"/")]
(and (or (= "*" accept-start) (= start accept-start))
(or (= "*" accept-end) (= end accept-end)))))))
(defn get-best-serializer [request]
(let [accept (get-in request [:headers "accept"] "*/*")
accept-content-types (conj (split accept #",\s*") "*/*")
xf (comp (map create-mime-type-predicate)
(map (fn [mime-pred] (filter #(mime-pred (first %)) mime-types)))
cat
(take 1))]
(first (sequence xf accept-content-types))))
(defn get-status [status request]
(if (nil? status)
(case (:request-method request)
:get 200
:post 201
200)
status))
(defn build-response [request {status :status body :body :as response}]
(let [[content-type serializer] (get-best-serializer request)]
{:body (serializer response)
:headers {"Content-Type" content-type}
:status (get-status status request)}))
+77
View File
@@ -0,0 +1,77 @@
(ns book.web
(:require [book.exchange :refer [empty-book]]
[org.httpkit.server :refer [run-server]]
[compojure.route :refer [not-found]]
[compojure.handler :refer [api]]
[compojure.core :refer :all]
[book.order] ;; for spec side effect
[book.rest.util :refer [build-response]]
[clojure.spec.alpha :as s]
[slingshot.slingshot :refer [try+ throw+]]
[cheshire.core :refer [generate-string]]
[clojure.edn :as edn]))
(def order-id (atom -1))
(defn api-description [req]
(let [body {"/api/1/securities" "list all securities."
"/api/1/book" "For all information about the book."}]
(build-response req body)))
;; the api should look like...
;; trades ...
;; /:sec-id/trades/:trade-id
;; book ...
;; /:sec-id/book/:order
;;
(defn securities [books]
(fn [request]
(build-response request {:securities (keys books)})))
(defn get-security [books]
(fn [{{sec-id-str :sec-id} :route-params body :body :as request}]))
(defn validate-order [asked-order]
(if (not (s/valid? :book.order/order asked-order))
(throw+ {:status :4XX :headers {"Content-Type" "application/json"} :body "foo"}))
(assoc asked-order :book.order/id (swap! order-id inc)))
(defn get-order [body]
(try+
(-> body clojure.java.io/reader slurp edn/read-string)
(catch Exception e
(throw+ {:error-type :4XX :status 400 :body {:error "Invalid body format. Body must be edn format."}}))))
(defn create-order [books]
(fn [{{sec-id-str :sec-id} :route-params body :body :as request}]
(try+
(let [asked-order (get-order body)
order (validate-order asked-order)
sec-id (keyword sec-id-str)
book (get books sec-id)]
(build-response request {:order order :book book}))
(catch [:error-type :4XX] e (build-response request e))
(catch [:error-type :5XX] e (build-response request e)))))
(defn create-routes [books]
(routes
(context "/api/1" []
(GET "/" [] api-description)
(GET "/securities" [] (securities books))
(GET "/securities/:sec-id" [] (get-security books))
(POST "/securities/:sec-id" [] (create-order books)))
(not-found "not found")))
(defn -main [& args]
(let [books {:foo empty-book :bar empty-book}
api-routes (create-routes books)]
(run-server (api api-routes) {:port 8080})))
-13
View File
@@ -1,13 +0,0 @@
(ns book.core-test
(:require [midje.sweet :refer :all]))
(facts "big fact"
(fact "about trivialities"
(let [expt 3]
(inc 2) => expt
(inc 2) => expt))
(fact "about trivialities 2"
(let [expt 3]
(inc 2) => expt
(inc 2) => expt)))
+28
View File
@@ -0,0 +1,28 @@
(ns book.exchange-test
(:require [midje.sweet :refer :all]
[book.exchange :refer :all]
[book.match :refer [create-buy-book
create-sell-book]]
[clojure.core.async :refer [chan go >!! <!!]]))
(facts "the exchange as an whole"
(fact "sell order followed by a buy order trivialities "
(let [incoming-order-channel (chan)
trades-channel (chan)
new-book-order-channel (chan)
master-book {:sell (create-sell-book) :buy (create-buy-book)}]
(go (process-orders master-book incoming-order-channel trades-channel new-book-order-channel))
(>!! incoming-order-channel {:type :sell :quantity 10 :price 2})
(<!! new-book-order-channel) => {:type :sell :quantity 10 :price 2}
(>!! incoming-order-channel {:type :buy :quantity 20 :price 2})
(<!! trades-channel) => #:book.order{:book-id nil
:order-id nil
:price 2
:quantity 10
:type :sell}
(<!! new-book-order-channel) => {:type :buy :quantity 10 :price 2})))
+43
View File
@@ -0,0 +1,43 @@
(ns book.rest.util-test
(:require [book.rest.util :refer [build-response]]
[midje.sweet :refer :all]
[clojure.test :refer :all]))
(defn expt-response [body content-type status]
{:body body
:headers {"Content-Type" content-type}
:status status})
(facts "the building the response"
(fact "building the response in edn"
(let [content-type "application/edn"
request {:headers {"accept" content-type}}
response "foo"
expt (expt-response "foo" "application/edn" 200)]
(build-response request response) => expt))
(fact "building the response in edn"
(let [content-type "application/edn"
request {:headers {"accept" content-type}}
response {:foo [1 2 3]}
expt (expt-response "{:foo [1 2 3]}" "application/edn" 200)]
(build-response request response) => expt))
(fact "building the response in edn"
(let [content-type "application/edn,application/json"
request {:headers {"accept" content-type}}
response {:foo [1 2 3]}
expt (expt-response "{:foo [1 2 3]}" "application/edn" 200)]
(build-response request response) => expt))
(fact "building the response in json"
(let [content-type "text/html,application/xhtml+xml,application/json,application/xml;q=0.9,*/*;q=0.8"
request {:headers {"accept" content-type}}
response {:foo [1 2 3]}
expt (expt-response "{\"foo\":[1,2,3]}" "application/json" 200)]
(build-response request response) => expt))
(fact "My browser should receive json"
(let [content-type "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
request {:headers {"accept" content-type}}
response {:foo [1 2 3]}
expt (expt-response "{\"foo\":[1,2,3]}" "application/json" 200)]
(build-response request response) => expt)))