Skip to content

Commit 6f42863

Browse files
committed
Update url-shortener project
Added: webserver, web API, and frontend.
1 parent 2820373 commit 6f42863

File tree

11 files changed

+1721
-47
lines changed

11 files changed

+1721
-47
lines changed

url-shortener/bun.lockb

41.4 KB
Binary file not shown.

url-shortener/package-lock.json

Lines changed: 1421 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

url-shortener/package.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"name": "url-shortener",
3+
"version": "0.1.0",
4+
"main": "index.js",
5+
"directories": {
6+
"test": "test"
7+
},
8+
"devDependencies": {
9+
"shadow-cljs": "^2.27.2"
10+
},
11+
"dependencies": {
12+
"react": "^18.2.0",
13+
"react-dom": "^18.2.0"
14+
}
15+
}

url-shortener/project.clj

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,29 @@
11
(defproject url-shortener "0.1.0-SNAPSHOT"
22
:description "URL shortener app"
33

4-
:dependencies [[org.clojure/clojure "1.11.1"]]
4+
:source-paths ["src" "resources"]
5+
6+
:dependencies [;; Backend
7+
[org.clojure/clojure "1.11.1"]
8+
[ring/ring-jetty-adapter "1.11.0"]
9+
[ring/ring-json "0.5.1"]
10+
[compojure "1.7.1"]
11+
12+
;; Frontend
13+
[reagent "1.2.0"]
14+
[org.clojure/core.async "1.6.681"]
15+
[cljs-http "0.1.48"
16+
:exclusions [org.clojure/core.async
17+
com.cognitect/transit-cljs
18+
com.cognitect/transit-js]]
19+
[thheller/shadow-cljs "2.27.2"] ; Keep it synced with npm version!
20+
]
521

622
:main ^:skip-aot url-shortener.core
723

824
:uberjar-name "url-shortener.jar"
925

1026
:resource-paths ["resources"]
1127

12-
:profiles {:uberjar {:aot :all}})
28+
:profiles {:dev {:dependencies [[ring/ring-devel "1.11.0"]]}
29+
:uberjar {:aot :all}})
15 KB
Binary file not shown.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<!doctype html>
2+
<html>
3+
4+
5+
<head>
6+
<meta charset="utf-8" />
7+
<title>URL Shortener</title>
8+
<link rel="stylesheet" href="https://unpkg.com/blocks.css/dist/blocks.min.css" />
9+
</head>
10+
11+
<body>
12+
<div id="root"></div>
13+
<script src="/js/main.js"></script>
14+
</body>
15+
16+
</html>

url-shortener/shadow-cljs.edn

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{:lein true
2+
; :source-paths and :dependencies are now ignored in this file
3+
; configure them via project.clj
4+
:builds {:app {:target :browser
5+
:output-dir "resources/public/js"
6+
:modules {:main {:entries [url-shortener.frontend]
7+
:init-fn url-shortener.frontend/init}}}}}
Lines changed: 60 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,78 @@
11
(ns url-shortener.core
2-
(:require
3-
[clojure.string :as string]))
2+
(:require [clojure.string :as string]))
43

4+
;; Consts
5+
(def ^:const alphabet-size 62)
56

6-
(def symbols
7-
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
7+
(def alphabet
8+
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz")
89

910

10-
;; =============================================================================
11-
;; Number -> String
12-
;; =============================================================================
13-
14-
15-
(defn get-idx [i]
16-
(Math/floor (/ i 62)))
17-
18-
19-
(defn get-symbol-by-idx [i]
20-
(get symbols (rem i 62)))
21-
22-
23-
(defn id->url [id]
24-
(let [idx-sequence (iterate get-idx id)
25-
valid-idxs (take-while #(> % 0) idx-sequence)
26-
code-sequence (map get-symbol-by-idx valid-idxs)]
27-
(string/join (reverse code-sequence))))
28-
11+
;; Logic
12+
(defn- get-idx [i]
13+
(Math/floor (/ i alphabet-size)))
2914

15+
(defn- get-character-by-idx [i]
16+
(get alphabet (rem i alphabet-size)))
3017

18+
(defn int->id [id]
19+
(if (< id alphabet-size)
20+
(str (get-character-by-idx id))
21+
(let [codes (->> (iterate get-idx id)
22+
(take-while pos?)
23+
(map get-character-by-idx))]
24+
(string/join (reverse codes)))))
3125

3226
(comment
33-
(get-idx 1000)
34-
(Math/floor (/ 1000 62))
35-
36-
(take 10 (iterate get-idx 10))
37-
(take 10 (iterate get-idx 100))
38-
(take 10 (iterate get-idx 5000))
27+
(int->id 0) ; => "0"
28+
(int->id alphabet-size) ; => "10"
29+
(int->id 9999999999999) ; => "2q3Rktod"
30+
(int->id 9223372036854775807) ; => "AzL8n0Y58W7"
31+
)
32+
33+
(defn id->int [id]
34+
(reduce (fn [id ch]
35+
(+ (* id alphabet-size)
36+
(string/index-of alphabet ch)))
37+
0
38+
id))
3939

40-
(get-symbol-by-idx 5000)
40+
(comment
41+
(id->int "0") ; => 0
42+
(id->int "z") ; => 61
43+
(id->int "clj") ; => 149031
44+
(id->int "Clojure") ; => 725410830262
45+
)
4146

42-
(id->url 12345) ;; "dnh"
43-
(id->url 3294233727)) ;;"dK6qQd"
4447

48+
;; State
49+
(defonce ^:private *counter (atom 0))
50+
(defonce ^:private *mapping (ref {}))
4551

46-
;; =============================================================================
47-
;; String -> Number
48-
;; =============================================================================
4952

53+
;; API
54+
(defn shorten!
55+
([url]
56+
(let [id (int->id (swap! *counter inc))]
57+
(or (shorten! url id)
58+
(recur url))))
59+
([url id]
60+
(dosync
61+
(when-not (@*mapping id)
62+
(alter *mapping assoc id url)
63+
id))))
5064

51-
(defn url->id [url]
52-
(let [url-symbols (seq url)]
53-
(reduce
54-
(fn [id symbol]
55-
(+ (* id 62)
56-
(string/index-of symbols symbol)))
57-
0
58-
url-symbols)))
65+
(defn url-for [id]
66+
(@*mapping id))
5967

68+
(defn list-all []
69+
(mapv #(-> {:id (key %) :url (val %)}) @*mapping))
6070

6171
(comment
62-
(url->id "dnh") ;; 12345
63-
(url->id "dK6qQd")) ;; 3294233727
72+
(shorten! "http://clojurebook.com")
73+
(shorten! "https://clojure.org" "clj")
74+
(shorten! "http://id-already-exists.com" "clj")
75+
(shorten! "https://clojurescript.org" "cljs")
76+
77+
(url-for "clj")
78+
(list-all))
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
(ns url-shortener.frontend
2+
(:require-macros [cljs.core.async.macros :refer [go]])
3+
(:require [reagent.core :as reagent]
4+
[reagent.dom.client :as rdomc]
5+
[cljs-http.client :as http]
6+
[cljs.core.async :refer [<!]]))
7+
8+
(defonce *app-state (reagent/atom {:page :main
9+
:short-url ""}))
10+
11+
(defn header [text]
12+
[:h2 {:style {:margin "8px 4px"}}
13+
text])
14+
15+
(defn short-page []
16+
(let [short-url (:short-url @*app-state)]
17+
[:<>
18+
[header "Your short link"]
19+
[:div {:style {:margin "16px 4px"}}
20+
[:a {:target "_blank"
21+
:href short-url}
22+
short-url]]
23+
[:button.block {:on-click (fn [_e]
24+
(swap! *app-state assoc :page :main))}
25+
"BACK"]]))
26+
27+
(defn main-page []
28+
(let [*input-value (reagent/atom "")]
29+
(fn []
30+
[:<>
31+
[header "Shorten a long link"]
32+
[:label {:for "url-input"
33+
:style {:margin-left 4}}
34+
"Paste a long URL"]
35+
[:div {:style {:display "flex"}}
36+
[:input.block.fixed {:id "url-input"
37+
:type "url"
38+
:placeholder "Example: http://super-long-link.com/"
39+
:style {:width "80%"}
40+
:value @*input-value
41+
:on-change (fn [e]
42+
(reset! *input-value (-> e .-target .-value)))}]
43+
[:button.block.accent
44+
{:on-click (fn [_e]
45+
(when (seq @*input-value)
46+
(go (let [response (<! (http/post "/" {:json-params {:url @*input-value}}))]
47+
(if (:success response)
48+
(let [id (-> response :body :id)
49+
host (.. js/window -location -href)
50+
short-url (str host id)]
51+
(js/console.log short-url)
52+
(reset! *app-state {:short-url short-url
53+
:page :short}))
54+
(js/console.log "Something went wrong: " response))))))}
55+
"SHORTEN IT"]]])))
56+
57+
(defn app []
58+
(let [page (:page @*app-state)]
59+
[:div.card.fixed.block {:style {:margin "0 auto"
60+
:font-family "Arial, serif"
61+
:width "800px"}}
62+
(case page
63+
:main [main-page]
64+
:short [short-page]
65+
[header "Page not found"])]))
66+
67+
(defonce root-el
68+
(rdomc/create-root (js/document.getElementById "root")))
69+
70+
(defn ^:dev/after-load mountit []
71+
(rdomc/render root-el [app]))
72+
73+
(defn init []
74+
(mountit))
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
(ns url-shortener.main
2+
(:require [ring.adapter.jetty :as jetty]
3+
[url-shortener.web :as web])
4+
(:import [org.eclipse.jetty.server Server]))
5+
6+
(set! *warn-on-reflection* true)
7+
8+
(defn start-server
9+
([] (start-server {}))
10+
([opts]
11+
(let [server (jetty/run-jetty #'web/handler opts)]
12+
(println "Server started.")
13+
server)))
14+
15+
(defn stop-server [server]
16+
(.stop ^Server server)
17+
(println "Server stopped."))
18+
19+
(defn -main [& _]
20+
(let [server (start-server)]
21+
(.addShutdownHook
22+
(Runtime/getRuntime)
23+
(Thread. ^Runnable #(stop-server server)))))
24+
25+
(comment
26+
(def server (start-server {:port 8000 :join? false}))
27+
(stop-server server)
28+
29+
(require '[clojure.java.shell :refer [sh]])
30+
31+
(sh "curl" "-X" "POST"
32+
"-H" "Content-Type: application/json"
33+
"http://localhost:8000/"
34+
"-d" "{\"url\": \"https://clojurescript.org/\"}")
35+
36+
(sh "curl" "-X" "PUT"
37+
"-H" "Content-Type: application/json"
38+
"http://localhost:8000/clj"
39+
"-d" "{\"url\": \"https://clojure.org/\"}")
40+
41+
(sh "curl" "-i" "http://localhost:8000/clj")
42+
43+
(sh "curl" "http://localhost:8000/list/")
44+
)
45+
46+

0 commit comments

Comments
 (0)