Compare commits

...

9 Commits

Author SHA1 Message Date
Andrey Antukh
04f573cb23 WIP 2025-08-19 14:09:07 +02:00
Andrey Antukh
d46ad9d6f1 Add generative test case for openapi json serialization
This will prevent possible regression on introducing schemas without
generators or schema with generators that can't be serialized to json.
2025-08-19 11:34:50 +02:00
Andrey Antukh
2e8d08d047 🐛 Use ::sm/any instead of any for get-file-fragment rpc method schema
The usage of `any?` predicate as-is uses the standard any generator that
causes to generate java.lang.Character instances created that are not
properly serialiable to JSON. The `::sm/any` schema delimits the
generator to a commonly known serializable types on json.
2025-08-19 11:32:41 +02:00
Andrey Antukh
9c57596954 🐛 Add missing generators 2025-08-19 10:49:17 +02:00
Andrey Antukh
cc41fb569d 🔥 Remove already deprecated change spec 2025-08-19 10:49:00 +02:00
Andrey Antukh
79ad322809 📎 Update .gitignore file 2025-08-19 10:49:00 +02:00
Andrey Antukh
362ea401a0 🐛 Add missing attrs to add-component change schema 2025-08-19 10:49:00 +02:00
Andrey Antukh
7afb5abf35 🐛 Add proper schema for decoding :obj on add-obj change 2025-08-19 10:49:00 +02:00
Andrey Antukh
1b16129c45 🐛 Fix incorrect handling of assign operation on changes protocol 2025-08-19 10:49:00 +02:00
10 changed files with 294 additions and 166 deletions

1
.gitignore vendored
View File

@@ -31,6 +31,7 @@
/.clj-kondo/.cache
/_dump
/notes
/playground/
/backend/*.md
/backend/*.sql
/backend/*.txt

View File

@@ -357,7 +357,7 @@
[:id ::sm/uuid]
[:file-id ::sm/uuid]
[:created-at ::dt/instant]
[:content any?]])
[:content ::sm/any]])
(def schema:get-file-fragment
[:map {:title "get-file-fragment"}

View File

@@ -0,0 +1,36 @@
;; 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
(ns backend-tests.rpc-doc-test
"Internal binfile test, no RPC involved"
(:require
[app.common.json :as json]
[app.common.pprint :as pp]
[app.common.schema :as sm]
[app.common.schema.generators :as sg]
[app.common.schema.test :as smt]
[app.rpc :as-alias rpc]
[app.rpc.doc :as rpc.doc]
[backend-tests.helpers :as th]
[clojure.test :as t]))
(t/use-fixtures :once th/state-init)
(t/deftest openapi-context-json-encode
(smt/check!
(smt/for [context (->> sg/int
(sg/fmap (fn [_]
(rpc.doc/prepare-openapi-context (::rpc/methods th/*system*)))))]
(try
(json/encode context)
true
(catch Throwable _cause
false)))
{:num 30}))

View File

@@ -177,12 +177,6 @@
:title "Change"
:decode/json #(update % :type keyword)
::smd/simplified true}
[:set-option
;; DEPRECATED: remove before 2.3 release
;;
;; Is still there for not cause error when event is received
[:map {:title "SetOptionChange"}]]
[:set-comment-thread-position
[:map
@@ -195,7 +189,7 @@
[:map {:title "AddObjChange"}
[:type [:= :add-obj]]
[:id ::sm/uuid]
[:obj :map]
[:obj cts/schema:shape]
[:page-id {:optional true} ::sm/uuid]
[:component-id {:optional true} ::sm/uuid]
[:frame-id ::sm/uuid]
@@ -328,7 +322,9 @@
[:id ::sm/uuid]
[:name :string]
[:shapes {:optional true} [:vector {:gen/max 3} ::sm/any]]
[:path {:optional true} :string]]]
[:path {:optional true} :string]
[:main-instance-id ::sm/uuid]
[:main-instance-page ::sm/uuid]]]
[:mod-component
[:map {:title "ModCompoenentChange"}
@@ -546,11 +542,6 @@
#?(:clj (validate-shapes! data result items))
result))))
;; DEPRECATED: remove after 2.3 release
(defmethod process-change :set-option
[data _]
data)
;; --- Comment Threads
(defmethod process-change :set-comment-thread-position
@@ -1090,21 +1081,23 @@
;; === Operations
(def ^:private decode-shape
(sm/decoder cts/schema:shape sm/json-transformer))
(def decode-shape-attrs
(sm/decoder cts/schema:shape-attrs sm/json-transformer))
(defmethod process-operation :assign
[{:keys [type] :as shape} {:keys [value] :as op}]
(let [modifications (assoc value :type type)
modifications (decode-shape modifications)]
modifications (decode-shape-attrs modifications)]
(reduce-kv (fn [shape k v]
(process-operation shape {:type :set
:attr k
:val v
:ignore-touched (:ignore-touched op)
:ignore-geometry (:ignore-geometry op)}))
(if (not= v (get shape k))
(process-operation shape {:type :set
:attr k
:val v
:ignore-touched (:ignore-touched op)
:ignore-geometry (:ignore-geometry op)})
shape))
shape
modifications)))
(dissoc modifications :type))))
(defmethod process-operation :set
[shape op]

View File

@@ -111,7 +111,7 @@
(defmethod visit :fn [_ _ _ _] "FN")
(defmethod visit :vector [_ _ children _]
(str "[" (last children) "]"))
(str "[" (str/trim (last children)) "]"))
(defn -tagged [children] (map (fn [[tag _ c]] (str c " (tag: " tag ")")) children))
@@ -137,8 +137,14 @@
(some? suffix)
(str suffix))))
(defmethod visit :map-of [_ _ children _]
(str "map[" (first children) "," (second children) "]"))
(defmethod visit :map-of
[_ schema children _]
(let [props (m/properties schema)
title (some->> (:title props) str/camel str/capital)]
(str (if title
(str "type " title ": ")
"")
"map[" (first children) "," (second children) "]")))
(defmethod visit :union [_ _ children _]
(str/join " | " children))
@@ -156,61 +162,123 @@
(or (:title props)
"*")))
(defn- format-map
[schema children]
(let [props (m/properties schema)
closed? (get props :closed)
title (some->> (:title props) str/camel str/capital)
level 0
optional (into #{} (comp (filter (m/-comp :optional second))
(map first))
children)
entries (->> children
(map (fn [[k _ s]]
;; NOTE: maybe we can detect multiple lines
;; and then just insert a break line
(str (pad " " level) (str/camel k)
(when (contains? optional k) "?")
": " (str/trim s))))
(str/join ",\n"))
header (cond-> (str "type " title)
closed? (str "!")
(some? title) (str " "))]
(str (pad header level) "{\n" entries "\n" (pad "}\n" level))))
(defmethod visit :map
[_ schema children {:keys [::level ::max-level] :as options}]
(let [props (m/properties schema)
closed? (:closed props)
title (some->> (:title props) str/camel str/capital)]
(let [props (m/properties schema)
closed? (get props :closed)
title (some->> (:title props) str/camel str/capital)
extracted? (get props ::extracted false)]
(if (>= level max-level)
(or (some-> title str)
"<untitled>")
(let [optional (into #{} (comp (filter (m/-comp :optional second))
(map first))
children)
entries (->> children
(map (fn [[k _ s]]
(str (pad " " level) (str/camel k)
(when (contains? optional k) "?")
": " s)))
(str/join ",\n"))
(if (= level 0)
(format-map schema children)
header (cond-> (str "type " title)
closed? (str "!")
(some? title) (str " "))]
(if title
(if extracted?
(format-map schema children)
(let [schema (mu/update-properties schema assoc ::extracted true)]
(swap! *definitions* conj (describe* schema options))
(pad title level)))
(format-map schema children)))))
(str (pad header level) "{\n" entries "\n" (pad "}\n" level))))))
(defn format-multi
[s children]
(let [props (m/properties s)
title (some-> (:title props) str/camel str/capital)
dispatcher (or (-> s m/properties :dispatch-description)
(-> s m/properties :dispatch))
(defmethod visit :multi
[_ s children {:keys [::level ::max-level] :as options}]
(let [props (m/properties s)
title (some-> (:title props) str/camel str/capital)]
(if (>= level max-level)
title
(let [dispatcher (or (-> s m/properties :dispatch-description)
(-> s m/properties :dispatch))
entries (->> children
(map (fn [[_ _ shape]]
(str shape)))
(str/join ",\n"))
prefix (apply str (take (inc level) (repeat " ")))
entries (->> children
(map (fn [[_ _ shape]]
(str prefix shape)))
(str/join ",\n"))
header (cond-> "multi"
header (cond-> "multi"
(some? title) (str " " title)
:always (str " [dispatch=" (d/name dispatcher) "]"))]
(str header " {\n" entries "\n" (pad "}" level))))))
(str header " {\n" entries "\n}")))
(defmethod visit :multi
[_ schema children {:keys [::level ::max-level] :as options}]
(let [props (m/properties schema)
title (some-> (:title props) str/camel str/capital)
extracted? (get props ::extracted false)]
(cond
(zero? level)
(format-multi schema children)
(and title extracted?)
(format-multi schema children)
(and title (not extracted?))
(let [schema (mu/update-properties schema assoc ::extracted true)]
(swap! *definitions* conj (describe* schema options))
(pad title level))
:else
(format-multi schema children))))
(defn- format-merge
[schema children]
(let [props (m/properties schema)
entries (->> children
(map (fn [shape]
(str shape)))
(str/join ",\n"))
title (some-> (:title props) str/camel str/capital)
header (str "merge type " title)]
(str header " {\n" entries "\n}")))
(defmethod visit :merge
[_ schema children _]
(let [entries (str/join ",\n" children)
props (m/properties schema)
title (or (some-> (:title props) str/camel str/capital)
"<untitled>")]
(str "merge type " title " { \n" entries "\n}\n")))
[_ schema children {:keys [::level ::max-level] :as options}]
(let [props (m/properties schema)
title (some-> (:title props) str/camel str/capital)
extracted? (get props ::extracted false)]
(cond
(zero? level)
(format-merge schema children)
(and title extracted?)
(format-merge schema children)
(and title (not extracted?))
(let [schema (mu/update-properties schema assoc ::extracted true)]
(swap! *definitions* conj (describe* schema options))
(pad title level))
:else
(format-merge schema children))))
(defmethod visit ::sm/one-of
[_ _ children _]
@@ -219,8 +287,21 @@
(map d/name)
(str/join "|")) ")")))
(defmethod visit :schema [_ schema children options]
(visit ::m/schema schema children options))
(defmethod visit :schema
[_ schema children options]
(let [props (m/properties schema)
title (some-> (:title props) str/camel str/capital)
extracted? (get props ::extracted false)]
(if title
(if extracted?
(str "type " title ": "
(visit ::m/schema schema children options))
(let [schema (mu/update-properties schema assoc ::extracted true)]
(swap! *definitions* conj (describe* schema options))
title))
(visit ::m/schema schema children options))))
(defmethod visit ::m/schema
[_ schema _ {:keys [::level ::limit ::max-level] :as options}]
@@ -239,9 +320,12 @@
(describe* schema' options)))
(and ref title)
(do
(when (<= limit max-level)
(swap! *definitions* conj (describe* schema' (assoc options ::base-limit limit))))
(let [options (-> options
(assoc ::base-level level)
)]
;; (when (<= limit max-level)
(swap! *definitions* conj (describe* schema' options))
title)
@@ -254,10 +338,11 @@
(describe* schema' (assoc options ::base-level level ::base-limit limit)))))
(defn describe* [s options]
(letfn [(walk-fn [schema path children {:keys [::base-level ::base-limit] :or {base-level 0 base-limit 0} :as options}]
(let [options (assoc options
::limit (+ base-limit (count path))
::level (+ base-level (count path)))]
(letfn [(walk-fn [schema path children {:keys [::base-level ::base-limit]
:or {base-level 0 base-limit 0} :as options}]
(let [options (-> options
(assoc ::limit (+ base-limit (count path)))
(assoc ::level (+ base-level (count path))))]
(visit (m/type schema) schema children options)))]
(m/walk s walk-fn options)))

View File

@@ -24,7 +24,7 @@
(def schema:plugin-data
(sm/register!
^{::sm/type ::plugin-data}
[:map-of {:gen/max 5}
[:map-of {:gen/max 5 :title "PluginsData"}
schema:keyword
[:map-of {:gen/max 5}
schema:string

View File

@@ -185,50 +185,50 @@
[:height ::sm/safe-number]])
(def schema:shape-generic-attrs
[:map {:title "ShapeAttrs"}
[:page-id {:optional true} ::sm/uuid]
[:component-id {:optional true} ::sm/uuid]
[:component-file {:optional true} ::sm/uuid]
[:component-root {:optional true} :boolean]
[:main-instance {:optional true} :boolean]
[:remote-synced {:optional true} :boolean]
[:shape-ref {:optional true} ::sm/uuid]
[:touched {:optional true} [:maybe [:set :keyword]]]
[:blocked {:optional true} :boolean]
[:collapsed {:optional true} :boolean]
[:locked {:optional true} :boolean]
[:hidden {:optional true} :boolean]
[:masked-group {:optional true} :boolean]
[:fills {:optional true}
[:vector {:gen/max 2} schema:fill]]
[:proportion {:optional true} ::sm/safe-number]
[:proportion-lock {:optional true} :boolean]
[:constraints-h {:optional true}
[::sm/one-of horizontal-constraint-types]]
[:constraints-v {:optional true}
[::sm/one-of vertical-constraint-types]]
[:fixed-scroll {:optional true} :boolean]
[:r1 {:optional true} ::sm/safe-number]
[:r2 {:optional true} ::sm/safe-number]
[:r3 {:optional true} ::sm/safe-number]
[:r4 {:optional true} ::sm/safe-number]
[:opacity {:optional true} ::sm/safe-number]
[:grids {:optional true}
[:vector {:gen/max 2} ::ctg/grid]]
[:exports {:optional true}
[:vector {:gen/max 2} ::ctse/export]]
[:strokes {:optional true}
[:vector {:gen/max 2} schema:stroke]]
[:blend-mode {:optional true}
[::sm/one-of blend-modes]]
[:map {:title "ShapeGenericAttrs"}
;; [:page-id {:optional true} ::sm/uuid]
;; [:component-id {:optional true} ::sm/uuid]
;; [:component-file {:optional true} ::sm/uuid]
;; [:component-root {:optional true} :boolean]
;; [:main-instance {:optional true} :boolean]
;; [:remote-synced {:optional true} :boolean]
;; [:shape-ref {:optional true} ::sm/uuid]
;; [:touched {:optional true} [:maybe [:set :keyword]]]
;; [:blocked {:optional true} :boolean]
;; [:collapsed {:optional true} :boolean]
;; [:locked {:optional true} :boolean]
;; [:hidden {:optional true} :boolean]
;; [:masked-group {:optional true} :boolean]
;; [:fills {:optional true}
;; [:vector {:gen/max 2} schema:fill]]
;; [:proportion {:optional true} ::sm/safe-number]
;; [:proportion-lock {:optional true} :boolean]
;; [:constraints-h {:optional true}
;; [::sm/one-of horizontal-constraint-types]]
;; [:constraints-v {:optional true}
;; [::sm/one-of vertical-constraint-types]]
;; [:fixed-scroll {:optional true} :boolean]
;; [:r1 {:optional true} ::sm/safe-number]
;; [:r2 {:optional true} ::sm/safe-number]
;; [:r3 {:optional true} ::sm/safe-number]
;; [:r4 {:optional true} ::sm/safe-number]
;; [:opacity {:optional true} ::sm/safe-number]
;; [:grids {:optional true}
;; [:vector {:gen/max 2} ::ctg/grid]]
;; [:exports {:optional true}
;; [:vector {:gen/max 2} ::ctse/export]]
;; [:strokes {:optional true}
;; [:vector {:gen/max 2} schema:stroke]]
;; [:blend-mode {:optional true}
;; [::sm/one-of blend-modes]]
[:interactions {:optional true}
[:vector {:gen/max 2} ::ctsi/interaction]]
[:shadow {:optional true}
[:vector {:gen/max 1} ctss/schema:shadow]]
[:blur {:optional true} ::ctsb/blur]
;; [:shadow {:optional true}
;; [:vector {:gen/max 1} ctss/schema:shadow]]
;; [:blur {:optional true} ::ctsb/blur]
[:grow-type {:optional true}
[::sm/one-of grow-types]]
[:applied-tokens {:optional true} cto/schema:applied-tokens]
;; [:applied-tokens {:optional true} cto/schema:applied-tokens]
[:plugin-data {:optional true} ::ctpg/plugin-data]])
(def schema:group-attrs
@@ -306,6 +306,15 @@
(merge attrs1 shape attrs2 attrs3)))))
(sg/fmap create-shape)))
(def kaka
[:merge {:title "BoolShape"}
ctsl/schema:layout-child-attrs
schema:bool-attrs
schema:shape-generic-attrs
schema:shape-base-attrs])
(def schema:shape-attrs
[:multi {:dispatch :type
:decode/json (fn [shape]

View File

@@ -175,21 +175,23 @@
[:url :string]])
(def schema:interaction
[:and {:title "Interaction"
:gen/gen (sg/one-of (sg/generator schema:navigate-interaction)
(sg/generator schema:open-overlay-interaction)
(sg/generator schema:close-overlay-interaction)
(sg/generator schema:toggle-overlay-interaction)
(sg/generator schema:prev-scren-interaction)
(sg/generator schema:open-url-interaction))}
schema:interaction-attrs
[:multi {:dispatch :action-type}
[:navigate schema:navigate-interaction]
[:open-overlay schema:open-overlay-interaction]
[:toggle-overlay schema:toggle-overlay-interaction]
[:close-overlay schema:close-overlay-interaction]
[:prev-screen schema:prev-scren-interaction]
[:open-url schema:open-url-interaction]]])
[:schema {:title "Interaction"
:gen/gen (sg/one-of (sg/generator schema:navigate-interaction)
(sg/generator schema:open-overlay-interaction)
(sg/generator schema:close-overlay-interaction)
(sg/generator schema:toggle-overlay-interaction)
(sg/generator schema:prev-scren-interaction)
(sg/generator schema:open-url-interaction))}
[:and
schema:interaction-attrs
[:multi {:dispatch :action-type}
;; [:navigate schema:navigate-interaction]
;; [:open-overlay schema:open-overlay-interaction]
;; [:toggle-overlay schema:toggle-overlay-interaction]
;; [:close-overlay schema:close-overlay-interaction]
;; [:prev-screen schema:prev-scren-interaction]
[:open-url schema:open-url-interaction]
]]])
(sm/register! ::interaction schema:interaction)

View File

@@ -8,6 +8,7 @@
(:require
[app.common.data :as d]
[app.common.schema :as sm]
[app.common.schema.desc-js-like :as smdj]
[clojure.data :as data]
[clojure.set :as set]
[malli.util :as mu]))
@@ -51,7 +52,8 @@
(into #{} (keys token-type->dtcg-token-type)))
(def token-name-ref
[:and :string [:re #"^(?!\$)([a-zA-Z0-9-$_]+\.?)*(?<!\.)$"]])
[:schema {:title "TokenNameRef"}
[:and :string [:re #"^(?!\$)([a-zA-Z0-9-$_]+\.?)*(?<!\.)$"]]])
(def ^:private schema:color
[:map
@@ -61,7 +63,7 @@
(def color-keys (schema-keys schema:color))
(def ^:private schema:border-radius
[:map
[:map {:title "BorderRadiusTokenAttrs"}
[:r1 {:optional true} token-name-ref]
[:r2 {:optional true} token-name-ref]
[:r3 {:optional true} token-name-ref]
@@ -76,7 +78,7 @@
(def stroke-width-keys (schema-keys schema:stroke-width))
(def ^:private schema:sizing
[:map
[:map {:title "SizingTokenAttrs"}
[:width {:optional true} token-name-ref]
[:height {:optional true} token-name-ref]
[:layout-item-min-w {:optional true} token-name-ref]
@@ -87,44 +89,46 @@
(def sizing-keys (schema-keys schema:sizing))
(def ^:private schema:opacity
[:map
[:map {:title "OpacityTokenAttrs"}
[:opacity {:optional true} token-name-ref]])
(def opacity-keys (schema-keys schema:opacity))
(def ^:private schema:spacing-gap
[:map
[:map {:title "SpacingGapTokenAttrs"}
[:row-gap {:optional true} token-name-ref]
[:column-gap {:optional true} token-name-ref]])
(def ^:private schema:spacing-padding
[:map
[:map {:title "SpacingPaddingTokenAttrs"}
[:p1 {:optional true} token-name-ref]
[:p2 {:optional true} token-name-ref]
[:p3 {:optional true} token-name-ref]
[:p4 {:optional true} token-name-ref]])
(def ^:private schema:spacing-margin
[:map
[:map {:title "SpacingMarginTokenAttrs"}
[:m1 {:optional true} token-name-ref]
[:m2 {:optional true} token-name-ref]
[:m3 {:optional true} token-name-ref]
[:m4 {:optional true} token-name-ref]])
(def ^:private schema:spacing
(reduce mu/union [schema:spacing-gap
schema:spacing-padding
schema:spacing-margin]))
(-> (reduce mu/union [schema:spacing-gap
schema:spacing-padding
schema:spacing-margin])
(mu/update-properties assoc :title "SpacingTokenAttrs")))
(def spacing-margin-keys (schema-keys schema:spacing-margin))
(def spacing-keys (schema-keys schema:spacing))
(def ^:private schema:dimensions
(reduce mu/union [schema:sizing
schema:spacing
schema:stroke-width
schema:border-radius]))
(-> (reduce mu/union [schema:sizing
schema:spacing
schema:stroke-width
schema:border-radius])
(mu/update-properties assoc :title "DimensionsTokenAttrs")))
(def dimensions-keys (schema-keys schema:dimensions))
@@ -135,22 +139,20 @@
(def axis-keys (schema-keys schema:axis))
(def ^:private schema:rotation
[:map
[:map {:title "RotationTokenAttrs"}
[:rotation {:optional true} token-name-ref]])
(def rotation-keys (schema-keys schema:rotation))
(def ^:private schema:font-size
[:map
[:map {:title "FontSizeTokenAttrs"}
[:font-size {:optional true} token-name-ref]])
(def font-size-keys (schema-keys schema:font-size))
(def ^:private schema:letter-spacing
[:map
[:map {:title "LetterSpacingTokenAttrs"}
[:letter-spacing {:optional true} token-name-ref]])
(def letter-spacing-keys (schema-keys schema:letter-spacing))
@@ -162,8 +164,9 @@
(def ff-typography-keys (set/difference typography-keys font-size-keys))
(def ^:private schema:number
(reduce mu/union [[:map [:line-height {:optional true} token-name-ref]]
schema:rotation]))
(-> (reduce mu/union [[:map [:line-height {:optional true} token-name-ref]]
schema:rotation])
(mu/update-properties assoc :title "NumberTokenAttrs")))
(def number-keys (schema-keys schema:number))
@@ -180,10 +183,10 @@
number-keys))
(def ^:private schema:tokens
[:map {:title "Applied Tokens"}])
[:map {:title "GenericTokenAttrs"}])
(def schema:applied-tokens
[:merge
[:merge {:title "AppliedTokens"}
schema:tokens
schema:border-radius
schema:sizing

View File

@@ -472,18 +472,17 @@
(get-ordered-set-names [_] "get an ordered sequence of all sets names in the library")
(get-set [_ set-name] "get one set looking for name"))
(def schema:token-set-node
[:schema {:registry {::node [:or [:fn token-set?]
[:and
[:map-of {:gen/max 5} :string [:ref ::node]]
[:fn d/ordered-map?]]]}}
(def ^:private schema:token-set-node
[:schema {:registry {::node
[:or [:fn token-set?]
[:and
[:map-of {:gen/max 5} :string [:ref ::node]]
[:fn d/ordered-map?]]]}}
[:ref ::node]])
(def schema:token-sets
[:and
[:map-of {:title "TokenSets"}
:string
schema:token-set-node]
(def ^:private schema:token-sets
[:and {:title "TokenSets"}
[:map-of :string schema:token-set-node]
[:fn d/ordered-map?]])
(def ^:private check-token-sets