Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions classad/classad.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
156 changes: 156 additions & 0 deletions classad/classad_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package classad

import (
"testing"

"github.com/PelicanPlatform/classad/ast"
)

func TestNewClassAd(t *testing.T) {
Expand Down Expand Up @@ -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)
Expand Down
Loading