okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
43ce3f9
main
1package daemon 2 3import ( 4"bytes" 5"encoding/json" 6"fmt" 7"reflect" 8 9"albedo/cli/internal/daemon/protocol" 10) 11 12type PageTone string 13 14const ( 15TonePlain PageTone = "plain" 16ToneActive PageTone = "active" 17ToneWarning PageTone = "warning" 18ToneMuted PageTone = "muted" 19) 20 21type PageRow struct { 22ID ,Text ,Badge ,Detail string 23Tone PageTone 24wire protocol.PageRow 25} 26type PageAction struct { 27ID ,Key ,Label ,Confirmation string 28Row ,Confirm bool 29Fields []protocol.FormField 30Operation protocol.ActionOperation 31} 32type PageGlance struct { 33Title string 34Rows []PageRow 35URL string 36} 37type PageDocument struct { 38Glance * PageGlance 39Title ,Summary ,Empty string 40Rows []PageRow 41Actions []PageAction 42Session * Session 43read * pageRead 44} 45type pageRead struct { 46Name ,Description string 47Operation protocol.ActionOperation 48Row * PageRow 49Form map [string ]json.RawMessage 50} 51type FormField = protocol.FormField 52 53func pageRowValue (row protocol.PageRow )PageRow { 54return PageRow {ID :row .ID ,Text :row .Text ,Badge :value (row .Badge ),Tone :PageTone (row .Tone ),Detail :value (row .Detail ),wire :row } 55} 56func glanceValue (glance protocol.Glance )* PageGlance { 57result := & PageGlance {Title :glance .Title ,URL :glance .URL } 58for _ ,row := range glance .Rows { 59result .Rows = append (result .Rows ,pageRowValue (row )) 60 } 61return result 62} 63func pageValue (wire protocol.PageDescriptor ) (* PageDocument ,error ) { 64if wire .Title == "" || wire .Rows == nil || wire .Actions == nil { 65return nil ,fieldError ("page descriptor" ) 66 } 67result := & PageDocument {Title :wire .Title ,Summary :wire .Summary ,Empty :wire .EmptyState ,Rows : []PageRow {},Actions : []PageAction {}} 68for _ ,row := range wire .Rows { 69result .Rows = append (result .Rows ,pageRowValue (row )) 70 } 71for _ ,action := range wire .Actions { 72if action .ID == "" || action .Operation .PathTemplate == "" { 73return nil ,fieldError ("page action" ) 74 } 75converted := PageAction {ID :action .ID ,Key :value (action .KeyboardHint ),Label :action .Label ,Confirmation :value (action .Confirmation ),Confirm :action .Confirmation != nil ,Fields :action .Fields ,Operation :action .Operation } 76for _ ,bindings := range []map [string ]protocol.Binding {action .Operation .Body ,action .Operation .Query ,action .Operation .Path ,action .Operation .Headers } { 77for _ ,raw := range bindings { 78var binding struct { 79Source string `json:"source"` 80 } 81if err := json .Unmarshal (raw ,& binding );err != nil { 82return nil ,fieldError ("page action binding" ) 83 } 84converted .Row = converted .Row || binding .Source == "row" 85 } 86 } 87for _ ,field := range action .Fields { 88converted .Row = converted .Row || field .DefaultBinding != nil && field .DefaultBinding .Source == "row" 89 } 90result .Actions = append (result .Actions ,converted ) 91 } 92if wire .Glance != nil { 93result .Glance = glanceValue (* wire .Glance ) 94 } 95return result ,nil 96} 97func decodePageDocument (data []byte ) (* PageDocument ,error ) { 98envelope ,err := object (data ) 99if err != nil { 100return nil ,err 101 } 102page ,ok := envelope ["page" ] 103if ! ok { 104return nil ,fieldError ("page" ) 105 } 106var document protocol.PageDescriptor 107if err := decodeRequired (page ,& document );err != nil { 108return nil ,err 109 } 110return pageValue (document ) 111} 112 113func readResultPage (data []byte ,name ,description string ) (* PageDocument ,error ) { 114result ,err := dynamicValue (data ) 115if err != nil { 116return nil ,err 117 } 118if object ,ok := result .(map [string ]any );ok { 119if _ ,described := object ["page" ];described { 120return decodePageDocument (data ) 121 } 122 } 123formatted ,err := json .MarshalIndent (result ,"" ," " ) 124if err != nil { 125return nil ,err 126 } 127return & PageDocument {Title :name ,Summary :description ,Rows : []PageRow {{ID :"result" ,Text :"Result" ,Detail :string (formatted ),Tone :TonePlain }},Actions : []PageAction {}},nil 128} 129 130func dynamicValue (data []byte ) (any ,error ) { 131if ! json .Valid (data ) { 132return nil ,fieldError ("JSON value" ) 133 } 134decoder := json .NewDecoder (bytes .NewReader (data )) 135decoder .UseNumber () 136var result any 137err := decoder .Decode (& result ) 138return result ,err 139} 140 141func actionFieldValue (field protocol.FormField ,text string ) (json.RawMessage ,error ) { 142if field .Type == "hidden" { 143if err := validateFormValue (field ,field .Default );err != nil { 144return nil ,err 145 } 146return field .Default ,nil 147 } 148if text == "" && field .Type != "text" && field .Type != "secret" && field .Default != nil && string (field .Default )!= "null" { 149if err := validateFormValue (field ,field .Default );err != nil { 150return nil ,err 151 } 152return field .Default ,nil 153 } 154if text == "" && ! field .Required && field .Type != "hidden" && (field .Default == nil || string (field .Default )== "null" ) { 155return nil ,nil 156 } 157if text == "" && field .Required { 158return nil ,fmt .Errorf ("%s is required" ,field .Label ) 159 } 160switch field .Type { 161case "text" ,"secret" : 162data ,err := json .Marshal (text ) 163return data ,err 164case "choice" : 165for _ ,choice := range field .Choices { 166if choice .Label == text || string (choice .Value )== text { 167return choice .Value ,nil 168 } 169 } 170return nil ,fmt .Errorf ("choose one of the values for %s" ,field .Label ) 171case "boolean" : 172if text == "true" || text == "false" { 173return json .RawMessage (text ),nil 174 } 175case "integer" : 176var number int64 177if json .Unmarshal ([]byte (text ),& number )== nil { 178if field .Minimum != nil && float64 (number )< * field .Minimum || field .Maximum != nil && float64 (number )> * field .Maximum { 179return nil ,fmt .Errorf ("%s is outside its permitted range" ,field .Label ) 180 } 181return json .RawMessage (text ),nil 182 } 183case "hidden" : 184return field .Default ,nil 185 } 186return nil ,fmt .Errorf ("invalid %s value" ,field .Label ) 187} 188 189// ParseActionField keeps values typed while the terminal edits their text representation. 190func ParseActionField (field FormField ,text string ) (json.RawMessage ,error ) { 191return actionFieldValue (field ,text ) 192} 193 194// ResolveFormField applies only the descriptor's explicit displayed-value binding. 195func ResolveFormField (field FormField ,row * PageRow ,session * Session ) (FormField ,error ) { 196if field .DefaultBinding == nil { 197return field ,nil 198 } 199if field .DefaultBinding .Source != "row" && field .DefaultBinding .Source != "session" { 200return field ,fmt .Errorf ("invalid displayed default source for %s" ,field .Label ) 201 } 202var rowData ,sessionData json.RawMessage 203if row != nil { 204rowData ,_ = json .Marshal (row .wire ) 205 } 206if session != nil { 207sessionData ,_ = json .Marshal (session .wire ) 208 } 209binding ,_ := json .Marshal (field .DefaultBinding ) 210resolved ,err := resolveBinding (binding ,rowData ,nil ,sessionData ) 211if err != nil { 212return field ,err 213 } 214if err := validateFormValue (field ,resolved );err != nil { 215return field ,err 216 } 217field .Default = resolved 218return field ,nil 219} 220func validateFormValue (field FormField ,raw json.RawMessage )error { 221switch field .Type { 222case "hidden" : 223if json .Valid (raw ) { 224return nil 225 } 226case "text" ,"secret" : 227var text string 228if string (raw )!= "null" && json .Unmarshal (raw ,& text )== nil { 229return nil 230 } 231case "boolean" : 232var flag bool 233if string (raw )!= "null" && json .Unmarshal (raw ,& flag )== nil { 234return nil 235 } 236case "integer" : 237var number int64 238if string (raw )!= "null" && json .Unmarshal (raw ,& number )== nil { 239if (field .Minimum == nil || float64 (number )>= * field .Minimum )&& (field .Maximum == nil || float64 (number )<= * field .Maximum ) { 240return nil 241 } 242 } 243case "choice" : 244received ,err := dynamicValue (raw ) 245if err == nil { 246for _ ,choice := range field .Choices { 247candidate ,err := dynamicValue (choice .Value ) 248if err == nil && reflect .DeepEqual (received ,candidate ) { 249return nil 250 } 251 } 252 } 253 } 254return fmt .Errorf ("invalid displayed default for %s" ,field .Label ) 255} 256 257func FormChoiceDefault (field FormField )int { 258wanted ,err := dynamicValue (field .Default ) 259if err != nil { 260return 0 261 } 262if field .Type == "boolean" { 263if wanted == true { 264return 1 265 } 266return 0 267 } 268for index ,choice := range field .Choices { 269candidate ,err := dynamicValue (choice .Value ) 270if err == nil && reflect .DeepEqual (wanted ,candidate ) { 271return index 272 } 273 } 274return 0 275}