🎉 Add proper schema encoding/decoding mechanism

this allows almost all api operations to success usin application/json
encoding with the exception of the update-file, which we need to
approach a bit differently;

the reason update-file is different, is because the operations vector
is right now defined without the context of shape type, so we are just
unable to properly parse the value to correct type using the schema
decoding mechanism
This commit is contained in:
Andrey Antukh 2024-08-16 12:07:53 +02:00
parent 0db1eed87f
commit cacee40d11
50 changed files with 1290 additions and 843 deletions

View file

@ -91,25 +91,25 @@
[:public-uri {:optional false} :string] [:public-uri {:optional false} :string]
[:host {:optional false} :string] [:host {:optional false} :string]
[:http-server-port {:optional true} :int] [:http-server-port {:optional true} ::sm/int]
[:http-server-host {:optional true} :string] [:http-server-host {:optional true} :string]
[:http-server-max-body-size {:optional true} :int] [:http-server-max-body-size {:optional true} ::sm/int]
[:http-server-max-multipart-body-size {:optional true} :int] [:http-server-max-multipart-body-size {:optional true} ::sm/int]
[:http-server-io-threads {:optional true} :int] [:http-server-io-threads {:optional true} ::sm/int]
[:http-server-worker-threads {:optional true} :int] [:http-server-worker-threads {:optional true} ::sm/int]
[:telemetry-uri {:optional true} :string] [:telemetry-uri {:optional true} :string]
[:telemetry-with-taiga {:optional true} :boolean] ;; DELETE [:telemetry-with-taiga {:optional true} ::sm/boolean] ;; DELETE
[:file-snapshot-total {:optional true} :int] [:file-snapshot-total {:optional true} ::sm/int]
[:file-snapshot-every {:optional true} :int] [:file-snapshot-every {:optional true} ::sm/int]
[:file-snapshot-timeout {:optional true} ::dt/duration] [:file-snapshot-timeout {:optional true} ::dt/duration]
[:media-max-file-size {:optional true} :int] [:media-max-file-size {:optional true} ::sm/int]
[:deletion-delay {:optional true} ::dt/duration] ;; REVIEW [:deletion-delay {:optional true} ::dt/duration] ;; REVIEW
[:telemetry-enabled {:optional true} :boolean] [:telemetry-enabled {:optional true} ::sm/boolean]
[:default-blob-version {:optional true} :int] [:default-blob-version {:optional true} ::sm/int]
[:allow-demo-users {:optional true} :boolean] [:allow-demo-users {:optional true} ::sm/boolean]
[:error-report-webhook {:optional true} :string] [:error-report-webhook {:optional true} :string]
[:user-feedback-destination {:optional true} :string] [:user-feedback-destination {:optional true} :string]
@ -118,30 +118,30 @@
[:rpc-climit-config {:optional true} ::fs/path] [:rpc-climit-config {:optional true} ::fs/path]
[:audit-log-archive-uri {:optional true} :string] [:audit-log-archive-uri {:optional true} :string]
[:audit-log-http-handler-concurrency {:optional true} :int] [:audit-log-http-handler-concurrency {:optional true} ::sm/int]
[:default-executor-parallelism {:optional true} :int] ;; REVIEW [:default-executor-parallelism {:optional true} ::sm/int] ;; REVIEW
[:scheduled-executor-parallelism {:optional true} :int] ;; REVIEW [:scheduled-executor-parallelism {:optional true} ::sm/int] ;; REVIEW
[:worker-default-parallelism {:optional true} :int] [:worker-default-parallelism {:optional true} ::sm/int]
[:worker-webhook-parallelism {:optional true} :int] [:worker-webhook-parallelism {:optional true} ::sm/int]
[:database-password {:optional true} [:maybe :string]] [:database-password {:optional true} [:maybe :string]]
[:database-uri {:optional true} :string] [:database-uri {:optional true} :string]
[:database-username {:optional true} [:maybe :string]] [:database-username {:optional true} [:maybe :string]]
[:database-readonly {:optional true} :boolean] [:database-readonly {:optional true} ::sm/boolean]
[:database-min-pool-size {:optional true} :int] [:database-min-pool-size {:optional true} ::sm/int]
[:database-max-pool-size {:optional true} :int] [:database-max-pool-size {:optional true} ::sm/int]
[:quotes-teams-per-profile {:optional true} :int] [:quotes-teams-per-profile {:optional true} ::sm/int]
[:quotes-access-tokens-per-profile {:optional true} :int] [:quotes-access-tokens-per-profile {:optional true} ::sm/int]
[:quotes-projects-per-team {:optional true} :int] [:quotes-projects-per-team {:optional true} ::sm/int]
[:quotes-invitations-per-team {:optional true} :int] [:quotes-invitations-per-team {:optional true} ::sm/int]
[:quotes-profiles-per-team {:optional true} :int] [:quotes-profiles-per-team {:optional true} ::sm/int]
[:quotes-files-per-project {:optional true} :int] [:quotes-files-per-project {:optional true} ::sm/int]
[:quotes-files-per-team {:optional true} :int] [:quotes-files-per-team {:optional true} ::sm/int]
[:quotes-font-variants-per-team {:optional true} :int] [:quotes-font-variants-per-team {:optional true} ::sm/int]
[:quotes-comment-threads-per-file {:optional true} :int] [:quotes-comment-threads-per-file {:optional true} ::sm/int]
[:quotes-comments-per-file {:optional true} :int] [:quotes-comments-per-file {:optional true} ::sm/int]
[:auth-data-cookie-domain {:optional true} :string] [:auth-data-cookie-domain {:optional true} :string]
[:auth-token-cookie-name {:optional true} :string] [:auth-token-cookie-name {:optional true} :string]
@ -178,15 +178,15 @@
[:ldap-bind-dn {:optional true} :string] [:ldap-bind-dn {:optional true} :string]
[:ldap-bind-password {:optional true} :string] [:ldap-bind-password {:optional true} :string]
[:ldap-host {:optional true} :string] [:ldap-host {:optional true} :string]
[:ldap-port {:optional true} :int] [:ldap-port {:optional true} ::sm/int]
[:ldap-ssl {:optional true} :boolean] [:ldap-ssl {:optional true} ::sm/boolean]
[:ldap-starttls {:optional true} :boolean] [:ldap-starttls {:optional true} ::sm/boolean]
[:ldap-user-query {:optional true} :string] [:ldap-user-query {:optional true} :string]
[:profile-bounce-max-age {:optional true} ::dt/duration] [:profile-bounce-max-age {:optional true} ::dt/duration]
[:profile-bounce-threshold {:optional true} :int] [:profile-bounce-threshold {:optional true} ::sm/int]
[:profile-complaint-max-age {:optional true} ::dt/duration] [:profile-complaint-max-age {:optional true} ::dt/duration]
[:profile-complaint-threshold {:optional true} :int] [:profile-complaint-threshold {:optional true} ::sm/int]
[:redis-uri {:optional true} :string] [:redis-uri {:optional true} :string]
@ -197,15 +197,15 @@
[:smtp-default-reply-to {:optional true} :string] [:smtp-default-reply-to {:optional true} :string]
[:smtp-host {:optional true} :string] [:smtp-host {:optional true} :string]
[:smtp-password {:optional true} [:maybe :string]] [:smtp-password {:optional true} [:maybe :string]]
[:smtp-port {:optional true} :int] [:smtp-port {:optional true} ::sm/int]
[:smtp-ssl {:optional true} :boolean] [:smtp-ssl {:optional true} ::sm/boolean]
[:smtp-tls {:optional true} :boolean] [:smtp-tls {:optional true} ::sm/boolean]
[:smtp-username {:optional true} [:maybe :string]] [:smtp-username {:optional true} [:maybe :string]]
[:urepl-host {:optional true} :string] [:urepl-host {:optional true} :string]
[:urepl-port {:optional true} :int] [:urepl-port {:optional true} ::sm/int]
[:prepl-host {:optional true} :string] [:prepl-host {:optional true} :string]
[:prepl-port {:optional true} :int] [:prepl-port {:optional true} ::sm/int]
[:media-directory {:optional true} :string] ;; REVIEW [:media-directory {:optional true} :string] ;; REVIEW
[:media-uri {:optional true} :string] [:media-uri {:optional true} :string]
@ -217,14 +217,14 @@
[:storage-assets-s3-bucket {:optional true} :string] [:storage-assets-s3-bucket {:optional true} :string]
[:storage-assets-s3-region {:optional true} :keyword] [:storage-assets-s3-region {:optional true} :keyword]
[:storage-assets-s3-endpoint {:optional true} :string] [:storage-assets-s3-endpoint {:optional true} :string]
[:storage-assets-s3-io-threads {:optional true} :int] [:storage-assets-s3-io-threads {:optional true} ::sm/int]
[:objects-storage-backend {:optional true} :keyword] [:objects-storage-backend {:optional true} :keyword]
[:objects-storage-fs-directory {:optional true} :string] [:objects-storage-fs-directory {:optional true} :string]
[:objects-storage-s3-bucket {:optional true} :string] [:objects-storage-s3-bucket {:optional true} :string]
[:objects-storage-s3-region {:optional true} :keyword] [:objects-storage-s3-region {:optional true} :keyword]
[:objects-storage-s3-endpoint {:optional true} :string] [:objects-storage-s3-endpoint {:optional true} :string]
[:objects-storage-s3-io-threads {:optional true} :int]])) [:objects-storage-s3-io-threads {:optional true} ::sm/int]]))
(def default-flags (def default-flags
[:enable-backend-api-doc [:enable-backend-api-doc
@ -253,7 +253,7 @@
env))) env)))
(def decode-config (def decode-config
(sm/decoder schema:config sm/default-transformer)) (sm/decoder schema:config sm/string-transformer))
(def validate-config (def validate-config
(sm/validator schema:config)) (sm/validator schema:config))

View file

@ -157,10 +157,10 @@
[:map [:map
[::username {:optional true} :string] [::username {:optional true} :string]
[::password {:optional true} :string] [::password {:optional true} :string]
[::tls {:optional true} :boolean] [::tls {:optional true} ::sm/boolean]
[::ssl {:optional true} :boolean] [::ssl {:optional true} ::sm/boolean]
[::host {:optional true} :string] [::host {:optional true} :string]
[::port {:optional true} :int] [::port {:optional true} ::sm/int]
[::default-from {:optional true} :string] [::default-from {:optional true} :string]
[::default-reply-to {:optional true} :string]]) [::default-reply-to {:optional true} :string]])

View file

@ -114,7 +114,7 @@
(sm/lazy-validator ::ctc/color)) (sm/lazy-validator ::ctc/color))
(def valid-fill? (def valid-fill?
(sm/lazy-validator ::cts/fill)) (sm/lazy-validator cts/schema:fill))
(def valid-stroke? (def valid-stroke?
(sm/lazy-validator ::cts/stroke)) (sm/lazy-validator ::cts/stroke))
@ -135,10 +135,10 @@
(sm/lazy-validator ::ctc/rgb-color)) (sm/lazy-validator ::ctc/rgb-color))
(def valid-shape-points? (def valid-shape-points?
(sm/lazy-validator ::cts/points)) (sm/lazy-validator cts/schema:points))
(def valid-image-attrs? (def valid-image-attrs?
(sm/lazy-validator ::cts/image-attrs)) (sm/lazy-validator cts/schema:image-attrs))
(def valid-column-grid-params? (def valid-column-grid-params?
(sm/lazy-validator ::ctg/column-params)) (sm/lazy-validator ::ctg/column-params))

View file

@ -7,11 +7,13 @@
(ns app.http.middleware (ns app.http.middleware
(:require (:require
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.json :as json]
[app.common.logging :as l] [app.common.logging :as l]
[app.common.schema :as-alias sm]
[app.common.transit :as t] [app.common.transit :as t]
[app.config :as cf] [app.config :as cf]
[app.http.errors :as errors] [app.http.errors :as errors]
[clojure.data.json :as json] [app.util.pointer-map :as pmap]
[cuerdas.core :as str] [cuerdas.core :as str]
[ring.request :as rreq] [ring.request :as rreq]
[ring.response :as rres] [ring.response :as rres]
@ -39,16 +41,6 @@
(java.io.BufferedReader. (java.io.BufferedReader.
(java.io.InputStreamReader. body)))) (java.io.InputStreamReader. body))))
(defn- read-json-key
[k]
(-> k str/kebab keyword))
(defn- write-json-key
[k]
(if (or (keyword? k) (symbol? k))
(str/camel k)
(str k)))
(defn wrap-parse-request (defn wrap-parse-request
[handler] [handler]
(letfn [(process-request [request] (letfn [(process-request [request]
@ -63,7 +55,7 @@
(str/starts-with? header "application/json") (str/starts-with? header "application/json")
(with-open [reader (get-reader request)] (with-open [reader (get-reader request)]
(let [params (json/read reader :key-fn read-json-key)] (let [params (json/read reader :key-fn json/read-kebab-key)]
(-> request (-> request
(assoc :body-params params) (assoc :body-params params)
(update :params merge params)))) (update :params merge params))))
@ -113,6 +105,12 @@
(def ^:const buffer-size (:xnio/buffer-size yt/defaults)) (def ^:const buffer-size (:xnio/buffer-size yt/defaults))
(defn- write-json-value
[_ val]
(if (pmap/pointer-map? val)
[(pmap/get-id val) (meta val)]
val))
(defn wrap-format-response (defn wrap-format-response
[handler] [handler]
(letfn [(transit-streamable-body [data opts] (letfn [(transit-streamable-body [data opts]
@ -134,10 +132,11 @@
(reify rres/StreamableResponseBody (reify rres/StreamableResponseBody
(-write-body-to-stream [_ _ output-stream] (-write-body-to-stream [_ _ output-stream]
(try (try
(let [encode (or (-> data meta :encode/json) identity)
data (encode data)]
(with-open [^OutputStream bos (buffered-output-stream output-stream buffer-size)] (with-open [^OutputStream bos (buffered-output-stream output-stream buffer-size)]
(with-open [^java.io.OutputStreamWriter writer (java.io.OutputStreamWriter. bos)] (with-open [^java.io.OutputStreamWriter writer (java.io.OutputStreamWriter. bos)]
(json/write data writer :key-fn write-json-key))) (json/write writer data :key-fn json/write-camel-key :value-fn write-json-value))))
(catch java.io.IOException _) (catch java.io.IOException _)
(catch Throwable cause (catch Throwable cause
(binding [l/*context* {:value data}] (binding [l/*context* {:value data}]

View file

@ -49,7 +49,7 @@
(sm/register! ::upload (sm/register! ::upload
[:map {:title "Upload"} [:map {:title "Upload"}
[:filename :string] [:filename :string]
[:size :int] [:size ::sm/int]
[:path ::fs/path] [:path ::fs/path]
[:mtype {:optional true} :string] [:mtype {:optional true} :string]
[:headers {:optional true} [:headers {:optional true}

View file

@ -178,38 +178,21 @@
(if-let [schema (::sm/params mdata)] (if-let [schema (::sm/params mdata)]
(let [validate (sm/validator schema) (let [validate (sm/validator schema)
explain (sm/explainer schema) explain (sm/explainer schema)
decode (sm/decoder schema)] decode (sm/decoder schema sm/json-transformer)
encode (sm/encoder schema sm/json-transformer)]
(fn [cfg params] (fn [cfg params]
(let [params (decode params)] (let [params (decode params)]
(if (validate params) (if (validate params)
(f cfg params) (let [result (f cfg params)]
(if (instance? clojure.lang.IObj result)
(vary-meta result assoc :encode/json encode)
result))
(let [params (d/without-qualified params)] (let [params (d/without-qualified params)]
(ex/raise :type :validation (ex/raise :type :validation
:code :params-validation :code :params-validation
::sm/explain (explain params))))))) ::sm/explain (explain params)))))))
f)) f))
(defn- wrap-output-validation
[_ f mdata]
(if (contains? cf/flags :rpc-output-validation)
(or (when-let [schema (::sm/result mdata)]
(let [schema (if (sm/lazy-schema? schema)
schema
(sm/define schema))
validate (sm/validator schema)
explain (sm/explainer schema)]
(fn [cfg params]
(let [response (f cfg params)]
(when (map? response)
(when-not (validate response)
(ex/raise :type :validation
:code :data-validation
::sm/explain (explain response))))
response))))
f)
f))
(defn- wrap-all (defn- wrap-all
[cfg f mdata] [cfg f mdata]
(as-> f $ (as-> f $
@ -220,7 +203,6 @@
(rlimit/wrap cfg $ mdata) (rlimit/wrap cfg $ mdata)
(wrap-audit cfg $ mdata) (wrap-audit cfg $ mdata)
(wrap-spec-conform cfg $ mdata) (wrap-spec-conform cfg $ mdata)
(wrap-output-validation cfg $ mdata)
(wrap-params-validation cfg $ mdata) (wrap-params-validation cfg $ mdata)
(wrap-authentication cfg $ mdata))) (wrap-authentication cfg $ mdata)))

View file

@ -178,12 +178,12 @@
[:map {:title "File"} [:map {:title "File"}
[:id ::sm/uuid] [:id ::sm/uuid]
[:features ::cfeat/features] [:features ::cfeat/features]
[:has-media-trimmed :boolean] [:has-media-trimmed ::sm/boolean]
[:comment-thread-seqn {:min 0} :int] [:comment-thread-seqn [::sm/int {:min 0}]]
[:name [:string {:max 250}]] [:name [:string {:max 250}]]
[:revn {:min 0} :int] [:revn [::sm/int {:min 0}]]
[:modified-at ::dt/instant] [:modified-at ::dt/instant]
[:is-shared :boolean] [:is-shared ::sm/boolean]
[:project-id ::sm/uuid] [:project-id ::sm/uuid]
[:created-at ::dt/instant] [:created-at ::dt/instant]
[:data {:optional true} :any]])) [:data {:optional true} :any]]))
@ -408,7 +408,7 @@
"Checks if the file has libraries. Returns a boolean" "Checks if the file has libraries. Returns a boolean"
{::doc/added "1.15.1" {::doc/added "1.15.1"
::sm/params schema:has-file-libraries ::sm/params schema:has-file-libraries
::sm/result :boolean} ::sm/result ::sm/boolean}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id]}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id]}]
(dm/with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(check-read-permissions! pool profile-id file-id) (check-read-permissions! pool profile-id file-id)
@ -917,7 +917,7 @@
(sm/define (sm/define
[:map {:title "set-file-shared"} [:map {:title "set-file-shared"}
[:id ::sm/uuid] [:id ::sm/uuid]
[:is-shared :boolean]])) [:is-shared ::sm/boolean]]))
(sv/defmethod ::set-file-shared (sv/defmethod ::set-file-shared
{::doc/added "1.17" {::doc/added "1.17"

View file

@ -91,7 +91,7 @@
[:name [:string {:max 250}]] [:name [:string {:max 250}]]
[:project-id ::sm/uuid] [:project-id ::sm/uuid]
[:id {:optional true} ::sm/uuid] [:id {:optional true} ::sm/uuid]
[:is-shared {:optional true} :boolean] [:is-shared {:optional true} ::sm/boolean]
[:features {:optional true} ::cfeat/features]]) [:features {:optional true} ::cfeat/features]])
(sv/defmethod ::create-file (sv/defmethod ::create-file

View file

@ -38,9 +38,9 @@
[:name [:string {:max 250}]] [:name [:string {:max 250}]]
[:project-id ::sm/uuid] [:project-id ::sm/uuid]
[:id {:optional true} ::sm/uuid] [:id {:optional true} ::sm/uuid]
[:is-shared :boolean] [:is-shared ::sm/boolean]
[:features ::cfeat/features] [:features ::cfeat/features]
[:create-page :boolean]]) [:create-page ::sm/boolean]])
(sv/defmethod ::create-temp-file (sv/defmethod ::create-temp-file
{::doc/added "1.17" {::doc/added "1.17"
@ -83,7 +83,7 @@
(def ^:private schema:update-temp-file (def ^:private schema:update-temp-file
[:map {:title "update-temp-file"} [:map {:title "update-temp-file"}
[:changes [:vector ::cpc/change]] [:changes [:vector ::cpc/change]]
[:revn {:min 0} :int] [:revn [::sm/int {:min 0}]]
[:session-id ::sm/uuid] [:session-id ::sm/uuid]
[:id ::sm/uuid]]) [:id ::sm/uuid]])

View file

@ -189,7 +189,7 @@
(sm/define (sm/define
[:map {:title "PartialFile"} [:map {:title "PartialFile"}
[:id ::sm/uuid] [:id ::sm/uuid]
[:revn {:min 0} :int] [:revn {:min 0} ::sm/int]
[:page :any]])) [:page :any]]))
(sv/defmethod ::get-file-data-for-thumbnail (sv/defmethod ::get-file-data-for-thumbnail
@ -385,7 +385,7 @@
schema:create-file-thumbnail schema:create-file-thumbnail
[:map {:title "create-file-thumbnail"} [:map {:title "create-file-thumbnail"}
[:file-id ::sm/uuid] [:file-id ::sm/uuid]
[:revn :int] [:revn ::sm/int]
[:media ::media/upload]]) [:media ::media/upload]])
(sv/defmethod ::create-file-thumbnail (sv/defmethod ::create-file-thumbnail

View file

@ -44,7 +44,7 @@
[:map {:title "update-file"} [:map {:title "update-file"}
[:id ::sm/uuid] [:id ::sm/uuid]
[:session-id ::sm/uuid] [:session-id ::sm/uuid]
[:revn {:min 0} :int] [:revn {:min 0} ::sm/int]
[:features {:optional true} ::cfeat/features] [:features {:optional true} ::cfeat/features]
[:changes {:optional true} [:vector ::cpc/change]] [:changes {:optional true} [:vector ::cpc/change]]
[:changes-with-metadata {:optional true} [:changes-with-metadata {:optional true}
@ -52,7 +52,7 @@
[:changes [:vector ::cpc/change]] [:changes [:vector ::cpc/change]]
[:hint-origin {:optional true} :keyword] [:hint-origin {:optional true} :keyword]
[:hint-events {:optional true} [:vector [:string {:max 250}]]]]]] [:hint-events {:optional true} [:vector [:string {:max 250}]]]]]]
[:skip-validate {:optional true} :boolean]]) [:skip-validate {:optional true} ::sm/boolean]])
(def ^:private (def ^:private
schema:update-file-result schema:update-file-result
@ -61,7 +61,7 @@
[:changes [:vector ::cpc/change]] [:changes [:vector ::cpc/change]]
[:file-id ::sm/uuid] [:file-id ::sm/uuid]
[:id ::sm/uuid] [:id ::sm/uuid]
[:revn {:min 0} :int] [:revn {:min 0} ::sm/int]
[:session-id ::sm/uuid]]]) [:session-id ::sm/uuid]]])
;; --- HELPERS ;; --- HELPERS

View file

@ -382,10 +382,9 @@
(def ^:private (def ^:private
schema:move-project schema:move-project
(sm/define
[:map {:title "move-project"} [:map {:title "move-project"}
[:team-id ::sm/uuid] [:team-id ::sm/uuid]
[:project-id ::sm/uuid]])) [:project-id ::sm/uuid]])
(sv/defmethod ::move-project (sv/defmethod ::move-project
"Move projects between teams" "Move projects between teams"
@ -425,10 +424,9 @@
(def ^:private (def ^:private
schema:clone-template schema:clone-template
(sm/define
[:map {:title "clone-template"} [:map {:title "clone-template"}
[:project-id ::sm/uuid] [:project-id ::sm/uuid]
[:template-id ::sm/word-string]])) [:template-id ::sm/word-string]])
(sv/defmethod ::clone-template (sv/defmethod ::clone-template
"Clone into the specified project the template by its id." "Clone into the specified project the template by its id."

View file

@ -46,7 +46,7 @@
[:map {:title "upload-file-media-object"} [:map {:title "upload-file-media-object"}
[:id {:optional true} ::sm/uuid] [:id {:optional true} ::sm/uuid]
[:file-id ::sm/uuid] [:file-id ::sm/uuid]
[:is-local :boolean] [:is-local ::sm/boolean]
[:name [:string {:max 250}]] [:name [:string {:max 250}]]
[:content ::media/upload]]) [:content ::media/upload]])
@ -172,7 +172,7 @@
(def ^:private schema:create-file-media-object-from-url (def ^:private schema:create-file-media-object-from-url
[:map {:title "create-file-media-object-from-url"} [:map {:title "create-file-media-object-from-url"}
[:file-id ::sm/uuid] [:file-id ::sm/uuid]
[:is-local :boolean] [:is-local ::sm/boolean]
[:url ::sm/uri] [:url ::sm/uri]
[:id {:optional true} ::sm/uuid] [:id {:optional true} ::sm/uuid]
[:name {:optional true} [:string {:max 250}]]]) [:name {:optional true} [:string {:max 250}]]])
@ -253,7 +253,7 @@
(def ^:private schema:clone-file-media-object (def ^:private schema:clone-file-media-object
[:map {:title "clone-file-media-object"} [:map {:title "clone-file-media-object"}
[:file-id ::sm/uuid] [:file-id ::sm/uuid]
[:is-local :boolean] [:is-local ::sm/boolean]
[:id ::sm/uuid]]) [:id ::sm/uuid]])
(sv/defmethod ::clone-file-media-object (sv/defmethod ::clone-file-media-object

View file

@ -60,10 +60,10 @@
[:id ::sm/uuid] [:id ::sm/uuid]
[:fullname [::sm/word-string {:max 250}]] [:fullname [::sm/word-string {:max 250}]]
[:email ::sm/email] [:email ::sm/email]
[:is-active {:optional true} :boolean] [:is-active {:optional true} ::sm/boolean]
[:is-blocked {:optional true} :boolean] [:is-blocked {:optional true} ::sm/boolean]
[:is-demo {:optional true} :boolean] [:is-demo {:optional true} ::sm/boolean]
[:is-muted {:optional true} :boolean] [:is-muted {:optional true} ::sm/boolean]
[:created-at {:optional true} ::sm/inst] [:created-at {:optional true} ::sm/inst]
[:modified-at {:optional true} ::sm/inst] [:modified-at {:optional true} ::sm/inst]
[:default-project-id {:optional true} ::sm/uuid] [:default-project-id {:optional true} ::sm/uuid]

View file

@ -208,7 +208,7 @@
(def ^:private schema:update-project-pin (def ^:private schema:update-project-pin
[:map {:title "update-project-pin"} [:map {:title "update-project-pin"}
[:team-id ::sm/uuid] [:team-id ::sm/uuid]
[:is-pinned :boolean] [:is-pinned ::sm/boolean]
[:id ::sm/uuid]]) [:id ::sm/uuid]])
(sv/defmethod ::update-project-pin (sv/defmethod ::update-project-pin

View file

@ -906,7 +906,7 @@
[:map {:title "create-team-invitations"} [:map {:title "create-team-invitations"}
[:team-id ::sm/uuid] [:team-id ::sm/uuid]
[:role schema:role] [:role schema:role]
[:emails ::sm/set-of-emails]]) [:emails [::sm/set ::sm/email]]])
(sv/defmethod ::create-team-invitations (sv/defmethod ::create-team-invitations
"A rpc call that allow to send a single or multiple invitations to "A rpc call that allow to send a single or multiple invitations to
@ -972,7 +972,7 @@
[:name [:string {:max 250}]] [:name [:string {:max 250}]]
[:features {:optional true} ::cfeat/features] [:features {:optional true} ::cfeat/features]
[:id {:optional true} ::sm/uuid] [:id {:optional true} ::sm/uuid]
[:emails ::sm/set-of-emails] [:emails [::sm/set ::sm/email]]
[:role schema:role]]) [:role schema:role]])
(sv/defmethod ::create-team-with-invitations (sv/defmethod ::create-team-with-invitations
@ -1175,7 +1175,7 @@
[:map {:title "create-team-access-request"} [:map {:title "create-team-access-request"}
[:file-id {:optional true} ::sm/uuid] [:file-id {:optional true} ::sm/uuid]
[:team-id {:optional true} ::sm/uuid] [:team-id {:optional true} ::sm/uuid]
[:is-viewer {:optional true} :boolean]] [:is-viewer {:optional true} ::sm/boolean]]
[:fn (fn [params] [:fn (fn [params]
(or (contains? params :file-id) (or (contains? params :file-id)

View file

@ -111,7 +111,7 @@
[:id ::sm/uuid] [:id ::sm/uuid]
[:uri ::sm/uri] [:uri ::sm/uri]
[:mtype [::sm/one-of {:format "string"} valid-mtypes]] [:mtype [::sm/one-of {:format "string"} valid-mtypes]]
[:is-active :boolean]]) [:is-active ::sm/boolean]])
(sv/defmethod ::update-webhook (sv/defmethod ::update-webhook
{::doc/added "1.17" {::doc/added "1.17"

View file

@ -26,7 +26,6 @@
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[cuerdas.core :as str] [cuerdas.core :as str]
[integrant.core :as ig] [integrant.core :as ig]
[malli.transform :as mt]
[pretty-spec.core :as ps] [pretty-spec.core :as ps]
[ring.response :as-alias rres])) [ring.response :as-alias rres]))
@ -98,15 +97,18 @@
;; OPENAPI / SWAGGER (v3.1) ;; OPENAPI / SWAGGER (v3.1)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def output-transformer
(mt/transformer
sm/default-transformer
(mt/key-transformer {:encode str/camel
:decode (comp keyword str/kebab)})))
(defn prepare-openapi-context (defn prepare-openapi-context
[methods] [methods]
(letfn [(gen-response-doc [tsx schema] (let [definitions (atom {})
options {:registry sr/default-registry
::oapi/definitions-path "#/components/schemas/"
::oapi/definitions definitions}
output-transformer
(sm/json-transformer)
gen-response-doc
(fn [tsx schema]
(let [schema (sm/schema schema) (let [schema (sm/schema schema)
example (sm/generate schema) example (sm/generate schema)
example (sm/encode schema example output-transformer)] example (sm/encode schema example output-transformer)]
@ -117,7 +119,8 @@
{:schema tsx {:schema tsx
:example example}}}})) :example example}}}}))
(gen-params-doc [tsx schema] gen-params-doc
(fn [tsx schema]
(let [example (sm/generate schema) (let [example (sm/generate schema)
example (sm/encode schema example output-transformer)] example (sm/encode schema example output-transformer)]
{:required true {:required true
@ -126,7 +129,8 @@
{:schema tsx {:schema tsx
:example example}}})) :example example}}}))
(gen-method-doc [options mdata] gen-method-doc
(fn [mdata]
(let [pschema (::sm/params mdata) (let [pschema (::sm/params mdata)
rschema (::sm/result mdata) rschema (::sm/result mdata)
@ -143,22 +147,19 @@
{:name (-> mdata ::sv/name d/name) {:name (-> mdata ::sv/name d/name)
:module (-> (:ns mdata) (str/split ".") last) :module (-> (:ns mdata) (str/split ".") last)
:repr {:post rpost}}))] :repr {:post rpost}}))
(let [definitions (atom {}) paths
options {:registry sr/default-registry (binding [oapi/*definitions* definitions]
::oapi/definitions-path "#/components/schemas/"
::oapi/definitions definitions}
paths (binding [oapi/*definitions* definitions]
(->> methods (->> methods
(map (comp first val)) (map (comp first val))
(filter ::sm/params) (filter ::sm/params)
(map (partial gen-method-doc options)) (map gen-method-doc)
(sort-by (juxt :module :name)) (sort-by (juxt :module :name))
(map (fn [doc] (map (fn [doc]
[(str/ffmt "/command/%" (:name doc)) (:repr doc)])) [(str/ffmt "/command/%" (:name doc)) (:repr doc)]))
(into {})))] (into {})))]
{:openapi "3.0.0" {:openapi "3.0.0"
:info {:version (:main cf/version)} :info {:version (:main cf/version)}
:servers [{:url (str/ffmt "%/api/rpc" (cf/get :public-uri)) :servers [{:url (str/ffmt "%/api/rpc" (cf/get :public-uri))
@ -168,7 +169,7 @@
{:api_key []} {:api_key []}
:paths paths :paths paths
:components {:schemas @definitions}}))) :components {:schemas @definitions}}))
(defn openapi-json-handler (defn openapi-json-handler
[context] [context]

View file

@ -15,11 +15,11 @@
(sm/register! ::permissions (sm/register! ::permissions
[:map {:title "Permissions"} [:map {:title "Permissions"}
[:type {:gen/elements [:membership :share-link]} :keyword] [:type {:gen/elements [:membership :share-link]} :keyword]
[:is-owner :boolean] [:is-owner ::sm/boolean]
[:is-admin :boolean] [:is-admin ::sm/boolean]
[:can-edit :boolean] [:can-edit ::sm/boolean]
[:can-read :boolean] [:can-read ::sm/boolean]
[:is-logged :boolean]]) [:is-logged ::sm/boolean]])
(s/def ::role #{:admin :owner :editor :viewer}) (s/def ::role #{:admin :owner :editor :viewer})

View file

@ -31,7 +31,7 @@
[::team-id {:optional true} ::sm/uuid] [::team-id {:optional true} ::sm/uuid]
[::project-id {:optional true} ::sm/uuid] [::project-id {:optional true} ::sm/uuid]
[::file-id {:optional true} ::sm/uuid] [::file-id {:optional true} ::sm/uuid]
[::incr {:optional true} [:int {:min 0}]] [::incr {:optional true} [::sm/int {:min 0}]]
[::id :keyword] [::id :keyword]
[::profile-id ::sm/uuid]])) [::profile-id ::sm/uuid]]))

View file

@ -13,7 +13,6 @@
[clojure.pprint :as pprint] [clojure.pprint :as pprint]
[datoteka.fs :as fs])) [datoteka.fs :as fs]))
(prefer-method print-method (prefer-method print-method
clojure.lang.IRecord clojure.lang.IRecord
clojure.lang.IDeref) clojure.lang.IDeref)
@ -26,7 +25,6 @@
clojure.lang.IPersistentMap clojure.lang.IPersistentMap
clojure.lang.IDeref) clojure.lang.IDeref)
(sm/register! ::fs/path (sm/register! ::fs/path
{:type ::fs/path {:type ::fs/path
:pred fs/path? :pred fs/path?
@ -36,6 +34,6 @@
:error/message "expected a valid fs path instance" :error/message "expected a valid fs path instance"
:error/code "errors.invalid-path" :error/code "errors.invalid-path"
:gen/gen (sg/generator :string) :gen/gen (sg/generator :string)
:decode/string fs/path
::oapi/type "string" ::oapi/type "string"
::oapi/format "unix-path" ::oapi/format "unix-path"}})
::oapi/decode fs/path}})

View file

@ -374,7 +374,10 @@
:type-properties :type-properties
{:error/message "should be an instant" {:error/message "should be an instant"
:title "instant" :title "instant"
::sm/decode instant :decode/string instant
:encode/string format-instant
:decode/json instant
:encode/json format-instant
:gen/gen (tgen/fmap (fn [i] (in-past i)) tgen/pos-int) :gen/gen (tgen/fmap (fn [i] (in-past i)) tgen/pos-int)
::oapi/type "string" ::oapi/type "string"
::oapi/format "iso"}}) ::oapi/format "iso"}})
@ -386,6 +389,9 @@
{:error/message "should be a duration" {:error/message "should be a duration"
:gen/gen (tgen/fmap duration tgen/pos-int) :gen/gen (tgen/fmap duration tgen/pos-int)
:title "duration" :title "duration"
::sm/decode duration :decode/string duration
:encode/string format-duration
:decode/json duration
:encode/json format-duration
::oapi/type "string" ::oapi/type "string"
::oapi/format "duration"}}) ::oapi/format "duration"}})

View file

@ -25,6 +25,20 @@
(t/use-fixtures :once th/state-init) (t/use-fixtures :once th/state-init)
(t/use-fixtures :each th/database-reset) (t/use-fixtures :each th/database-reset)
(defn- update-file!
[& {:keys [profile-id file-id changes revn] :or {revn 0}}]
(let [params {::th/type :update-file
::rpc/profile-id profile-id
:id file-id
:session-id (uuid/random)
:revn revn
:features cfeat/supported-features
:changes changes}
out (th/command! params)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(:result out)))
(t/deftest files-crud (t/deftest files-crud
(let [prof (th/create-profile* 1 {:is-active true}) (let [prof (th/create-profile* 1 {:is-active true})
team-id (:default-team-id prof) team-id (:default-team-id prof)
@ -569,7 +583,7 @@
(t/is (nil? (:error out))) (t/is (nil? (:error out)))
(:result out))) (:result out)))
(update-file! [& {:keys [profile-id file-id changes revn] :or {revn 0}}] #_(update-file! [& {:keys [profile-id file-id changes revn] :or {revn 0}}]
(let [params {::th/type :update-file (let [params {::th/type :update-file
::rpc/profile-id profile-id ::rpc/profile-id profile-id
:id file-id :id file-id
@ -604,7 +618,6 @@
:frame-id frame-id-2)] :frame-id frame-id-2)]
;; Add a two frames ;; Add a two frames
(update-file! (update-file!
:file-id (:id file) :file-id (:id file)
:profile-id (:id profile) :profile-id (:id profile)
@ -1214,21 +1227,6 @@
(let [rows (th/db-query :file-thumbnail {:file-id (:id file)})] (let [rows (th/db-query :file-thumbnail {:file-id (:id file)})]
(t/is (= 1 (count rows))))))) (t/is (= 1 (count rows)))))))
(defn- update-file!
[& {:keys [profile-id file-id changes revn] :or {revn 0}}]
(let [params {::th/type :update-file
::rpc/profile-id profile-id
:id file-id
:session-id (uuid/random)
:revn revn
:features cfeat/supported-features
:changes changes}
out (th/command! params)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(:result out)))
(t/deftest file-tiered-storage (t/deftest file-tiered-storage
(let [profile (th/create-profile* 1) (let [profile (th/create-profile* 1)
file (th/create-file* 1 {:profile-id (:id profile) file (th/create-file* 1 {:profile-id (:id profile)

View file

@ -260,6 +260,7 @@
(th/reset-mock! mock) (th/reset-mock! mock)
(let [data (assoc data :emails [(:email profile2)]) (let [data (assoc data :emails [(:email profile2)])
out (th/command! data)] out (th/command! data)]
;; (th/print-result! out)
(t/is (th/success? out)) (t/is (th/success? out))
(t/is (= 0 (:call-count (deref mock))))) (t/is (= 0 (:call-count (deref mock)))))

View file

@ -166,7 +166,6 @@
out9 (th/command! params)] out9 (th/command! params)]
(t/is (= 8 (:call-count @http-mock))) (t/is (= 8 (:call-count @http-mock)))
(t/is (nil? (:error out1))) (t/is (nil? (:error out1)))
(t/is (nil? (:error out2))) (t/is (nil? (:error out2)))
(t/is (nil? (:error out3))) (t/is (nil? (:error out3)))

View file

@ -89,7 +89,7 @@
{:title "FileFeatures" {:title "FileFeatures"
::smdj/inline true ::smdj/inline true
:gen/gen (smg/subseq supported-features)} :gen/gen (smg/subseq supported-features)}
::sm/set-of-strings]) [::sm/set :string]])
(defn- flag->feature (defn- flag->feature
"Translate a flag to a feature name" "Translate a flag to a feature name"

View file

@ -67,16 +67,6 @@
([a b c d e f] ([a b c d e f]
(pos->Matrix a b c d e f))) (pos->Matrix a b c d e f)))
(def number-regex
#"[+-]?\d*(\.\d+)?([eE][+-]?\d+)?")
(defn str->matrix
[matrix-str]
(let [params (->> (re-seq number-regex matrix-str)
(filter #(-> % first seq))
(map (comp d/parse-double first)))]
(apply matrix params)))
(def ^:private schema:matrix-attrs (def ^:private schema:matrix-attrs
[:map {:title "MatrixAttrs"} [:map {:title "MatrixAttrs"}
[:a ::sm/safe-double] [:a ::sm/safe-double]
@ -87,41 +77,70 @@
[:f ::sm/safe-double]]) [:f ::sm/safe-double]])
(def valid-matrix? (def valid-matrix?
(sm/lazy-validator (sm/validator
[:and [:fn matrix?] schema:matrix-attrs])) [:and [:fn matrix?] schema:matrix-attrs]))
(sm/register! ::matrix (defn matrix-generator
(letfn [(decode [o] []
(if (map? o) (->> (sg/tuple (sg/small-double)
(map->Matrix o)
(if (string? o)
(str->matrix o)
o)))
(encode [o]
(dm/str (dm/get-prop o :a) ","
(dm/get-prop o :b) ","
(dm/get-prop o :c) ","
(dm/get-prop o :d) ","
(dm/get-prop o :e) ","
(dm/get-prop o :f) ","))]
{:type ::matrix
:pred valid-matrix?
:type-properties
{:title "matrix"
:description "Matrix instance"
:error/message "expected a valid point"
:gen/gen (->> (sg/tuple (sg/small-double)
(sg/small-double) (sg/small-double)
(sg/small-double) (sg/small-double)
(sg/small-double) (sg/small-double)
(sg/small-double) (sg/small-double)
(sg/small-double)) (sg/small-double))
(sg/fmap #(apply pos->Matrix %))) (sg/fmap #(apply pos->Matrix %))))
(def ^:private number-regex
#"[+-]?\d*(\.\d+)?([eE][+-]?\d+)?")
(defn str->matrix
[matrix-str]
(let [params (->> (re-seq number-regex matrix-str)
(filter #(-> % first seq))
(map (comp d/parse-double first)))]
(apply matrix params)))
(defn- matrix->str
[o]
(if (matrix? o)
(dm/str (dm/get-prop o :a) ","
(dm/get-prop o :b) ","
(dm/get-prop o :c) ","
(dm/get-prop o :d) ","
(dm/get-prop o :e) ","
(dm/get-prop o :f) ",")
o))
(defn- matrix->json
[o]
(if (matrix? o)
(into {} o)
o))
(defn- decode-matrix
[o]
(if (map? o)
(map->Matrix o)
(if (string? o)
(str->matrix o)
o)))
(def schema:matrix
{:type :map
:pred valid-matrix?
:type-properties
{:title "matrix"
:description "Matrix instance"
:error/message "expected a valid matrix instance"
:gen/gen (matrix-generator)
:decode/json decode-matrix
:decode/string decode-matrix
:encode/json matrix->json
:encode/string matrix->str
::oapi/type "string" ::oapi/type "string"
::oapi/format "matrix" ::oapi/format "matrix"}})
::oapi/decode decode
::oapi/encode encode}})) (sm/register! ::matrix schema:matrix)
;; FIXME: deprecated ;; FIXME: deprecated
(s/def ::a ::us/safe-float) (s/def ::a ::us/safe-float)

View file

@ -51,18 +51,17 @@
(s/def ::point (s/def ::point
(s/and ::point-attrs point?)) (s/and ::point-attrs point?))
(def ^:private schema:point-attrs (def ^:private schema:point-attrs
[:map {:title "PointAttrs"} [:map {:title "PointAttrs"}
[:x ::sm/safe-number] [:x ::sm/safe-number]
[:y ::sm/safe-number]]) [:y ::sm/safe-number]])
(def valid-point? (def valid-point?
(sm/lazy-validator (sm/validator
[:and [:fn point?] schema:point-attrs])) [:and [:fn point?] schema:point-attrs]))
(sm/register! ::point (defn decode-point
(letfn [(decode [p] [p]
(if (map? p) (if (map? p)
(map->Point p) (map->Point p)
(if (string? p) (if (string? p)
@ -70,11 +69,22 @@
(pos->Point x y)) (pos->Point x y))
p))) p)))
(encode [p] (defn point->str
[p]
(if (point? p)
(dm/str (dm/get-prop p :x) "," (dm/str (dm/get-prop p :x) ","
(dm/get-prop p :y)))] (dm/get-prop p :y))
p))
{:type ::point (defn point->json
[p]
(if (point? p)
(into {} p)
p))
;; FIXME: make like matrix
(def schema:point
{:type :map
:pred valid-point? :pred valid-point?
:type-properties :type-properties
{:title "point" {:title "point"
@ -84,8 +94,12 @@
(sg/fmap #(apply pos->Point %))) (sg/fmap #(apply pos->Point %)))
::oapi/type "string" ::oapi/type "string"
::oapi/format "point" ::oapi/format "point"
::oapi/decode decode :decode/json decode-point
::oapi/encode encode}})) :decode/string decode-point
:encode/json point->json
:encode/string point->str}})
(sm/register! ::point schema:point)
(defn point-like? (defn point-like?
[{:keys [x y] :as v}] [{:keys [x y] :as v}]

View file

@ -80,19 +80,38 @@
[:x2 ::sm/safe-number] [:x2 ::sm/safe-number]
[:y2 ::sm/safe-number]]) [:y2 ::sm/safe-number]])
(sm/register! ::rect (defn- rect-generator
[:and []
{:gen/gen (->> (sg/tuple (sg/small-double) (->> (sg/tuple (sg/small-double)
(sg/small-double) (sg/small-double)
(sg/small-double) (sg/small-double)
(sg/small-double)) (sg/small-double))
(sg/fmap #(apply make-rect %)))} (sg/fmap #(apply make-rect %))))
[:fn rect?]
schema:rect-attrs]) (defn- decode-rect
[o]
(if (map? o)
(map->Rect o)
o))
(defn- rect->json
[o]
(if (rect? o)
(into {} o)
o))
(def schema:rect
[:and {:error/message "errors.invalid-rect"
:gen/gen (rect-generator)
:decode/json {:leave decode-rect}
:encode/json rect->json}
schema:rect-attrs
[:fn rect?]])
(def valid-rect? (def valid-rect?
(sm/lazy-validator (sm/validator schema:rect))
[:and [:fn rect?] schema:rect-attrs]))
(sm/register! ::rect schema:rect)
(def empty-rect (def empty-rect
(make-rect 0 0 0.01 0.01)) (make-rect 0 0 0.01 0.01))

View file

@ -7,8 +7,8 @@
(ns app.common.json (ns app.common.json
(:refer-clojure :exclude [read]) (:refer-clojure :exclude [read])
(:require (:require
[cuerdas.core :as str] #?(:clj [clojure.data.json :as j])
#?(:clj [clojure.data.json :as j]))) [cuerdas.core :as str]))
#?(:clj #?(:clj

View file

@ -5,7 +5,7 @@
;; Copyright (c) KALEIDOS INC ;; Copyright (c) KALEIDOS INC
(ns app.common.schema (ns app.common.schema
(:refer-clojure :exclude [deref merge parse-uuid]) (:refer-clojure :exclude [deref merge parse-uuid parse-long parse-double parse-boolean])
#?(:cljs (:require-macros [app.common.schema :refer [ignoring]])) #?(:cljs (:require-macros [app.common.schema :refer [ignoring]]))
(:require (:require
[app.common.data :as d] [app.common.data :as d]
@ -113,34 +113,49 @@
[schema] [schema]
(mu/optional-keys schema default-options)) (mu/optional-keys schema default-options))
(def default-transformer (defn transformer
(let [default-decoder [& transformers]
{:compile (fn [s _registry] (apply mt/transformer transformers))
(let [props (m/type-properties s)]
(or (::oapi/decode props)
(::decode props))))}
default-encoder ;; (defn key-transformer
{:compile (fn [s _] ;; [& {:as opts}]
(let [props (m/type-properties s)] ;; (mt/key-transformer opts))
(or (::oapi/encode props)
(::encode props))))}
coders {:vector mt/-sequential-or-set->vector ;; (defn- transform-map-keys
:sequential mt/-sequential-or-set->seq ;; [f o]
:set mt/-sequential->set ;; (cond
:tuple mt/-sequential->vector}] ;; (record? o)
;; (reduce-kv (fn [res k v]
;; (let [k' (f k)]
;; (if (= k k')
;; res
;; (-> res
;; (assoc k' v)
;; (dissoc k)))))
;; o
;; o)
;; (map? o)
;; (persistent!
;; (reduce-kv (fn [res k v]
;; (assoc! res (f k) v))
;; (transient {})
;; o))
;; :else
;; o))
(defn json-transformer
[]
(mt/transformer (mt/transformer
{:name :penpot (mt/json-transformer)
:default-decoder default-decoder (mt/collection-transformer)))
:default-encoder default-encoder}
{:name :string (defn string-transformer
:decoders (mt/-string-decoders) []
:encoders (mt/-string-encoders)} (mt/transformer
{:name :collections (mt/string-transformer)
:decoders coders (mt/collection-transformer)))
:encoders coders})))
(defn encode (defn encode
([s val transformer] ([s val transformer]
@ -149,8 +164,6 @@
(m/encode s val options transformer))) (m/encode s val options transformer)))
(defn decode (defn decode
([s val]
(m/decode s val default-options default-transformer))
([s val transformer] ([s val transformer]
(m/decode s val default-options transformer)) (m/decode s val default-options transformer))
([s val options transformer] ([s val options transformer]
@ -170,9 +183,8 @@
(defn encoder (defn encoder
([s] ([s]
(if (lazy-schema? s) (assert (lazy-schema? s) "expected lazy schema")
(-get-decoder s) (-get-decoder s))
(encoder s default-options default-transformer)))
([s transformer] ([s transformer]
(m/encoder s default-options transformer)) (m/encoder s default-options transformer))
([s options transformer] ([s options transformer]
@ -180,9 +192,8 @@
(defn decoder (defn decoder
([s] ([s]
(if (lazy-schema? s) (assert (lazy-schema? s) "expected lazy schema")
(-get-decoder s) (-get-decoder s))
(decoder s default-options default-transformer)))
([s transformer] ([s transformer]
(m/decoder s default-options transformer)) (m/decoder s default-options transformer))
([s options transformer] ([s options transformer]
@ -199,10 +210,9 @@
(fn [v] (@vfn v)))) (fn [v] (@vfn v))))
(defn lazy-decoder (defn lazy-decoder
([s] (lazy-decoder s default-transformer)) [s transformer]
([s transformer]
(let [vfn (delay (decoder (if (delay? s) (deref s) s) transformer))] (let [vfn (delay (decoder (if (delay? s) (deref s) s) transformer))]
(fn [v] (@vfn v))))) (fn [v] (@vfn v))))
(defn humanize-explain (defn humanize-explain
"Returns a string representation of the explain data structure" "Returns a string representation of the explain data structure"
@ -244,27 +254,6 @@
`(try ~expr (catch :default e# nil)) `(try ~expr (catch :default e# nil))
`(try ~expr (catch Throwable e# nil)))) `(try ~expr (catch Throwable e# nil))))
(defn simple-schema
[& {:keys [pred] :as options}]
(cond-> options
(contains? options :type-properties)
(update :type-properties (fn [props]
(cond-> props
(contains? props :decode/string)
(update :decode/string (fn [decode-fn]
(fn [s]
(if (pred s)
s
(or (ignoring (decode-fn s)) s)))))
(contains? props ::decode)
(update ::decode (fn [decode-fn]
(fn [s]
(if (pred s)
s
(or (ignoring (decode-fn s)) s))))))))
:always
(m/-simple-schema)))
(defn lookup (defn lookup
"Lookups schema from registry." "Lookups schema from registry."
([s] (lookup sr/default-registry s)) ([s] (lookup sr/default-registry s))
@ -308,7 +297,6 @@
::explain explain})))) ::explain explain}))))
true))) true)))
(defn fast-validate! (defn fast-validate!
"A fast path for validation process, assumes the ILazySchema protocol "A fast path for validation process, assumes the ILazySchema protocol
implemented on the provided `s` schema. Sould not be used directly." implemented on the provided `s` schema. Sould not be used directly."
@ -353,19 +341,18 @@
params)) params))
(defn register! [type s] (defn register! [type s]
(let [s (if (map? s) (simple-schema s) s)] (let [s (if (map? s) (m/-simple-schema s) s)]
(swap! sr/registry assoc type s) (swap! sr/registry assoc type s)
nil)) nil))
(defn define (defn define
"Create ans instance of ILazySchema" "Create ans instance of ILazySchema"
[s & {:keys [transformer] :as options}] [s & {:keys [transformer] :or {transformer json-transformer} :as options}]
(let [schema (delay (schema s)) (let [schema (delay (schema s))
validator (delay (m/validator @schema)) validator (delay (m/validator @schema))
explainer (delay (m/explainer @schema)) explainer (delay (m/explainer @schema))
options (c/merge default-options (dissoc options :transformer)) options (c/merge default-options (dissoc options :transformer))
transformer (or transformer default-transformer)
decoder (delay (m/decoder @schema options transformer)) decoder (delay (m/decoder @schema options transformer))
encoder (delay (m/encoder @schema options transformer))] encoder (delay (m/encoder @schema options transformer))]
@ -449,9 +436,12 @@
:description "UUID formatted string" :description "UUID formatted string"
:error/message "should be an uuid" :error/message "should be an uuid"
:gen/gen (sg/uuid) :gen/gen (sg/uuid)
:decode/string parse-uuid
:decode/json parse-uuid
:encode/string str
:encode/json str
::oapi/type "string" ::oapi/type "string"
::oapi/format "uuid" ::oapi/format "uuid"}})
::oapi/decode parse-uuid}})
(def email-re #"[a-zA-Z0-9_.+-\\\\]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+") (def email-re #"[a-zA-Z0-9_.+-\\\\]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+")
@ -481,11 +471,10 @@
:description "string with valid email address" :description "string with valid email address"
:error/code "errors.invalid-email" :error/code "errors.invalid-email"
:gen/gen (sg/email) :gen/gen (sg/email)
:decode/string (fn [v] (or (parse-email v) v))
:decode/json (fn [v] (or (parse-email v) v))
::oapi/type "string" ::oapi/type "string"
::oapi/format "email" ::oapi/format "email"}})
::oapi/decode
(fn [v]
(or (parse-email v) v))}})
(def non-empty-strings-xf (def non-empty-strings-xf
(comp (comp
@ -505,12 +494,18 @@
(comp non-empty-strings-xf (map coerce)) (comp non-empty-strings-xf (map coerce))
non-empty-strings-xf) non-empty-strings-xf)
kind (or (last children) kind) kind (or (last children) kind)
pred (cond
pred
(cond
(fn? kind) kind (fn? kind) kind
(nil? kind) any? (nil? kind) any?
:else (validator kind)) :else (validator kind))
pred (cond encode-child
(encoder kind string-transformer)
pred
(cond
(and max min) (and max min)
(fn [value] (fn [value]
(let [size (count value)] (let [size (count value)]
@ -534,7 +529,24 @@
:else :else
(fn [value] (fn [value]
(every? pred value)))] (every? pred value)))
decode
(fn [v]
(let [v (if (string? v) (str/split v #"[\s,]+") v)]
(into #{} xform v)))
encode-json
(fn [o]
(if (set? o)
(vec o)
o))
encode-string
(fn [o]
(if (set? o)
(str/join ", " (map encode-child o))
o))]
{:pred pred {:pred pred
:type-properties :type-properties
@ -542,13 +554,14 @@
:description "Set of Strings" :description "Set of Strings"
:error/message "should be a set of strings" :error/message "should be a set of strings"
:gen/gen (-> kind sg/generator sg/set) :gen/gen (-> kind sg/generator sg/set)
:decode/string decode
:decode/json decode
:encode/string encode-string
:encode/json encode-json
::oapi/type "array" ::oapi/type "array"
::oapi/format "set" ::oapi/format "set"
::oapi/items {:type "string"} ::oapi/items {:type "string"}
::oapi/unique-items true ::oapi/unique-items true}}))})
::oapi/decode (fn [v]
(let [v (if (string? v) (str/split v #"[\s,]+") v)]
(into #{} xform v)))}}))})
(register! ::vec (register! ::vec
@ -562,12 +575,17 @@
non-empty-strings-xf) non-empty-strings-xf)
kind (or (last children) kind) kind (or (last children) kind)
pred (cond pred
(cond
(fn? kind) kind (fn? kind) kind
(nil? kind) any? (nil? kind) any?
:else (validator kind)) :else (validator kind))
pred (cond encode-child
(encoder kind string-transformer)
pred
(cond
(and max min) (and max min)
(fn [value] (fn [value]
(let [size (count value)] (let [size (count value)]
@ -591,7 +609,18 @@
:else :else
(fn [value] (fn [value]
(every? pred value)))] (every? pred value)))
decode
(fn [v]
(let [v (if (string? v) (str/split v #"[\s,]+") v)]
(into [] xform v)))
encode-string
(fn [o]
(if (vector? o)
(str/join ", " (map encode-child o))
o))]
{:pred pred {:pred pred
:type-properties :type-properties
@ -599,14 +628,13 @@
:description "Set of Strings" :description "Set of Strings"
:error/message "should be a set of strings" :error/message "should be a set of strings"
:gen/gen (-> kind sg/generator sg/set) :gen/gen (-> kind sg/generator sg/set)
:decode/string decode
:decode/json decode
:encode/string encode-string
::oapi/type "array" ::oapi/type "array"
::oapi/format "set" ::oapi/format "set"
::oapi/items {:type "string"} ::oapi/items {:type "string"}
::oapi/unique-items true ::oapi/unique-items true}}))})
::oapi/decode (fn [v]
(let [v (if (string? v) (str/split v #"[\s,]+") v)]
(into [] xform v)))}}))})
(register! ::set-of-strings (register! ::set-of-strings
{:type ::set-of-strings {:type ::set-of-strings
@ -616,13 +644,13 @@
:description "Set of Strings" :description "Set of Strings"
:error/message "should be a set of strings" :error/message "should be a set of strings"
:gen/gen (-> :string sg/generator sg/set) :gen/gen (-> :string sg/generator sg/set)
:decode/string (fn [v]
(let [v (if (string? v) (str/split v #"[\s,]+") v)]
(into #{} non-empty-strings-xf v)))
::oapi/type "array" ::oapi/type "array"
::oapi/format "set" ::oapi/format "set"
::oapi/items {:type "string"} ::oapi/items {:type "string"}
::oapi/unique-items true ::oapi/unique-items true}})
::oapi/decode (fn [v]
(let [v (if (string? v) (str/split v #"[\s,]+") v)]
(into #{} non-empty-strings-xf v)))}})
(register! ::set-of-keywords (register! ::set-of-keywords
{:type ::set-of-keywords {:type ::set-of-keywords
@ -632,29 +660,13 @@
:description "Set of Strings" :description "Set of Strings"
:error/message "should be a set of strings" :error/message "should be a set of strings"
:gen/gen (-> :keyword sg/generator sg/set) :gen/gen (-> :keyword sg/generator sg/set)
:decode/string (fn [v]
(let [v (if (string? v) (str/split v #"[\s,]+") v)]
(into #{} (comp non-empty-strings-xf (map keyword)) v)))
::oapi/type "array" ::oapi/type "array"
::oapi/format "set" ::oapi/format "set"
::oapi/items {:type "string" :format "keyword"} ::oapi/items {:type "string" :format "keyword"}
::oapi/unique-items true ::oapi/unique-items true}})
::oapi/decode (fn [v]
(let [v (if (string? v) (str/split v #"[\s,]+") v)]
(into #{} (comp non-empty-strings-xf (map keyword)) v)))}})
(register! ::set-of-emails
{:type ::set-of-emails
:pred #(and (set? %) (every? string? %))
:type-properties
{:title "set[email]"
:description "Set of Emails"
:error/message "should be a set of emails"
:gen/gen (-> ::email sg/generator sg/set)
::oapi/type "array"
::oapi/format "set"
::oapi/items {:type "string" :format "email"}
::oapi/unique-items true
::decode (fn [v]
(let [v (if (string? v) (str/split v #"[\s,]+") v)]
(into #{} (keep parse-email) v)))}})
(register! ::set-of-uuid (register! ::set-of-uuid
{:type ::set-of-uuid {:type ::set-of-uuid
@ -664,13 +676,13 @@
:description "Set of UUID" :description "Set of UUID"
:error/message "should be a set of UUID instances" :error/message "should be a set of UUID instances"
:gen/gen (-> ::uuid sg/generator sg/set) :gen/gen (-> ::uuid sg/generator sg/set)
:decode/string (fn [v]
(let [v (if (string? v) (str/split v #"[\s,]+") v)]
(into #{} (keep parse-uuid) v)))
::oapi/type "array" ::oapi/type "array"
::oapi/format "set" ::oapi/format "set"
::oapi/items {:type "string" :format "uuid"} ::oapi/items {:type "string" :format "uuid"}
::oapi/unique-items true ::oapi/unique-items true}})
::oapi/decode (fn [v]
(let [v (if (string? v) (str/split v #"[\s,]+") v)]
(into #{} (keep parse-uuid) v)))}})
(register! ::coll-of-uuid (register! ::coll-of-uuid
{:type ::set-of-uuid {:type ::set-of-uuid
@ -680,13 +692,13 @@
:description "Coll of UUID" :description "Coll of UUID"
:error/message "should be a coll of UUID instances" :error/message "should be a coll of UUID instances"
:gen/gen (-> ::uuid sg/generator sg/set) :gen/gen (-> ::uuid sg/generator sg/set)
:decode/string (fn [v]
(let [v (if (string? v) (str/split v #"[\s,]+") v)]
(into [] (keep parse-uuid) v)))
::oapi/type "array" ::oapi/type "array"
::oapi/format "array" ::oapi/format "array"
::oapi/items {:type "string" :format "uuid"} ::oapi/items {:type "string" :format "uuid"}
::oapi/unique-items false ::oapi/unique-items false}})
::oapi/decode (fn [v]
(let [v (if (string? v) (str/split v #"[\s,]+") v)]
(into [] (keep parse-uuid) v)))}})
(register! ::one-of (register! ::one-of
{:type ::one-of {:type ::one-of
@ -694,70 +706,168 @@
:max 1 :max 1
:compile (fn [props children _] :compile (fn [props children _]
(let [options (into #{} (last children)) (let [options (into #{} (last children))
format (:format props "keyword")] format (:format props "keyword")
decode (if (= format "keyword")
keyword
identity)]
{:pred #(contains? options %) {:pred #(contains? options %)
:type-properties :type-properties
{:title "one-of" {:title "one-of"
:description "One of the Set" :description "One of the Set"
:gen/gen (sg/elements options) :gen/gen (sg/elements options)
:decode/string decode
:decode/json decode
::oapi/type "string" ::oapi/type "string"
::oapi/format (:format props "keyword") ::oapi/format (:format props "keyword")}}))})
::oapi/decode (if (= format "keyword")
keyword
identity)}}))})
;; Integer/MAX_VALUE ;; Integer/MAX_VALUE
(def max-safe-int 2147483647) (def max-safe-int 2147483647)
;; Integer/MIN_VALUE ;; Integer/MIN_VALUE
(def min-safe-int -2147483648) (def min-safe-int -2147483648)
(register! ::safe-int (defn parse-long
{:type ::safe-int [v]
:pred #(and (int? %) (>= max-safe-int %) (>= % min-safe-int)) (or (ignoring
(if (string? v)
(c/parse-long v)
v))
v))
(def type:int
{:type :int
:min 0
:max 0
:compile
(fn [{:keys [max min] :as props} _ _]
(let [pred int?
pred (if (some? min)
(fn [v]
(and (>= v min)
(pred v)))
pred)
pred (if (some? max)
(fn [v]
(and (>= max v)
(pred v)))
pred)]
{:pred pred
:type-properties :type-properties
{:title "int" {:title "int"
:description "Safe Integer" :description "int"
:error/message "expected to be int in safe range" :error/message "expected to be int/long"
:gen/gen (sg/small-int) :error/code "errors.invalid-integer"
:gen/gen (sg/small-int :max max :min min)
:decode/string parse-long
:decode/json parse-long
::oapi/type "integer" ::oapi/type "integer"
::oapi/format "int64" ::oapi/format "int64"}}))})
::oapi/decode (fn [s]
(if (string? s)
(parse-long s)
s))}})
(register! ::safe-number (defn parse-double
{:type ::safe-number [v]
:pred #(and (number? %) (>= max-safe-int %) (>= % min-safe-int)) (or (ignoring
(if (string? v)
(c/parse-double v)
v))
v))
(def type:double
{:type :double
:min 0
:max 0
:compile
(fn [{:keys [max min] :as props} _ _]
(let [pred double?
pred (if (some? min)
(fn [v]
(and (>= v min)
(pred v)))
pred)
pred (if (some? max)
(fn [v]
(and (>= max v)
(pred v)))
pred)]
{:pred pred
:type-properties :type-properties
{:title "number" {:title "doble"
:description "Safe Number" :description "double number"
:error/message "expected to be number in safe range" :error/message "expected to be double"
:gen/gen (sg/one-of (sg/small-int) :error/code "errors.invalid-double"
(sg/small-double)) :gen/gen (sg/small-double :max max :min min)
:decode/string parse-double
:decode/json parse-double
::oapi/type "number" ::oapi/type "number"
::oapi/format "double" ::oapi/format "double"}}))})
::oapi/decode (fn [s]
(if (string? s)
(parse-double s)
s))}})
(register! ::safe-double (def type:number
{:type ::safe-double {:type :number
:pred #(and (double? %) (>= max-safe-int %) (>= % min-safe-int)) :min 0
:max 0
:compile
(fn [{:keys [max min] :as props} _ _]
(let [pred number?
pred (if (some? min)
(fn [v]
(and (>= v min)
(pred v)))
pred)
pred (if (some? max)
(fn [v]
(and (>= max v)
(pred v)))
pred)
gen (sg/one-of
(sg/small-int :max max :min min)
(sg/small-double :max max :min min))]
{:pred pred
:type-properties :type-properties
{:title "number" {:title "int"
:description "Safe Number" :description "int"
:error/message "expected to be number in safe range" :error/message "expected to be number"
:gen/gen (sg/small-double) :error/code "errors.invalid-number"
::oapi/type "number" :gen/gen gen
::oapi/format "double" :decode/string parse-double
::oapi/decode (fn [s] :decode/json parse-double
(if (string? s) ::oapi/type "number"}}))})
(parse-double s)
s))}})
(register! ::contains-any (register! ::int type:int)
(register! ::double type:double)
(register! ::number type:number)
(register! ::safe-int [::int {:max max-safe-int :min min-safe-int}])
(register! ::safe-double [::double {:max max-safe-int :min min-safe-int}])
(register! ::safe-number [::number {:max max-safe-int :min min-safe-int}])
(defn parse-boolean
[v]
(if (string? v)
(case v
("true" "t" "1") true
("false" "f" "0") false
v)
v))
(def type:boolean
{:type :boolean
:pred boolean?
:type-properties
{:title "boolean"
:description "boolean"
:error/message "expected boolean"
:error/code "errors.invalid-boolean"
:gen/gen sg/boolean
:decode/string parse-boolean
:decode/json parse-boolean
:encode/string str
::oapi/type "boolean"}})
(register! ::boolean type:boolean)
(def type:contains-any
{:type ::contains-any {:type ::contains-any
:min 1 :min 1
:max 1 :max 1
@ -775,17 +885,26 @@
{:title "contains" {:title "contains"
:description "contains predicate"}}))}) :description "contains predicate"}}))})
(register! ::inst (register! ::contains-any type:contains-any)
(def type:inst
{:type ::inst {:type ::inst
:pred inst? :pred inst?
:type-properties :type-properties
{:title "inst" {:title "inst"
:description "Satisfies Inst protocol" :description "Satisfies Inst protocol"
:error/message "expected to be number in safe range" :error/message "should be an instant"
:gen/gen (->> (sg/small-int) :gen/gen (->> (sg/small-int)
(sg/fmap (fn [v] (tm/instant v)))) (sg/fmap (fn [v] (tm/instant v))))
::oapi/type "number"
::oapi/format "int64"}}) :decode/string tm/instant
:encode/string tm/format-instant
:decode/json tm/instant
:encode/json tm/format-instant
::oapi/type "string"
::oapi/format "iso"}})
(register! ::inst type:inst)
(register! ::fn (register! ::fn
[:schema fn?]) [:schema fn?])
@ -804,6 +923,13 @@
::oapi/type "string" ::oapi/type "string"
::oapi/format "string"}}) ::oapi/format "string"}})
(defn decode-uri
[val]
(if (u/uri? val)
val
(-> val str/trim u/uri)))
(register! ::uri (register! ::uri
{:type ::uri {:type ::uri
:pred u/uri? :pred u/uri?
@ -839,13 +965,10 @@
:description "URI formatted string" :description "URI formatted string"
:error/code "errors.invalid-uri" :error/code "errors.invalid-uri"
:gen/gen (sg/uri) :gen/gen (sg/uri)
:decode/string decode-uri
:decode/json decode-uri
::oapi/type "string" ::oapi/type "string"
::oapi/format "uri" ::oapi/format "uri"}})
::oapi/decode
(fn [val]
(if (u/uri? val)
val
(-> val str/trim u/uri)))}})
(register! ::text (register! ::text
{:type :string {:type :string
@ -926,4 +1049,4 @@
(check-fn ::set-of-uuid)) (check-fn ::set-of-uuid))
(def check-set-of-emails! (def check-set-of-emails!
(check-fn ::set-of-emails)) (check-fn [::set ::email]))

View file

@ -5,7 +5,7 @@
;; Copyright (c) KALEIDOS INC ;; Copyright (c) KALEIDOS INC
(ns app.common.schema.generators (ns app.common.schema.generators
(:refer-clojure :exclude [set subseq uuid for filter map let]) (:refer-clojure :exclude [set subseq uuid for filter map let boolean])
#?(:cljs (:require-macros [app.common.schema.generators])) #?(:cljs (:require-macros [app.common.schema.generators]))
(:require (:require
[app.common.schema.registry :as sr] [app.common.schema.registry :as sr]
@ -122,6 +122,9 @@
(c/map second)) (c/map second))
(c/map list bools elements))))))) (c/map list bools elements)))))))
(def any tg/any)
(def boolean tg/boolean)
(defn set (defn set
[g] [g]
(tg/set g)) (tg/set g))

View file

@ -5,13 +5,14 @@
;; Copyright (c) KALEIDOS INC ;; Copyright (c) KALEIDOS INC
(ns app.common.time (ns app.common.time
"A new cross-platform date and time API. It should be preferred over "Minimal cross-platoform date time api for specific use cases on types
a platform specific implementation found on `app.util.time`." definition and other common code."
#?(:cljs #?(:cljs
(:require (:require
["luxon" :as lxn]) ["luxon" :as lxn])
:clj :clj
(:import (:import
java.time.format.DateTimeFormatter
java.time.Instant java.time.Instant
java.time.Duration))) java.time.Duration)))
@ -28,8 +29,16 @@
(defn instant (defn instant
[s] [s]
(if (int? s)
#?(:clj (Instant/ofEpochMilli s) #?(:clj (Instant/ofEpochMilli s)
:cljs (.fromMillis ^js DateTime s #js {:zone "local" :setZone false}))) :cljs (.fromMillis ^js DateTime s #js {:zone "local" :setZone false}))
#?(:clj (Instant/parse s)
:cljs (.fromISO ^js DateTime s))))
(defn format-instant
[v]
#?(:clj (.format DateTimeFormatter/ISO_INSTANT ^Instant v)
:cljs (.toISO ^js v)))
#?(:cljs #?(:cljs
(extend-protocol IComparable (extend-protocol IComparable
@ -45,7 +54,6 @@
0 0
(if (< (inst-ms it) (inst-ms other)) -1 1))))) (if (< (inst-ms it) (inst-ms other)) -1 1)))))
#?(:cljs #?(:cljs
(extend-type DateTime (extend-type DateTime
cljs.core/IEquiv cljs.core/IEquiv

View file

@ -9,25 +9,23 @@
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.schema.generators :as sg]
[app.common.schema.openapi :as-alias oapi] [app.common.schema.openapi :as-alias oapi]
[app.common.text :as txt] [app.common.text :as txt]
[app.common.types.color.generic :as-alias color-generic]
[app.common.types.color.gradient :as-alias color-gradient]
[app.common.types.color.gradient.stop :as-alias color-gradient-stop]
[app.common.types.plugins :as ctpg] [app.common.types.plugins :as ctpg]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[clojure.test.check.generators :as tgen]
[cuerdas.core :as str])) [cuerdas.core :as str]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SCHEMAS ;; SCHEMAS & TYPES
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def rgb-color-re (def rgb-color-re
#"^#(?:[0-9a-fA-F]{3}){1,2}$") #"^#(?:[0-9a-fA-F]{3}){1,2}$")
(defn- random-rgb-color (defn- generate-rgb-color
[] []
(sg/fmap (fn [_]
#?(:clj (format "#%06x" (rand-int 16rFFFFFF)) #?(:clj (format "#%06x" (rand-int 16rFFFFFF))
:cljs :cljs
(let [r (rand-int 255) (let [r (rand-int 255)
@ -37,20 +35,25 @@
(.. r (toString 16) (padStart 2 "0")) (.. r (toString 16) (padStart 2 "0"))
(.. g (toString 16) (padStart 2 "0")) (.. g (toString 16) (padStart 2 "0"))
(.. b (toString 16) (padStart 2 "0")))))) (.. b (toString 16) (padStart 2 "0"))))))
sg/any))
(sm/register! ::rgb-color (defn rgb-color-string?
{:type ::rgb-color [o]
:pred #(and (string? %) (some? (re-matches rgb-color-re %))) (and (string? o) (some? (re-matches rgb-color-re o))))
(def ^:private type:rgb-color
{:type :string
:pred rgb-color-string?
:type-properties :type-properties
{:title "rgb-color" {:title "rgb-color"
:description "RGB Color String" :description "RGB Color String"
:error/message "expected a valid RGB color" :error/message "expected a valid RGB color"
:gen/gen (->> tgen/any (tgen/fmap (fn [_] (random-rgb-color)))) :error/code "errors.invalid-rgb-color"
:gen/gen (generate-rgb-color)
::oapi/type "integer" ::oapi/type "integer"
::oapi/format "int64"}}) ::oapi/format "int64"}})
(sm/register! ::image-color (def schema:image-color
[:map {:title "ImageColor"} [:map {:title "ImageColor"}
[:name {:optional true} :string] [:name {:optional true} :string]
[:width :int] [:width :int]
@ -59,7 +62,10 @@
[:id ::sm/uuid] [:id ::sm/uuid]
[:keep-aspect-ratio {:optional true} :boolean]]) [:keep-aspect-ratio {:optional true} :boolean]])
(sm/register! ::gradient (def gradient-types
#{:linear :radial})
(def schema:gradient
[:map {:title "Gradient"} [:map {:title "Gradient"}
[:type [::sm/one-of #{:linear :radial}]] [:type [::sm/one-of #{:linear :radial}]]
[:start-x ::sm/safe-number] [:start-x ::sm/safe-number]
@ -74,7 +80,7 @@
[:opacity {:optional true} [:maybe ::sm/safe-number]] [:opacity {:optional true} [:maybe ::sm/safe-number]]
[:offset ::sm/safe-number]]]]]) [:offset ::sm/safe-number]]]]])
(sm/register! ::color (def schema:color
[:and [:and
[:map {:title "Color"} [:map {:title "Color"}
[:id {:optional true} ::sm/uuid] [:id {:optional true} ::sm/uuid]
@ -86,26 +92,32 @@
[:modified-at {:optional true} ::sm/inst] [:modified-at {:optional true} ::sm/inst]
[:ref-id {:optional true} ::sm/uuid] [:ref-id {:optional true} ::sm/uuid]
[:ref-file {:optional true} ::sm/uuid] [:ref-file {:optional true} ::sm/uuid]
[:gradient {:optional true} [:maybe ::gradient]] [:gradient {:optional true} [:maybe schema:gradient]]
[:image {:optional true} [:maybe ::image-color]] [:image {:optional true} [:maybe schema:image-color]]
[:plugin-data {:optional true} [:plugin-data {:optional true} ::ctpg/plugin-data]]
[:map-of {:gen/max 5} :keyword ::ctpg/plugin-data]]]
[::sm/contains-any {:strict true} [:color :gradient :image]]]) [::sm/contains-any {:strict true} [:color :gradient :image]]])
(sm/register! ::recent-color (def schema:recent-color
[:and [:and
[:map {:title "RecentColor"} [:map {:title "RecentColor"}
[:opacity {:optional true} [:maybe ::sm/safe-number]] [:opacity {:optional true} [:maybe ::sm/safe-number]]
[:color {:optional true} [:maybe ::rgb-color]] [:color {:optional true} [:maybe ::rgb-color]]
[:gradient {:optional true} [:maybe ::gradient]] [:gradient {:optional true} [:maybe schema:gradient]]
[:image {:optional true} [:maybe ::image-color]]] [:image {:optional true} [:maybe schema:image-color]]]
[::sm/contains-any {:strict true} [:color :gradient :image]]]) [::sm/contains-any {:strict true} [:color :gradient :image]]])
(sm/register! ::rgb-color type:rgb-color)
(sm/register! ::color schema:color)
(sm/register! ::gradient schema:gradient)
(sm/register! ::image-color schema:image-color)
(sm/register! ::recent-color schema:recent-color)
(def check-color! (def check-color!
(sm/check-fn ::color)) (sm/check-fn schema:color))
(def check-recent-color! (def check-recent-color!
(sm/check-fn ::recent-color)) (sm/check-fn schema:recent-color))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS ;; HELPERS

View file

@ -37,8 +37,7 @@
[:modified-at {:optional true} ::sm/inst] [:modified-at {:optional true} ::sm/inst]
[:objects {:optional true} [:objects {:optional true}
[:map-of {:gen/max 10} ::sm/uuid :map]] [:map-of {:gen/max 10} ::sm/uuid :map]]
[:plugin-data {:optional true} [:plugin-data {:optional true} ::ctpg/plugin-data]])
[:map-of {:gen/max 5} :keyword ::ctpg/plugin-data]]])
(def check-container! (def check-container!
(sm/check-fn ::container)) (sm/check-fn ::container))

View file

@ -59,8 +59,7 @@
[:map-of {:gen/max 2} ::sm/uuid ::cty/typography]] [:map-of {:gen/max 2} ::sm/uuid ::cty/typography]]
[:media {:optional true} [:media {:optional true}
[:map-of {:gen/max 5} ::sm/uuid ::media-object]] [:map-of {:gen/max 5} ::sm/uuid ::media-object]]
[:plugin-data {:optional true} [:plugin-data {:optional true} ::ctpg/plugin-data]])
[:map-of {:gen/max 5} :keyword ::ctpg/plugin-data]]])
(def check-file-data! (def check-file-data!
(sm/check-fn ::data)) (sm/check-fn ::data))

View file

@ -13,47 +13,54 @@
;; SCHEMA ;; SCHEMA
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(sm/register! ::grid-color (def schema:grid-color
[:map {:title "PageGridColor"} [:map {:title "PageGridColor"}
[:color ::ctc/rgb-color] [:color ::ctc/rgb-color]
[:opacity ::sm/safe-number]]) [:opacity ::sm/safe-number]])
(sm/register! ::column-params (def schema:column-params
[:map [:map
[:color ::grid-color] [:color schema:grid-color]
[:type {:optional true} [::sm/one-of #{:stretch :left :center :right}]] [:type {:optional true} [::sm/one-of #{:stretch :left :center :right}]]
[:size {:optional true} [:maybe ::sm/safe-number]] [:size {:optional true} [:maybe ::sm/safe-number]]
[:margin {:optional true} [:maybe ::sm/safe-number]] [:margin {:optional true} [:maybe ::sm/safe-number]]
[:item-length {:optional true} [:maybe ::sm/safe-number]] [:item-length {:optional true} [:maybe ::sm/safe-number]]
[:gutter {:optional true} [:maybe ::sm/safe-number]]]) [:gutter {:optional true} [:maybe ::sm/safe-number]]])
(sm/register! ::square-params (def schema:square-params
[:map [:map
[:size {:optional true} [:maybe ::sm/safe-number]] [:size {:optional true} [:maybe ::sm/safe-number]]
[:color ::grid-color]]) [:color schema:grid-color]])
(sm/register! ::grid (def schema:grid
[:multi {:dispatch :type} [:multi {:title "Grid"
:dispatch :type
:decode/json #(update % :type keyword)}
[:column [:column
[:map [:map
[:type [:= :column]] [:type [:= :column]]
[:display :boolean] [:display :boolean]
[:params ::column-params]]] [:params schema:column-params]]]
[:row [:row
[:map [:map
[:type [:= :row]] [:type [:= :row]]
[:display :boolean] [:display :boolean]
[:params ::column-params]]] [:params schema:column-params]]]
[:square [:square
[:map [:map
[:type [:= :square]] [:type [:= :square]]
[:display :boolean] [:display :boolean]
[:params ::square-params]]]]) [:params schema:square-params]]]])
(sm/register! ::saved-grids (def schema:saved-grids
[:map {:title "PageGrid"} [:map {:title "PageGrid"}
[:square {:optional true} ::square-params] [:square {:optional true} ::square-params]
[:row {:optional true} ::column-params] [:row {:optional true} ::column-params]
[:column {:optional true} ::column-params]]) [:column {:optional true} ::column-params]])
(sm/register! ::square-params schema:square-params)
(sm/register! ::column-params schema:column-params)
(sm/register! ::grid schema:grid)
(sm/register! ::saved-grids schema:saved-grids)

View file

@ -45,8 +45,7 @@
[:vector {:gen/max 2} ::flow]] [:vector {:gen/max 2} ::flow]]
[:guides {:optional true} [:guides {:optional true}
[:map-of {:gen/max 2} ::sm/uuid ::guide]] [:map-of {:gen/max 2} ::sm/uuid ::guide]]
[:plugin-data {:optional true} [:plugin-data {:optional true} ::ctpg/plugin-data]]]])
[:map-of {:gen/max 5} :keyword ::ctpg/plugin-data]]]]])
(def check-page-guide! (def check-page-guide!
(sm/check-fn ::guide)) (sm/check-fn ::guide))

View file

@ -6,11 +6,26 @@
(ns app.common.types.plugins (ns app.common.types.plugins
(:require (:require
[app.common.schema :as sm])) [app.common.schema :as sm]
[app.common.schema.generators :as sg]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SCHEMAS ;; SCHEMAS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(sm/register! ::plugin-data (def ^:private schema:string
[:map-of {:gen/max 5} :string :string]) [:schema {:gen/gen (sg/word-string)} :string])
(def ^:private schema:keyword
[:schema {:gen/gen (->> (sg/word-string)
(sg/fmap keyword))}
:keyword])
(def schema:plugin-data
[:map-of {:gen/max 5}
schema:keyword
[:map-of {:gen/max 5}
schema:string
schema:string]])
(sm/register! ::plugin-data schema:plugin-data)

View file

@ -86,10 +86,15 @@
:exclude :exclude
:intersection}) :intersection})
(sm/register! ::points (def grow-types
#{:auto-width
:auto-height
:fixed})
(def schema:points
[:vector {:gen/max 4 :gen/min 4} ::gpt/point]) [:vector {:gen/max 4 :gen/min 4} ::gpt/point])
(sm/register! ::fill (def schema:fill
[:map {:title "Fill"} [:map {:title "Fill"}
[:fill-color {:optional true} ::ctc/rgb-color] [:fill-color {:optional true} ::ctc/rgb-color]
[:fill-opacity {:optional true} ::sm/safe-number] [:fill-opacity {:optional true} ::sm/safe-number]
@ -98,7 +103,9 @@
[:fill-color-ref-id {:optional true} [:maybe ::sm/uuid]] [:fill-color-ref-id {:optional true} [:maybe ::sm/uuid]]
[:fill-image {:optional true} ::ctc/image-color]]) [:fill-image {:optional true} ::ctc/image-color]])
(sm/register! ::stroke (sm/register! ::fill schema:fill)
(def ^:private schema:stroke
[:map {:title "Stroke"} [:map {:title "Stroke"}
[:stroke-color {:optional true} :string] [:stroke-color {:optional true} :string]
[:stroke-color-ref-file {:optional true} ::sm/uuid] [:stroke-color-ref-file {:optional true} ::sm/uuid]
@ -116,43 +123,42 @@
[:stroke-color-gradient {:optional true} ::ctc/gradient] [:stroke-color-gradient {:optional true} ::ctc/gradient]
[:stroke-image {:optional true} ::ctc/image-color]]) [:stroke-image {:optional true} ::ctc/image-color]])
(sm/register! ::shape-base-attrs (sm/register! ::stroke schema:stroke)
(def ^:private schema:shape-base-attrs
[:map {:title "ShapeMinimalRecord"} [:map {:title "ShapeMinimalRecord"}
[:id ::sm/uuid] [:id ::sm/uuid]
[:name :string] [:name :string]
[:type [::sm/one-of shape-types]] [:type [::sm/one-of shape-types]]
[:selrect ::grc/rect] [:selrect ::grc/rect]
[:points ::points] [:points schema:points]
[:transform ::gmt/matrix] [:transform ::gmt/matrix]
[:transform-inverse ::gmt/matrix] [:transform-inverse ::gmt/matrix]
[:parent-id ::sm/uuid] [:parent-id ::sm/uuid]
[:frame-id ::sm/uuid]]) [:frame-id ::sm/uuid]])
(sm/register! ::shape-geom-attrs (def ^:private schema:shape-geom-attrs
[:map {:title "ShapeGeometryAttrs"} [:map {:title "ShapeGeometryAttrs"}
[:x ::sm/safe-number] [:x ::sm/safe-number]
[:y ::sm/safe-number] [:y ::sm/safe-number]
[:width ::sm/safe-number] [:width ::sm/safe-number]
[:height ::sm/safe-number]]) [:height ::sm/safe-number]])
(sm/register! ::shape-attrs (def schema:shape-attrs
[:map {:title "ShapeAttrs"} [:map {:title "ShapeAttrs"}
[:name {:optional true} :string]
[:component-id {:optional true} ::sm/uuid] [:component-id {:optional true} ::sm/uuid]
[:component-file {:optional true} ::sm/uuid] [:component-file {:optional true} ::sm/uuid]
[:component-root {:optional true} :boolean] [:component-root {:optional true} :boolean]
[:main-instance {:optional true} :boolean] [:main-instance {:optional true} :boolean]
[:remote-synced {:optional true} :boolean] [:remote-synced {:optional true} :boolean]
[:shape-ref {:optional true} ::sm/uuid] [:shape-ref {:optional true} ::sm/uuid]
[:selrect {:optional true} ::grc/rect]
[:points {:optional true} ::points]
[:blocked {:optional true} :boolean] [:blocked {:optional true} :boolean]
[:collapsed {:optional true} :boolean] [:collapsed {:optional true} :boolean]
[:locked {:optional true} :boolean] [:locked {:optional true} :boolean]
[:hidden {:optional true} :boolean] [:hidden {:optional true} :boolean]
[:masked-group {:optional true} :boolean] [:masked-group {:optional true} :boolean]
[:fills {:optional true} [:fills {:optional true}
[:vector {:gen/max 2} ::fill]] [:vector {:gen/max 2} schema:fill]]
[:hide-fill-on-export {:optional true} :boolean] [:hide-fill-on-export {:optional true} :boolean]
[:proportion {:optional true} ::sm/safe-number] [:proportion {:optional true} ::sm/safe-number]
[:proportion-lock {:optional true} :boolean] [:proportion-lock {:optional true} :boolean]
@ -167,36 +173,30 @@
[:r2 {:optional true} ::sm/safe-number] [:r2 {:optional true} ::sm/safe-number]
[:r3 {:optional true} ::sm/safe-number] [:r3 {:optional true} ::sm/safe-number]
[:r4 {:optional true} ::sm/safe-number] [:r4 {:optional true} ::sm/safe-number]
[:x {:optional true} [:maybe ::sm/safe-number]]
[:y {:optional true} [:maybe ::sm/safe-number]]
[:width {:optional true} [:maybe ::sm/safe-number]]
[:height {:optional true} [:maybe ::sm/safe-number]]
[:opacity {:optional true} ::sm/safe-number] [:opacity {:optional true} ::sm/safe-number]
[:grids {:optional true} [:grids {:optional true}
[:vector {:gen/max 2} ::ctg/grid]] [:vector {:gen/max 2} ::ctg/grid]]
[:exports {:optional true} [:exports {:optional true}
[:vector {:gen/max 2} ::ctse/export]] [:vector {:gen/max 2} ::ctse/export]]
[:strokes {:optional true} [:strokes {:optional true}
[:vector {:gen/max 2} ::stroke]] [:vector {:gen/max 2} schema:stroke]]
[:transform {:optional true} ::gmt/matrix] [:blend-mode {:optional true}
[:transform-inverse {:optional true} ::gmt/matrix] [::sm/one-of blend-modes]]
[:blend-mode {:optional true} [::sm/one-of blend-modes]]
[:interactions {:optional true} [:interactions {:optional true}
[:vector {:gen/max 2} ::ctsi/interaction]] [:vector {:gen/max 2} ::ctsi/interaction]]
[:shadow {:optional true} [:shadow {:optional true}
[:vector {:gen/max 1} ::ctss/shadow]] [:vector {:gen/max 1} ::ctss/shadow]]
[:blur {:optional true} ::ctsb/blur] [:blur {:optional true} ::ctsb/blur]
[:grow-type {:optional true} [:grow-type {:optional true}
[::sm/one-of #{:auto-width :auto-height :fixed}]] [::sm/one-of grow-types]]
[:plugin-data {:optional true} [:plugin-data {:optional true} ::ctpg/plugin-data]])
[:map-of {:gen/max 5} :keyword ::ctpg/plugin-data]]])
(sm/register! ::group-attrs (def schema:group-attrs
[:map {:title "GroupAttrs"} [:map {:title "GroupAttrs"}
[:type [:= :group]] [:type [:= :group]]
[:shapes [:vector {:gen/max 10 :gen/min 1} ::sm/uuid]]]) [:shapes [:vector {:gen/max 10 :gen/min 1} ::sm/uuid]]])
(sm/register! ::frame-attrs (def ^:private schema:frame-attrs
[:map {:title "FrameAttrs"} [:map {:title "FrameAttrs"}
[:type [:= :frame]] [:type [:= :frame]]
[:shapes [:vector {:gen/max 10 :gen/min 1} ::sm/uuid]] [:shapes [:vector {:gen/max 10 :gen/min 1} ::sm/uuid]]
@ -204,165 +204,168 @@
[:show-content {:optional true} :boolean] [:show-content {:optional true} :boolean]
[:hide-in-viewer {:optional true} :boolean]]) [:hide-in-viewer {:optional true} :boolean]])
(sm/register! ::bool-attrs (def ^:private schema:bool-attrs
[:map {:title "BoolAttrs"} [:map {:title "BoolAttrs"}
[:type [:= :bool]] [:type [:= :bool]]
[:shapes [:vector {:gen/max 10 :gen/min 1} ::sm/uuid]] [:shapes [:vector {:gen/max 10 :gen/min 1} ::sm/uuid]]
[:bool-type [::sm/one-of bool-types]]
[:bool-content ::ctsp/content]])
[:bool-type :keyword] (def ^:private schema:rect-attrs
;; FIXME: This should be the spec but we need to create a migration
;; to make this transition safely
;; [:bool-type [::sm/one-of bool-types]]
[:bool-content
[:vector {:gen/max 2}
[:map
[:command :keyword]
[:relative {:optional true} :boolean]
[:prev-pos {:optional true} ::gpt/point]
[:params {:optional true}
[:maybe
[:map-of {:gen/max 5} :keyword ::sm/safe-number]]]]]]])
(sm/register! ::rect-attrs
[:map {:title "RectAttrs"} [:map {:title "RectAttrs"}
[:type [:= :rect]]]) [:type [:= :rect]]])
(sm/register! ::circle-attrs (def ^:private schema:circle-attrs
[:map {:title "CircleAttrs"} [:map {:title "CircleAttrs"}
[:type [:= :circle]]]) [:type [:= :circle]]])
(sm/register! ::svg-raw-attrs (def ^:private schema:svg-raw-attrs
[:map {:title "SvgRawAttrs"} [:map {:title "SvgRawAttrs"}
[:type [:= :svg-raw]]]) [:type [:= :svg-raw]]])
(sm/register! ::image-attrs (def schema:image-attrs
[:map {:title "ImageAttrs"} [:map {:title "ImageAttrs"}
[:type [:= :image]] [:type [:= :image]]
[:metadata [:metadata
[:map [:map
[:width :int] [:width {:gen/gen (sg/small-int :min 1)} :int]
[:height :int] [:height {:gen/gen (sg/small-int :min 1)} :int]
[:mtype {:optional true} [:maybe :string]] [:mtype {:optional true
:gen/gen (sg/elements ["image/jpeg"
"image/png"])}
[:maybe :string]]
[:id ::sm/uuid]]]]) [:id ::sm/uuid]]]])
(sm/register! ::path-attrs (def ^:private schema:path-attrs
[:map {:title "PathAttrs"} [:map {:title "PathAttrs"}
[:type [:= :path]] [:type [:= :path]]
[:content ::ctsp/content]]) [:content ::ctsp/content]])
(sm/register! ::text-attrs (def ^:private schema:text-attrs
[:map {:title "TextAttrs"} [:map {:title "TextAttrs"}
[:type [:= :text]] [:type [:= :text]]
[:content {:optional true} [:maybe ::ctsx/content]]]) [:content {:optional true} [:maybe ::ctsx/content]]])
(sm/register! ::shape-map (defn- decode-shape
[:multi {:dispatch :type :title "Shape"} [o]
[:group (if (map? o)
[:and {:title "GroupShape"} (map->Shape o)
::shape-base-attrs o))
::shape-geom-attrs
::shape-attrs
::group-attrs
::ctsl/layout-child-attrs]]
[:frame (defn- shape-generator
[:and {:title "FrameShape"} "Get the shape generator."
::shape-base-attrs []
::shape-geom-attrs (->> (sg/generator schema:shape-base-attrs)
::frame-attrs
::ctsl/layout-attrs
::ctsl/layout-child-attrs]]
[:bool
[:and {:title "BoolShape"}
::shape-base-attrs
::shape-attrs
::bool-attrs
::ctsl/layout-child-attrs]]
[:rect
[:and {:title "RectShape"}
::shape-base-attrs
::shape-geom-attrs
::shape-attrs
::rect-attrs
::ctsl/layout-child-attrs]]
[:circle
[:and {:title "CircleShape"}
::shape-base-attrs
::shape-geom-attrs
::shape-attrs
::circle-attrs
::ctsl/layout-child-attrs]]
[:image
[:and {:title "ImageShape"}
::shape-base-attrs
::shape-geom-attrs
::shape-attrs
::image-attrs
::ctsl/layout-child-attrs]]
[:svg-raw
[:and {:title "SvgRawShape"}
::shape-base-attrs
::shape-geom-attrs
::shape-attrs
::svg-raw-attrs
::ctsl/layout-child-attrs]]
[:path
[:and {:title "PathShape"}
::shape-base-attrs
::shape-attrs
::path-attrs
::ctsl/layout-child-attrs]]
[:text
[:and {:title "TextShape"}
::shape-base-attrs
::shape-geom-attrs
::shape-attrs
::text-attrs
::ctsl/layout-child-attrs]]])
(sm/register! ::shape
[:and
{:title "Shape"
:gen/gen (->> (sg/generator ::shape-base-attrs)
(sg/mcat (fn [{:keys [type] :as shape}] (sg/mcat (fn [{:keys [type] :as shape}]
(sg/let [attrs1 (sg/generator ::shape-attrs) (sg/let [attrs1 (sg/generator schema:shape-attrs)
attrs2 (sg/generator ::shape-geom-attrs) attrs2 (sg/generator schema:shape-geom-attrs)
attrs3 (case type attrs3 (case type
:text (sg/generator ::text-attrs) :text (sg/generator schema:text-attrs)
:path (sg/generator ::path-attrs) :path (sg/generator schema:path-attrs)
:svg-raw (sg/generator ::svg-raw-attrs) :svg-raw (sg/generator schema:svg-raw-attrs)
:image (sg/generator ::image-attrs) :image (sg/generator schema:image-attrs)
:circle (sg/generator ::circle-attrs) :circle (sg/generator schema:circle-attrs)
:rect (sg/generator ::rect-attrs) :rect (sg/generator schema:rect-attrs)
:bool (sg/generator ::bool-attrs) :bool (sg/generator schema:bool-attrs)
:group (sg/generator ::group-attrs) :group (sg/generator schema:group-attrs)
:frame (sg/generator ::frame-attrs))] :frame (sg/generator schema:frame-attrs))]
(if (or (= type :path) (if (or (= type :path)
(= type :bool)) (= type :bool))
(merge attrs1 shape attrs3) (merge attrs1 shape attrs3)
(merge attrs1 shape attrs2 attrs3))))) (merge attrs1 shape attrs2 attrs3)))))
(sg/fmap map->Shape))} (sg/fmap map->Shape)))
::shape-map
[:fn shape?]]) (def schema:shape
[:and {:title "Shape"
:gen/gen (shape-generator)
:decode/json {:leave decode-shape}}
[:fn shape?]
[:multi {:dispatch :type
:decode/json (fn [shape]
(update shape :type keyword))
:title "Shape"}
[:group
[:merge {:title "GroupShape"}
::ctsl/layout-child-attrs
schema:group-attrs
schema:shape-attrs
schema:shape-geom-attrs
schema:shape-base-attrs]]
[:frame
[:merge {:title "FrameShape"}
::ctsl/layout-child-attrs
::ctsl/layout-attrs
schema:frame-attrs
schema:shape-attrs
schema:shape-geom-attrs
schema:shape-base-attrs]]
[:bool
[:merge {:title "BoolShape"}
::ctsl/layout-child-attrs
schema:bool-attrs
schema:shape-attrs
schema:shape-base-attrs]]
[:rect
[:merge {:title "RectShape"}
::ctsl/layout-child-attrs
schema:rect-attrs
schema:shape-attrs
schema:shape-geom-attrs
schema:shape-base-attrs]]
[:circle
[:merge {:title "CircleShape"}
::ctsl/layout-child-attrs
schema:circle-attrs
schema:shape-attrs
schema:shape-geom-attrs
schema:shape-base-attrs]]
[:image
[:merge {:title "ImageShape"}
::ctsl/layout-child-attrs
schema:image-attrs
schema:shape-attrs
schema:shape-geom-attrs
schema:shape-base-attrs]]
[:svg-raw
[:merge {:title "SvgRawShape"}
::ctsl/layout-child-attrs
schema:svg-raw-attrs
schema:shape-attrs
schema:shape-geom-attrs
schema:shape-base-attrs]]
[:path
[:merge {:title "PathShape"}
::ctsl/layout-child-attrs
schema:path-attrs
schema:shape-attrs
schema:shape-base-attrs]]
[:text
[:merge {:title "TextShape"}
::ctsl/layout-child-attrs
schema:text-attrs
schema:shape-attrs
schema:shape-geom-attrs
schema:shape-base-attrs]]]])
(sm/register! ::shape schema:shape)
(def check-shape-attrs! (def check-shape-attrs!
(sm/check-fn ::shape-attrs)) (sm/check-fn schema:shape-attrs))
(def check-shape! (def check-shape!
(sm/check-fn ::shape)) (sm/check-fn schema:shape))
(defn has-images? (defn has-images?
[{:keys [fills strokes]}] [{:keys [fills strokes]}]
(or (or (some :fill-image fills)
(some :fill-image fills)
(some :stroke-image strokes))) (some :stroke-image strokes)))
;; --- Initialization ;; --- Initialization

View file

@ -8,10 +8,12 @@
(:require (:require
[app.common.schema :as sm])) [app.common.schema :as sm]))
(def export-types #{:png :jpeg :svg :pdf}) (def types #{:png :jpeg :svg :pdf})
(sm/register! ::export (def schema:export
[:map {:title "ShapeExport"} [:map {:title "ShapeExport"}
[:type [::sm/one-of export-types]] [:type [::sm/one-of types]]
[:scale ::sm/safe-number] [:scale ::sm/safe-number]
[:suffix :string]]) [:suffix :string]])
(sm/register! ::export schema:export)

View file

@ -11,7 +11,8 @@
[app.common.files.helpers :as cfh] [app.common.files.helpers :as cfh]
[app.common.geom.point :as gpt] [app.common.geom.point :as gpt]
[app.common.geom.shapes.bounds :as gsb] [app.common.geom.shapes.bounds :as gsb]
[app.common.schema :as sm])) [app.common.schema :as sm]
[app.common.schema.generators :as sg]))
;; WARNING: options are not deleted when changing event or action type, so it can be ;; WARNING: options are not deleted when changing event or action type, so it can be
;; restored if the user changes it back later. ;; restored if the user changes it back later.
@ -71,41 +72,53 @@
(def animation-types (def animation-types
#{:dissolve :slide :push}) #{:dissolve :slide :push})
(sm/register! ::animation (def schema:dissolve-animation
[:multi {:dispatch :animation-type :title "Animation"}
[:dissolve
[:map {:title "AnimationDisolve"} [:map {:title "AnimationDisolve"}
[:animation-type [:= :dissolve]] [:animation-type [:= :dissolve]]
[:duration ::sm/safe-int] [:duration ::sm/safe-int]
[:easing [::sm/one-of easing-types]]]] [:easing [::sm/one-of easing-types]]])
[:slide
(def schema:slide-animation
[:map {:title "AnimationSlide"} [:map {:title "AnimationSlide"}
[:animation-type [:= :slide]] [:animation-type [:= :slide]]
[:duration ::sm/safe-int] [:duration ::sm/safe-int]
[:easing [::sm/one-of easing-types]] [:easing [::sm/one-of easing-types]]
[:way [::sm/one-of way-types]] [:way [::sm/one-of way-types]]
[:direction [::sm/one-of direction-types]] [:direction [::sm/one-of direction-types]]
[:offset-effect :boolean]]] [:offset-effect :boolean]])
[:push
[:map {:title "AnimationPush"} (def schema:push-animation
[:map {:title "PushAnimation"}
[:animation-type [:= :push]] [:animation-type [:= :push]]
[:duration ::sm/safe-int] [:duration ::sm/safe-int]
[:easing [::sm/one-of easing-types]] [:easing [::sm/one-of easing-types]]
[:direction [::sm/one-of direction-types]]]]]) [:direction [::sm/one-of direction-types]]])
(def schema:animation
[:multi {:dispatch :animation-type
:title "Animation"
:gen/gen (sg/one-of (sg/generator schema:dissolve-animation)
(sg/generator schema:slide-animation)
(sg/generator schema:push-animation))
:decode/json #(update % :animation-type keyword)}
[:dissolve schema:dissolve-animation]
[:slide schema:slide-animation]
[:push schema:push-animation]])
(sm/register! ::animation schema:animation)
(def check-animation! (def check-animation!
(sm/check-fn ::animation)) (sm/check-fn schema:animation))
(sm/register! ::interaction (def schema:navigate-interaction
[:multi {:dispatch :action-type}
[:navigate
[:map [:map
[:action-type [:= :navigate]] [:action-type [:= :navigate]]
[:event-type [::sm/one-of event-types]] [:event-type [::sm/one-of event-types]]
[:destination {:optional true} [:maybe ::sm/uuid]] [:destination {:optional true} [:maybe ::sm/uuid]]
[:preserve-scroll {:optional true} :boolean] [:preserve-scroll {:optional true} :boolean]
[:animation {:optional true} ::animation]]] [:animation {:optional true} ::animation]])
[:open-overlay
(def schema:open-overlay-interaction
[:map [:map
[:action-type [:= :open-overlay]] [:action-type [:= :open-overlay]]
[:event-type [::sm/one-of event-types]] [:event-type [::sm/one-of event-types]]
@ -115,8 +128,9 @@
[:close-click-outside {:optional true} :boolean] [:close-click-outside {:optional true} :boolean]
[:background-overlay {:optional true} :boolean] [:background-overlay {:optional true} :boolean]
[:animation {:optional true} ::animation] [:animation {:optional true} ::animation]
[:position-relative-to {:optional true} [:maybe ::sm/uuid]]]] [:position-relative-to {:optional true} [:maybe ::sm/uuid]]])
[:toggle-overlay
(def schema:toggle-overlay-interaction
[:map [:map
[:action-type [:= :toggle-overlay]] [:action-type [:= :toggle-overlay]]
[:event-type [::sm/one-of event-types]] [:event-type [::sm/one-of event-types]]
@ -126,26 +140,48 @@
[:close-click-outside {:optional true} :boolean] [:close-click-outside {:optional true} :boolean]
[:background-overlay {:optional true} :boolean] [:background-overlay {:optional true} :boolean]
[:animation {:optional true} ::animation] [:animation {:optional true} ::animation]
[:position-relative-to {:optional true} [:maybe ::sm/uuid]]]] [:position-relative-to {:optional true} [:maybe ::sm/uuid]]])
[:close-overlay
(def schema:close-overlay-interaction
[:map [:map
[:action-type [:= :close-overlay]] [:action-type [:= :close-overlay]]
[:event-type [::sm/one-of event-types]] [:event-type [::sm/one-of event-types]]
[:destination {:optional true} [:maybe ::sm/uuid]] [:destination {:optional true} [:maybe ::sm/uuid]]
[:animation {:optional true} ::animation] [:animation {:optional true} ::animation]
[:position-relative-to {:optional true} [:maybe ::sm/uuid]]]] [:position-relative-to {:optional true} [:maybe ::sm/uuid]]])
[:prev-screen
(def schema:prev-scren-interaction
[:map [:map
[:action-type [:= :prev-screen]] [:action-type [:= :prev-screen]]
[:event-type [::sm/one-of event-types]]]] [:event-type [::sm/one-of event-types]]])
[:open-url
(def schema:open-url-interaction
[:map [:map
[:action-type [:= :open-url]] [:action-type [:= :open-url]]
[:event-type [::sm/one-of event-types]] [:event-type [::sm/one-of event-types]]
[:url :string]]]]) [:url :string]])
(def schema:interaction
[:multi {:dispatch :action-type
: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))
:decode/json #(update % :action-type keyword)}
[: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)
(def check-interaction! (def check-interaction!
(sm/check-fn ::interaction)) (sm/check-fn schema:interaction))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS ;; HELPERS

View file

@ -8,30 +8,27 @@
(:require (:require
[app.common.schema :as sm])) [app.common.schema :as sm]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (def schema:line-to-segment
;; SCHEMA
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(sm/register! ::segment
[:multi {:title "PathSegment" :dispatch :command}
[:line-to
[:map [:map
[:command [:= :line-to]] [:command [:= :line-to]]
[:params [:params
[:map [:map
[:x ::sm/safe-number] [:x ::sm/safe-number]
[:y ::sm/safe-number]]]]] [:y ::sm/safe-number]]]])
[:close-path
(def schema:close-path-segment
[:map [:map
[:command [:= :close-path]]]] [:command [:= :close-path]]])
[:move-to
(def schema:move-to-segment
[:map [:map
[:command [:= :move-to]] [:command [:= :move-to]]
[:params [:params
[:map [:map
[:x ::sm/safe-number] [:x ::sm/safe-number]
[:y ::sm/safe-number]]]]] [:y ::sm/safe-number]]]])
[:curve-to
(def schema:curve-to-segment
[:map [:map
[:command [:= :curve-to]] [:command [:= :curve-to]]
[:params [:params
@ -41,7 +38,19 @@
[:c1x ::sm/safe-number] [:c1x ::sm/safe-number]
[:c1y ::sm/safe-number] [:c1y ::sm/safe-number]
[:c2x ::sm/safe-number] [:c2x ::sm/safe-number]
[:c2y ::sm/safe-number]]]]]]) [:c2y ::sm/safe-number]]]])
(sm/register! ::content (def schema:path-segment
[:vector ::segment]) [:multi {:title "PathSegment"
:dispatch :command
:decode/json #(update % :command keyword)}
[:line-to schema:line-to-segment]
[:close-path schema:close-path-segment]
[:move-to schema:move-to-segment]
[:curve-to schema:curve-to-segment]])
(def schema:path-content
[:vector schema:path-segment])
(sm/register! ::segment schema:path-segment)
(sm/register! ::content schema:path-content)

View file

@ -7,17 +7,23 @@
(ns app.common.types.shape.shadow (ns app.common.types.shape.shadow
(:require (:require
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.schema.generators :as sg]
[app.common.types.color :as ctc])) [app.common.types.color :as ctc]))
(def styles #{:drop-shadow :inner-shadow}) (def styles #{:drop-shadow :inner-shadow})
(sm/register! ::shadow (def schema:shadow
[:map {:title "Shadow"} [:map {:title "Shadow"}
[:id [:maybe ::sm/uuid]] [:id [:maybe ::sm/uuid]]
[:style [::sm/one-of styles]] [:style
[:and {:gen/gen (sg/elements styles)}
:keyword
[::sm/one-of styles]]]
[:offset-x ::sm/safe-number] [:offset-x ::sm/safe-number]
[:offset-y ::sm/safe-number] [:offset-y ::sm/safe-number]
[:blur ::sm/safe-number] [:blur ::sm/safe-number]
[:spread ::sm/safe-number] [:spread ::sm/safe-number]
[:hidden :boolean] [:hidden :boolean]
[:color ::ctc/color]]) [:color ::ctc/color]])
(sm/register! ::shadow schema:shadow)

View file

@ -31,8 +31,7 @@
[:text-transform :string] [:text-transform :string]
[:modified-at {:optional true} ::sm/inst] [:modified-at {:optional true} ::sm/inst]
[:path {:optional true} [:maybe :string]] [:path {:optional true} [:maybe :string]]
[:plugin-data {:optional true} [:plugin-data {:optional true} ::ctpg/plugin-data]])
[:map-of {:gen/max 5} :keyword ::ctpg/plugin-data]]])
(def check-typography! (def check-typography!
(sm/check-fn ::typography)) (sm/check-fn ::typography))

View file

@ -0,0 +1,41 @@
;; 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 common-tests.schema-test
(:require
[app.common.schema :as sm]
[app.common.schema.generators :as sg]
[clojure.test :as t]))
(t/deftest test-set-of-email
(t/testing "decoding"
(let [candidate1 "a@b.com a@c.net"
schema [::sm/set ::sm/email]
result1 (sm/decode schema candidate1 sm/string-transformer)
result2 (sm/decode schema candidate1 sm/json-transformer)]
(t/is (= result1 #{"a@b.com" "a@c.net"}))
(t/is (= result2 #{"a@b.com" "a@c.net"}))))
(t/testing "encoding"
(let [candidate #{"a@b.com" "a@c.net"}
schema [::sm/set ::sm/email]
result1 (sm/encode schema candidate sm/string-transformer)
result2 (sm/decode schema candidate sm/json-transformer)]
(t/is (= result1 "a@b.com, a@c.net"))
(t/is (= result2 candidate))))
(t/testing "validate"
(let [candidate #{"a@b.com" "a@c.net"}
schema [::sm/set ::sm/email]]
(t/is (true? (sm/validate schema candidate)))
(t/is (true? (sm/validate schema #{})))
(t/is (false? (sm/validate schema #{"a"})))))
(t/testing "generate"
(let [schema [::sm/set ::sm/email]
value (sg/generate schema)]
(t/is (true? (sm/validate schema (sg/generate schema)))))))

View file

@ -0,0 +1,150 @@
;; 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 common-tests.types.decoder-test
(:require
[app.common.json :as json]
[app.common.pprint :as pp]
[app.common.schema :as sm]
[app.common.schema.generators :as sg]
[app.common.types.color :refer [schema:color schema:gradient]]
[app.common.types.plugins :refer [schema:plugin-data]]
[app.common.types.shape :as tsh]
[app.common.types.shape.interactions :refer [schema:animation schema:interaction]]
[app.common.types.shape.path :refer [schema:path-content]]
[app.common.types.shape.shadow :refer [schema:shadow]]
[app.common.uuid :as uuid]
[clojure.test :as t]))
(defn json-roundtrip
[data]
(-> data
(json/encode :key-fn json/write-camel-key)
(json/decode :key-fn json/read-kebab-key)))
(t/deftest map-of-with-strings
(let [schema [:map [:data [:map-of :string :int]]]
encode (sm/encoder schema (sm/json-transformer))
decode (sm/decoder schema (sm/json-transformer))
data1 {:data {"foo/bar" 1
"foo-baz" 2}}
data2 (encode data1)
data3 (json-roundtrip data2)
data4 (decode data3)]
;; (pp/pprint data1)
;; (pp/pprint data2)
;; (pp/pprint data3)
;; (pp/pprint data4)
(t/is (= data1 data2))
(t/is (= data1 data4))
(t/is (not= data1 data3))))
(t/deftest gradient-json-roundtrip
(let [encode (sm/encoder schema:gradient (sm/json-transformer))
decode (sm/decoder schema:gradient (sm/json-transformer))]
(sg/check!
(sg/for [gradient (sg/generator schema:gradient)]
(let [gradient-1 (encode gradient)
gradient-2 (json-roundtrip gradient-1)
gradient-3 (decode gradient-2)]
;; (app.common.pprint/pprint gradient)
;; (app.common.pprint/pprint gradient-3)
(t/is (= gradient gradient-3))))
{:num 500})))
(t/deftest color-json-roundtrip
(let [encode (sm/encoder schema:color (sm/json-transformer))
decode (sm/decoder schema:color (sm/json-transformer))]
(sg/check!
(sg/for [color (sg/generator schema:color)]
(let [color-1 (encode color)
color-2 (json-roundtrip color-1)
color-3 (decode color-2)]
;; (app.common.pprint/pprint color)
;; (app.common.pprint/pprint color-3)
(t/is (= color color-3))))
{:num 500})))
(t/deftest shape-shadow-json-roundtrip
(let [encode (sm/encoder schema:shadow (sm/json-transformer))
decode (sm/decoder schema:shadow (sm/json-transformer))]
(sg/check!
(sg/for [shadow (sg/generator schema:shadow)]
(let [shadow-1 (encode shadow)
shadow-2 (json-roundtrip shadow-1)
shadow-3 (decode shadow-2)]
;; (app.common.pprint/pprint shadow)
;; (app.common.pprint/pprint shadow-3)
(t/is (= shadow shadow-3))))
{:num 500})))
(t/deftest shape-animation-json-roundtrip
(let [encode (sm/encoder schema:animation (sm/json-transformer))
decode (sm/decoder schema:animation (sm/json-transformer))]
(sg/check!
(sg/for [animation (sg/generator schema:animation)]
(let [animation-1 (encode animation)
animation-2 (json-roundtrip animation-1)
animation-3 (decode animation-2)]
;; (app.common.pprint/pprint animation)
;; (app.common.pprint/pprint animation-3)
(t/is (= animation animation-3))))
{:num 500})))
(t/deftest shape-interaction-json-roundtrip
(let [encode (sm/encoder schema:interaction (sm/json-transformer))
decode (sm/decoder schema:interaction (sm/json-transformer))]
(sg/check!
(sg/for [interaction (sg/generator schema:interaction)]
(let [interaction-1 (encode interaction)
interaction-2 (json-roundtrip interaction-1)
interaction-3 (decode interaction-2)]
;; (app.common.pprint/pprint interaction)
;; (app.common.pprint/pprint interaction-3)
(t/is (= interaction interaction-3))))
{:num 500})))
(t/deftest shape-path-content-json-roundtrip
(let [encode (sm/encoder schema:path-content (sm/json-transformer))
decode (sm/decoder schema:path-content (sm/json-transformer))]
(sg/check!
(sg/for [path-content (sg/generator schema:path-content)]
(let [path-content-1 (encode path-content)
path-content-2 (json-roundtrip path-content-1)
path-content-3 (decode path-content-2)]
;; (app.common.pprint/pprint path-content)
;; (app.common.pprint/pprint path-content-3)
(t/is (= path-content path-content-3))))
{:num 500})))
(t/deftest plugin-data-json-roundtrip
(let [encode (sm/encoder schema:plugin-data (sm/json-transformer))
decode (sm/decoder schema:plugin-data (sm/json-transformer))]
(sg/check!
(sg/for [data (sg/generator schema:plugin-data)]
(let [data-1 (encode data)
data-2 (json-roundtrip data-1)
data-3 (decode data-2)]
(t/is (= data data-3))))
{:num 500})))
(t/deftest shape-json-roundtrip
(let [encode (sm/encoder ::tsh/shape (sm/json-transformer))
decode (sm/decoder ::tsh/shape (sm/json-transformer))]
(sg/check!
(sg/for [shape (sg/generator ::tsh/shape)]
(let [shape-1 (encode shape)
shape-2 (json-roundtrip shape-1)
shape-3 (decode shape-2)]
;; (app.common.pprint/pprint shape)
;; (app.common.pprint/pprint shape-3)
(t/is (= shape shape-3))))
{:num 1000})))

View file

@ -115,7 +115,7 @@
(def ^:private render-objects-decoder (def ^:private render-objects-decoder
(sm/lazy-decoder schema:render-objects (sm/lazy-decoder schema:render-objects
sm/default-transformer)) sm/string-transformer))
(def ^:private render-objects-validator (def ^:private render-objects-validator
(sm/lazy-validator schema:render-objects)) (sm/lazy-validator schema:render-objects))
@ -236,7 +236,7 @@
(def ^:private render-components-decoder (def ^:private render-components-decoder
(sm/lazy-decoder schema:render-components (sm/lazy-decoder schema:render-components
sm/default-transformer)) sm/string-transformer))
(def ^:private render-components-validator (def ^:private render-components-validator
(sm/lazy-validator schema:render-components)) (sm/lazy-validator schema:render-components))

View file

@ -79,7 +79,7 @@
[f {:keys [schema validators]}] [f {:keys [schema validators]}]
(fn [& args] (fn [& args]
(let [state (apply f args) (let [state (apply f args)
cleaned (sm/decode schema (:data state)) cleaned (sm/decode schema (:data state) sm/string-transformer)
valid? (sm/validate schema cleaned) valid? (sm/validate schema cleaned)
errors (when-not valid? errors (when-not valid?
(collect-schema-errors schema validators state))] (collect-schema-errors schema validators state))]

View file

@ -129,6 +129,8 @@
:else :else
(transform-prop-key k)))) (transform-prop-key k))))
;; FIXME: REPEATED from app.common.json
(defn map->obj (defn map->obj
"A simplified version of clj->js with focus on performance" "A simplified version of clj->js with focus on performance"
([x] (map->obj x identity)) ([x] (map->obj x identity))