0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-02-04 13:29:14 -05:00

Merge pull request #3538 from penpot/hiru-validate-shapes

 Add function to validate shape referential integrity
This commit is contained in:
Pablo Alba 2023-08-23 10:03:38 +02:00 committed by GitHub
commit 9618bd6697
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 348 additions and 19 deletions

View file

@ -10,6 +10,7 @@
[app.common.data :as d] [app.common.data :as d]
[app.common.logging :as l] [app.common.logging :as l]
[app.common.pages.helpers :as cph] [app.common.pages.helpers :as cph]
[app.common.pprint :refer [pprint]]
[app.common.types.component :as ctk] [app.common.types.component :as ctk]
[app.common.types.container :as ctn] [app.common.types.container :as ctn]
[app.common.types.file :as ctf] [app.common.types.file :as ctf]
@ -19,6 +20,21 @@
[app.srepl.helpers :as h] [app.srepl.helpers :as h]
[app.util.blob :as blob])) [app.util.blob :as blob]))
(defn validate-file
[file]
(let [libs (->> (files/get-file-libraries app.srepl.helpers/*conn* (:id file))
(cons file)
(map #(files/get-file app.srepl.helpers/*conn* (:id %) (:features file)))
(d/index-by :id))
update-page (fn [page]
(let [errors (ctf/validate-shape uuid/zero file page libs)]
(when (seq errors)
(prn "******Errors in file " (:id file) " page " (:id page))
(pprint errors {:level 3}))))]
(update file :data h/update-pages update-page)))
(defn repair-orphaned-shapes (defn repair-orphaned-shapes
"There are some shapes whose parent has been deleted. This function "There are some shapes whose parent has been deleted. This function
detects them and puts them as children of the root node." detects them and puts them as children of the root node."

View file

@ -122,6 +122,13 @@
[libraries library-id component-id & {:keys [include-deleted?] :or {include-deleted? false}}] [libraries library-id component-id & {:keys [include-deleted?] :or {include-deleted? false}}]
(ctkl/get-component (dm/get-in libraries [library-id :data]) component-id include-deleted?)) (ctkl/get-component (dm/get-in libraries [library-id :data]) component-id include-deleted?))
(defn resolve-component
"Retrieve the referenced component, from the local file or from a library"
[shape file libraries & params]
(if (= (:component-file shape) (:id file))
(ctkl/get-component (:data file) (:component-id shape) params)
(get-component libraries (:component-file shape) (:component-id shape) params)))
(defn get-component-library (defn get-component-library
"Retrieve the library the component belongs to." "Retrieve the library the component belongs to."
[libraries instance-root] [libraries instance-root]
@ -162,21 +169,23 @@
(dm/get-in component [:objects shape-id])))) (dm/get-in component [:objects shape-id]))))
(defn get-ref-shape (defn get-ref-shape
"Retrieve the shape in the component that is referenced by the "Retrieve the shape in the component that is referenced by the instance shape."
instance shape."
[file-data component shape] [file-data component shape]
(when (:shape-ref shape) (when (:shape-ref shape)
(get-component-shape file-data component (:shape-ref shape)))) (get-component-shape file-data component (:shape-ref shape))))
(defn get-component-shapes (defn find-ref-shape
"Retrieve all shapes of the component" "Locate the near component in the local file or libraries, and retrieve the shape
[file-data component] referenced by the instance shape."
(let [components-v2 (dm/get-in file-data [:options :components-v2])] [file page libraries shape]
(if (and components-v2 (let [root-shape (ctn/get-component-shape (:objects page) shape)
(not (:deleted component))) ;; the deleted components have its children in the :objects property component-file (if (= (:component-file root-shape) (:id file))
(let [instance-page (get-component-page file-data component)] file
(cph/get-children-with-self (:objects instance-page) (:main-instance-id component))) (get libraries (:component-file root-shape)))
(vals (:objects component))))) component (when component-file
(ctkl/get-component (:data component-file) (:component-id root-shape)))]
(when component
(get-ref-shape (:data component-file) component shape))))
(defn find-remote-shape (defn find-remote-shape
"Recursively go back by the :shape-ref of the shape until find the correct shape of the original component" "Recursively go back by the :shape-ref of the shape until find the correct shape of the original component"
@ -190,6 +199,16 @@
shape shape
(find-remote-shape component-container libraries remote-shape)))) (find-remote-shape component-container libraries remote-shape))))
(defn get-component-shapes
"Retrieve all shapes of the component"
[file-data component]
(let [components-v2 (dm/get-in file-data [:options :components-v2])]
(if (and components-v2
(not (:deleted component))) ;; the deleted components have its children in the :objects property
(let [instance-page (get-component-page file-data component)]
(cph/get-children-with-self (:objects instance-page) (:main-instance-id component)))
(vals (:objects component)))))
;; Return true if the object is a component that exists on the file or its libraries (even a deleted one) ;; Return true if the object is a component that exists on the file or its libraries (even a deleted one)
(defn is-known-component? (defn is-known-component?
[shape libraries] [shape libraries]
@ -583,7 +602,6 @@
(d/not-empty? used-typographies) (d/not-empty? used-typographies)
(absorb-typographies used-typographies)))) (absorb-typographies used-typographies))))
;; Debug helpers ;; Debug helpers
(declare dump-shape-component-info) (declare dump-shape-component-info)
@ -838,3 +856,283 @@
(when-not (:deleted component) (when-not (:deleted component)
(println) (println)
(dump-page page file libraries* (assoc flags :root-id (:id root)))))))))))))))) (dump-page page file libraries* (assoc flags :root-id (:id root))))))))))))))))
;; Validation
(declare validate-shape)
(defn validate-parent-children
"Validate parent and children exists, and the link is bidirectional."
[shape file page report-error]
(let [parent (ctst/get-shape page (:parent-id shape))]
(if (nil? parent)
(report-error :parent-not-found
(str/format "Parent %s not found" (:parent-id shape))
shape file page)
(do
(when-not (cph/root? shape)
(when-not (some #{(:id shape)} (:shapes parent))
(report-error :child-not-in-parent
(str/format "Shape %s not in parent's children list" (:id shape))
shape file page)))
(doseq [child-id (:shapes shape)]
(when (nil? (ctst/get-shape page child-id))
(report-error :child-not-found
(str/format "Child %s not found" child-id)
shape file page)))))))
(defn validate-component-main-head
"Validate shape is a main instance head, component exists and its main-instance points to this shape."
[shape file page libraries report-error]
(when (nil? (:main-instance shape))
(report-error :component-not-main
(str/format "Shape expected to be main instance")
shape file page))
(let [component (resolve-component shape file libraries {:include-deleted? true})]
(if (nil? component)
(report-error :component-not-found
(str/format "Component %s not found in file" (:component-id shape) (:component-file shape))
shape file page)
(do
(when-not (= (:main-instance-id component) (:id shape))
(report-error :invalid-main-instance-id
(str/format "Main instance id of component %s is not valid" (:component-id shape))
shape file page))
(when-not (= (:main-instance-page component) (:id page))
(report-error :invalid-main-instance-page
(str/format "Main instance page of component %s is not valid" (:component-id shape))
shape file page))))))
(defn validate-component-not-main-head
"Validate shape is a not-main instance head, component exists and its main-instance does not point to this shape."
[shape file page libraries report-error]
(when (some? (:main-instance shape))
(report-error :component-not-main
(str/format "Shape not expected to be main instance")
shape file page))
(let [component (resolve-component shape file libraries {:include-deleted? true})]
(if (nil? component)
(report-error :component-not-found
(str/format "Component %s not found in file" (:component-id shape) (:component-file shape))
shape file page)
(do
(when (and (= (:main-instance-id component) (:id shape))
(= (:main-instance-page component) (:id page)))
(report-error :invalid-main-instance
(str/format "Main instance of component %s should not be this shape" (:id component))
shape file page))))))
(defn validate-component-not-main-not-head
"Validate that this shape is not main instance and not head."
[shape file page report-error]
(when (some? (:main-instance shape))
(report-error :component-main
(str/format "Shape not expected to be main instance")
shape file page))
(when (or (some? (:component-id shape))
(some? (:component-file shape)))
(report-error :component-main
(str/format "Shape not expected to be component head")
shape file page)))
(defn validate-component-root
"Validate that this shape is an instance root."
[shape file page report-error]
(when (nil? (:component-root shape))
(report-error :missing-component-root
(str/format "Shape should be component root")
shape file page)))
(defn validate-component-not-root
"Validate that this shape is not an instance root."
[shape file page report-error]
(when (some? (:component-root shape))
(report-error :missing-component-root
(str/format "Shape should not be component root")
shape file page)))
(defn validate-component-ref
"Validate that the referenced shape exists in the near component."
[shape file page libraries report-error]
(let [ref-shape (find-ref-shape file page libraries shape)]
(when (nil? ref-shape)
(report-error :missing-component-root
(str/format "Referenced shape %s not found in near component" (:shape-ref shape))
shape file page))))
(defn validate-component-not-ref
"Validate that this shape does not reference other one."
[shape file page report-error]
(when (some? (:shape-ref shape))
(report-error :shape-ref-in-main
(str/format "Shape inside main instance should not have shape-ref")
shape file page)))
(defn validate-shape-main-root-top
"Root shape of a top main instance
:main-instance
:component-id
:component-file
:component-root"
[shape file page libraries report-error]
(validate-component-main-head shape file page libraries report-error)
(validate-component-root shape file page report-error)
(validate-component-not-ref shape file page report-error)
(doseq [child-id (:shapes shape)]
(validate-shape child-id file page libraries :context :main-top :report-error report-error)))
(defn validate-shape-main-root-nested
"Root shape of a nested main instance
:main-instance
:component-id
:component-file"
[shape file page libraries report-error]
(validate-component-main-head shape file page libraries report-error)
(validate-component-not-root shape file page report-error)
(validate-component-not-ref shape file page report-error)
(doseq [child-id (:shapes shape)]
(validate-shape child-id file page libraries :context :main-nested :report-error report-error)))
(defn validate-shape-copy-root-top
"Root shape of a top copy instance
:component-id
:component-file
:component-root
:shape-ref"
[shape file page libraries report-error]
(validate-component-not-main-head shape file page libraries report-error)
(validate-component-root shape file page report-error)
(validate-component-ref shape file page libraries report-error)
(doseq [child-id (:shapes shape)]
(validate-shape child-id file page libraries :context :copy-top :report-error report-error)))
(defn validate-shape-copy-root-nested
"Root shape of a nested copy instance
:component-id
:component-file
:shape-ref"
[shape file page libraries report-error]
(validate-component-not-main-head shape file page libraries report-error)
(validate-component-not-root shape file page report-error)
(validate-component-ref shape file page libraries report-error)
(doseq [child-id (:shapes shape)]
(validate-shape child-id file page libraries :context :copy-nested :report-error report-error)))
(defn validate-shape-main-not-root
"Not-root shape of a main instance
(not any attribute)"
[shape file page libraries report-error]
(validate-component-not-main-not-head shape file page report-error)
(validate-component-not-root shape file page report-error)
(validate-component-not-ref shape file page report-error)
(doseq [child-id (:shapes shape)]
(validate-shape child-id file page libraries :context :main-any :report-error report-error)))
(defn validate-shape-copy-not-root
"Not-root shape of a copy instance
:shape-ref"
[shape file page libraries report-error]
(validate-component-not-main-not-head shape file page report-error)
(validate-component-not-root shape file page report-error)
(validate-component-ref shape file page libraries report-error)
(doseq [child-id (:shapes shape)]
(validate-shape child-id file page libraries :context :copy-any :report-error report-error)))
(defn validate-shape-not-component
"Shape is not in a component or is a fostered children
(not any attribute)"
[shape file page libraries report-error]
(validate-component-not-main-not-head shape file page report-error)
(validate-component-not-root shape file page report-error)
(validate-component-not-ref shape file page report-error)
(doseq [child-id (:shapes shape)]
(validate-shape child-id file page libraries :context :not-component :report-error report-error)))
(defn validate-shape
"Validate referential integrity and semantic coherence of a shape and all its children.
The context is the situation of the parent in respect to components:
:not-component
:main-top
:main-nested
:copy-top
:copy-nested
:main-any
:copy-any"
[shape-id file page libraries & {:keys [context throw? report-error]
:or {context :not-component throw? false}}]
(let [shape (ctst/get-shape page shape-id)
errors (volatile! [])
report-error (or report-error
(fn [code msg shape file page]
(if throw?
(throw (ex-info msg {:type :validation
:code code
:hint msg
::explain (str/format "file %s\npage %s\nshape %s"
(:id file)
(:id page)
(:id shape))}))
(vswap! errors conj {:hint msg
:code code
:shape shape
:file file
:page page}))))]
(dm/assert! (str/format "Shape %s not found" shape-id) (some? shape))
(validate-parent-children shape file page report-error)
(if (ctk/main-instance? shape)
(if (ctk/instance-root? shape)
(if (not= context :not-component)
(report-error :root-main-not-allowed
(str/format "Root main component not allowed inside other component")
shape file page)
(validate-shape-main-root-top shape file page libraries report-error))
(if (= context :not-component)
(report-error :nested-main-not-allowed
(str/format "Nested main component only allowed inside other component")
shape file page)
(validate-shape-main-root-nested shape file page libraries report-error)))
(if (ctk/instance-head? shape)
(if (ctk/instance-root? shape)
(if (not= context :not-component)
(report-error :root-copy-not-allowed
(str/format "Root copy not allowed inside other component")
shape file page)
(validate-shape-copy-root-top shape file page libraries report-error))
(if (= context :not-component)
(report-error :nested-copy-not-allowed
(str/format "Nested copy only allowed inside other component")
shape file page)
(validate-shape-copy-root-nested shape file page libraries report-error)))
(if (ctn/component-main? (:objects page) shape)
(if-not (#{:main-top :main-nested :main-any} context)
(report-error :not-head-main-not-allowed
(str/format "Non-root main only allowed inside a main component")
shape file page)
(validate-shape-main-not-root shape file page libraries report-error))
(if (ctk/in-component-copy? shape)
(if-not (#{:copy-top :copy-nested :copy-any} context)
(report-error :not-head-copy-not-allowed
(str/format "Non-root copy only allowed inside a copy")
shape file page)
(validate-shape-copy-not-root shape file page libraries report-error))
(if (#{:main-top :main-nested :main-any} context)
(report-error :not-component-not-allowed
(str/format "Not compoments are not allowed inside a main")
shape file page)
(validate-shape-not-component shape file page libraries report-error))))))
@errors))

View file

@ -18,20 +18,20 @@
[shape] [shape]
(t/is (some? (:shape-ref shape))) (t/is (some? (:shape-ref shape)))
(t/is (some? (:component-id shape))) (t/is (some? (:component-id shape)))
(t/is (= (:component-root? shape) true))) (t/is (= (:component-root shape) true)))
(defn check-instance-subroot (defn check-instance-subroot
[shape] [shape]
(t/is (some? (:shape-ref shape))) (t/is (some? (:shape-ref shape)))
(t/is (some? (:component-id shape))) (t/is (some? (:component-id shape)))
(t/is (nil? (:component-root? shape)))) (t/is (nil? (:component-root shape))))
(defn check-instance-child (defn check-instance-child
[shape] [shape]
(t/is (some? (:shape-ref shape))) (t/is (some? (:shape-ref shape)))
(t/is (nil? (:component-id shape))) (t/is (nil? (:component-id shape)))
(t/is (nil? (:component-file shape))) (t/is (nil? (:component-file shape)))
(t/is (nil? (:component-root? shape)))) (t/is (nil? (:component-root shape))))
(defn check-instance-inner (defn check-instance-inner
[shape] [shape]
@ -44,7 +44,7 @@
(t/is (nil? (:shape-ref shape))) (t/is (nil? (:shape-ref shape)))
(t/is (nil? (:component-id shape))) (t/is (nil? (:component-id shape)))
(t/is (nil? (:component-file shape))) (t/is (nil? (:component-file shape)))
(t/is (nil? (:component-root? shape))) (t/is (nil? (:component-root shape)))
(t/is (nil? (:remote-synced? shape))) (t/is (nil? (:remote-synced? shape)))
(t/is (nil? (:touched shape)))) (t/is (nil? (:touched shape))))

View file

@ -7,6 +7,7 @@
(ns debug (ns debug
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.logging :as l] [app.common.logging :as l]
[app.common.math :as mth] [app.common.math :as mth]
[app.common.transit :as t] [app.common.transit :as t]
@ -421,6 +422,23 @@
[read-only?] [read-only?]
(st/emit! (dw/set-workspace-read-only read-only?))) (st/emit! (dw/set-workspace-read-only read-only?)))
;; Validation and repair
(defn ^:export validate
([] (validate nil))
([shape-id]
(let [file (assoc (get @st/state :workspace-file)
:data (get @st/state :workspace-data))
page (dm/get-in file [:data :pages-index (get @st/state :current-page-id)])
libraries (get @st/state :workspace-libraries)
errors (ctf/validate-shape (or shape-id uuid/zero)
file
page
libraries)]
(clj->js errors))))
(defn ^:export fix-orphan-shapes (defn ^:export fix-orphan-shapes
[] []
(st/emit! (dw/fix-orphan-shapes))) (st/emit! (dw/fix-orphan-shapes)))
@ -433,12 +451,10 @@
[id shape-ref] [id shape-ref]
(st/emit! (dw/set-shape-ref id shape-ref))) (st/emit! (dw/set-shape-ref id shape-ref)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SNAPSHOTS ;; SNAPSHOTS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn ^:export list-available-snapshots (defn ^:export list-available-snapshots
[file-id] [file-id]
(let [file-id (d/parse-uuid file-id)] (let [file-id (d/parse-uuid file-id)]
@ -453,7 +469,6 @@
(update row :id str))))] (update row :id str))))]
(js/console.table (clj->js result)))))))) (js/console.table (clj->js result))))))))
(defn ^:export take-snapshot (defn ^:export take-snapshot
[file-id label] [file-id label]
(let [file-id (d/parse-uuid file-id)] (let [file-id (d/parse-uuid file-id)]