Files
summercms/modules/sieve/README.md
Jakub Zych a0bf8ece05 feat(sieve): add the remaining collection helpers and docs mentions
- Add TryMap, Filter, Reduce, KeyBy, GroupBy, Uniq, UniqBy, Partition
  and Find, plus the Set type built with SetOf (Has, Add, Slice).
- Add unit tests at 100% statement coverage and a runnable example for
  every function and Set method.
- Complete the module README: contracts, use cases, Pluck rationale,
  standard-library pointers and the full API reference.
- docs/architecture/introduction.md: add a Utilities row for sieve.
- docs/setup/coming-from-wintercms.md: map Laravel collection methods
  to sieve and narrow the not-provided row to the fluent collection
  object.
2026-10-11 14:27:13 +02:00

7.7 KiB

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, the objects that Eloquent queries return (map, filter, reduce, keyBy, groupBy, unique, partition, first). It is a set of generic functions rather than a fluent collection object, so type inference works from the slice argument and no wrapper type leaks into your models.

Four contracts hold for every function:

  • It is eager and never modifies its input.
  • Its result never shares 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. sieve.TryMap and sieve.Find stop early.
  • Nil or empty input yields a non-nil empty slice or map, so encoding/json writes [] or {} 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. It complements the standard slices and maps packages rather than wrapping them: nothing here duplicates a function those packages already provide.

Use cases:

  • Shape API responses from models, for example turning a slice of records into a slice of response structs.
  • Key records by ID to join two result sets without a nested loop.
  • Group records by a field, such as posts by author.
  • Deduplicate IDs before an IN query.
  • Check set membership, such as whether an ID is in an allow list.

Features

  • sieve.Map applies a function to every element; with a field accessor it is the pluck operation.
  • sieve.TryMap maps with a function that can fail and stops at the first error, returning it unchanged.
  • sieve.Filter keeps the elements that match a predicate, and sieve.Partition splits a slice into the matches and the rest.
  • sieve.Reduce folds a slice from left to right into an accumulator of any type.
  • sieve.KeyBy builds a map from a key to an element (later items win), and sieve.GroupBy builds a map from a key to the elements with that key, in input order.
  • sieve.Uniq removes duplicates of comparable values, and sieve.UniqBy removes elements whose key was already seen; both keep the first occurrence.
  • sieve.Find returns the first element that matches a predicate, with a found flag.
  • sieve.Set, built with sieve.SetOf, is a plain map-backed set with sieve.Set.Has, sieve.Set.Add and sieve.Set.Slice.
  • Functions that keep the element type (sieve.Filter, sieve.Uniq, sieve.UniqBy, sieve.Partition and the groups of sieve.GroupBy) return the caller's named slice type.

Usage

package blog

import (
	"fmt"
	"strconv"

	"git.golem15.com/golem15/summercms/modules/sieve"
)

type Post struct {
	ID        int
	Author    string
	Title     string
	Published bool
}

type PostResponse struct {
	ID    int    `json:"id"`
	Title string `json:"title"`
}

func shape(posts []Post, rawIDs []string) error {
	// Keep the published posts and turn them into response structs.
	published := sieve.Filter(posts, func(p Post) bool { return p.Published })
	body := sieve.Map(published, func(p Post) PostResponse {
		return PostResponse{ID: p.ID, Title: p.Title}
	})

	// Key by ID for lookups, group by author, keep one post per author.
	byID := sieve.KeyBy(posts, func(p Post) int { return p.ID })
	byAuthor := sieve.GroupBy(posts, func(p Post) string { return p.Author })
	firstPerAuthor := sieve.UniqBy(posts, func(p Post) string { return p.Author })

	// Split drafts from published posts, and find the first draft.
	live, drafts := sieve.Partition(posts, func(p Post) bool { return p.Published })
	firstDraft, found := sieve.Find(posts, func(p Post) bool { return !p.Published })

	// Parse IDs from a query string, stopping at the first bad one.
	ids, err := sieve.TryMap(rawIDs, strconv.Atoi)
	if err != nil {
		return fmt.Errorf("blog: bad post id: %w", err)
	}
	wanted := sieve.SetOf(ids...)

	fmt.Println(len(body), len(byID), len(byAuthor), len(firstPerAuthor),
		len(live), len(drafts), firstDraft.Title, found, wanted.Has(1))
	return nil
}

There is no Pluck. Laravel's pluck('title') takes a field name as a string, which in Go would need reflection and would give up compile-time type checking. The typed equivalent is sieve.Map(posts, func(p Post) string { return p.Title }).

What the standard library already covers:

  • slices.Chunk splits a slice into chunks lazily; use slices.Collect(slices.Chunk(s, n)) for an eager [][]T.
  • slices.Contains and slices.IndexFunc test for and locate elements.
  • slices.Sort and slices.SortFunc sort in place.
  • slices.Min and slices.Max return the smallest and largest element.
  • slices.Compact removes consecutive duplicates only, in place.
  • slices.DeleteFunc filters in place.
  • maps.Keys and maps.Values iterate a map; wrap them in slices.Sorted for ordered output.

API reference

Identifier Description
sieve.Map Returns f applied to each element, in input order. Nil input gives a non-nil empty slice.
sieve.TryMap Like Map with a fallible f. Stops at the first error and returns nil with that error unchanged. Nil input gives a non-nil empty slice and a nil error.
sieve.Filter Returns the elements for which keep is true, in input order, as the caller's slice type. Nil input or no matches gives a non-nil empty slice.
sieve.Reduce Folds the elements from left to right, starting from init. The accumulator type may differ from the element type. Nil input returns init.
sieve.KeyBy Returns a map from key to element; when two elements share a key, the later one wins. Nil input gives a non-nil empty map.
sieve.GroupBy Returns a map from key to the elements with that key, each group in input order and of the caller's slice type. Nil input gives a non-nil empty map.
sieve.Uniq Returns the elements without duplicates, keeping the first occurrence, in input order. Nil input gives a non-nil empty slice.
sieve.UniqBy Returns the elements whose key was not seen before, keeping the first occurrence of each key, in input order. Nil input gives a non-nil empty slice.
sieve.Partition Returns (matched, rest), both in input order and both non-nil, even for nil input.
sieve.Find Returns the first matching element and true; the predicate is not called after the first match. No match or nil input gives the zero value and false.
sieve.Set A set backed by map[T]struct{}; len, range, delete and maps.Keys work on it directly. Has and Slice work on a nil Set; Add on a nil Set panics.
sieve.SetOf Returns a new set holding the given items without duplicates. With no arguments it returns an empty, non-nil set that Add can use.
sieve.Set.Has Reports whether a value is in the set. A nil Set reports false.
sieve.Set.Add Adds one or more values; values already present are ignored. Panics on a nil Set.
sieve.Set.Slice Returns each element exactly once, in unspecified order. Non-nil even for an empty or nil Set.

Dependencies

  • SummerCMS modules: none.
  • Third-party: none.
  • Standard library: none (the package has no imports).

Testing

go test ./modules/sieve/...

The tests are in-memory and need no external services. They include runnable examples for every function and sieve.Set method, and cover every statement.