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
46 changes: 46 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-go@v5
with:
go-version-file: go.mod

- name: Run tests
run: go test ./... -coverprofile=coverage.out -covermode=atomic

- name: Validate coverage threshold
shell: bash
run: |
THRESHOLD=90
EXIT_CODE=0

while read -r line; do
pkg=$(echo "$line" | awk '{print $2}')
pct=$(echo "$line" | sed -n 's/.*coverage: \([0-9.]*\)%.*/\1/p')

if [ -z "$pct" ]; then
continue
fi

pass=$(echo "$pct >= $THRESHOLD" | bc -l)

if [ "$pass" -eq 1 ]; then
echo "OK: $pkg ${pct}%"
else
echo "FAIL: $pkg ${pct}% (minimum: ${THRESHOLD}%)"
EXIT_CODE=1
fi
done < <(go test ./... -cover 2>&1)

exit $EXIT_CODE
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.DS_Store

.idea
bin/
!bin/.gitkeep

4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.PHONY: format

format:
docker-compose -f go-fmt.compose.yaml run --rm go-fmt format .
81 changes: 51 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,30 @@
# Collection

A Go port of Laravel's [Illuminate Collections](https://laravel.com/docs/collections) -- fluent, type-safe generic collections powered by Go generics.
[![Go Reference](https://pkg.go.dev/badge/github.com/gocanto/collection.svg)](https://pkg.go.dev/github.com/gocanto/collection)
[![Go Version](https://img.shields.io/badge/go-1.25-blue.svg)](https://golang.org/doc/devel/release.html#go1.25)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

A powerful Go port of [Laravel Collections](https://laravel.com/docs/collections) — fluent, type-safe, and powered by Go generics and `iter.Seq` lazy evaluation.

```bash
go get github.com/gocanto/collection
```

Requires **Go 1.25** or later.
---

## ⚡️ Why use this?

## Overview
Go's slices and maps are powerful but often lead to repetitive boilerplate loops for common operations. `collection` provides a **fluent, expressive API** to handle data transformations with ease:

This library is a port of Laravel's [Illuminate\Support\Collection](https://github.com/laravel/framework/tree/master/src/Illuminate/Collections) to Go. It preserves the same rich API surface -- `filter`, `map`, `reduce`, `flatMap`, `chunk`, `partition`, and many more -- while redesigning everything idiomatically for Go with generics, `iter.Seq` for lazy evaluation, and full type safety.
- **💎 Fluent Pipelines:** Chain `.Filter().Take().Each()` instead of nesting `for` loops.
- **🛡️ Type-Safe:** Built with Go generics—no `interface{}` casting or runtime type errors.
- **♻️ Immutable by Default:** Transformation methods return new collections, preserving your original data.
- **🐢 Lazy Evaluation:** Process massive datasets efficiently using `iter.Seq` sequences.
- **🧩 Modular:** Use the full fluent API or lightweight standalone utilities (`arr` and `kv`).

- **Readable data pipelines** -- chain `.Filter().Map().Take()` instead of nesting loops.
- **Type-safe generics** -- no `interface{}` casting; the compiler catches type errors at build time.
- **Immutable returns** -- methods return new collections, leaving the original untouched.
- **Lazy evaluation** -- process large or infinite datasets without allocating the entire result up front.
- **Standalone utilities** -- use individual functions from `arr` or `kv` without constructing a collection object.
---

## Quick Start
## 🚀 Quick Start

```go
package main
Expand All @@ -29,36 +35,51 @@ import (
)

func main() {
// 1. Create a collection from a slice
numbers := collection.Collect([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10})

result := numbers.
Filter(func(n int, _ int) bool { return n%2 == 0 }).
// 2. Chain operations fluently
even := numbers.
Filter(func(n int, _ int) bool { return n % 2 == 0 }).
Take(3)

fmt.Println(result.All()) // [2, 4, 6]
// 3. Transform types using top-level generics
// (Go doesn't allow methods to introduce new type parameters)
result := collection.Map(even, func(n int, _ int) string {
return fmt.Sprintf("Number: %d", n)
})

fmt.Println(result.All()) // ["Number: 2", "Number: 4", "Number: 6"]
}
```

## Packages
---

## 📦 Package Ecosystem

| Package | Purpose | Use when... |
|:---|:---|:---|
| **`collection`** | **The Core** | You want the full fluent `Collection[T]` experience for slices. |
| **`lazy`** | **Lazy Sequences** | You're handling large/infinite streams and want deferred execution. |
| **`collectible`** | **Key-Value Maps** | You need an ordered `collectible.Collection[K, V]` with a fluent API. |
| **`arr`** | **Slice Utils** | You need a quick one-off helper (e.g., `Sort`, `Flatten`) on a raw `[]T`. |
| **`kv`** | **Map Utils** | You need dot-notation access (`"user.profile.name"`) for `map[string]any`. |

---

| Package | Description |
|---------|-------------|
| `collection` | Core collection types: `Collection[T]`, `MapCollection[K,V]`, `LazyCollection[T]`, and helper functions. |
| `collection/arr` | Standalone generic slice utilities: `Flatten`, `Sort`, `Where`, `Map`, `Pluck`, `Partition`, etc. |
| `collection/kv` | Map utilities with dot-notation support: `Get`, `Set`, `Has`, `Dot`, `Undot`, `Only`, `Query`, etc. |
## 📖 Deep Dive

## Documentation
Explore our comprehensive documentation for each component:

Full API documentation is available in the [docs](docs/) directory:
- [**Architecture Overview**](docs/overview.md) — Design philosophy and core concepts.
- [**Collection API**](docs/collection.md) — Reference for the standard fluent collection.
- [**Collectible API**](docs/collectible.md) — Reference for key-value collection operations.
- [**Lazy API**](docs/lazy.md) — Reference for lazy sequences and `iter.Seq`.
- [**Array Utilities (`arr`)**](docs/arr.md) — Standalone functions for raw slices.
- [**Key-Value Utilities (`kv`)**](docs/kv.md) — Standalone functions for maps and dot-notation.

- [Overview](docs/overview.md) -- Architecture, design decisions, and usage guide.
- [Collection](docs/collection.md) -- `Collection[T]` API reference.
- [MapCollection](docs/map_collection.md) -- `MapCollection[K,V]` API reference.
- [LazyCollection](docs/lazy_collection.md) -- `LazyCollection[T]` API reference.
- [arr](docs/arr.md) -- Slice utilities API reference.
- [kv](docs/kv.md) -- Map utilities API reference.
- [Helpers](docs/helpers.md) -- Root-package helper functions.
---

## License
## ⚖️ License

MIT -- see [LICENSE](LICENSE) for details.
Released under the [MIT License](LICENSE).
146 changes: 146 additions & 0 deletions arr/access.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package arr

import "fmt"

// First returns the first element that matches the optional callback.
// If no callback is provided, the first element of the slice is returned.
// The second return value reports whether a match was found.
func First[T any](items []T, callbacks ...func(T, int) bool) (T, bool) {
if len(callbacks) == 0 || callbacks[0] == nil {
if len(items) > 0 {
return items[0], true
}

var zero T

return zero, false
}

callback := callbacks[0]

for i, item := range items {
if callback(item, i) {
return item, true
}
}

var zero T

return zero, false
}

// Last returns the last element that matches the optional callback.
// If no callback is provided, the last element of the slice is returned.
// The second return value reports whether a match was found.
func Last[T any](items []T, callbacks ...func(T, int) bool) (T, bool) {
if len(callbacks) == 0 || callbacks[0] == nil {
if len(items) > 0 {
return items[len(items)-1], true
}

var zero T

return zero, false
}

callback := callbacks[0]

for i := len(items) - 1; i >= 0; i-- {
if callback(items[i], i) {
return items[i], true
}
}

var zero T

return zero, false
}

// Get returns the element at the given index.
// If the index is out of bounds, the first default value is returned,
// or the zero value of T.
func Get[T any](items []T, index int, defaults ...T) T {
if index >= 0 && index < len(items) {
return items[index]
}

if len(defaults) > 0 {
return defaults[0]
}

var zero T

return zero
}

// Sole returns the only element matching the optional callback.
// It returns an error if zero or more than one element matches.
func Sole[T any](items []T, callbacks ...func(T, int) bool) (T, error) {
if len(callbacks) == 0 || callbacks[0] == nil {
if len(items) == 1 {
return items[0], nil
}

if len(items) == 0 {
var zero T

return zero, fmt.Errorf("no items found")
}

var zero T

return zero, fmt.Errorf("multiple items found: %d items", len(items))
}

callback := callbacks[0]

var result T
found := 0

for i, item := range items {
if callback(item, i) {
result = item
found++

if found > 1 {
var zero T

return zero, fmt.Errorf("multiple items found: %d+ items", found)
}
}
}

if found == 0 {
var zero T

return zero, fmt.Errorf("no items found")
}

return result, nil
}

// Take returns up to limit elements from the slice.
// A positive limit takes from the front; a negative limit takes from the end.
func Take[T any](items []T, limit int) []T {
if limit < 0 {
start := len(items) + limit

if start < 0 {
start = 0
}

result := make([]T, len(items)-start)
copy(result, items[start:])

return result
}

if limit > len(items) {
limit = len(items)
}

result := make([]T, limit)
copy(result, items[:limit])

return result
}
Loading
Loading