Skip to content

Commit 486e896

Browse files
authored
Merge pull request #32 from mycelium-clj/feature/lite-schema
Add lite schema syntax for cell definitions
2 parents c2fa20d + a50c63f commit 486e896

5 files changed

Lines changed: 309 additions & 15 deletions

File tree

src/mycelium/cell.clj

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
(ns mycelium.cell
22
"Cell registry for Mycelium. Cells are registered via `defmethod cell-spec`."
3-
(:require [mycelium.validation :as v]))
3+
(:require [mycelium.schema :as schema]
4+
[mycelium.validation :as v]))
45

56
(defmulti cell-spec
67
"Multimethod-backed cell registry. Dispatches on cell-id keyword,
@@ -35,26 +36,38 @@
3536
(throw (ex-info (str "Cell " id " not found in registry")
3637
{:id id}))))
3738

39+
(defn- output-dispatched?
40+
"Heuristic for defcell/set-cell-schema! (no edge context available):
41+
a map output schema is per-transition if all values are vectors (Malli schema
42+
forms like [:map ...], [:or ...], etc.). If any value is a keyword or map,
43+
it's treated as lite syntax. For manifests, edge context is used instead.
44+
Trade-off: per-transition maps with bare keyword schemas (e.g. {:success :any})
45+
would be misclassified as lite — use vector form [:any] in that rare case."
46+
[output]
47+
(and (map? output) (seq output) (every? vector? (vals output))))
48+
3849
(defn set-cell-schema!
3950
"Sets or overwrites the schema for an already-registered cell.
40-
Validates that the schema is well-formed Malli before updating.
51+
Normalizes lite syntax, validates Malli, then updates.
4152
Throws if the cell is not found or the schema is invalid."
4253
[cell-id schema]
4354
(when-not (cell-spec cell-id)
4455
(throw (ex-info (str "Cell " cell-id " not found in registry")
4556
{:id cell-id})))
46-
(when (:input schema)
47-
(v/validate-malli-schema! (:input schema) (str cell-id " :input")))
48-
(when (:output schema)
49-
(v/validate-output-schema! (:output schema) (str cell-id " :output")))
50-
(swap! cell-overrides update cell-id merge {:schema schema})
51-
schema)
57+
(let [dispatched? (output-dispatched? (:output schema))
58+
schema (schema/normalize-cell-schema schema dispatched?)]
59+
(when (:input schema)
60+
(v/validate-malli-schema! (:input schema) (str cell-id " :input")))
61+
(when (:output schema)
62+
(v/validate-output-schema! (:output schema) (str cell-id " :output")))
63+
(swap! cell-overrides update cell-id merge {:schema schema})
64+
schema))
5265

5366
(defn set-cell-meta!
5467
"Sets metadata overrides (schema, requires) on a registered cell.
5568
The manifest calls this to inject metadata into cells that were registered
56-
without schemas/requires. Validates schema is well-formed.
57-
Throws if the cell is not found."
69+
without schemas/requires. Expects schemas to be pre-normalized.
70+
Validates schema is well-formed. Throws if the cell is not found."
5871
[cell-id {:keys [schema requires] :as meta-map}]
5972
(when-not (cell-spec cell-id)
6073
(throw (ex-info (str "Cell " cell-id " not found in registry")
@@ -110,9 +123,12 @@
110123
([cell-id opts handler-fn]
111124
(let [schema-keys #{:input :output}
112125
opt-keys #{:doc :requires :async?}
113-
schema (when opts
126+
raw-schema (when opts
114127
(let [s (select-keys opts schema-keys)]
115128
(when (seq s) s)))
129+
dispatched? (and raw-schema (output-dispatched? (:output raw-schema)))
130+
schema (when raw-schema
131+
(schema/normalize-cell-schema raw-schema dispatched?))
116132
extra (when opts (select-keys opts opt-keys))
117133
spec (cond-> {:id cell-id :handler handler-fn}
118134
schema (assoc :schema schema)

src/mycelium/manifest.clj

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
[malli.generator :as mg]
88
[mycelium.cell :as cell]
99
[mycelium.fragment :as fragment]
10+
[mycelium.schema :as schema]
1011
[mycelium.validation :as v]))
1112

1213
;; ===== Cell definition validation =====
@@ -131,8 +132,26 @@
131132
(throw (ex-info "Manifest missing :cells" {:id id})))
132133
(when-not edges
133134
(throw (ex-info "Manifest missing :edges" {:id id})))
134-
;; Resolve :schema :inherit before validation
135+
;; Resolve :schema :inherit, then normalize lite syntax before validation
135136
(let [cells (resolve-inherit-schemas cells)
137+
;; Normalize cell schemas using edge context for output disambiguation
138+
;; Join members have no edge entries, so fall back to heuristic:
139+
;; a map output where all values are vectors is per-transition.
140+
cells (into {}
141+
(map (fn [[cell-name cell-def]]
142+
(if (or (= :inherit (:schema cell-def))
143+
(nil? (:schema cell-def)))
144+
[cell-name cell-def]
145+
(let [edge-def (get edges cell-name)
146+
output (get-in cell-def [:schema :output])
147+
dispatched? (or (map? edge-def)
148+
(and (map? output)
149+
(seq output)
150+
(every? vector? (vals output))))
151+
normalized (schema/normalize-cell-schema
152+
(:schema cell-def) dispatched?)]
153+
[cell-name (assoc cell-def :schema normalized)]))))
154+
cells)
136155
manifest (assoc manifest :cells cells)]
137156
;; Validate each cell definition
138157
(doseq [[cell-name cell-def] cells]
@@ -174,9 +193,9 @@
174193
effective-dispatches (merge join-dispatches (or dispatches {}))]
175194
(v/validate-dispatch-coverage! edges effective-dispatches))
176195
(v/validate-reachability! edges valid-names))
177-
;; Validate :input-schema if present
196+
;; Validate :input-schema if present (normalize lite syntax first)
178197
(when-let [input-schema (:input-schema manifest)]
179-
(v/validate-malli-schema! input-schema "input-schema"))
198+
(v/validate-malli-schema! (schema/normalize-schema input-schema) "input-schema"))
180199
;; Validate :regions if present
181200
(when-let [regions (:regions manifest)]
182201
(validate-regions! regions cells))
@@ -320,7 +339,7 @@
320339
:edges edges}
321340
dispatches (assoc :dispatches dispatches)
322341
(:joins manifest) (assoc :joins (:joins manifest))
323-
(:input-schema manifest) (assoc :input-schema (:input-schema manifest))
342+
(:input-schema manifest) (assoc :input-schema (schema/normalize-schema (:input-schema manifest)))
324343
(:interceptors manifest) (assoc :interceptors (:interceptors manifest))
325344
(:resilience manifest) (assoc :resilience (:resilience manifest))
326345
(:transforms manifest) (assoc :transforms (:transforms manifest)))))

src/mycelium/schema.clj

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,48 @@
77
[malli.transform :as mt]
88
[maestro.core :as fsm]))
99

10+
;; ===== Lite schema normalization =====
11+
12+
(defn normalize-schema
13+
"Normalizes a schema that may be in lite syntax to standard Malli.
14+
- Plain maps become [:map [:k1 v1] [:k2 v2] ...] with values recursively normalized
15+
- Vectors, keywords, and nil pass through unchanged
16+
Examples:
17+
{:subtotal :double} → [:map [:subtotal :double]]
18+
{:address {:street :string}} → [:map [:address [:map [:street :string]]]]
19+
[:map [:x :int]] → [:map [:x :int]]
20+
:int → :int"
21+
[schema]
22+
(cond
23+
(nil? schema) nil
24+
(map? schema) (into [:map] (map (fn [[k v]] [k (normalize-schema v)])) schema)
25+
:else schema))
26+
27+
(defn normalize-output-schema
28+
"Normalizes an output schema, accounting for per-transition maps.
29+
When `dispatched?` is true and the schema is a map, each value is normalized
30+
individually (per-transition output). Otherwise the map is treated as lite syntax."
31+
[schema dispatched?]
32+
(cond
33+
(nil? schema) nil
34+
(vector? schema) schema
35+
(map? schema) (if dispatched?
36+
(into {} (map (fn [[k v]] [k (normalize-schema v)])) schema)
37+
(normalize-schema schema))
38+
:else schema))
39+
40+
(defn normalize-cell-schema
41+
"Normalizes a cell's :schema map, converting lite syntax to Malli.
42+
`dispatched?` — true if the cell has branching edges (per-transition output)."
43+
([schema] (normalize-cell-schema schema false))
44+
([schema dispatched?]
45+
(when schema
46+
(let [input (:input schema)
47+
output (:output schema)]
48+
(cond-> schema
49+
input (assoc :input (normalize-schema input))
50+
output (assoc :output (normalize-output-schema output dispatched?)))))))
51+
1052
(def ^:private terminal-states
1153
#{::fsm/end ::fsm/error ::fsm/halt})
1254

src/mycelium/validation.clj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
(defn validate-cell-def!
4141
"Validates a single cell definition (manifest or fragment).
4242
Checks :id and :schema presence, validates Malli schemas.
43+
Expects schemas to be pre-normalized (lite syntax already converted).
4344
Skips schema validation for :schema :inherit (resolved separately).
4445
`context` is a string prefix for error messages (e.g. \"Cell\" or \"Fragment cell\")."
4546
[cell-name cell-def context]

0 commit comments

Comments
 (0)