;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; ;;; REST API bindings for sr.ht ;;; ;;; Copyright (c) 2019, Evan Hanson ;;; ;;; See LICENSE for details. ;;; (declare (module topham) (export ;; public api access-token service-domain create retrieve update delete page ;; internal use keyword-arguments->alist signal-condition)) (import (chicken condition) (chicken format) (chicken io) (chicken keyword) (chicken port) (chicken process-context) (chicken type) (http-client) (medea) (intarweb) (uri-common)) ;;; ;;; Configurables. ;;; ;; ;; (: access-token (#!optional (or string false) -> (or string false))) (define access-token (make-parameter #f)) (: service-domain (#!optional string -> string)) (define service-domain (make-parameter "sr.ht")) ;;; ;;; Some helpers for the other libraries to use. ;;; (: signal-condition (#!rest (list-of any) -> any)) (define signal-condition (compose signal condition)) (: keyword-arguments->alist (list -> (list-of pair))) (define (keyword-arguments->alist list) (map (lambda (pair) (cons (keyword->symbol (car pair)) (cadr pair))) (chop list 2))) ;;; ;;; Keyword shenanigans. ;;; (define keyword->symbol (compose string->symbol keyword->string)) (define (set-keyword key args value #!optional (default (constantly #f))) (update-keyword key args (lambda (_) value) default)) (define (update-keyword key args update #!optional (default (constantly #f))) (let loop ((args args)) (cond ((null? args) (list key (update (default)))) ((eq? (car args) key) (cons (car args) (cons (update (cadr args)) (cddr args)))) (else (cons (car args) (cons (cadr args) (loop (cddr args)))))))) (define (delete-keyword key args) (let loop ((args args)) (cond ((null? args) args) ((eq? (car args) key) (cddr args)) (else (cons (car args) (cons (cadr args) (loop (cddr args)))))))) ;;; ;;; HTTP helpers. ;;; (define (json-response? response) (eq? (header-value 'content-type (response-headers response)) 'application/json)) (define (empty-response? response) (eq? (response-status response) 'no-content)) (define (not-found-condition? e) (eq? (response-status (client-error-response e)) 'not-found)) (define client-error-response (condition-property-accessor 'client-error 'response)) (define client-error-body (condition-property-accessor 'client-error 'body)) (define (signal-service-condition e) (signal (make-composite-condition e (condition (let ((body (client-error-body e))) (if (not body) '(topham) `(topham body ,(read-json body)))))))) ;;; ;;; Miniature CRUD framework. ;;; (define (query crud . args) (cons (update-keyword #:query (car crud) (lambda (query) (append (keyword-arguments->alist args) query)) (lambda () '())) (cdr crud))) (define (body crud . args) (let ((spec (car crud)) (body (optional args (cdr crud)))) (cons (if (eq? (get-keyword #:method spec) 'GET) spec (set-keyword #:body spec body)) (cdr crud)))) (define (submit-crud-request method crud) (apply submit-api-request method: method (car crud))) (define ((make-crud-dispatcher method) crud) (##sys#check-pair crud method) ; FIXME (call-with-values (lambda () (submit-crud-request method (body crud))) (lambda (result response) (if (or (null? result) (pair? result)) (values (cons (car crud) result) response) (values result response))))) (: create ((list-of pair) -> any (struct response))) (define create (make-crud-dispatcher 'POST)) (: retrieve ((list-of pair) -> any (struct response))) (define retrieve (make-crud-dispatcher 'GET)) (: update ((list-of pair) -> any (struct response))) (define update (make-crud-dispatcher 'PUT)) (: delete ((list-of pair) -> any (struct response))) (define delete (make-crud-dispatcher 'DELETE)) ;;; ;;; Service-specific combinators. ;;; (: page ((list-of pair) number -> (list-of pair))) (define (page crud start) (query crud start: start)) ;;; ;;; The actual request/response cycle. ;;; (define (authorization-header) `#(token ,(or (access-token) (get-environment-variable "SRHT_ACCESS_TOKEN") (error 'access-token "no access-token defined")))) (define (make-json-request #!rest args #!key method) (let ((request (apply make-request method: method args))) (update-request request headers: (replace-header-contents 'content-type '(#(application/json ())) (request-headers request))))) (define (make-authorized-json-request service method path query) (let ((host (format "~A.~A" service (service-domain)))) (make-json-request method: method uri: (update-uri (uri-reference path) scheme: 'https host: host query: query) headers: (headers `((authorization ,(authorization-header))))))) (define (submit-api-request #!key service method path body query) (parameterize ((authorization-param-subunparsers (cons (cons 'token (lambda (token) token)) (authorization-param-subunparsers)))) (let ((request (make-authorized-json-request service method path query))) (handle-exceptions e (condition-case (signal e) ; FIXME (e (http client-error) (let ((response (client-error-response e))) (if (not-found-condition? e) (values #f response) (signal-service-condition e)))) (e () (signal-service-condition e))) (receive (result uri response) ;; https://todo.sr.ht/~sircmpwn/sr.ht/184 (fluid-let ((symbol->http-name symbol->string)) (call-with-input-request* request (and body (call-with-output-string (lambda (port) (write-json body port)))) (lambda (port response) (if (json-response? response) (read-json port) (read-string #f port))))) (if (empty-response? response) (values #t response) (values result response)))))))