;; The wire/storage boundary — the one place a declared shape becomes a JS
;; object, or is read back out of one.
;;
;; WHY IT IS A NAMESPACE. Every JSON shape this app persists or sends is a
;; contract: PROTOCOL.md fixes the key ORDER of the wire ones, and the storage
;; ones are what an existing user's browser already holds. Before this file,
;; each shape was spelled out at its call site as a `j/ordered "a" x "b" y …`
;; and re-read field by field with `unchecked-get` somewhere else, so the
;; contract existed only as the sum of two lists of string literals that had to
;; be kept in step by eye.
;;
;; A shape is now an ordered vector of [clojure-key wire-key] pairs. `encode`
;; walks it, doing exactly the sequential `unchecked-set` that
;; ardegazu.rooms.js/ordered does — so the bytes do not move, and neither does
;; the eight-pair `#js {}` hash-order cliff, which this construct never had.
;; What changes is that key order is a VALUE: printable, diffable, and testable
;; without serialising anything.
;;
;; `decode` goes the other way and returns a Clojure map — never a half-JS one.
;; It drops absent fields (JS `undefined` and `null` are both nil to CLJS), and
;; it does NOT validate: each caller states its own drop-vs-clamp rule next to
;; the rule it implements, instead of the policy emerging from a compound `or`
;; two hundred lines from the clamp.
;;
;; ABOVE THIS FILE, nil is the only absence, so plain Clojure predicates are
;; safe. The JS forms live here and stay here: `arr?` is `Array.isArray`, NOT
;; `array?` — the latter compiles to `instance? js/Array`, which is
;; realm-sensitive, and in a validator that difference IS the wire contract.
(ns sueta.wire
(:require [ardegazu.rooms.js :as j]))
;; ---- the JS forms ----------------------------------------------------------
(defn ^boolean arr? [x] (js/Array.isArray x))
(defn ^boolean str? [x] (identical? "string" (js* "typeof ~{}" x)))
(defn ^boolean num? [x] (identical? "number" (js* "typeof ~{}" x)))
(defn ^boolean finite-num? [x] (and (num? x) (js/Number.isFinite x)))
(defn non-empty
"The string, or nil if it is absent, not a string, or \"\" — the JS truthiness
test the originals wrote as a bare `if (s)` on a string-or-missing field.
Returning the value rather than a boolean is what lets it feed `encode`
directly, where nil means 'omit this key'."
[x]
(when (and (str? x) (pos? (.-length ^string x))) x))
(defn oget
"TS's `o?.k`: nil for a nullish holder. `j/truthy?` is load-bearing here — it
is what makes a null/undefined holder safe to read through, exactly as the
original's `x && x.k` did. CLJS already reads `undefined` as nil for `nil?`
and `some?`, so the result needs no further normalising."
[o k]
(when (j/truthy? o)
(unchecked-get o k)))
;; ---- shapes ----------------------------------------------------------------
(defn encode
"A fresh JS object carrying `shape`'s fields, set in declaration order.
A nil value is omitted — that is how an optional field stays optional."
[shape m]
(let [o (js-obj)]
(doseq [[k wire] shape]
(when-some [v (get m k)] (unchecked-set o wire v)))
o))
(defn encode-all [shape ms]
(into-array (map #(encode shape %) ms)))
(defn decode
"`shape`'s fields off a JS object, as a Clojure map; nil for a nullish input.
Absent fields are absent from the map, so `nil` is the only absence above."
[shape o]
(when (j/truthy? o)
(reduce (fn [m [k wire]]
(let [v (unchecked-get o wire)]
(if (some? v) (assoc m k v) m)))
{}
shape)))
(defn decode-all [shape xs]
(map #(decode shape %) xs))