55 "fmt"
66 "path/filepath"
77 "reflect"
8+ "slices"
89 "strings"
910
1011 "github.com/Jguer/yay/v13/pkg/text"
@@ -61,16 +62,7 @@ func (e *Engine) Apply(cfg any) (unknown []string, errs []error) {
6162 }
6263
6364 sv := v .Elem ()
64- st := sv .Type ()
65-
66- index := make (map [string ]int , st .NumField ())
67-
68- for i := range st .NumField () {
69- field := st .Field (i )
70- if name := luaKeyForField (& field ); name != "" {
71- index [name ] = i
72- }
73- }
65+ index := luaFieldIndex (sv .Type ())
7466
7567 optTbl , ok := e .optTable ()
7668 if ! ok {
@@ -149,6 +141,20 @@ func luaKeyForField(field *reflect.StructField) string {
149141 return ""
150142}
151143
144+ // luaFieldIndex maps each lua-tagged field name of st to its field index.
145+ func luaFieldIndex (st reflect.Type ) map [string ]int {
146+ index := make (map [string ]int , st .NumField ())
147+
148+ for i := range st .NumField () {
149+ field := st .Field (i )
150+ if name := luaKeyForField (& field ); name != "" {
151+ index [name ] = i
152+ }
153+ }
154+
155+ return index
156+ }
157+
152158func assign (field reflect.Value , val lua.LValue ) error {
153159 switch field .Kind () {
154160 case reflect .String :
@@ -172,9 +178,128 @@ func assign(field reflect.Value, val lua.LValue) error {
172178 }
173179
174180 field .SetInt (int64 (n ))
181+ case reflect .Slice :
182+ return assignStructSlice (field , val )
175183 default :
176184 return fmt .Errorf ("unsupported field kind %s" , field .Kind ())
177185 }
178186
179187 return nil
180188}
189+
190+ // assignStructSlice fills a []Struct field from a Lua table keyed by name, e.g.
191+ //
192+ // { ["core"] = { url = "..." }, ["extra"] = { url = "..." } }
193+ //
194+ // Each entry becomes one struct: the table key populates the element's
195+ // lua:"name" field and the sub-table populates the remaining fields. Entries
196+ // are sorted by name so the resulting slice is deterministic despite Lua's
197+ // unordered table iteration.
198+ func assignStructSlice (field reflect.Value , val lua.LValue ) error {
199+ elemType := field .Type ().Elem ()
200+ if elemType .Kind () != reflect .Struct {
201+ return fmt .Errorf ("unsupported slice element kind %s" , elemType .Kind ())
202+ }
203+
204+ tbl , ok := val .(* lua.LTable )
205+ if ! ok {
206+ return fmt .Errorf ("expected table, got %s" , val .Type ())
207+ }
208+
209+ // The name comes from the table key, so a "name" key inside the entry table
210+ // would silently override it and let two entries share one name. Drop it
211+ // from the assignable set so it is reported as an unknown key instead.
212+ fieldIndex := luaFieldIndex (elemType )
213+ nameIdx , hasName := fieldIndex ["name" ]
214+ delete (fieldIndex , "name" )
215+
216+ type namedElem struct {
217+ name string
218+ elem reflect.Value
219+ }
220+
221+ var (
222+ entries []namedElem
223+ firstErr error
224+ )
225+
226+ tbl .ForEach (func (k , entry lua.LValue ) {
227+ if firstErr != nil {
228+ return
229+ }
230+
231+ name , ok := k .(lua.LString )
232+ if ! ok {
233+ firstErr = fmt .Errorf ("entry keys must be strings, got %s" , k .Type ())
234+ return
235+ }
236+
237+ entryTbl , ok := entry .(* lua.LTable )
238+ if ! ok {
239+ firstErr = fmt .Errorf ("entry %q must be a table, got %s" , string (name ), entry .Type ())
240+ return
241+ }
242+
243+ elem := reflect .New (elemType ).Elem ()
244+ if hasName {
245+ elem .Field (nameIdx ).SetString (string (name ))
246+ }
247+
248+ if err := assignStructFields (elem , entryTbl , fieldIndex ); err != nil {
249+ firstErr = fmt .Errorf ("entry %q: %w" , string (name ), err )
250+ return
251+ }
252+
253+ entries = append (entries , namedElem {name : string (name ), elem : elem })
254+ })
255+
256+ if firstErr != nil {
257+ return firstErr
258+ }
259+
260+ // Sort by name so the resulting slice is deterministic despite Lua's
261+ // unordered table iteration.
262+ slices .SortFunc (entries , func (a , b namedElem ) int {
263+ return strings .Compare (a .name , b .name )
264+ })
265+
266+ out := reflect .MakeSlice (field .Type (), len (entries ), len (entries ))
267+ for i , entry := range entries {
268+ out .Index (i ).Set (entry .elem )
269+ }
270+
271+ field .Set (out )
272+
273+ return nil
274+ }
275+
276+ // assignStructFields assigns the entries of tbl onto struct value sv, matching
277+ // each key against the lua:"..." tags in index. Unknown keys are errors so
278+ // typos in nested option tables fail fast, mirroring top-level opt handling.
279+ func assignStructFields (sv reflect.Value , tbl * lua.LTable , index map [string ]int ) error {
280+ var firstErr error
281+
282+ tbl .ForEach (func (k , entry lua.LValue ) {
283+ if firstErr != nil {
284+ return
285+ }
286+
287+ key , ok := k .(lua.LString )
288+ if ! ok {
289+ firstErr = fmt .Errorf ("keys must be strings, got %s" , k .Type ())
290+ return
291+ }
292+
293+ fieldIdx , found := index [string (key )]
294+ if ! found {
295+ firstErr = fmt .Errorf ("unknown key %q" , string (key ))
296+ return
297+ }
298+
299+ if err := assign (sv .Field (fieldIdx ), entry ); err != nil {
300+ firstErr = fmt .Errorf ("%s: %w" , string (key ), err )
301+ }
302+ })
303+
304+ return firstErr
305+ }
0 commit comments