@@ -34,14 +34,6 @@ fn signature(spec: &CommandSpec) -> String {
3434 }
3535}
3636
37- /// A command's field types, keyed by field name, for the per-field tooltips.
38- fn field_types ( spec : & CommandSpec ) -> HashMap < String , String > {
39- spec. fields
40- . iter ( )
41- . map ( |field| ( field. name . to_string ( ) , field. type_name . to_string ( ) ) )
42- . collect ( )
43- }
44-
4537/// Every command variant the api exposes, as (name, arguments schema), read
4638/// from the one source: `command_schema`. The builder lists all of them, so it
4739/// always matches the `Command` enum instead of a hand-picked subset.
@@ -75,22 +67,10 @@ fn variant_fields(args: &Value) -> Vec<(String, Value)> {
7567 . unwrap_or_default ( )
7668}
7769
78- /// Whether a field's schema is an entity `Ref`: an enum with an `Existing` or
79- /// `Entity` arm, including the nullable `opt_entity` form. Those fields take a
80- /// live entity, filled from the selection by id.
81- fn is_entity_ref ( schema : & Value ) -> bool {
82- let arms = schema
83- . get ( "oneOf" )
84- . or_else ( || schema. get ( "anyOf" ) )
85- . and_then ( Value :: as_array) ;
86- arms. is_some_and ( |variants| {
87- variants. iter ( ) . any ( |variant| {
88- variant
89- . get ( "properties" )
90- . and_then ( Value :: as_object)
91- . is_some_and ( |props| props. contains_key ( "Existing" ) || props. contains_key ( "Entity" ) )
92- } )
93- } )
70+ /// Whether a field's dispatch role binds a live entity, filled from the
71+ /// selection by id. Read from the manifest, not guessed from the schema shape.
72+ fn is_entity_role ( role : & str ) -> bool {
73+ role == "entity" || role == "opt_entity"
9474}
9575
9676/// The string options of a `string` enum field, for a dropdown.
@@ -125,63 +105,92 @@ fn number_array_len(schema: &Value) -> Option<usize> {
125105 . map ( |count| count as usize )
126106}
127107
128- /// The default text for a field, so the form starts valid and editable. Entity
129- /// references prefill with the current selection by id.
130- fn field_default ( schema : & Value , selected : Option < u32 > ) -> String {
131- if is_entity_ref ( schema) {
132- return selected. map ( |id| id. to_string ( ) ) . unwrap_or_default ( ) ;
133- }
134- if let Some ( options) = enum_options ( schema) {
135- return options. into_iter ( ) . next ( ) . unwrap_or_default ( ) ;
136- }
137- if let Some ( len) = number_array_len ( schema) {
138- return vec ! [ "0" ; len] . join ( ", " ) ;
139- }
140- match schema. get ( "type" ) . and_then ( Value :: as_str) {
141- Some ( "integer" ) | Some ( "number" ) => "0" . to_string ( ) ,
142- Some ( "boolean" ) => "false" . to_string ( ) ,
143- Some ( "string" ) => String :: new ( ) ,
144- _ => String :: new ( ) ,
145- }
108+ /// The comma list `raw` parses to, dropping blank entries.
109+ fn parse_numbers ( raw : & str ) -> Vec < f64 > {
110+ raw. split ( ',' )
111+ . filter ( |part| !part. trim ( ) . is_empty ( ) )
112+ . map ( |part| part. trim ( ) . parse :: < f64 > ( ) . unwrap_or ( 0.0 ) )
113+ . collect ( )
146114}
147115
148- /// Converts a field's edited text into its json value, by the field's schema.
149- fn field_to_json ( schema : & Value , raw : & str ) -> Value {
150- if is_entity_ref ( schema) {
151- return json ! ( { "Existing" : raw. trim( ) . parse:: <u32 >( ) . unwrap_or( 0 ) } ) ;
152- }
153- if enum_options ( schema) . is_some ( ) {
154- return json ! ( raw) ;
155- }
156- if number_array_len ( schema) . is_some ( ) {
157- let numbers: Vec < f64 > = raw
158- . split ( ',' )
159- . filter ( |part| !part. trim ( ) . is_empty ( ) )
160- . map ( |part| part. trim ( ) . parse :: < f64 > ( ) . unwrap_or ( 0.0 ) )
161- . collect ( ) ;
162- return json ! ( numbers) ;
116+ /// The default text for a field, so the form starts valid and editable. The
117+ /// dispatch role picks the shape: an entity prefills with the current selection
118+ /// by id, a vec is a comma list, the rest fall to the schema's primitive type.
119+ fn field_default ( role : & str , schema : & Value , selected : Option < u32 > ) -> String {
120+ match role {
121+ "entity" | "opt_entity" => selected. map ( |id| id. to_string ( ) ) . unwrap_or_default ( ) ,
122+ "vec3" => vec ! [ "0" ; number_array_len( schema) . unwrap_or( 3 ) ] . join ( ", " ) ,
123+ _ => {
124+ if let Some ( options) = enum_options ( schema) {
125+ return options. into_iter ( ) . next ( ) . unwrap_or_default ( ) ;
126+ }
127+ if let Some ( len) = number_array_len ( schema) {
128+ return vec ! [ "0" ; len] . join ( ", " ) ;
129+ }
130+ match schema. get ( "type" ) . and_then ( Value :: as_str) {
131+ Some ( "integer" ) | Some ( "number" ) => "0" . to_string ( ) ,
132+ Some ( "boolean" ) => "false" . to_string ( ) ,
133+ _ => String :: new ( ) ,
134+ }
135+ }
163136 }
164- match schema. get ( "type" ) . and_then ( Value :: as_str) {
165- Some ( "integer" ) => json ! ( raw. trim( ) . parse:: <i64 >( ) . unwrap_or( 0 ) ) ,
166- Some ( "number" ) => json ! ( raw. trim( ) . parse:: <f64 >( ) . unwrap_or( 0.0 ) ) ,
167- Some ( "boolean" ) => json ! ( raw == "true" ) ,
168- Some ( "string" ) => json ! ( raw) ,
169- _ => serde_json:: from_str :: < Value > ( raw) . unwrap_or ( Value :: Null ) ,
137+ }
138+
139+ /// Converts a field's edited text into its json value, dispatched by the
140+ /// manifest role first, then by the schema's primitive type for the rest.
141+ fn field_to_json ( role : & str , schema : & Value , raw : & str ) -> Value {
142+ match role {
143+ "entity" => json ! ( { "Existing" : raw. trim( ) . parse:: <u32 >( ) . unwrap_or( 0 ) } ) ,
144+ "opt_entity" => {
145+ let trimmed = raw. trim ( ) ;
146+ if trimmed. is_empty ( ) {
147+ Value :: Null
148+ } else {
149+ json ! ( { "Existing" : trimmed. parse:: <u32 >( ) . unwrap_or( 0 ) } )
150+ }
151+ }
152+ "vec3" => json ! ( parse_numbers( raw) ) ,
153+ _ => {
154+ if enum_options ( schema) . is_some ( ) {
155+ return json ! ( raw) ;
156+ }
157+ if number_array_len ( schema) . is_some ( ) {
158+ return json ! ( parse_numbers( raw) ) ;
159+ }
160+ match schema. get ( "type" ) . and_then ( Value :: as_str) {
161+ Some ( "integer" ) => json ! ( raw. trim( ) . parse:: <i64 >( ) . unwrap_or( 0 ) ) ,
162+ Some ( "number" ) => json ! ( raw. trim( ) . parse:: <f64 >( ) . unwrap_or( 0.0 ) ) ,
163+ Some ( "boolean" ) => json ! ( raw == "true" ) ,
164+ Some ( "string" ) => json ! ( raw) ,
165+ _ => serde_json:: from_str :: < Value > ( raw) . unwrap_or ( Value :: Null ) ,
166+ }
167+ }
170168 }
171169}
172170
173- fn defaults_for ( args : & Value , selected : Option < u32 > ) -> HashMap < String , String > {
174- variant_fields ( args)
175- . into_iter ( )
176- . map ( |( name, schema) | ( name, field_default ( & schema, selected) ) )
171+ fn defaults_for (
172+ spec : & CommandSpec ,
173+ schemas : & HashMap < String , Value > ,
174+ selected : Option < u32 > ,
175+ ) -> HashMap < String , String > {
176+ spec. fields
177+ . iter ( )
178+ . map ( |field| {
179+ let schema = schemas. get ( field. name ) . cloned ( ) . unwrap_or ( Value :: Null ) ;
180+ (
181+ field. name . to_string ( ) ,
182+ field_default ( field. role , & schema, selected) ,
183+ )
184+ } )
177185 . collect ( )
178186}
179187
180- /// One labeled input for a field, chosen by the field's schema: a checkbox for
181- /// a bool, a dropdown for a string enum , a pick-able id for an entity, a comma
182- /// list for a vec , a number or text box otherwise.
188+ /// One labeled input for a field, chosen by the manifest role first, then the
189+ /// schema: a pick-able id for an entity , a comma list for a vec, then a checkbox
190+ /// for a bool, a dropdown for a string enum , a number or text box otherwise.
183191fn field_input (
184192 name : String ,
193+ role : String ,
185194 schema : Value ,
186195 type_name : String ,
187196 fields : RwSignal < HashMap < String , String > > ,
@@ -196,10 +205,10 @@ fn field_input(
196205 } ) ;
197206 } ;
198207
199- if is_entity_ref ( & schema ) {
208+ if is_entity_role ( & role ) {
200209 let key_pick = name. clone ( ) ;
201- let hint = if number_array_len ( & schema ) . is_some ( ) {
202- ""
210+ let hint = if role == "opt_entity" {
211+ "entity id (optional) "
203212 } else {
204213 "entity id"
205214 } ;
@@ -304,44 +313,53 @@ pub fn Console(
304313 catalog. with_value ( |c| c. iter ( ) . map ( |( name, _) | name. clone ( ) ) . collect ( ) ) ;
305314 let first = names. first ( ) . cloned ( ) . unwrap_or_default ( ) ;
306315
307- let args_for = move |name : & str | -> Option < Value > {
316+ let schemas_for = move |name : & str | -> HashMap < String , Value > {
308317 catalog. with_value ( |c| {
309318 c. iter ( )
310319 . find ( |( item, _) | item == name)
311- . map ( |( _, args) | args. clone ( ) )
320+ . map ( |( _, args) | variant_fields ( args) . into_iter ( ) . collect ( ) )
321+ . unwrap_or_default ( )
312322 } )
313323 } ;
324+ let spec_for =
325+ move |name : & str | -> Option < CommandSpec > { docs. with_value ( |map| map. get ( name) . cloned ( ) ) } ;
314326
315327 let selected = RwSignal :: new ( first. clone ( ) ) ;
316328 let fields: RwSignal < HashMap < String , String > > = RwSignal :: new (
317- args_for ( & first)
318- . map ( |args | {
329+ spec_for ( & first)
330+ . map ( |spec | {
319331 defaults_for (
320- & args,
332+ & spec,
333+ & schemas_for ( & first) ,
321334 state. selected . get_untracked ( ) . map ( |detail| detail. id ) ,
322335 )
323336 } )
324337 . unwrap_or_default ( ) ,
325338 ) ;
326339
327340 let choose = move |name : String | {
328- if let Some ( args ) = args_for ( & name) {
341+ if let Some ( spec ) = spec_for ( & name) {
329342 let selected_id = state. selected . get_untracked ( ) . map ( |detail| detail. id ) ;
330- fields. set ( defaults_for ( & args , selected_id) ) ;
343+ fields. set ( defaults_for ( & spec , & schemas_for ( & name ) , selected_id) ) ;
331344 }
332345 selected. set ( name) ;
333346 } ;
334347
335348 let submit = move |_| {
336349 let name = selected. get ( ) ;
337- let Some ( args ) = args_for ( & name) else {
350+ let Some ( spec ) = spec_for ( & name) else {
338351 return ;
339352 } ;
353+ let schemas = schemas_for ( & name) ;
340354 let mut object = serde_json:: Map :: new ( ) ;
341355 fields. with ( |map| {
342- for ( field, schema) in variant_fields ( & args) {
343- let raw = map. get ( & field) . cloned ( ) . unwrap_or_default ( ) ;
344- object. insert ( field, field_to_json ( & schema, & raw ) ) ;
356+ for field in & spec. fields {
357+ let raw = map. get ( field. name ) . cloned ( ) . unwrap_or_default ( ) ;
358+ let schema = schemas. get ( field. name ) . cloned ( ) . unwrap_or ( Value :: Null ) ;
359+ object. insert (
360+ field. name . to_string ( ) ,
361+ field_to_json ( field. role , & schema, & raw ) ,
362+ ) ;
345363 }
346364 } ) ;
347365 let command = json ! ( { name: Value :: Object ( object) } ) ;
@@ -497,18 +515,28 @@ pub fn Console(
497515 <div class="console-fields" >
498516 { move || {
499517 let name = selected. get( ) ;
500- let types = docs
501- . with_value( |map| map. get( & name) . map( field_types) )
502- . unwrap_or_default( ) ;
503- match args_for( & name) {
504- Some ( args) => variant_fields( & args)
505- . into_iter( )
506- . map( |( field, schema) | {
507- let type_name = types. get( & field) . cloned( ) . unwrap_or_default( ) ;
508- field_input( field, schema, type_name, fields, state)
509- } )
510- . collect_view( )
511- . into_any( ) ,
518+ match spec_for( & name) {
519+ Some ( spec) => {
520+ let schemas = schemas_for( & name) ;
521+ spec. fields
522+ . into_iter( )
523+ . map( |field| {
524+ let schema = schemas
525+ . get( field. name)
526+ . cloned( )
527+ . unwrap_or( Value :: Null ) ;
528+ field_input(
529+ field. name. to_string( ) ,
530+ field. role. to_string( ) ,
531+ schema,
532+ field. type_name. to_string( ) ,
533+ fields,
534+ state,
535+ )
536+ } )
537+ . collect_view( )
538+ . into_any( )
539+ }
512540 None => ( ) . into_any( ) ,
513541 }
514542 } }
0 commit comments