diff --git a/classad/classad.go b/classad/classad.go index 48e89df..370ea15 100644 --- a/classad/classad.go +++ b/classad/classad.go @@ -273,6 +273,111 @@ func (c *ClassAd) InsertAttrClassAd(name string, value *ClassAd) { c.Insert(name, &ast.RecordLiteral{ClassAd: inner}) } +// InsertAttrList inserts an attribute with a list of values using generics. +// Supported types are: int64, float64, string, bool, *ClassAd, and *Expr. +// +// Example: +// +// ad := classad.New() +// InsertAttrList(ad, "numbers", []int64{1, 2, 3, 4, 5}) +// InsertAttrList(ad, "names", []string{"Alice", "Bob", "Charlie"}) +// InsertAttrList(ad, "flags", []bool{true, false, true}) +// InsertAttrList(ad, "values", []float64{1.5, 2.7, 3.14}) +// +// // Also works with ClassAds +// ad1, ad2 := classad.New(), classad.New() +// ad1.InsertAttr("x", 1) +// ad2.InsertAttr("y", 2) +// InsertAttrList(ad, "items", []*classad.ClassAd{ad1, ad2}) +// +// // And with expressions +// expr1, _ := classad.ParseExpr("\"hello\"") +// expr2, _ := classad.ParseExpr("42") +// InsertAttrList(ad, "mixed", []*classad.Expr{expr1, expr2}) +func InsertAttrList[T int64 | float64 | string | bool | *ClassAd | *Expr](c *ClassAd, name string, values []T) { + elements := make([]ast.Expr, len(values)) + for i, v := range values { + elements[i] = valueToAstExpr(v) + } + c.Insert(name, &ast.ListLiteral{Elements: elements}) +} + +// valueToAstExpr converts a value to an ast.Expr based on its type. +func valueToAstExpr[T int64 | float64 | string | bool | *ClassAd | *Expr](v T) ast.Expr { + switch val := any(v).(type) { + case int64: + return &ast.IntegerLiteral{Value: val} + case float64: + return &ast.RealLiteral{Value: val} + case string: + return &ast.StringLiteral{Value: val} + case bool: + return &ast.BooleanLiteral{Value: val} + case *ClassAd: + var inner *ast.ClassAd + if val != nil { + inner = val.ad + } + if inner == nil { + inner = &ast.ClassAd{Attributes: []*ast.AttributeAssignment{}} + } + return &ast.RecordLiteral{ClassAd: inner} + case *Expr: + if val == nil { + return &ast.UndefinedLiteral{} + } + return val.internal() + default: + return &ast.UndefinedLiteral{} + } +} + +// InsertListElement inserts an element into a list attribute. +// If the attribute doesn't exist, it creates a new list with the element. +// If the attribute exists and is a list, it appends the element to the existing list. +// If the attribute exists but is not a list, it replaces it with a new list containing the element. +// +// Example: +// +// ad := classad.New() +// expr1, _ := classad.ParseExpr("\"first\"") +// expr2, _ := classad.ParseExpr("\"second\"") +// ad.InsertListElement("items", expr1) +// ad.InsertListElement("items", expr2) +// // Result: items = {"first", "second"} +func (c *ClassAd) InsertListElement(name string, element *Expr) { + if c.ad == nil { + c.ad = &ast.ClassAd{Attributes: []*ast.AttributeAssignment{}} + } + + var astExpr ast.Expr + if element == nil { + astExpr = &ast.UndefinedLiteral{} + } else { + astExpr = element.internal() + } + + // Check if attribute already exists + for i, attr := range c.ad.Attributes { + if attr.Name == name { + // If it's a list, append to it + if list, ok := attr.Value.(*ast.ListLiteral); ok { + list.Elements = append(list.Elements, astExpr) + return + } + // Otherwise, replace with a new list containing the element + c.ad.Attributes[i].Value = &ast.ListLiteral{Elements: []ast.Expr{astExpr}} + return + } + } + + // Add new list attribute + c.ad.Attributes = append(c.ad.Attributes, &ast.AttributeAssignment{ + Name: name, + Value: &ast.ListLiteral{Elements: []ast.Expr{astExpr}}, + }) +} + // Lookup returns the unevaluated expression for an attribute. // Returns nil if the attribute doesn't exist. // This is useful for inspecting or copying expressions without evaluating them. diff --git a/classad/classad_test.go b/classad/classad_test.go index 67f6d1c..aed195e 100644 --- a/classad/classad_test.go +++ b/classad/classad_test.go @@ -2,6 +2,8 @@ package classad import ( "testing" + + "github.com/PelicanPlatform/classad/ast" ) func TestNewClassAd(t *testing.T) { @@ -155,6 +157,160 @@ func TestInsertAttr(t *testing.T) { } } +func TestInsertListElement(t *testing.T) { + ad := New() + + // Test creating a new list + expr1 := &Expr{expr: &ast.StringLiteral{Value: "first"}} + ad.InsertListElement("items", expr1) + if ad.Size() != 1 { + t.Errorf("Expected size 1, got %d", ad.Size()) + } + + // Verify it's a list + expr, ok := ad.Lookup("items") + if !ok || expr == nil { + t.Fatal("Attribute 'items' not found") + } + if expr.String() != "{\"first\"}" { + t.Errorf("Expected list {\"first\"}, got %s", expr.String()) + } + + // Test appending to existing list + expr2 := &Expr{expr: &ast.StringLiteral{Value: "second"}} + expr3 := &Expr{expr: &ast.IntegerLiteral{Value: 42}} + ad.InsertListElement("items", expr2) + ad.InsertListElement("items", expr3) + + expr, ok = ad.Lookup("items") + if !ok || expr == nil { + t.Fatal("Attribute 'items' not found after append") + } + expected := "{\"first\", \"second\", 42}" + if expr.String() != expected { + t.Errorf("Expected list %s, got %s", expected, expr.String()) + } + + // Test that size didn't change (still one attribute) + if ad.Size() != 1 { + t.Errorf("Expected size 1, got %d", ad.Size()) + } + + // Test replacing non-list attribute with list + ad.InsertAttr("x", 100) + expr4 := &Expr{expr: &ast.IntegerLiteral{Value: 200}} + ad.InsertListElement("x", expr4) + + expr, ok = ad.Lookup("x") + if !ok || expr == nil { + t.Fatal("Attribute 'x' not found") + } + if expr.String() != "{200}" { + t.Errorf("Expected list {200}, got %s", expr.String()) + } +} + +func TestInsertAttrList(t *testing.T) { + ad := New() + + // Test with integers + InsertAttrList(ad, "numbers", []int64{1, 2, 3, 4, 5}) + expr, ok := ad.Lookup("numbers") + if !ok || expr == nil { + t.Fatal("Attribute 'numbers' not found") + } + expected := "{1, 2, 3, 4, 5}" + if expr.String() != expected { + t.Errorf("Expected list %s, got %s", expected, expr.String()) + } + + // Test with strings + InsertAttrList(ad, "names", []string{"Alice", "Bob", "Charlie"}) + expr, ok = ad.Lookup("names") + if !ok || expr == nil { + t.Fatal("Attribute 'names' not found") + } + expected = "{\"Alice\", \"Bob\", \"Charlie\"}" + if expr.String() != expected { + t.Errorf("Expected list %s, got %s", expected, expr.String()) + } + + // Test with floats + InsertAttrList(ad, "values", []float64{1.5, 2.7, 3.14}) + expr, ok = ad.Lookup("values") + if !ok || expr == nil { + t.Fatal("Attribute 'values' not found") + } + expected = "{1.5, 2.7, 3.14}" + if expr.String() != expected { + t.Errorf("Expected list %s, got %s", expected, expr.String()) + } + + // Test with bools + InsertAttrList(ad, "flags", []bool{true, false, true}) + expr, ok = ad.Lookup("flags") + if !ok || expr == nil { + t.Fatal("Attribute 'flags' not found") + } + expected = "{true, false, true}" + if expr.String() != expected { + t.Errorf("Expected list %s, got %s", expected, expr.String()) + } + + // Test with ClassAds + ad1 := New() + ad1.InsertAttr("x", 1) + ad2 := New() + ad2.InsertAttr("y", 2) + ad3 := New() + ad3.InsertAttr("z", 3) + InsertAttrList(ad, "items", []*ClassAd{ad1, ad2, ad3}) + expr, ok = ad.Lookup("items") + if !ok || expr == nil { + t.Fatal("Attribute 'items' not found") + } + expected = "{[x = 1], [y = 2], [z = 3]}" + if expr.String() != expected { + t.Errorf("Expected list %s, got %s", expected, expr.String()) + } + + // Test with Exprs + elements := []*Expr{ + {expr: &ast.StringLiteral{Value: "hello"}}, + {expr: &ast.IntegerLiteral{Value: 42}}, + {expr: &ast.BooleanLiteral{Value: true}}, + {expr: &ast.RealLiteral{Value: 3.14}}, + } + InsertAttrList(ad, "mixed", elements) + expr, ok = ad.Lookup("mixed") + if !ok || expr == nil { + t.Fatal("Attribute 'mixed' not found") + } + expected = "{\"hello\", 42, true, 3.14}" + if expr.String() != expected { + t.Errorf("Expected list %s, got %s", expected, expr.String()) + } + + // Test empty lists + InsertAttrList(ad, "emptyInts", []int64{}) + expr, ok = ad.Lookup("emptyInts") + if !ok || expr == nil { + t.Fatal("Attribute 'emptyInts' not found") + } + if expr.String() != "{}" { + t.Errorf("Expected empty list {}, got %s", expr.String()) + } + + InsertAttrList(ad, "emptyExprs", []*Expr{}) + expr, ok = ad.Lookup("emptyExprs") + if !ok || expr == nil { + t.Fatal("Attribute 'emptyExprs' not found") + } + if expr.String() != "{}" { + t.Errorf("Expected empty list {}, got %s", expr.String()) + } +} + func TestLookup(t *testing.T) { ad := New() ad.InsertAttr("x", 10)