🚧 WIP Step 12.1, Improve case validation

AI-assisted-by: muse-spark, glm-5.3
This commit is contained in:
Miguel de Benito Delgado 2026-09-27 18:31:52 +02:00
parent 9e5620600f
commit 3b8b417045
2 changed files with 67 additions and 16 deletions

View File

@ -63,10 +63,14 @@
[:dpr {:optional true} ::sm/positive-safe-number]])
(def schema:view
"Camera placement plus optional viewport override. All numbers are finite:
unbounded `number?` accepts Infinity, which JSON cannot carry and the
renderer cannot use. Bounds come from `::sm/safe-number`, whose min/max
also rejects NaN and infinities."
[:map {:closed true}
[:scale [:and number? pos?]]
[:x number?]
[:y number?]
[:scale ::sm/positive-safe-number]
[:x ::sm/safe-number]
[:y ::sm/safe-number]
[:viewport {:optional true} schema:viewport]])
(defn resolve-viewport
@ -360,19 +364,41 @@
;; Projection
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- finite-number?
"True for numbers JSON preserves: rejects NaN and infinities, which
`number?` accepts but `JSON.stringify` turns into null."
[value]
(and (number? value)
(js/isFinite value)))
(defn serializable?
"True when `value` is plain data the runner can hand to the browser and
store in results: nil, booleans, numbers, strings, keywords, and maps and
vectors thereof. Records fail: a record reaches the runner as a host
object, not plain data. Functions, atoms and host objects fail."
store in results.
True for nil, booleans, finite numbers, strings, keywords, and vectors, and
maps with strings or keyword keys.
NaN, infinities and non-string map keys fail: JSON does not carry them
losslessly.
Records fail: a record reaches the runner as a host object, not plain data.
Functions, atoms and host objects fail.
Keywords are encoded with `encode-id`: the planned `app.common.json` converter
renders keyword values by unqualified name, so `:rects/load` would arrive as
`\"load\"`."
[value]
(cond
(or (nil? value) (boolean? value) (number? value)
(or (nil? value) (boolean? value) (finite-number? value)
(string? value) (keyword? value))
true
(and (map? value) (not (record? value)))
(every? (fn [[k v]] (and (serializable? k) (serializable? v))) value)
(every? (fn [[k v]]
(and (or (string? k) (keyword? k))
(serializable? v)))
value)
(vector? value)
(every? serializable? value)
@ -380,6 +406,22 @@
:else
false))
(defn encode-id
"Lossless JSON-wire encoding of a keyword identity: `:rects/load` becomes
`\"rects/load\"`, `:fresh` becomes `\"fresh\"`. `decode-id` inverts it.
The runner (ticket 11) applies this pair at the JSON boundary; nothing
else may turn identities into bare `name` strings."
[id]
(when-not (keyword? id)
(throw (ex-info (str "wire identities are keywords, got: " (pr-str id))
{:type ::invalid-case :id id})))
(subs (str id) 1))
(defn decode-id
"Inverts `encode-id`: `\"rects/load\"` becomes `:rects/load`."
[s]
(keyword s))
(defn project-case
"Plain-data projection of a registered case: internal keys (`:ns`, `:run!`,
`:body-source`) stripped, collection defaults applied."
@ -392,8 +434,9 @@
(update :preparation #(or % {:version 1}))))
(defn check-collected-case!
"Validates a collected case descriptor and returns it unchanged. Rejects
anything that is not plain data: browser-side run functions must never
"Validates a collected case descriptor and returns it unchanged.
Rejects anything that is not plain data: browser-side run functions must never
reach the runner."
[projected]
(when-not (sm/validate schema:collected-case projected)

View File

@ -26,6 +26,7 @@
where `rtx` is the injected runtime and each operation contributes its
plain-data identity to the collected case."
(:require
[app.common.schema :as sm]
[benches.render-wasm.scenes.builder :as sb :include-macros true]
[benches.render-wasm.scenes.core :as core :include-macros true]))
@ -37,12 +38,19 @@
:max-size 100})
(def ^:private schema:params
[:map {:closed true}
[:count [:int {:min 1 :max 100000}]]
[:width pos?]
[:height pos?]
[:min-size pos?]
[:max-size pos?]])
"Workload parameters. Sizes are finite (`pos?` accepts Infinity, which JSON
cannot carry and the generator cannot honor); `:min-size` above `:max-size`
is rejected so parameter sweeps cannot silently measure a reversed range."
[:and
[:map {:closed true}
[:count [:int {:min 1 :max 100000}]]
[:width ::sm/positive-safe-number]
[:height ::sm/positive-safe-number]
[:min-size ::sm/positive-safe-number]
[:max-size ::sm/positive-safe-number]]
[:fn {:error/message "min-size exceeds max-size"}
(fn [{:keys [min-size max-size]}]
(<= min-size max-size))]])
(defn- rect-defaults
"Attribute generators for one rectangle of the workload."