Skip to content

mtumilowicz/clojure-ring-reitit-h2-workshop

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

72 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Build Status License: GPL v3

clojure-ring-reitit-h2-workshop

preface

  • it may be worthwhile to refer first (basics)
  • goals of this workshop
    • introduction into clojure web development: ring, reitit
    • introduction to validation with struct
    • show how to integrate with relational db: conman, mount, hugsql
    • advanced clojure features: threading, polymorphism, macros, pattern matching
    • modeling domain with records
    • practicing destructuring
  • note that in this project we have standard dependency injection (map with dependencies passed as a first param)
    • components would be subject to different workshops
  • workshop plan
    • add PATCH method - to edit parts of person data

macros

  • most distinguishing feature of Clojure when compared to Java etc
  • Clojure runtime processes source code differently compared to other languages
    1. read phase: reader converts a stream of characters (the source code) into Clojure data structures
    2. evaluation phase: data structures are evaluated to execute the program
    • Clojure offers a hook between the two phases
      • allow code to be modified programmatically before evaluation
  • hello world example
    • similar tradition when it comes to explaining macros
      • add unless control structure to the language
    • without macros it is impossible
      (defn unless [test then] (if (not test) then)) 
      • all functions execute according to the following rules
        • evaluate all arguments passed to the function call form
        • evaluate the function using the values of the arguments
      • remark: you can pass thunk (function) instead of then to delay evaluation (it changes sematics)
        (defn unless [test then] (if (not test) (then))) 
    • with macros
      (defmacro unless [test then] `(if (not ~test) ~then)) 
  • syntax
    • templating: backquote (`)
    • inserting value: ~
  • verifying macros
    • macroexpand-1
      • if form represents a macro form, returns its expansion, else returns form
    • macroexpand
      • repeatedly calls macroexpand-1 on form until it no longer represents a macro form, then returns it
    • macroexpand vs macroexpand-1
      (defmacro inner-macro [arg] `(println ~arg)) (defmacro top-level-macro [arg] `(inner-macro ~arg)) (macroexpand-1 '(inner-macro "hello")) // (clojure.core/println "hello") (macroexpand-1 '(top-level-macro "hello")) // (user/inner-macro "hello") (macroexpand '(top-level-macro "hello")) // (clojure.core/println "hello") 
  • macros in syntax: when, when-not, cond, if-not, etc

polymorphism

  • two approaches: multimethods and protocols
  • multimethod
    • example
      (defmulti make-sound :type) (defmethod make-sound :Dog [x] "Woof Woof") (defmethod make-sound :Cat [x] "Miauuu") (make-sound {:type :Dog}) => "Woof Woof" (make-sound {:type :Cat}) => "Miauuu" 
    • defmulti: name, signature and dispatch function
      • note that dispatch function can be any function
        (def QUICK-SORT-THRESHOLD 5) (defmulti my-sort (fn [arr] (if (every? integer? arr) :counting-sort (if (< (count arr) QUICK-SORT-THRESHOLD) :quick-sort :merge-sort)))) 
        • in an OOP language this behavior can’t be implemented in a Polymorphic way
          • it would have to be a code with an if statement
    • defmethod: function implementation for a particular dispatch value
    • default case: if no method is associated with the dispatching value, the multimethod will look for a method associated with the default dispatching value (:default), and will use that if present
  • protocols
    • replace what in an OOP language we know as interfaces
    • example
      (defprotocol Shape (area [this]) (perimeter [this])) (defrecord Rectangle [width length] Shape (area [_] (* width length)) (perimeter [_] (+ (* 2 width) (* 2 length)))) (defrecord Square [side] Shape (area [_] (* side side)) (perimeter [_] (* 4 side))) (def square (->Square 4)) (def rectangle (->Rectangle 2 5)) (map area [square rectangle]) // (16 10) (map :side [square rectangle]) // (4 nil) (map :length [square rectangle]) // (nil 5) 
  • protocols are usually preferred for type-based dispatch
    • have the ability to group related functions together in a single protocol

syntax

  • threading
    • example
      (- (/ (+ c 3) 2) 1) // difficult to read, because it’s written inside out 
      vs
      (-> c (+ 3) (/ 2) (- 1)) 
    • thread first: ->
      • takes the first argument supplied and place it in the second position of the next expression
      • example of how a macro can manipulate code to make it easier to read
        • something like this is nearly impossible in most other languages
      • useful for pulling nested data structures
        (-> person :employer :address :city) 
        vs
        (:city (:address (:employer person))) 
    • thread last: ->>
      • takes the first expression and moving it into the last place
      • common use case: working with sequences + using map, reduce and filter
        • these functions accepts the sequence as the last element
    • two related ones: some-> and some->>
      • computation ends if the result of any step in the expansion is nil
  • records
    • custom, maplike data types (associate keys with values)
    • example
      (defrecord Person [fname lname address]) (defrecord Address [street city state zip]) (def stu (map->Planet {:fname "Stu" :lname "Halloway" :address (map-Address { :street "200 N Mangum" :city "Durham" :state "NC" :zip 27701}) })) // factory methods map-EntityName; expects map 
    • most of the time records are a better choice for domain entities than Map
    • internals
      • dynamically generate compiled bytecode for a named class with a set of given fields
      • fields can have type hints, and can be primitive
      • provides a complete implementation of a persistent map
        • value-based equality and hashCode
        • associative support
        • keyword accessors for fields
        • extensible fields (you can assoc keys not supplied with the defrecord definition)
      • deftype vs defrecord
        • deftype creates a bare-bones object which implements a protocol
        • defrecord creates an immutable persistent map which implements a protocol
  • destructuring
    • is a way to concisely bind names to the values inside a data structure
    • is broken up into two categories
      • sequential destructuring
        (let [[x y z] my-vector] (println x y z)) 
        • if the vector is too small, the extra symbols will be bound to nil
      • associative destructuring
        (defn greet-user [person] (let [first (:first-name person) last (:last-name person)] (println "Welcome," first last))) 
        • we can destructure it in method declaration
          (defn greet-user [{first :first-name last :last-name}] (println "Welcome," first last))) 
        • :or - supply a default value if the key is not present
          (defn greet-user [{first :first-name last :last-name} :or {:first "unknown" :last "unknown"}] (println "Welcome," first last))) 
        • :as - binds whole argument
          (defn greet-user [{first :first-name last :last-name} :as person] (println "Welcome," first last))) 
        • :keys - if local bindings and keys are the same
          (defn greet-user [{:keys [first-name last-name]}] (println "Welcome," first-name last-name)) 
  • pattern matching
    • adds pattern matching support to the Clojure programming language
    • examples
      • vector
        (let [x [1 2 3]] (match [x] [[_ _ 2]] :a0 [[1 1 3]] :a1 [[1 2 3]] :a2 :else :a3)) 
      • map
        (let [x {:a 1 :b 1}] (match [x] [{:a _ :b 2}] :a0 [{:a 1 :b 1}] :a1 [{:c 3 :d _ :e 4}] :a2 :else nil)) 
      • types that we not control
        • define accessors
          (extend-type java.util.Date clojure.core.match.protocols/IMatchLookup (val-at [this k not-found] (case k :day (.getDay this) :month (.getMonth this) :year (.getYear this) not-found))) 
        • and then pattern match
          (match [(java.util.Date. 2010 10 1 12 30)] [{:year 2009 :month a}] a [{:year (:or 2010 2011) :month b}] b :else :no-match) 

validation

  • using: https://github.com/funcool/struct
    • structural validation library
  • features
    • no macros: validators are defined using plain data.
    • dependent validators: the ability to access to already validated data
    • coercion: the ability to coerce incoming values to other types
    • no exceptions: no exceptions used in the validation process
  • by default
    • all validators are optional
      • to make the value mandatory, you should use a specific required validator
    • additional entries in the map are not stripped
      • (st/validate +scheme+ {:strip true})
  • example
    (require '[struct.core :as st]) (def MoviePremierScheme {:name [st/required st/string] :year [st/required st/number]}) (-> {:name "Blood of Elves" :year 1994} (st/validate MoviePremierScheme)) // [nil {:name "Blood of Elves" :year 1994}] (-> {:year "1994"} (st/validate MoviePremierScheme)) // [{:name "this field is mandatory", :year "must be a number"} {}] 
    • then we could pattern match
  • support for nested data structures
    (def PersonScheme {[:personal-details :born-year] st/integer [:address :street] st/string}) 
  • custom messages
    (def schema {:age [[st/in-range 18 26 :message "The age must be between %s and %s"]]}) 
    • wildcards will be replaced by args of validator
  • coercions
    (def schema {:year [[st/integer :coerce str]]}) (def schema {:year [st/required st/integer-str] // predefined coercions :id [st/required st/uuid-str]}) 

mount

  • how to manage state in application
    • either use components or mount
    • components vs mount
      • framework vs lib
      • if a managing state library requires a whole app buy-in - it is a framework
        • dependency graph is usually quite large and complex
          • it has everything (every piece of the application) in it
      • if stateful things are kept lean and low level (i.e. I/O, queues, threads, connections, etc.), dependency graphs are simple and small
        • everything else is just namespaces and functions: the way it should be
  • provides a defstate macro that allows us to declare something which can be started and stopped
    • example: database connection, a thread-pool, or an HTTP server
      • sometimes referred to as resources
    • we provide :start and :stop keys
      • specify the code that should run when the resource is started and stopped, respectively
    • once a resource is started, the return value of the :start function is bound to the symbol we used in our defstate
  • example
    • defining
      (require '[mount.core :refer [defstate]]) (defstate conn :start (create-conn)) 
    • using
      (ns app (:require [above :refer [conn]])) 
  • to make the application state enjoyably reloadable
    • mount has start and stop functions that will walk all the states created with defstate and start / stop them accordingly
    • reloading with REPL
      (mount/stop) (mount/start) 
  • dependencies are "injected" by requiring on the namespace level
    • mount trusts the Clojure compiler to maintain the start and stop order for all the defstates

conman

  • luminus database connection management and SQL query generation library
  • provides pooled connections using the HikariCP library
  • queries are generated using HugSQL and wrapped with connection aware functions
  • HugSql
    • can find any SQL file in your classpath
    • example
      • first, define in users.sql file:
        -- :name add-user! :! :n -- :doc adds a new user INSERT INTO users (id, pass) VALUES (:id, :pass) 
      • then
        (hugsql/def-db-fns "users.sql") (add-user! db {:id "hug" :pass "sql"}) 
        • function accepts the database connection as its first parameter, followed by the query map
          • keys in the map have the same names as those we defined earlier in the users.sql file
    • uses specially formatted SQL comments as metadata for defining functions
      • :name - name of the function
      • :execute or :! — can be used for any statement
        • indicates that the function modifies the data
      • :query or :? — indicates a query with a result set
      • :returning-execute or :<! — is used to indicate an INSERT … RETURNING
      • :insert or :i! — attempts to return the generated keys
      • return hints
        • :one or :1 — a result with a single row
        • :many or :* — a result with multiple rows
        • :affected or :n — the number of affected rows
        • :raw — the result generated by the underlying database adapter
      • placeholder keys for the VALUES
        • HugSQL uses these keys to look up the parameters in the input map when the generated function is called
  • queries are bound to the connection using the bind-connection macro
    • macro accepts the connection var followed by one or more strings representing SQL query files
    • example
      (conman/bind-connection *db* "sql/queries.sql") 
    • bind-connection generates functions from sql in the current namespace
  • connect! function should be called to initialize the database connection
    (defstate ^:dynamic *db* :start (conman/connect! {:jdbc-url (env :database-url)}) :stop (conman/disconnect! *db*)) 
  • it's possible to use the with-transaction macro to rebind it to the transaction connection

config

  • cprop.core
  • loads an EDN config from a classpath and/or file system and merges it with system properties and ENV variables
    • returns an (immutable) map
      • working with a config is no different than just working with a map
    • edn digression
      • extensible data notation
      • used by Datomic and other applications as a data transfer format
      • includes keywords, symbols, strings, numbers, lists, sets, vectors, and maps
      • tags are the core differentiator
        • reason why it's called Extensible Data Notation
        • character allows the subsequent form to be parsed in a special way

        • example
          • #uuid tag converts a string representation of a UUID into the environment’s underlying UUID implementation (e.g. java.util.UUID )
  • example
    • definition
      {:datomic {:url "datomic:sql://?jdbc:postgresql://localhost:5432/datomic?user=datomic&password=datomic"} :source {:account {:rabbit {:host "127.0.0.1" :port 5672 :vhost "/z-broker" :username "guest" :password "guest"}}} :answer 42} 
    • loading
      (def conf (load-config)) (conf :answer) // 42 
  • by default cprop would look in two places for configuration files:
    • classpath: for the config.edn resource
    • file system: for a path identified by the conf system property

ring

  • is a Clojure web applications library
  • higher level frameworks such as Compojure or lib-noir use Ring as a common basis
    • good to have a basic understanding of Ring
  • supports synchronous and asynchronous endpoints and middleware
    • we focus here only on synchronous part
  • four components:
    • handler
      • synchronous handlers
        (defn what-is-my-ip [request] {:status 200 :headers {"Content-Type" "text/plain"} :body (:remote-addr request)}) 
    • request
      • represented by Clojure maps
      • some standard keys: :headers, :body, :content-type, :path-params
    • response
      • created by the handler
      • contains three keys: :status, :headers, :body
    • middleware
      • higher-level functions
      • adds additional functionality to handlers
      • first argument of a middleware function should be a handler
      • its return value should be a new handler function
      • threading macro (->) can be used to chain middleware together
        (def app (-> handler (wrap-content-type "text/html") (wrap-keyword-params) (wrap-params))) 
      • muuntaja/wrap-formats
        • negotiates a request body based on accept, accept-charset and content-type headers
          • decodes the body with an attached Muuntaja instance into :body-params
        • encodes also the response body
      • query params
        • required: [app.gateway.middleware :refer [wrap-params]
          • adds three new keys to the request map:
            • :query-params - map of parameters from the query string
            • :form-params - map of parameters from submitted form data
            • :params - merged map of all parameters
          • example
            • :query-string "q=clojure" is transformed into :query-params {"q" "clojure"}

reitit

  • fast data-driven router for Clojure
  • routes are defined as vectors
    • [String path and optional (non-sequential) route argument]
  • paths can have path-parameters (:id) or catch-all-parameters (*path)
  • example
    • define routes
      ["/api" ["/admin" {:middleware [middleware-wrappers...]} ["" admin-handler] ["/db" db-handler]] ["/ping" ping-handler]] 
    • and create router
      (def router (r/router routes)) 
  • coercion
    • process of transforming parameters (and responses) from one format into another
    • by default, all wildcard and catch-all parameters are parsed into strings
    • problem
      (def router (r/router ["/:company/users/:user-id" ::user-view])) // :path-params {:company "metosin", :user-id "123"}, 
      but we would like to treat user-id as a number, so
      (def router (r/router ["/:company/users/:user-id" {:name ::user-view // {:path {:company "metosin", :user-id 123}} :coercion reitit.coercion.schema/coercion :parameters {:path {:company s/Str :user-id s/Int}}}] {:compile coercion/compile-request-coercers})) 
  • default handlers
    (reitit/create-default-handler {:not-found (constantly (response/not-found "404 - Page not found")) :method-not-allowed (constantly (response/method-not-allowed "405 - Not allowed")) :not-acceptable (constantly (response/not-acceptable "406 - Not acceptable"))}))) 
  • exception handling
    • good practice
      • add {:exception pretty/exception}
      • it makes routing errors (for example conflicting route) increases readability of errors
      • only for exceptions thrown in router creation
  • middleware
    • debugging
      • reitit.ring.middleware.dev/print-request-diffs
        • prints a request and response diff between each middleware
    • custom param decoders
      • example
        (def new-muuntaja (m/create (-> m/default-options (assoc-in [:formats "application/json" :decoder-opts :bigdecimals] true) (assoc-in [:formats "application/json" :encoder-opts :date-format] "yyyy-MM-dd")))) 

Releases

No releases published

Packages

No packages published