1+ import type { StandardSchemaV1 } from '@standard-schema/spec'
12import type { DevframeNodeContext , DevframeScopedNodeContext } from 'devframe'
23import type { SharedState , SharedStatePatch } from 'devframe/utils/shared-state'
34import type { DevframeJsonRenderSpec , JsonRenderStatePatch , JsonRenderView } from '../types'
@@ -8,15 +9,17 @@ import { JSON_RENDER_INDEX_KEY } from '../view-index'
89import { diagnostics } from './diagnostics'
910
1011/** Options for {@link createJsonRenderView}. */
11- export interface CreateJsonRenderViewOptions {
12+ export interface CreateJsonRenderViewOptions < SpecType extends DevframeJsonRenderSpec = DevframeJsonRenderSpec > {
1213 /**
1314 * Stable, author-supplied id, unique within the view's scope. Forms the
1415 * shared-state key `devframe:json-render:<scope>:<id>` and never changes
1516 * across updates, so a client keeps its subscription across reconnects.
1617 */
1718 id : string
1819 /** The initial spec. */
19- spec : DevframeJsonRenderSpec
20+ spec : SpecType
21+ /** A replacement Standard Schema validator, or `false` to disable validation. */
22+ schema ?: StandardSchemaV1 | false
2023 /**
2124 * Override the scope segment of the view's stable id. Defaults to the
2225 * context's namespace when created from a scoped context, otherwise
@@ -98,9 +101,43 @@ function validateElementProps(id: string, spec: DevframeJsonRenderSpec): void {
98101 }
99102}
100103
104+ function formatStandardSchemaIssues ( issues : readonly StandardSchemaV1 . Issue [ ] ) : string {
105+ return issues
106+ . map ( ( issue ) => {
107+ const path = issue . path
108+ ?. map ( segment => typeof segment === 'object' ? String ( segment . key ) : String ( segment ) )
109+ . join ( '.' )
110+ return `${ path || '(root)' } : ${ issue . message } `
111+ } )
112+ . join ( '; ' )
113+ }
114+
115+ function isPromise < Result > ( value : Result | Promise < Result > ) : value is Promise < Result > {
116+ return typeof ( value as Promise < Result > ) . then === 'function'
117+ }
118+
119+ function validateSpec (
120+ id : string ,
121+ spec : DevframeJsonRenderSpec ,
122+ schema : StandardSchemaV1 | false | undefined ,
123+ ) : void {
124+ if ( schema === false )
125+ return
126+ if ( ! schema ) {
127+ validateElementProps ( id , spec )
128+ return
129+ }
130+
131+ const result = schema [ '~standard' ] . validate ( spec )
132+ if ( isPromise ( result ) )
133+ throw diagnostics . DF0074 ( { id } )
134+ if ( result . issues )
135+ throw diagnostics . DF0073 ( { id, issues : formatStandardSchemaIssues ( result . issues ) } )
136+ }
137+
101138// Ensure the spec always carries a `state` object so JSON-Pointer patches
102139// into `/state/...` have a container to target.
103- function normalizeSpec ( spec : DevframeJsonRenderSpec ) : DevframeJsonRenderSpec {
140+ function normalizeSpec < SpecType extends DevframeJsonRenderSpec > ( spec : SpecType ) : SpecType {
104141 return spec . state ? spec : { ...spec , state : { } }
105142}
106143
@@ -119,10 +156,10 @@ function normalizeSpec(spec: DevframeJsonRenderSpec): DevframeJsonRenderSpec {
119156 * view.dispose()
120157 * ```
121158 */
122- export function createJsonRenderView (
159+ export function createJsonRenderView < SpecType extends DevframeJsonRenderSpec = DevframeJsonRenderSpec > (
123160 ctx : AnyContext ,
124- options : CreateJsonRenderViewOptions ,
125- ) : JsonRenderView {
161+ options : CreateJsonRenderViewOptions < SpecType > ,
162+ ) : JsonRenderView < SpecType > {
126163 const scoped = isScoped ( ctx )
127164 const baseCtx = scoped ? ctx . base : ctx
128165 const scope = options . scope ?? ( scoped ? ctx . namespace : 'global' )
@@ -135,10 +172,10 @@ export function createJsonRenderView(
135172 throw diagnostics . DF0039 ( { id, scope } )
136173
137174 const initial = normalizeSpec ( options . spec )
138- validateElementProps ( id , initial )
175+ validateSpec ( id , initial , options . schema )
139176 assertJsonSerializable ( id , initial )
140177
141- const state : SharedState < DevframeJsonRenderSpec > = createSharedState ( {
178+ const state : SharedState < SpecType > = createSharedState ( {
142179 initialValue : initial ,
143180 enablePatches : true ,
144181 } )
@@ -165,11 +202,11 @@ export function createJsonRenderView(
165202 id,
166203 title,
167204 ref : { stateKey } ,
168- value : ( ) => state . value ( ) as DevframeJsonRenderSpec ,
205+ value : ( ) => state . value ( ) as SpecType ,
169206 update ( spec ) {
170207 assertLive ( )
171208 const next = normalizeSpec ( spec )
172- validateElementProps ( id , next )
209+ validateSpec ( id , next , options . schema )
173210 assertJsonSerializable ( id , next )
174211 state . mutate ( ( ) => next )
175212 } ,
0 commit comments