okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

Aly Raffaufrefactor(tui): use declared form fields directly43ce3f9

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