|
| 1 | +# CLAUDE.md |
| 2 | + |
| 3 | +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. |
| 4 | + |
| 5 | +## Project Overview |
| 6 | + |
| 7 | +`samber/ro` is a Go implementation of the ReactiveX spec — a library for reactive/stream programming with Observables, Observers, and Operators. It uses Go 1.18+ generics extensively. The library is v0 and follows SemVer strictly. |
| 8 | + |
| 9 | +## Build & Test Commands |
| 10 | + |
| 11 | +```bash |
| 12 | +make build # Build all modules |
| 13 | +make test # Run all tests with race detector |
| 14 | +make lint # Run golangci-lint + license header check |
| 15 | +make lint-fix # Auto-fix lint issues |
| 16 | +make bench # Run benchmarks |
| 17 | +make coverage # Generate coverage report (cover.html) |
| 18 | +``` |
| 19 | + |
| 20 | +Run a single test: |
| 21 | +```bash |
| 22 | +go test -race -run TestFunctionName ./... |
| 23 | +``` |
| 24 | + |
| 25 | +Run tests for a specific plugin: |
| 26 | +```bash |
| 27 | +cd plugins/encoding/json && go test -race ./... |
| 28 | +``` |
| 29 | + |
| 30 | +## Multi-Module Architecture |
| 31 | + |
| 32 | +This is a **Go workspace** (`go.work`) with many independent modules. The root module is `github.com/samber/ro`. Each plugin under `plugins/` has its own `go.mod`. Some plugins are commented out in `go.work` because they require newer Go versions. |
| 33 | + |
| 34 | +The SIMD plugin (`plugins/exp/simd`) requires `GOEXPERIMENT=simd` and `GOWORK=off` to build/test. |
| 35 | + |
| 36 | +## Code Layout |
| 37 | + |
| 38 | +- **Root package (`ro`)** — Core types and all built-in operators |
| 39 | +- **`internal/`** — Internal helpers: `xsync` (mutex wrappers), `xatomic`, `xrand`, `xtime`, `xerrors`, `constraints` |
| 40 | +- **`testing/`** — Package `rotesting` with `AssertSpec[T]` interface for fluent test assertions |
| 41 | +- **`plugins/`** — Each plugin is a separate Go module with its own `go.mod`. Categories: encoding, observability, rate limiting, I/O, data manipulation, etc. |
| 42 | +- **`ee/`** — Enterprise Edition (separate license). Contains `otel` and `prometheus` plugins, plus licensing infrastructure |
| 43 | +- **`examples/`** — Working example applications (each is its own module) |
| 44 | +- **`docs/`** — Docusaurus documentation site. Has its own `CLAUDE.md` for doc-writing conventions |
| 45 | + |
| 46 | +## Operator Pattern |
| 47 | + |
| 48 | +All chainable operators follow this signature pattern: |
| 49 | +```go |
| 50 | +func OperatorName[T any](params) func(Observable[T]) Observable[R] |
| 51 | +``` |
| 52 | + |
| 53 | +Example: |
| 54 | + |
| 55 | +```go |
| 56 | +func Filter[T any](predicate func(item T) bool) func(Observable[T]) Observable[T] { |
| 57 | + return func(source Observable[T]) Observable[T] { |
| 58 | + return NewUnsafeObservableWithContext(func(subscriberCtx context.Context, destination Observer[T]) Teardown { |
| 59 | + sub := source.SubscribeWithContext( |
| 60 | + subscriberCtx, |
| 61 | + NewObserverWithContext( |
| 62 | + func(ctx context.Context, value T) { |
| 63 | + ok := predicate(value) |
| 64 | + if ok { |
| 65 | + destination.NextWithContext(ctx, value) |
| 66 | + } |
| 67 | + }, |
| 68 | + destination.ErrorWithContext, |
| 69 | + destination.CompleteWithContext, |
| 70 | + ), |
| 71 | + ) |
| 72 | + |
| 73 | + return sub.Unsubscribe |
| 74 | + }) |
| 75 | + } |
| 76 | +} |
| 77 | +``` |
| 78 | + |
| 79 | +They return a function that transforms one Observable into another, enabling composition via `Pipe()`. |
| 80 | + |
| 81 | +### Operator Variant Suffixes |
| 82 | + |
| 83 | +Most operators have variants created by combining these suffixes: |
| 84 | + |
| 85 | +- **`I`** — Adds an `index int64` parameter to the callback (e.g., `FilterI`, `MapI`) |
| 86 | +- **`WithContext`** — Adds `context.Context` to the callback signature (e.g., `FilterWithContext`, `MapWithContext`) |
| 87 | +- **`Err`** — The callback can return an `error` that terminates the stream (e.g., `MapErr`) |
| 88 | + |
| 89 | +These suffixes combine in a fixed order: **`Err` + `I` + `WithContext`**. Examples: |
| 90 | +- `Map` → `MapI` → `MapWithContext` → `MapIWithContext` |
| 91 | +- `MapErr` → `MapErrI` → `MapErrWithContext` → `MapErrIWithContext` |
| 92 | + |
| 93 | +Other naming patterns: |
| 94 | +- **Numbered suffixes** (2, 3, 4, 5...) — Arity variants for multi-observable operators (e.g., `CombineLatest2`, `Zip3`, `MergeWith1`). Also used for type-safe pipe: `Pipe1` through `Pipe11` |
| 95 | +- **`Op`** — Operator version of a creation function, for use inside `Pipe()` (e.g., `PipeOp`) |
| 96 | + |
| 97 | +## Core Operators vs Plugins |
| 98 | + |
| 99 | +**Core operators** live in the root `ro` package and have no external dependencies beyond `samber/lo`. They cover the standard ReactiveX operator categories (creation, transformation, filtering, combining, etc.) and are imported as `github.com/samber/ro`. |
| 100 | + |
| 101 | +**Plugins** are separate Go modules under `plugins/`, each with its own `go.mod` and third-party dependencies. They follow the same `func(Observable[T]) Observable[R]` signature pattern and compose with core operators via `Pipe()`. Plugins wrap external libraries (e.g., `zap`, `sentry`, `fsnotify`) or provide domain-specific operators (e.g., JSON encoding, CSV I/O, rate limiting). Import them separately, e.g., `github.com/samber/ro/plugins/encoding/json`. |
| 102 | + |
| 103 | +## Testing Conventions |
| 104 | + |
| 105 | +- Tests use `testify` assertions and `go.uber.org/goleak` for goroutine leak detection |
| 106 | +- Test files follow Go convention: `foo_test.go` alongside `foo.go` |
| 107 | +- Example tests use `_example_test.go` suffix |
| 108 | +- The `plugins/testify` plugin provides reactive stream assertion helpers |
| 109 | + |
| 110 | +Typical test pattern — use `Collect()` to gather all emitted values and assert: |
| 111 | + |
| 112 | +```go |
| 113 | +func TestOperatorTransformationMap(t *testing.T) { |
| 114 | + t.Parallel() |
| 115 | + is := assert.New(t) |
| 116 | + |
| 117 | + values, err := Collect( |
| 118 | + Map(func(v int) int { return v * 2 })(Just(1, 2, 3)), |
| 119 | + ) |
| 120 | + is.Equal([]int{2, 4, 6}, values) |
| 121 | + is.NoError(err) |
| 122 | + |
| 123 | + // Test error propagation |
| 124 | + values, err = Collect( |
| 125 | + Map(func(v int) int { return v * 2 })(Throw[int](assert.AnError)), |
| 126 | + ) |
| 127 | + is.Equal([]int{}, values) |
| 128 | + is.EqualError(err, assert.AnError.Error()) |
| 129 | +} |
| 130 | +``` |
| 131 | + |
| 132 | +Always test edge cases with `Empty[T]()` (empty source) and `Throw[T](assert.AnError)` (error source). Also test early unsubscription, context propagation, and context cancellation where applicable. |
| 133 | + |
| 134 | +## Contributing Conventions |
| 135 | + |
| 136 | +- **Operator naming**: Must be self-explanatory and respect ReactiveX/RxJS standards. Inspired by https://reactivex.io/documentation/operators.html and https://rxjs.dev/api |
| 137 | +- **Context propagation**: Operators must not break the context chain. Always use `SubscribeWithContext`, `NextWithContext`, `ErrorWithContext`, `CompleteWithContext`. The `WithContext` variant callbacks receive and return a `context.Context` |
| 138 | +- **Callback naming**: `predicate` for bool-returning callbacks, `transform`/`project` for value-transforming callbacks, `callback` for void callbacks |
| 139 | +- **Variadic operators**: Some operators accept `...Observable[T]` for flexibility (e.g., `Zip`, `Merge`, `MergeWith`) |
| 140 | +- **Type aliases**: Some operators use `~[]T` constraints to accept named slice types, not just `[]T` (e.g., `Flatten`) |
| 141 | +- **Documentation**: Each operator needs a Go Playground link in its comment, a markdown doc in `docs/data/`, an example in `ro_example_test.go`, and an entry in `docs/static/llms.txt` |
| 142 | +- **License headers**: All `.go` files require license headers (`licenses/header.apache.txt` for open source, `licenses/header.ee.txt` for `ee/`). Run `make lint` to verify |
0 commit comments