AtlatestRepositorysigil-lemonsqueezy

sigil-lemonsqueezy / tree / srclemonsqueezy.sgl

1;;; (lemonsqueezy) - Lemon Squeezy API client library.
2;;;
3;;; Core module providing authentication, HTTP helpers, JSON:API parsing
4;;; 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.
8
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 ;; Client
19 ls-client
20 ls-client?
21 ls-client-api-key
22 ls-client-base-url
24 ;; Shared HTTP helpers (for sub-modules)
25 ls-auth-headers
26 ls-api-url
27 ls-get/json
28 ls-post/json
29 ls-patch/json
30 ls-delete/json
32 ;; JSON:API helpers
33 jsonapi-id
34 jsonapi-type
35 jsonapi-attr
36 jsonapi-attrs
37 jsonapi-relationship-id
38 jsonapi-relationship-ids
39 jsonapi-data
40 jsonapi-data-list
41 jsonapi-included
42 jsonapi-find-included
43 jsonapi-meta
44 jsonapi-links
45 jsonapi-pagination-meta
46 maybe-null
48 ;; Pagination
49 ls-paginate
50 ls-list-params
52 ;; Generic endpoint helpers
53 ls-list-endpoint
54 ls-get-endpoint
56 ;; Error
57 ls-error)
59 (begin
61 ;; ---------------------------------------------------------------
62 ;; Records
63 ;; ---------------------------------------------------------------
65 (define-struct ls-client
66 (api-key)
67 (base-url default: "https://api.lemonsqueezy.com"))
69 ;; ---------------------------------------------------------------
70 ;; Internal helpers
71 ;; ---------------------------------------------------------------
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 is
86 ;;; 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 (cond
104 ((= status 401)
105 (error (string-append
106 "Lemon Squeezy API 401 Unauthorized. "
107 "Check your API key. " detail)))
108 ((= status 404)
109 (error (string-append
110 "Lemon Squeezy API 404 Not Found. " detail)))
111 ((= status 422)
112 (error (string-append
113 "Lemon Squeezy API 422 Validation Error. " detail)))
114 ((= status 429)
115 (error (string-append
116 "Lemon Squeezy API 429 Rate Limited. "
117 "Retry after a delay.")))
118 (else
119 (error (string-append
120 "Lemon Squeezy API error " (number->string status)
121 ": " detail))))))
123 ;;; Response checker with Lemon Squeezy JSON:API error parsing.
124 (define check-ls-response
125 (make-response-checker
126 name: "Lemon Squeezy API"
127 parse-error: ls-error))
129 ;;; Authenticated JSON GET request.
130 (define (ls-get/json client url)
131 (check-ls-response
132 (http-get url headers: (ls-auth-headers client))))
134 ;;; Authenticated JSON POST request.
135 (define (ls-post/json client url body)
136 (check-ls-response
137 (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-response
143 (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-response
149 (http-delete url headers: (ls-auth-headers client))))
151 ;; ---------------------------------------------------------------
152 ;; Generic endpoint helpers
153 ;; ---------------------------------------------------------------
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 record
158 ;;; opts: optional dict with page:, per-page:, filter:, include: keys
159 (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-append
163 (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 string
171 ;;; parser: function to convert a JSON:API resource to a record
172 (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 helpers
179 ;; ---------------------------------------------------------------
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.id
203 (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 (cond
246 ((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 & Filtering
274 ;; ---------------------------------------------------------------
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-params
287 (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-params
294 (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 returning
306 ;;; 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 ))