Aditya Athalye
evalapply.org
(about, contact, hire)
2025-01-24
Optionally, share:
Do you even Cloje?
user=> (map always-be-clojing? [โฅ $])
[โฅ $] ; emoji if yes, else nil
Create empty dependency file
echo '{}' > "deps.edn"
Create namespace with no-op entry point
cat > "app.clj" << 'EOF'
(ns app
(:gen-class))
;; FIXME, please. Make me do some work!
(defn -main [])
EOF
Invoke from cmd line
$ ls
deps.edn app.clj
$ clojure -M -m app # does nothing
Globally namespaced
deps.edn
src/org/evalapply/catchall_app.clj
Add basic definitions to deps.edn
{:paths ["src"]
:deps {org.clojure/clojure {:mvn/version "1.12.0"}
ring/ring-jetty-adapter {:mvn/version "1.13.0"}}}
To catchall_app.clj
(ns org.evalapply.catchall-app
(:require [ring.adapter.jetty :as jetty])
(:gen-class))
(defn echo-handler [request]
{:status 200
:headers {"Content-Type" "text/plain;charset=utf-8"}
:body (pr-str request)})
(defn run-jetty
[port]
(println "Starting jetty at: " port)
(jetty/run-jetty echo-handler
{:port port :join? false}))
(defn -main [& args]
(printl "Calling -main with args: " args)
(run-jetty 3000))
Java-like edit-compile-run cycle
clojure -M -m org.evalapply.catchall-app
curl http://localhost:3000
curl -XPOST http://localhost:3000/foobar
curl http://localhost:3000/foobar?search=%22can%20you%20read%20me%22
Run and mould one's software LIVE
(comment ; "'Rich' comment form"
;; Inspect live object
(do (require 'clojure.reflect)
(clojure.reflect/reflect server))
;; Capture values to inspect them at will.
(def responses (atom []))
(defn capture-response
[response]
(swap! responses conj response))
(add-tap capture-response)
(tap> {:status 200 :body "hi"})
;; Try Out dependencies:
;; - Add lib for current REPL session,
;; - without modifying deps.edn file
(require 'clojure.repl.deps)
(clojure.repl.deps/add-lib
'org.clojure/data.json {:mvn/version "2.5.1"})
;; Temporarily replace top-level defn
(defn echo-handler
"TODO:
- copy down body of top-level defn
- handle json=true query param
- evaluate to replace definition"
[]))
"It's Just Data and Functions"
deps.edn
build.clj
src/org/evalapply/catchall_app.clj
test/org/evalapply/catchall_app_test.clj
deps.clj
: Add real-world HTTP middlewarebuild.clj
: deps are data, builds are programssrc
: make custom routertest
: to avoid creating a scandal :)
Mix in Libraries as needed
{:paths ["src"]
:deps {org.clojure/clojure {:mvn/version "1.12.0"}
;; Ring HTTP utilities: https://github.com/ring-clojure/ring
ring/ring-core {:mvn/version "1.12.2"}
ring/ring-jetty-adapter {:mvn/version "1.12.2"} ; embedded Jetty
ring-cors/ring-cors {:mvn/version "0.1.13"}
;; System composition and configuration: https://github.com/weavejester/integrant
integrant/integrant {:mvn/version "0.13.0"} ; define/start/stop system
aero/aero {:mvn/version "1.1.6"} ; EDN-file-based configuration, might not need it
;; HTTP Routing and coercion: https://github.com/metosin/reitit
metosin/reitit-core {:mvn/version "0.7.2"} ; routing core
metosin/reitit-ring {:mvn/version "0.7.2"} ; ring router
metosin/reitit-middleware {:mvn/version "0.7.2"} ; common middleware
metosin/reitit-malli {:mvn/version "0.7.2"} ; malli coercion
;; HTTP API format negotiation, encoding and decoding
metosin/muuntaja {:mvn/version "0.6.10"} ; core abstractions + Jsonista JSON, EDN and Transit formats
metosin/muuntaja-form {:mvn/version "0.6.10"} ; application/x-www-form-urlencoded formatter using ring-codec
;; Data Utilities
metosin/malli {:mvn/version "0.16.4"} ; specify, validate, coerce data
;; Database Utilities
com.github.seancorfield/next.jdbc {:mvn/version "1.3.939"} ; JDBC adapter
org.xerial/sqlite-jdbc {:mvn/version "3.46.1.0"} ; SQLite JDBC driver
com.zaxxer/HikariCP {:mvn/version "6.0.0"} ; connection pooling
;; Web Frontend
hiccup/hiccup {:mvn/version "2.0.0-RC3"} ; Server-rendered HTML as Clojure data
;; Cryptography, Authentication, and Authorization
buddy/buddy-auth {:mvn/version "3.0.1"} ; authenticate, authorize
;; buddy/buddy-hashers {:mvn/version "2.0.167"} ; hashing utils
;; buddy/buddy-sign {:mvn/version "3.6.1-359"} ; High level message signing library.
;; Time
clojure.java-time/clojure.java-time {:mvn/version "1.4.2"}
;; Logging
org.clojure/tools.logging {:mvn/version "1.3.0"}
org.slf4j/slf4j-simple {:mvn/version "2.0.16"}}}
courtesy: @lambduhh
"It's simple to be happy, difficult to be simple." - Bawarchi
"Slow is smooth. Smooth is Fast." - The SEALs
Aditya Athalye
evalapply.org/hire
I help small B2B SaaS teams deploy writing culture as a 10x-ing strategy.
(Also can Cloje up that Micro SaaS you want to build!)
https://x.com/jordwalke/status/1720370436322857327
Playtime:
| X * Y | y1 | y2 | y3 | ... |
|-------+----+----+----+-----|
| x1 | | | | |
| x2 | | | | |
| x3 | | | | |
| ... | | | | |
x
Classes| Method * Class | FormSubmit | Dropdown | CheckList | ... |
|----------------+------------+----------+-----------+-----|
| click! | | | | |
| exists? | | | | |
| visible? | | | | |
| select! | | | | |
| deselect! | | | | |
| ... | | | | |
x
Types| fn * <T> | List | Array | Tuple | HashMap | ... |
|----------+------+-------+-------+---------+-----|
| map | | | | | |
| filter | | | | | |
| take | | | | | |
| drop | | | | | |
| ... | | | | | |
x
Domain Entity
Expression Problem?
| Data x Entity | Person | Persons | Singles | Couples |
|-------------------+--------+---------+---------+---------|
| List[Pair[?,?]] | | | | |
| Array[Tuple[?,?]] | | | | |
| HashMap[?,?] | | | | |
| ... | | | | |
x
HTTP requests
What if we frame in terms of Expression Problem?
| Verb * Request | /slug-1/ | /slug-2/ | /slug-3/ | ... |
|----------------+----------+----------+----------+-----|
| GET | | | | |
| PUT | | | | |
| POST | | | | |
| DELETE | | | | |
| PATCH | | | | |
Expression Problem?
| Parts * Apps | app-1 | app-2 | service-1 | service-2 | ... |
|--------------+-------+-------+-----------+-----------+-----|
| primary DB | | | | | |
| cache | | | | | |
| queue | | | | | |
| web server | | | | | |
| settings | | | | | |
Expression Problem?
| Infra * Use | primary DB | cache | queue | search | ... |
|-------------+------------+-------+-------+--------+-----|
| File System | | | | | |
| SQLite | | | | | |
| Postgres | | | | | |
| Redis | | | | | |
| Kafka | | | | | |
| Server * Client | Chrome | FF | MobileApps | curl | ... |
|-----------------+--------+----+------------+------+-----|
| nginx | | | | | |
| node | | | | | |
| jetty | | | | | |
| hunchertoot | | | | | |
| caddy | | | | | |
| WSGI | | | | | |
| CGI | | | | | |
| Gunicorn | | | | | |
| etc... | | | | | |
HTTP Request -> app -> HTTP Response
HTTP Request -> app -> HTTP Response
HTTP request ->
/pattern-1/ method-1
/pattern-2/ method-2
/pattern-3/ method-3
-> HTTP response
HTTP Request -> app -> HTTP Response
HTTP request ->
GET /uri-1/ getter-method
PUT /uri-1/ putter-method
POST /uri-1/ poster-method
PATCH /uri-1/ patcher-method
DELETE /uri-1/ deleter-method
-> HTTP response
Polymorphic Dispatch Machine
curl -v https://www.evalapply.org/posts/hello-world/index.html
Request
> GET /posts/hello-world/index.html HTTP/2
> Host: www.evalapply.org
> User-Agent: curl/8.5.0
> Accept: */*
>
Response
< HTTP/2 200
< date: Sun, 19 Jan 2025 13:00:58 GMT
< content-type: text/html; charset=utf-8
< last-modified: Tue, 07 Jan 2025 18:40:29 GMT
< access-control-allow-origin: *
Response (contd…)
< expires: Sun, 19 Jan 2025 13:10:21 GMT
< cache-control: max-age=600
< x-proxy-cache: MISS
< x-github-request-id: 30B3:11B9:1374016:13A8BA4:678CF765
< via: 1.1 varnish
< age: 0
< x-served-by: cache-mrs10566-MRS
< x-cache: HIT
< x-cache-hits: 1
< x-timer: S1737291658.048786,VS0,VE119
< vary: Accept-Encoding
Response (contd…)
< x-fastly-request-id: 5acec03e79d6fd85384ab5d43d0d862a41261297
< cf-cache-status: DYNAMIC
< report-to: {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=ovUNa9WMW%2FtaMfQlrsj3fsvm9XCyTXlWOzIO5PNfH6TVZx4aoWGLJVyGNFBP4KdhlwJGsd%2FJ5q3c1x2zTX2rzEBVX0HQd27jZNAfXR%2FwavmW8XexYZ8RnSjr3a2ORaE66mFxQ%2FAKRGqDdYP73FBBBA%3D%3D"}],"group":"cf-nel","max_age":604800}
< nel: {"success_fraction":0,"report_to":"cf-nel","max_age":604800}
< server: cloudflare
< cf-ray: 904702be4c31076a-MRS
< alt-svc: h3=":443"; ma=86400
< server-timing: cfL4;desc="?proto=TCP&rtt=173437&min_rtt=165923&rtt_var=60025&sent=8&recv=9&lost=0&retrans=0&sent_bytes=3416&recv_bytes=778&delivery_rate=18968&cwnd=254&unsent_bytes=0&cid=ad859fdfe8400633&ts=428&x=0"
<
{Any, Any}
{:scheme :http,
:request-method :get,
:uri "/foo/bar/baz",
:headers {"accept" "*/*",
"user-agent" "curl/7.81.0",
"host" "localhost:3000"},
:query-string "search=wassup%20world",
:body #object[org.eclipse.jetty.server.HttpInput 0x2a91914a "HttpInput@714182986 cs=HttpChannelState@2eae00c0{s=HANDLING rs=BLOCKING os=OPEN is=IDLE awp=false se=false i=true al=0} cp=org.eclipse.jetty.server.BlockingContentProducer@6bac9b71 eof=false"]}
{:status 200
:headers {"Content-Type" "text/html;charset=UTF-8"}
:body "<h1>optional</h1>"}
[:div {:class "wow-list"}
[:ul (list
[:li "such data"]
[:li "much generic"]
[:li "very html"]
[:li "wow!"])]]