diff --git a/README.md b/README.md index 4c7d1cc..78c6b26 100644 --- a/README.md +++ b/README.md @@ -42,10 +42,12 @@ This project provides a complete parser and evaluation engine for the ClassAds l ```go import "github.com/PelicanPlatform/classad/classad" -// Create a ClassAd programmatically +// Create a ClassAd programmatically with generic Set() API ad := classad.New() -ad.InsertAttr("Cpus", 4) -ad.InsertAttrFloat("Memory", 8192.0) +ad.Set("Cpus", 4) +ad.Set("Memory", 8192.0) +ad.Set("Name", "worker-01") +ad.Set("Tags", []string{"production", "gpu"}) // Parse from string (new format) jobAd, err := classad.Parse(`[ @@ -67,7 +69,8 @@ defer file.Close() reader := classad.NewReader(file) for reader.Next() { ad := reader.ClassAd() - if owner, ok := ad.EvaluateAttrString("Owner"); ok { + // Use generic GetAs[T]() for type-safe retrieval + if owner, ok := classad.GetAs[string](ad, "Owner"); ok { fmt.Printf("Owner: %s\n", owner) } } @@ -77,16 +80,21 @@ if err := reader.Err(); err != nil { // Or use Go 1.23+ range-over-function: for ad := range classad.All(file) { - if owner, ok := ad.EvaluateAttrString("Owner"); ok { + if owner, ok := classad.GetAs[string](ad, "Owner"); ok { fmt.Printf("Owner: %s\n", owner) } } -// Evaluate attributes -if cpus, ok := jobAd.EvaluateAttrInt("Cpus"); ok { +// Get attributes with type-safe generic API +if cpus, ok := classad.GetAs[int](jobAd, "Cpus"); ok { fmt.Printf("Cpus = %d\n", cpus) } +// GetOr() provides defaults for missing values +owner := classad.GetOr(jobAd, "Owner", "unknown") +fmt.Printf("Owner: %s\n", owner) + +// Traditional evaluation methods still available if requirements, ok := jobAd.EvaluateAttrBool("Requirements"); ok { fmt.Printf("Requirements = %v\n", requirements) } @@ -94,6 +102,85 @@ if requirements, ok := jobAd.EvaluateAttrBool("Requirements"); ok { See [docs/EVALUATION_API.md](docs/EVALUATION_API.md) for complete API documentation. +## Generic Get/Set API + +The library provides a modern, idiomatic API using Go generics for type-safe attribute access: + +```go +ad := classad.New() + +// Set() accepts any type +ad.Set("Cpus", 4) +ad.Set("Memory", 8192.0) +ad.Set("Owner", "alice") +ad.Set("Tags", []string{"prod", "gpu"}) +ad.Set("IsActive", true) + +// GetAs[T]() for type-safe retrieval with two-value return +if cpus, ok := classad.GetAs[int](ad, "Cpus"); ok { + fmt.Printf("Cpus: %d\n", cpus) +} + +if memory, ok := classad.GetAs[float64](ad, "Memory"); ok { + fmt.Printf("Memory: %.0f MB\n", memory) +} + +// GetOr[T]() provides defaults for missing or wrong-type values +owner := classad.GetOr(ad, "Owner", "unknown") +priority := classad.GetOr(ad, "Priority", 10) // Uses default if missing + +// Works with slices +tags := classad.GetOr(ad, "Tags", []string{"default"}) + +// Type conversions happen automatically where safe +cpusFloat := classad.GetOr(ad, "Cpus", 0.0) // int -> float64 +``` + +The generic API is recommended for new code. Traditional methods like `EvaluateAttrInt()`, `InsertAttr()`, etc. remain available for compatibility. + +See [examples/generic_api_demo](examples/generic_api_demo/) for comprehensive examples. + +## Struct Marshaling + +The library supports marshaling Go structs to/from both ClassAd and JSON formats: + +```go +// Define a struct with tags +type Job struct { + ID int `classad:"JobId"` + Owner string `classad:"Owner"` + CPUs int `json:"cpus"` // Falls back to json tag + Tags []string `classad:"Tags,omitempty"` +} + +// Marshal to ClassAd format +job := Job{ID: 123, Owner: "alice", CPUs: 4, Tags: []string{"prod"}} +classadStr, _ := classad.Marshal(job) +// Result: [JobId = 123; Owner = "alice"; cpus = 4; Tags = {"prod"}] + +// Unmarshal from ClassAd format +var job2 Job +classad.Unmarshal(classadStr, &job2) + +// JSON marshaling with expression support +ad, _ := classad.Parse(`[x = 5; y = x + 3]`) +jsonBytes, _ := json.Marshal(ad) +// Result: {"x":5,"y":"\/Expr(x + 3)\/"} + +// JSON unmarshaling +var ad2 classad.ClassAd +json.Unmarshal(jsonBytes, &ad2) +``` + +Features: +- Struct tags: `classad:"name"` or falls back to `json:"name"` +- Options: `omitempty`, `-` (skip field) +- Nested structs and slices +- Map support: `map[string]T`, `map[string]interface{}` +- JSON expressions with `/Expr(...))/` format + +See [docs/MARSHALING.md](docs/MARSHALING.md) for complete marshaling documentation. + ## Project Structure ``` @@ -122,13 +209,17 @@ golang-classads/ │ ├── features_demo/ # Advanced features demo │ ├── reader_demo/ # Reader/iterator demo │ ├── range_demo/ # Go 1.23+ range-over-function demo +│ ├── struct_demo/ # Struct marshaling examples +│ ├── json_demo/ # JSON marshaling examples │ ├── simple_reader/ # CLI tool for reading ClassAd files │ ├── README.md # Examples documentation │ ├── machine.ad │ ├── job.ad │ └── expressions.txt ├── docs/ # Documentation -│ └── EVALUATION_API.md +│ ├── EVALUATION_API.md # API reference for evaluation +│ ├── MARSHALING.md # Struct marshaling guide (ClassAd & JSON) +│ └── MARSHALING_QUICKREF.md # Quick reference card ├── generate.go # go generate directive ├── go.mod └── README.md @@ -185,7 +276,38 @@ go build -o bin/classad-parser ./cmd/classad-parser ### As a Library -Using the high-level ClassAd API: +Using the modern generic API (recommended): + +```go +package main + +import ( + "fmt" + "github.com/PelicanPlatform/classad/classad" +) + +func main() { + // Create a ClassAd with Set() + ad := classad.New() + ad.Set("Cpus", 4) + ad.Set("Memory", 8192.0) + ad.Set("Owner", "alice") + ad.Set("Tags", []string{"prod", "gpu"}) + + // Type-safe retrieval with GetAs[T]() + if cpus, ok := classad.GetAs[int](ad, "Cpus"); ok { + fmt.Printf("Cpus: %d\n", cpus) + } + + // Get with defaults using GetOr[T]() + owner := classad.GetOr(ad, "Owner", "unknown") + priority := classad.GetOr(ad, "Priority", 10) + + fmt.Printf("Owner: %s, Priority: %d\n", owner, priority) +} +``` + +Using the traditional API (still supported): ```go package main diff --git a/classad/classad.go b/classad/classad.go index 370ea15..66e4e7a 100644 --- a/classad/classad.go +++ b/classad/classad.go @@ -3,7 +3,10 @@ package classad import ( + "encoding/json" "fmt" + "reflect" + "strings" "github.com/PelicanPlatform/classad/ast" "github.com/PelicanPlatform/classad/parser" @@ -418,6 +421,135 @@ func (c *ClassAd) lookupInternal(name string) ast.Expr { return nil } +// Set is a generic method that accepts any Go value and inserts it as an attribute. +// This provides a more idiomatic alternative to the type-specific Insert* methods. +// Supported types: int/int64/etc., float64, string, bool, []T, *ClassAd, *Expr, and structs. +// +// Example: +// +// ad := classad.New() +// ad.Set("cpus", 4) // int +// ad.Set("name", "job-1") // string +// ad.Set("price", 3.14) // float64 +// ad.Set("enabled", true) // bool +// ad.Set("tags", []string{"a", "b"}) // slice +// ad.Set("config", nestedClassAd) // *ClassAd +func (c *ClassAd) Set(name string, value any) error { + if value == nil { + c.Insert(name, &ast.UndefinedLiteral{}) + return nil + } + + val := reflect.ValueOf(value) + + // Handle special types first + switch v := value.(type) { + case *ClassAd: + c.InsertAttrClassAd(name, v) + return nil + case *Expr: + c.InsertExpr(name, v) + return nil + case int64: + c.InsertAttr(name, v) + return nil + case float64: + c.InsertAttrFloat(name, v) + return nil + case string: + c.InsertAttrString(name, v) + return nil + case bool: + c.InsertAttrBool(name, v) + return nil + } + + // Handle other integer types + switch val.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32: + c.InsertAttr(name, val.Int()) + return nil + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + c.InsertAttr(name, int64(val.Uint())) + return nil + case reflect.Float32: + c.InsertAttrFloat(name, val.Float()) + return nil + } + + // Try to marshal the value using the struct marshaling logic + expr, err := marshalValue(val) + if err != nil { + return fmt.Errorf("failed to marshal value of type %T: %w", value, err) + } + c.Insert(name, expr) + return nil +} + +// GetAs retrieves and evaluates an attribute, converting it to the specified type. +// This is a type-safe generic getter that handles type conversions automatically. +// Returns the zero value and false if the attribute doesn't exist or conversion fails. +// +// Example: +// +// cpus, ok := classad.GetAs[int](ad, "cpus") +// name, ok := classad.GetAs[string](ad, "name") +// price, ok := classad.GetAs[float64](ad, "price") +// tags, ok := classad.GetAs[[]string](ad, "tags") +// config, ok := classad.GetAs[*classad.ClassAd](ad, "config") +func GetAs[T any](c *ClassAd, name string) (T, bool) { + var zero T + + // Special case for *Expr - return unevaluated expression + if _, isExpr := any(zero).(*Expr); isExpr { + expr, ok := c.Lookup(name) + if !ok { + return zero, false + } + // Safe type assertion since we checked isExpr above + result, ok := any(expr).(T) + if !ok { + return zero, false + } + return result, true + } + + // For other types, evaluate the attribute + val := c.EvaluateAttr(name) + if val.IsUndefined() { + return zero, false + } + + // Try to unmarshal into the target type + target := reflect.New(reflect.TypeOf(zero)).Elem() + if err := unmarshalValueInto(val, target); err != nil { + return zero, false + } + + // Safe type assertion since unmarshalValueInto ensures type compatibility + result, ok := target.Interface().(T) + if !ok { + return zero, false + } + return result, true +} + +// GetOr retrieves and evaluates an attribute with a default value. +// If the attribute doesn't exist or conversion fails, returns the default value. +// This is a type-safe generic getter with fallback. +// +// Example: +// +// cpus := classad.GetOr(ad, "cpus", 1) // Defaults to 1 +// name := classad.GetOr(ad, "name", "unknown") // Defaults to "unknown" +// timeout := classad.GetOr(ad, "timeout", 300) // Defaults to 300 +func GetOr[T any](c *ClassAd, name string, defaultValue T) T { + if value, ok := GetAs[T](c, name); ok { + return value + } + return defaultValue +} + // Delete removes an attribute from the ClassAd. // Returns true if the attribute was found and deleted. func (c *ClassAd) Delete(name string) bool { @@ -933,3 +1065,192 @@ func (c *ClassAd) evaluateUnaryOp(op string, operand Value) Value { } return evaluator.Evaluate(tempOp) } + +// MarshalJSON implements the json.Marshaler interface for ClassAd. +// ClassAds are serialized as JSON objects where: +// - Attribute names are JSON keys +// - Simple values (strings, numbers, booleans) are JSON values +// - Lists are JSON arrays +// - Nested ClassAds are JSON objects +// - Expressions are serialized as strings with the format "/Expr()/" +// (which appears as "\/Expr()\/" in JSON due to escaping) +// +// Example: +// +// ad, _ := classad.Parse(`[x = 5; y = x + 3; name = "test"]`) +// jsonBytes, _ := json.Marshal(ad) +// // {"name":"test","x":5,"y":"\/Expr(x + 3)\/"} +func (c *ClassAd) MarshalJSON() ([]byte, error) { + if c.ad == nil { + return []byte("{}"), nil + } + + result := make(map[string]interface{}) + for _, attr := range c.ad.Attributes { + value, err := c.marshalValue(attr.Value) + if err != nil { + return nil, fmt.Errorf("failed to marshal attribute %s: %w", attr.Name, err) + } + result[attr.Name] = value + } + + jsonBytes, err := json.Marshal(result) + if err != nil { + return nil, err + } + + // Post-process to escape forward slashes in /Expr(...)/ patterns + // Go's json.Marshal doesn't escape / by default, but we prefer \/ for expressions + jsonBytes = []byte(strings.ReplaceAll(string(jsonBytes), "\"/Expr(", "\"\\/Expr(")) + jsonBytes = []byte(strings.ReplaceAll(string(jsonBytes), ")/\"", ")\\/\"")) + + return jsonBytes, nil +} + +// marshalValue converts an AST expression to a JSON-serializable value. +// Simple literals are converted directly, complex expressions are wrapped. +func (c *ClassAd) marshalValue(expr ast.Expr) (interface{}, error) { + switch v := expr.(type) { + case *ast.IntegerLiteral: + return v.Value, nil + case *ast.RealLiteral: + return v.Value, nil + case *ast.StringLiteral: + return v.Value, nil + case *ast.BooleanLiteral: + return v.Value, nil + case *ast.UndefinedLiteral: + return nil, nil + case *ast.ListLiteral: + list := make([]interface{}, len(v.Elements)) + for i, elem := range v.Elements { + val, err := c.marshalValue(elem) + if err != nil { + return nil, err + } + list[i] = val + } + return list, nil + case *ast.RecordLiteral: + // Nested ClassAd + nested := &ClassAd{ad: v.ClassAd} + nestedMap := make(map[string]interface{}) + for _, attr := range v.ClassAd.Attributes { + val, err := nested.marshalValue(attr.Value) + if err != nil { + return nil, err + } + nestedMap[attr.Name] = val + } + return nestedMap, nil + default: + // Complex expression - serialize as string with special markers + // Format: /Expr()/ + exprStr := expr.String() + return fmt.Sprintf("/Expr(%s)/", exprStr), nil + } +} + +// UnmarshalJSON implements the json.Unmarshaler interface for ClassAd. +// Deserializes JSON into a ClassAd, handling the special expression format. +// Strings matching the pattern "/Expr()/" are parsed as expressions. +// Note: When unmarshaling from JSON, the Go json package automatically unescapes +// "\/Expr(...)\/" to "/Expr(...)/" so only the unescaped format is checked. +// +// Example: +// +// jsonStr := `{"name":"test","x":5,"y":"\/Expr(x + 3)\/"}` +// var ad ClassAd +// json.Unmarshal([]byte(jsonStr), &ad) +func (c *ClassAd) UnmarshalJSON(data []byte) error { + var raw map[string]interface{} + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + attributes := make([]*ast.AttributeAssignment, 0, len(raw)) + for name, value := range raw { + expr, err := c.unmarshalValue(value) + if err != nil { + return fmt.Errorf("failed to unmarshal attribute %s: %w", name, err) + } + attributes = append(attributes, &ast.AttributeAssignment{ + Name: name, + Value: expr, + }) + } + + c.ad = &ast.ClassAd{Attributes: attributes} + return nil +} + +// unmarshalValue converts a JSON value back into an AST expression. +func (c *ClassAd) unmarshalValue(value interface{}) (ast.Expr, error) { + switch v := value.(type) { + case nil: + return &ast.UndefinedLiteral{}, nil + case bool: + return &ast.BooleanLiteral{Value: v}, nil + case float64: + // JSON numbers are always float64 + // Check if it's actually an integer + if v == float64(int64(v)) { + return &ast.IntegerLiteral{Value: int64(v)}, nil + } + return &ast.RealLiteral{Value: v}, nil + case string: + // Check if it's an expression string + // Only accept the format /Expr(...)/ + if strings.HasPrefix(v, "/Expr(") && strings.HasSuffix(v, ")/") { + exprStr := v[6 : len(v)-2] // Remove "/Expr(" and ")/" + return c.parseExpression(exprStr) + } + // Regular string literal + return &ast.StringLiteral{Value: v}, nil + case []interface{}: + // JSON array -> ListLiteral + elements := make([]ast.Expr, len(v)) + for i, elem := range v { + expr, err := c.unmarshalValue(elem) + if err != nil { + return nil, err + } + elements[i] = expr + } + return &ast.ListLiteral{Elements: elements}, nil + case map[string]interface{}: + // JSON object -> RecordLiteral (nested ClassAd) + attributes := make([]*ast.AttributeAssignment, 0, len(v)) + for name, val := range v { + expr, err := c.unmarshalValue(val) + if err != nil { + return nil, err + } + attributes = append(attributes, &ast.AttributeAssignment{ + Name: name, + Value: expr, + }) + } + return &ast.RecordLiteral{ + ClassAd: &ast.ClassAd{Attributes: attributes}, + }, nil + default: + return nil, fmt.Errorf("unsupported JSON value type: %T", value) + } +} + +// parseExpression parses an expression string into an AST expression. +func (c *ClassAd) parseExpression(exprStr string) (ast.Expr, error) { + // Wrap in a temporary ClassAd for parsing + wrapped := fmt.Sprintf("[__tmp__ = %s]", exprStr) + node, err := parser.Parse(wrapped) + if err != nil { + return nil, fmt.Errorf("failed to parse expression %q: %w", exprStr, err) + } + + if ad, ok := node.(*ast.ClassAd); ok && len(ad.Attributes) == 1 { + return ad.Attributes[0].Value, nil + } + + return nil, fmt.Errorf("unable to extract expression from parsed result") +} diff --git a/classad/generic_api_test.go b/classad/generic_api_test.go new file mode 100644 index 0000000..0f35037 --- /dev/null +++ b/classad/generic_api_test.go @@ -0,0 +1,449 @@ +package classad + +import ( + "testing" +) + +// Tests for the generic Set(), GetAs(), and GetOr() API + +func TestSet_BasicTypes(t *testing.T) { + ad := New() + + // Test integer types + if err := ad.Set("int", 42); err != nil { + t.Fatalf("Set int failed: %v", err) + } + if err := ad.Set("int64", int64(100)); err != nil { + t.Fatalf("Set int64 failed: %v", err) + } + if err := ad.Set("uint", uint(50)); err != nil { + t.Fatalf("Set uint failed: %v", err) + } + + // Test float + if err := ad.Set("float", 3.14); err != nil { + t.Fatalf("Set float failed: %v", err) + } + + // Test string + if err := ad.Set("name", "test"); err != nil { + t.Fatalf("Set string failed: %v", err) + } + + // Test bool + if err := ad.Set("enabled", true); err != nil { + t.Fatalf("Set bool failed: %v", err) + } + + // Verify values + if val, ok := ad.EvaluateAttrInt("int"); !ok || val != 42 { + t.Errorf("Expected int=42, got %d", val) + } + if val, ok := ad.EvaluateAttrInt("int64"); !ok || val != 100 { + t.Errorf("Expected int64=100, got %d", val) + } + if val, ok := ad.EvaluateAttrReal("float"); !ok || val != 3.14 { + t.Errorf("Expected float=3.14, got %f", val) + } + if val, ok := ad.EvaluateAttrString("name"); !ok || val != "test" { + t.Errorf("Expected name='test', got %q", val) + } + if val, ok := ad.EvaluateAttrBool("enabled"); !ok || !val { + t.Errorf("Expected enabled=true, got %v", val) + } +} + +func TestSet_Slices(t *testing.T) { + ad := New() + + // Test string slice + if err := ad.Set("tags", []string{"a", "b", "c"}); err != nil { + t.Fatalf("Set string slice failed: %v", err) + } + + // Test int slice + if err := ad.Set("numbers", []int{1, 2, 3}); err != nil { + t.Fatalf("Set int slice failed: %v", err) + } + + // Verify slices + tagsVal := ad.EvaluateAttr("tags") + if !tagsVal.IsList() { + t.Errorf("Expected tags to be a list") + } + + numbersVal := ad.EvaluateAttr("numbers") + if !numbersVal.IsList() { + t.Errorf("Expected numbers to be a list") + } +} + +func TestSet_ClassAdAndExpr(t *testing.T) { + ad := New() + + // Test *ClassAd + nested := New() + nested.InsertAttr("x", 10) + nested.InsertAttrString("y", "hello") + + if err := ad.Set("config", nested); err != nil { + t.Fatalf("Set *ClassAd failed: %v", err) + } + + configVal := ad.EvaluateAttr("config") + if !configVal.IsClassAd() { + t.Errorf("Expected config to be a ClassAd") + } + + // Test *Expr + expr, _ := ParseExpr("x + 10") + if err := ad.Set("formula", expr); err != nil { + t.Fatalf("Set *Expr failed: %v", err) + } + + formulaExpr, ok := ad.Lookup("formula") + if !ok { + t.Errorf("Expected formula attribute to exist") + } + if formulaExpr.String() != "(x + 10)" { + t.Errorf("Expected formula='(x + 10)', got %q", formulaExpr.String()) + } +} + +func TestSet_Nil(t *testing.T) { + ad := New() + + if err := ad.Set("nil_value", nil); err != nil { + t.Fatalf("Set nil failed: %v", err) + } + + val := ad.EvaluateAttr("nil_value") + if !val.IsUndefined() { + t.Errorf("Expected nil_value to be undefined") + } +} + +func TestSet_Struct(t *testing.T) { + ad := New() + + type Config struct { + Timeout int + Server string + } + + config := Config{Timeout: 30, Server: "example.com"} + if err := ad.Set("config", config); err != nil { + t.Fatalf("Set struct failed: %v", err) + } + + configVal := ad.EvaluateAttr("config") + if !configVal.IsClassAd() { + t.Errorf("Expected config to be a ClassAd") + } +} + +func TestGetAs_BasicTypes(t *testing.T) { + ad := New() + ad.InsertAttr("int", 42) + ad.InsertAttrFloat("float", 3.14) + ad.InsertAttrString("name", "test") + ad.InsertAttrBool("enabled", true) + + // Test int + intVal, ok := GetAs[int](ad, "int") + if !ok || intVal != 42 { + t.Errorf("Expected int=42, got %d (ok=%v)", intVal, ok) + } + + // Test int64 + int64Val, ok := GetAs[int64](ad, "int") + if !ok || int64Val != 42 { + t.Errorf("Expected int64=42, got %d (ok=%v)", int64Val, ok) + } + + // Test float64 + floatVal, ok := GetAs[float64](ad, "float") + if !ok || floatVal != 3.14 { + t.Errorf("Expected float=3.14, got %f (ok=%v)", floatVal, ok) + } + + // Test string + strVal, ok := GetAs[string](ad, "name") + if !ok || strVal != "test" { + t.Errorf("Expected name='test', got %q (ok=%v)", strVal, ok) + } + + // Test bool + boolVal, ok := GetAs[bool](ad, "enabled") + if !ok || !boolVal { + t.Errorf("Expected enabled=true, got %v (ok=%v)", boolVal, ok) + } +} + +func TestGetAs_Slice(t *testing.T) { + ad := New() + InsertAttrList(ad, "tags", []string{"a", "b", "c"}) + InsertAttrList(ad, "numbers", []int64{1, 2, 3}) + + // Test string slice + tags, ok := GetAs[[]string](ad, "tags") + if !ok { + t.Fatal("Expected tags to be retrievable") + } + if len(tags) != 3 || tags[0] != "a" || tags[1] != "b" || tags[2] != "c" { + t.Errorf("Expected tags=[a,b,c], got %v", tags) + } + + // Test int slice + numbers, ok := GetAs[[]int](ad, "numbers") + if !ok { + t.Fatal("Expected numbers to be retrievable") + } + if len(numbers) != 3 || numbers[0] != 1 || numbers[1] != 2 || numbers[2] != 3 { + t.Errorf("Expected numbers=[1,2,3], got %v", numbers) + } +} + +func TestGetAs_ClassAd(t *testing.T) { + ad := New() + nested := New() + nested.InsertAttr("x", 10) + nested.InsertAttrString("y", "hello") + ad.InsertAttrClassAd("config", nested) + + // Test *ClassAd retrieval + config, ok := GetAs[*ClassAd](ad, "config") + if !ok { + t.Fatal("Expected config to be retrievable") + } + + x, ok := config.EvaluateAttrInt("x") + if !ok || x != 10 { + t.Errorf("Expected x=10, got %d", x) + } + + y, ok := config.EvaluateAttrString("y") + if !ok || y != "hello" { + t.Errorf("Expected y='hello', got %q", y) + } +} + +func TestGetAs_Expr(t *testing.T) { + ad := New() + expr, _ := ParseExpr("x + 10") + ad.InsertExpr("formula", expr) + + // Test *Expr retrieval (should return unevaluated) + formula, ok := GetAs[*Expr](ad, "formula") + if !ok { + t.Fatal("Expected formula to be retrievable") + } + + if formula.String() != "(x + 10)" { + t.Errorf("Expected formula='(x + 10)', got %q", formula.String()) + } +} + +func TestGetAs_Missing(t *testing.T) { + ad := New() + + // Test missing attribute + val, ok := GetAs[int](ad, "missing") + if ok { + t.Errorf("Expected missing attribute to return false") + } + if val != 0 { + t.Errorf("Expected zero value, got %d", val) + } + + strVal, ok := GetAs[string](ad, "missing") + if ok { + t.Errorf("Expected missing attribute to return false") + } + if strVal != "" { + t.Errorf("Expected empty string, got %q", strVal) + } +} + +func TestGetAs_TypeConversion(t *testing.T) { + ad := New() + ad.InsertAttr("value", 42) + + // Test int to float conversion + floatVal, ok := GetAs[float64](ad, "value") + if !ok || floatVal != 42.0 { + t.Errorf("Expected float=42.0, got %f (ok=%v)", floatVal, ok) + } + + // Test with real value to int conversion + ad.InsertAttrFloat("real", 3.7) + intVal, ok := GetAs[int](ad, "real") + if !ok || intVal != 3 { + t.Errorf("Expected int=3 (truncated), got %d (ok=%v)", intVal, ok) + } +} + +func TestGetAs_Struct(t *testing.T) { + ad := New() + + // Create a nested ClassAd for config + configAd := New() + configAd.InsertAttr("timeout", 30) + configAd.InsertAttrString("server", "example.com") + ad.InsertAttrClassAd("config", configAd) + + type Config struct { + Timeout int `classad:"timeout"` + Server string `classad:"server"` + } + + // Get the nested ClassAd and unmarshal it to a struct + config, ok := GetAs[*ClassAd](ad, "config") + if !ok { + t.Fatal("Expected config ClassAd to be retrievable") + } + + // Now unmarshal the ClassAd into a struct + var configStruct Config + err := Unmarshal(config.String(), &configStruct) + if err != nil { + t.Fatalf("Failed to unmarshal config: %v", err) + } + + if configStruct.Timeout != 30 { + t.Errorf("Expected timeout=30, got %d", configStruct.Timeout) + } + if configStruct.Server != "example.com" { + t.Errorf("Expected server='example.com', got %q", configStruct.Server) + } +} + +func TestGetOr_WithDefault(t *testing.T) { + ad := New() + ad.InsertAttr("cpus", 4) + ad.InsertAttrString("name", "job-1") + + // Test existing values + cpus := GetOr(ad, "cpus", 1) + if cpus != 4 { + t.Errorf("Expected cpus=4, got %d", cpus) + } + + name := GetOr(ad, "name", "unknown") + if name != "job-1" { + t.Errorf("Expected name='job-1', got %q", name) + } + + // Test missing values (should return defaults) + timeout := GetOr(ad, "timeout", 300) + if timeout != 300 { + t.Errorf("Expected timeout=300 (default), got %d", timeout) + } + + owner := GetOr(ad, "owner", "unknown") + if owner != "unknown" { + t.Errorf("Expected owner='unknown' (default), got %q", owner) + } + + enabled := GetOr(ad, "enabled", true) + if !enabled { + t.Errorf("Expected enabled=true (default), got %v", enabled) + } +} + +func TestGetOr_Slice(t *testing.T) { + ad := New() + InsertAttrList(ad, "tags", []string{"a", "b"}) + + // Test existing slice + tags := GetOr(ad, "tags", []string{"default"}) + if len(tags) != 2 || tags[0] != "a" || tags[1] != "b" { + t.Errorf("Expected tags=[a,b], got %v", tags) + } + + // Test missing slice (should return default) + labels := GetOr(ad, "labels", []string{"x", "y"}) + if len(labels) != 2 || labels[0] != "x" || labels[1] != "y" { + t.Errorf("Expected labels=[x,y] (default), got %v", labels) + } +} + +func TestGetOr_ZeroValue(t *testing.T) { + ad := New() + ad.InsertAttr("zero", 0) + ad.InsertAttrString("empty", "") + + // Zero value exists, should return it (not default) + zero := GetOr(ad, "zero", 999) + if zero != 0 { + t.Errorf("Expected zero=0, got %d", zero) + } + + // Empty string exists, should return it (not default) + empty := GetOr(ad, "empty", "default") + if empty != "" { + t.Errorf("Expected empty='', got %q", empty) + } +} + +func TestRoundTrip_GenericAPI(t *testing.T) { + ad := New() + + // Set values using generic API + ad.Set("cpus", 4) + ad.Set("memory", 8192) + ad.Set("name", "test-job") + ad.Set("enabled", true) + ad.Set("price", 0.05) + ad.Set("tags", []string{"prod", "critical"}) + + // Get values using generic API + cpus := GetOr(ad, "cpus", 1) + memory := GetOr(ad, "memory", 1024) + name := GetOr(ad, "name", "unknown") + enabled := GetOr(ad, "enabled", false) + price := GetOr(ad, "price", 0.0) + tags := GetOr(ad, "tags", []string{}) + + // Verify + if cpus != 4 { + t.Errorf("Expected cpus=4, got %d", cpus) + } + if memory != 8192 { + t.Errorf("Expected memory=8192, got %d", memory) + } + if name != "test-job" { + t.Errorf("Expected name='test-job', got %q", name) + } + if !enabled { + t.Errorf("Expected enabled=true, got %v", enabled) + } + if price != 0.05 { + t.Errorf("Expected price=0.05, got %f", price) + } + if len(tags) != 2 || tags[0] != "prod" || tags[1] != "critical" { + t.Errorf("Expected tags=[prod,critical], got %v", tags) + } +} + +func TestGenericAPI_WithExpressionEvaluation(t *testing.T) { + ad := New() + ad.Set("base", 10) + + // Set an expression + expr, _ := ParseExpr("base * 2") + ad.Set("computed", expr) + + // Get the unevaluated expression + computedExpr, ok := GetAs[*Expr](ad, "computed") + if !ok { + t.Fatal("Expected computed expression to be retrievable") + } + + // Evaluate in context + result := computedExpr.Eval(ad) + resultInt, _ := result.IntValue() + if resultInt != 20 { + t.Errorf("Expected computed=20 (10*2), got %d", resultInt) + } +} diff --git a/classad/json_test.go b/classad/json_test.go new file mode 100644 index 0000000..6de0da9 --- /dev/null +++ b/classad/json_test.go @@ -0,0 +1,470 @@ +package classad + +import ( + "encoding/json" + "testing" +) + +func TestMarshalJSON_SimpleValues(t *testing.T) { + ad, err := Parse(`[ + x = 5; + y = 3.14; + name = "test"; + active = true; + inactive = false + ]`) + if err != nil { + t.Fatalf("Failed to parse ClassAd: %v", err) + } + + jsonBytes, err := json.Marshal(ad) + if err != nil { + t.Fatalf("Failed to marshal to JSON: %v", err) + } + + // Unmarshal to verify structure + var result map[string]interface{} + if err := json.Unmarshal(jsonBytes, &result); err != nil { + t.Fatalf("Failed to unmarshal JSON: %v", err) + } + + // Verify values + if result["x"] != float64(5) { + t.Errorf("Expected x=5, got %v", result["x"]) + } + if result["y"] != 3.14 { + t.Errorf("Expected y=3.14, got %v", result["y"]) + } + if result["name"] != "test" { + t.Errorf("Expected name='test', got %v", result["name"]) + } + if result["active"] != true { + t.Errorf("Expected active=true, got %v", result["active"]) + } + if result["inactive"] != false { + t.Errorf("Expected inactive=false, got %v", result["inactive"]) + } +} + +func TestMarshalJSON_Expression(t *testing.T) { + ad, err := Parse(`[ + x = 5; + y = x + 3 + ]`) + if err != nil { + t.Fatalf("Failed to parse ClassAd: %v", err) + } + + jsonBytes, err := json.Marshal(ad) + if err != nil { + t.Fatalf("Failed to marshal to JSON: %v", err) + } + + // Unmarshal to check the expression format + var result map[string]interface{} + if err := json.Unmarshal(jsonBytes, &result); err != nil { + t.Fatalf("Failed to unmarshal JSON: %v", err) + } + + // x should be a simple number + if result["x"] != float64(5) { + t.Errorf("Expected x=5, got %v", result["x"]) + } + + // y should be an expression string + // Note: json.Unmarshal automatically converts \/ to / when parsing JSON + yStr, ok := result["y"].(string) + if !ok { + t.Fatalf("Expected y to be a string, got %T", result["y"]) + } + if yStr != "/Expr((x + 3))/" { + t.Errorf("Expected y to be '/Expr((x + 3))/', got %q", yStr) + } +} + +func TestMarshalJSON_List(t *testing.T) { + ad, err := Parse(`[nums = {1, 2, 3, 4, 5}]`) + if err != nil { + t.Fatalf("Failed to parse ClassAd: %v", err) + } + + jsonBytes, err := json.Marshal(ad) + if err != nil { + t.Fatalf("Failed to marshal to JSON: %v", err) + } + + var result map[string]interface{} + if err := json.Unmarshal(jsonBytes, &result); err != nil { + t.Fatalf("Failed to unmarshal JSON: %v", err) + } + + numsList, ok := result["nums"].([]interface{}) + if !ok { + t.Fatalf("Expected nums to be a list, got %T", result["nums"]) + } + + expected := []float64{1, 2, 3, 4, 5} + if len(numsList) != len(expected) { + t.Fatalf("Expected list length %d, got %d", len(expected), len(numsList)) + } + + for i, exp := range expected { + if numsList[i] != exp { + t.Errorf("Element %d: expected %v, got %v", i, exp, numsList[i]) + } + } +} + +func TestMarshalJSON_NestedClassAd(t *testing.T) { + ad, err := Parse(`[ + config = [ + timeout = 30; + retries = 3; + server = "example.com" + ]; + enabled = true + ]`) + if err != nil { + t.Fatalf("Failed to parse ClassAd: %v", err) + } + + jsonBytes, err := json.Marshal(ad) + if err != nil { + t.Fatalf("Failed to marshal to JSON: %v", err) + } + + var result map[string]interface{} + if err := json.Unmarshal(jsonBytes, &result); err != nil { + t.Fatalf("Failed to unmarshal JSON: %v", err) + } + + // Check nested object + config, ok := result["config"].(map[string]interface{}) + if !ok { + t.Fatalf("Expected config to be a nested object, got %T", result["config"]) + } + + if config["timeout"] != float64(30) { + t.Errorf("Expected timeout=30, got %v", config["timeout"]) + } + if config["retries"] != float64(3) { + t.Errorf("Expected retries=3, got %v", config["retries"]) + } + if config["server"] != "example.com" { + t.Errorf("Expected server='example.com', got %v", config["server"]) + } +} + +func TestUnmarshalJSON_SimpleValues(t *testing.T) { + jsonStr := `{ + "x": 5, + "y": 3.14, + "name": "test", + "active": true, + "inactive": false + }` + + var ad ClassAd + if err := json.Unmarshal([]byte(jsonStr), &ad); err != nil { + t.Fatalf("Failed to unmarshal JSON: %v", err) + } + + // Evaluate and check values + x := ad.EvaluateAttr("x") + if !x.IsInteger() || x.String() != "5" { + t.Errorf("Expected x=5, got %v", x) + } + + y := ad.EvaluateAttr("y") + if !y.IsReal() { + t.Errorf("Expected y to be real, got %v", y.Type()) + } + + name := ad.EvaluateAttr("name") + if !name.IsString() { + t.Errorf("Expected name to be string, got %v", name.Type()) + } + nameStr, _ := name.StringValue() + if nameStr != "test" { + t.Errorf("Expected name='test', got %q", nameStr) + } + + active := ad.EvaluateAttr("active") + if !active.IsBool() { + t.Errorf("Expected active to be bool, got %v", active.Type()) + } + activeBool, _ := active.BoolValue() + if !activeBool { + t.Errorf("Expected active=true, got false") + } +} + +func TestUnmarshalJSON_Expression(t *testing.T) { + // JSON with \/Expr format (forward slash escaped in JSON) + jsonStr := "{\"x\": 5, \"y\": \"\\/Expr(x + 3)\\/\"}" + + var ad ClassAd + if err := json.Unmarshal([]byte(jsonStr), &ad); err != nil { + t.Fatalf("Failed to unmarshal JSON: %v", err) + } + + // x should evaluate to 5 + x := ad.EvaluateAttr("x") + if !x.IsInteger() { + t.Fatalf("Expected x to be integer, got %v", x.Type()) + } + xInt, _ := x.IntValue() + if xInt != 5 { + t.Errorf("Expected x=5, got %d", xInt) + } + + // y should evaluate to 8 (x + 3) + y := ad.EvaluateAttr("y") + if !y.IsInteger() { + t.Fatalf("Expected y to be integer, got %v", y.Type()) + } + yInt, _ := y.IntValue() + if yInt != 8 { + t.Errorf("Expected y=8, got %d", yInt) + } +} + +func TestUnmarshalJSON_List(t *testing.T) { + jsonStr := `{"nums": [1, 2, 3, 4, 5]}` + + var ad ClassAd + if err := json.Unmarshal([]byte(jsonStr), &ad); err != nil { + t.Fatalf("Failed to unmarshal JSON: %v", err) + } + + nums := ad.EvaluateAttr("nums") + if !nums.IsList() { + t.Fatalf("Expected nums to be a list, got %v", nums.Type()) + } + + numsList, _ := nums.ListValue() + expected := []int64{1, 2, 3, 4, 5} + if len(numsList) != len(expected) { + t.Fatalf("Expected list length %d, got %d", len(expected), len(numsList)) + } + + for i, exp := range expected { + if !numsList[i].IsInteger() { + t.Errorf("Element %d: expected integer, got %v", i, numsList[i].Type()) + continue + } + val, _ := numsList[i].IntValue() + if val != exp { + t.Errorf("Element %d: expected %d, got %d", i, exp, val) + } + } +} + +func TestUnmarshalJSON_NestedClassAd(t *testing.T) { + jsonStr := `{ + "config": { + "timeout": 30, + "retries": 3, + "server": "example.com" + }, + "enabled": true + }` + + var ad ClassAd + if err := json.Unmarshal([]byte(jsonStr), &ad); err != nil { + t.Fatalf("Failed to unmarshal JSON: %v", err) + } + + // Check nested ClassAd + config := ad.EvaluateAttr("config") + if !config.IsClassAd() { + t.Fatalf("Expected config to be a ClassAd, got %v", config.Type()) + } + + configAd, _ := config.ClassAdValue() + timeout := configAd.EvaluateAttr("timeout") + if !timeout.IsInteger() { + t.Errorf("Expected timeout to be integer, got %v", timeout.Type()) + } + timeoutInt, _ := timeout.IntValue() + if timeoutInt != 30 { + t.Errorf("Expected timeout=30, got %d", timeoutInt) + } +} + +func TestRoundTrip_ComplexClassAd(t *testing.T) { + // Create a complex ClassAd + original := `[ + x = 10; + y = x * 2; + name = "test"; + nums = {1, 2, 3}; + config = [a = 1; b = 2]; + result = x + y + ]` + + ad, err := Parse(original) + if err != nil { + t.Fatalf("Failed to parse ClassAd: %v", err) + } + + // Marshal to JSON + jsonBytes, err := json.Marshal(ad) + if err != nil { + t.Fatalf("Failed to marshal to JSON: %v", err) + } + + // Unmarshal back + var ad2 ClassAd + if err := json.Unmarshal(jsonBytes, &ad2); err != nil { + t.Fatalf("Failed to unmarshal JSON: %v", err) + } + + // Compare results + result1 := ad.EvaluateAttr("result") + result2 := ad2.EvaluateAttr("result") + + if result1.Type() != result2.Type() { + t.Errorf("Type mismatch: %v vs %v", result1.Type(), result2.Type()) + } + + if result1.String() != result2.String() { + t.Errorf("Value mismatch: %v vs %v", result1, result2) + } +} + +func TestMarshalJSON_UndefinedValue(t *testing.T) { + ad, err := Parse(`[x = undefined]`) + if err != nil { + t.Fatalf("Failed to parse ClassAd: %v", err) + } + + jsonBytes, err := json.Marshal(ad) + if err != nil { + t.Fatalf("Failed to marshal to JSON: %v", err) + } + + var result map[string]interface{} + if err := json.Unmarshal(jsonBytes, &result); err != nil { + t.Fatalf("Failed to unmarshal JSON: %v", err) + } + + if result["x"] != nil { + t.Errorf("Expected x to be null, got %v", result["x"]) + } +} + +func TestUnmarshalJSON_ComplexExpression(t *testing.T) { + // JSON with \/Expr format (forward slash escaped in JSON) + jsonStr := "{\"cpus\": 4, \"memory\": 8192, \"score\": \"\\/Expr((cpus * 100) + (memory / 1024))\\/\"}" + + var ad ClassAd + if err := json.Unmarshal([]byte(jsonStr), &ad); err != nil { + t.Fatalf("Failed to unmarshal JSON: %v", err) + } + + // Evaluate the expression + score := ad.EvaluateAttr("score") + if !score.IsInteger() { + t.Fatalf("Expected score to be integer, got %v", score.Type()) + } + + // score should be (4 * 100) + (8192 / 1024) = 400 + 8 = 408 + scoreInt, _ := score.IntValue() + if scoreInt != 408 { + t.Errorf("Expected score=408, got %d", scoreInt) + } +} + +func TestMarshalJSON_EmptyClassAd(t *testing.T) { + ad := New() + jsonBytes, err := json.Marshal(ad) + if err != nil { + t.Fatalf("Failed to marshal empty ClassAd: %v", err) + } + + if string(jsonBytes) != "{}" { + t.Errorf("Expected '{}', got %q", string(jsonBytes)) + } +} + +func TestUnmarshalJSON_EmptyObject(t *testing.T) { + jsonStr := `{}` + var ad ClassAd + if err := json.Unmarshal([]byte(jsonStr), &ad); err != nil { + t.Fatalf("Failed to unmarshal empty JSON: %v", err) + } + + // Should have no attributes + if ad.ad != nil && len(ad.ad.Attributes) != 0 { + t.Errorf("Expected 0 attributes, got %d", len(ad.ad.Attributes)) + } +} + +func TestMarshalJSON_ConditionalExpression(t *testing.T) { + ad, err := Parse(`[x = 5; result = x > 3 ? "yes" : "no"]`) + if err != nil { + t.Fatalf("Failed to parse ClassAd: %v", err) + } + + jsonBytes, err := json.Marshal(ad) + if err != nil { + t.Fatalf("Failed to marshal to JSON: %v", err) + } + + // Unmarshal and check + var ad2 ClassAd + if err := json.Unmarshal(jsonBytes, &ad2); err != nil { + t.Fatalf("Failed to unmarshal JSON: %v", err) + } + + result := ad2.EvaluateAttr("result") + if !result.IsString() { + t.Fatalf("Expected result to be string, got %v", result.Type()) + } + resultStr, _ := result.StringValue() + if resultStr != "yes" { + t.Errorf("Expected result='yes', got %q", resultStr) + } +} + +func TestUnmarshalJSON_BothExpressionFormats(t *testing.T) { + // The JSON standard allows both / and \/ as equivalent representations. + // When Go's json.Unmarshal processes JSON, it automatically converts + // "\/Expr(...)\/" to "/Expr(...)/", so we only need to accept the + // unescaped format in our code. + tests := []struct { + name string + jsonStr string + expected int64 + }{ + { + name: "Escaped format in JSON (auto-unescaped by json.Unmarshal)", + jsonStr: "{\"x\": 5, \"y\": \"\\/Expr(x + 3)\\/\"}", + expected: 8, + }, + { + name: "Unescaped format", + jsonStr: "{\"x\": 5, \"y\": \"/Expr(x + 3)/\"}", + expected: 8, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var ad ClassAd + if err := json.Unmarshal([]byte(tt.jsonStr), &ad); err != nil { + t.Fatalf("Failed to unmarshal JSON: %v", err) + } + + y := ad.EvaluateAttr("y") + if !y.IsInteger() { + t.Fatalf("Expected y to be integer, got %v", y.Type()) + } + yInt, _ := y.IntValue() + if yInt != tt.expected { + t.Errorf("Expected y=%d, got %d", tt.expected, yInt) + } + }) + } +} diff --git a/classad/struct.go b/classad/struct.go new file mode 100644 index 0000000..7211d74 --- /dev/null +++ b/classad/struct.go @@ -0,0 +1,615 @@ +package classad + +import ( + "fmt" + "reflect" + "strings" + + "github.com/PelicanPlatform/classad/ast" +) + +// Marshal converts a Go value to a ClassAd string representation. +// It works similarly to encoding/json.Marshal but produces ClassAd format. +// Struct fields can use "classad" struct tags to control marshaling behavior. +// If no "classad" tag is present, it falls back to the "json" tag. +// +// Supported struct tag options: +// - Field name: classad:"custom_name" or json:"custom_name" +// - Omit field: classad:"-" or json:"-" +// - Omit if empty: classad:"name,omitempty" or json:"name,omitempty" +// +// Example: +// +// type Job struct { +// ID int `classad:"JobId"` +// Name string `classad:"Name"` +// CPUs int `json:"cpus"` // falls back to json tag +// Priority int // uses field name "Priority" +// } +// job := Job{ID: 123, Name: "test", CPUs: 4, Priority: 10} +// classadStr, err := classad.Marshal(job) +// // Result: [JobId = 123; Name = "test"; cpus = 4; Priority = 10] +func Marshal(v interface{}) (string, error) { + val := reflect.ValueOf(v) + expr, err := marshalValue(val) + if err != nil { + return "", err + } + + // If it's a ClassAd (struct), format it properly + if record, ok := expr.(*ast.RecordLiteral); ok { + ad := &ClassAd{ad: record.ClassAd} + return ad.String(), nil + } + + // Otherwise, just return the expression string + return expr.String(), nil +} + +// Unmarshal parses a ClassAd string and stores the result in the value pointed to by v. +// It works similarly to encoding/json.Unmarshal but expects ClassAd format. +// Struct fields can use "classad" struct tags to control unmarshaling behavior. +// If no "classad" tag is present, it falls back to the "json" tag. +// +// Example: +// +// type Job struct { +// ID int `classad:"JobId"` +// Name string `classad:"Name"` +// } +// var job Job +// err := classad.Unmarshal("[JobId = 123; Name = \"test\"]", &job) +func Unmarshal(data string, v interface{}) error { + // Parse the ClassAd string + ad, err := Parse(data) + if err != nil { + return err + } + + // Unmarshal into the provided value + return unmarshalInto(ad, v) +} + +// marshalValue converts a Go reflect.Value to an AST expression +func marshalValue(val reflect.Value) (ast.Expr, error) { + // Handle pointers - check for special types first + if val.Kind() == reflect.Ptr { + if val.IsNil() { + return &ast.UndefinedLiteral{}, nil + } + // Check if it's a *ClassAd or *Expr type + if classad, ok := val.Interface().(*ClassAd); ok { + if classad == nil || classad.ad == nil { + return &ast.UndefinedLiteral{}, nil + } + return &ast.RecordLiteral{ClassAd: classad.ad}, nil + } + if expr, ok := val.Interface().(*Expr); ok { + if expr == nil || expr.expr == nil { + return &ast.UndefinedLiteral{}, nil + } + return expr.expr, nil + } + val = val.Elem() + } + + // Handle interfaces + if val.Kind() == reflect.Interface { + if val.IsNil() { + return &ast.UndefinedLiteral{}, nil + } + val = val.Elem() + } + + switch val.Kind() { + case reflect.Bool: + return &ast.BooleanLiteral{Value: val.Bool()}, nil + + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return &ast.IntegerLiteral{Value: val.Int()}, nil + + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return &ast.IntegerLiteral{Value: int64(val.Uint())}, nil + + case reflect.Float32, reflect.Float64: + return &ast.RealLiteral{Value: val.Float()}, nil + + case reflect.String: + return &ast.StringLiteral{Value: val.String()}, nil + + case reflect.Slice, reflect.Array: + elements := make([]ast.Expr, val.Len()) + for i := 0; i < val.Len(); i++ { + elem, err := marshalValue(val.Index(i)) + if err != nil { + return nil, fmt.Errorf("slice element %d: %w", i, err) + } + elements[i] = elem + } + return &ast.ListLiteral{Elements: elements}, nil + + case reflect.Map: + if val.Type().Key().Kind() != reflect.String { + return nil, fmt.Errorf("maps must have string keys, got %v", val.Type().Key().Kind()) + } + attributes := make([]*ast.AttributeAssignment, 0, val.Len()) + iter := val.MapRange() + for iter.Next() { + key := iter.Key().String() + value, err := marshalValue(iter.Value()) + if err != nil { + return nil, fmt.Errorf("map key %q: %w", key, err) + } + attributes = append(attributes, &ast.AttributeAssignment{ + Name: key, + Value: value, + }) + } + return &ast.RecordLiteral{ClassAd: &ast.ClassAd{Attributes: attributes}}, nil + + case reflect.Struct: + return marshalStruct(val) + + default: + return nil, fmt.Errorf("unsupported type: %v", val.Type()) + } +} + +// marshalStruct converts a struct to a ClassAd record +func marshalStruct(val reflect.Value) (ast.Expr, error) { + typ := val.Type() + attributes := make([]*ast.AttributeAssignment, 0) + + for i := 0; i < typ.NumField(); i++ { + field := typ.Field(i) + fieldVal := val.Field(i) + + // Skip unexported fields + if !field.IsExported() { + continue + } + + // Get field name and options from struct tags + name, opts := parseStructTag(field) + if name == "-" { + continue // Skip this field + } + + // Handle omitempty + if opts.omitEmpty && isEmptyValue(fieldVal) { + continue + } + + // Marshal the field value + expr, err := marshalValue(fieldVal) + if err != nil { + return nil, fmt.Errorf("field %s: %w", field.Name, err) + } + + attributes = append(attributes, &ast.AttributeAssignment{ + Name: name, + Value: expr, + }) + } + + return &ast.RecordLiteral{ClassAd: &ast.ClassAd{Attributes: attributes}}, nil +} + +// unmarshalInto unmarshals a ClassAd into a Go value +func unmarshalInto(ad *ClassAd, v interface{}) error { + val := reflect.ValueOf(v) + if val.Kind() != reflect.Ptr { + return fmt.Errorf("unmarshal target must be a pointer, got %v", val.Type()) + } + if val.IsNil() { + return fmt.Errorf("unmarshal target cannot be nil") + } + + return unmarshalValue(ad.ad, val.Elem()) +} + +// unmarshalValue unmarshals an AST ClassAd into a reflect.Value +func unmarshalValue(node *ast.ClassAd, val reflect.Value) error { + if node == nil { + return nil + } + + // Handle pointers + if val.Kind() == reflect.Ptr { + if val.IsNil() { + val.Set(reflect.New(val.Type().Elem())) + } + val = val.Elem() + } + + switch val.Kind() { + case reflect.Struct: + return unmarshalStruct(node, val) + + case reflect.Map: + if val.Type().Key().Kind() != reflect.String { + return fmt.Errorf("maps must have string keys") + } + if val.IsNil() { + val.Set(reflect.MakeMap(val.Type())) + } + return unmarshalMap(node, val) + + default: + return fmt.Errorf("can only unmarshal ClassAd into struct or map, got %v", val.Type()) + } +} + +// unmarshalStruct unmarshals a ClassAd into a struct +func unmarshalStruct(node *ast.ClassAd, val reflect.Value) error { + typ := val.Type() + + // Create a map of classad names to struct field indices + fieldMap := make(map[string]int) + for i := 0; i < typ.NumField(); i++ { + field := typ.Field(i) + if !field.IsExported() { + continue + } + name, _ := parseStructTag(field) + if name != "-" { + fieldMap[name] = i + } + } + + // Iterate over ClassAd attributes + for _, attr := range node.Attributes { + fieldIdx, ok := fieldMap[attr.Name] + if !ok { + // Ignore unknown fields + continue + } + + field := typ.Field(fieldIdx) + fieldVal := val.Field(fieldIdx) + + // Special handling for *Expr fields - don't evaluate + if field.Type == reflect.TypeOf((*Expr)(nil)) { + expr := &Expr{expr: attr.Value} + fieldVal.Set(reflect.ValueOf(expr)) + continue + } + + // Evaluate the attribute in the context of the ClassAd + ad := &ClassAd{ad: node} + result := ad.EvaluateAttr(attr.Name) + + // Unmarshal the result into the field + if err := unmarshalValueInto(result, fieldVal); err != nil { + return fmt.Errorf("field %s: %w", field.Name, err) + } + } + + return nil +} + +// unmarshalMap unmarshals a ClassAd into a map[string]T +func unmarshalMap(node *ast.ClassAd, val reflect.Value) error { + ad := &ClassAd{ad: node} + elemType := val.Type().Elem() + + for _, attr := range node.Attributes { + result := ad.EvaluateAttr(attr.Name) + + // Create a new element of the appropriate type + elemVal := reflect.New(elemType).Elem() + + // Unmarshal into the element + if err := unmarshalValueInto(result, elemVal); err != nil { + return fmt.Errorf("attribute %s: %w", attr.Name, err) + } + + // Set in map + val.SetMapIndex(reflect.ValueOf(attr.Name), elemVal) + } + + return nil +} + +// unmarshalValueInto unmarshals a Value into a reflect.Value +func unmarshalValueInto(result Value, val reflect.Value) error { + // Handle pointers - check for special types first + if val.Kind() == reflect.Ptr { + // Check if target is *ClassAd + if val.Type() == reflect.TypeOf((*ClassAd)(nil)) { + if result.IsClassAd() { + nestedAd, err := result.ClassAdValue() + if err != nil { + return fmt.Errorf("failed to get ClassAd value: %w", err) + } + val.Set(reflect.ValueOf(nestedAd)) + return nil + } + return fmt.Errorf("expected ClassAd, got %v", result.Type()) + } + // Check if target is *Expr + if val.Type() == reflect.TypeOf((*Expr)(nil)) { + // *Expr fields are handled in unmarshalStruct to preserve the unevaluated expression + return fmt.Errorf("*Expr fields should be handled in unmarshalStruct") + } + + if val.IsNil() { + val.Set(reflect.New(val.Type().Elem())) + } + val = val.Elem() + } + + switch val.Kind() { + case reflect.Bool: + if result.IsBool() { + v, err := result.BoolValue() + if err != nil { + return fmt.Errorf("failed to get bool value: %w", err) + } + val.SetBool(v) + } else { + return fmt.Errorf("expected bool, got %v", result.Type()) + } + + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if result.IsInteger() { + v, err := result.IntValue() + if err != nil { + return fmt.Errorf("failed to get int value: %w", err) + } + val.SetInt(v) + } else if result.IsReal() { + v, err := result.RealValue() + if err != nil { + return fmt.Errorf("failed to get real value: %w", err) + } + val.SetInt(int64(v)) + } else { + return fmt.Errorf("expected integer, got %v", result.Type()) + } + + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + if result.IsInteger() { + v, err := result.IntValue() + if err != nil { + return fmt.Errorf("failed to get int value: %w", err) + } + val.SetUint(uint64(v)) + } else if result.IsReal() { + v, err := result.RealValue() + if err != nil { + return fmt.Errorf("failed to get real value: %w", err) + } + val.SetUint(uint64(v)) + } else { + return fmt.Errorf("expected integer, got %v", result.Type()) + } + + case reflect.Float32, reflect.Float64: + if result.IsReal() { + v, err := result.RealValue() + if err != nil { + return fmt.Errorf("failed to get real value: %w", err) + } + val.SetFloat(v) + } else if result.IsInteger() { + v, err := result.IntValue() + if err != nil { + return fmt.Errorf("failed to get int value: %w", err) + } + val.SetFloat(float64(v)) + } else { + return fmt.Errorf("expected real, got %v", result.Type()) + } + + case reflect.String: + if result.IsString() { + v, err := result.StringValue() + if err != nil { + return fmt.Errorf("failed to get string value: %w", err) + } + val.SetString(v) + } else { + return fmt.Errorf("expected string, got %v", result.Type()) + } + + case reflect.Slice: + if result.IsList() { + listVals, err := result.ListValue() + if err != nil { + return fmt.Errorf("failed to get list value: %w", err) + } + slice := reflect.MakeSlice(val.Type(), len(listVals), len(listVals)) + for i, item := range listVals { + if err := unmarshalValueInto(item, slice.Index(i)); err != nil { + return fmt.Errorf("list element %d: %w", i, err) + } + } + val.Set(slice) + } else { + return fmt.Errorf("expected list, got %v", result.Type()) + } + + case reflect.Struct: + if result.IsClassAd() { + nestedAd, err := result.ClassAdValue() + if err != nil { + return fmt.Errorf("failed to get ClassAd value: %w", err) + } + return unmarshalStruct(nestedAd.ad, val) + } else { + return fmt.Errorf("expected ClassAd, got %v", result.Type()) + } + + case reflect.Map: + if result.IsClassAd() { + nestedAd, err := result.ClassAdValue() + if err != nil { + return fmt.Errorf("failed to get ClassAd value: %w", err) + } + if val.IsNil() { + val.Set(reflect.MakeMap(val.Type())) + } + return unmarshalMap(nestedAd.ad, val) + } else { + return fmt.Errorf("expected ClassAd, got %v", result.Type()) + } + + case reflect.Interface: + // For interface{}, convert to a concrete Go type + if val.Type().NumMethod() == 0 { // empty interface + concreteVal := valueToInterface(result) + val.Set(reflect.ValueOf(concreteVal)) + } else { + return fmt.Errorf("cannot unmarshal into non-empty interface") + } + + default: + return fmt.Errorf("unsupported type: %v", val.Type()) + } + + return nil +} + +// valueToInterface converts a Value to a Go interface{} type +func valueToInterface(result Value) interface{} { + if result.IsUndefined() { + return nil + } + if result.IsBool() { + v, err := result.BoolValue() + if err != nil { + return nil + } + return v + } + if result.IsInteger() { + v, err := result.IntValue() + if err != nil { + return nil + } + return v + } + if result.IsReal() { + v, err := result.RealValue() + if err != nil { + return nil + } + return v + } + if result.IsString() { + v, err := result.StringValue() + if err != nil { + return nil + } + return v + } + if result.IsList() { + listVals, err := result.ListValue() + if err != nil { + return nil + } + slice := make([]interface{}, len(listVals)) + for i, item := range listVals { + slice[i] = valueToInterface(item) + } + return slice + } + if result.IsClassAd() { + nestedAd, err := result.ClassAdValue() + if err != nil { + return nil + } + m := make(map[string]interface{}) + for _, attr := range nestedAd.ad.Attributes { + attrVal := nestedAd.EvaluateAttr(attr.Name) + m[attr.Name] = valueToInterface(attrVal) + } + return m + } + return nil +} + +// tagOptions represents parsed struct tag options +type tagOptions struct { + omitEmpty bool +} + +// parseStructTag parses a struct field's tags to determine the ClassAd field name and options +func parseStructTag(field reflect.StructField) (string, tagOptions) { + var opts tagOptions + + // Try classad tag first + if tag, ok := field.Tag.Lookup("classad"); ok { + return parseTag(tag, field.Name, &opts) + } + + // Fall back to json tag + if tag, ok := field.Tag.Lookup("json"); ok { + return parseTag(tag, field.Name, &opts) + } + + // Use field name as-is + return field.Name, opts +} + +// parseTag parses a tag value into name and options +func parseTag(tag, defaultName string, opts *tagOptions) (string, tagOptions) { + parts := strings.Split(tag, ",") + name := parts[0] + + // Handle empty name + if name == "" { + name = defaultName + } + + // Parse options + for i := 1; i < len(parts); i++ { + if parts[i] == "omitempty" { + opts.omitEmpty = true + } + } + + return name, *opts +} + +// isEmptyValue checks if a value is empty (for omitempty) +func isEmptyValue(v reflect.Value) bool { + switch v.Kind() { + case reflect.Array, reflect.Map, reflect.Slice, reflect.String: + return v.Len() == 0 + case reflect.Bool: + return !v.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return v.Uint() == 0 + case reflect.Float32, reflect.Float64: + return v.Float() == 0 + case reflect.Interface, reflect.Ptr: + return v.IsNil() + } + return false +} + +// MarshalClassAd is an alias for Marshal for clarity +func MarshalClassAd(v interface{}) (string, error) { + return Marshal(v) +} + +// UnmarshalClassAd is an alias for Unmarshal for clarity +func UnmarshalClassAd(data string, v interface{}) error { + return Unmarshal(data, v) +} + +// Marshaler is the interface implemented by types that can marshal themselves into ClassAd format. +// This is similar to json.Marshaler. +type Marshaler interface { + MarshalClassAd() (string, error) +} + +// Unmarshaler is the interface implemented by types that can unmarshal a ClassAd representation of themselves. +// This is similar to json.Unmarshaler. +type Unmarshaler interface { + UnmarshalClassAd(data string) error +} diff --git a/classad/struct_test.go b/classad/struct_test.go new file mode 100644 index 0000000..5e30cab --- /dev/null +++ b/classad/struct_test.go @@ -0,0 +1,795 @@ +package classad + +import ( + "testing" +) + +func TestMarshal_SimpleStruct(t *testing.T) { + type Job struct { + ID int + Name string + CPUs int + Priority float64 + Active bool + } + + job := Job{ + ID: 123, + Name: "test-job", + CPUs: 4, + Priority: 10.5, + Active: true, + } + + result, err := Marshal(job) + if err != nil { + t.Fatalf("Marshal failed: %v", err) + } + + // Parse it back to verify + ad, err := Parse(result) + if err != nil { + t.Fatalf("Failed to parse marshaled result: %v", err) + } + + // Verify values + id := ad.EvaluateAttr("ID") + if !id.IsInteger() || id.String() != "123" { + t.Errorf("Expected ID=123, got %v", id) + } + + name := ad.EvaluateAttr("Name") + nameVal, _ := name.StringValue() + if nameVal != "test-job" { + t.Errorf("Expected Name='test-job', got %q", nameVal) + } +} + +func TestMarshal_WithClassAdTags(t *testing.T) { + type Job struct { + JobID int `classad:"JobId"` + UserName string `classad:"User"` + CPUs int `classad:"RequestCpus"` + } + + job := Job{ + JobID: 456, + UserName: "alice", + CPUs: 8, + } + + result, err := Marshal(job) + if err != nil { + t.Fatalf("Marshal failed: %v", err) + } + + ad, err := Parse(result) + if err != nil { + t.Fatalf("Failed to parse: %v", err) + } + + // Verify the tags were used + jobID := ad.EvaluateAttr("JobId") + if !jobID.IsInteger() || jobID.String() != "456" { + t.Errorf("Expected JobId=456, got %v", jobID) + } + + user := ad.EvaluateAttr("User") + userVal, _ := user.StringValue() + if userVal != "alice" { + t.Errorf("Expected User='alice', got %q", userVal) + } +} + +func TestMarshal_WithJSONTagsFallback(t *testing.T) { + type Config struct { + Timeout int `json:"timeout"` + Server string `json:"server"` + Port int // No tag, uses field name + } + + config := Config{ + Timeout: 30, + Server: "example.com", + Port: 8080, + } + + result, err := Marshal(config) + if err != nil { + t.Fatalf("Marshal failed: %v", err) + } + + ad, err := Parse(result) + if err != nil { + t.Fatalf("Failed to parse: %v", err) + } + + // Verify json tags were used + timeout := ad.EvaluateAttr("timeout") + if timeout.String() != "30" { + t.Errorf("Expected timeout=30, got %v", timeout) + } + + // Verify field name was used when no tag present + port := ad.EvaluateAttr("Port") + if port.String() != "8080" { + t.Errorf("Expected Port=8080, got %v", port) + } +} + +func TestMarshal_WithOmitEmpty(t *testing.T) { + type Job struct { + ID int + Name string + Optional string `classad:"Optional,omitempty"` + Tags []string `classad:"Tags,omitempty"` + } + + job := Job{ + ID: 123, + Name: "test", + // Optional and Tags are zero values + } + + result, err := Marshal(job) + if err != nil { + t.Fatalf("Marshal failed: %v", err) + } + + ad, err := Parse(result) + if err != nil { + t.Fatalf("Failed to parse: %v", err) + } + + // Verify omitted fields are not present + optional := ad.EvaluateAttr("Optional") + if !optional.IsUndefined() { + t.Errorf("Expected Optional to be omitted, got %v", optional) + } + + tags := ad.EvaluateAttr("Tags") + if !tags.IsUndefined() { + t.Errorf("Expected Tags to be omitted, got %v", tags) + } +} + +func TestMarshal_SkipField(t *testing.T) { + type Job struct { + ID int + Internal string `classad:"-"` + Secret string `json:"-"` + } + + job := Job{ + ID: 123, + Internal: "skip-me", + Secret: "secret", + } + + result, err := Marshal(job) + if err != nil { + t.Fatalf("Marshal failed: %v", err) + } + + ad, err := Parse(result) + if err != nil { + t.Fatalf("Failed to parse: %v", err) + } + + // Verify skipped fields are not present + internal := ad.EvaluateAttr("Internal") + if !internal.IsUndefined() { + t.Errorf("Expected Internal to be skipped, got %v", internal) + } + + secret := ad.EvaluateAttr("Secret") + if !secret.IsUndefined() { + t.Errorf("Expected Secret to be skipped, got %v", secret) + } +} + +func TestMarshal_NestedStruct(t *testing.T) { + type Config struct { + Timeout int + Retries int + } + + type Job struct { + ID int + Config Config + } + + job := Job{ + ID: 123, + Config: Config{ + Timeout: 30, + Retries: 3, + }, + } + + result, err := Marshal(job) + if err != nil { + t.Fatalf("Marshal failed: %v", err) + } + + ad, err := Parse(result) + if err != nil { + t.Fatalf("Failed to parse: %v", err) + } + + // Verify nested struct + config := ad.EvaluateAttr("Config") + if !config.IsClassAd() { + t.Fatalf("Expected Config to be a ClassAd, got %v", config.Type()) + } + + configAd, _ := config.ClassAdValue() + timeout := configAd.EvaluateAttr("Timeout") + if timeout.String() != "30" { + t.Errorf("Expected Timeout=30, got %v", timeout) + } +} + +func TestMarshal_Slice(t *testing.T) { + type Job struct { + ID int + Tags []string + Nums []int + } + + job := Job{ + ID: 123, + Tags: []string{"prod", "batch", "high-priority"}, + Nums: []int{1, 2, 3, 4, 5}, + } + + result, err := Marshal(job) + if err != nil { + t.Fatalf("Marshal failed: %v", err) + } + + ad, err := Parse(result) + if err != nil { + t.Fatalf("Failed to parse: %v", err) + } + + // Verify slices + tags := ad.EvaluateAttr("Tags") + if !tags.IsList() { + t.Fatalf("Expected Tags to be a list, got %v", tags.Type()) + } + tagsList, _ := tags.ListValue() + if len(tagsList) != 3 { + t.Errorf("Expected 3 tags, got %d", len(tagsList)) + } +} + +func TestMarshal_Map(t *testing.T) { + data := map[string]interface{}{ + "id": 123, + "name": "test", + "cpus": 4, + } + + result, err := Marshal(data) + if err != nil { + t.Fatalf("Marshal failed: %v", err) + } + + ad, err := Parse(result) + if err != nil { + t.Fatalf("Failed to parse: %v", err) + } + + id := ad.EvaluateAttr("id") + if id.String() != "123" { + t.Errorf("Expected id=123, got %v", id) + } +} + +func TestUnmarshal_SimpleStruct(t *testing.T) { + type Job struct { + ID int + Name string + CPUs int + Priority float64 + Active bool + } + + classadStr := `[ID = 123; Name = "test-job"; CPUs = 4; Priority = 10.5; Active = true]` + + var job Job + err := Unmarshal(classadStr, &job) + if err != nil { + t.Fatalf("Unmarshal failed: %v", err) + } + + if job.ID != 123 { + t.Errorf("Expected ID=123, got %d", job.ID) + } + if job.Name != "test-job" { + t.Errorf("Expected Name='test-job', got %q", job.Name) + } + if job.CPUs != 4 { + t.Errorf("Expected CPUs=4, got %d", job.CPUs) + } + if job.Priority != 10.5 { + t.Errorf("Expected Priority=10.5, got %f", job.Priority) + } + if !job.Active { + t.Errorf("Expected Active=true, got false") + } +} + +func TestUnmarshal_WithClassAdTags(t *testing.T) { + type Job struct { + JobID int `classad:"JobId"` + UserName string `classad:"User"` + CPUs int `classad:"RequestCpus"` + } + + classadStr := `[JobId = 456; User = "alice"; RequestCpus = 8]` + + var job Job + err := Unmarshal(classadStr, &job) + if err != nil { + t.Fatalf("Unmarshal failed: %v", err) + } + + if job.JobID != 456 { + t.Errorf("Expected JobID=456, got %d", job.JobID) + } + if job.UserName != "alice" { + t.Errorf("Expected UserName='alice', got %q", job.UserName) + } + if job.CPUs != 8 { + t.Errorf("Expected CPUs=8, got %d", job.CPUs) + } +} + +func TestUnmarshal_WithJSONTagsFallback(t *testing.T) { + type Config struct { + Timeout int `json:"timeout"` + Server string `json:"server"` + Port int // No tag + } + + classadStr := `[timeout = 30; server = "example.com"; Port = 8080]` + + var config Config + err := Unmarshal(classadStr, &config) + if err != nil { + t.Fatalf("Unmarshal failed: %v", err) + } + + if config.Timeout != 30 { + t.Errorf("Expected Timeout=30, got %d", config.Timeout) + } + if config.Server != "example.com" { + t.Errorf("Expected Server='example.com', got %q", config.Server) + } + if config.Port != 8080 { + t.Errorf("Expected Port=8080, got %d", config.Port) + } +} + +func TestUnmarshal_NestedStruct(t *testing.T) { + type Config struct { + Timeout int + Retries int + } + + type Job struct { + ID int + Config Config + } + + classadStr := `[ID = 123; Config = [Timeout = 30; Retries = 3]]` + + var job Job + err := Unmarshal(classadStr, &job) + if err != nil { + t.Fatalf("Unmarshal failed: %v", err) + } + + if job.ID != 123 { + t.Errorf("Expected ID=123, got %d", job.ID) + } + if job.Config.Timeout != 30 { + t.Errorf("Expected Config.Timeout=30, got %d", job.Config.Timeout) + } + if job.Config.Retries != 3 { + t.Errorf("Expected Config.Retries=3, got %d", job.Config.Retries) + } +} + +func TestUnmarshal_Slice(t *testing.T) { + type Job struct { + ID int + Tags []string + Nums []int + } + + classadStr := `[ID = 123; Tags = {"prod", "batch", "high-priority"}; Nums = {1, 2, 3, 4, 5}]` + + var job Job + err := Unmarshal(classadStr, &job) + if err != nil { + t.Fatalf("Unmarshal failed: %v", err) + } + + if job.ID != 123 { + t.Errorf("Expected ID=123, got %d", job.ID) + } + if len(job.Tags) != 3 { + t.Errorf("Expected 3 tags, got %d", len(job.Tags)) + } + if job.Tags[0] != "prod" { + t.Errorf("Expected first tag='prod', got %q", job.Tags[0]) + } + if len(job.Nums) != 5 { + t.Errorf("Expected 5 nums, got %d", len(job.Nums)) + } +} + +func TestUnmarshal_IntoMap(t *testing.T) { + classadStr := `[ID = 123; Name = "test"; CPUs = 4]` + + var data map[string]interface{} + err := Unmarshal(classadStr, &data) + if err != nil { + t.Fatalf("Unmarshal failed: %v", err) + } + + if len(data) != 3 { + t.Errorf("Expected 3 entries, got %d", len(data)) + } +} + +func TestRoundTrip_ComplexStruct(t *testing.T) { + type Metadata struct { + Created string + Modified string + } + + type Job struct { + ID int `classad:"JobId"` + Name string `classad:"Name"` + CPUs int `json:"cpus"` + Tags []string `classad:"Tags"` + Metadata Metadata `classad:"Metadata"` + Extra map[string]interface{} `classad:"Extra,omitempty"` + } + + original := Job{ + ID: 789, + Name: "complex-job", + CPUs: 16, + Tags: []string{"prod", "critical"}, + Metadata: Metadata{ + Created: "2024-01-01", + Modified: "2024-01-02", + }, + } + + // Marshal + classadStr, err := Marshal(original) + if err != nil { + t.Fatalf("Marshal failed: %v", err) + } + + // Unmarshal + var restored Job + err = Unmarshal(classadStr, &restored) + if err != nil { + t.Fatalf("Unmarshal failed: %v", err) + } + + // Compare + if restored.ID != original.ID { + t.Errorf("ID mismatch: expected %d, got %d", original.ID, restored.ID) + } + if restored.Name != original.Name { + t.Errorf("Name mismatch: expected %q, got %q", original.Name, restored.Name) + } + if restored.CPUs != original.CPUs { + t.Errorf("CPUs mismatch: expected %d, got %d", original.CPUs, restored.CPUs) + } + if len(restored.Tags) != len(original.Tags) { + t.Errorf("Tags length mismatch: expected %d, got %d", len(original.Tags), len(restored.Tags)) + } + if restored.Metadata.Created != original.Metadata.Created { + t.Errorf("Metadata.Created mismatch") + } +} + +func TestUnmarshal_IgnoreUnknownFields(t *testing.T) { + type Job struct { + ID int + Name string + } + + classadStr := `[ID = 123; Name = "test"; ExtraField = "ignored"; AnotherField = 999]` + + var job Job + err := Unmarshal(classadStr, &job) + if err != nil { + t.Fatalf("Unmarshal failed: %v", err) + } + + // Should succeed and ignore unknown fields + if job.ID != 123 { + t.Errorf("Expected ID=123, got %d", job.ID) + } + if job.Name != "test" { + t.Errorf("Expected Name='test', got %q", job.Name) + } +} + +func TestUnmarshal_TypeConversion(t *testing.T) { + type Job struct { + IntField int + FloatField float64 + UintField uint + } + + // Int to float conversion + classadStr1 := `[IntField = 10; FloatField = 20; UintField = 30]` + var job1 Job + if err := Unmarshal(classadStr1, &job1); err != nil { + t.Fatalf("Unmarshal failed: %v", err) + } + if job1.FloatField != 20.0 { + t.Errorf("Expected FloatField=20.0, got %f", job1.FloatField) + } + + // Float to int conversion (truncation) + classadStr2 := `[IntField = 10.7; FloatField = 20.3; UintField = 30.9]` + var job2 Job + if err := Unmarshal(classadStr2, &job2); err != nil { + t.Fatalf("Unmarshal failed: %v", err) + } + if job2.IntField != 10 { + t.Errorf("Expected IntField=10, got %d", job2.IntField) + } +} + +func TestMarshal_EmptyStruct(t *testing.T) { + type Empty struct{} + + result, err := Marshal(Empty{}) + if err != nil { + t.Fatalf("Marshal failed: %v", err) + } + + // Should produce empty ClassAd + if result != "[]" { + t.Errorf("Expected '[]', got %q", result) + } +} + +func TestMarshal_ClassAdField(t *testing.T) { + type Container struct { + Name string + Config *ClassAd + } + + config := New() + config.InsertAttr("timeout", 30) + config.InsertAttrString("server", "example.com") + + container := Container{ + Name: "mycontainer", + Config: config, + } + + result, err := Marshal(container) + if err != nil { + t.Fatalf("Marshal failed: %v", err) + } + + // Parse it back + ad, err := Parse(result) + if err != nil { + t.Fatalf("Failed to parse marshaled result: %v", err) + } + + // Verify the nested ClassAd + configVal := ad.EvaluateAttr("Config") + if !configVal.IsClassAd() { + t.Fatalf("Expected Config to be a ClassAd, got %v", configVal.Type()) + } + + configAd, _ := configVal.ClassAdValue() + timeout, ok := configAd.EvaluateAttrInt("timeout") + if !ok || timeout != 30 { + t.Errorf("Expected timeout=30, got %d", timeout) + } + + server, ok := configAd.EvaluateAttrString("server") + if !ok || server != "example.com" { + t.Errorf("Expected server='example.com', got %q", server) + } +} + +func TestUnmarshal_ClassAdField(t *testing.T) { + type Container struct { + Name string + Config *ClassAd + } + + input := `[Name = "mycontainer"; Config = [timeout = 30; server = "example.com"]]` + + var container Container + err := Unmarshal(input, &container) + if err != nil { + t.Fatalf("Unmarshal failed: %v", err) + } + + if container.Name != "mycontainer" { + t.Errorf("Expected Name='mycontainer', got %q", container.Name) + } + + if container.Config == nil { + t.Fatal("Expected Config to be non-nil") + } + + timeout, ok := container.Config.EvaluateAttrInt("timeout") + if !ok || timeout != 30 { + t.Errorf("Expected timeout=30, got %d", timeout) + } + + server, ok := container.Config.EvaluateAttrString("server") + if !ok || server != "example.com" { + t.Errorf("Expected server='example.com', got %q", server) + } +} + +func TestMarshal_ExprField(t *testing.T) { + type Job struct { + Name string + CPUs int + Memory *Expr + Formula *Expr + } + + memExpr, _ := ParseExpr("RequestMemory * 1024") + formulaExpr, _ := ParseExpr("Cpus * 2 + 8") + + job := Job{ + Name: "test-job", + CPUs: 4, + Memory: memExpr, + Formula: formulaExpr, + } + + result, err := Marshal(job) + if err != nil { + t.Fatalf("Marshal failed: %v", err) + } + + // Parse it back + ad, err := Parse(result) + if err != nil { + t.Fatalf("Failed to parse marshaled result: %v", err) + } + + // Verify expressions are preserved (not evaluated) + memoryExpr, ok := ad.Lookup("Memory") + if !ok { + t.Fatal("Expected Memory attribute") + } + // Parser adds parentheses for operator precedence + if memoryExpr.String() != "(RequestMemory * 1024)" { + t.Errorf("Expected Memory='(RequestMemory * 1024)', got %q", memoryExpr.String()) + } + + formulaExpr2, ok := ad.Lookup("Formula") + if !ok { + t.Fatal("Expected Formula attribute") + } + // Parser adds parentheses for operator precedence + if formulaExpr2.String() != "((Cpus * 2) + 8)" { + t.Errorf("Expected Formula='((Cpus * 2) + 8)', got %q", formulaExpr2.String()) + } +} + +func TestUnmarshal_ExprField(t *testing.T) { + type Job struct { + Name string + CPUs int + Memory *Expr + Formula *Expr + } + + input := `[Name = "test-job"; CPUs = 4; Memory = RequestMemory * 1024; Formula = CPUs * 2 + 8]` + + var job Job + err := Unmarshal(input, &job) + if err != nil { + t.Fatalf("Unmarshal failed: %v", err) + } + + if job.Name != "test-job" { + t.Errorf("Expected Name='test-job', got %q", job.Name) + } + + if job.CPUs != 4 { + t.Errorf("Expected CPUs=4, got %d", job.CPUs) + } + + if job.Memory == nil { + t.Fatal("Expected Memory to be non-nil") + } + // Parser adds parentheses for operator precedence + if job.Memory.String() != "(RequestMemory * 1024)" { + t.Errorf("Expected Memory='(RequestMemory * 1024)', got %q", job.Memory.String()) + } + + if job.Formula == nil { + t.Fatal("Expected Formula to be non-nil") + } + // Parser adds parentheses for operator precedence + if job.Formula.String() != "((CPUs * 2) + 8)" { + t.Errorf("Expected Formula='((CPUs * 2) + 8)', got %q", job.Formula.String()) + } + + // Test that we can evaluate the expression with context + testAd := New() + testAd.InsertAttr("RequestMemory", 2048) + testAd.InsertAttr("CPUs", 4) + + memVal := job.Memory.Eval(testAd) + if !memVal.IsInteger() { + t.Errorf("Expected Memory to evaluate to integer, got %v", memVal.Type()) + } + memInt, _ := memVal.IntValue() + if memInt != 2048*1024 { + t.Errorf("Expected Memory to evaluate to %d, got %d", 2048*1024, memInt) + } + + formulaVal := job.Formula.Eval(testAd) + if !formulaVal.IsInteger() { + t.Errorf("Expected Formula to evaluate to integer, got %v", formulaVal.Type()) + } + formulaInt, _ := formulaVal.IntValue() + if formulaInt != 16 { + t.Errorf("Expected Formula to evaluate to 16, got %d", formulaInt) + } +} + +func TestMarshal_NilClassAdAndExprFields(t *testing.T) { + type Container struct { + Name string + Config *ClassAd + Formula *Expr + } + + container := Container{ + Name: "test", + Config: nil, + Formula: nil, + } + + result, err := Marshal(container) + if err != nil { + t.Fatalf("Marshal failed: %v", err) + } + + // Parse it back + ad, err := Parse(result) + if err != nil { + t.Fatalf("Failed to parse marshaled result: %v", err) + } + + // Verify nil fields are marshaled as undefined + configVal := ad.EvaluateAttr("Config") + if !configVal.IsUndefined() { + t.Errorf("Expected Config to be undefined, got %v", configVal.Type()) + } + + formulaVal := ad.EvaluateAttr("Formula") + if !formulaVal.IsUndefined() { + t.Errorf("Expected Formula to be undefined, got %v", formulaVal.Type()) + } +} diff --git a/docs/EVALUATION_API.md b/docs/EVALUATION_API.md index 8bf7fe8..d15718c 100644 --- a/docs/EVALUATION_API.md +++ b/docs/EVALUATION_API.md @@ -10,16 +10,23 @@ The `classad` package provides a high-level API for working with ClassAds, inclu - Evaluating expressions with type safety - Modifying ClassAd attributes +The library offers two API styles: +- **Modern Generic API** (recommended): `Set()`, `GetAs[T]()`, `GetOr[T]()` +- **Traditional API** (still supported): `InsertAttr*()`, `EvaluateAttr*()` + ## Quick Start +### Modern API (Recommended) + ```go import "github.com/PelicanPlatform/classad/classad" -// Create a new ClassAd +// Create a new ClassAd with Set() ad := classad.New() -ad.InsertAttr("Cpus", 4) -ad.InsertAttrFloat("Memory", 8192.0) -ad.InsertAttrString("Name", "worker-01") +ad.Set("Cpus", 4) +ad.Set("Memory", 8192.0) +ad.Set("Name", "worker-01") +ad.Set("Tags", []string{"prod", "gpu"}) // Parse a ClassAd from string jobAd, err := classad.Parse(`[ @@ -29,7 +36,30 @@ jobAd, err := classad.Parse(`[ Requirements = (Cpus >= 2) && (Memory >= 2048) ]`) -// Evaluate attributes with type safety +// Type-safe retrieval with GetAs[T]() +if cpus, ok := classad.GetAs[int](jobAd, "Cpus"); ok { + fmt.Printf("Cpus = %d\n", cpus) +} + +if owner, ok := classad.GetAs[string](jobAd, "Owner"); ok { + fmt.Printf("Owner = %s\n", owner) +} + +// Get with defaults using GetOr[T]() +priority := classad.GetOr(jobAd, "Priority", 10) +status := classad.GetOr(jobAd, "Status", "Unknown") +``` + +### Traditional API (Still Supported) + +```go +// Create a new ClassAd with InsertAttr methods +ad := classad.New() +ad.InsertAttr("Cpus", 4) +ad.InsertAttrFloat("Memory", 8192.0) +ad.InsertAttrString("Name", "worker-01") + +// Evaluate attributes with type-specific methods if cpus, ok := jobAd.EvaluateAttrInt("Cpus"); ok { fmt.Printf("Cpus = %d\n", cpus) } @@ -86,7 +116,9 @@ defer file.Close() reader := classad.NewReader(file) for reader.Next() { ad := reader.ClassAd() - // Process ClassAd... + // Process ClassAd with modern API + owner := classad.GetOr(ad, "Owner", "unknown") + cpus := classad.GetOr(ad, "Cpus", 0) } if err := reader.Err(); err != nil { log.Fatal(err) @@ -114,20 +146,25 @@ import ( "github.com/PelicanPlatform/classad/classad" ) -// Simple iteration +// Simple iteration with modern API for ad := range classad.All(strings.NewReader(input)) { - // Process ClassAd... + owner := classad.GetOr(ad, "Owner", "unknown") + cpus := classad.GetOr(ad, "Cpus", 0) + fmt.Printf("Owner: %s, Cpus: %d\n", owner, cpus) } // Iteration with index for i, ad := range classad.AllWithIndex(file) { - fmt.Printf("ClassAd %d: %v\n", i, ad) + jobId := classad.GetOr(ad, "JobId", 0) + fmt.Printf("ClassAd %d: JobId=%d\n", i, jobId) } // Iteration with error handling var err error for ad := range classad.AllWithError(file, &err) { - // Process ClassAd... + if name, ok := classad.GetAs[string](ad, "Name"); ok { + fmt.Printf("Name: %s\n", name) + } } if err != nil { log.Fatal(err) @@ -136,11 +173,22 @@ if err != nil { #### Attribute Manipulation +**Modern API (Recommended):** + +- `Set(name string, value any) error` - Sets an attribute with any type (generic) +- `GetAs[T any](ad *ClassAd, name string) (T, bool)` - Type-safe generic retrieval +- `GetOr[T any](ad *ClassAd, name string, defaultValue T) T` - Get with default value + +**Traditional API (Still Supported):** + - `InsertAttr(name string, value interface{})` - Inserts an attribute (auto-detects type) - `InsertAttrInt(name string, value int64)` - Inserts an integer attribute - `InsertAttrFloat(name string, value float64)` - Inserts a float attribute - `InsertAttrString(name string, value string)` - Inserts a string attribute - `InsertAttrBool(name string, value bool)` - Inserts a boolean attribute + +**Common Methods:** + - `Insert(name string, expr ast.Expr)` - Inserts an attribute with an AST expression - `InsertExpr(name string, expr *Expr)` - Inserts an attribute with an Expr (see Expression API) - `Lookup(name string) (*Expr, bool)` - Returns the unevaluated expression for an attribute @@ -151,6 +199,27 @@ if err != nil { #### Evaluation Methods +**Modern API (Recommended):** + +Using the generic functions: +```go +// Type-safe retrieval with two-value return +if cpus, ok := classad.GetAs[int](ad, "Cpus"); ok { + fmt.Printf("Cpus: %d\n", cpus) +} + +if owner, ok := classad.GetAs[string](ad, "Owner"); ok { + fmt.Printf("Owner: %s\n", owner) +} + +// Get with defaults (no error checking needed) +priority := classad.GetOr(ad, "Priority", 10) +status := classad.GetOr(ad, "Status", "Unknown") +tags := classad.GetOr(ad, "Tags", []string{"default"}) +``` + +**Traditional API (Still Supported):** + - `EvaluateAttr(name string) Value` - Evaluates an attribute and returns a Value - `EvaluateAttrInt(name string) (int64, bool)` - Evaluates as integer - `EvaluateAttrReal(name string) (float64, bool)` - Evaluates as float @@ -197,7 +266,7 @@ if expr, ok := ad.Lookup("y"); ok { ```go expr, _ := classad.ParseExpr("Cpus * 2") ad := classad.New() -ad.InsertAttr("Cpus", 8) +ad.Set("Cpus", 8) result := expr.Eval(ad) if value, ok := result.IntValue(); ok { @@ -209,10 +278,10 @@ if value, ok := result.IntValue(); ok { ```go job := classad.New() -job.InsertAttr("RequestCpus", 4) +job.Set("RequestCpus", 4) machine := classad.New() -machine.InsertAttr("Cpus", 8) +machine.Set("Cpus", 8) expr, _ := classad.ParseExpr("MY.RequestCpus <= TARGET.Cpus") result := expr.EvalWithContext(job, machine) // job=MY, machine=TARGET @@ -241,8 +310,8 @@ template, _ := classad.Parse(`[ // Create a new ClassAd and copy expressions newAd := classad.New() -newAd.InsertAttr("Cpus", 4) -newAd.InsertAttr("Memory", 8192) +newAd.Set("Cpus", 4) +newAd.Set("Memory", 8192) // Copy the StandardReq expression if req, ok := template.Lookup("StandardReq"); ok { @@ -254,11 +323,11 @@ if score, ok := template.Lookup("ResourceScore"); ok { newAd.InsertExpr("Score", score) } -// Evaluate in new context -if reqVal, ok := newAd.EvaluateAttrBool("Requirements"); ok { +// Evaluate in new context with modern API +if reqVal, ok := classad.GetAs[bool](newAd, "Requirements"); ok { fmt.Printf("Requirements: %v\n", reqVal) // true } -if scoreVal, ok := newAd.EvaluateAttrInt("Score"); ok { +if scoreVal, ok := classad.GetAs[int](newAd, "Score"); ok { fmt.Printf("Score: %d\n", scoreVal) // 4008 } ``` @@ -269,12 +338,12 @@ The Expression API provides explicit control over MY and TARGET scopes for match ```go job := classad.New() -job.InsertAttr("RequestCpus", 4) -job.InsertAttr("RequestMemory", 8192) +job.Set("RequestCpus", 4) +job.Set("RequestMemory", 8192) machine := classad.New() -machine.InsertAttr("Cpus", 8) -machine.InsertAttr("Memory", 16384) +machine.Set("Cpus", 8) +machine.Set("Memory", 16384) // Job requirements: MY=job, TARGET=machine jobReq, _ := classad.ParseExpr("MY.RequestCpus <= TARGET.Cpus && MY.RequestMemory <= TARGET.Memory") @@ -463,18 +532,21 @@ if requirements, ok := job.EvaluateAttrBool("Requirements"); ok { ## Examples -See `examples/api_demo/main.go` for comprehensive examples of: -1. Creating ClassAds programmatically +See `examples/api_demo/main.go` for comprehensive examples using the modern API: +1. Creating ClassAds programmatically with `Set()` 2. Parsing ClassAds from strings 3. Looking up attributes -4. Evaluating with type safety -5. Complex expressions -6. Arithmetic operations -7. Logical expressions -8. Conditional expressions -9. Modifying ClassAds -10. Real-world HTCondor scenarios -11. Handling undefined values +4. Type-safe retrieval with `GetAs[T]()` +5. Using `GetOr[T]()` with defaults +6. Complex expressions +7. Arithmetic operations +8. Logical expressions +9. Conditional expressions +10. Modifying ClassAds +11. Real-world HTCondor scenarios +12. Handling undefined values + +See `examples/generic_api_demo/main.go` for focused examples of the modern generic API. See `examples/features_demo/main.go` for advanced features including: - Scoped attribute references (MY., TARGET., PARENT.) @@ -483,6 +555,7 @@ See `examples/features_demo/main.go` for advanced features including: Run the examples with: ```bash go run ./examples/api_demo/main.go +go run ./examples/generic_api_demo/main.go go run ./examples/features_demo/main.go ``` @@ -505,11 +578,38 @@ The test suite includes: - Nested ClassAds and lists - IS/ISNT operators - Built-in functions +- Generic API (Set, GetAs, GetOr) ## Nested ClassAds and Lists ClassAds support nested structures: +### Using Modern API + +```go +// Lists +ad, _ := classad.Parse(`[numbers = {1, 2, 3, 4, 5}]`) + +// Get list with type safety +if numbers, ok := classad.GetAs[[]interface{}](ad, "numbers"); ok { + fmt.Printf("Numbers: %v\n", numbers) +} + +// Nested ClassAds +ad, _ := classad.Parse(`[ + server = [host = "example.com"; port = 8080]; + name = "web-server" +]`) + +if server, ok := classad.GetAs[*classad.ClassAd](ad, "server"); ok { + host := classad.GetOr(server, "host", "localhost") + port := classad.GetOr(server, "port", 80) + fmt.Printf("Server: %s:%d\n", host, port) +} +``` + +### Using Traditional API + ```go // Lists ad, _ := classad.Parse(`[numbers = {1, 2, 3, 4, 5}]`) @@ -759,6 +859,11 @@ ad, _ := classad.Parse(`[ secondSalary = company.employees[1].salary ]`) +// Modern API +name := classad.GetOr(ad, "firstEmpName", "") // "Alice" +salary := classad.GetOr(ad, "secondSalary", 0) // 95000 + +// Traditional API name, _ := ad.EvaluateAttrString("firstEmpName") // "Alice" salary, _ := ad.EvaluateAttrInt("secondSalary") // 95000 ``` @@ -777,16 +882,16 @@ The `MatchClassAd` type provides symmetric matching between two ClassAds, inspir ```go import "github.com/PelicanPlatform/classad/classad" -// Create job and machine ClassAds +// Create job and machine ClassAds with modern API job := classad.New() -job.InsertAttr("Cpus", 2) -job.InsertAttr("Memory", 2048) -job.InsertAttrString("Requirements", "TARGET.Cpus >= MY.Cpus && TARGET.Memory >= MY.Memory") +job.Set("Cpus", 2) +job.Set("Memory", 2048) +job.Set("Requirements", "TARGET.Cpus >= MY.Cpus && TARGET.Memory >= MY.Memory") machine := classad.New() -machine.InsertAttr("Cpus", 4) -machine.InsertAttr("Memory", 8192) -machine.InsertAttrString("Requirements", "TARGET.Cpus <= MY.Cpus && TARGET.Memory <= MY.Memory") +machine.Set("Cpus", 4) +machine.Set("Memory", 8192) +machine.Set("Requirements", "TARGET.Cpus <= MY.Cpus && TARGET.Memory <= MY.Memory") // Create MatchClassAd - automatically sets up TARGET references matchAd := classad.NewMatchClassAd(job, machine) @@ -849,21 +954,21 @@ rightRank := matchAd.EvaluateRankRight("Rank") ### Complete Matching Example ```go -// Job ClassAd +// Job ClassAd with modern API job := classad.New() -job.InsertAttr("Cpus", 2) -job.InsertAttr("Memory", 2048) -job.InsertAttrString("Owner", "alice") -job.InsertAttrString("Requirements", "TARGET.Cpus >= MY.Cpus && TARGET.Memory >= MY.Memory") -job.InsertAttrString("Rank", "TARGET.Memory") // Prefer more memory +job.Set("Cpus", 2) +job.Set("Memory", 2048) +job.Set("Owner", "alice") +job.Set("Requirements", "TARGET.Cpus >= MY.Cpus && TARGET.Memory >= MY.Memory") +job.Set("Rank", "TARGET.Memory") // Prefer more memory -// Machine ClassAd +// Machine ClassAd with modern API machine := classad.New() -machine.InsertAttr("Cpus", 4) -machine.InsertAttr("Memory", 8192) -machine.InsertAttrString("Name", "slot1@worker1") -machine.InsertAttrString("Requirements", "TARGET.Cpus <= MY.Cpus") -machine.InsertAttrString("Rank", "1000 - TARGET.Memory") // Prefer lighter jobs +machine.Set("Cpus", 4) +machine.Set("Memory", 8192) +machine.Set("Name", "slot1@worker1") +machine.Set("Requirements", "TARGET.Cpus <= MY.Cpus") +machine.Set("Rank", "1000 - TARGET.Memory") // Prefer lighter jobs // Create MatchClassAd and check match matchAd := classad.NewMatchClassAd(job, machine) @@ -1111,7 +1216,7 @@ Returns a sorted list of attribute names referenced in the expression but not de ```go expr, _ := classad.ParseExpr("RequestCpus * 1000 + Memory / 1024") job := classad.New() -job.InsertAttr("RequestCpus", 4) +job.Set("RequestCpus", 4) missing := job.ExternalRefs(expr) // Returns: ["Memory"] @@ -1208,8 +1313,8 @@ flattened := job.Flatten(expr) requirement, _ := classad.ParseExpr("Cpus >= RequestCpus && Memory >= RequestMemory") job := classad.New() -job.InsertAttr("RequestCpus", 4) -job.InsertAttr("RequestMemory", 2048) +job.Set("RequestCpus", 4) +job.Set("RequestMemory", 2048) // Scheduler flattens the requirement once flattened := job.Flatten(requirement) @@ -1230,7 +1335,7 @@ for _, machine := range machines { ```go expr, _ := classad.ParseExpr("x > 5 ? 100 : 200") ad := classad.New() -ad.InsertAttr("x", 10) +ad.Set("x", 10) flattened := ad.Flatten(expr) // Returns: 100 @@ -1241,8 +1346,8 @@ flattened := ad.Flatten(expr) ```go expr, _ := classad.ParseExpr("(x + y) * z") ad := classad.New() -ad.InsertAttr("x", 10) -ad.InsertAttr("y", 20) +ad.Set("x", 10) +ad.Set("y", 20) // z is undefined flattened := ad.Flatten(expr) diff --git a/docs/MARSHALING.md b/docs/MARSHALING.md new file mode 100644 index 0000000..eddf06c --- /dev/null +++ b/docs/MARSHALING.md @@ -0,0 +1,786 @@ +# Struct Marshaling and Unmarshaling Guide + +This guide explains how to marshal and unmarshal Go structs to/from ClassAd format and JSON format using the `classad` package. + +## Table of Contents + +- [Overview](#overview) +- [ClassAd Format Marshaling](#classad-format-marshaling) +- [JSON Format Marshaling](#json-format-marshaling) +- [Struct Tags](#struct-tags) +- [Advanced Features](#advanced-features) +- [Examples](#examples) +- [Best Practices](#best-practices) + +## Overview + +The `classad` package provides two complementary marshaling systems: + +1. **ClassAd Format**: Native HTCondor ClassAd text format +2. **JSON Format**: Standard JSON with special handling for expressions + +Both systems support the same struct tag conventions for a consistent API. + +## ClassAd Format Marshaling + +### Basic Usage + +```go +import "github.com/PelicanPlatform/classad/classad" + +// Define your struct +type Job struct { + ID int + Name string + CPUs int + Priority float64 +} + +// Marshal to ClassAd format +job := Job{ID: 123, Name: "test", CPUs: 4, Priority: 10.5} +classadStr, err := classad.Marshal(job) +// Result: [ID = 123; Name = "test"; CPUs = 4; Priority = 10.5] + +// Unmarshal from ClassAd format +var job2 Job +err = classad.Unmarshal(classadStr, &job2) +``` + +### Supported Types + +| Go Type | ClassAd Representation | +|---------|----------------------| +| `int`, `int8`, `int16`, `int32`, `int64` | Integer literal | +| `uint`, `uint8`, `uint16`, `uint32`, `uint64` | Integer literal | +| `float32`, `float64` | Real literal | +| `string` | String literal (quoted) | +| `bool` | Boolean literal (`true`/`false`) | +| `[]T`, `[N]T` | List literal `{...}` | +| `struct` | Nested ClassAd `[...]` | +| `map[string]T` | ClassAd record `[...]` | +| `*classad.ClassAd` | Nested ClassAd `[...]` (flexible) | +| `*classad.Expr` | Unevaluated expression | +| `*T` | Dereferences pointer, `undefined` if nil | + +## JSON Format Marshaling + +### Basic Usage + +```go +import ( + "encoding/json" + "github.com/PelicanPlatform/classad/classad" +) + +type Job struct { + ID int + Name string +} + +job := Job{ID: 123, Name: "test"} + +// ClassAd implements json.Marshaler interface +ad, _ := classad.Parse(`[ID = 123; Name = "test"]`) +jsonBytes, err := json.Marshal(ad) +// Result: {"ID":123,"Name":"test"} + +// ClassAd implements json.Unmarshaler interface +var ad2 classad.ClassAd +err = json.Unmarshal(jsonBytes, &ad2) +``` + +### Expression Handling in JSON + +Complex expressions are serialized with a special format: `/Expr(...)/` + +```go +ad, _ := classad.Parse(`[x = 5; y = x + 3]`) +jsonBytes, _ := json.Marshal(ad) +// Result: {"x":5,"y":"\/Expr(x + 3)\/"} +``` + +When unmarshaling, the JSON library automatically converts `\/` to `/`, so the expression is correctly recognized. + +## Struct Tags + +### Tag Priority + +The package uses struct tags to control field names and behavior: + +1. **Primary**: `classad:"name"` - Used for both ClassAd and JSON marshaling +2. **Fallback**: `json:"name"` - Used if no `classad` tag is present +3. **Default**: Field name - Used if no tags are present + +### Tag Syntax + +```go +type Job struct { + // Use custom name in ClassAd/JSON + JobID int `classad:"ClusterId"` + + // Falls back to json tag + Memory int `json:"request_memory"` + + // Uses field name "CPUs" + CPUs int + + // Skip this field entirely + Internal string `classad:"-"` + + // Omit if zero value + Optional string `classad:"Optional,omitempty"` + + // Can also use json tag with omitempty + Tags []string `json:"tags,omitempty"` +} +``` + +### Tag Options + +| Option | Description | Example | +|--------|-------------|---------| +| Custom name | Sets the field name | `classad:"ClusterId"` | +| `-` | Skip field entirely | `classad:"-"` | +| `omitempty` | Omit zero values | `classad:"name,omitempty"` | + +## Advanced Features + +### Nested Structs + +Both systems support nested structs: + +```go +type Resources struct { + CPUs int + Memory int +} + +type Job struct { + ID int + Resources Resources +} + +job := Job{ + ID: 123, + Resources: Resources{CPUs: 4, Memory: 8192}, +} + +// ClassAd format +str, _ := classad.Marshal(job) +// [ID = 123; Resources = [CPUs = 4; Memory = 8192]] + +// JSON format +ad, _ := classad.Parse(str) +jsonBytes, _ := json.Marshal(ad) +// {"ID":123,"Resources":{"CPUs":4,"Memory":8192}} +``` + +### Lists and Slices + +```go +type Job struct { + ID int + Tags []string + Nums []int +} + +job := Job{ + ID: 123, + Tags: []string{"prod", "critical"}, + Nums: []int{1, 2, 3}, +} + +// ClassAd: [ID = 123; Tags = {"prod", "critical"}; Nums = {1, 2, 3}] +// JSON: {"ID":123,"Tags":["prod","critical"],"Nums":[1,2,3]} +``` + +### Map Support + +```go +// Map with interface{} values +data := map[string]interface{}{ + "id": 123, + "name": "test", + "cpus": 4, +} + +// ClassAd format +str, _ := classad.Marshal(data) +// [id = 123; name = "test"; cpus = 4] + +// Can also unmarshal into map +var restored map[string]interface{} +classad.Unmarshal(str, &restored) +``` + +### ClassAd and Expr Fields + +You can include `*classad.ClassAd` and `*classad.Expr` fields in your structs. This is useful for: +- Storing nested ClassAds without defining intermediate struct types +- Preserving unevaluated expressions for later evaluation +- Building dynamic configurations + +#### Using *ClassAd Fields + +`*ClassAd` fields allow you to embed arbitrary ClassAds within your struct: + +```go +type Container struct { + Name string + Config *classad.ClassAd +} + +// Create a ClassAd for the Config field +config := classad.New() +config.InsertAttr("timeout", 30) +config.InsertAttrString("server", "example.com") +config.InsertAttr("retries", 3) + +container := Container{ + Name: "mycontainer", + Config: config, +} + +// Marshal to ClassAd format +str, _ := classad.Marshal(container) +// Result: [Name = "mycontainer"; Config = [timeout = 30; server = "example.com"; retries = 3]] + +// Unmarshal back +var restored Container +classad.Unmarshal(str, &restored) + +// Access the nested ClassAd +timeout, _ := restored.Config.EvaluateAttrInt("timeout") +server, _ := restored.Config.EvaluateAttrString("server") +``` + +#### Using *Expr Fields + +`*Expr` fields store unevaluated expressions. This is important when: +- The expression references attributes not available at marshal time +- You want to preserve the formula for evaluation in different contexts +- Building templates that will be evaluated later + +```go +type Job struct { + Name string + CPUs int + Memory *classad.Expr // Unevaluated expression + Formula *classad.Expr // Unevaluated expression +} + +// Create expressions +memExpr, _ := classad.ParseExpr("RequestMemory * 1024") +formulaExpr, _ := classad.ParseExpr("CPUs * 2 + 8") + +job := Job{ + Name: "test-job", + CPUs: 4, + Memory: memExpr, + Formula: formulaExpr, +} + +// Marshal - expressions are preserved +str, _ := classad.Marshal(job) +// Result: [Name = "test-job"; CPUs = 4; Memory = RequestMemory * 1024; Formula = CPUs * 2 + 8] + +// Unmarshal - expressions remain unevaluated +var restored Job +classad.Unmarshal(str, &restored) + +// Expression is stored, not evaluated +fmt.Println(restored.Memory.String()) // "(RequestMemory * 1024)" + +// Evaluate later with a context +context := classad.New() +context.InsertAttr("RequestMemory", 2048) +context.InsertAttr("CPUs", 4) + +memVal := restored.Memory.Eval(context) // Evaluates to 2048 * 1024 = 2097152 +formulaVal := restored.Formula.Eval(context) // Evaluates to 4 * 2 + 8 = 16 +``` + +#### Nil ClassAd and Expr Fields + +Nil `*ClassAd` and `*Expr` fields are marshaled as `undefined`: + +```go +type Container struct { + Name string + Config *classad.ClassAd + Formula *classad.Expr +} + +container := Container{ + Name: "test", + Config: nil, // Will be undefined + Formula: nil, // Will be undefined +} + +str, _ := classad.Marshal(container) +// Result: [Name = "test"; Config = undefined; Formula = undefined] +``` + +### Type Conversions + +The package handles automatic type conversions during unmarshal: + +```go +type Job struct { + IntField int // Can read from integer or real + FloatField float64 // Can read from integer or real +} + +// Integer to float +classad.Unmarshal(`[IntField = 10; FloatField = 20]`, &job) +// FloatField becomes 20.0 + +// Real to integer (truncates) +classad.Unmarshal(`[IntField = 10.7; FloatField = 20.3]`, &job) +// IntField becomes 10 +``` + +## Examples + +### Example 1: HTCondor Job Description + +```go +type HTCondorJob struct { + ClusterID int `classad:"ClusterId"` + ProcID int `classad:"ProcId"` + Owner string `classad:"Owner"` + RequestCPUs int `classad:"RequestCpus"` + RequestMemory int `classad:"RequestMemory"` + Executable string `classad:"Executable"` + Arguments string `classad:"Arguments,omitempty"` + Requirements []string `classad:"Requirements,omitempty"` +} + +job := HTCondorJob{ + ClusterID: 100, + ProcID: 0, + Owner: "alice", + RequestCPUs: 4, + RequestMemory: 8192, + Executable: "/usr/bin/python", + Requirements: []string{`OpSys == "LINUX"`, `Arch == "X86_64"`}, +} + +// Marshal to ClassAd format +classadStr, _ := classad.Marshal(job) + +// Can also convert to JSON for REST APIs +ad, _ := classad.Parse(classadStr) +jsonBytes, _ := json.Marshal(ad) +``` + +### Example 2: Configuration File + +```go +type ServerConfig struct { + Host string `classad:"host"` + Port int `classad:"port"` + Timeout int `classad:"timeout"` + TLS bool `classad:"tls"` + Options struct { + MaxConns int `classad:"max_connections"` + KeepAlive bool `classad:"keep_alive"` + } `classad:"options"` +} + +// Read from ClassAd config file +configData, _ := os.ReadFile("config.classad") +var config ServerConfig +classad.Unmarshal(string(configData), &config) +``` + +### Example 3: Round-trip with Both Formats + +```go +type Job struct { + ID int `classad:"JobId"` + Name string `classad:"Name"` + Tags []string `classad:"Tags"` +} + +original := Job{ID: 123, Name: "test", Tags: []string{"a", "b"}} + +// 1. Marshal to ClassAd +classadStr, _ := classad.Marshal(original) +// [JobId = 123; Name = "test"; Tags = {"a", "b"}] + +// 2. Parse as ClassAd +ad, _ := classad.Parse(classadStr) + +// 3. Convert to JSON +jsonBytes, _ := json.Marshal(ad) +// {"JobId":123,"Name":"test","Tags":["a","b"]} + +// 4. Parse JSON back +var ad2 classad.ClassAd +json.Unmarshal(jsonBytes, &ad2) + +// 5. Marshal back to struct +jsonBytes2, _ := json.Marshal(map[string]interface{}{ + "JobId": 123, + "Name": "test", + "Tags": []string{"a", "b"}, +}) +ad3 := &classad.ClassAd{} +json.Unmarshal(jsonBytes2, ad3) + +// 6. Extract to struct (manual, or re-marshal to ClassAd first) +var restored Job +classadStr2 := ad3.String() +classad.Unmarshal(classadStr2, &restored) +``` + +### Example 4: Dynamic Job Template with Expressions + +This example shows how to use `*Expr` fields to create job templates with formulas that evaluate based on runtime context: + +```go +type JobTemplate struct { + Name string `classad:"Name"` + BaseCPUs int `classad:"BaseCPUs"` + BaseMemory int `classad:"BaseMemory"` + ComputedCPUs *classad.Expr `classad:"ComputedCPUs"` // Formula + ComputedMemory *classad.Expr `classad:"ComputedMemory"` // Formula + Priority *classad.Expr `classad:"Priority"` // Formula +} + +// Create a template with formulas +cpuExpr, _ := classad.ParseExpr("BaseCPUs * ScaleFactor") +memExpr, _ := classad.ParseExpr("BaseMemory * ScaleFactor") +priorityExpr, _ := classad.ParseExpr("BaseCPUs * 10 + BaseMemory / 1024") + +template := JobTemplate{ + Name: "batch-job", + BaseCPUs: 2, + BaseMemory: 4096, + ComputedCPUs: cpuExpr, + ComputedMemory: memExpr, + Priority: priorityExpr, +} + +// Save template to file +templateStr, _ := classad.Marshal(template) +os.WriteFile("job-template.classad", []byte(templateStr), 0644) + +// Later, load and evaluate with different contexts +data, _ := os.ReadFile("job-template.classad") +var loaded JobTemplate +classad.Unmarshal(string(data), &loaded) + +// Evaluate for small job +smallContext := classad.New() +smallContext.InsertAttr("ScaleFactor", 1) +smallCPUs := loaded.ComputedCPUs.Eval(smallContext) // 2 * 1 = 2 +smallMem := loaded.ComputedMemory.Eval(smallContext) // 4096 * 1 = 4096 + +// Evaluate for large job +largeContext := classad.New() +largeContext.InsertAttr("ScaleFactor", 4) +largeCPUs := loaded.ComputedCPUs.Eval(largeContext) // 2 * 4 = 8 +largeMem := loaded.ComputedMemory.Eval(largeContext) // 4096 * 4 = 16384 +``` + +### Example 5: Nested Configuration with ClassAd Fields + +This example shows using `*ClassAd` fields for flexible nested configurations: + +```go +type ServiceConfig struct { + Name string `classad:"Name"` + Enabled bool `classad:"Enabled"` + Database *classad.ClassAd `classad:"Database"` + Cache *classad.ClassAd `classad:"Cache"` + Auth *classad.ClassAd `classad:"Auth,omitempty"` +} + +// Build database config +dbConfig := classad.New() +dbConfig.InsertAttrString("host", "db.example.com") +dbConfig.InsertAttr("port", 5432) +dbConfig.InsertAttrString("name", "myapp") +dbConfig.InsertAttr("pool_size", 10) + +// Build cache config +cacheConfig := classad.New() +cacheConfig.InsertAttrString("type", "redis") +cacheConfig.InsertAttrString("host", "cache.example.com") +cacheConfig.InsertAttr("port", 6379) +cacheConfig.InsertAttr("ttl", 3600) + +// Create service config +config := ServiceConfig{ + Name: "myservice", + Enabled: true, + Database: dbConfig, + Cache: cacheConfig, + Auth: nil, // Optional, will be undefined +} + +// Marshal to file +configStr, _ := classad.Marshal(config) +os.WriteFile("service.config", []byte(configStr), 0644) +// Result: +// [Name = "myservice"; Enabled = true; +// Database = [host = "db.example.com"; port = 5432; name = "myapp"; pool_size = 10]; +// Cache = [type = "redis"; host = "cache.example.com"; port = 6379; ttl = 3600]; +// Auth = undefined] + +// Load and use +data, _ := os.ReadFile("service.config") +var loaded ServiceConfig +classad.Unmarshal(string(data), &loaded) + +// Access nested values +dbHost, _ := loaded.Database.EvaluateAttrString("host") +dbPort, _ := loaded.Database.EvaluateAttrInt("port") +cacheType, _ := loaded.Cache.EvaluateAttrString("type") +cacheTTL, _ := loaded.Cache.EvaluateAttrInt("ttl") + +// Check if optional field is present +if loaded.Auth != nil { + // Auth config is available +} +``` + +### Example 6: REST API Integration + +```go +// API handler that accepts both formats +func CreateJob(w http.ResponseWriter, r *http.Request) { + var job Job + + contentType := r.Header.Get("Content-Type") + + if contentType == "application/json" { + // Handle JSON + var ad classad.ClassAd + json.NewDecoder(r.Body).Decode(&ad) + classad.Unmarshal(ad.String(), &job) + } else { + // Handle ClassAd format + body, _ := io.ReadAll(r.Body) + classad.Unmarshal(string(body), &job) + } + + // Process job... + + // Return in requested format + accept := r.Header.Get("Accept") + if accept == "application/json" { + // Return as JSON + classadStr, _ := classad.Marshal(job) + ad, _ := classad.Parse(classadStr) + json.NewEncoder(w).Encode(ad) + } else { + // Return as ClassAd + classadStr, _ := classad.Marshal(job) + w.Write([]byte(classadStr)) + } +} +``` + +## Best Practices + +### 1. Use Struct Tags Consistently + +Choose one tagging convention and stick to it: + +```go +// Good: Consistent use of classad tags +type Job struct { + ID int `classad:"JobId"` + Name string `classad:"Name"` + CPUs int `classad:"RequestCpus"` +} + +// Acceptable: Consistent use of json tags +type Job struct { + ID int `json:"job_id"` + Name string `json:"name"` + CPUs int `json:"cpus"` +} + +// Avoid: Mixed tags without reason +type Job struct { + ID int `classad:"JobId"` + Name string `json:"name"` // Why different? + CPUs int // And no tag here? +} +``` + +### 2. Use omitempty for Optional Fields + +```go +type Job struct { + ID int `classad:"JobId"` + Name string `classad:"Name"` + // Optional fields + Email string `classad:"Email,omitempty"` + Tags []string `classad:"Tags,omitempty"` + Metadata map[string]string `classad:"Metadata,omitempty"` +} +``` + +### 3. Document ClassAd-Specific Field Names + +When using custom field names for HTCondor compatibility: + +```go +type Job struct { + // ClusterId is the HTCondor cluster ID (ClusterId in ClassAd) + ClusterID int `classad:"ClusterId"` + + // ProcId is the process ID within the cluster (ProcId in ClassAd) + ProcID int `classad:"ProcId"` + + // Owner is the job owner (Owner in ClassAd) + Owner string `classad:"Owner"` +} +``` + +### 4. Validate After Unmarshal + +```go +var job Job +if err := classad.Unmarshal(input, &job); err != nil { + return fmt.Errorf("unmarshal failed: %w", err) +} + +// Validate required fields +if job.ID == 0 { + return errors.New("job ID is required") +} +if job.Name == "" { + return errors.New("job name is required") +} +``` + +### 5. Handle Unknown Fields Gracefully + +The unmarshal process ignores unknown fields by default, which is usually desirable: + +```go +// Struct with subset of fields +type JobSummary struct { + ID int `classad:"JobId"` + Name string `classad:"Name"` + // Other fields in ClassAd are ignored +} + +// Full ClassAd with many fields +fullClassAd := `[ + JobId = 123; + Name = "test"; + Owner = "alice"; + RequestCpus = 4; + ExtraField = "ignored"; +]` + +var summary JobSummary +classad.Unmarshal(fullClassAd, &summary) +// Only ID and Name are populated +``` + +### 6. Use Pointers for Optional Structs + +```go +type Job struct { + ID int + Metadata *Metadata `classad:"Metadata,omitempty"` +} + +type Metadata struct { + Created string + Modified string +} + +// If Metadata is nil, it won't be marshaled +job := Job{ID: 123, Metadata: nil} +``` + +### 7. Combine Both Formats When Appropriate + +```go +// Store in ClassAd format (native HTCondor) +classadStr, _ := classad.Marshal(job) +os.WriteFile("job.classad", []byte(classadStr), 0644) + +// Expose via JSON API +ad, _ := classad.Parse(classadStr) +jsonBytes, _ := json.Marshal(ad) +w.Header().Set("Content-Type", "application/json") +w.Write(jsonBytes) +``` + +## Performance Considerations + +### Marshaling Performance + +- **ClassAd marshaling** uses reflection and is comparable to `json.Marshal` +- **JSON marshaling** of ClassAds converts to intermediate map structure +- For hot paths, consider caching marshaled results + +### Memory Usage + +- Both systems create intermediate representations during marshaling +- For large structures, consider streaming or chunking if possible +- Reuse struct instances when processing many items + +### Tips for Better Performance + +```go +// Pre-allocate slices with known capacity +type Job struct { + Tags []string `classad:"Tags"` +} + +job := Job{ + Tags: make([]string, 0, 10), // Pre-allocate +} + +// Reuse structs in loops +var job Job +for _, input := range inputs { + if err := classad.Unmarshal(input, &job); err != nil { + continue + } + // Process job... + // job will be reused in next iteration +} +``` + +## Error Handling + +Common errors and how to handle them: + +```go +// Unmarshal into wrong type +var job Job +err := classad.Unmarshal(`[ID = "not-a-number"]`, &job) +// Error: expected integer, got string + +// Unmarshal into non-pointer +err := classad.Unmarshal(input, job) // Wrong! +// Error: unmarshal target must be a pointer + +// Unmarshal nil pointer +var job *Job +err := classad.Unmarshal(input, job) // Wrong! +// Error: unmarshal target cannot be nil + +// Correct usage +var job Job +err := classad.Unmarshal(input, &job) // Correct! +``` + +## See Also + +- [Evaluation API Documentation](EVALUATION_API.md) +- [examples/struct_demo/](../examples/struct_demo/) - Working examples +- [examples/json_demo/](../examples/json_demo/) - JSON marshaling examples +- [HTCondor ClassAd Documentation](https://htcondor.readthedocs.io/en/latest/misc/classad-mechanism.html) diff --git a/docs/MARSHALING_QUICKREF.md b/docs/MARSHALING_QUICKREF.md new file mode 100644 index 0000000..086a885 --- /dev/null +++ b/docs/MARSHALING_QUICKREF.md @@ -0,0 +1,256 @@ +# Marshaling Quick Reference + +Quick reference for marshaling Go structs to/from ClassAd and JSON formats. + +## Struct Tags + +```go +type Job struct { + ID int `classad:"JobId"` // Custom ClassAd name + Owner string `json:"owner"` // Falls back to json tag + CPUs int // Uses field name "CPUs" + Optional string `classad:"Optional,omitempty"` // Omit if zero value + Tags []string `json:"tags,omitempty"` // json tag with omitempty + Internal string `classad:"-"` // Skip this field +} +``` + +## ClassAd Format + +### Marshal +```go +import "github.com/PelicanPlatform/classad/classad" + +job := Job{ID: 123, Owner: "alice", CPUs: 4} +classadStr, err := classad.Marshal(job) +// [JobId = 123; owner = "alice"; CPUs = 4] +``` + +### Unmarshal +```go +var job Job +err := classad.Unmarshal(`[JobId = 123; owner = "alice"; CPUs = 4]`, &job) +``` + +## JSON Format + +### Marshal ClassAd to JSON +```go +import "encoding/json" + +ad, _ := classad.Parse(`[x = 5; y = x + 3]`) +jsonBytes, err := json.Marshal(ad) +// {"x":5,"y":"\/Expr(x + 3)\/"} +``` + +### Unmarshal JSON to ClassAd +```go +var ad classad.ClassAd +err := json.Unmarshal(jsonBytes, &ad) +``` + +## Type Mapping + +| Go Type | ClassAd | JSON | +|---------|---------|------| +| `int`, `int64`, etc. | `123` | `123` | +| `float64` | `3.14` | `3.14` | +| `string` | `"text"` | `"text"` | +| `bool` | `true`/`false` | `true`/`false` | +| `[]T` | `{a, b, c}` | `["a","b","c"]` | +| `struct` | `[...]` | `{...}` | +| `map[string]T` | `[...]` | `{...}` | +| `*classad.ClassAd` | `[...]` | `{...}` | +| `*classad.Expr` | unevaluated expr | `"\/Expr(...)\/")` | +| Complex expr | N/A | `"\/Expr(...)\/")` | + +## Supported Operations + +| Operation | ClassAd Format | JSON Format | +|-----------|---------------|-------------| +| Struct → String | `classad.Marshal(v)` | `json.Marshal(ad)` | +| String → Struct | `classad.Unmarshal(s, &v)` | `json.Unmarshal(b, &ad)` | +| Parse to ClassAd | `classad.Parse(s)` | `json.Unmarshal(b, &ad)` | +| ClassAd → String | `ad.String()` | `json.Marshal(ad)` | + +## Expression Handling + +### In ClassAd Format +Expressions are evaluated: +```go +classadStr := `[x = 5; y = x + 3]` +ad, _ := classad.Parse(classadStr) +y := ad.EvaluateAttr("y") // Returns 8 +``` + +### In JSON Format +Expressions are serialized with `/Expr(...)/)` format: +```go +ad, _ := classad.Parse(`[x = 5; y = x + 3]`) +jsonBytes, _ := json.Marshal(ad) +// {"x":5,"y":"\/Expr(x + 3)\/"} + +var ad2 classad.ClassAd +json.Unmarshal(jsonBytes, &ad2) +y := ad2.EvaluateAttr("y") // Returns 8 (evaluated in context) +``` + +## Common Patterns + +### Round-trip: Struct → ClassAd → JSON +```go +// 1. Struct to ClassAd +job := Job{ID: 123, Owner: "alice"} +classadStr, _ := classad.Marshal(job) + +// 2. Parse ClassAd +ad, _ := classad.Parse(classadStr) + +// 3. ClassAd to JSON +jsonBytes, _ := json.Marshal(ad) + +// 4. JSON back to ClassAd +var ad2 classad.ClassAd +json.Unmarshal(jsonBytes, &ad2) + +// 5. ClassAd to Struct +classadStr2 := ad2.String() +var job2 Job +classad.Unmarshal(classadStr2, &job2) +``` + +### Work with map[string]interface{} +```go +// Marshal map to ClassAd +data := map[string]interface{}{ + "id": 123, + "name": "test", +} +classadStr, _ := classad.Marshal(data) + +// Unmarshal ClassAd to map +var restored map[string]interface{} +classad.Unmarshal(classadStr, &restored) +``` + +### Nested Structs +```go +type Config struct { + Timeout int + Server string +} + +type Job struct { + ID int + Config Config +} + +job := Job{ID: 123, Config: Config{Timeout: 30, Server: "host"}} +classadStr, _ := classad.Marshal(job) +// [ID = 123; Config = [Timeout = 30; Server = "host"]] +``` + +### ClassAd Fields +```go +type Container struct { + Name string + Config *classad.ClassAd // Flexible nested ClassAd +} + +config := classad.New() +config.InsertAttr("timeout", 30) +config.InsertAttrString("server", "example.com") + +container := Container{Name: "test", Config: config} +classadStr, _ := classad.Marshal(container) +// [Name = "test"; Config = [timeout = 30; server = "example.com"]] + +// Unmarshal back +var restored Container +classad.Unmarshal(classadStr, &restored) +timeout, _ := restored.Config.EvaluateAttrInt("timeout") +``` + +### Expr Fields (Unevaluated Expressions) +```go +type Job struct { + Name string + CPUs int + Formula *classad.Expr // Preserved as expression +} + +formula, _ := classad.ParseExpr("CPUs * 2 + 8") +job := Job{Name: "test", CPUs: 4, Formula: formula} + +// Marshal - formula is NOT evaluated +classadStr, _ := classad.Marshal(job) +// [Name = "test"; CPUs = 4; Formula = CPUs * 2 + 8] + +// Unmarshal - formula preserved +var restored Job +classad.Unmarshal(classadStr, &restored) + +// Evaluate later with context +context := classad.New() +context.InsertAttr("CPUs", 4) +result := restored.Formula.Eval(context) // Evaluates to 16 +``` + +## Tag Options Summary + +| Tag | Effect | Example | +|-----|--------|---------| +| `classad:"name"` | Custom name | `JobId int \`classad:"ClusterId"\`` | +| `json:"name"` | Fallback name | `CPUs int \`json:"cpus"\`` | +| `,omitempty` | Skip if zero | `Tags []string \`classad:"Tags,omitempty"\`` | +| `"-"` | Always skip | `Secret string \`classad:"-"\`` | +| (no tag) | Use field name | `CPUs int` → `CPUs` | + +## Zero Values Behavior + +Without `omitempty`: +```go +type Job struct { + ID int // 0 will be marshaled + Name string // "" will be marshaled + Tags []string // nil/empty will be marshaled as {} +} +``` + +With `omitempty`: +```go +type Job struct { + ID int `classad:",omitempty"` // 0 will be omitted + Name string `classad:",omitempty"` // "" will be omitted + Tags []string `classad:",omitempty"` // nil/empty will be omitted +} +``` + +## Error Handling + +```go +// Always check errors +classadStr, err := classad.Marshal(job) +if err != nil { + log.Fatal(err) +} + +// Unmarshal requires pointer +var job Job +err = classad.Unmarshal(classadStr, &job) // Note: &job, not job +if err != nil { + log.Fatal(err) +} + +// Validate after unmarshal +if job.ID == 0 { + return errors.New("ID is required") +} +``` + +## See Also + +- [MARSHALING.md](MARSHALING.md) - Complete marshaling guide +- [EVALUATION_API.md](EVALUATION_API.md) - Evaluation API reference +- [examples/struct_demo/](../examples/struct_demo/) - Struct marshaling examples +- [examples/json_demo/](../examples/json_demo/) - JSON marshaling examples diff --git a/examples/README.md b/examples/README.md index 320c8d5..f28bdb9 100644 --- a/examples/README.md +++ b/examples/README.md @@ -4,29 +4,50 @@ This directory contains example programs and ClassAd files demonstrating various ## Example Programs -### simple_reader -**Location:** `examples/simple_reader/main.go` +### generic_api_demo +**Location:** `examples/generic_api_demo/main.go` -A command-line tool for reading and displaying ClassAds from files: -- Accepts a filename as argument -- Supports both new-style and old-style formats (use `--old` flag) -- Displays all attributes of each ClassAd -- Useful for testing and inspecting ClassAd files +**⭐ Recommended starting point** - Demonstrates the modern generic API for working with ClassAds: +- **Set()**: Setting attributes with any type +- **GetAs[T]()**: Type-safe attribute retrieval with generics +- **GetOr[T]()**: Getting attributes with default values +- **Type conversions**: Automatic safe conversions (int ↔ float64) +- **Slices and complex types**: Working with lists and nested data +- **Comparison**: Side-by-side with traditional API + +This example showcases the **recommended idiomatic API** for new Go code. Run with: ```bash -go run examples/simple_reader/main.go examples/jobs-multiple.ad -go run examples/simple_reader/main.go examples/machines-old.ad --old +go run examples/generic_api_demo/main.go +``` + +### api_demo +**Location:** `examples/api_demo/main.go` + +Comprehensive demonstration of the ClassAd API including: +- Creating ClassAds programmatically with **Set()** +- Parsing ClassAds from strings +- Type-safe retrieval with **GetAs[T]()** and **GetOr[T]()** +- Evaluating attributes (both new and traditional APIs) +- Working with different value types +- Arithmetic and logical expressions +- Modifying ClassAds +- Real-world HTCondor scenarios + +Run with: +```bash +go run examples/api_demo/main.go ``` ### reader_demo **Location:** `examples/reader_demo/main.go` -Demonstrates reading multiple ClassAds from various sources using the Reader API: +Demonstrates reading multiple ClassAds from various sources using the Reader API with generic API: - **Reading new-style ClassAds**: Parsing bracketed ClassAds from strings/files - **Reading old-style ClassAds**: Parsing newline-delimited ClassAds separated by blank lines - **For-loop iteration**: Using the Reader in idiomatic Go for-loops -- **Filtering**: Processing only ClassAds that match certain criteria +- **Filtering**: Processing only ClassAds that match certain criteria (using **GetAs[T]**) - **File I/O**: Reading ClassAds from files - **Nested structures**: Working with nested ClassAds in iteration @@ -38,12 +59,13 @@ go run examples/reader_demo/main.go ### range_demo **Location:** `examples/range_demo/main.go` -Demonstrates Go 1.23+ range-over-function iterator pattern: +Demonstrates Go 1.23+ range-over-function iterator pattern with generic API: - **Simple iteration**: Using `for ad := range classad.All(reader)` - **Indexed iteration**: Using `for i, ad := range classad.AllWithIndex(reader)` - **Error handling**: Using `AllWithError` to capture errors during iteration - **Old-style support**: Using `AllOld` for newline-delimited ClassAds - **File I/O**: Reading ClassAds from files with range syntax +- **Generic API**: Using **GetOr[T]()** for safe attribute access This example showcases the modern, ergonomic way to iterate over ClassAds using Go 1.23+ features. @@ -52,38 +74,19 @@ Run with: go run examples/range_demo/main.go ``` -### features_demo -**Location:** `examples/features_demo/main.go` - -A comprehensive demonstration of advanced ClassAd features including: -- **Nested ClassAds**: Working with hierarchical data structures -- **IS and ISNT operators**: Strict identity checking vs. value equality -- **Meta-equal operators**: `=?=` and `=!=` aliases for `is` and `isnt` -- **Attribute selection**: `record.field` syntax for accessing nested attributes -- **Subscript expressions**: `list[index]` and `record["key"]` for indexing -- **String functions**: `strcat`, `substr`, `size`, `toUpper`, `toLower` -- **Math functions**: `floor`, `ceiling`, `round`, `int`, `real`, `random` -- **Type checking**: `isString`, `isInteger`, `isReal`, `isBoolean`, `isList`, `isClassAd`, `isUndefined` -- **List operations**: `member`, `size` -- **Real-world scenario**: HTCondor job matching simulation - -Run with: -```bash -go run examples/features_demo/main.go -``` - -### api_demo -**Location:** `examples/api_demo/main.go` +### simple_reader +**Location:** `examples/simple_reader/main.go` -Basic demonstration of the ClassAd API including: -- Parsing ClassAds from strings -- Evaluating attributes -- Working with different value types -- Attribute references and expressions +A command-line tool for reading and displaying ClassAds from files: +- Accepts a filename as argument +- Supports both new-style and old-style formats (use `--old` flag) +- Displays all attributes of each ClassAd +- Useful for testing and inspecting ClassAd files Run with: ```bash -go run examples/api_demo/main.go +go run examples/simple_reader/main.go examples/jobs-multiple.ad +go run examples/simple_reader/main.go examples/machines-old.ad --old ``` ### expr_demo @@ -158,6 +161,29 @@ Collection of sample ClassAd expressions demonstrating: ## Usage Examples +### Modern API (Recommended) + +#### Creating and accessing attributes with generics +```go +// Create a ClassAd +ad := classad.New() + +// Set attributes with any type +ad.Set("Cpus", 4) +ad.Set("Memory", 8192.0) +ad.Set("Owner", "alice") +ad.Set("Tags", []string{"prod", "gpu"}) + +// Type-safe retrieval with GetAs[T]() +if cpus, ok := classad.GetAs[int](ad, "Cpus"); ok { + fmt.Printf("Cpus: %d\n", cpus) +} + +// Get with default values using GetOr[T]() +owner := classad.GetOr(ad, "Owner", "unknown") +priority := classad.GetOr(ad, "Priority", 10) // Uses default if missing +``` + ### Parsing a ClassAd from a file (new format) ```go data, err := os.ReadFile("examples/job.ad") @@ -169,6 +195,10 @@ ad, err := classad.Parse(string(data)) if err != nil { log.Fatal(err) } + +// Access attributes with generic API +jobId := classad.GetOr(ad, "JobId", 0) +owner := classad.GetOr(ad, "Owner", "unknown") ``` ### Parsing a ClassAd from a file (old format) @@ -184,12 +214,14 @@ if err != nil { } ``` -### Evaluating attributes +### Traditional API (Still Supported) + +#### Evaluating attributes ```go // Evaluate to specific type -owner, err := ad.EvaluateAttrString("Owner") -cpus, err := ad.EvaluateAttrInt("RequestCpus") -memory, err := ad.EvaluateAttrInt("RequestMemory") +owner, ok := ad.EvaluateAttrString("Owner") +cpus, ok := ad.EvaluateAttrInt("RequestCpus") +memory, ok := ad.EvaluateAttrInt("RequestMemory") // Evaluate to generic Value val := ad.EvaluateAttr("Requirements") diff --git a/examples/api_demo/main.go b/examples/api_demo/main.go index e4a9969..e5ff0d4 100644 --- a/examples/api_demo/main.go +++ b/examples/api_demo/main.go @@ -11,13 +11,13 @@ func main() { fmt.Println("=== ClassAd Public API Examples ===") fmt.Println() - // Example 1: Creating a ClassAd programmatically - fmt.Println("Example 1: Creating ClassAds programmatically") + // Example 1: Creating a ClassAd programmatically with generic Set() API + fmt.Println("Example 1: Creating ClassAds programmatically (using Set)") ad := classad.New() - ad.InsertAttr("Cpus", 4) - ad.InsertAttrFloat("Memory", 8192.0) - ad.InsertAttrString("Name", "worker-01") - ad.InsertAttrBool("IsAvailable", true) + ad.Set("Cpus", 4) + ad.Set("Memory", 8192.0) + ad.Set("Name", "worker-01") + ad.Set("IsAvailable", true) fmt.Printf("Created ClassAd: %s\n", ad) fmt.Printf("Size: %d attributes\n", ad.Size()) @@ -46,35 +46,48 @@ func main() { } fmt.Println() - // Example 4: Evaluating attributes with type safety - fmt.Println("Example 4: Evaluating attributes with type safety") + // Example 4: Using generic GetAs[T]() for type-safe retrieval + fmt.Println("Example 4: Type-safe retrieval with GetAs[T]()") - if jobId, ok := jobAd.EvaluateAttrInt("JobId"); ok { + if jobId, ok := classad.GetAs[int](jobAd, "JobId"); ok { fmt.Printf("JobId = %d\n", jobId) } - if owner, ok := jobAd.EvaluateAttrString("Owner"); ok { + if owner, ok := classad.GetAs[string](jobAd, "Owner"); ok { fmt.Printf("Owner = %s\n", owner) } - if cpus, ok := jobAd.EvaluateAttrNumber("Cpus"); ok { - fmt.Printf("Cpus = %g\n", cpus) + if cpus, ok := classad.GetAs[int](jobAd, "Cpus"); ok { + fmt.Printf("Cpus = %d\n", cpus) } - if memory, ok := jobAd.EvaluateAttrInt("Memory"); ok { + if memory, ok := classad.GetAs[int](jobAd, "Memory"); ok { fmt.Printf("Memory = %d MB\n", memory) } fmt.Println() - // Example 5: Evaluating complex expressions - fmt.Println("Example 5: Evaluating complex expressions") + // Example 5: Using GetOr[T]() with defaults + fmt.Println("Example 5: Using GetOr[T]() with default values") + + status := classad.GetOr(jobAd, "Status", "Unknown") + fmt.Printf("Status = %s\n", status) + + priority := classad.GetOr(jobAd, "Priority", 10) // Missing, uses default + fmt.Printf("Priority = %d (default)\n", priority) + + cpusWithDefault := classad.GetOr(jobAd, "Cpus", 1) + fmt.Printf("Cpus = %d\n", cpusWithDefault) + fmt.Println() + + // Example 6: Evaluating complex expressions (traditional API) + fmt.Println("Example 6: Evaluating complex expressions") if requirements, ok := jobAd.EvaluateAttrBool("Requirements"); ok { fmt.Printf("Requirements evaluate to: %v\n", requirements) } fmt.Println() - // Example 6: Using EvaluateExpr for direct Value evaluation - fmt.Println("Example 6: Using EvaluateExpr with attribute values") + // Example 7: Using EvaluateExpr for direct Value evaluation + fmt.Println("Example 7: Using EvaluateExpr with attribute values") val := jobAd.EvaluateAttr("JobId") fmt.Printf("JobId value: %s (type: %v)\n", val.String(), val.Type()) @@ -82,8 +95,8 @@ func main() { fmt.Printf("Requirements value: %s\n", requirementsVal.String()) fmt.Println() - // Example 7: Working with arithmetic expressions - fmt.Println("Example 7: Working with arithmetic expressions") + // Example 8: Working with arithmetic expressions + fmt.Println("Example 8: Working with arithmetic expressions") calcAd, _ := classad.Parse(`[ a = 10; b = 20; @@ -94,10 +107,10 @@ func main() { remainder = b % a ]`) - if sum, ok := calcAd.EvaluateAttrInt("sum"); ok { + if sum, ok := classad.GetAs[int](calcAd, "sum"); ok { fmt.Printf("sum = %d\n", sum) } - if diff, ok := calcAd.EvaluateAttrInt("difference"); ok { + if diff, ok := classad.GetAs[int](calcAd, "difference"); ok { fmt.Printf("difference = %d\n", diff) } if prod, ok := calcAd.EvaluateAttrInt("product"); ok { @@ -111,8 +124,8 @@ func main() { } fmt.Println() - // Example 8: Working with logical expressions - fmt.Println("Example 8: Working with logical expressions") + // Example 9: Working with logical expressions + fmt.Println("Example 9: Working with logical expressions") logicAd, _ := classad.Parse(`[ hasEnoughCpus = Cpus >= 2; hasEnoughMemory = Memory >= 2048; @@ -121,13 +134,13 @@ func main() { Memory = 4096 ]`) - if meets, ok := logicAd.EvaluateAttrBool("meetsRequirements"); ok { + if meets, ok := classad.GetAs[bool](logicAd, "meetsRequirements"); ok { fmt.Printf("meetsRequirements = %v\n", meets) } fmt.Println() - // Example 9: Conditional expressions - fmt.Println("Example 9: Conditional expressions") + // Example 10: Conditional expressions + fmt.Println("Example 10: Conditional expressions") condAd, _ := classad.Parse(`[ x = 10; y = 5; @@ -136,24 +149,24 @@ func main() { status = x > y ? "x is greater" : "y is greater" ]`) - if max, ok := condAd.EvaluateAttrInt("max"); ok { + if max, ok := classad.GetAs[int](condAd, "max"); ok { fmt.Printf("max = %d\n", max) } - if min, ok := condAd.EvaluateAttrInt("min"); ok { + if min, ok := classad.GetAs[int](condAd, "min"); ok { fmt.Printf("min = %d\n", min) } - if status, ok := condAd.EvaluateAttrString("status"); ok { + if status, ok := classad.GetAs[string](condAd, "status"); ok { fmt.Printf("status = %s\n", status) } fmt.Println() - // Example 10: Modifying ClassAds - fmt.Println("Example 10: Modifying ClassAds") + // Example 11: Modifying ClassAds with Set() + fmt.Println("Example 11: Modifying ClassAds") fmt.Printf("Attributes before: %v\n", ad.GetAttributes()) - // Update an existing attribute - ad.InsertAttr("Cpus", 8) - if cpus, ok := ad.EvaluateAttrInt("Cpus"); ok { + // Update an existing attribute with Set() + ad.Set("Cpus", 8) + if cpus, ok := classad.GetAs[int](ad, "Cpus"); ok { fmt.Printf("Updated Cpus = %d\n", cpus) } @@ -166,8 +179,8 @@ func main() { fmt.Printf("Size: %d attributes\n", ad.Size()) fmt.Println() - // Example 11: Real-world HTCondor scenario - fmt.Println("Example 11: Real-world HTCondor scenario") + // Example 12: Real-world HTCondor scenario with generic API + fmt.Println("Example 12: Real-world HTCondor scenario") machineAd, _ := classad.Parse(`[ Name = "slot1@worker.example.com"; Machine = "worker.example.com"; @@ -181,29 +194,39 @@ func main() { ]`) fmt.Println("Machine ClassAd:") - if name, ok := machineAd.EvaluateAttrString("Name"); ok { - fmt.Printf(" Name: %s\n", name) - } - if cpus, ok := machineAd.EvaluateAttrInt("Cpus"); ok { - fmt.Printf(" Cpus: %d\n", cpus) - } - if memory, ok := machineAd.EvaluateAttrInt("Memory"); ok { - fmt.Printf(" Memory: %d MB\n", memory) - } - if disk, ok := machineAd.EvaluateAttrInt("Disk"); ok { - fmt.Printf(" Disk: %d KB\n", disk) - } - if state, ok := machineAd.EvaluateAttrString("State"); ok { - fmt.Printf(" State: %s\n", state) - } + name := classad.GetOr(machineAd, "Name", "unknown") + fmt.Printf(" Name: %s\n", name) + + cpusM := classad.GetOr(machineAd, "Cpus", 0) + fmt.Printf(" Cpus: %d\n", cpusM) + + memoryM := classad.GetOr(machineAd, "Memory", 0) + fmt.Printf(" Memory: %d MB\n", memoryM) + + diskM := classad.GetOr(machineAd, "Disk", 0) + fmt.Printf(" Disk: %d KB\n", diskM) + + state := classad.GetOr(machineAd, "State", "Unknown") + fmt.Printf(" State: %s\n", state) fmt.Println() - // Example 12: Handling undefined values - fmt.Println("Example 12: Handling undefined values") + // Example 13: Handling undefined values + fmt.Println("Example 13: Handling undefined values") testAd := classad.New() - testAd.InsertAttr("x", 10) + testAd.Set("x", 10) + + // Try to evaluate a non-existent attribute with GetAs + if value, ok := classad.GetAs[int](testAd, "nonexistent"); ok { + fmt.Printf("Value: %d\n", value) + } else { + fmt.Println("Attribute 'nonexistent' is undefined or wrong type") + } + + // GetOr provides a safe default + defaultValue := classad.GetOr(testAd, "nonexistent", 999) + fmt.Printf("Using GetOr with default: %d\n", defaultValue) - // Try to evaluate a non-existent attribute + // Traditional API for checking undefined value := testAd.EvaluateAttr("nonexistent") if value.IsUndefined() { fmt.Println("Attribute 'nonexistent' is undefined") diff --git a/examples/classad_expr_demo/README.md b/examples/classad_expr_demo/README.md new file mode 100644 index 0000000..5eccd18 --- /dev/null +++ b/examples/classad_expr_demo/README.md @@ -0,0 +1,112 @@ +# ClassAd and Expr Fields Demo + +This example demonstrates how to use `*classad.ClassAd` and `*classad.Expr` fields in Go structs when marshaling and unmarshaling ClassAds. + +## Features Demonstrated + +1. **`*classad.ClassAd` Fields**: Store nested ClassAds without defining intermediate struct types +2. **`*classad.Expr` Fields**: Preserve unevaluated expressions for later evaluation in different contexts +3. **Nil Handling**: How nil `*ClassAd` and `*Expr` fields are marshaled + +## Running the Example + +```bash +go run main.go +``` + +## Example 1: *classad.ClassAd Fields + +Shows how to embed arbitrary ClassAds within a struct: + +```go +type ServiceConfig struct { + Name string `classad:"Name"` + Database *classad.ClassAd `classad:"Database"` + Cache *classad.ClassAd `classad:"Cache"` +} +``` + +This is useful when: +- You don't want to define a separate struct for nested configuration +- The nested structure varies dynamically +- You need to preserve the exact ClassAd structure + +## Example 2: *classad.Expr Fields + +Shows how to preserve unevaluated expressions: + +```go +type JobTemplate struct { + BaseCPUs int `classad:"BaseCPUs"` + ComputedCPUs *classad.Expr `classad:"ComputedCPUs"` +} +``` + +Key benefits: +- Expressions are NOT evaluated during marshal/unmarshal +- Can evaluate later with different contexts +- Perfect for templates and formulas + +```go +// Create expression +cpuExpr, _ := classad.ParseExpr("BaseCPUs * ScaleFactor") + +// Marshal preserves the formula +template := JobTemplate{BaseCPUs: 2, ComputedCPUs: cpuExpr} +classadStr, _ := classad.Marshal(template) +// Result: [BaseCPUs = 2; ComputedCPUs = BaseCPUs * ScaleFactor] + +// Evaluate with context +context := classad.New() +context.InsertAttr("ScaleFactor", 4) +result := restored.ComputedCPUs.Eval(context) // 2 * 4 = 8 +``` + +## Example 3: Nil Fields + +Shows how nil `*ClassAd` and `*Expr` fields are handled: + +```go +type OptionalConfig struct { + Name string `classad:"Name"` + Database *classad.ClassAd `classad:"Database,omitempty"` + Formula *classad.Expr `classad:"Formula,omitempty"` +} + +// Nil fields are marshaled as undefined (or omitted with omitempty) +config := OptionalConfig{Name: "test", Database: nil, Formula: nil} +``` + +## Use Cases + +### Dynamic Configuration +```go +type AppConfig struct { + Name string + Settings *classad.ClassAd // Can vary per deployment +} +``` + +### Job Templates +```go +type JobTemplate struct { + BaseCPUs int + BaseMemory int + Formula *classad.Expr // Evaluated per job +} +``` + +### Multi-Environment Configs +```go +type DeployConfig struct { + App string + Development *classad.ClassAd + Production *classad.ClassAd +} +``` + +## See Also + +- [MARSHALING.md](../../docs/MARSHALING.md) - Complete marshaling guide +- [MARSHALING_QUICKREF.md](../../docs/MARSHALING_QUICKREF.md) - Quick reference +- [struct_demo](../struct_demo/) - Basic struct marshaling examples diff --git a/examples/classad_expr_demo/main.go b/examples/classad_expr_demo/main.go new file mode 100644 index 0000000..4a2fedf --- /dev/null +++ b/examples/classad_expr_demo/main.go @@ -0,0 +1,177 @@ +package main + +import ( + "fmt" + "log" + + "github.com/PelicanPlatform/classad/classad" +) + +// Example demonstrating *classad.ClassAd and *classad.Expr fields in structs + +func exampleClassAdFields() { + fmt.Println("\n=== Example: *classad.ClassAd Fields ===") + + type ServiceConfig struct { + Name string `classad:"Name"` + Enabled bool `classad:"Enabled"` + Database *classad.ClassAd `classad:"Database"` + Cache *classad.ClassAd `classad:"Cache"` + } + + // Build nested ClassAds + dbConfig := classad.New() + dbConfig.InsertAttrString("host", "db.example.com") + dbConfig.InsertAttr("port", 5432) + dbConfig.InsertAttrString("name", "myapp") + + cacheConfig := classad.New() + cacheConfig.InsertAttrString("type", "redis") + cacheConfig.InsertAttrString("host", "cache.example.com") + cacheConfig.InsertAttr("port", 6379) + + config := ServiceConfig{ + Name: "myservice", + Enabled: true, + Database: dbConfig, + Cache: cacheConfig, + } + + // Marshal to ClassAd format + classadStr, err := classad.Marshal(config) + if err != nil { + log.Fatal(err) + } + fmt.Println("Marshaled:") + fmt.Println(classadStr) + + // Unmarshal back + var restored ServiceConfig + err = classad.Unmarshal(classadStr, &restored) + if err != nil { + log.Fatal(err) + } + + // Access nested ClassAd values + dbHost, _ := restored.Database.EvaluateAttrString("host") + dbPort, _ := restored.Database.EvaluateAttrInt("port") + cacheType, _ := restored.Cache.EvaluateAttrString("type") + + fmt.Printf("\nRestored values:\n") + fmt.Printf(" Name: %s\n", restored.Name) + fmt.Printf(" Database host: %s:%d\n", dbHost, dbPort) + fmt.Printf(" Cache type: %s\n", cacheType) +} + +func exampleExprFields() { + fmt.Println("\n=== Example: *classad.Expr Fields ===") + + type JobTemplate struct { + Name string `classad:"Name"` + BaseCPUs int `classad:"BaseCPUs"` + BaseMemory int `classad:"BaseMemory"` + ComputedCPUs *classad.Expr `classad:"ComputedCPUs"` + ComputedMemory *classad.Expr `classad:"ComputedMemory"` + } + + // Create expressions that will be evaluated later + cpuExpr, _ := classad.ParseExpr("BaseCPUs * ScaleFactor") + memExpr, _ := classad.ParseExpr("BaseMemory * ScaleFactor") + + template := JobTemplate{ + Name: "batch-job", + BaseCPUs: 2, + BaseMemory: 4096, + ComputedCPUs: cpuExpr, + ComputedMemory: memExpr, + } + + // Marshal - expressions are preserved (not evaluated) + classadStr, err := classad.Marshal(template) + if err != nil { + log.Fatal(err) + } + fmt.Println("Marshaled template:") + fmt.Println(classadStr) + + // Unmarshal back + var restored JobTemplate + err = classad.Unmarshal(classadStr, &restored) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("\nExpression formulas (unevaluated):\n") + fmt.Printf(" ComputedCPUs: %s\n", restored.ComputedCPUs.String()) + fmt.Printf(" ComputedMemory: %s\n", restored.ComputedMemory.String()) + + // Evaluate with different scale factors + // Note: Need to provide both BaseCPUs/BaseMemory and ScaleFactor + fmt.Println("\nEvaluating with ScaleFactor=1:") + context1 := classad.New() + context1.InsertAttr("BaseCPUs", int64(restored.BaseCPUs)) + context1.InsertAttr("BaseMemory", int64(restored.BaseMemory)) + context1.InsertAttr("ScaleFactor", 1) + cpus1 := restored.ComputedCPUs.Eval(context1) + mem1 := restored.ComputedMemory.Eval(context1) + cpusVal1, _ := cpus1.IntValue() + memVal1, _ := mem1.IntValue() + fmt.Printf(" CPUs: %d, Memory: %d MB\n", cpusVal1, memVal1) + + fmt.Println("\nEvaluating with ScaleFactor=4:") + context2 := classad.New() + context2.InsertAttr("BaseCPUs", int64(restored.BaseCPUs)) + context2.InsertAttr("BaseMemory", int64(restored.BaseMemory)) + context2.InsertAttr("ScaleFactor", 4) + cpus2 := restored.ComputedCPUs.Eval(context2) + mem2 := restored.ComputedMemory.Eval(context2) + cpusVal2, _ := cpus2.IntValue() + memVal2, _ := mem2.IntValue() + fmt.Printf(" CPUs: %d, Memory: %d MB\n", cpusVal2, memVal2) +} + +func exampleNilFields() { + fmt.Println("\n=== Example: Nil *ClassAd and *Expr Fields ===") + + type OptionalConfig struct { + Name string `classad:"Name"` + Required int `classad:"Required"` + Database *classad.ClassAd `classad:"Database,omitempty"` + Formula *classad.Expr `classad:"Formula,omitempty"` + } + + // Create with nil optional fields + config := OptionalConfig{ + Name: "minimal", + Required: 42, + Database: nil, + Formula: nil, + } + + classadStr, err := classad.Marshal(config) + if err != nil { + log.Fatal(err) + } + fmt.Println("Marshaled with nil fields:") + fmt.Println(classadStr) + + // Unmarshal and check + var restored OptionalConfig + err = classad.Unmarshal(classadStr, &restored) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("\nRestored nil checks:\n") + fmt.Printf(" Database is nil: %v\n", restored.Database == nil) + fmt.Printf(" Formula is nil: %v\n", restored.Formula == nil) +} + +func main() { + fmt.Println("ClassAd and Expr Fields Examples") + fmt.Println("=================================") + + exampleClassAdFields() + exampleExprFields() + exampleNilFields() +} diff --git a/examples/generic_api_demo/README.md b/examples/generic_api_demo/README.md new file mode 100644 index 0000000..d342721 --- /dev/null +++ b/examples/generic_api_demo/README.md @@ -0,0 +1,176 @@ +# Generic ClassAd API Demo + +This example demonstrates the new idiomatic generic API for working with ClassAds: `Set()`, `GetAs[T]()`, and `GetOr[T]()`. + +## Overview + +The generic API provides a more Go-idiomatic way to work with ClassAds, reducing verbosity and leveraging Go's type inference and generics. + +## Features + +### 1. **Generic `Set()` Method** + +Set any type of value without type-specific methods: + +```go +ad := classad.New() +ad.Set("cpus", 4) // int +ad.Set("price", 0.05) // float64 +ad.Set("name", "worker-1") // string +ad.Set("enabled", true) // bool +ad.Set("tags", []string{"a"}) // slice +ad.Set("config", nestedAd) // *ClassAd +ad.Set("formula", expr) // *Expr +``` + +**Old API:** +```go +ad.InsertAttr("cpus", 4) +ad.InsertAttrFloat("price", 0.05) +ad.InsertAttrString("name", "worker-1") +ad.InsertAttrBool("enabled", true) +``` + +### 2. **Type-Safe `GetAs[T]()` Method** + +Retrieve values with compile-time type safety: + +```go +cpus, ok := classad.GetAs[int](ad, "cpus") +name, ok := classad.GetAs[string](ad, "name") +tags, ok := classad.GetAs[[]string](ad, "tags") +config, ok := classad.GetAs[*classad.ClassAd](ad, "config") +``` + +**Old API:** +```go +cpus, ok := ad.EvaluateAttrInt("cpus") +name, ok := ad.EvaluateAttrString("name") +// Slices required more complex handling +``` + +### 3. **Convenient `GetOr[T]()` with Defaults** + +Eliminate boilerplate if-checks by providing defaults: + +```go +cpus := classad.GetOr(ad, "cpus", 1) // Defaults to 1 +timeout := classad.GetOr(ad, "timeout", 300) // Defaults to 300 +owner := classad.GetOr(ad, "owner", "unknown") // Defaults to "unknown" +``` + +**Old API:** +```go +var cpus int64 +if val, ok := ad.EvaluateAttrInt("cpus"); ok { + cpus = val +} else { + cpus = 1 +} +``` + +## Running the Example + +```bash +go run main.go +``` + +## Examples in This Demo + +1. **Basic Set and Get**: Simple type handling +2. **GetOr with Defaults**: Eliminating nil checks +3. **Working with Slices**: Arrays of values +4. **Nested ClassAds**: Complex hierarchical data +5. **Expressions**: Unevaluated formulas +6. **Type Conversion**: Automatic int/float conversions +7. **API Comparison**: Old vs new approaches +8. **Real-World Config**: Practical configuration example + +## Benefits + +### Less Verbose +- `Set("x", 5)` vs `InsertAttr("x", 5)` +- No need to remember method names for each type + +### Type Inference +- Compiler infers types automatically +- Fewer type annotations needed + +### Defaults Made Easy +- `GetOr(ad, "timeout", 300)` handles missing values +- Eliminates if-checks and nil handling + +### Type Safety +- Compile-time type checking with generics +- `GetAs[int]()` ensures you get an `int` + +### Consistent API +- Same pattern for all types +- Easier to learn and remember + +## Backward Compatibility + +The old API remains available: +- `InsertAttr()`, `InsertAttrString()`, etc. still work +- `EvaluateAttrInt()`, `EvaluateAttrString()`, etc. still work +- Existing code continues to function + +The generic API is additive and provides an alternative, more idiomatic approach. + +## Type Support + +Supported types: +- **Integers**: `int`, `int8`, `int16`, `int32`, `int64`, `uint`, `uint8`, etc. +- **Floats**: `float32`, `float64` +- **Strings**: `string` +- **Booleans**: `bool` +- **Slices**: `[]T` for any supported type `T` +- **ClassAds**: `*classad.ClassAd` +- **Expressions**: `*classad.Expr` +- **Structs**: Any struct (marshaled to nested ClassAd) + +## Automatic Type Conversion + +The API handles common conversions: +```go +ad.Set("value", 42) + +// All of these work: +asInt, _ := classad.GetAs[int](ad, "value") // 42 +asInt64, _ := classad.GetAs[int64](ad, "value") // 42 +asFloat, _ := classad.GetAs[float64](ad, "value") // 42.0 +``` + +## Use Cases + +### Configuration Files +```go +serverName := classad.GetOr(config, "server_name", "default") +port := classad.GetOr(config, "port", 8080) +timeout := classad.GetOr(config, "timeout", 30) +``` + +### Job Specifications +```go +ad.Set("RequestCpus", 4) +ad.Set("RequestMemory", 8192) +ad.Set("Owner", "alice") + +cpus := classad.GetOr(ad, "RequestCpus", 1) +memory := classad.GetOr(ad, "RequestMemory", 1024) +``` + +### Dynamic Templates +```go +formula, _ := classad.ParseExpr("RequestCpus * 2") +ad.Set("ComputedCpus", formula) + +expr, _ := classad.GetAs[*classad.Expr](ad, "ComputedCpus") +result := expr.Eval(ad) +``` + +## See Also + +- [MARSHALING.md](../../docs/MARSHALING.md) - Struct marshaling guide +- [struct_demo](../struct_demo/) - Struct marshaling examples +- [api_demo](../api_demo/) - Original API examples diff --git a/examples/generic_api_demo/main.go b/examples/generic_api_demo/main.go new file mode 100644 index 0000000..b42a6da --- /dev/null +++ b/examples/generic_api_demo/main.go @@ -0,0 +1,289 @@ +package main + +import ( + "fmt" + "log" + + "github.com/PelicanPlatform/classad/classad" +) + +// Demonstrates the new generic Set(), GetAs[T](), and GetOr[T]() API + +func basicSetAndGet() { + fmt.Println("\n=== Basic Set() and GetAs[T]() ===") + + ad := classad.New() + + // Set values using the generic Set() API + ad.Set("cpus", 4) + ad.Set("memory", 8192) + ad.Set("name", "worker-node-1") + ad.Set("enabled", true) + ad.Set("price", 0.05) + + // Get values using the type-safe generic GetAs[T]() API + cpus, ok := classad.GetAs[int](ad, "cpus") + fmt.Printf("CPUs: %d (ok=%v)\n", cpus, ok) + + memory, ok := classad.GetAs[int](ad, "memory") + fmt.Printf("Memory: %d MB (ok=%v)\n", memory, ok) + + name, ok := classad.GetAs[string](ad, "name") + fmt.Printf("Name: %q (ok=%v)\n", name, ok) + + enabled, ok := classad.GetAs[bool](ad, "enabled") + fmt.Printf("Enabled: %v (ok=%v)\n", enabled, ok) + + price, ok := classad.GetAs[float64](ad, "price") + fmt.Printf("Price: $%.3f/hour (ok=%v)\n", price, ok) +} + +func setAndGetWithDefaults() { + fmt.Println("\n=== GetOr[T]() with Defaults ===") + + ad := classad.New() + ad.Set("cpus", 4) + ad.Set("memory", 8192) + // Note: timeout and owner are NOT set + + // Get existing values + cpus := classad.GetOr(ad, "cpus", 1) + fmt.Printf("CPUs: %d (exists)\n", cpus) + + memory := classad.GetOr(ad, "memory", 1024) + fmt.Printf("Memory: %d MB (exists)\n", memory) + + // Get missing values with defaults + timeout := classad.GetOr(ad, "timeout", 300) + fmt.Printf("Timeout: %d seconds (default)\n", timeout) + + owner := classad.GetOr(ad, "owner", "unknown") + fmt.Printf("Owner: %q (default)\n", owner) + + priority := classad.GetOr(ad, "priority", 5) + fmt.Printf("Priority: %d (default)\n", priority) +} + +func workWithSlices() { + fmt.Println("\n=== Working with Slices ===") + + ad := classad.New() + + // Set slices using generic Set() + ad.Set("tags", []string{"production", "critical", "monitored"}) + ad.Set("ports", []int{80, 443, 8080}) + + // Get slices using GetAs[T]() + tags, ok := classad.GetAs[[]string](ad, "tags") + fmt.Printf("Tags: %v (ok=%v)\n", tags, ok) + + ports, ok := classad.GetAs[[]int](ad, "ports") + fmt.Printf("Ports: %v (ok=%v)\n", ports, ok) + + // Get missing slice with default + labels := classad.GetOr(ad, "labels", []string{"default-label"}) + fmt.Printf("Labels: %v (default)\n", labels) +} + +func workWithNestedClassAds() { + fmt.Println("\n=== Working with Nested ClassAds ===") + + ad := classad.New() + + // Create nested configuration + dbConfig := classad.New() + dbConfig.Set("host", "db.example.com") + dbConfig.Set("port", 5432) + dbConfig.Set("name", "myapp") + dbConfig.Set("pool_size", 10) + + cacheConfig := classad.New() + cacheConfig.Set("type", "redis") + cacheConfig.Set("host", "cache.example.com") + cacheConfig.Set("port", 6379) + + // Set nested ClassAds + ad.Set("database", dbConfig) + ad.Set("cache", cacheConfig) + + // Get nested ClassAds + db, ok := classad.GetAs[*classad.ClassAd](ad, "database") + if ok { + host := classad.GetOr(db, "host", "localhost") + port := classad.GetOr(db, "port", 5432) + fmt.Printf("Database: %s:%d\n", host, port) + } + + cache, ok := classad.GetAs[*classad.ClassAd](ad, "cache") + if ok { + cacheType := classad.GetOr(cache, "type", "memory") + fmt.Printf("Cache type: %s\n", cacheType) + } +} + +func workWithExpressions() { + fmt.Println("\n=== Working with Expressions ===") + + ad := classad.New() + ad.Set("base_cpus", 2) + ad.Set("base_memory", 4096) + ad.Set("scale_factor", 4) + + // Set expressions that reference other attributes + cpuExpr, _ := classad.ParseExpr("base_cpus * scale_factor") + memExpr, _ := classad.ParseExpr("base_memory * scale_factor") + + ad.Set("computed_cpus", cpuExpr) + ad.Set("computed_memory", memExpr) + + // Get the unevaluated expressions + computedCpusExpr, ok := classad.GetAs[*classad.Expr](ad, "computed_cpus") + if ok { + fmt.Printf("CPU formula: %s\n", computedCpusExpr.String()) + + // Evaluate in context + result := computedCpusExpr.Eval(ad) + if value, err := result.IntValue(); err == nil { + fmt.Printf("Computed CPUs: %d\n", value) + } + } + + computedMemExpr, ok := classad.GetAs[*classad.Expr](ad, "computed_memory") + if ok { + fmt.Printf("Memory formula: %s\n", computedMemExpr.String()) + + // Evaluate in context + result := computedMemExpr.Eval(ad) + if value, err := result.IntValue(); err == nil { + fmt.Printf("Computed Memory: %d MB\n", value) + } + } +} + +func typeConversion() { + fmt.Println("\n=== Automatic Type Conversion ===") + + ad := classad.New() + ad.Set("value", 42) + + // Get as different types + asInt, ok := classad.GetAs[int](ad, "value") + fmt.Printf("As int: %d (ok=%v)\n", asInt, ok) + + asInt64, ok := classad.GetAs[int64](ad, "value") + fmt.Printf("As int64: %d (ok=%v)\n", asInt64, ok) + + asFloat, ok := classad.GetAs[float64](ad, "value") + fmt.Printf("As float64: %f (ok=%v)\n", asFloat, ok) + + // Set a float and get as int (truncates) + ad.Set("real_value", 3.7) + truncated, ok := classad.GetAs[int](ad, "real_value") + fmt.Printf("Float 3.7 as int: %d (truncated, ok=%v)\n", truncated, ok) +} + +func comparisonWithOldAPI() { + fmt.Println("\n=== API Comparison ===") + + ad := classad.New() + + fmt.Println("\n--- Old API ---") + // Old way (type-specific methods) + ad.InsertAttr("cpus", 4) + ad.InsertAttrFloat("price", 0.05) + ad.InsertAttrString("name", "worker-1") + ad.InsertAttrBool("enabled", true) + + cpus, ok := ad.EvaluateAttrInt("cpus") + fmt.Printf("CPUs: %d (ok=%v)\n", cpus, ok) + + price, ok := ad.EvaluateAttrReal("price") + fmt.Printf("Price: %f (ok=%v)\n", price, ok) + + fmt.Println("\n--- New Generic API ---") + // New way (generic methods) + ad2 := classad.New() + ad2.Set("cpus", 4) + ad2.Set("price", 0.05) + ad2.Set("name", "worker-1") + ad2.Set("enabled", true) + + cpus2 := classad.GetOr(ad2, "cpus", 1) + price2 := classad.GetOr(ad2, "price", 0.0) + name2 := classad.GetOr(ad2, "name", "unknown") + + fmt.Printf("CPUs: %d\n", cpus2) + fmt.Printf("Price: %f\n", price2) + fmt.Printf("Name: %s\n", name2) + + fmt.Println("\nBenefits:") + fmt.Println(" - Less verbose: Set() vs InsertAttr*()") + fmt.Println(" - Type inference: compiler infers types") + fmt.Println(" - Defaults: GetOr() eliminates if-checks") + fmt.Println(" - Type-safe: Generic types catch errors at compile time") +} + +func realWorldExample() { + fmt.Println("\n=== Real-World Configuration Example ===") + + // Load configuration from ClassAd + configStr := `[ + server_name = "api-server-1"; + port = 8080; + max_connections = 100; + timeout = 30; + tls_enabled = true; + allowed_origins = {"https://example.com", "https://app.example.com"}; + database = [ + host = "db.example.com"; + port = 5432; + pool_size = 20 + ] + ]` + + ad, err := classad.Parse(configStr) + if err != nil { + log.Fatal(err) + } + + // Extract configuration with defaults using generic API + serverName := classad.GetOr(ad, "server_name", "default-server") + port := classad.GetOr(ad, "port", 8080) + maxConn := classad.GetOr(ad, "max_connections", 50) + timeout := classad.GetOr(ad, "timeout", 60) + tlsEnabled := classad.GetOr(ad, "tls_enabled", false) + origins := classad.GetOr(ad, "allowed_origins", []string{}) + + fmt.Printf("Server: %s:%d\n", serverName, port) + fmt.Printf("Max connections: %d\n", maxConn) + fmt.Printf("Timeout: %d seconds\n", timeout) + fmt.Printf("TLS enabled: %v\n", tlsEnabled) + fmt.Printf("Allowed origins: %v\n", origins) + + // Get nested database config + if db, ok := classad.GetAs[*classad.ClassAd](ad, "database"); ok { + dbHost := classad.GetOr(db, "host", "localhost") + dbPort := classad.GetOr(db, "port", 5432) + poolSize := classad.GetOr(db, "pool_size", 10) + fmt.Printf("Database: %s:%d (pool size: %d)\n", dbHost, dbPort, poolSize) + } + + // The old API would require many more lines: + // if val, ok := ad.EvaluateAttrString("server_name"); ok { ... } else { serverName = "default" } + // if val, ok := ad.EvaluateAttrInt("port"); ok { ... } else { port = 8080 } + // ... and so on +} + +func main() { + fmt.Println("Generic ClassAd API Examples") + fmt.Println("============================") + + basicSetAndGet() + setAndGetWithDefaults() + workWithSlices() + workWithNestedClassAds() + workWithExpressions() + typeConversion() + comparisonWithOldAPI() + realWorldExample() +} diff --git a/examples/json_demo/README.md b/examples/json_demo/README.md new file mode 100644 index 0000000..c57b472 --- /dev/null +++ b/examples/json_demo/README.md @@ -0,0 +1,65 @@ +# JSON Serialization Demo + +This example demonstrates how to use JSON marshaling and unmarshaling with ClassAds. + +## Features Demonstrated + +1. **Marshal ClassAd to JSON** - Convert a ClassAd into JSON format +2. **Expression Serialization** - See how expressions are serialized with the special `\/Expr()\/` format +3. **Lists and Nested ClassAds** - Handle complex nested structures +4. **Unmarshal JSON to ClassAd** - Convert JSON back into a ClassAd +5. **Round-trip Serialization** - Verify that marshal→unmarshal preserves data + +## JSON Format + +The ClassAd JSON serialization follows these rules: + +- **Simple literals** (integers, reals, strings, booleans) → JSON values +- **undefined** → JSON `null` +- **Lists** → JSON arrays +- **Nested ClassAds** → JSON objects +- **Complex expressions** → Special format: `"/Expr()/"` (appears as `"\/Expr()\/"` in JSON due to `/` escaping) + +### Expression Format Example + +A ClassAd like: +``` +[x = 5; y = x + 3] +``` + +Serializes to JSON as: +```json +{ + "x": 5, + "y": "\/Expr(x + 3)\/" +} +``` + +The format is `/Expr(...)/)` where the forward slashes are escaped in JSON as `\/`. + +When unmarshaled back, the expression `x + 3` is preserved and will correctly evaluate to `8` in the context of the ClassAd. + +## Running the Demo + +```bash +go run main.go +``` + +## Sample Output + +The demo shows: +- Simple value serialization +- Expression handling with the special format +- Lists of both values and expressions +- Nested ClassAds as JSON objects +- JSON-to-ClassAd unmarshaling with expression evaluation +- Round-trip verification + +## Use Cases + +This JSON serialization is useful for: +- **REST APIs** - Send/receive ClassAds over HTTP +- **Configuration files** - Store ClassAds in JSON format +- **Interoperability** - Exchange ClassAds with non-Go systems +- **Database storage** - Store ClassAds in JSON columns +- **Logging** - Serialize ClassAds for structured logging diff --git a/examples/json_demo/main.go b/examples/json_demo/main.go new file mode 100644 index 0000000..f3dadcb --- /dev/null +++ b/examples/json_demo/main.go @@ -0,0 +1,160 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + + "github.com/PelicanPlatform/classad/classad" +) + +func main() { + fmt.Println("=== ClassAd JSON Serialization Demo ===") + fmt.Println() + + // Example 1: Marshal a simple ClassAd to JSON + fmt.Println("Example 1: Marshal ClassAd to JSON") + fmt.Println("-----------------------------------") + + ad1, err := classad.Parse(`[ + name = "job-123"; + cpus = 4; + memory = 8192; + priority = 10.5; + active = true + ]`) + if err != nil { + log.Fatal(err) + } + + jsonBytes, err := json.MarshalIndent(ad1, "", " ") + if err != nil { + log.Fatal(err) + } + + fmt.Printf("Original ClassAd:\n%s\n\n", ad1) + fmt.Printf("JSON representation:\n%s\n\n", string(jsonBytes)) + + // Example 2: Marshal expressions with the special format + fmt.Println("Example 2: Expressions in JSON") + fmt.Println("-------------------------------") + + ad2, err := classad.Parse(`[ + x = 10; + y = 20; + sum = x + y; + product = x * y; + conditional = x > 5 ? "high" : "low" + ]`) + if err != nil { + log.Fatal(err) + } + + jsonBytes, err = json.MarshalIndent(ad2, "", " ") + if err != nil { + log.Fatal(err) + } + + fmt.Printf("ClassAd with expressions:\n%s\n\n", ad2) + fmt.Printf("JSON (note expression format):\n%s\n\n", string(jsonBytes)) + + // Example 3: Lists and nested ClassAds + fmt.Println("Example 3: Lists and Nested ClassAds") + fmt.Println("-------------------------------------") + + ad3, err := classad.Parse(`[ + name = "complex-job"; + requirements = {cpus > 2, memory > 4096, disk > 10000}; + config = [ + timeout = 300; + retries = 3; + server = "example.com" + ]; + tags = {"production", "high-priority", "batch"} + ]`) + if err != nil { + log.Fatal(err) + } + + jsonBytes, err = json.MarshalIndent(ad3, "", " ") + if err != nil { + log.Fatal(err) + } + + fmt.Printf("ClassAd with nested structures:\n%s\n\n", ad3) + fmt.Printf("JSON representation:\n%s\n\n", string(jsonBytes)) + + // Example 4: Unmarshal JSON back to ClassAd + fmt.Println("Example 4: Unmarshal JSON to ClassAd") + fmt.Println("-------------------------------------") + + jsonInput := `{ + "job_id": 456, + "user": "alice", + "cpus": 8, + "memory": 16384, + "score": "\\/Expr((cpus * 100) + (memory / 1024))\\/", + "requirements": [2, 4, 8, 16], + "metadata": { + "created": "2024-01-01", + "priority": 5 + } + }` + + var ad4 classad.ClassAd + if err := json.Unmarshal([]byte(jsonInput), &ad4); err != nil { + log.Fatal(err) + } + + fmt.Printf("Input JSON:\n%s\n\n", jsonInput) + fmt.Printf("Resulting ClassAd:\n%s\n\n", &ad4) + + // Evaluate the expression + score := ad4.EvaluateAttr("score") + fmt.Printf("Evaluated 'score' attribute: %v\n", score) + if score.IsInteger() { + scoreVal, _ := score.IntValue() + fmt.Printf("Score value: %d (calculated from cpus and memory)\n\n", scoreVal) + } + + // Example 5: Round-trip serialization + fmt.Println("Example 5: Round-trip Serialization") + fmt.Println("------------------------------------") + + original, err := classad.Parse(`[ + a = 1; + b = a + 2; + c = {1, 2, 3}; + d = [x = 10; y = 20] + ]`) + if err != nil { + log.Fatal(err) + } + + // Marshal to JSON + jsonBytes, err = json.Marshal(original) + if err != nil { + log.Fatal(err) + } + + // Unmarshal back + var restored classad.ClassAd + if err := json.Unmarshal(jsonBytes, &restored); err != nil { + log.Fatal(err) + } + + fmt.Printf("Original ClassAd:\n%s\n\n", original) + fmt.Printf("After round-trip:\n%s\n\n", &restored) + + // Compare evaluations + fmt.Println("Comparing evaluated attributes:") + attrs := []string{"a", "b", "c", "d"} + for _, attr := range attrs { + orig := original.EvaluateAttr(attr) + rest := restored.EvaluateAttr(attr) + fmt.Printf(" %s: original=%v, restored=%v, match=%v\n", + attr, orig, rest, orig.String() == rest.String()) + } + + fmt.Println("\n=== Demo Complete ===") +} diff --git a/examples/range_demo/main.go b/examples/range_demo/main.go index 0902945..145aafb 100644 --- a/examples/range_demo/main.go +++ b/examples/range_demo/main.go @@ -33,16 +33,16 @@ func main() { } func example1() { - fmt.Println("Example 1: Simple iteration") + fmt.Println("Example 1: Simple iteration with generic API") input := `[JobId = 1; Owner = "alice"; Cpus = 2] [JobId = 2; Owner = "bob"; Cpus = 4] [JobId = 3; Owner = "charlie"; Cpus = 8]` for ad := range classad.All(strings.NewReader(input)) { - jobId, _ := ad.EvaluateAttrInt("JobId") - owner, _ := ad.EvaluateAttrString("Owner") - cpus, _ := ad.EvaluateAttrInt("Cpus") + jobId := classad.GetOr(ad, "JobId", 0) + owner := classad.GetOr(ad, "Owner", "unknown") + cpus := classad.GetOr(ad, "Cpus", 0) fmt.Printf(" Job %d: owner=%s, cpus=%d\n", jobId, owner, cpus) } @@ -50,15 +50,15 @@ func example1() { } func example2() { - fmt.Println("Example 2: Iteration with index") + fmt.Println("Example 2: Iteration with index and generic API") input := `[Name = "Machine1"; Available = true] [Name = "Machine2"; Available = false] [Name = "Machine3"; Available = true]` for i, ad := range classad.AllWithIndex(strings.NewReader(input)) { - name, _ := ad.EvaluateAttrString("Name") - available, _ := ad.EvaluateAttrBool("Available") + name := classad.GetOr(ad, "Name", "unknown") + available := classad.GetOr(ad, "Available", false) fmt.Printf(" [%d] %s: available=%v\n", i, name, available) } @@ -66,7 +66,7 @@ func example2() { } func example3() { - fmt.Println("Example 3: Iteration with error handling") + fmt.Println("Example 3: Iteration with error handling and generic API") // Valid input input := `[Status = "Running"] @@ -75,7 +75,7 @@ func example3() { var err error for ad := range classad.AllWithError(strings.NewReader(input), &err) { - status, _ := ad.EvaluateAttrString("Status") + status := classad.GetOr(ad, "Status", "unknown") fmt.Printf(" Status: %s\n", status) } @@ -103,7 +103,7 @@ func example3() { } func example4() { - fmt.Println("Example 4: Old-style ClassAds") + fmt.Println("Example 4: Old-style ClassAds with generic API") input := `MyType = "Machine" Name = "slot1@server1" @@ -114,8 +114,8 @@ Name = "slot2@server2" Cpus = 8` for ad := range classad.AllOld(strings.NewReader(input)) { - name, _ := ad.EvaluateAttrString("Name") - cpus, _ := ad.EvaluateAttrInt("Cpus") + name := classad.GetOr(ad, "Name", "unknown") + cpus := classad.GetOr(ad, "Cpus", 0) fmt.Printf(" %s: %d CPUs\n", name, cpus) } @@ -123,7 +123,7 @@ Cpus = 8` } func example5() { - fmt.Println("Example 5: Reading from file") + fmt.Println("Example 5: Reading from file with generic API") // Try to open the example file file, err := os.Open("../jobs-multiple.ad") @@ -136,8 +136,8 @@ func example5() { count := 0 for ad := range classad.All(file) { - jobId, _ := ad.EvaluateAttrInt("JobId") - owner, _ := ad.EvaluateAttrString("Owner") + jobId := classad.GetOr(ad, "JobId", 0) + owner := classad.GetOr(ad, "Owner", "unknown") fmt.Printf(" Job %d: %s\n", jobId, owner) count++ } diff --git a/examples/reader_demo/main.go b/examples/reader_demo/main.go index 4559023..4b7ac68 100644 --- a/examples/reader_demo/main.go +++ b/examples/reader_demo/main.go @@ -13,7 +13,7 @@ func main() { fmt.Println("=== ClassAd Reader Demo ===") fmt.Println() - // Example 1: Reading new-style ClassAds + // Example 1: Reading new-style ClassAds with generic API fmt.Println("Example 1: Reading new-style ClassAds from string") newStyleAds := ` [JobId = 1; Owner = "alice"; Cpus = 2; Memory = 2048] @@ -25,10 +25,10 @@ func main() { fmt.Println("Jobs:") for reader.Next() { ad := reader.ClassAd() - jobId, _ := ad.EvaluateAttrInt("JobId") - owner, _ := ad.EvaluateAttrString("Owner") - cpus, _ := ad.EvaluateAttrInt("Cpus") - memory, _ := ad.EvaluateAttrInt("Memory") + jobId := classad.GetOr(ad, "JobId", 0) + owner := classad.GetOr(ad, "Owner", "unknown") + cpus := classad.GetOr(ad, "Cpus", 0) + memory := classad.GetOr(ad, "Memory", 0) fmt.Printf(" Job %d: Owner=%s, Cpus=%d, Memory=%dMB\n", jobId, owner, cpus, memory) @@ -39,7 +39,7 @@ func main() { } fmt.Println() - // Example 2: Reading old-style ClassAds + // Example 2: Reading old-style ClassAds with generic API fmt.Println("Example 2: Reading old-style ClassAds from string") oldStyleAds := `MyType = "Machine" Name = "worker01.example.com" @@ -64,10 +64,10 @@ Arch = "ARM64" fmt.Println("Machines:") for oldReader.Next() { ad := oldReader.ClassAd() - name, _ := ad.EvaluateAttrString("Name") - cpus, _ := ad.EvaluateAttrInt("Cpus") - memory, _ := ad.EvaluateAttrInt("Memory") - arch, _ := ad.EvaluateAttrString("Arch") + name := classad.GetOr(ad, "Name", "unknown") + cpus := classad.GetOr(ad, "Cpus", 0) + memory := classad.GetOr(ad, "Memory", 0) + arch := classad.GetOr(ad, "Arch", "unknown") fmt.Printf(" %s: %d CPUs, %dMB RAM, %s\n", name, cpus, memory, arch) @@ -78,7 +78,7 @@ Arch = "ARM64" } fmt.Println() - // Example 3: Processing with filtering + // Example 3: Processing with filtering using generic API fmt.Println("Example 3: Filtering ClassAds (jobs requiring >= 4 CPUs)") filterAds := ` [JobId = 100; Cpus = 2; Priority = 10] @@ -93,14 +93,14 @@ Arch = "ARM64" count := 0 for filterReader.Next() { ad := filterReader.ClassAd() - cpus, ok := ad.EvaluateAttrInt("Cpus") + cpus, ok := classad.GetAs[int](ad, "Cpus") if !ok { continue } if cpus >= 4 { - jobId, _ := ad.EvaluateAttrInt("JobId") - priority, _ := ad.EvaluateAttrInt("Priority") + jobId := classad.GetOr(ad, "JobId", 0) + priority := classad.GetOr(ad, "Priority", 0) fmt.Printf(" Job %d: %d CPUs (priority=%d)\n", jobId, cpus, priority) count++ } @@ -126,11 +126,11 @@ Arch = "ARM64" ad := fileReader.ClassAd() fmt.Printf(" Successfully read ClassAd from %s\n", filename) - // Try to get some attributes - if owner, ok := ad.EvaluateAttrString("Owner"); ok { + // Try to get some attributes with generic API + if owner, ok := classad.GetAs[string](ad, "Owner"); ok { fmt.Printf(" Owner: %s\n", owner) } - if jobId, ok := ad.EvaluateAttrInt("JobId"); ok { + if jobId, ok := classad.GetAs[int](ad, "JobId"); ok { fmt.Printf(" JobId: %d\n", jobId) } } diff --git a/examples/struct_demo/README.md b/examples/struct_demo/README.md new file mode 100644 index 0000000..f7d79f2 --- /dev/null +++ b/examples/struct_demo/README.md @@ -0,0 +1,151 @@ +# Struct Marshaling Demo + +This example demonstrates how to marshal and unmarshal Go structs to/from ClassAd format using struct tags. + +## Features Demonstrated + +1. **Simple Struct Marshaling** - Convert basic Go structs to ClassAd format +2. **Struct Tags** - Use `classad` and `json` tags to control field names +3. **Tag Options** - `omitempty` and `-` for skipping fields +4. **Nested Structs** - Handle complex hierarchical data structures +5. **Unmarshal** - Parse ClassAd strings into Go structs +6. **Round-trip** - Verify marshal→unmarshal preserves data +7. **Map Support** - Work with `map[string]interface{}` + +## API Overview + +### Marshal +```go +classadStr, err := classad.Marshal(v) +``` + +Converts a Go value to ClassAd format string. Works with: +- Structs +- Maps +- Slices/arrays +- Basic types (int, float, string, bool) + +### Unmarshal +```go +err := classad.Unmarshal(classadStr, &v) +``` + +Parses a ClassAd string into a Go value. The target must be a pointer. + +## Struct Tags + +### classad Tag +Primary tag for controlling ClassAd marshaling: + +```go +type Job struct { + ID int `classad:"JobId"` // Use custom name + Name string `classad:"Name,omitempty"` // Omit if zero value + Temp string `classad:"-"` // Skip this field +} +``` + +### json Tag Fallback +If no `classad` tag is present, falls back to `json` tag: + +```go +type Config struct { + Timeout int `json:"timeout"` // Uses "timeout" in ClassAd + Port int // Uses "Port" (field name) +} +``` + +### Tag Options + +- **Field name**: `classad:"custom_name"` +- **Skip field**: `classad:"-"` +- **Omit if empty**: `classad:"name,omitempty"` + +## Examples + +### Basic Struct +```go +type Job struct { + ID int + Name string +} + +job := Job{ID: 123, Name: "test"} +str, _ := classad.Marshal(job) +// [ID = 123; Name = "test"] +``` + +### With Tags +```go +type Job struct { + JobID int `classad:"ClusterId"` + Owner string `classad:"Owner"` +} + +job := Job{JobID: 100, Owner: "alice"} +str, _ := classad.Marshal(job) +// [ClusterId = 100; Owner = "alice"] +``` + +### Nested Structs +```go +type Config struct { + Timeout int +} + +type Job struct { + ID int + Config Config +} + +job := Job{ID: 123, Config: Config{Timeout: 30}} +str, _ := classad.Marshal(job) +// [ID = 123; Config = [Timeout = 30]] +``` + +### Unmarshal +```go +classadStr := `[JobId = 456; Owner = "bob"]` + +type Job struct { + JobID int `classad:"JobId"` + Owner string `classad:"Owner"` +} + +var job Job +classad.Unmarshal(classadStr, &job) +// job.JobID = 456, job.Owner = "bob" +``` + +## Running the Demo + +```bash +go run main.go +``` + +## Use Cases + +This struct marshaling is useful for: +- **Configuration files** - Store configs in ClassAd format +- **HTCondor integration** - Work with HTCondor job descriptions +- **Data serialization** - Alternative to JSON for certain workflows +- **Type-safe APIs** - Use Go structs with ClassAd backend +- **Testing** - Easy conversion between Go types and ClassAds + +## Comparison with JSON + +| Feature | classad.Marshal | json.Marshal | +|---------|----------------|--------------| +| Struct tags | `classad` or `json` | `json` | +| Output format | ClassAd | JSON | +| Expressions | Supported | Not applicable | +| Nested data | ✓ | ✓ | +| omitempty | ✓ | ✓ | +| Skip fields | ✓ | ✓ | + +## Notes + +- Unexported fields are automatically skipped +- Zero values can be omitted with `omitempty` +- Unknown fields in ClassAd are ignored during unmarshal +- Type conversions are performed when possible (int ↔ float) diff --git a/examples/struct_demo/main.go b/examples/struct_demo/main.go new file mode 100644 index 0000000..c9015fc --- /dev/null +++ b/examples/struct_demo/main.go @@ -0,0 +1,240 @@ +package main + +import ( + "fmt" + "log" + + "github.com/PelicanPlatform/classad/classad" +) + +func main() { + fmt.Println("=== ClassAd Struct Marshaling Demo ===") + fmt.Println() + + // Example 1: Simple struct marshaling + fmt.Println("Example 1: Marshal a simple struct") + fmt.Println("-----------------------------------") + + type Job struct { + ID int + Name string + CPUs int + Memory int + Priority float64 + Active bool + } + + job := Job{ + ID: 12345, + Name: "data-processing-job", + CPUs: 8, + Memory: 16384, + Priority: 10.5, + Active: true, + } + + classadStr, err := classad.Marshal(job) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("Go struct:\n%+v\n\n", job) + fmt.Printf("ClassAd format:\n%s\n\n", classadStr) + + // Example 2: Using struct tags + fmt.Println("Example 2: Using classad and json struct tags") + fmt.Println("----------------------------------------------") + + type HTCondorJob struct { + JobID int `classad:"ClusterId"` + ProcID int `classad:"ProcId"` + Owner string `classad:"Owner"` + RequestCPUs int `classad:"RequestCpus"` + RequestMemory int `json:"request_memory"` // Falls back to json tag + Requirements []string `classad:"Requirements"` + Rank int // Uses field name as-is + } + + htcJob := HTCondorJob{ + JobID: 100, + ProcID: 0, + Owner: "alice", + RequestCPUs: 4, + RequestMemory: 8192, + Requirements: []string{"OpSysAndVer == \"RedHat8\"", "Arch == \"X86_64\""}, + Rank: 5, + } + + classadStr, err = classad.Marshal(htcJob) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("Go struct:\n%+v\n\n", htcJob) + fmt.Printf("ClassAd format:\n%s\n\n", classadStr) + + // Example 3: Omitempty and skip fields + fmt.Println("Example 3: Using omitempty and skip options") + fmt.Println("-------------------------------------------") + + type JobWithOptions struct { + ID int + Name string + Optional string `classad:"Optional,omitempty"` + Tags []string `classad:"Tags,omitempty"` + Internal string `classad:"-"` // Skip this field + } + + jobOpts := JobWithOptions{ + ID: 123, + Name: "test-job", + Internal: "secret-data", // This won't be marshaled + // Optional and Tags are zero values, will be omitted + } + + classadStr, err = classad.Marshal(jobOpts) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("Go struct:\n%+v\n\n", jobOpts) + fmt.Printf("ClassAd format (Optional, Tags, and Internal omitted):\n%s\n\n", classadStr) + + // Example 4: Nested structs + fmt.Println("Example 4: Nested structs") + fmt.Println("-------------------------") + + type Resources struct { + CPUs int + Memory int + Disk int + } + + type Config struct { + Timeout int + Retries int + Server string + } + + type ComplexJob struct { + ID int + Resources Resources + Config Config + } + + complexJob := ComplexJob{ + ID: 999, + Resources: Resources{ + CPUs: 16, + Memory: 32768, + Disk: 100000, + }, + Config: Config{ + Timeout: 300, + Retries: 3, + Server: "scheduler.example.com", + }, + } + + classadStr, err = classad.Marshal(complexJob) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("Go struct:\n%+v\n\n", complexJob) + fmt.Printf("ClassAd format:\n%s\n\n", classadStr) + + // Example 5: Unmarshal ClassAd into struct + fmt.Println("Example 5: Unmarshal ClassAd into struct") + fmt.Println("-----------------------------------------") + + classadInput := `[ + ClusterId = 200; + ProcId = 0; + Owner = "bob"; + RequestCpus = 8; + request_memory = 16384; + Rank = 10 + ]` + + var unmarshaledJob HTCondorJob + err = classad.Unmarshal(classadInput, &unmarshaledJob) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("Input ClassAd:\n%s\n\n", classadInput) + fmt.Printf("Unmarshaled Go struct:\n%+v\n\n", unmarshaledJob) + + // Example 6: Round-trip conversion + fmt.Println("Example 6: Round-trip conversion") + fmt.Println("----------------------------------") + + type RoundTripJob struct { + ID int `classad:"JobId"` + Name string `classad:"Name"` + CPUs int `classad:"CPUs"` + Tags []string `classad:"Tags"` + Priority float64 `classad:"Priority"` + } + + original := RoundTripJob{ + ID: 555, + Name: "round-trip-test", + CPUs: 4, + Tags: []string{"test", "demo"}, + Priority: 7.5, + } + + // Marshal to ClassAd + classadStr, err = classad.Marshal(original) + if err != nil { + log.Fatal(err) + } + + // Unmarshal back to struct + var restored RoundTripJob + err = classad.Unmarshal(classadStr, &restored) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("Original: %+v\n", original) + fmt.Printf("ClassAd: %s\n", classadStr) + fmt.Printf("Restored: %+v\n", restored) + fmt.Printf("Match: %v\n", original.ID == restored.ID && original.Name == restored.Name) + + // Example 7: Working with maps + fmt.Println() + fmt.Println("Example 7: Marshal/Unmarshal maps") + fmt.Println("----------------------------------") + + jobMap := map[string]interface{}{ + "JobId": 777, + "Owner": "charlie", + "CPUs": 12, + "Memory": 24576, + "Active": true, + "Tags": []string{"prod", "critical"}, + } + + classadStr, err = classad.Marshal(jobMap) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("Go map: %v\n\n", jobMap) + fmt.Printf("ClassAd format:\n%s\n\n", classadStr) + + // Unmarshal back into map + var restoredMap map[string]interface{} + err = classad.Unmarshal(classadStr, &restoredMap) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("Restored map: %v\n", restoredMap) + + fmt.Println() + fmt.Println("=== Demo Complete ===") +}