host: Slice 8 — typed scalar fields on types + the generic, type-driven form

The keystone: a type declares :fields [{name, value-type, widget}], an instance carries
:field-values, and the SAME edit form is generated from the type definitions — no per-type
code. 'The editor maps onto the types.'

8a (field model): host/blog-value-types (String/Text/URL/Int/Date/Bool -> default widget),
host/blog--widget-for (explicit > value-type default > text), host/blog-fields-of +
--set-fields! (on the type-post, like schema), --fields-summary. Article seeded with
subtitle:String + hero:URL. /meta gains a Fields column. host/blog-type-defs (the subtype-of
hierarchy = type DEFINITIONS, vs instances-of = is-a instances).

8b (instance form): host/blog-field-values-of + --set-field-values!; host/blog--fields-for-post
(union of the post's transitive types' fields, deduped); host/blog--field-inputs (one labelled
input per field, widget per value-type, pre-filled). edit-form injects the Fields section
(durable reads pre-fetched); edit-submit reads field-* inputs via host/field and stores them.

Verified live-path (ephemeral, SX_SERVING_JIT=1): relate is-a article -> field inputs appear
-> save -> values persist. Blog suite 132/132.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-30 12:18:34 +00:00
parent 360acbe33c
commit f5f4e93dcf
2 changed files with 155 additions and 5 deletions

View File

@@ -602,6 +602,55 @@
(contains? body "h1") (contains? body "related")
(contains? body "symmetric")))
(list true true true true true))
;; -- Slice 8: typed scalar fields on a type --
(host-bl-test "fields-of reads a type's declared fields (seeded on article)"
(map (fn (f) (get f :name)) (host/blog-fields-of "article"))
(list "subtitle" "hero"))
(host-bl-test "widget-for: explicit > value-type default > text fallback"
(list (host/blog--widget-for {:name "a" :type "URL"})
(host/blog--widget-for {:name "b" :type "Text"})
(host/blog--widget-for {:name "c" :type "Nonsense"})
(host/blog--widget-for {:name "d" :type "String" :widget "custom"}))
(list "url" "textarea" "text" "custom"))
(host-bl-test "set-fields! is idempotent + preserves the rest of the record"
(begin
(host/blog--set-fields! "article"
(list {:name "subtitle" :type "String"} {:name "hero" :type "URL"}))
(list (get (host/blog-get "article") :title) (len (host/blog-fields-of "article"))))
(list "Article" 2))
(host-bl-test "a type with no declared fields -> empty list"
(host/blog-fields-of "tag") (list))
(host-bl-test "/meta shows the article's typed fields"
(contains? (dream-resp-body (host-bl-app (host-bl-req "/meta"))) "subtitle:String") true)
;; -- Slice 8b: field values + the generic, type-driven edit form --
(host-bl-test "fields-for-post = union of the post's (transitive) types' fields"
(begin
(host/blog-put! "fpost" "F Post" "(article (h1 \"F\"))" "published")
(host/blog-relate! "fpost" "article" "is-a")
(map (fn (f) (get f :name)) (host/blog--fields-for-post "fpost")))
(list "subtitle" "hero"))
(host-bl-test "a post of no typed type has no fields"
(host/blog--fields-for-post "hello") (list))
(host-bl-test "set/get field-values round-trips on an instance"
(begin
(host/blog--set-field-values! "fpost" {"subtitle" "A subtitle" "hero" "http://x/y.png"})
(list (get (host/blog-field-values-of "fpost") "subtitle")
(get (host/blog-field-values-of "fpost") "hero")))
(list "A subtitle" "http://x/y.png"))
(host-bl-test "edit form renders one input per field for a typed post"
(let ((body (dream-resp-body (host-bl-wapp (host-bl-send "GET" "/fpost/edit" "Bearer good" nil "")))))
(list (contains? body "field-subtitle") (contains? body "field-hero") (contains? body "Fields")))
(list true true true))
(host-bl-test "edit-submit stores the typed field values from the form"
(begin
(host-bl-wapp (host-bl-send "POST" "/fpost/edit" "Bearer good"
"application/x-www-form-urlencoded"
"sx_content=(article+(h1+%22F%22))&field-subtitle=Saved+Sub&field-hero=http%3A%2F%2Fz%2Fq.png"))
(list (get (host/blog-field-values-of "fpost") "subtitle")
(get (host/blog-field-values-of "fpost") "hero")))
(list "Saved Sub" "http://z/q.png"))
(host-bl-test "a post with no schema'd type is vacuously valid"
(host/blog-type-valid? "ppost" "(p \"anything\")") true)
(host-bl-test "edit-submit rejects content violating the type schema (not saved)"