master
go 272 lines 6.42 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package funcapi
4
5 import "encoding/json"
6
7 // ParamSelection defines whether a required param allows single or multiple selections.
8 type ParamSelection uint8
9
10 const (
11 // ParamSelect allows a single choice.
12 ParamSelect ParamSelection = iota
13 // ParamMultiSelect allows multiple choices.
14 ParamMultiSelect
15 )
16
17 // String returns the UI keyword used for this selection mode.
18 func (p ParamSelection) String() string {
19 switch p {
20 case ParamMultiSelect:
21 return "multiselect"
22 default:
23 return "select"
24 }
25 }
26
27 // MarshalJSON encodes the selection mode as a UI keyword.
28 func (p ParamSelection) MarshalJSON() ([]byte, error) {
29 return json.Marshal(p.String())
30 }
31
32 // ParamOption defines a single option for a required param.
33 // Column is not serialized and can be used for safe SQL mapping (e.g., __sort).
34 type ParamOption struct {
35 // ID is the stable identifier returned in selected values.
36 ID string
37 // Name is the label shown in the UI.
38 Name string
39 // Default marks the option as the default selection (if none are set, the first option is used).
40 Default bool
41 // Disabled prevents selection in the UI.
42 Disabled bool
43 // Sort includes a sort directive with the option.
44 Sort *FieldSort
45 // Column is not serialized and can be used for safe SQL mapping.
46 Column string
47 }
48
49 // ParamConfig defines a required param and its available options.
50 type ParamConfig struct {
51 // ID identifies the required param in requests.
52 ID string
53 // Name is the label shown in the UI.
54 Name string
55 // Help provides UI help text for the param.
56 Help string
57 // Selection sets single or multi-select behavior.
58 Selection ParamSelection
59 // Options supplies the available choices.
60 Options []ParamOption
61 // UniqueView requests unique view behavior for the param in the UI.
62 UniqueView bool
63 }
64
65 // RequiredParam converts ParamConfig to the wire format used by required_params.
66 func (p ParamConfig) RequiredParam() map[string]any {
67 out := map[string]any{
68 "id": p.ID,
69 "name": p.Name,
70 "type": p.Selection.String(),
71 "options": buildParamOptions(p.Options),
72 }
73 if p.Help != "" {
74 out["help"] = p.Help
75 }
76 if p.UniqueView {
77 out["unique_view"] = true
78 }
79 return out
80 }
81
82 func buildParamOptions(opts []ParamOption) []map[string]any {
83 if len(opts) == 0 {
84 return []map[string]any{}
85 }
86
87 hasDefault := false
88 for _, opt := range opts {
89 if opt.Default {
90 hasDefault = true
91 break
92 }
93 }
94
95 options := make([]map[string]any, 0, len(opts))
96 for i, opt := range opts {
97 o := map[string]any{
98 "id": opt.ID,
99 "name": opt.Name,
100 }
101 if opt.Disabled {
102 o["disabled"] = true
103 }
104 if opt.Sort != nil {
105 o["sort"] = opt.Sort.String()
106 }
107 if opt.Default || (!hasDefault && i == 0) {
108 o["defaultSelected"] = true
109 }
110 options = append(options, o)
111 }
112 return options
113 }
114
115 // ResolvedParam holds resolved values for a required param.
116 type ResolvedParam struct {
117 // IDs contains selected option IDs in order.
118 IDs []string
119 // Options contains selected option metadata in order.
120 Options []ParamOption
121 }
122
123 // GetOne returns the first selected ID.
124 func (p ResolvedParam) GetOne() string {
125 if len(p.IDs) > 0 {
126 return p.IDs[0]
127 }
128 return ""
129 }
130
131 // ResolvedParams maps param ID to resolved values.
132 type ResolvedParams map[string]ResolvedParam
133
134 // Get returns all selected IDs for the param.
135 func (p ResolvedParams) Get(id string) []string {
136 if p == nil {
137 return nil
138 }
139 return p[id].IDs
140 }
141
142 // GetOne returns the first selected ID for the param.
143 func (p ResolvedParams) GetOne(id string) string {
144 if p == nil {
145 return ""
146 }
147 if v, ok := p[id]; ok && len(v.IDs) > 0 {
148 return v.IDs[0]
149 }
150 return ""
151 }
152
153 // Option returns the first selected option for the param.
154 func (p ResolvedParams) Option(id string) (ParamOption, bool) {
155 if p == nil {
156 return ParamOption{}, false
157 }
158 if v, ok := p[id]; ok && len(v.Options) > 0 {
159 return v.Options[0], true
160 }
161 return ParamOption{}, false
162 }
163
164 // Column returns the column mapping for the selected option (used by __sort).
165 // If the option has no Column mapping, the selected ID is returned.
166 func (p ResolvedParams) Column(id string) string {
167 opt, ok := p.Option(id)
168 if !ok {
169 return ""
170 }
171 if opt.Column != "" {
172 return opt.Column
173 }
174 return opt.ID
175 }
176
177 // ResolveParam resolves user values against a ParamConfig, applying defaults or the first option when needed.
178 func ResolveParam(cfg ParamConfig, values []string) ResolvedParam {
179 byID := make(map[string]ParamOption, len(cfg.Options))
180 for _, opt := range cfg.Options {
181 byID[opt.ID] = opt
182 }
183
184 var selected []ParamOption
185
186 switch cfg.Selection {
187 case ParamMultiSelect:
188 for _, val := range values {
189 if opt, ok := byID[val]; ok {
190 selected = append(selected, opt)
191 }
192 }
193 default:
194 if len(values) > 0 {
195 if opt, ok := byID[values[0]]; ok {
196 selected = []ParamOption{opt}
197 }
198 }
199 }
200
201 if len(selected) == 0 {
202 selected = defaultOptions(cfg)
203 }
204
205 resolved := ResolvedParam{}
206 if len(selected) > 0 {
207 resolved.Options = selected
208 resolved.IDs = make([]string, 0, len(selected))
209 for _, opt := range selected {
210 resolved.IDs = append(resolved.IDs, opt.ID)
211 }
212 }
213 return resolved
214 }
215
216 // ResolveParams resolves multiple ParamConfig entries.
217 func ResolveParams(cfgs []ParamConfig, values map[string][]string) ResolvedParams {
218 resolved := ResolvedParams{}
219 for _, cfg := range cfgs {
220 resolved[cfg.ID] = ResolveParam(cfg, values[cfg.ID])
221 }
222 return resolved
223 }
224
225 func defaultOptions(cfg ParamConfig) []ParamOption {
226 var defaults []ParamOption
227 for _, opt := range cfg.Options {
228 if opt.Default {
229 defaults = append(defaults, opt)
230 }
231 }
232 if len(defaults) > 0 {
233 if cfg.Selection == ParamMultiSelect {
234 return defaults
235 }
236 return []ParamOption{defaults[0]}
237 }
238 if len(cfg.Options) == 0 {
239 return nil
240 }
241 return []ParamOption{cfg.Options[0]}
242 }
243
244 // MergeParamConfigs replaces base configs with overrides by ID, preserving base order.
245 func MergeParamConfigs(base, overrides []ParamConfig) []ParamConfig {
246 if len(overrides) == 0 {
247 return base
248 }
249
250 overrideByID := make(map[string]ParamConfig, len(overrides))
251 for _, cfg := range overrides {
252 overrideByID[cfg.ID] = cfg
253 }
254
255 seen := make(map[string]bool, len(base)+len(overrides))
256 merged := make([]ParamConfig, 0, len(base)+len(overrides))
257 for _, cfg := range base {
258 if override, ok := overrideByID[cfg.ID]; ok {
259 merged = append(merged, override)
260 seen[cfg.ID] = true
261 continue
262 }
263 merged = append(merged, cfg)
264 seen[cfg.ID] = true
265 }
266 for _, cfg := range overrides {
267 if !seen[cfg.ID] {
268 merged = append(merged, cfg)
269 }
270 }
271 return merged
272 }