diff --git a/sx/sx/handlers/examples.sx b/sx/sx/handlers/examples.sx new file mode 100644 index 0000000..5d2f90e --- /dev/null +++ b/sx/sx/handlers/examples.sx @@ -0,0 +1,792 @@ +;; ========================================================================== +;; Example API endpoints — live demos for hypermedia examples pages +;; +;; Each defhandler with :path registers as a public route automatically. +;; OOB swaps show wire format and component source alongside each demo. +;; ========================================================================== + + +;; -------------------------------------------------------------------------- +;; Data constants (captured in handler closures) +;; -------------------------------------------------------------------------- + +(define search-languages + (list "Python" "JavaScript" "TypeScript" "Rust" "Go" "Java" "C" "C++" + "Ruby" "Elixir" "Haskell" "Clojure" "Scala" "Kotlin" "Swift" + "Zig" "OCaml" "Lua" "Perl" "PHP")) + +(define value-select-data + {"Languages" (list "Python" "JavaScript" "Rust" "Go") + "Frameworks" (list "Quart" "FastAPI" "React" "Svelte") + "Databases" (list "PostgreSQL" "Redis" "SQLite" "MongoDB")}) + +(define taken-emails + (list "admin@example.com" "test@example.com" "user@example.com")) + +(define tab-content + {"tab1" (div (p :class "text-stone-700" "Welcome to the Overview tab.") + (p :class "text-stone-500 text-sm mt-2" + "This is the default tab content loaded via sx-get.")) + "tab2" (div (p :class "text-stone-700" "Here are the details.") + (ul :class "mt-2 space-y-1 text-sm text-stone-600" + (li "Version: 1.0.0") + (li "Build: 2024-01-15") + (li "Engine: sx"))) + "tab3" (div (p :class "text-stone-700" "Recent history:") + (ol :class "mt-2 space-y-1 text-sm text-stone-600 list-decimal list-inside" + (li "Initial release") + (li "Added component caching") + (li "Wire format v2")))}) + +(define kbd-actions + {"s" "Search panel activated" + "n" "New item created" + "h" "Help panel opened"}) + +(define anim-colors + (list "bg-violet-100" "bg-emerald-100" "bg-blue-100" "bg-amber-100" "bg-rose-100")) + +(define edit-row-defaults + {"1" {"id" "1" "name" "Widget A" "price" "19.99" "stock" "142"} + "2" {"id" "2" "name" "Widget B" "price" "24.50" "stock" "89"} + "3" {"id" "3" "name" "Widget C" "price" "12.00" "stock" "305"} + "4" {"id" "4" "name" "Widget D" "price" "45.00" "stock" "67"}}) + +(define bulk-user-defaults + {"1" {"id" "1" "name" "Alice Chen" "email" "alice@example.com" "status" "active"} + "2" {"id" "2" "name" "Bob Rivera" "email" "bob@example.com" "status" "inactive"} + "3" {"id" "3" "name" "Carol Zhang" "email" "carol@example.com" "status" "active"} + "4" {"id" "4" "name" "Dan Okafor" "email" "dan@example.com" "status" "inactive"} + "5" {"id" "5" "name" "Eve Larsson" "email" "eve@example.com" "status" "active"}}) + + +;; -------------------------------------------------------------------------- +;; Click to Load +;; -------------------------------------------------------------------------- + +(defhandler ex-click + :path "/geography/hypermedia/examples/api/click" + :method :get + :returns "element" + (&key) + (let ((now (now "%Y-%m-%d %H:%M:%S"))) + (<> + (~click-result :time now) + (~doc-oob-code :target-id "click-comp" + :text (component-source "click-result")) + (~doc-oob-code :target-id "click-wire" + :text (str "(~click-result :time \"" now "\")"))))) + + +;; -------------------------------------------------------------------------- +;; Form Submission +;; -------------------------------------------------------------------------- + +(defhandler ex-form + :path "/geography/hypermedia/examples/api/form" + :method :post + :csrf false + :returns "element" + (&key) + (let ((name (request-form "name" ""))) + (<> + (~form-result :name name) + (~doc-oob-code :target-id "form-comp" + :text (component-source "form-result")) + (~doc-oob-code :target-id "form-wire" + :text (str "(~form-result :name \"" name "\")"))))) + + +;; -------------------------------------------------------------------------- +;; Polling +;; -------------------------------------------------------------------------- + +(defhandler ex-poll + :path "/geography/hypermedia/examples/api/poll" + :method :get + :returns "element" + (&key) + (let ((n (+ (state-get "ex-poll-n" 0) 1))) + (state-set! "ex-poll-n" n) + (let ((now (now "%H:%M:%S")) + (count (if (< n 10) n 10))) + (<> + (~poll-result :time now :count count) + (~doc-oob-code :target-id "poll-comp" + :text (component-source "poll-result")) + (~doc-oob-code :target-id "poll-wire" + :text (str "(~poll-result :time \"" now "\" :count " count ")")))))) + + +;; -------------------------------------------------------------------------- +;; Delete Row +;; -------------------------------------------------------------------------- + +(defhandler ex-delete + :path "/geography/hypermedia/examples/api/delete/" + :method :delete + :csrf false + :returns "element" + (&key item-id) + (<> + (~doc-oob-code :target-id "delete-comp" + :text (component-source "delete-row")) + (~doc-oob-code :target-id "delete-wire" + :text "(empty — row removed by outerHTML swap)"))) + + +;; -------------------------------------------------------------------------- +;; Inline Edit +;; -------------------------------------------------------------------------- + +(defhandler ex-edit-form + :path "/geography/hypermedia/examples/api/edit" + :method :get + :returns "element" + (&key) + (let ((value (request-arg "value" ""))) + (<> + (~inline-edit-form :value value) + (~doc-oob-code :target-id "edit-comp" + :text (component-source "inline-edit-form")) + (~doc-oob-code :target-id "edit-wire" + :text (str "(~inline-edit-form :value \"" value "\")"))))) + +(defhandler ex-edit-save + :path "/geography/hypermedia/examples/api/edit" + :method :post + :csrf false + :returns "element" + (&key) + (let ((value (request-form "value" ""))) + (<> + (~inline-view :value value) + (~doc-oob-code :target-id "edit-comp" + :text (component-source "inline-view")) + (~doc-oob-code :target-id "edit-wire" + :text (str "(~inline-view :value \"" value "\")"))))) + +(defhandler ex-edit-cancel + :path "/geography/hypermedia/examples/api/edit/cancel" + :method :get + :returns "element" + (&key) + (let ((value (request-arg "value" ""))) + (<> + (~inline-view :value value) + (~doc-oob-code :target-id "edit-comp" + :text (component-source "inline-view")) + (~doc-oob-code :target-id "edit-wire" + :text (str "(~inline-view :value \"" value "\")"))))) + + +;; -------------------------------------------------------------------------- +;; Out-of-Band Swaps +;; -------------------------------------------------------------------------- + +(defhandler ex-oob + :path "/geography/hypermedia/examples/api/oob" + :method :get + :returns "element" + (&key) + (let ((now (now "%H:%M:%S"))) + (<> + (p :class "text-emerald-600 font-medium" "Box A updated!") + (p :class "text-sm text-stone-500" (str "at " now)) + (div :id "oob-box-b" :sx-swap-oob "innerHTML" + (p :class "text-violet-600 font-medium" "Box B updated via OOB!") + (p :class "text-sm text-stone-500" (str "at " now))) + (~doc-oob-code :target-id "oob-wire" + :text (str "(<> (p ... \"Box A updated!\") (div :id \"oob-box-b\" :sx-swap-oob \"innerHTML\" (p ... \"Box B updated!\")))"))))) + + +;; -------------------------------------------------------------------------- +;; Lazy Loading +;; -------------------------------------------------------------------------- + +(defhandler ex-lazy + :path "/geography/hypermedia/examples/api/lazy" + :method :get + :returns "element" + (&key) + (let ((now (now "%H:%M:%S"))) + (<> + (~lazy-result :time now) + (~doc-oob-code :target-id "lazy-comp" + :text (component-source "lazy-result")) + (~doc-oob-code :target-id "lazy-wire" + :text (str "(~lazy-result :time \"" now "\")"))))) + + +;; -------------------------------------------------------------------------- +;; Infinite Scroll +;; -------------------------------------------------------------------------- + +(defhandler ex-scroll + :path "/geography/hypermedia/examples/api/scroll" + :method :get + :returns "element" + (&key) + (let ((page (request-arg "page" "2"))) + (let ((pg (parse-int page)) + (start (+ (* (- (parse-int page) 1) 5) 1))) + (<> + (map (fn (i) + (div :class "px-4 py-3 border-b border-stone-100 text-sm text-stone-700" + (str "Item " i " — loaded from page " page))) + (range start (+ start 5))) + (if (<= (+ pg 1) 6) + (div :id "scroll-sentinel" + :sx-get (str "/geography/hypermedia/examples/api/scroll?page=" (+ pg 1)) + :sx-trigger "intersect once" + :sx-target "#scroll-items" + :sx-swap "beforeend" + :class "p-3 text-center text-stone-400 text-sm" + "Loading more...") + (div :class "p-3 text-center text-stone-500 text-sm font-medium" + "All items loaded.")) + (~doc-oob-code :target-id "scroll-wire" + :text (str "(items for page " page " + sentinel)")))))) + + +;; -------------------------------------------------------------------------- +;; Progress Bar +;; -------------------------------------------------------------------------- + +(defhandler ex-progress-start + :path "/geography/hypermedia/examples/api/progress/start" + :method :post + :csrf false + :returns "element" + (&key) + (let ((n (+ (state-get "ex-job-counter" 0) 1))) + (state-set! "ex-job-counter" n) + (let ((job-id (str "job-" n))) + (state-set! (str "ex-job-" job-id) 0) + (<> + (~progress-status :percent 0 :job-id job-id) + (~doc-oob-code :target-id "progress-comp" + :text (component-source "progress-status")) + (~doc-oob-code :target-id "progress-wire" + :text (str "(~progress-status :percent 0 :job-id \"" job-id "\")")))))) + +(defhandler ex-progress-status + :path "/geography/hypermedia/examples/api/progress/status" + :method :get + :returns "element" + (&key) + (let ((job-id (request-arg "job" ""))) + (let ((current (state-get (str "ex-job-" job-id) 0))) + (let ((next (if (>= (+ current (random-int 15 30)) 100) 100 (+ current (random-int 15 30))))) + (state-set! (str "ex-job-" job-id) next) + (<> + (~progress-status :percent next :job-id job-id) + (~doc-oob-code :target-id "progress-comp" + :text (component-source "progress-status")) + (~doc-oob-code :target-id "progress-wire" + :text (str "(~progress-status :percent " next " :job-id \"" job-id "\")"))))))) + + +;; -------------------------------------------------------------------------- +;; Active Search +;; -------------------------------------------------------------------------- + +(defhandler ex-search + :path "/geography/hypermedia/examples/api/search" + :method :get + :returns "element" + (&key) + (let ((q (request-arg "q" ""))) + (let ((results (if (= q "") + search-languages + (filter (fn (lang) (contains? (lower-case lang) (lower-case q))) + search-languages)))) + (<> + (~search-results :items results :query q) + (~doc-oob-code :target-id "search-comp" + :text (component-source "search-results")) + (~doc-oob-code :target-id "search-wire" + :text (str "(~search-results :items (list ...) :query \"" q "\")")))))) + + +;; -------------------------------------------------------------------------- +;; Inline Validation +;; -------------------------------------------------------------------------- + +(defhandler ex-validate + :path "/geography/hypermedia/examples/api/validate" + :method :get + :returns "element" + (&key) + (let ((email (request-arg "email" ""))) + (let ((result + (cond + (= email "") + (list "validation-error" "(~validation-error :message \"Email is required\")" + (~validation-error :message "Email is required")) + (not (contains? email "@")) + (list "validation-error" "(~validation-error :message \"Invalid email format\")" + (~validation-error :message "Invalid email format")) + (some (fn (e) (= (lower-case e) (lower-case email))) taken-emails) + (list "validation-error" (str "(~validation-error :message \"" email " is already taken\")") + (~validation-error :message (str email " is already taken"))) + :else + (list "validation-ok" (str "(~validation-ok :email \"" email "\")") + (~validation-ok :email email))))) + (<> + (nth result 2) + (~doc-oob-code :target-id "validate-comp" + :text (component-source (first result))) + (~doc-oob-code :target-id "validate-wire" + :text (nth result 1)))))) + +(defhandler ex-validate-submit + :path "/geography/hypermedia/examples/api/validate/submit" + :method :post + :csrf false + :returns "element" + (&key) + (let ((email (request-form "email" ""))) + (if (or (= email "") (not (contains? email "@"))) + (p :class "text-sm text-rose-600 mt-2" "Please enter a valid email.") + (p :class "text-sm text-emerald-600 mt-2" (str "Form submitted with: " email))))) + + +;; -------------------------------------------------------------------------- +;; Value Select +;; -------------------------------------------------------------------------- + +(defhandler ex-values + :path "/geography/hypermedia/examples/api/values" + :method :get + :returns "element" + (&key) + (let ((cat (request-arg "category" ""))) + (let ((items (get value-select-data cat (list)))) + (let ((options (if (empty? items) + (list (option :value "" "No items")) + (map (fn (i) (option :value i i)) items)))) + (<> + options + (~doc-oob-code :target-id "values-wire" + :text (str "(options for \"" cat "\")"))))))) + + +;; -------------------------------------------------------------------------- +;; Reset on Submit +;; -------------------------------------------------------------------------- + +(defhandler ex-reset-submit + :path "/geography/hypermedia/examples/api/reset-submit" + :method :post + :csrf false + :returns "element" + (&key) + (let ((msg (request-form "message" "(empty)")) + (now (now "%H:%M:%S"))) + (<> + (~reset-message :message msg :time now) + (~doc-oob-code :target-id "reset-comp" + :text (component-source "reset-message")) + (~doc-oob-code :target-id "reset-wire" + :text (str "(~reset-message :message \"" msg "\" :time \"" now "\")"))))) + + +;; -------------------------------------------------------------------------- +;; Edit Row +;; -------------------------------------------------------------------------- + +(defhandler ex-editrow-form + :path "/geography/hypermedia/examples/api/editrow/" + :method :get + :returns "element" + (&key row-id) + (let ((default (get edit-row-defaults row-id {"id" row-id "name" "" "price" "0" "stock" "0"}))) + (let ((row (state-get (str "ex-row-" row-id) default))) + (<> + (~edit-row-form :id (get row "id") :name (get row "name") + :price (get row "price") :stock (get row "stock")) + (~doc-oob-code :target-id "editrow-comp" + :text (component-source "edit-row-form")) + (~doc-oob-code :target-id "editrow-wire" + :text (str "(~edit-row-form :id \"" (get row "id") "\" ...)")))))) + +(defhandler ex-editrow-save + :path "/geography/hypermedia/examples/api/editrow/" + :method :post + :csrf false + :returns "element" + (&key row-id) + (let ((name (request-form "name" "")) + (price (request-form "price" "0")) + (stock (request-form "stock" "0"))) + (state-set! (str "ex-row-" row-id) + {"id" row-id "name" name "price" price "stock" stock}) + (<> + (~edit-row-view :id row-id :name name :price price :stock stock) + (~doc-oob-code :target-id "editrow-comp" + :text (component-source "edit-row-view")) + (~doc-oob-code :target-id "editrow-wire" + :text (str "(~edit-row-view :id \"" row-id "\" ...)"))))) + +(defhandler ex-editrow-cancel + :path "/geography/hypermedia/examples/api/editrow//cancel" + :method :get + :returns "element" + (&key row-id) + (let ((default (get edit-row-defaults row-id {"id" row-id "name" "" "price" "0" "stock" "0"}))) + (let ((row (state-get (str "ex-row-" row-id) default))) + (<> + (~edit-row-view :id (get row "id") :name (get row "name") + :price (get row "price") :stock (get row "stock")) + (~doc-oob-code :target-id "editrow-comp" + :text (component-source "edit-row-view")) + (~doc-oob-code :target-id "editrow-wire" + :text (str "(~edit-row-view :id \"" (get row "id") "\" ...)")))))) + +;; -------------------------------------------------------------------------- +;; Bulk Update +;; -------------------------------------------------------------------------- + +(defhandler ex-bulk + :path "/geography/hypermedia/examples/api/bulk" + :method :post + :csrf false + :returns "element" + (&key) + (let ((action (request-arg "action" "activate")) + (ids (request-form-list "ids"))) + (let ((new-status (if (= action "activate") "active" "inactive"))) + ;; Update matching users in state + (for-each (fn (uid) + (let ((default (get bulk-user-defaults uid nil))) + (let ((user (state-get (str "ex-bulk-" uid) default))) + (when user + (state-set! (str "ex-bulk-" uid) + (assoc user "status" new-status)))))) + ids) + ;; Return all rows + (let ((rows (map (fn (uid) + (let ((default (get bulk-user-defaults uid + {"id" uid "name" "" "email" "" "status" "active"}))) + (let ((u (state-get (str "ex-bulk-" uid) default))) + (~bulk-row :id (get u "id") :name (get u "name") + :email (get u "email") :status (get u "status"))))) + (list "1" "2" "3" "4" "5")))) + (<> + rows + (~doc-oob-code :target-id "bulk-comp" + :text (component-source "bulk-row")) + (~doc-oob-code :target-id "bulk-wire" + :text (str "(updated " (len ids) " users to " new-status ")"))))))) + + +;; -------------------------------------------------------------------------- +;; Swap Positions +;; -------------------------------------------------------------------------- + +(defhandler ex-swap-log + :path "/geography/hypermedia/examples/api/swap-log" + :method :post + :csrf false + :returns "element" + (&key) + (let ((mode (request-arg "mode" "beforeend")) + (n (+ (state-get "ex-swap-n" 0) 1)) + (now (now "%H:%M:%S"))) + (state-set! "ex-swap-n" n) + (<> + (div :class "px-3 py-2 text-sm text-stone-700" + (str "[" now "] " mode " (#" n ")")) + (span :id "swap-counter" :sx-swap-oob "innerHTML" + :class "self-center text-sm text-stone-500" + (str "Count: " n)) + (~doc-oob-code :target-id "swap-wire" + :text (str "(entry + oob counter: " n ")"))))) + + +;; -------------------------------------------------------------------------- +;; Select Filter (Dashboard) +;; -------------------------------------------------------------------------- + +(defhandler ex-dashboard + :path "/geography/hypermedia/examples/api/dashboard" + :method :get + :returns "element" + (&key) + (let ((now (now "%H:%M:%S"))) + (<> + (div :id "dash-header" :class "p-3 bg-violet-50 rounded mb-3" + (h4 :class "font-semibold text-violet-800" "Dashboard Header") + (p :class "text-sm text-violet-600" (str "Generated at " now))) + (div :id "dash-stats" :class "grid grid-cols-3 gap-3 mb-3" + (div :class "p-3 bg-emerald-50 rounded text-center" + (p :class "text-2xl font-bold text-emerald-700" "142") + (p :class "text-xs text-emerald-600" "Users")) + (div :class "p-3 bg-blue-50 rounded text-center" + (p :class "text-2xl font-bold text-blue-700" "89") + (p :class "text-xs text-blue-600" "Orders")) + (div :class "p-3 bg-amber-50 rounded text-center" + (p :class "text-2xl font-bold text-amber-700" "$4.2k") + (p :class "text-xs text-amber-600" "Revenue"))) + (div :id "dash-footer" :class "p-3 bg-stone-50 rounded" + (p :class "text-sm text-stone-500" (str "Last updated: " now))) + (~doc-oob-code :target-id "filter-wire" + :text (str "(<> (div :id \"dash-header\" ...) (div :id \"dash-stats\" ...) (div :id \"dash-footer\" ...))"))))) + + +;; -------------------------------------------------------------------------- +;; Tabs +;; -------------------------------------------------------------------------- + +(defhandler ex-tabs + :path "/geography/hypermedia/examples/api/tabs/" + :method :get + :returns "element" + (&key tab) + (let ((content (get tab-content tab (get tab-content "tab1")))) + (<> + content + (div :id "tab-buttons" :sx-swap-oob "innerHTML" + :class "flex border-b border-stone-200" + (~tab-btn :tab "tab1" :label "Overview" :active (if (= tab "tab1") "true" "false")) + (~tab-btn :tab "tab2" :label "Details" :active (if (= tab "tab2") "true" "false")) + (~tab-btn :tab "tab3" :label "History" :active (if (= tab "tab3") "true" "false"))) + (~doc-oob-code :target-id "tabs-wire" + :text (str "(content for " tab " + oob tab buttons"))))) + + +;; -------------------------------------------------------------------------- +;; Animations +;; -------------------------------------------------------------------------- + +(defhandler ex-animate + :path "/geography/hypermedia/examples/api/animate" + :method :get + :returns "element" + (&key) + (let ((idx (random-int 0 4)) + (now (now "%H:%M:%S"))) + (let ((color (nth anim-colors idx))) + (<> + (~anim-result :color color :time now) + (~doc-oob-code :target-id "anim-comp" + :text (component-source "anim-result")) + (~doc-oob-code :target-id "anim-wire" + :text (str "(~anim-result :color \"" color "\" :time \"" now "\")")))))) + + +;; -------------------------------------------------------------------------- +;; Dialogs +;; -------------------------------------------------------------------------- + +(defhandler ex-dialog + :path "/geography/hypermedia/examples/api/dialog" + :method :get + :returns "element" + (&key) + (<> + (~dialog-modal :title "Confirm Action" + :message "Are you sure you want to proceed? This is a demo dialog rendered entirely with sx components.") + (~doc-oob-code :target-id "dialog-comp" + :text (component-source "dialog-modal")) + (~doc-oob-code :target-id "dialog-wire" + :text "(~dialog-modal :title \"Confirm Action\" :message \"...\")"))) + +(defhandler ex-dialog-close + :path "/geography/hypermedia/examples/api/dialog/close" + :method :get + :returns "element" + (&key) + (<> + (~doc-oob-code :target-id "dialog-wire" + :text "(empty — dialog closed)"))) + + +;; -------------------------------------------------------------------------- +;; Keyboard Shortcuts +;; -------------------------------------------------------------------------- + +(defhandler ex-keyboard + :path "/geography/hypermedia/examples/api/keyboard" + :method :get + :returns "element" + (&key) + (let ((key (request-arg "key" ""))) + (let ((action (get kbd-actions key (str "Unknown key: " key)))) + (<> + (~kbd-result :key key :action action) + (~doc-oob-code :target-id "kbd-comp" + :text (component-source "kbd-result")) + (~doc-oob-code :target-id "kbd-wire" + :text (str "(~kbd-result :key \"" key "\" :action \"" action "\")")))))) + + +;; -------------------------------------------------------------------------- +;; PUT / PATCH +;; -------------------------------------------------------------------------- + +(defhandler ex-pp-edit-all + :path "/geography/hypermedia/examples/api/putpatch/edit-all" + :method :get + :returns "element" + (&key) + (let ((p (state-get "ex-profile" + {"name" "Ada Lovelace" "email" "ada@example.com" "role" "Engineer"}))) + (<> + (~pp-form-full :name (get p "name") :email (get p "email") :role (get p "role")) + (~doc-oob-code :target-id "pp-comp" + :text (component-source "pp-form-full")) + (~doc-oob-code :target-id "pp-wire" + :text (str "(~pp-form-full :name \"" (get p "name") "\" ...)"))))) + +(defhandler ex-pp-put + :path "/geography/hypermedia/examples/api/putpatch" + :method :put + :csrf false + :returns "element" + (&key) + (let ((name (request-form "name" "")) + (email (request-form "email" "")) + (role (request-form "role" ""))) + (state-set! "ex-profile" {"name" name "email" email "role" role}) + (<> + (~pp-view :name name :email email :role role) + (~doc-oob-code :target-id "pp-comp" + :text (component-source "pp-view")) + (~doc-oob-code :target-id "pp-wire" + :text (str "(~pp-view :name \"" name "\" ...)"))))) + +(defhandler ex-pp-cancel + :path "/geography/hypermedia/examples/api/putpatch/cancel" + :method :get + :returns "element" + (&key) + (let ((p (state-get "ex-profile" + {"name" "Ada Lovelace" "email" "ada@example.com" "role" "Engineer"}))) + (<> + (~pp-view :name (get p "name") :email (get p "email") :role (get p "role")) + (~doc-oob-code :target-id "pp-comp" + :text (component-source "pp-view")) + (~doc-oob-code :target-id "pp-wire" + :text (str "(~pp-view :name \"" (get p "name") "\" ...)"))))) + + +;; -------------------------------------------------------------------------- +;; JSON Encoding +;; -------------------------------------------------------------------------- + +(defhandler ex-json-echo + :path "/geography/hypermedia/examples/api/json-echo" + :method :post + :csrf false + :returns "element" + (&key) + (let ((data (request-json)) + (ct (request-content-type))) + (let ((body (json-encode data))) + (<> + (~json-result :body body :content-type ct) + (~doc-oob-code :target-id "json-comp" + :text (component-source "json-result")) + (~doc-oob-code :target-id "json-wire" + :text (str "(~json-result :body \"" body "\" :content-type \"" ct "\")")))))) + + +;; -------------------------------------------------------------------------- +;; Vals & Headers +;; -------------------------------------------------------------------------- + +(defhandler ex-echo-vals + :path "/geography/hypermedia/examples/api/echo-vals" + :method :get + :returns "element" + (&key) + (let ((vals (into (list) (request-args-all)))) + (let ((filtered (filter (fn (pair) (and (not (= (first pair) "_")) + (not (= (first pair) "sx-request")))) + vals))) + (let ((items (map (fn (pair) (str (first pair) ": " (nth pair 1))) filtered))) + (<> + (~echo-result :label "values" :items items) + (~doc-oob-code :target-id "vals-comp" + :text (component-source "echo-result")) + (~doc-oob-code :target-id "vals-wire" + :text (str "(~echo-result :label \"values\" :items (list ...))"))))))) + +(defhandler ex-echo-headers + :path "/geography/hypermedia/examples/api/echo-headers" + :method :get + :returns "element" + (&key) + (let ((all-headers (into (list) (request-headers-all)))) + (let ((custom (filter (fn (pair) (starts-with? (first pair) "x-")) all-headers))) + (let ((items (map (fn (pair) (str (first pair) ": " (nth pair 1))) custom))) + (<> + (~echo-result :label "headers" :items items) + (~doc-oob-code :target-id "vals-comp" + :text (component-source "echo-result")) + (~doc-oob-code :target-id "vals-wire" + :text (str "(~echo-result :label \"headers\" :items (list ...))"))))))) + + +;; -------------------------------------------------------------------------- +;; Loading States +;; -------------------------------------------------------------------------- + +(defhandler ex-slow + :path "/geography/hypermedia/examples/api/slow" + :method :get + :returns "element" + (&key) + (sleep 2000) + (let ((now (now "%H:%M:%S"))) + (<> + (~loading-result :time now) + (~doc-oob-code :target-id "loading-comp" + :text (component-source "loading-result")) + (~doc-oob-code :target-id "loading-wire" + :text (str "(~loading-result :time \"" now "\")"))))) + + +;; -------------------------------------------------------------------------- +;; Request Abort (sync replace) +;; -------------------------------------------------------------------------- + +(defhandler ex-slow-search + :path "/geography/hypermedia/examples/api/slow-search" + :method :get + :returns "element" + (&key) + (let ((delay-ms (random-int 500 2000))) + (sleep delay-ms) + (let ((q (request-arg "q" ""))) + (<> + (~sync-result :query q :delay (str delay-ms)) + (~doc-oob-code :target-id "sync-comp" + :text (component-source "sync-result")) + (~doc-oob-code :target-id "sync-wire" + :text (str "(~sync-result :query \"" q "\" :delay \"" delay-ms "\")")))))) + + +;; -------------------------------------------------------------------------- +;; Retry +;; -------------------------------------------------------------------------- + +(defhandler ex-flaky + :path "/geography/hypermedia/examples/api/flaky" + :method :get + :returns "element" + (&key) + (let ((n (+ (state-get "ex-flaky-n" 0) 1))) + (state-set! "ex-flaky-n" n) + (if (not (= (mod n 3) 0)) + (do + (set-response-status 503) + "") + (<> + (~retry-result :attempt (str n) :message "Success! The endpoint finally responded.") + (~doc-oob-code :target-id "retry-comp" + :text (component-source "retry-result")) + (~doc-oob-code :target-id "retry-wire" + :text (str "(~retry-result :attempt \"" n "\" ...)"))))))