From 6933004b8720500918e5adb5871d33e36d543fb1 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Sun, 11 Oct 2026 14:22:56 +0200 Subject: [PATCH] feat(sieve): add sieve module with Map --- README.md | 1 + modules/sieve/README.md | 56 +++++++++++++++++++++++++++++++++++ modules/sieve/example_test.go | 24 +++++++++++++++ modules/sieve/sieve.go | 22 ++++++++++++++ 4 files changed, 103 insertions(+) create mode 100644 modules/sieve/README.md create mode 100644 modules/sieve/example_test.go create mode 100644 modules/sieve/sieve.go diff --git a/README.md b/README.md index cc63ba1..b00b5bb 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,7 @@ The `migrate`, `migrate:status`, `migrate:rollback`, `serve` and admin commands | [party](modules/party/README.md) | Compiled plugin registry that orders plugins by their dependencies and runs their Register and Boot lifecycle. | The guest list and the order people arrive in; it registers plugins and boots them in dependency order. | | [phrasebook](modules/phrasebook/README.md) | Namespaced translation catalogs loaded from plugin YAML, with locale fallback, placeholder interpolation and CLDR pluralization. | What you pack to speak the local language on holiday; it holds the translations. | | [postcard](modules/postcard/README.md) | Transactional mail from plugin-owned Markdown templates and layouts, delivered through a memory, log or SMTP driver. | What you send home in summer; it delivers the application's mail. | +| [sieve](modules/sieve/README.md) | Generic, dependency-free collection helpers that the standard library slices and maps packages lack: mapping, filtering, reducing, keying, grouping, deduplicating, partitioning and finding, plus a small set type. | A sand sieve: you shake a collection through it and keep what you need; map, filter and reduce for slices and maps. | | [sunrise](modules/sunrise/README.md) | Bootstrap script that installs the summer CLI and creates a new SummerCMS application, or updates an existing one, in one command, embedded so it is versioned with the framework. | The start of a summer day; it starts a new application, or updates one, in one command. | | [sunscreen](modules/sunscreen/README.md) | Credential-redacting slog handler that keeps API keys, tokens and passwords out of application logs. | Protects you from exposure; it keeps secrets out of the logs. | | [surf](modules/surf/README.md) | HTTP routing for SummerCMS: collects plugin routes and named middleware into a `net/http` ServeMux with constraints, rate limiting, body limits, CORS and panic recovery, and provides the `serve` and `route:list` commands. | Catching the incoming waves; it routes incoming HTTP requests to their handlers. | diff --git a/modules/sieve/README.md b/modules/sieve/README.md new file mode 100644 index 0000000..3f9ebe1 --- /dev/null +++ b/modules/sieve/README.md @@ -0,0 +1,56 @@ +# sieve + +Generic, dependency-free collection helpers that the standard library slices and maps packages lack: mapping, filtering, reducing, keying, grouping, deduplicating, partitioning and finding, plus a small set type. + +`import "git.golem15.com/golem15/summercms/modules/sieve"` + +**Name:** A sand sieve: you shake a collection through it and keep what you need; map, filter and reduce for slices and maps. + +## Overview + +`sieve` gives plain Go slices the collection operations that WinterCMS code reaches for on Laravel collections. Every function is eager and never modifies its input, results never share a backing array with the input, callbacks run once per element in input order, and nil or empty input yields a non-nil empty result, so `encoding/json` writes `[]` rather than `null`. + +## Role in the framework + +A dependency-free leaf with no imports, so any module, plugin or application can use it without import cycles. + +## Features + +- `sieve.Map` applies a function to every element and returns the results in order; with a field accessor it is the pluck operation. + +## Usage + +```go +package blog + +import "git.golem15.com/golem15/summercms/modules/sieve" + +type Post struct { + ID int + Title string +} + +func titles(posts []Post) []string { + return sieve.Map(posts, func(p Post) string { return p.Title }) +} +``` + +## API reference + +| Identifier | Description | +|------------|-------------| +| `sieve.Map` | Returns f applied to each element, in input order. Nil input gives a non-nil empty slice. | + +## Dependencies + +- SummerCMS modules: none. +- Third-party: none. +- Standard library: none (the package has no imports). + +## Testing + +```sh +go test ./modules/sieve/... +``` + +The tests are in-memory and need no external services. diff --git a/modules/sieve/example_test.go b/modules/sieve/example_test.go new file mode 100644 index 0000000..2e1c740 --- /dev/null +++ b/modules/sieve/example_test.go @@ -0,0 +1,24 @@ +package sieve_test + +import ( + "fmt" + + "git.golem15.com/golem15/summercms/modules/sieve" +) + +type post struct { + ID int + Title string +} + +func ExampleMap() { + posts := []post{ + {ID: 1, Title: "Hello"}, + {ID: 2, Title: "Summer"}, + {ID: 3, Title: "Goodbye"}, + } + // Pluck the titles with a field accessor. + titles := sieve.Map(posts, func(p post) string { return p.Title }) + fmt.Println(titles) + // Output: [Hello Summer Goodbye] +} diff --git a/modules/sieve/sieve.go b/modules/sieve/sieve.go new file mode 100644 index 0000000..a6225bd --- /dev/null +++ b/modules/sieve/sieve.go @@ -0,0 +1,22 @@ +// Package sieve provides generic, dependency-free collection helpers that the +// standard library slices and maps packages lack. +// +// Every function is eager and never modifies its input. Results never share a +// backing array with the input, so changing or appending to a result leaves the +// caller's slice unchanged. Callbacks run once per element, in input order. +// Nil or empty input yields a non-nil empty slice or map, so encoding/json +// writes [] or {} rather than null. +package sieve + +// Map returns a new slice holding f applied to each element of s, in order. +// f is called once per element. A nil or empty s yields a non-nil empty slice. +// +// Map is also the pluck operation: pass an accessor func to extract one field +// from every element, for example the titles of a slice of posts. +func Map[S ~[]T, T, U any](s S, f func(T) U) []U { + out := make([]U, 0, len(s)) + for _, v := range s { + out = append(out, f(v)) + } + return out +}