From bc06ba5a1b7804c02e199abe96af18b569a30491 Mon Sep 17 00:00:00 2001
From: Akira Tempaku <paku@skyizwhite.dev>
Date: Sat, 19 Apr 2025 00:14:19 +0900
Subject: [PATCH] init

---
 .gitignore    |   1 +
 README.md     |  53 ++++++++++++++++++++++++
 microcms.asd  |   8 ++++
 qlfile        |   5 +++
 qlfile.lock   |  24 +++++++++++
 src/main.lisp | 110 ++++++++++++++++++++++++++++++++++++++++++++++++++
 6 files changed, 201 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 README.md
 create mode 100644 microcms.asd
 create mode 100644 qlfile
 create mode 100644 qlfile.lock
 create mode 100644 src/main.lisp

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..95b52b0
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+.qlot
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..42a1504
--- /dev/null
+++ b/README.md
@@ -0,0 +1,53 @@
+# microcms-lisp-sdk
+
+microcms-lisp-sdk is a Common Lisp SDK for interacting with [microCMS](https://microcms.io) via its REST API. It provides macros to define client functions for both list and object type endpoints.
+
+## ⚙️ Configuration
+
+Before making API requests, set your API key and service domain:
+
+```lisp
+(setf microcms:*api-key* "your-api-key")
+(setf microcms:*service-domain* "your-service-domain") ; e.g., "example" for example.microcms.io
+```
+
+## 🚀 Usage
+
+### List Type Endpoint
+
+Use `define-list-client` macro to define functions for list-type content.
+
+```lisp
+(microcms:define-list-client article)
+```
+This will generate the following functions:
+
+| Function Name | Arguments | Description |
+|---------------|-----------|-------------|
+| `get-article-list` | (&optional `query`) | Get a list of articles. |
+| `get-article-list-detail` | (`id`, &optional `query`) | Get details of a specific article by ID. |
+| `create-article` | (`content`, &optional `query`) | Create a new article with the given content. |
+| `update-article` | (`id`, `content`) | Update an existing article by its ID with new content. |
+| `delete-article` | (`id`) | Delete an article by its ID. |
+
+Note: query arguments should be provided as a property list (plist), where keys use kebab-case (e.g., `:draft-key`).
+
+### Object Type Endpoint
+
+Use `define-object-client` macro to define functions for object-type content.
+
+```lisp
+(microcms:define-object-client profile)
+```
+
+This will generate the following functions:
+
+| Function Name | Arguments | Description |
+|---------------|-----------|-------------|
+| `get-profile-object` | () | Retrieve the profile object. |
+| `update-profile` | (`content`) | Update the content of the profile object. |
+
+### 📄 License
+
+MIT License
+© 2025 Akira Tempaku
diff --git a/microcms.asd b/microcms.asd
new file mode 100644
index 0000000..ba0929f
--- /dev/null
+++ b/microcms.asd
@@ -0,0 +1,8 @@
+(defsystem "microcms"
+  :version "0.1.0"
+  :description "microCMS Common Lisp SDK."
+  :author "Akira Tempaku"
+  :license "MIT"
+  :class :package-inferred-system
+  :pathname "src"
+  :depends-on ("microcms/main"))
diff --git a/qlfile b/qlfile
new file mode 100644
index 0000000..a7de216
--- /dev/null
+++ b/qlfile
@@ -0,0 +1,5 @@
+ql alexandria
+ql dexador
+ql jonathan
+ql quri
+ql kebab
diff --git a/qlfile.lock b/qlfile.lock
new file mode 100644
index 0000000..26a1d71
--- /dev/null
+++ b/qlfile.lock
@@ -0,0 +1,24 @@
+("quicklisp" .
+ (:class qlot/source/dist:source-dist
+  :initargs (:distribution "https://beta.quicklisp.org/dist/quicklisp.txt" :%version :latest)
+  :version "2024-10-12"))
+("alexandria" .
+ (:class qlot/source/ql:source-ql
+  :initargs (:%version :latest)
+  :version "ql-2024-10-12"))
+("dexador" .
+ (:class qlot/source/ql:source-ql
+  :initargs (:%version :latest)
+  :version "ql-2024-10-12"))
+("jonathan" .
+ (:class qlot/source/ql:source-ql
+  :initargs (:%version :latest)
+  :version "ql-2020-09-25"))
+("quri" .
+ (:class qlot/source/ql:source-ql
+  :initargs (:%version :latest)
+  :version "ql-2024-10-12"))
+("kebab" .
+ (:class qlot/source/ql:source-ql
+  :initargs (:%version :latest)
+  :version "ql-2015-06-08"))
diff --git a/src/main.lisp b/src/main.lisp
new file mode 100644
index 0000000..6ab8019
--- /dev/null
+++ b/src/main.lisp
@@ -0,0 +1,110 @@
+(defpackage #:microcms
+  (:nicknames #:microcms/main)
+  (:use #:cl)
+  (:import-from #:alexandria
+                #:alist-plist
+                #:assoc-value
+                #:remove-from-plist
+                #:symbolicate)
+  (:import-from #:jonathan
+                #:to-json
+                #:parse)
+  (:import-from #:dexador
+                #:request
+                #:http-request-failed)
+  (:import-from #:quri
+                #:make-uri
+                #:render-uri)
+  (:import-from #:kebab
+                #:to-camel-case)
+  (:export #:*api-key*
+           #:*service-domain*
+           #:define-list-client
+           #:define-object-client))
+(in-package #:microcms)
+
+(defparameter *api-key* nil)
+(defparameter *service-domain* nil)
+
+(defun %get-list (endpoint &optional query)
+  (%request :get endpoint "" query))
+
+(defun %get-list-detail (endpoint id &optional (query nil))
+  (%request :get endpoint id query))
+
+(defun %create (endpoint content &optional query)
+  (let ((id (getf content :|id|))
+        (pure-content (remove-from-plist content :|id|)))
+    (if id
+        (%put endpoint id pure-content)
+        (%post endpoint pure-content query))))
+
+(defun %put (endpoint id content &optional query)
+  (%request :put endpoint id query content))
+
+(defun %post (endpoint content &optional query)
+  (%request :post endpoint "" query content))
+
+(defun %update (endpoint id content)
+  (%request :patch endpoint id nil content))
+
+(defun %delete (endpoint id)
+  (%request :delete endpoint id))
+
+(defun %get-object (endpoint)
+  (%request :get endpoint))
+
+(defun %request (method endpoint &optional (path "") (query nil) (body nil))
+  (let* ((url (%build-uri endpoint path query))
+         (headers `(("X-MICROCMS-API-KEY" . ,*api-key*)
+                    ("Content-Type" . "application/json"))))
+    (format t "API request url: ~a~%" url)
+    (handler-case
+        (multiple-value-bind (resp-body status resp-headers)
+            (request url
+                     :method method
+                     :headers headers
+                     :content (and body (to-json body))
+                     :force-binary nil)
+          (format t "API response status: ~a~%" status)
+          (when (and (stringp resp-body)
+                     (search "application/json" (gethash "content-type" resp-headers)))
+            (parse resp-body)))
+      (http-request-failed ()
+        '(:|error| "API request failed")))))
+
+(defun %build-uri (endpoint &optional (path "") (query nil))
+  (let ((uri (make-uri
+              :scheme "https"
+              :host (format nil "~A.microcms.io" *service-domain*)
+              :path (format nil "/api/v1/~A/~A" endpoint path)
+              :query (%build-query query))))
+    (render-uri uri)))
+
+(defun %build-query (query)
+  (loop :for (key val) :on query :by #'cddr
+        :collect (cons (to-camel-case (symbol-name key)) val)))
+
+(defmacro define-list-client (endpoint)
+  (let ((str-endpoint (string-downcase (string endpoint))))
+    `(progn
+       (defun ,(symbolicate 'get- endpoint '-list) (&optional query)
+         (%get-list ,str-endpoint query))
+       (defun ,(symbolicate 'get- endpoint '-list-detail) (id &optional query)
+         (%get-list-detail ,str-endpoint id query))
+       (defun ,(symbolicate 'create- endpoint) (content &optional query)
+         (%create ,str-endpoint content query))
+       (defun ,(symbolicate 'update- endpoint) (id content)
+         (%update ,str-endpoint id content))
+       (defun ,(symbolicate 'delete- endpoint) (id)
+         (%delete ,str-endpoint id))
+       nil)))
+
+(defmacro define-object-client (endpoint)
+  (let ((str-endpoint (string-downcase (string endpoint))))
+    `(progn
+       (defun ,(symbolicate 'get- endpoint '-object) ()
+         (%get-object ,str-endpoint))
+       (defun ,(symbolicate 'update- endpoint) (content)
+         (%update ,str-endpoint nil content))
+       nil)))