11import { Dynamic } from '@solidjs/web'
2- import { mapAsync } from 'es-toolkit'
2+ import { mapAsync , mapValues } from 'es-toolkit'
33import {
44 createMemo ,
55 createStore ,
@@ -14,8 +14,24 @@ import * as v from 'valibot'
1414import { ExerciseContext } from './context'
1515
1616type MaybeAsync < T > = T | Promise < T >
17- type RawShape = Record < string , v . BaseSchema < any , any , any > >
18- type InferFromShape < T extends RawShape > = v . InferOutput < v . ObjectSchema < T , undefined > >
17+
18+ type Field < T = any , U = any > = {
19+ base : v . GenericSchema < any , T >
20+ feedback : v . GenericSchema < T , U >
21+ }
22+
23+ export function defineField < T , U > ( field : Field < T , U > ) {
24+ return field
25+ }
26+
27+ type RawShape = Record < string , Field >
28+ function RawShapeSchema < T extends RawShape , S extends 'base' | 'feedback' > ( shape : T , stage : S ) {
29+ return v . object ( mapValues ( shape , ( field ) => field [ stage ] ) as { [ K in keyof T ] : T [ K ] [ S ] } )
30+ }
31+
32+ type InferFromShape < T extends Record < string , Field > , S extends 'base' | 'feedback' > = v . InferOutput <
33+ ReturnType < typeof RawShapeSchema < T , S > >
34+ >
1935
2036/**
2137 * Infer the type associated with a schema factory
@@ -43,7 +59,7 @@ type Infer<
4359export function defineSchema <
4460 const N extends string ,
4561 Q extends RawShape ,
46- T extends ( question : InferFromShape < Q > ) => Promise < InferFromShape < Q > > ,
62+ T extends ( question : InferFromShape < Q , 'feedback' > ) => Promise < InferFromShape < Q , 'base' > > ,
4763 const S extends Record < string , { previous : ( keyof S ) [ ] ; state : RawShape } > & {
4864 start : { previous : [ ] ; state : RawShape }
4965 } ,
@@ -54,53 +70,63 @@ export function defineSchema<
5470type Schema <
5571 N extends string = any ,
5672 Q extends RawShape = any ,
57- T extends ( question : InferFromShape < Q > ) => Promise < InferFromShape < Q > > = any ,
73+ T extends ( question : InferFromShape < Q , 'feedback' > ) => Promise < InferFromShape < Q , 'base' > > = any ,
5874 S extends Record < string , { previous : ( keyof S ) [ ] ; state : RawShape } > & {
5975 start : { previous : [ ] ; state : RawShape }
6076 } = any ,
6177> = ReturnType < typeof defineSchema < N , Q , T , S > >
6278
63- function Part < T extends Schema , K extends keyof T [ 'steps' ] , S extends boolean = false > (
64- schema : T ,
65- step : K ,
66- withState ?: S ,
67- ) {
79+ function Part <
80+ T extends Schema ,
81+ K extends keyof T [ 'steps' ] ,
82+ S extends boolean = false ,
83+ V extends 'base' | 'feedback' = 'base' ,
84+ > ( schema : T , step : K , withState ?: S , stage ?: V ) {
6885 const base = v . object ( {
6986 step : v . literal ( step as K ) ,
7087 } )
7188 const extended = v . object ( {
7289 ...base . entries ,
73- state : v . object ( schema . steps [ step ] . state as T [ 'steps' ] [ K ] [ 'state' ] ) ,
90+ state : RawShapeSchema ( schema . steps [ step ] . state as T [ 'steps' ] [ K ] [ 'state' ] , stage ?? 'base' ) ,
7491 } )
7592 return ( withState ? extended : base ) as S extends true ? typeof extended : typeof base
7693}
7794
78- type Part < T extends Schema , K extends keyof T [ 'steps' ] , S extends boolean = false > = Infer <
79- typeof Part < T , K , S >
80- >
95+ type Part <
96+ T extends Schema ,
97+ K extends keyof T [ 'steps' ] ,
98+ S extends boolean = false ,
99+ V extends 'base' | 'feedback' = 'base' ,
100+ > = Infer < typeof Part < T , K , S , V > >
81101
82102function PartUnion <
83103 T extends Schema ,
84104 K extends readonly ( keyof T [ 'steps' ] ) [ ] ,
85105 S extends boolean = false ,
86- > ( schema : T , steps : K , withState ?: S ) {
106+ V extends 'base' | 'feedback' = 'base' ,
107+ > ( schema : T , steps : K , withState ?: S , stage ?: V ) {
87108 return v . variant (
88109 'step' ,
89- steps . map ( ( step ) => Part ( schema , step , withState ) ) ,
90- ) as v . VariantSchema < 'step' , { [ I in keyof K ] : ReturnType < typeof Part < T , K [ I ] , S > > } , undefined >
110+ steps . map ( ( step ) => Part ( schema , step , withState , stage ) ) ,
111+ ) as v . VariantSchema <
112+ 'step' ,
113+ { [ I in keyof K ] : ReturnType < typeof Part < T , K [ I ] , S , V > > } ,
114+ undefined
115+ >
91116}
92117
93118type PartUnion <
94119 T extends Schema ,
95120 K extends readonly ( keyof T [ 'steps' ] ) [ ] = readonly ( keyof T [ 'steps' ] ) [ ] ,
96121 S extends boolean = false ,
97- > = Infer < typeof PartUnion < T , K , S > >
122+ V extends 'base' | 'feedback' = 'base' ,
123+ > = Infer < typeof PartUnion < T , K , S , V > >
98124
99125type Props < T extends Schema , K extends keyof T [ 'steps' ] , F extends boolean = true > = {
100- question : InferFromShape < T [ 'question' ] >
101- state ?: InferFromShape < T [ 'steps' ] [ K ] [ 'state' ] >
126+ question : InferFromShape < T [ 'question' ] , 'feedback' >
127+ state ?: InferFromShape < T [ 'steps' ] [ K ] [ 'state' ] , 'feedback' >
102128 previous : {
103- [ S in T [ 'steps' ] [ K ] [ 'previous' ] [ number ] ] : InferFromShape < T [ 'steps' ] [ S ] [ 'state' ] >
129+ [ S in T [ 'steps' ] [ K ] [ 'previous' ] [ number ] ] : InferFromShape < T [ 'steps' ] [ S ] [ 'state' ] , 'feedback' >
104130 } [ T [ 'steps' ] [ K ] [ 'previous' ] [ number ] ] [ ]
105131} & ( F extends true ? Partial < { correct : boolean ; score : [ number , number ] } > : { } )
106132
@@ -114,32 +140,41 @@ export function defineFeedback<T extends Schema>(data: {
114140 return data
115141}
116142
143+ function Attempt < T extends Schema , V extends 'base' | 'feedback' > ( schema : T , stage : V ) {
144+ const steps = Object . keys ( schema . steps ) as T [ 'steps' ] [ keyof T [ 'steps' ] ]
145+ return v . array (
146+ v . union ( [ PartUnion ( schema , steps , true , stage ) , PartUnion ( schema , steps , false , stage ) ] ) ,
147+ )
148+ }
149+
117150export function buildSchemas < T extends Schema > (
118151 schema : T ,
119152 feedback : ReturnType < typeof defineFeedback < T > > ,
120153) {
121- const steps = Object . keys ( schema . steps ) as T [ 'steps' ] [ keyof T [ 'steps' ] ]
122154 return {
123155 Student : v . pipeAsync (
124156 v . object ( {
125157 name : v . literal ( schema . name as T [ 'name' ] ) ,
126- question : v . object ( schema . question as T [ 'question' ] ) ,
127- attempt : v . array (
128- v . union ( [ PartUnion ( schema , steps , true ) , PartUnion ( schema , steps , false ) ] ) ,
129- ) ,
158+ question : RawShapeSchema ( schema . question as T [ 'question' ] , 'base' ) ,
159+ attempt : Attempt ( schema , 'base' ) ,
130160 } ) ,
131161 // TODO: calls need to be deduped, waiting for solid-router release?
132162 v . transformAsync ( async ( { attempt, question, ...exercise } ) => {
163+ const parsedQuestion = v . parse (
164+ RawShapeSchema ( schema . question as T [ 'question' ] , 'feedback' ) ,
165+ question ,
166+ )
167+ const parsedAttempt = v . parse ( Attempt ( schema , 'feedback' ) , attempt )
133168 let modifiedAttempt = attempt
134169 if ( attempt . length === 0 && schema . transform ) {
135- question = await schema . transform ( question )
170+ question = await schema . transform ( parsedQuestion )
136171 }
137172 modifiedAttempt = await mapAsync ( modifiedAttempt , async ( part , i ) => {
138- if ( 'state' in part && part . state ) {
173+ if ( 'state' in part && 'state' in parsedAttempt [ i ] ! && part . state ) {
139174 const result = await feedback [ part . step ] ( {
140- question : question ,
141- state : part . state ,
142- previous : modifiedAttempt . slice ( 0 , i ) . toReversed ( ) as any ,
175+ question : parsedQuestion ,
176+ state : parsedAttempt [ i ] ! . state ?? part . state ,
177+ previous : parsedAttempt . slice ( 0 , i ) . toReversed ( ) as any ,
143178 } )
144179 return { ...part , ...result }
145180 }
@@ -176,10 +211,13 @@ export function createView<T extends Schema>(
176211 const { Student, grade } = buildSchemas ( schema , feedback )
177212 return function Component ( props : FinalViewProps < T > ) {
178213 const exercise = createMemo ( async ( ) => grade ( await props . fetch ( ) ) )
214+ const parsedQuestion = createMemo ( ( ) =>
215+ v . parse ( RawShapeSchema ( schema . question as T [ 'question' ] , 'feedback' ) , exercise ( ) . question ) ,
216+ )
217+ const parsedAttempt = createMemo ( ( ) => v . parse ( Attempt ( schema , 'feedback' ) , exercise ( ) . attempt ) )
179218 return (
180219 < Loading fallback = "Génération de l'exercice..." >
181- < p > { exercise ( ) . attempt . length } étapes</ p >
182- < For each = { exercise ( ) . attempt } >
220+ < For each = { parsedAttempt ( ) } >
183221 { < K extends keyof T [ 'steps' ] > (
184222 part : ( ) =>
185223 | Part < T , K , true >
@@ -195,23 +233,22 @@ export function createView<T extends Schema>(
195233 )
196234 const submit = async ( ) => {
197235 if ( ! validated ( ) . success ) return
198- await props . save (
199- await grade ( {
200- ...exercise ( ) ,
201- attempt : [
202- ...exercise ( ) . attempt . toSpliced ( - 1 ) ,
203- validated ( ) . output as Part < T , K , true > ,
204- ] ,
205- } ) ,
206- )
236+ const graded = await grade ( {
237+ ...exercise ( ) ,
238+ attempt : [
239+ ...exercise ( ) . attempt . toSpliced ( - 1 ) ,
240+ validated ( ) . output as Part < T , K , true > ,
241+ ] ,
242+ } )
243+ await props . save ( graded )
207244 refresh ( ( ) => exercise )
208245 }
209246 return (
210- < ExerciseContext value = { { state, setState } } >
247+ < ExerciseContext value = { { readOnly : ! ! part ( ) . state , state, setState } } >
211248 < Dynamic
212249 component = { view [ part ( ) . step ] }
213250 { ...( {
214- question : exercise ( ) . question ,
251+ question : parsedQuestion ( ) ,
215252 state : part ( ) . state ,
216253 previous : exercise ( ) . attempt . slice ( 0 , i ( ) ) . toReversed ( ) as any ,
217254 } satisfies Props < T , K > as ComponentProps < View < T > [ K ] > ) }
0 commit comments