🎉 Add repair functions for variant validation errors (#10768)

* 🎉 Add repair functions for variant validation errors

* 📚 Fix copyright notice
This commit is contained in:
Andrés Moya 2026-08-04 16:53:36 +02:00 committed by GitHub
parent 3fba272848
commit 8e713df5f0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 432 additions and 98 deletions

View File

@ -10,12 +10,14 @@
[app.common.files.changes-builder :as pcb]
[app.common.files.helpers :as cfh]
[app.common.logging :as log]
[app.common.path-names :as cpn]
[app.common.types.component :as ctk]
[app.common.types.components-list :as ctkl]
[app.common.types.container :as ctn]
[app.common.types.file :as ctf]
[app.common.types.pages-list :as ctpl]
[app.common.types.shape :as cts]
[app.common.types.variant :as ctv]
[app.common.uuid :as uuid]))
(log/set-level! :debug)
@ -35,7 +37,7 @@
(assoc :width 0.01)
(assoc :height 0.01)
(cts/setup-rect)))]
(log/dbg :hint "repairing shape :invalid-geometry" :id (:id shape) :name (:name shape) :page-id page-id)
(log/debug :hint "repairing shape :invalid-geometry" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
(pcb/update-shapes [(:id shape)] repair-shape))))
@ -48,7 +50,7 @@
(log/debug :hint " -> set to " :parent-id uuid/zero)
(assoc shape :parent-id uuid/zero))]
(log/dbg :hint "repairing shape :parent-not-found" :id (:id shape) :name (:name shape) :page-id page-id)
(log/debug :hint "repairing shape :parent-not-found" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
(pcb/update-shapes [(:id shape)] repair-shape))))
@ -61,7 +63,7 @@
(log/debug :hint " -> add children to" :parent-id (:id parent-shape))
(update parent-shape :shapes conj (:id shape)))]
(log/dbg :hint "repairing shape :child-not-in-parent" :id (:id shape) :name (:name shape) :page-id page-id)
(log/debug :hint "repairing shape :child-not-in-parent" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
(pcb/update-shapes [(:parent-id shape)] repair-shape))))
@ -74,7 +76,7 @@
(log/debug :hint " -> remove duplicated children")
(update shape :shapes distinct))]
(log/dbg :hint "repairing shape :duplicated-children" :id (:id shape) :name (:name shape) :page-id page-id)
(log/debug :hint "repairing shape :duplicated-children" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
(pcb/update-shapes [(:id shape)] repair-shape))))
@ -86,14 +88,14 @@
(log/debug :hint " -> remove child" :child-id (:child-id args))
(update parent-shape :shapes (fn [shapes]
(d/removev #(= (:child-id args) %) shapes))))]
(log/dbg :hint "repairing shape :child-not-found" :id (:id shape) :name (:name shape) :page-id page-id)
(log/debug :hint "repairing shape :child-not-found" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
(pcb/update-shapes [(:id shape)] repair-shape))))
(defmethod repair-error :invalid-parent
[_ {:keys [shape page-id args] :as error} file-data _]
(log/dbg :hint "repairing shape :invalid-parent" :id (:id shape) :name (:name shape) :page-id page-id)
(log/debug :hint "repairing shape :invalid-parent" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
(pcb/change-parent (:parent-id args) [shape] nil {:allow-altering-copies true})))
@ -109,7 +111,7 @@
(log/debug :hint " -> set to " :frame-id frame-id)
(assoc shape :frame-id frame-id)))]
(log/dbg :hint "repairing shape :frame-not-found" :id (:id shape) :name (:name shape) :page-id page-id)
(log/debug :hint "repairing shape :frame-not-found" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
(pcb/update-shapes [(:id shape)] repair-shape))))
@ -125,7 +127,7 @@
(log/debug :hint " -> set to " :frame-id frame-id)
(assoc shape :frame-id frame-id)))]
(log/dbg :hint "repairing shape :invalid-frame" :id (:id shape) :name (:name shape) :page-id page-id)
(log/debug :hint "repairing shape :invalid-frame" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
(pcb/update-shapes [(:id shape)] repair-shape))))
@ -138,7 +140,7 @@
(log/debug :hint " -> set :main-instance")
(assoc shape :main-instance true))]
(log/dbg :hint "repairing shape :component-not-main" :id (:id shape) :name (:name shape) :page-id page-id)
(log/debug :hint "repairing shape :component-not-main" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
(pcb/update-shapes [(:id shape)] repair-shape))))
@ -155,7 +157,7 @@
;; (log/warn :hint " -> CANNOT REPAIR THIS AUTOMATICALLY.")
;; shape)]
(log/dbg :hint "repairing shape :component-main-external" :id (:id shape) :name (:name shape) :page-id page-id)
(log/debug :hint "repairing shape :component-main-external" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
(pcb/update-shapes [(:id shape)] repair-shape))))
@ -174,7 +176,7 @@
;; (log/warn :hint " -> CANNOT REPAIR THIS AUTOMATICALLY.")
;; shape)]
(log/dbg :hint "repairing shape :component-not-found" :id (:id shape) :name (:name shape) :page-id page-id)
(log/debug :hint "repairing shape :component-not-found" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
(pcb/update-shapes shape-ids repair-shape))))
@ -194,7 +196,7 @@
(log/debug :hint " -> detach shape" :shape-id (:id shape))
(ctk/detach-shape shape))]
(log/dbg :hint "repairing shape :invalid-main-instance-id" :id (:id shape) :name (:name shape) :page-id page-id)
(log/debug :hint "repairing shape :invalid-main-instance-id" :id (:id shape) :name (:name shape) :page-id page-id)
(if (and (some? component) (not (:deleted component)))
(-> (pcb/empty-changes nil page-id)
(pcb/with-library-data file-data)
@ -211,7 +213,7 @@
;; Assign main instance in the component to current shape
(log/debug :hint " -> assign main-instance-page" :component-id (:id component))
(assoc component :main-instance-page page-id))]
(log/dbg :hint "repairing shape :invalid-main-instance-page" :id (:id shape) :name (:name shape) :page-id page-id)
(log/debug :hint "repairing shape :invalid-main-instance-page" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-library-data file-data)
(pcb/update-component (:component-id shape) repair-component))))
@ -224,7 +226,7 @@
(log/warn :hint " -> CANNOT REPAIR THIS AUTOMATICALLY.")
shape)]
(log/dbg :hint "repairing shape :invalid-main-instance" :id (:id shape) :name (:name shape) :page-id page-id)
(log/debug :hint "repairing shape :invalid-main-instance" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
(pcb/update-shapes [(:id shape)] repair-shape))))
@ -237,7 +239,7 @@
(log/debug :hint " -> unset :main-instance")
(dissoc shape :main-instance))]
(log/dbg :hint "repairing shape :component-main" :id (:id shape) :name (:name shape) :page-id page-id)
(log/debug :hint "repairing shape :component-main" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
(pcb/update-shapes [(:id shape)] repair-shape))))
@ -250,7 +252,7 @@
(log/debug :hint " -> set :component-root")
(assoc shape :component-root true))]
(log/dbg :hint "repairing shape :should-be-component-root" :id (:id shape) :name (:name shape) :page-id page-id)
(log/debug :hint "repairing shape :should-be-component-root" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
(pcb/update-shapes [(:id shape)] repair-shape))))
@ -263,7 +265,7 @@
(log/debug :hint " -> unset :component-root")
(dissoc shape :component-root))]
(log/dbg :hint "repairing shape :should-not-be-component-root" :id (:id shape) :name (:name shape) :page-id page-id)
(log/debug :hint "repairing shape :should-not-be-component-root" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
(pcb/update-shapes [(:id shape)] repair-shape))))
@ -310,7 +312,7 @@
;; If the shape still refers to the remote component, try to find the corresponding near one
;; and link to it. If not, detach the shape.
(log/dbg :hint "repairing shape :ref-shape-not-found" :id (:id shape) :name (:name shape) :page-id page-id)
(log/debug :hint "repairing shape :ref-shape-not-found" :id (:id shape) :name (:name shape) :page-id page-id)
(if (some? matching-shape)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
@ -329,7 +331,7 @@
(log/debug :hint " -> unhead shape")
(ctk/unhead-shape shape))]
(log/dbg :hint "repairing shape :shape-ref-is-not-head" :id (:id shape) :name (:name shape) :page-id page-id)
(log/debug :hint "repairing shape :shape-ref-is-not-head" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
(pcb/update-shapes [(:id shape)] repair-shape))))
@ -354,7 +356,7 @@
(nil? (:component-file args))
(dissoc :component-file)))]
(log/dbg :hint "repairing shape :component-id-mismatch" :id (:id shape) :name (:name shape) :page-id page-id)
(log/debug :hint "repairing shape :component-id-mismatch" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
(pcb/update-shapes [(:id shape)] repair-shape))))
@ -367,7 +369,7 @@
(log/debug :hint " -> reroot shape")
(ctk/rehead-shape shape (:component-file args) (:component-id args)))]
(log/dbg :hint "repairing shape :shape-ref-is-head" :id (:id shape) :name (:name shape) :page-id page-id)
(log/debug :hint "repairing shape :shape-ref-is-head" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
(pcb/update-shapes [(:id shape)] repair-shape))))
@ -396,7 +398,7 @@
(assoc acc k v)))
{}
objects)))))]
(log/dbg :hint "repairing component :shape-ref-cycle" :id (:id shape) :name (:name shape))
(log/debug :hint "repairing component :shape-ref-cycle" :id (:id shape) :name (:name shape))
(-> (pcb/empty-changes nil nil)
(pcb/with-library-data file-data)
(pcb/update-component (:id shape) repair-component))))
@ -409,7 +411,7 @@
(log/debug :hint " -> unset :shape-ref")
(dissoc shape :shape-ref))]
(log/dbg :hint "repairing shape :shape-ref-in-main" :id (:id shape) :name (:name shape) :page-id page-id)
(log/debug :hint "repairing shape :shape-ref-in-main" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
(pcb/update-shapes [(:id shape)] repair-shape))))
@ -422,7 +424,7 @@
(log/debug :hint " -> unset :component-root")
(dissoc shape :component-root))]
(log/dbg :hint "repairing shape :root-main-not-allowed" :id (:id shape) :name (:name shape) :page-id page-id)
(log/debug :hint "repairing shape :root-main-not-allowed" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
(pcb/update-shapes [(:id shape)] repair-shape))))
@ -435,7 +437,7 @@
(log/debug :hint " -> set :component-root")
(assoc shape :component-root true))]
(log/dbg :hint "repairing shape :nested-main-not-allowed" :id (:id shape) :name (:name shape) :page-id page-id)
(log/debug :hint "repairing shape :nested-main-not-allowed" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
(pcb/update-shapes [(:id shape)] repair-shape)
@ -449,7 +451,7 @@
(log/debug :hint " -> unset :component-root")
(dissoc shape :component-root))]
(log/dbg :hint "repairing shape :root-copy-not-allowed" :id (:id shape) :name (:name shape) :page-id page-id)
(log/debug :hint "repairing shape :root-copy-not-allowed" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
(pcb/update-shapes [(:id shape)] repair-shape))))
@ -462,7 +464,7 @@
(log/debug :hint " -> set :component-root")
(assoc shape :component-root true))]
(log/dbg :hint "repairing shape :nested-copy-not-allowed" :id (:id shape) :name (:name shape) :page-id page-id)
(log/debug :hint "repairing shape :nested-copy-not-allowed" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
(pcb/update-shapes [(:id shape)] repair-shape))))
@ -475,7 +477,7 @@
(log/debug :hint " -> detach shape" :shape-id (:id shape))
(ctk/detach-shape shape))]
(log/dbg :hint "repairing shape :not-head-main-not-allowed" :id (:id shape) :name (:name shape) :page-id page-id)
(log/debug :hint "repairing shape :not-head-main-not-allowed" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
(pcb/update-shapes [(:id shape)] repair-shape))))
@ -488,7 +490,7 @@
(log/debug :hint " -> detach shape" :shape-id (:id shape))
(ctk/detach-shape shape))]
(log/dbg :hint "repairing shape :not-head-copy-not-allowed" :id (:id shape) :name (:name shape) :page-id page-id)
(log/debug :hint "repairing shape :not-head-copy-not-allowed" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
(pcb/update-shapes [(:id shape)] repair-shape))))
@ -501,7 +503,7 @@
(log/warn :hint " -> CANNOT REPAIR THIS AUTOMATICALLY.")
shape)]
(log/dbg :hint "repairing shape :not-component-not-allowed" :id (:id shape) :name (:name shape) :page-id page-id)
(log/debug :hint "repairing shape :not-component-not-allowed" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
(pcb/update-shapes [(:id shape)] repair-shape))))
@ -520,7 +522,7 @@
:r3 0
:r4 0))]
(log/dbg :hint "repairing shape :instance-head-not-frame" :id (:id shape) :name (:name shape) :page-id page-id)
(log/debug :hint "repairing shape :instance-head-not-frame" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
(pcb/update-shapes [(:id shape)] repair-shape))))
@ -538,7 +540,7 @@
(log/debug :hint " -> remove :objects")
(dissoc component :objects))))]
(log/dbg :hint "repairing component :component-nil-objects-not-allowed" :id (:id component) :name (:name component))
(log/debug :hint "repairing component :component-nil-objects-not-allowed" :id (:id component) :name (:name component))
(-> (pcb/empty-changes nil)
(pcb/with-library-data file-data)
(pcb/update-component (:id component) repair-component))))
@ -554,7 +556,7 @@
(dissoc component :objects))
component))]
(log/dbg :hint "repairing component :non-deleted-component-cannot-have-objects" :id (:id component) :name (:name component))
(log/debug :hint "repairing component :non-deleted-component-cannot-have-objects" :id (:id component) :name (:name component))
(-> (pcb/empty-changes nil)
(pcb/with-library-data file-data)
(pcb/update-component (:id component) repair-component))))
@ -567,7 +569,7 @@
(log/debug :hint " -> add :content-group to :touched-groups")
(update shape :touched ctk/set-touched-group :content-group))]
(log/dbg :hint "repairing shape :invalid-text-touched" :id (:id shape) :name (:name shape) :page-id page-id)
(log/debug :hint "repairing shape :invalid-text-touched" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
(pcb/update-shapes [(:id shape)] repair-shape))))
@ -580,7 +582,7 @@
(log/debug :hint " -> remove swap-slot")
(ctk/remove-swap-slot shape))]
(log/dbg :hint "repairing shape :misplaced-slot" :id (:id shape) :name (:name shape) :page-id page-id)
(log/debug :hint "repairing shape :misplaced-slot" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
(pcb/update-shapes [(:id shape)] repair-shape))))
@ -603,13 +605,11 @@
(log/debug :hint " -> remove swap-slot" :child-id (:id shape))
(ctk/remove-swap-slot shape))]
(log/dbg :hint "repairing shape :duplicated-slot" :id (:id shape) :name (:name shape) :page-id page-id)
(log/debug :hint "repairing shape :duplicated-slot" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
(pcb/update-shapes (map :id child-with-duplicate) repair-shape))))
(defmethod repair-error :component-duplicate-slot
[_ {:keys [shape] :as error} file-data _]
(let [main-shape (get-in shape [:objects (:main-instance-id shape)])
@ -633,7 +633,7 @@
(:objects component))]
(assoc component :objects objects)))]
(log/dbg :hint "repairing component :component-duplicated-slot" :id (:id shape) :name (:name shape))
(log/debug :hint "repairing component :component-duplicated-slot" :id (:id shape) :name (:name shape))
(-> (pcb/empty-changes nil)
(pcb/with-library-data file-data)
(pcb/update-component (:id shape) repair-component))))
@ -649,50 +649,139 @@
(ctk/set-swap-slot shape slot))
shape)))]
(log/dbg :hint "repairing shape :missing-slot" :id (:id shape) :name (:name shape) :page-id page-id)
(log/debug :hint "repairing shape :missing-slot" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
(pcb/update-shapes [(:id shape)] repair-shape))))
(defmethod repair-error :not-a-variant
[_ error file _]
(log/error :hint "Variant error code, we don't want to auto repair it for now" :code (:code error))
file)
(defmethod repair-error :main-instance-not-a-variant
[_ {:keys [shape page-id args]} file-data _]
(let [repair-shape
(fn [shape]
(let [variant-id (:variant-id args)]
;; Set the desired variant-id
(log/debug :hint (str " -> set variant-id to " variant-id))
(assoc shape :variant-id variant-id)))]
(defmethod repair-error :invalid-variant-id
[_ error file _]
(log/error :hint "Variant error code, we don't want to auto repair it for now" :code (:code error))
file)
(log/debug :hint "repairing shape :main-instance-not-a-variant" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
(pcb/update-shapes [(:id shape)] repair-shape))))
(defmethod repair-error :main-instance-invalid-variant-id
[_ {:keys [shape page-id args]} file-data _]
(let [repair-shape
(fn [shape]
(let [variant-id (:variant-id args)]
;; Set the desired variant-id
(log/debug :hint (str " -> set variant-id to " variant-id))
(assoc shape
:variant-id variant-id)))]
(log/debug :hint "repairing shape :main-instance-invalid-variant-id" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
(pcb/update-shapes [(:id shape)] repair-shape))))
(defmethod repair-error :invalid-variant-properties
[_ error file _]
(log/error :hint "Variant error code, we don't want to auto repair it for now" :code (:code error))
file)
[_ {:keys [shape page-id args]} file-data _]
(let [prop-names (:prop-names args)
component (get-in file-data [:components (:component-id shape)])
prop-values (into {} (map (juxt :name :value)) (:variant-properties component))
properties' (mapv (fn [name] {:name name :value (get prop-values name "")}) prop-names)
variant-name (ctv/properties-to-name properties')
repair-component
(fn [component]
;; Rebuild component properties, removing any extra ones and adding missing ones with empty value
(log/debug :hint " -> rebuild properties" :component-id (:id component) :prop-names (str prop-names))
(assoc component :variant-properties properties'))
repair-shape
(fn [shape]
(log/debug :hint " -> set variant-name" :variant-name variant-name)
(assoc shape :variant-name variant-name))]
(log/debug :hint "repairing shape :invalid-variant-properties" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
(pcb/with-library-data file-data)
(pcb/update-component (:component-id shape) repair-component)
(pcb/update-shapes [(:id shape)] repair-shape))))
(defmethod repair-error :variant-not-main
[_ error file _]
(log/error :hint "Variant error code, we don't want to auto repair it for now" :code (:code error))
file)
[_ {:keys [shape page-id]} file-data _]
(let [page (ctpl/get-page file-data page-id)
shape-ids (cfh/get-children-ids-with-self (:objects page) (:id shape))]
(log/debug :hint "repairing shape :variant-not-main" :id (:id shape) :name (:name shape) :page-id page-id)
(log/debug :hint " -> delete shapes" :shape-ids shape-ids)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
(pcb/remove-objects shape-ids))))
(defmethod repair-error :parent-not-variant
[_ error file _]
(log/error :hint "Variant error code, we don't want to auto repair it for now" :code (:code error))
file)
[_ {:keys [shape page-id]} file-data _]
(let [parent-id (:parent-id shape)
repair-fn
(fn [parent]
(log/debug :hint " -> set :is-variant-container true")
(assoc parent :is-variant-container true))]
(log/debug :hint "repairing shape :parent-not-variant" :id (:id shape) :name (:name shape) :parent-id parent-id :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
(pcb/update-shapes [parent-id] repair-fn))))
(defmethod repair-error :variant-bad-name
[_ error file _]
(log/error :hint "Variant error code, we don't want to auto repair it for now" :code (:code error))
file)
(defmethod repair-error :variant-main-bad-name
[_ {:keys [shape page-id args]} file-data _]
(let [repair-fn
(fn [shape]
(log/debug :hint " -> set :name" :name (:variant-name args))
(assoc shape :name (:variant-name args)))]
(log/debug :hint "repairing shape :variant-main-bad-name" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
(pcb/update-shapes [(:id shape)] repair-fn))))
(defmethod repair-error :variant-bad-variant-name
[_ error file _]
(log/error :hint "Variant error code, we don't want to auto repair it for now" :code (:code error))
file)
(defmethod repair-error :variant-main-bad-variant-name
[_ {:keys [shape page-id]} file-data _]
(let [component (get-in file-data [:components (:component-id shape)])
variant-name (ctv/properties-to-name (:variant-properties component))
repair-fn
(fn [shape]
(log/debug :hint " -> set :variant-name" :variant-name variant-name)
(assoc shape :variant-name variant-name))]
(log/dbg :hint "repairing shape :variant-main-bad-variant-name" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
(pcb/update-shapes [(:id shape)] repair-fn))))
(defmethod repair-error :variant-component-bad-name
[_ error file _]
(log/error :hint "Variant error code, we don't want to auto repair it for now" :code (:code error))
file)
[_ {:keys [shape page-id args]} file-data _]
(let [[path name] (cpn/split-group-name (:variant-container-name args))
repair-fn
(fn [component]
(log/debug :hint " -> set :path and :name" :path path :name name)
(assoc component :path path :name name))]
(log/dbg :hint "repairing shape :variant-component-bad-name" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-library-data file-data)
(pcb/update-component (:component-id shape) repair-fn))))
(defmethod repair-error :variant-component-bad-id
[_ {:keys [shape page-id args]} file-data _]
(let [repair-shape
(fn [shape]
(let [variant-id (:variant-id args)]
;; Set the desired variant-id
(log/debug :hint (str " -> set variant-id to " variant-id))
(assoc shape
:variant-id variant-id)))]
(log/debug :hint "repairing shape :variant-component-bad-id" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
(pcb/update-shapes [(:id shape)] repair-shape))))
(defmethod repair-error :default
[_ error file _]
@ -701,7 +790,7 @@
(defn repair-file
[{:keys [data id] :as file} libraries errors]
(log/dbg :hint "repairing file" :id (str id) :errors (count errors))
(log/debug :hint "repairing file" :id (str id) :errors (count errors))
(let [{:keys [redo-changes]}
(reduce (fn [changes error]
(pcb/concat-changes changes

View File

@ -65,13 +65,13 @@
:misplaced-slot
:missing-slot
:shape-ref-cycle
:not-a-variant
:invalid-variant-id
:main-instance-not-a-variant
:main-instance-invalid-variant-id
:invalid-variant-properties
:variant-not-main
:parent-not-variant
:variant-bad-name
:variant-bad-variant-name
:variant-main-bad-name
:variant-main-bad-variant-name
:variant-component-bad-name
:variant-component-bad-id})
@ -573,19 +573,23 @@
(run! (fn [child-id]
(when-let [child (get objects child-id)]
(if (not (ctk/is-variant? child))
(report-error :not-a-variant
(str/ffmt "Shape % should be a variant" (:id child))
child file page)
(report-error :main-instance-not-a-variant
(str/ffmt "Main instance shape % should be a variant" (:id child))
child file page
:variant-id shape-id)
(do
(when (not= (:variant-id child) shape-id)
(report-error :invalid-variant-id
(str/ffmt "Variant % has invalid variant-id %" (:id child) (:variant-id child))
child file page))
(report-error :main-instance-invalid-variant-id
(str/ffmt "Main instance in variant % should have the variant-id of the container but has %" (:id child) (:variant-id child))
child file page
:variant-id shape-id))
(when (not= prop-names (cfv/extract-properties-names child file-data))
(report-error :invalid-variant-properties
(str/ffmt "Variant % has invalid properties %" (:id child) (vec prop-names))
child file page))))))
child file page
:prop-names prop-names))))))
shapes)))
(defn- check-variant
"Shape is a variant, so
-it should be a main component
@ -594,9 +598,9 @@
-its name should be the same as its parent's
"
[shape file page]
(let [parent (ctst/get-shape page (:parent-id shape))
component (ctkl/get-component (:data file) (:component-id shape) true)
name (ctv/properties-to-name (:variant-properties component))]
(let [parent (ctst/get-shape page (:parent-id shape))
component (ctkl/get-component (:data file) (:component-id shape) true)
variant-name (ctv/properties-to-name (:variant-properties component))]
(when-not (ctk/main-instance? shape)
(report-error :variant-not-main
(str/ffmt "Variant % is not a main instance" (:id shape))
@ -605,23 +609,26 @@
(report-error :parent-not-variant
(str/ffmt "Variant % has an invalid parent" (:id shape))
shape file page))
(when-not (= name (:variant-name shape))
(report-error :variant-bad-variant-name
(when-not (= variant-name (:variant-name shape))
(report-error :variant-main-bad-variant-name
(str/ffmt "Variant % has an invalid variant-name" (:id shape))
shape file page))
shape file page
:variant-name variant-name))
(when-not (= (:name parent) (:name shape))
(report-error :variant-bad-name
(str/ffmt "Variant % has an invalid name" (:id shape))
shape file page))
(report-error :variant-main-bad-name
(str/ffmt "Main instance inside variant % has an invalid name" (:id shape))
shape file page
:variant-name (:name parent)))
(when-not (= (:name parent) (cpn/merge-path-item (:path component) (:name component)))
(report-error :variant-component-bad-name
(str/ffmt "Component % has an invalid name" (:id shape))
shape file page))
shape file page
:variant-container-name (:name parent)))
(when-not (= (:variant-id component) (:variant-id shape))
(report-error :variant-component-bad-id
(str/ffmt "Variant % has adifferent variant-id than its component" (:id shape))
shape file page))))
shape file page
:variant-id (:variant-id component)))))
(defn- check-shape
"Validate referential integrity and semantic coherence of
@ -740,14 +747,15 @@
-It should have at least one variant property"
[component file]
(let [component-page (ctf/get-component-page (:data file) component)
main-component (if (:deleted component)
main-instance (if (:deleted component)
(dm/get-in component [:objects (:main-instance-id component)])
(ctst/get-shape component-page (:main-instance-id component)))]
(when (and main-component
(not (ctk/is-variant? main-component)))
(report-error :not-a-variant
(str/ffmt "Shape % should be a variant" (:id main-component))
main-component file component-page))))
(when (and main-instance
(not (ctk/is-variant? main-instance)))
(report-error :main-instance-not-a-variant
(str/ffmt "Main instance shape % should be a variant" (:id main-instance))
main-instance file component-page
:variant-id (:variant-id component)))))
(defn- check-main-inside-main
[component file]

View File

@ -13,6 +13,11 @@
[app.common.types.text :as txt]))
(defn add-variant
"Add a variant component to a file with two variants, each with a root shape.
:variant-label [:name Board]
{:root2-label} [:name Board] # [Component :component2-label]
{:root1-label} [:name Board] # [Component :component1-label]
"
[file variant-label component1-label root1-label component2-label root2-label
& {:keys [variant1-params variant2-params]
:or {variant1-params {} variant2-params {}}}]

View File

@ -912,8 +912,10 @@
(let [shape (get objects shape-id)]
(println (str/pad (str (str/repeat " " level)
(when (:main-instance shape) "{")
(when (:is-variant-container shape) "{{")
(:name shape)
(when (:main-instance shape) "}")
(when (:is-variant-container shape) "}}")
(when (seq (:touched shape)) "*")
(when show-ids (str/format " %s" (:id shape))))
{:length 20

View File

@ -0,0 +1,230 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns common-tests.files.repair-test
"Tests for the validate / repair functions in app.common.files.validate
and app.common.files.repair.
The tests generate cases of broken files and check that the validation functions
generate accurate errors, and that the repair functions return the file to
a stable state."
(:require
[app.common.files.repair :as cfr]
[app.common.files.validate :as cfv]
[app.common.test-helpers.components :as thc]
[app.common.test-helpers.files :as thf]
[app.common.test-helpers.ids-map :as thi]
[app.common.test-helpers.shapes :as ths]
[app.common.test-helpers.variants :as thv]
[app.common.uuid :as uuid]
[clojure.test :as t]))
(t/use-fixtures :each thi/test-fixture)
(t/deftest repair-main-instance-not-a-variant
(t/testing "detect and repair a variant component whose root shape is not a variant"
(let [file (-> (thf/sample-file :file1 :page-label :page1)
(thv/add-variant :variant1 :component1 :root1 :component2 :root2)
(ths/update-shape :root1 :variant-id nil))
errors (cfv/validate-file file {})
changes (cfr/repair-file file {} errors)
file' (thf/apply-changes file {:redo-changes changes} :validate? false)
errors' (cfv/validate-file file' {})
root1' (ths/get-shape file' :root1 :page-label :page1)]
(t/is (= 2 (count errors))) ;; There are two different checks that detect the same problem
(t/is (= :main-instance-not-a-variant (:code (first errors))))
(t/is (nil? errors'))
(t/is (= (thi/id :variant1) (:variant-id root1'))))))
(t/deftest repair-invalid-variant-id-variant-component-bad-id
(t/testing "detect and repair a variant component whose variant id does not match the container's id"
(let [file (-> (thf/sample-file :file1 :page-label :page1)
(thv/add-variant :variant1 :component1 :root1 :component2 :root2)
(ths/update-shape :root1 :variant-id (uuid/next)))
errors (cfv/validate-file file {})
changes (cfr/repair-file file {} errors)
file' (thf/apply-changes file {:redo-changes changes} :validate? false)
errors' (cfv/validate-file file' {})
root1' (ths/get-shape file' :root1 :page-label :page1)]
(t/is (= 2 (count errors))) ;; There are two different validation that actually check the same problem
(t/is (= :main-instance-invalid-variant-id (:code (first errors))))
(t/is (= :variant-component-bad-id (:code (second errors))))
(t/is (nil? errors'))
(t/is (= (thi/id :variant1) (:variant-id root1'))))))
(t/deftest repair-invalid-variant-properties
(t/testing "detect and repair a second variant component whose properties do not match the first variant component's properties"
(let [file (-> (thf/sample-file :file1 :page-label :page1)
(thv/add-variant :variant1 :component1 :root1 :component2 :root2)
;; Component1 has ["Property 1", "Property 2"], component2 gets ["Property 1", "Property 3"]
;; This breaks validation: prop-names mismatch (missing "Property 2", extra "Property 3")
(thc/update-component :component1 {:variant-properties [{:name "Property 1" :value "Value1"}
{:name "Property 2" :value "ValueA"}]})
(thc/update-component :component2 {:variant-properties [{:name "Property 1" :value "Value2"}
{:name "Property 3" :value "ValueB"}]})
(ths/update-shape :root1 :variant-name "Value1, ValueA")
(ths/update-shape :root2 :variant-name "Value2, ValueB"))
errors (cfv/validate-file file {})
changes (cfr/repair-file file {} errors)
file' (thf/apply-changes file {:redo-changes changes} :validate? false)
errors' (cfv/validate-file file' {})
comp1' (thc/get-component file' :component1)
comp2' (thc/get-component file' :component2)
root1' (ths/get-shape file' :root1)
root2' (ths/get-shape file' :root2)]
(t/is (= 1 (count errors)))
(t/is (= :invalid-variant-properties (:code (first errors))))
(t/is (nil? errors'))
;; After repair, component1's properties are rebuilt to match component2's property names
;; (the first child in the variant container is root2, so prop-names come from component2)
;; "Property 1" keeps its value, "Property 3" is added with empty value, "Property 2" is removed
(t/is (= [{:name "Property 1" :value "Value1"}
{:name "Property 3" :value ""}]
(:variant-properties comp1')))
(t/is (= "Value1" (:variant-name root1')))
;; Component2 is unchanged (it was the reference for the property names)
(t/is (= [{:name "Property 1" :value "Value2"}
{:name "Property 3" :value "ValueB"}]
(:variant-properties comp2')))
(t/is (= "Value2, ValueB" (:variant-name root2'))))))
(t/deftest repair-variant-not-main
(t/testing "detect and repair a non-main-instance shape inside a variant container"
(let [file (-> (thf/sample-file :file1 :page-label :page1)
(thv/add-variant :variant1 :component1 :root1 :component2 :root2)
;; Add a third child to the variant container with :variant-id but NOT a main-instance
(ths/add-sample-shape :bad-shape
:type :frame
:parent-label :variant1
:variant-id (thi/id :variant1)
:variant-name "")
;; Add a child to the bad shape (to verify the repair deletes it too)
(ths/add-sample-shape :bad-child
:type :rect
:parent-label :bad-shape))
errors (cfv/validate-file file {})
changes (cfr/repair-file file {} errors)
file' (thf/apply-changes file {:redo-changes changes} :validate? false)
errors' (cfv/validate-file file' {})
bad-shape' (ths/get-shape file' :bad-shape)
bad-child' (ths/get-shape file' :bad-child)]
(t/is (= 4 (count errors))) ;; The bad container also triggers other errors
(t/is (= :invalid-variant-properties (:code (nth errors 0))))
(t/is (= :variant-not-main (:code (nth errors 1))))
(t/is (= :variant-component-bad-name (:code (nth errors 2))))
(t/is (= :variant-component-bad-id (:code (nth errors 3))))
(t/is (nil? errors'))
(t/is (nil? bad-shape'))
(t/is (nil? bad-child')))))
(t/deftest repair-parent-not-variant
(t/testing "detect and repair a variant shape whose parent is not a variant-container"
(let [file (-> (thf/sample-file :file1 :page-label :page1)
(thv/add-variant :variant1 :component1 :root1 :component2 :root2)
;; Break the variant container
(ths/update-shape :variant1 :is-variant-container false))
errors (cfv/validate-file file {})
changes (cfr/repair-file file {} errors)
file' (thf/apply-changes file {:redo-changes changes} :validate? false)
errors' (cfv/validate-file file' {})
container' (ths/get-shape file' :variant1)]
(t/is (= 2 (count errors))) ;; The error is detected twice, once for each child of the variant container
(t/is (= :parent-not-variant (:code (first errors))))
(t/is (= :parent-not-variant (:code (second errors))))
(t/is (nil? errors'))
(t/is (true? (:is-variant-container container'))))))
(t/deftest repair-variant-main-bad-name
(t/testing "detect and repair a main instance whose name doesn't match the variant container's name"
(let [file (-> (thf/sample-file :file1 :page-label :page1)
(thv/add-variant :variant1 :component1 :root1 :component2 :root2)
;; Change root1's name so it doesn't match the container
(ths/update-shape :root1 :name "WrongName"))
errors (cfv/validate-file file {})
changes (cfr/repair-file file {} errors)
file' (thf/apply-changes file {:redo-changes changes} :validate? false)
errors' (cfv/validate-file file' {})
root1' (ths/get-shape file' :root1)]
(t/is (= 1 (count errors)))
(t/is (= :variant-main-bad-name (:code (first errors))))
(t/is (nil? errors'))
(t/is (= "Board" (:name root1'))))))
(t/deftest repair-variant-main-bad-variant-name
(t/testing "detect and repair a variant shape whose :variant-name doesn't match the component's properties"
(let [file (-> (thf/sample-file :file1 :page-label :page1)
(thv/add-variant :variant1 :component1 :root1 :component2 :root2)
(thc/update-component :component1 {:variant-properties [{:name "Property 1" :value "Value1"}
{:name "Property 2" :value "ValueA"}]})
(thc/update-component :component2 {:variant-properties [{:name "Property 1" :value "Value2"}
{:name "Property 2" :value "ValueB"}]})
;; Change root1's :variant-name to something wrong
(ths/update-shape :root1 :variant-name "WrongVariantName")
(ths/update-shape :root2 :variant-name "Value2, ValueB"))
errors (cfv/validate-file file {})
changes (cfr/repair-file file {} errors)
file' (thf/apply-changes file {:redo-changes changes} :validate? false)
errors' (cfv/validate-file file' {})
root1' (ths/get-shape file' :root1)]
(t/is (= 1 (count errors)))
(t/is (= :variant-main-bad-variant-name (:code (first errors))))
(t/is (nil? errors'))
(t/is (= "Value1, ValueA" (:variant-name root1'))))))
(t/deftest repair-variant-component-bad-name
(t/testing "detect and repair a variant component whose path/name doesn't match the container name"
(let [file (-> (thf/sample-file :file1 :page-label :page1)
(thv/add-variant :variant1 :component1 :root1 :component2 :root2)
;; Update names to have path structure
(ths/update-shape :variant1 :name "Group / Subgroup / Component")
(ths/update-shape :root1 :name "Group / Subgroup / Component")
(ths/update-shape :root2 :name "Group / Subgroup / Component")
;; Update component paths and names
(thc/update-component :component1 {:path "Group / Subgroup" :name "Component"})
(thc/update-component :component2 {:path "Group / Subgroup" :name "Component"})
;; Break component1's name
(thc/update-component :component1 {:name "WrongName"}))
errors (cfv/validate-file file {})
changes (cfr/repair-file file {} errors)
file' (thf/apply-changes file {:redo-changes changes} :validate? false)
errors' (cfv/validate-file file' {})
comp1' (thc/get-component file' :component1)]
(t/is (= 1 (count errors)))
(t/is (= :variant-component-bad-name (:code (first errors))))
(t/is (nil? errors'))
(t/is (= "Group / Subgroup" (:path comp1')))
(t/is (= "Component" (:name comp1'))))))