🔧 Add TokensStatus data type for forward compatibility (#11314)

* 🔧 Add TokensStatus data type for forward compatibility

* 📚 Add Transit round-trip tests for TokensStatus type

Add serialization tests validating TokensStatus survives Transit
encode/decode and Fressian round-trip (JVM). Clarify tokens-source
field comment as forward-compatibility placeholder.

AI-assisted-by: mimo-v2.5-pro

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
This commit is contained in:
Andrés Moya 2026-08-27 11:51:47 +02:00 committed by GitHub
parent 582187dd03
commit c9a2b8f12f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 256 additions and 2 deletions

View File

@ -28,7 +28,8 @@
[app.common.types.shape :as cts]
[app.common.types.shape-tree :as ctst]
[app.common.types.text :as txt]
[app.common.types.tokens-lib :refer [schema:tokens-lib]]
[app.common.types.tokens-lib :as ctob]
[app.common.types.tokens-status :as ctos]
[app.common.types.typographies-list :as ctyl]
[app.common.types.typography :as cty]
[app.common.uuid :as uuid]
@ -86,7 +87,9 @@
[:components {:optional true} schema:components]
[:typographies {:optional true} schema:typographies]
[:plugin-data {:optional true} schema:plugin-data]
[:tokens-lib {:optional true} schema:tokens-lib]])
[:tokens-source {:optional true} ::sm/uuid] ;; Forward-compat: UUID of external library containing tokens-lib (full support in follow-up PR)
[:tokens-lib {:optional true} ctob/schema:tokens-lib]
[:tokens-status {:optional true} ctos/schema:tokens-status]])
(def schema:file-metadata
[:map {:title "Metadata"}
@ -316,6 +319,7 @@
(update-objects-tree container f)))))
;; Asset helpers
(defn find-component-file
[file libraries component-file]
(if (and (some? file) (= component-file (:id file)))

View File

@ -0,0 +1,147 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.common.types.tokens-status
(:require
#?(:clj [app.common.fressian :as fres])
#?(:clj [clojure.data.json :as c.json])
[app.common.schema :as sm]
[app.common.schema.generators :as sg]
[app.common.transit :as t]
[clojure.core.protocols :as cp]
[clojure.datafy :refer [datafy]]
[clojure.pprint :as pp]))
;; TokensStatus datatype contains the activation status of the themes and sets
;; in a tokens library.
(defprotocol ITokensStatus
(get-active-theme-ids [_] "Return a clojure set of active theme ids")
(get-active-set-ids [_] "Return a clojure set of active set ids")
(theme-active? [_ theme-id] "Check if a theme is active")
(set-active? [_ set-id] "Check if a set is active")
(set-tokens-status [_ theme-ids set-ids] "Set the activation status of the themes and sets"))
(deftype TokensStatus [active-theme-ids active-set-ids]
cp/Datafiable
(datafy [_]
{:active-theme-ids active-theme-ids
:active-set-ids active-set-ids})
#?@(:clj
[c.json/JSONWriter
(-write [this writter options]
(c.json/-write (datafy this) writter options))])
ITokensStatus
(get-active-theme-ids [_]
active-theme-ids)
(get-active-set-ids [_]
active-set-ids)
(theme-active? [_ theme-id]
(assert (uuid? theme-id))
(contains? active-theme-ids theme-id))
(set-active? [_ set-id]
(assert (uuid? set-id))
(contains? active-set-ids set-id))
(set-tokens-status [_ theme-ids set-ids]
(assert (set? theme-ids))
(assert (set? set-ids))
(TokensStatus. theme-ids set-ids)))
;; === Helper & Predicate ===
(defn map->TokensStatus
[{:keys [active-theme-ids active-set-ids]}]
(TokensStatus. active-theme-ids active-set-ids))
(defn tokens-status?
[o]
(instance? TokensStatus o))
;; === Schemas, Check functions & Constructor ===
(declare make-tokens-status)
(def schema:tokens-status-attrs
[:map {:title "TokensStatus"}
[:active-theme-ids {:optional true} [:set {:gen/max 5} ::sm/uuid]]
[:active-set-ids {:optional true} [:set {:gen/max 5} ::sm/uuid]]])
(def schema:tokens-status
[:and {:gen/gen (->> (sg/generator schema:tokens-status-attrs)
(sg/fmap #(make-tokens-status %)))}
[:fn tokens-status?]])
(def ^:private check-tokens-status-attrs
(sm/check-fn schema:tokens-status-attrs
:hint "expected valid params for tokens-status"))
(def check-tokens-status
(sm/check-fn schema:tokens-status
:hint "expected valid tokens-status"))
(defn make-tokens-status
[& {:as attrs}]
(-> attrs
(update :active-theme-ids #(or % #{}))
(update :active-set-ids #(or % #{}))
(check-tokens-status-attrs)
(map->TokensStatus)))
;; === Pretty-print for debugging ===
(defmethod pp/simple-dispatch TokensStatus [^TokensStatus obj]
(.write *out* "#penpot/tokens-status ")
(pp/pprint-newline :miser)
(pp/pprint (datafy obj)))
#?(:clj
(do
(defmethod print-method TokensStatus
[^TokensStatus this ^java.io.Writer w]
(.write w "#penpot/tokens-status ")
(print-method (datafy this) w))
(defmethod print-dup TokensStatus
[^TokensStatus this ^java.io.Writer w]
(print-method this w)))
:cljs
(extend-type TokensStatus
cljs.core/IPrintWithWriter
(-pr-writer [this writer opts]
(-write writer "#penpot/tokens-status ")
(-pr-writer (datafy this) writer opts))
cljs.core/IEncodeJS
(-clj->js [this]
(clj->js (datafy this)))))
;; === Transit serialization ===
(t/add-handlers!
{:id "penpot/tokens-status"
:class TokensStatus
:wfn datafy
:rfn #(make-tokens-status %)})
;; === Fressian serialization ===
#?(:clj
(fres/add-handlers!
{:name "penpot/tokens-status/v1"
:class TokensStatus
:wfn (fn [n w o]
(fres/write-tag! w n 1)
(fres/write-object! w (datafy o)))
:rfn (fn [r]
(let [obj (fres/read-object! r)]
(make-tokens-status obj)))}))

View File

@ -86,6 +86,7 @@
[common-tests.types.shape-layout-test]
[common-tests.types.token-test]
[common-tests.types.tokens-lib-test]
[common-tests.types.tokens-status-test]
[common-tests.undo-stack-test]
[common-tests.uuid-test]))
@ -164,6 +165,7 @@
'common-tests.types.shape-layout-test
'common-tests.types.token-test
'common-tests.types.tokens-lib-test
'common-tests.types.tokens-status-test
'common-tests.undo-stack-test
'common-tests.uuid-test])

View File

@ -0,0 +1,101 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns common-tests.types.tokens-status-test
(:require
#?(:clj [app.common.fressian :as fres])
#?(:clj [clojure.data.json :as json])
[app.common.transit :as tr]
[app.common.types.tokens-status :as ctos]
[app.common.uuid :as uuid]
[clojure.datafy :refer [datafy]]
[clojure.test :as t]))
(t/deftest make-tokens-status
(let [theme-id (uuid/next)
set-id (uuid/next)
status (ctos/make-tokens-status :active-theme-ids #{theme-id}
:active-set-ids #{set-id})]
(t/is (ctos/tokens-status? status))
(t/is (ctos/check-tokens-status status))
(t/is (= 1 (count (ctos/get-active-theme-ids status))))
(t/is (ctos/theme-active? status theme-id))
(t/is (= 1 (count (ctos/get-active-set-ids status))))
(t/is (ctos/set-active? status set-id))))
(t/deftest make-tokens-status-defaults
(let [status (ctos/make-tokens-status)]
(t/is (ctos/tokens-status? status))
(t/is (ctos/check-tokens-status status))
(t/is (= 0 (count (ctos/get-active-theme-ids status))))
(t/is (= 0 (count (ctos/get-active-set-ids status))))))
(t/deftest make-invalid-tokens-status
(t/testing "non-set for active-themes"
(t/is (thrown-with-msg? #?(:cljs js/Error :clj Exception)
#"expected valid params for tokens-status"
(ctos/make-tokens-status :active-theme-ids []))))
(t/testing "non-uuid in active-sets"
(t/is (thrown-with-msg? #?(:cljs js/Error :clj Exception)
#"expected valid params for tokens-status"
(ctos/make-tokens-status :active-set-ids #{"not-a-uuid"})))))
(t/deftest set-tokens-status
(let [theme1-id (uuid/next)
theme2-id (uuid/next)
theme3-id (uuid/next)
set1-id (uuid/next)
set2-id (uuid/next)
set3-id (uuid/next)
status (-> (ctos/make-tokens-status {:active-theme-ids #{theme3-id}
:active-set-ids #{set3-id}})
(ctos/set-tokens-status #{theme1-id theme2-id} #{set1-id set2-id}))]
(t/is (= #{theme1-id theme2-id} (ctos/get-active-theme-ids status)))
(t/is (= #{set1-id set2-id} (ctos/get-active-set-ids status)))))
(t/deftest datafy-tokens-status
(let [theme-id (uuid/next)
set-id (uuid/next)
status (ctos/make-tokens-status :active-theme-ids #{theme-id}
:active-set-ids #{set-id})
result (datafy status)]
(t/is (map? result))
(t/is (not (ctos/tokens-status? result)))
(t/is (= (:active-theme-ids result) #{theme-id}))
(t/is (= (:active-set-ids result) #{set-id}))))
(t/deftest transit-serialization
(let [theme-id (uuid/next)
set-id (uuid/next)
status (ctos/make-tokens-status :active-theme-ids #{theme-id}
:active-set-ids #{set-id})
encoded (tr/encode-str status)
status' (tr/decode-str encoded)]
(t/is (ctos/tokens-status? status'))
(t/is (= (datafy status') (datafy status)))))
#?(:clj
(t/deftest fressian-serialization
(let [theme-id (uuid/next)
set-id (uuid/next)
status (ctos/make-tokens-status :active-theme-ids #{theme-id}
:active-set-ids #{set-id})
encoded (fres/encode status)
status' (fres/decode encoded)]
(t/is (ctos/tokens-status? status'))
(t/is (= (datafy status') (datafy status))))))
#?(:clj
(t/deftest json-serialization
(let [theme-id (uuid/next)
set-id (uuid/next)
status (ctos/make-tokens-status :active-theme-ids #{theme-id}
:active-set-ids #{set-id})
json-str (json/write-str status)
parsed (json/read-str json-str :key-fn keyword)]
(t/is (map? parsed))
(t/is (= [(str theme-id)] (:active-theme-ids parsed)))
(t/is (= [(str set-id)] (:active-set-ids parsed))))))