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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@
*.out

# Intellij
.idea/
.idea/
.vscode/
cliff.toml
87 changes: 87 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

flanksource/commons is a Go utility library providing common functionality for the Flanksource ecosystem. It's a collection of reusable modules organized by concern: core utilities, infrastructure helpers, development tools, data processing, and system integration.

## Key Commands

```bash
# Testing
make test # Run all tests with verbose output
go test ./... # Run all tests (standard Go)
go test ./logger/... # Run tests for a specific package

# Linting and Code Quality
make lint # Run golangci-lint with project-specific rules

# Dependency Management
make tidy # Clean up go.mod dependencies
go mod tidy # Standard Go dependency cleanup

# Build
go build # Build the dependency installer binary
go run main.go <dep> # Download and install a dependency (e.g., go run main.go jq)
```

## Architecture and Code Organization

### Module Structure
The codebase follows a flat package structure where each directory represents a focused utility module:

- **HTTP Client (`http/`)**: Enhanced HTTP client with authentication middleware pattern. Supports Digest, NTLM, OAuth2, and custom auth. Uses functional options for configuration.
- **Logger (`logger/`)**: Dual logging system supporting both logrus and slog backends. Global logger instance with interface-based design for flexibility.
- **Collections (`collections/`)**: Generic utilities for maps, slices, sets, and priority queues. Heavy use of Go generics.
- **Dependency Management (`deps/`)**: Template-based system for downloading and installing external binaries. Supports cross-platform paths and verification.
- **Properties (`properties/`)**: Global configuration management with environment variable support and structured properties.

### Key Patterns

1. **Global State Pattern**: Logger and Properties packages use global instances initialized via `Init()` functions. Always check if initialization is needed before using these packages.

2. **Middleware Pattern**: HTTP client uses layered middleware for auth, tracing, and logging:
```go
client := http.NewClient().
WithLogger(logger).
WithAuth(username, password).
WithTrace(true)
```

3. **Interface-First Design**: Most packages expose interfaces (e.g., `Logger` interface) allowing for mock implementations in tests.

4. **Builder Pattern**: Used extensively in HTTP client and dependency configurations for fluent API design.

### Testing Approach

- Uses testify for assertions in most tests
- Some modules (like `diff/`) use Ginkgo/Gomega for BDD-style tests
- Test files are colocated with source files following `*_test.go` convention
- No global test setup/teardown - each test is self-contained

### Important Development Notes

1. **Go Version**: Requires Go 1.23.0+ (uses newer generic features)
2. **Linting**: The project has custom golangci-lint rules that disable certain checks for HTTP-related naming
3. **Imports**: When adding new functionality, prefer using existing utilities from within the project (e.g., use `collections` package for slice operations)
4. **Error Handling**: The codebase uses explicit error returns rather than panics - maintain this pattern
5. **Logging**: Use the `logger` package for all logging needs, avoid fmt.Print statements

### Common Tasks

To add a new utility module:
1. Create a new directory at the root level
2. Follow the existing pattern of having a main file with the package name
3. Include comprehensive tests in `*_test.go` files
4. Update imports in other packages if the utility is widely useful

To modify the HTTP client:
1. Changes go in `http/client.go` or `http/middleware.go`
2. Authentication providers are in `http/auth_*.go` files
3. Always maintain backward compatibility with the builder pattern

To work with the dependency installer:
1. Dependencies are defined in `deps/binary.go`
2. Binary definitions include version, download URLs, and install paths
3. Use templates for cross-platform path handling
35 changes: 34 additions & 1 deletion collections/collections.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,28 @@
// Package collections provides generic utilities for working with
// collections like slices, maps, sets, and priority queues.
//
// The package leverages Go generics to provide type-safe operations
// without runtime overhead. It includes utilities for common operations
// like filtering, mapping, grouping, and set operations.
//
// Slice Operations:
//
// numbers := []int{1, 2, 3, 4, 5}
// evens := collections.Filter(numbers, func(n int) bool { return n%2 == 0 })
// squares := collections.Map(numbers, func(n int) int { return n * n })
//
// Map Operations:
//
// data := map[string]int{"a": 1, "b": 2}
// keys := collections.Keys(data)
// values := collections.Values(data)
//
// Set Operations:
//
// set1 := collections.NewSet(1, 2, 3)
// set2 := collections.NewSet(2, 3, 4)
// union := set1.Union(set2) // {1, 2, 3, 4}
// intersect := set1.Intersect(set2) // {2, 3}
package collections

import (
Expand All @@ -19,7 +44,15 @@ func takeSliceArg(arg interface{}) (out []interface{}, ok bool) {
return out, true
}

// ToString takes an object and tries to convert it to a string
// ToString converts various types to their string representation.
// Handles slices by joining elements with commas, fmt.Stringer types,
// strings, booleans, and falls back to fmt.Sprintf for other types.
//
// Example:
//
// collections.ToString([]int{1, 2, 3}) // "1, 2, 3"
// collections.ToString(true) // "true"
// collections.ToString(nil) // ""
func ToString(i interface{}) string {
if slice, ok := takeSliceArg(i); ok {
s := ""
Expand Down
88 changes: 74 additions & 14 deletions collections/maps.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@ import (
"strings"
)

// ToGenericMap converts a map[string]string to a map[string]interface{}
// ToGenericMap converts a map[string]string to a map[string]interface{}.
// Useful when you need to pass string maps to functions expecting generic maps.
//
// Example:
//
// strMap := map[string]string{"name": "John", "age": "30"}
// generic := collections.ToGenericMap(strMap)
func ToGenericMap(m map[string]string) map[string]interface{} {
var out = map[string]interface{}{}
for k, v := range m {
Expand All @@ -17,7 +23,14 @@ func ToGenericMap(m map[string]string) map[string]interface{} {
return out
}

// ToStringMap converts a map[string]interface{} to a map[string]string by using each objects String() fn
// ToStringMap converts a map[string]interface{} to a map[string]string.
// Each value is converted using fmt.Sprintf("%v", value).
//
// Example:
//
// data := map[string]interface{}{"count": 42, "active": true}
// strings := collections.ToStringMap(data)
// // Result: {"count": "42", "active": "true"}
func ToStringMap(m map[string]interface{}) map[string]string {
var out = make(map[string]string)
for k, v := range m {
Expand All @@ -26,7 +39,17 @@ func ToStringMap(m map[string]interface{}) map[string]string {
return out
}

// ToBase64Map converts a map[string]interface{} to a map[string]string by converting []byte to base64
// ToBase64Map converts a map[string]interface{} to a map[string]string.
// []byte values are encoded as base64, other types use fmt.Sprintf.
//
// Example:
//
// data := map[string]interface{}{
// "text": "hello",
// "binary": []byte{0x48, 0x65, 0x6c, 0x6c, 0x6f},
// }
// encoded := collections.ToBase64Map(data)
// // Result: {"text": "hello", "binary": "SGVsbG8="}
func ToBase64Map(m map[string]interface{}) map[string]string {
var out = make(map[string]string)
for k, v := range m {
Expand All @@ -40,7 +63,8 @@ func ToBase64Map(m map[string]interface{}) map[string]string {
return out
}

// ToByteMap converts a map[string]interface{} to a map[string]string by converting []byte to base64
// ToByteMap converts a map[string]interface{} to a map[string][]byte.
// String values are converted to []byte, []byte values are kept as-is.
func ToByteMap(m map[string]interface{}) map[string][]byte {
var out = make(map[string][]byte)
for k, v := range m {
Expand All @@ -54,8 +78,15 @@ func ToByteMap(m map[string]interface{}) map[string][]byte {
return out
}

// MergeMap will merge map b into a.
// On key collision, map b takes precedence.
// MergeMap merges two maps, with values from b taking precedence.
// Returns the merged map (modifies map a in place).
//
// Example:
//
// defaults := map[string]int{"timeout": 30, "retries": 3}
// overrides := map[string]int{"timeout": 60}
// config := collections.MergeMap(defaults, overrides)
// // Result: {"timeout": 60, "retries": 3}
func MergeMap[T1 comparable, T2 any](a, b map[T1]T2) map[T1]T2 {
if a == nil {
a = make(map[T1]T2)
Expand All @@ -72,10 +103,14 @@ func MergeMap[T1 comparable, T2 any](a, b map[T1]T2) map[T1]T2 {
return a
}

// KeyValueSliceToMap takes in a list of strings in a=b format
// and returns a map from it.
// KeyValueSliceToMap converts a slice of "key=value" strings to a map.
// Strings without '=' are treated as keys with empty values.
//
// Example:
//
// Any string that's not in a=b format will be ignored.
// args := []string{"env=prod", "debug=true", "verbose"}
// config := collections.KeyValueSliceToMap(args)
// // Result: {"env": "prod", "debug": "true", "verbose": ""}
func KeyValueSliceToMap(in []string) map[string]string {
sanitized := make(map[string]string, len(in))
for _, item := range in {
Expand All @@ -89,15 +124,27 @@ func KeyValueSliceToMap(in []string) map[string]string {
return sanitized
}

// SelectorToMap returns a map from a selector string.
// i.e. "a=b,c=d" -> {"a": "b", "c": "d"}
// SelectorToMap parses a comma-separated selector string into a map.
// Commonly used for Kubernetes-style label selectors.
//
// Example:
//
// selector := "app=nginx,env=prod,tier=frontend"
// labels := collections.SelectorToMap(selector)
// // Result: {"app": "nginx", "env": "prod", "tier": "frontend"}
func SelectorToMap(selector string) (matchLabels map[string]string) {
labels := strings.Split(selector, ",")
return KeyValueSliceToMap(labels)
}

// SortedMap takes a map and returns a sorted key value string
// i.e. {"a": "b", "c": "d"} -> "a=b,c=d"
// SortedMap converts a map to a sorted key=value string representation.
// Keys are sorted alphabetically for consistent output.
//
// Example:
//
// labels := map[string]string{"tier": "web", "app": "nginx"}
// result := collections.SortedMap(labels)
// // Result: "app=nginx,tier=web"
func SortedMap(m map[string]string) string {
keys := MapKeys(m)
sort.Slice(keys, func(i, j int) bool { return keys[i] < keys[j] })
Expand All @@ -110,7 +157,13 @@ func SortedMap(m map[string]string) string {
return strings.Join(items, ",")
}

// MapToIni takes a map and converts it into an INI formatted string
// MapToIni converts a map to INI format with one key=value pair per line.
//
// Example:
//
// config := map[string]string{"host": "localhost", "port": "8080"}
// ini := collections.MapToIni(config)
// // Result: "host=localhost\nport=8080\n"
func MapToIni(Map map[string]string) string {
str := ""
for k, v := range Map {
Expand All @@ -137,6 +190,13 @@ func IniToMap(path string) map[string]string {
return result
}

// MapKeys returns a slice containing all keys from the map.
// The order of keys is non-deterministic.
//
// Example:
//
// users := map[int]string{1: "Alice", 2: "Bob", 3: "Charlie"}
// ids := collections.MapKeys(users) // [1, 2, 3] (order may vary)
func MapKeys[K comparable, T any](m map[K]T) []K {
keys := make([]K, 0, len(m))
for k := range m {
Expand Down
37 changes: 26 additions & 11 deletions collections/priorityqueue.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,6 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package priorityqueue implements a priority queue backed by binary queue.
//
// A thread-safe priority queue based on a priority queue.
// The elements of the priority queue are ordered by a comparator provided at queue construction time.
//
// The heap of this queue is the least/smallest element with respect to the specified ordering.
// If multiple elements are tied for least value, the heap is one of those elements arbitrarily.
//
// Structure is thread safe.
//
// References: https://en.wikipedia.org/wiki/Priority_queue
package collections

import (
Expand Down Expand Up @@ -182,6 +171,20 @@ type queueItem[T comparable] struct {
var _ queues.Queue[int] = (*Queue[int])(nil)

// Queue holds elements in an array-list
// Queue is a thread-safe priority queue implementation.
// Elements are ordered by a comparator function and can optionally
// be deduplicated using an equality function.
//
// Example:
//
// queue, _ := collections.NewQueue(collections.QueueOpts[int]{
// Comparator: func(a, b int) int { return a - b }, // Min heap
// Dedupe: true,
// Equals: func(a, b int) bool { return a == b },
// })
// queue.Enqueue(5)
// queue.Enqueue(1)
// item, _ := queue.Dequeue() // Returns 1
type Queue[T comparable] struct {
heap *binaryheap.Heap[queueItem[T]]
Comparator utils.Comparator[T]
Expand All @@ -191,13 +194,25 @@ type Queue[T comparable] struct {
Dedupe bool
}

// QueueOpts configures a priority queue.
type QueueOpts[T comparable] struct {
Comparator utils.Comparator[T]
Dedupe bool
Equals func(a, b T) bool
Metrics MetricsOpts[T]
}

// NewQueue creates a new priority queue with the specified options.
// Returns an error if Dedupe is true but no Equals function is provided.
//
// Example:
//
// // Create a min-heap priority queue for tasks
// queue, err := collections.NewQueue(collections.QueueOpts[Task]{
// Comparator: func(a, b Task) int {
// return a.Priority - b.Priority // Lower priority first
// },
// })
func NewQueue[T comparable](opts QueueOpts[T]) (*Queue[T], error) {
if opts.Dedupe && opts.Equals == nil {
return nil, errors.New("dedupe requires Equals function")
Expand Down
Loading
Loading