├── .lein-failures
├── Procfile
├── .gitignore
├── src
├── cljs
│ └── clojure_serverless_demo
│ │ ├── db.cljs
│ │ ├── config.cljs
│ │ ├── subs.cljs
│ │ ├── core.cljs
│ │ ├── events.cljs
│ │ └── views.cljs
└── clj
│ └── clojure_serverless_demo
│ ├── aws
│ ├── dynamodb_local.clj
│ ├── lambda_simple.clj
│ └── lambda.clj
│ ├── config.clj
│ ├── handler.clj
│ ├── chat.clj
│ ├── storage.clj
│ ├── core.clj
│ └── api.clj
├── resources
├── sqlite4java-libs
│ ├── sqlite4java.jar
│ ├── libsqlite4java-osx.dylib
│ ├── sqlite4java-win32-x64.dll
│ ├── sqlite4java-win32-x86.dll
│ ├── libsqlite4java-linux-amd64.so
│ └── libsqlite4java-linux-i386.so
└── public
│ └── index.html
├── test
└── clj
│ └── clojure_serverless_demo
│ ├── core_test.clj
│ ├── integration
│ ├── dynamodb_local_test.clj
│ └── http_test.clj
│ ├── api_test.clj
│ ├── fixtures.clj
│ └── aws
│ └── lambda_test.clj
├── bin
└── deploy
├── README.md
├── serverless.yml
└── project.clj
/.lein-failures:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: java $JVM_OPTS -cp target/clojure-serverless-demo.jar clojure.main -m clojure-serverless-demo.server
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /*.log
2 | /target
3 | /*-init.clj
4 | /resources/public/js/compiled
5 | out
6 | node_modules
7 | .serverless
8 |
--------------------------------------------------------------------------------
/src/cljs/clojure_serverless_demo/db.cljs:
--------------------------------------------------------------------------------
1 | (ns clojure-serverless-demo.db)
2 |
3 | (def default-db
4 | {:name nil
5 | :messages []})
6 |
--------------------------------------------------------------------------------
/resources/sqlite4java-libs/sqlite4java.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stuart-robinson/clojure-serverless-demo/HEAD/resources/sqlite4java-libs/sqlite4java.jar
--------------------------------------------------------------------------------
/resources/sqlite4java-libs/libsqlite4java-osx.dylib:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stuart-robinson/clojure-serverless-demo/HEAD/resources/sqlite4java-libs/libsqlite4java-osx.dylib
--------------------------------------------------------------------------------
/resources/sqlite4java-libs/sqlite4java-win32-x64.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stuart-robinson/clojure-serverless-demo/HEAD/resources/sqlite4java-libs/sqlite4java-win32-x64.dll
--------------------------------------------------------------------------------
/resources/sqlite4java-libs/sqlite4java-win32-x86.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stuart-robinson/clojure-serverless-demo/HEAD/resources/sqlite4java-libs/sqlite4java-win32-x86.dll
--------------------------------------------------------------------------------
/resources/sqlite4java-libs/libsqlite4java-linux-amd64.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stuart-robinson/clojure-serverless-demo/HEAD/resources/sqlite4java-libs/libsqlite4java-linux-amd64.so
--------------------------------------------------------------------------------
/resources/sqlite4java-libs/libsqlite4java-linux-i386.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stuart-robinson/clojure-serverless-demo/HEAD/resources/sqlite4java-libs/libsqlite4java-linux-i386.so
--------------------------------------------------------------------------------
/test/clj/clojure_serverless_demo/core_test.clj:
--------------------------------------------------------------------------------
1 | (ns clojure-serverless-demo.core-test
2 | (:require [clojure.test :refer :all]
3 | [clojure-serverless-demo.api :as api]
4 | [ring.mock.request :as mock]))
5 |
--------------------------------------------------------------------------------
/bin/deploy:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | lein clean
4 |
5 | #build and deploy front-end
6 |
7 | lein cljsbuild once min
8 | serverless client deploy -s dev
9 |
10 | #build and deploy back-end
11 |
12 | lein uberjar
13 | serverless deploy -s dev
14 |
--------------------------------------------------------------------------------
/src/cljs/clojure_serverless_demo/config.cljs:
--------------------------------------------------------------------------------
1 | (ns clojure-serverless-demo.config)
2 |
3 | (def debug?
4 | ^boolean goog.DEBUG)
5 |
6 | (def host
7 | (if debug?
8 | "http://localhost:8888"
9 | "https://odkas8ut1b.execute-api.eu-west-2.amazonaws.com/prod"))
10 |
--------------------------------------------------------------------------------
/src/cljs/clojure_serverless_demo/subs.cljs:
--------------------------------------------------------------------------------
1 | (ns clojure-serverless-demo.subs
2 | (:require [re-frame.core :as re-frame]))
3 |
4 | (re-frame/reg-sub
5 | ::name
6 | (fn [db]
7 | (:name db)))
8 |
9 | (re-frame/reg-sub
10 | ::messages
11 | (fn [db]
12 | (:messages db)))
13 |
--------------------------------------------------------------------------------
/src/clj/clojure_serverless_demo/aws/dynamodb_local.clj:
--------------------------------------------------------------------------------
1 | (ns clojure-serverless-demo.aws.dynamodb-local
2 | (:import [com.amazonaws.services.dynamodbv2.local.main ServerRunner]))
3 |
4 | (System/setProperty "sqlite4java.library.path"
5 | (.getPath (clojure.java.io/resource "sqlite4java-libs")));
6 |
7 | (def args (into-array String ["-inMemory"]))
8 |
9 | (defn build-server []
10 | (ServerRunner/createServerFromCommandLineArgs args))
11 |
--------------------------------------------------------------------------------
/src/clj/clojure_serverless_demo/config.clj:
--------------------------------------------------------------------------------
1 | (ns clojure-serverless-demo.config
2 | (:require [environ.core :refer [env]]))
3 |
4 | (def db-config {:access-key (env :x-aws-access-key-id)
5 | :secret-key (env :x-aws-secret-access-key)
6 | :endpoint (env :endpoint)})
7 |
8 | (def table-config
9 | {:name (or (keyword (env :dynamodb-table-name)) :messages-dev)
10 | :primary-key [:id :s]
11 | :options {:throughput {:read 5 :write 1}
12 | :block? true}})
13 |
--------------------------------------------------------------------------------
/src/clj/clojure_serverless_demo/handler.clj:
--------------------------------------------------------------------------------
1 | (ns clojure-serverless-demo.handler
2 | (:require [compojure.core :refer [GET defroutes]]
3 | [compojure.route :refer [resources]]
4 | [ring.util.response :refer [resource-response]]
5 | [ring.middleware.reload :refer [wrap-reload]]))
6 |
7 | (defroutes routes
8 | (GET "/" [] (resource-response "index.html" {:root "public"}))
9 | (resources "/"))
10 |
11 | (def dev-handler (-> #'routes wrap-reload))
12 |
13 | (def handler routes)
14 |
--------------------------------------------------------------------------------
/src/clj/clojure_serverless_demo/chat.clj:
--------------------------------------------------------------------------------
1 | (ns clojure-serverless-demo.chat)
2 |
3 | (defn say [{:keys [name message]}]
4 | (let [timenow-ms (System/currentTimeMillis)]
5 | {:id (str (java.util.UUID/randomUUID))
6 | :name name
7 | :message message
8 | :channel "default"
9 | :timestamp timenow-ms
10 | :order (- 2147483647 timenow-ms)}))
11 |
12 | (defn join [{:keys [name]}]
13 | (say {:name "channel"
14 | :message (str name " joined the channel...")}))
15 |
16 | (defn process-messages [messages]
17 | messages)
18 |
--------------------------------------------------------------------------------
/resources/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | chat.stuart.cloud
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/clj/clojure_serverless_demo/aws/lambda_simple.clj:
--------------------------------------------------------------------------------
1 | ;;simple lambda function in clojure
2 | (ns clojure-serverless-demo.aws.lambda-simple
3 | (:require [cheshire.core :refer [generate-stream parse-stream generate-string]]
4 | [clojure.java.io :as io])
5 | (:import [com.amazonaws.services.lambda.runtime.RequestStreamHandler])
6 | (:gen-class
7 | :name clojure_serverless_demo.SimpleHandler
8 | :implements [com.amazonaws.services.lambda.runtime.RequestStreamHandler]))
9 |
10 | (defn -handleRequest
11 | [_ input-stream output-stream context]
12 | (with-open [writer (io/writer output-stream)]
13 | (generate-stream {:hello "world"} writer)))
14 |
--------------------------------------------------------------------------------
/src/cljs/clojure_serverless_demo/core.cljs:
--------------------------------------------------------------------------------
1 | (ns clojure-serverless-demo.core
2 | (:require [reagent.core :as reagent]
3 | [re-frame.core :as re-frame]
4 | [clojure-serverless-demo.events :as events]
5 | [clojure-serverless-demo.views :as views]
6 | [clojure-serverless-demo.config :as config]))
7 |
8 | (defn dev-setup []
9 | (when config/debug?
10 | (enable-console-print!)
11 | (println "dev mode")))
12 |
13 | (defn mount-root []
14 | (re-frame/clear-subscription-cache!)
15 | (reagent/render [views/main-panel]
16 | (.getElementById js/document "app")))
17 |
18 | (defn ^:export init []
19 | (re-frame/dispatch-sync [::events/initialize-db])
20 | (dev-setup)
21 | (mount-root))
22 |
--------------------------------------------------------------------------------
/test/clj/clojure_serverless_demo/integration/dynamodb_local_test.clj:
--------------------------------------------------------------------------------
1 | (ns clojure-serverless-demo.integration.dynamodb-local-test
2 | (:require [clojure.test :refer :all]
3 | [taoensso.faraday :as far]
4 | [clojure-serverless-demo.fixtures :refer [with-local-db with-table]]))
5 |
6 | (def db-config {:access-key "ACCESSKEY"
7 | :secret-key "TOPSECRET"
8 | :endpoint "http://localhost:8000"})
9 |
10 | (def table-config
11 | {:name :test-table
12 | :primary-key [:id :n]
13 | :throughput {:read 5 :write 1}})
14 |
15 | (use-fixtures :once (with-local-db db-config) (with-table table-config db-config))
16 |
17 | (deftest dynamodb-local-test
18 | (is (= (far/list-tables db-config)
19 | [:test-table])))
20 |
--------------------------------------------------------------------------------
/test/clj/clojure_serverless_demo/api_test.clj:
--------------------------------------------------------------------------------
1 | (ns clojure-serverless-demo.api-test
2 | (:require [clojure-serverless-demo.api :as api]
3 | [clojure.test :refer :all]
4 | [ring.mock.request :as mock]
5 | [ring.middleware.json :refer [wrap-json-body]]))
6 |
7 | (def api (wrap-json-body (api/builder {}) {:keywords? true}))
8 |
9 | (deftest api-ping-test
10 | (is (= (api (mock/request :get "/ping"))
11 | {:status 200
12 | :headers {"Cache-Control" "max-age=0"}
13 | :body {:result "pong"}})))
14 |
15 | (deftest api-echo-test
16 | (is (= (api (-> (mock/request :post "/echo")
17 | (mock/json-body {:foo "bar"})))
18 | {:status 200
19 | :headers {"Cache-Control" "max-age=0"}
20 | :body {:foo "bar"}})))
21 |
--------------------------------------------------------------------------------
/src/clj/clojure_serverless_demo/storage.clj:
--------------------------------------------------------------------------------
1 | (ns clojure-serverless-demo.storage
2 | (:require [taoensso.faraday :as far]
3 | [clojure-serverless-demo.config :as config]))
4 |
5 | ;;require secondary index inorder to be able to sort correctly
6 | ;; scan should use filter expression to discard messsage older than 10 minutes
7 |
8 | ;;require secondary index inorder to be able to sort correctly
9 | ;; scan should use filter expression to discard messsage older than 10 minutes
10 | (defn fetch-messages [db-config]
11 | (sort-by :timestamp (far/scan db-config
12 | (:name config/table-config)
13 | {:limit 10
14 | :span-reqs {:max 1}})))
15 |
16 | (defn save-message [message db-config]
17 | (far/put-item db-config
18 | (:name config/table-config)
19 | message)
20 | {:result "success"})
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # clojure-serverless-demo
2 |
3 | A full-stack clojure application deployed to API Gateway, AWS Lambda, S3 and Dynamodb
4 |
5 | WARNING: This application was built as a proof-of-concept and as such the code should not be considered production ready.
6 |
7 | ## Installation
8 |
9 | In order to deploy this application you will need to install the Serverless Framework using npm
10 |
11 | ```
12 | # Install serverless framework
13 |
14 | npm install -g serverless
15 |
16 | # Install serverless S3 plugin
17 |
18 | npm install --save serverless-finch
19 |
20 | # Setup AWS Credentials
21 |
22 | aws configure
23 | ```
24 |
25 | ## Tests
26 |
27 | ```
28 | lein test
29 | ```
30 |
31 | ## Deployment
32 |
33 | ### Front-end application:
34 |
35 | ```
36 | lein clean
37 | lein cljsbuild once min
38 | serverless client deploy -s dev
39 | ```
40 |
41 | ### Back-end application:
42 |
43 | ```
44 | lein clean
45 | lein uberjar
46 | serverless deploy -s dev
47 | ```
48 |
49 |
50 | ## Other Libraries
51 |
52 | * https://github.com/mhjort/ring-apigw-lambda-proxy - APIGateway Ring Middleware
53 |
54 | * https://github.com/uswitch/lambada - Clojure library for writing Lambda functions
55 |
--------------------------------------------------------------------------------
/test/clj/clojure_serverless_demo/fixtures.clj:
--------------------------------------------------------------------------------
1 | (ns clojure-serverless-demo.fixtures
2 | (:require [clojure.test :as t]
3 | [taoensso.faraday :as far]
4 | [ring.adapter.jetty :refer [run-jetty]]
5 | [clojure-serverless-demo.aws.dynamodb-local :as dynamodb-local]))
6 |
7 | (defn with-local-http [handler config]
8 | (fn [f]
9 | (let [server (run-jetty handler {:port (:port config)
10 | :join? false})]
11 | (try
12 | (f)
13 | (finally
14 | (.stop server))))))
15 |
16 | (def client-opts {:access-key "ACCESSKEY"
17 | :secret-key "TOPSECRET"
18 | :endpoint "http://localhost:8000"})
19 |
20 | (defn with-local-db [db-config]
21 | (fn [f]
22 | (let [s (dynamodb-local/build-server)]
23 | (try
24 | (.start s)
25 | (f)
26 | (finally
27 | (.stop s))))))
28 |
29 | (defn with-table [table-config db-config]
30 | (fn [f]
31 | (far/ensure-table db-config
32 | (:name table-config)
33 | (:primary-key table-config)
34 | (:options table-config))
35 | (f)))
36 |
--------------------------------------------------------------------------------
/src/clj/clojure_serverless_demo/core.clj:
--------------------------------------------------------------------------------
1 | (ns clojure-serverless-demo.core
2 | (:require [clojure-serverless-demo.aws.dynamodb-local :as local-db]
3 | [ring.adapter.jetty :refer [run-jetty]]
4 | [taoensso.faraday :as far]
5 | [clojure-serverless-demo.api :as api]
6 | [clojure-serverless-demo.config :as config]))
7 |
8 | (def default {:http-server nil
9 | :db-server nil})
10 |
11 | (def server (atom default))
12 |
13 | (defn run-local-server []
14 | (let [db-config {:access-key "ACCESSKEY"
15 | :secret-key "TOPSECRET"
16 | :endpoint "http://localhost:8000"}
17 | api (api/handler (api/builder db-config))
18 | table-config config/table-config
19 | db-server (local-db/build-server)]
20 | (swap! server assoc :db-server db-server
21 | :http-server (run-jetty api {:port 8888 :join? false}))
22 | (.start db-server)
23 | (far/ensure-table db-config
24 | (:name table-config)
25 | (:primary-key table-config)
26 | (:options table-config))))
27 |
28 | (defn stop-local-server []
29 | (.stop (:http-server @server))
30 | (.stop (:db-server @server))
31 | (reset! server default))
32 |
33 | (defn -main [& args])
34 |
--------------------------------------------------------------------------------
/test/clj/clojure_serverless_demo/aws/lambda_test.clj:
--------------------------------------------------------------------------------
1 | (ns clojure-serverless-demo.aws.lambda-test
2 | (:require [clojure-serverless-demo.aws.lambda :as sut]
3 | [clojure.test :refer :all]))
4 |
5 | (deftest api-gateway-request->ring-request-test
6 | (let [api-gateway-request
7 |
8 | {:path "/ping"
9 | :queryStringParameters nil
10 | :pathParameters {:proxy "ping"}
11 | :headers {:X-Forwarded-Proto "https"
12 | :X-Forwarded-Port "443"
13 | :X-Forwarded-For "81.106.81.0, 54.182.244.13"
14 | :Host "kofu3jdkgj.execute-api.us-east-1.amazonaws.com"
15 | :User-Agent "curl/7.55.1"}
16 | :resource "/{proxy+}"
17 | :httpMethod "GET"
18 | :requestContext {:path "/dev/ping"
19 | :stage "dev"
20 | :protocol "HTTP/1.1"
21 | :resourceId "suovu9"
22 | :requestTime "17/Apr/2018:21:17:59 +0000"
23 | :requestId "ce486fc6-4284-11e8-951d-b96ffe0b5d52"
24 | :httpMethod "GET"}
25 | :body "test"
26 | :query-string "test"}
27 |
28 |
29 | result (sut/api-gateway-request->ring-request api-gateway-request)]
30 | (is (= 1 1))))
31 |
--------------------------------------------------------------------------------
/test/clj/clojure_serverless_demo/integration/http_test.clj:
--------------------------------------------------------------------------------
1 | (ns clojure-serverless-demo.integration.http-test
2 | (:require [clojure-serverless-demo.fixtures :as f]
3 | [clojure-serverless-demo.api :as api]
4 | [clojure-serverless-demo.config :as config]
5 | [clj-http.client :as http]
6 | [clojure.test :refer :all]))
7 |
8 | (def http-config
9 | {:port 8888})
10 |
11 | (def db-config {:access-key "ACCESSKEY"
12 | :secret-key "TOPSECRET"
13 | :endpoint "http://localhost:8000"})
14 |
15 | (def api (api/handler (api/builder db-config)))
16 |
17 | (def http-addr (str "http://localhost:" (:port http-config)))
18 |
19 | (use-fixtures :once
20 | (f/with-local-http api http-config)
21 | (f/with-local-db db-config)
22 | (f/with-table config/table-config db-config))
23 |
24 | (deftest api-ping-test
25 | (is (= (:body (http/get (str http-addr "/ping") {:as :auto}))
26 | {:result "pong"})))
27 |
28 | (deftest api-join-request-test
29 | (http/post (str http-addr "/join")
30 | {:form-params {:name "test-name"}
31 | :content-type :json})
32 | (http/post (str http-addr "/say")
33 | {:form-params {:name "test-name"
34 | :message "hello world"}
35 | :content-type :json})
36 | (let [results (:body (http/get
37 | (str http-addr "/fetch-messages")
38 | {:as :auto}))]
39 | (is (= ["channel" "test-name"] (map :name results)))))
40 |
--------------------------------------------------------------------------------
/src/clj/clojure_serverless_demo/api.clj:
--------------------------------------------------------------------------------
1 | (ns clojure-serverless-demo.api
2 | (:require [compojure.core :refer [GET POST defroutes]]
3 | [ring.middleware.cors :refer [wrap-cors]]
4 | [clojure-serverless-demo.chat :as chat]
5 | [clojure-serverless-demo.storage :as storage]
6 | [ring.middleware.keyword-params :refer [wrap-keyword-params]]
7 | [ring.middleware.json :refer [wrap-json-response wrap-json-body]]))
8 |
9 | (defn response [body status max-age]
10 | {:status status
11 | :body body
12 | :headers {"Cache-Control" (str "max-age=" max-age)}})
13 |
14 | (defn builder [db-config]
15 | (defroutes api
16 | (GET "/ping" []
17 | (response {:result "pong"} 200 0))
18 |
19 | (POST "/echo" {:keys [body] :as request}
20 | (response body 200 0))
21 |
22 | (GET "/fetch-messages" []
23 | (-> (storage/fetch-messages db-config)
24 | (chat/process-messages)
25 | (response 200 0)))
26 |
27 | (POST "/join" {:keys [body] :as request}
28 | (-> (chat/join body)
29 | (storage/save-message db-config)
30 | (response 200 0)))
31 |
32 | (POST "/say" {:keys [body] :as request}
33 | (-> (chat/say body)
34 | (storage/save-message db-config)
35 | (response 200 0)))))
36 |
37 | (defn handler [api]
38 | (-> (wrap-cors api :access-control-allow-origin [#".*"]
39 | :access-control-allow-methods [:get :put :post :delete])
40 | (wrap-json-body {:keywords? true})
41 | (wrap-json-response)))
42 |
--------------------------------------------------------------------------------
/serverless.yml:
--------------------------------------------------------------------------------
1 | service: clojure-serverless-demo
2 |
3 | provider:
4 | name: aws
5 | runtime: java8
6 | stage: ${opt:stage, 'dev'}
7 | region: eu-west-2
8 |
9 | # Frontend Deployment Config
10 | plugins:
11 | - serverless-finch
12 |
13 | custom:
14 | client:
15 | bucketName: clojure-serverless-demo-london-${opt:stage, 'dev'}
16 | distributionFolder: resources/public/
17 |
18 | # Backend Deployment Config
19 | package:
20 | artifact: target/clojure-serverless-demo-standalone.jar
21 |
22 | functions:
23 | simplehandler:
24 | handler: clojure_serverless_demo.SimpleHandler
25 |
26 | apihandler:
27 | handler: clojure_serverless_demo.ApiHandler
28 | events:
29 | - http:
30 | path: /{path}
31 | method: ANY
32 |
33 | resources:
34 | Resources:
35 | DynamoDbTable:
36 | Type: AWS::DynamoDB::Table
37 | Properties:
38 | TableName: messages-${self:provider.stage}
39 | AttributeDefinitions:
40 | - AttributeName: channel
41 | AttributeType: S
42 | - AttributeName: order
43 | AttributeType: N
44 | KeySchema:
45 | - AttributeName: channel
46 | KeyType: HASH
47 | - AttributeName: order
48 | KeyType: RANGE
49 | ProvisionedThroughput:
50 | ReadCapacityUnits: 20
51 | WriteCapacityUnits: 5
52 | DynamoDBIamPolicy:
53 | Type: AWS::IAM::Policy
54 | DependsOn: DynamoDbTable
55 | Properties:
56 | PolicyName: lambda-dynamodb-${self:provider.stage}
57 | PolicyDocument:
58 | Version: '2012-10-17'
59 | Statement:
60 | - Effect: Allow
61 | Action:
62 | - dynamodb:*
63 | Resource: arn:aws:dynamodb:*:*:table/messages-${self:provider.stage}
64 | Roles:
65 | - Ref: IamRoleLambdaExecution
66 |
--------------------------------------------------------------------------------
/src/cljs/clojure_serverless_demo/events.cljs:
--------------------------------------------------------------------------------
1 | (ns clojure-serverless-demo.events
2 | (:require [re-frame.core :as re-frame]
3 | [clojure-serverless-demo.db :as db]
4 | [clojure-serverless-demo.config :as config]
5 | [ajax.core :refer [GET POST]]))
6 |
7 | (re-frame/reg-event-db
8 | ::initialize-db
9 | (fn [_ _]
10 | db/default-db))
11 |
12 | (re-frame/reg-event-db
13 | ::user-submitted-message
14 | (fn [db [_ message]]
15 | (POST
16 | (str config/host "/say")
17 | {:params {:name (:name db)
18 | :message message}
19 | :format :json
20 | :response-format :json
21 | :keywords? true
22 | :handler #(re-frame/dispatch [::process-say-response %])
23 | :error-handler #(re-frame/dispatch [::bad-response %])})
24 | db))
25 |
26 | (re-frame/reg-event-db
27 | ::user-submitted-username
28 | (fn [db [_ name]]
29 | (POST
30 | (str config/host "/join")
31 | {:params {:name name}
32 | :format :json
33 | :response-format :json
34 | :keywords? true
35 | :handler #(re-frame/dispatch [::process-join-response % name])
36 | :error-handler #(re-frame/dispatch [::bad-response %])})
37 | db))
38 |
39 | (re-frame/reg-event-db
40 | ::process-join-response
41 | (fn [db [_ response name]]
42 | (re-frame/dispatch [::fetch-messages true])
43 | (assoc db :name name)))
44 |
45 | (re-frame/reg-event-db
46 | ::fetch-messages
47 | (fn [db [_ cached?]]
48 | (let []
49 | (GET
50 | (str config/host "/fetch-messages")
51 | {:response-format :json
52 | :keywords? true
53 | :handler #(re-frame/dispatch [::process-fetch-messages-response %])
54 | :error-handler #(re-frame/dispatch [::bad-response %])}
55 | ))
56 | db))
57 |
58 | (re-frame/reg-event-db
59 | ::process-say-response
60 | (fn [db [_ response]]
61 | (re-frame/dispatch [::fetch-messages true])
62 | db))
63 |
64 | (re-frame/reg-event-db
65 | ::process-fetch-messages-response
66 | (fn [db [_ response]]
67 | (assoc db :messages response)))
68 |
69 |
70 | (re-frame/reg-event-db
71 | ::bad-response
72 | (fn [db [_ response]]
73 | (.debug js/console response)
74 | db))
75 |
76 |
77 | (defn poll-for-new-messages [interval]
78 | (js/setInterval #(re-frame/dispatch [::fetch-messages false]) interval))
79 |
80 |
81 | (re-frame.core/reg-fx
82 | ::Interval
83 | (poll-for-new-messages 1000))
84 |
--------------------------------------------------------------------------------
/src/clj/clojure_serverless_demo/aws/lambda.clj:
--------------------------------------------------------------------------------
1 | (ns clojure-serverless-demo.aws.lambda
2 | (:require [clojure-serverless-demo.api :as api]
3 | [clojure-serverless-demo.config :as config]
4 | [cheshire.core :refer [generate-stream parse-stream generate-string]]
5 | [clojure.java.io :as io])
6 |
7 | (:import [com.amazonaws.services.lambda.runtime.RequestStreamHandler])
8 | (:gen-class
9 | :name clojure_serverless_demo.ApiHandler
10 | :implements [com.amazonaws.services.lambda.runtime.RequestStreamHandler]))
11 |
12 | (defn api-gateway-request->ring-request
13 | "coerce an API gateway request to a valid Ring request"
14 | [request]
15 | {:server-port (-> (:headers request) :X-Forwarded-Port Integer/parseInt)
16 | :server-name (:Host (:headers request))
17 | :remote-addr (-> (:headers request)
18 | :X-Forwarded-For
19 | (clojure.string/split #", ")
20 | first)
21 | :uri (:path request)
22 | :scheme (-> (:headers request) :X-Forwarded-Proto keyword)
23 | :protocol (:protocol (:requestContext request))
24 | :headers (into {} (for [[k v] (:headers request)]
25 | [(clojure.string/lower-case (name k)) v]))
26 | :request-method (-> (:httpMethod request)
27 | (clojure.string/lower-case)
28 | (keyword))
29 | :body (when-let [body (:body request)] (io/input-stream (.getBytes body)))
30 | :query-string (:queryStringParameters request)})
31 |
32 | (defn ring-response->api-gateway-response
33 | "coerce a Ring response to an API gateway response"
34 | [response]
35 | {:statusCode (str (:status response))
36 | :body (:body response)
37 | :headers (:headers response)})
38 |
39 | (defn wrap-api-gateway-request [f]
40 | (fn [request]
41 | (-> (api-gateway-request->ring-request request)
42 | (f)
43 | (ring-response->api-gateway-response))))
44 |
45 |
46 | (def api-gateway-handler (-> (api/builder config/db-config)
47 | (api/handler)
48 | (wrap-api-gateway-request)))
49 |
50 | (defn -handleRequest
51 | [_ input-stream output-stream context]
52 | (with-open [writer (io/writer output-stream)]
53 | (let [request (parse-stream (io/reader input-stream) true)
54 | logger (.getLogger context)
55 | _ (.log logger (str request))
56 | _ (.log logger (str (merge config/db-config config/table-config)))
57 | response (api-gateway-handler request)]
58 | (.log logger (str response))
59 | (generate-stream response writer))))
60 |
--------------------------------------------------------------------------------
/src/cljs/clojure_serverless_demo/views.cljs:
--------------------------------------------------------------------------------
1 | (ns clojure-serverless-demo.views
2 | (:require [re-frame.core :as re-frame]
3 | [clojure-serverless-demo.subs :as subs]
4 | [clojure-serverless-demo.events :as events]
5 | [reagent.core :as r]))
6 |
7 | (defn user-message-input []
8 | (let [message (r/atom "")
9 | ok-click (fn [event]
10 | (.preventDefault event)
11 | (when-not (empty? @message)
12 | (re-frame/dispatch [::events/user-submitted-message @message])
13 | (reset! message "")))
14 | fetch-click (fn [_]
15 | (re-frame/dispatch [::events/fetch-messages "foo"]))]
16 | (fn []
17 | [:form
18 | [:div.box.columns
19 | [:div.column.is-four-fifths
20 | [:input.input.is-primary {:type "text"
21 | :placeholder " something..."
22 | :value @message
23 | :on-change #(reset! message (-> % .-target .-value))}]]
24 | [:div.column.is-one-fifth
25 | [:button.button.is-primary.is-rounded
26 | {:type "input"
27 | :on-click #(ok-click %) }
28 | "Send"]]]])))
29 |
30 | (defn user-join-input []
31 | (let [name (r/atom "")
32 | ok-click (fn [event]
33 | (.preventDefault event)
34 | (when-not (empty? @name)
35 | (re-frame/dispatch [::events/user-submitted-username @name])
36 | (reset! name "")))]
37 | (fn []
38 | [:form
39 | [:div.box.columns
40 | [:div.column.is-one-fifth
41 | [:input.input {:type "text"
42 | :placeholder "enter username"
43 | :value @name
44 | :on-change #(reset! name (-> % .-target .-value))}]]
45 | [:div.column.is-one-fifth
46 | [:span
47 | [:button.button.is-primary {:type "input"
48 | :on-click #(ok-click %) }
49 | "Join"]
50 | ]]]])))
51 |
52 |
53 | (defn message-line [message]
54 | [:li.box
55 | [:span.has-text-weight-bold (:name message)]
56 | [:span " "]
57 | [:span (:message message)]])
58 |
59 | (defn message-panel []
60 | (let [messages (re-frame/subscribe [::subs/messages])]
61 | (fn []
62 | [:div
63 | (into [:ol.chat] (map message-line @messages))])))
64 |
65 | (defn join-panel []
66 | (fn []
67 | [user-join-input]))
68 |
69 | (defn main-panel []
70 | (let [name (re-frame/subscribe [::subs/name])]
71 | (fn []
72 | [:div#main.has-background-grey-lighter
73 | [:div.navbar.is-fixed-top.is-primary
74 | [:span.navbar-item.has-text-white.has-text-weight-bold "Chat"]]
75 | (if (nil? @name)
76 | [join-panel]
77 | [:div
78 | [:section.section.has-background-grey-lighter
79 | [message-panel]]
80 | [:section.section
81 | [user-message-input]]])])))
82 |
--------------------------------------------------------------------------------
/project.clj:
--------------------------------------------------------------------------------
1 | (defproject clojure-serverless-demo "0.1.0-SNAPSHOT"
2 | :dependencies [[org.clojure/clojure "1.8.0"]
3 | [org.clojure/clojurescript "1.9.908"]
4 | [reagent "0.7.0"]
5 | [re-frame "0.10.5"]
6 | [cljs-ajax "0.5.1"]
7 | [com.amazonaws/aws-lambda-java-core "1.0.0"]
8 | [clj-http "3.7.0"]
9 | [compojure "1.6.0"]
10 | [environ "1.1.0"]
11 | [ring "1.6.3"]
12 | [ring-cors "0.1.12"]
13 | [ring/ring-mock "0.3.2"]
14 | [ring/ring-json "0.4.0"]
15 | [ring/ring-jetty-adapter "1.6.3"]
16 | [com.amazonaws/DynamoDBLocal "1.10.5.1"]
17 | [com.taoensso/faraday "1.9.0"]]
18 |
19 | :repositories [["dynamodblocal" {:url "https://s3-us-west-2.amazonaws.com/dynamodb-local/release"}]]
20 |
21 | :plugins [[lein-cljsbuild "1.1.5"]]
22 |
23 | :min-lein-version "2.5.3"
24 |
25 | :source-paths ["src/clj" "test/clj"]
26 |
27 | :clean-targets ^{:protect false} ["resources/public/js/compiled" "target"]
28 |
29 | :figwheel {:css-dirs ["resources/public/css"]
30 | :ring-handler clojure-serverless-demo.handler/dev-handler}
31 |
32 | :repl-options {:nrepl-middleware [cemerick.piggieback/wrap-cljs-repl]}
33 |
34 | :profiles
35 | {:uberjar {:aot :all
36 | :uberjar-name "clojure-serverless-demo-standalone.jar"}
37 | :dev
38 | {:dependencies [[binaryage/devtools "0.9.4"]
39 | [day8.re-frame/re-frame-10x "0.3.0"]
40 | [day8.re-frame/tracing "0.5.0"]
41 | [figwheel-sidecar "0.5.13"]
42 | [com.cemerick/piggieback "0.2.2"]]
43 |
44 | :plugins [[lein-figwheel "0.5.13"]]}
45 | :prod { :dependencies [[day8.re-frame/tracing-stubs "0.5.0"]]}}
46 |
47 | :cljsbuild
48 | {:builds
49 | [{:id "dev"
50 | :source-paths ["src/cljs"]
51 | :figwheel {:on-jsload "clojure-serverless-demo.core/mount-root"}
52 | :compiler {:main clojure-serverless-demo.core
53 | :output-to "resources/public/js/compiled/app.js"
54 | :output-dir "resources/public/js/compiled/out"
55 | :asset-path "js/compiled/out"
56 | :source-map-timestamp true
57 | :preloads [devtools.preload
58 | day8.re-frame-10x.preload]
59 | :closure-defines {"re_frame.trace.trace_enabled_QMARK_" true
60 | "day8.re_frame.tracing.trace_enabled_QMARK_" true}
61 | :external-config {:devtools/config {:features-to-install :all}}
62 | }}
63 |
64 | {:id "min"
65 | :source-paths ["src/cljs"]
66 | :jar true
67 | :compiler {:main clojure-serverless-demo.core
68 | :output-to "resources/public/js/compiled/app.js"
69 | :optimizations :advanced
70 | :closure-defines {goog.DEBUG false}
71 | :pretty-print false}}
72 |
73 |
74 | ]}
75 |
76 | :main clojure-serverless-demo.core
77 |
78 |
79 | ;; :prep-tasks [["cljsbuild" "once" "min"] "compile"]
80 | )
81 |
--------------------------------------------------------------------------------