| Introduction |
| ------------------------------------------------------------ |
| |
| dicho is a Clojure library for creating standardized success |
| and error responses. It provides a structured way to handle |
| responses in your application, ensuring consistency and type |
| safety. The library also integrates with other tools like |
| failjure [1] for seamless error handling. |
| |
| |
| Features |
| ------------------------------------------------------------ |
| |
| - Success Responses (OkResponse): |
| - Includes a :status field (always :ok) and a :result |
| field for the actual data. |
| - Supports optional metadata fields like :trace-id and |
| :timestamp. |
| |
| - Error Responses (ErrorResponse): |
| - Includes a :status field (error type keyword) and a |
| :title field for a human-readable error message. |
| - Supports additional fields like :detail, :retry?, |
| :cause, and :fields. |
| |
| - Protocols: |
| - Result: Allows unwrapping of responses. Throws an |
| exception for ErrorResponse and returns the result for |
| OkResponse. |
| |
| - Integration: |
| - Compatible with failjure [1] for error handling. |
| |
| |
| Installation |
| ------------------------------------------------------------ |
| |
| This library can be installed from Clojars [2]. |
| |
| |
| Usage |
| ------------------------------------------------------------ |
| |
| Creating Responses |
| ------------------ |
| |
| Success Response |
| ---------------- |
| |
| (require '[dicho.core :refer [ok]]) |
| |
| (def response (ok "Success data")) |
| (println response) ;; => #dicho.types.OkResponse{:status :ok, :result "Success data"} |
| |
| ;; With metadata |
| (def response-with-meta (ok {:data 42} {:trace-id "abc123"})) |
| (println response-with-meta) |
| ;; => #dicho.types.OkResponse{:status :ok, :result {:data 42}, :trace-id "abc123"} |
| |
| |
| Error Response |
| -------------- |
| |
| (require '[dicho.core :refer [err]]) |
| |
| (def error-response (err :not-found "Resource not found")) |
| (println error-response) |
| ;; => #dicho.types.ErrorResponse{:status :not-found, :title "Resource not found"} |
| |
| ;; With additional fields |
| (def detailed-error (err :invalid-params "Invalid input" {:detail "Field 'name' is required"})) |
| (println detailed-error) |
| ;; => #dicho.types.ErrorResponse{:status :invalid-params, :title "Invalid input", :detail "Field 'name' is required"} |
| |
| |
| Checking Response Types |
| ----------------------- |
| (require '[dicho.core :refer [ok? error?]]) |
| |
| (ok? (ok "Success")) ;; => true |
| (error? (ok "Success")) ;; => false |
| |
| (error? (err :not-found "Not found")) ;; => true |
| (ok? (err :not-found "Not found")) ;; => false |
| |
| |
| Pattern Matching on Responses |
| ----------------------------- |
| |
| dicho provides powerful pattern matching macros for handling |
| responses conditionally. |
| |
| |
| when-ok - Execute code only on success |
| -------------------------------------- |
| |
| Execute code only when the response is successful, binding |
| the result value: |
| |
| (require '[dicho.core :refer [when-ok]]) |
| |
| (when-ok [data (ok {:users ["Alice" "Bob"]})] |
| (count (:users data))) |
| ;; => 2 |
| |
| (when-ok [data (err :not-found "No users")] |
| (count (:users data))) |
| ;; => nil (body not executed) |
| |
| |
| when-failed - Execute code only on errors |
| ----------------------------------------- |
| |
| Execute code only when the response is an error, binding the |
| error response: |
| |
| (require '[dicho.core :refer [when-failed]]) |
| |
| (when-failed [error (err :timeout "Request timeout" {:retry? true})] |
| (if (:retry? error) |
| "Will retry" |
| "Giving up")) |
| ;; => "Will retry" |
| |
| (when-failed [error (ok "success")] |
| "This won't execute") |
| ;; => nil (body not executed) |
| |
| |
| either - Handle both success and error cases |
| -------------------------------------------- |
| |
| Pattern match with separate handlers for success and error |
| cases: |
| |
| (require '[dicho.core :refer [either]]) |
| |
| (either (ok 42) |
| [result] (* result 2) ; success handler |
| [error] (str "Error: " (:title error))) ; error handler |
| ;; => 84 |
| |
| (either (err :invalid-params "Bad input") |
| [result] (* result 2) |
| [error] (str "Error: " (:title error))) |
| ;; => "Error: Bad input" |
| |
| |
| match-status - Case-like matching on status |
| ------------------------------------------- |
| |
| Match on specific response statuses with compile-time |
| optimization: |
| |
| (require '[dicho.core :refer [match-status]]) |
| |
| (defn handle-api-response [response] |
| (match-status response |
| :ok "Operation successful" |
| :not-found "Resource not found" |
| :timeout "Request timed out" |
| :rate-limited "Too many requests" |
| "Unknown error")) |
| |
| (handle-api-response (ok "data")) ;; => "Operation successful" |
| (handle-api-response (err :timeout "Slow")) ;; => "Request timed out" |
| (handle-api-response (err :custom-error "Unknown")) ;; => "Unknown error" |
| |
| |
| Unwrapping Responses |
| -------------------- |
| |
| (require '[dicho.core :refer [result]]) |
| |
| (def success-response (ok "Success data")) |
| (result success-response) ;; => "Success data" |
| |
| (def error-response (err :not-found "Resource not found")) |
| (result error-response) ;; Throws ExceptionInfo with message "Resource not found" |
| |
| |
| Integration with Failjure |
| ------------------------- |
| |
| dicho integrates with the failjure library [1] for error |
| handling. OkResponse and ErrorResponse implement the |
| HasFailed protocol. |
| |
| (require '[failjure.core :as f] |
| '[dicho.core :refer [ok err]]) |
| |
| (def success-response (ok "Success data")) |
| (def error-response (err :not-found "Resource not found")) |
| |
| (f/failed? success-response) ;; => false |
| (f/failed? error-response) ;; => true |
| |
| (f/message success-response) ;; => "Success" |
| (f/message error-response) ;; => "Resource not found" |
| |
| |
| Specs |
| ----- |
| |
| dicho uses clojure.spec to validate responses. The specs |
| ensure that responses conform to the expected structure. |
| |
| |
| Success Response Spec |
| --------------------- |
| |
| (require '[clojure.spec.alpha :as s] |
| '[dicho.specs :as specs] |
| '[dicho.core :refer [ok]]) |
| |
| ;; Valid response with a valid trace-id |
| (s/valid? ::specs/ok (ok {:data 42} {:trace-id "valid-trace-id"})) |
| ;; => true |
| |
| |
| Error Response Spec |
| ------------------- |
| |
| (require '[clojure.spec.alpha :as s] |
| '[dicho.specs :as specs] |
| '[dicho.core :refer [err]]) |
| |
| ;; Valid error response |
| ( s/valid? ::specs/error (err :not-found "Not found")) |
| ;; => true |
| |
| |
| Conversion Utilities |
| -------------------- |
| |
| The dicho.convert namespace provides utilities for |
| converting between dicho response records and other data |
| structures like maps and exceptions. This is particularly |
| useful for serialization, deserialization, and |
| interoperability with other systems. |
| |
| |
| Converting Responses to Maps |
| ---------------------------- |
| |
| Use response->map to convert any dicho response to a plain |
| map: |
| |
| (require '[dicho.convert :as convert] |
| '[dicho.core :refer [ok err]]) |
| |
| ;; Convert success response |
| (def success-response (ok {:data 42} {:trace-id "abc123"})) |
| (convert/response->map success-response) |
| ;; => {:status :ok, :result {:data 42}, :trace-id "abc123"} |
| |
| ;; Convert error response |
| (def error-response (err :not-found "Resource not found" {:detail "User ID 123 not found"})) |
| (convert/response->map error-response) |
| ;; => {:status :not-found, :title "Resource not found", :detail "User ID 123 not found"} |
| |
| |
| Converting Maps to Responses |
| ---------------------------- |
| |
| Use map->response to reconstruct dicho responses from maps. |
| The function validates the resulting response against dicho |
| specs: |
| |
| ;; Convert map to success response |
| (def success-map {:status :ok :result "Success data"}) |
| (convert/map->response success-map) |
| ;; => #dicho.types.OkResponse{:status :ok, :result "Success data"} |
| |
| ;; Convert map to error response |
| (def error-map {:status :invalid-params :title "Validation failed"}) |
| (convert/map->response error-map) |
| ;; => #dicho.types.ErrorResponse{:status :invalid-params, :title "Validation failed"} |
| |
| ;; Invalid maps throw exceptions |
| (convert/map->response {:status :not-found}) ; Throws ExceptionInfo - missing title |
| |
| |
| Exception Conversion |
| -------------------- |
| |
| Convert between error responses and ExceptionInfo objects |
| for seamless integration with Clojure's exception handling: |
| |
| ;; Convert error response to exception |
| (def error-response (err :timeout "Request timeout" {:retry? true})) |
| (def exception (convert/response->ex-info error-response)) |
| |
| (.getMessage exception) ;; => "Request timeout" |
| (ex-data exception) ;; => {:status :timeout, :retry? true} |
| |
| ;; Convert exception back to error response |
| (convert/ex-info->response exception) |
| ;; => #dicho.types.ErrorResponse{:status :timeout, :title "Request timeout", :retry? true} |
| |
| |
| References |
| ------------------------------------------------------------ |
| [1]: failjure |
| [2]: Clojars |
| |
| |
| Links |
| ------------------------------------------------------------ |
| main.zip |
| GitHub |