AtlatestRepositorysigil-lemonsqueezy
sigil-lemonsqueezy / tree / srclemonsqueezy.sgl
1
;;; (lemonsqueezy) - Lemon Squeezy API client library.2
;;;3
;;; Core module providing authentication, HTTP helpers, JSON:API parsing4
;;; utilities, pagination, and record types for the Lemon Squeezy API.5
;;;6
;;; Lemon Squeezy uses JSON:API format (jsonapi.org) for all responses.7
;;; This module provides helpers to extract data from that structure.9
(define-library (lemonsqueezy)10
(import (sigil core)11
(sigil dict)12
(sigil string)13
(sigil struct)14
(sigil json)15
(only (sigil http) url-encode-value build-query-string)16
(sigil http client))18
(export ;; Client19
ls-client20
ls-client?21
ls-client-api-key22
ls-client-base-url24
;; Shared HTTP helpers (for sub-modules)25
ls-auth-headers26
ls-api-url27
ls-get/json28
ls-post/json29
ls-patch/json30
ls-delete/json32
;; JSON:API helpers33
jsonapi-id34
jsonapi-type35
jsonapi-attr36
jsonapi-attrs37
jsonapi-relationship-id38
jsonapi-relationship-ids39
jsonapi-data40
jsonapi-data-list41
jsonapi-included42
jsonapi-find-included43
jsonapi-meta44
jsonapi-links45
jsonapi-pagination-meta46
maybe-null48
;; Pagination49
ls-paginate50
ls-list-params52
;; Generic endpoint helpers53
ls-list-endpoint54
ls-get-endpoint56
;; Error57
ls-error)59
(begin61
;; ---------------------------------------------------------------62
;; Records63
;; ---------------------------------------------------------------65
(define-struct ls-client66
(api-key)67
(base-url default: "https://api.lemonsqueezy.com"))69
;; ---------------------------------------------------------------70
;; Internal helpers71
;; ---------------------------------------------------------------73
;;; Build authorization headers for the Lemon Squeezy API.74
;;; All requests require Accept and Content-Type for JSON:API.75
(define (ls-auth-headers client)76
#{ authorization: (string-append "Bearer " (ls-client-api-key client))77
accept: "application/vnd.api+json"78
content-type: "application/vnd.api+json" })80
;;; Build an API URL from the client base URL and path segments.81
(define (ls-api-url client . parts)82
(apply build-api-url (ls-client-base-url client) parts))84
;;; Normalize a JSON null value to #f.85
;;; json-decode returns the symbol 'null' for JSON null, which is86
;;; truthy in boolean context. This helper normalizes it.87
(define (maybe-null v)88
(if (or (not v) (eq? v 'null)) #f v))90
;;; Raise a Lemon Squeezy API error with context from the response.91
(define (ls-error status body)92
(let* ((parsed (if (and body (not (string=? body "")))93
(guard (exn (else #f))94
(json-decode body))95
#f))96
(errors (if (and parsed (dict? parsed))97
(dict-ref parsed errors: #f)98
#f))99
(detail (if (and errors (array? errors) (> (array-length errors) 0))100
(let ((err (array-ref errors 0)))101
(dict-ref err detail: "Unknown error"))102
"Unknown error")))103
(cond104
((= status 401)105
(error (string-append106
"Lemon Squeezy API 401 Unauthorized. "107
"Check your API key. " detail)))108
((= status 404)109
(error (string-append110
"Lemon Squeezy API 404 Not Found. " detail)))111
((= status 422)112
(error (string-append113
"Lemon Squeezy API 422 Validation Error. " detail)))114
((= status 429)115
(error (string-append116
"Lemon Squeezy API 429 Rate Limited. "117
"Retry after a delay.")))118
(else119
(error (string-append120
"Lemon Squeezy API error " (number->string status)121
": " detail))))))123
;;; Response checker with Lemon Squeezy JSON:API error parsing.124
(define check-ls-response125
(make-response-checker126
name: "Lemon Squeezy API"127
parse-error: ls-error))129
;;; Authenticated JSON GET request.130
(define (ls-get/json client url)131
(check-ls-response132
(http-get url headers: (ls-auth-headers client))))134
;;; Authenticated JSON POST request.135
(define (ls-post/json client url body)136
(check-ls-response137
(http-post url (if (string? body) body (json-encode body))138
headers: (ls-auth-headers client))))140
;;; Authenticated JSON PATCH request.141
(define (ls-patch/json client url body)142
(check-ls-response143
(http-patch url (if (string? body) body (json-encode body))144
headers: (ls-auth-headers client))))146
;;; Authenticated JSON DELETE request.147
(define (ls-delete/json client url)148
(check-ls-response149
(http-delete url headers: (ls-auth-headers client))))151
;; ---------------------------------------------------------------152
;; Generic endpoint helpers153
;; ---------------------------------------------------------------155
;;; Fetch a list from a JSON:API endpoint with optional filtering/pagination.156
;;; endpoint: the API path segments (e.g., "v1" "products")157
;;; parser: function to convert a JSON:API resource to a record158
;;; opts: optional dict with page:, per-page:, filter:, include: keys159
(define (ls-list-endpoint client parser endpoint . rest)160
(let* ((opts (if (null? rest) #{} (car rest)))161
(params (ls-list-params opts))162
(url (string-append163
(apply ls-api-url client endpoint)164
(build-query-string params)))165
(response (ls-get/json client url)))166
(map parser (jsonapi-data-list response))))168
;;; Fetch a single resource from a JSON:API endpoint by ID.169
;;; endpoint: the API path segments (e.g., "v1" "products")170
;;; id: the resource ID string171
;;; parser: function to convert a JSON:API resource to a record172
(define (ls-get-endpoint client parser endpoint id)173
(let* ((url (apply ls-api-url client (append endpoint (list id))))174
(response (ls-get/json client url)))175
(parser (jsonapi-data response))))177
;; ---------------------------------------------------------------178
;; JSON:API helpers179
;; ---------------------------------------------------------------181
;;; Extract the id from a JSON:API resource object.182
(define (jsonapi-id resource)183
(dict-ref resource id:))185
;;; Extract the type from a JSON:API resource object.186
(define (jsonapi-type resource)187
(dict-ref resource type:))189
;;; Extract a single attribute from a JSON:API resource object.190
;;; Returns default if the attribute is missing or null.191
(define (jsonapi-attr resource key . rest)192
(let* ((default (if (null? rest) #f (car rest)))193
(attrs (dict-ref resource attributes: #{}))194
(val (dict-ref attrs key default)))195
(maybe-null val)))197
;;; Extract the full attributes dict from a JSON:API resource object.198
(define (jsonapi-attrs resource)199
(dict-ref resource attributes: #{}))201
;;; Extract a single relationship ID from a JSON:API resource.202
;;; Relationships are under data.relationships.<name>.data.id203
(define (jsonapi-relationship-id resource rel-name)204
(let* ((rels (dict-ref resource relationships: #{}))205
(rel (dict-ref rels rel-name #{}))206
(data (dict-ref rel data: #f)))207
(if (and data (dict? data))208
(maybe-null (dict-ref data id: #f))209
#f)))211
;;; Extract relationship IDs for a has-many relationship.212
;;; Returns a list of ID strings.213
(define (jsonapi-relationship-ids resource rel-name)214
(let* ((rels (dict-ref resource relationships: #{}))215
(rel (dict-ref rels rel-name #{}))216
(data (dict-ref rel data: #f)))217
(if (and data (array? data))218
(map (lambda (d) (dict-ref d id:)) (array->list data))219
'())))221
;;; Extract the data field from a JSON:API response.222
;;; For single-resource responses, returns the resource object.223
(define (jsonapi-data response)224
(dict-ref response data:))226
;;; Extract data as a list from a JSON:API list response.227
;;; For list endpoints, data is an array.228
(define (jsonapi-data-list response)229
(let ((data (dict-ref response data: #[])))230
(if (array? data)231
(array->list data)232
(list data))))234
;;; Extract the included array from a JSON:API response.235
;;; Returns a list of included resource objects.236
(define (jsonapi-included response)237
(let ((included (dict-ref response included: #f)))238
(if (and included (array? included))239
(array->list included)240
'())))242
;;; Find an included resource by type and id.243
(define (jsonapi-find-included response type id)244
(let loop ((items (jsonapi-included response)))245
(cond246
((null? items) #f)247
((and (string=? (dict-ref (car items) type:) type)248
(equal? (dict-ref (car items) id:) id))249
(car items))250
(else (loop (cdr items))))))252
;;; Extract meta from a JSON:API response.253
(define (jsonapi-meta response)254
(dict-ref response meta: #{}))256
;;; Extract links from a JSON:API response.257
(define (jsonapi-links response)258
(dict-ref response links: #{}))260
;;; Extract pagination metadata from a JSON:API list response.261
;;; Returns a dict with current-page, last-page, per-page, total, from, to.262
(define (jsonapi-pagination-meta response)263
(let ((meta (jsonapi-meta response)))264
(let ((page (dict-ref meta page: #{})))265
#{ current-page: (dict-ref page currentPage: 1)266
last-page: (dict-ref page lastPage: 1)267
per-page: (dict-ref page perPage: 10)268
total: (dict-ref page total: 0)269
from: (dict-ref page from: #f)270
to: (dict-ref page to: #f) })))272
;; ---------------------------------------------------------------273
;; Pagination & Filtering274
;; ---------------------------------------------------------------276
;;; Build query parameters for list endpoints.277
;;; Takes an optional dict with page:, per-page:, filter:, and include: keys.278
;;; filter: should be a dict of filter params (e.g., #{ store_id: "1" }).279
;;; include: should be a comma-separated string (e.g., "variants,store").280
(define (ls-list-params . rest)281
(let ((opts (if (null? rest) #{} (car rest))))282
(let ((page (dict-ref opts page: #f))283
(per-page (dict-ref opts per-page: #f))284
(filters (dict-ref opts filter: #f))285
(include (dict-ref opts include: #f)))286
(let ((base-params287
(list (cons "page[number]"288
(if page (number->string page) #f))289
(cons "page[size]"290
(if per-page (number->string per-page) #f))291
(cons "include" include))))292
(if (and filters (dict? filters))293
(append base-params294
(map (lambda (entry)295
(cons (string-append "filter["296
(keyword->string (car entry)) "]")297
(if (number? (cdr entry))298
(number->string (cdr entry))299
(cdr entry))))300
(dict-entries filters)))301
base-params)))))303
;;; Fetch all pages from a paginated list endpoint.304
;;; Calls fetcher with successive page numbers and accumulates results.305
;;; fetcher should be a function taking a page number and returning306
;;; a JSON:API response.307
;;; Returns a list of all resource objects across all pages.308
(define (ls-paginate fetcher)309
(let loop ((page 1) (chunks '()))310
(let* ((response (fetcher page))311
(items (jsonapi-data-list response))312
(meta (jsonapi-pagination-meta response))313
(current (dict-ref meta current-page:))314
(last-page (dict-ref meta last-page:))315
(chunks (cons items chunks)))316
(if (>= current last-page)317
(apply append (reverse chunks))318
(loop (+ page 1) chunks)))))320
))