From 3da9aa5ecbb559f267f73ddd432791a101dd8e3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Gonz=C3=A1lez=20Di=20Antonio?= Date: Thu, 23 Jul 2026 18:38:16 +0200 Subject: [PATCH] feat!: modernize for Go 1.26 and prepare v1.0.0 Rework the library for Go 1.26, fix correctness bugs, add new capabilities, and align the repository structure with the slashdevops open-source layout (e5t / comparator). Correctness fixes: - Eliminate potential deadlocks in MapKeyValue caused by recursive read locking (Clone, Map, Filter, Partition, DeepEqual, SortKeys/SortValues). - Fix SMapKeyValue.Size() over-counting when overwriting existing keys. - Fix a non-atomic counter update race in SMapKeyValue. - Use sync.Map.Clear and the clear builtin instead of reallocating. New features: - All() range-over-func iterators (iter.Seq2) on both containers. - GetOrSet (atomic get-or-insert), Merge, and JSON (Marshal/Unmarshal). - Modern internals: maps, slices, clear, atomic.Int64. Breaking changes (see docs/migration.md): - Requires Go 1.26 (was 1.19). - GetAnDelete renamed to GetAndDelete. - Removed IsFull() (use !IsEmpty()) and Key() (use ContainsKey()). - SortKeys/SortValues return []K/[]T instead of []*K/[]*T. Docs & tooling: - New doc.go, extensive docs/ guides with mermaid diagrams, richer README. - Modernized Makefile, AGENTS.md, GitHub workflows (main/pr/release/codeql), dependabot, golangci config; removed gosec/codeql-analysis. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/codeql/codeql-config.yml | 5 + .github/copilot-instructions.md | 98 +++++++ .github/dependabot.yml | 11 + .github/release.yml | 16 + .github/workflows/codeql-analysis.yml | 72 ----- .github/workflows/codeql.yml | 44 +++ .github/workflows/gosec.yml | 44 --- .github/workflows/main.yml | 96 +++--- .github/workflows/pr.yml | 58 ++++ .github/workflows/release.yml | 85 +++--- .gitignore | 23 +- .golangci.yaml | 21 ++ .gosec.json | 9 - .vscode/settings.json | 26 +- AGENTS.md | 1 + Makefile | 182 ++++++++++-- README.md | 314 +++++++++++++------- SECURITY.md | 18 +- additions_test.go | 208 +++++++++++++ doc.go | 68 +++++ docs/README.md | 59 ++++ docs/concurrency.md | 134 +++++++++ docs/containers.md | 109 +++++++ docs/faq.md | 115 ++++++++ docs/getting-started.md | 134 +++++++++ docs/iteration.md | 112 +++++++ docs/json.md | 109 +++++++ docs/migration.md | 157 ++++++++++ docs/operations.md | 162 ++++++++++ docs/performance.md | 116 ++++++++ example_test.go | 243 +++++++++------ go.mod | 2 +- godoc.go | 9 - mapkeyvalue.go | 408 +++++++++++++++----------- mapkeyvalue_test.go | 138 +++------ smapkeyvalue.go | 380 +++++++++++++----------- smapkeyvalue_test.go | 144 +++------ 37 files changed, 2932 insertions(+), 998 deletions(-) create mode 100644 .github/codeql/codeql-config.yml create mode 100644 .github/copilot-instructions.md create mode 100644 .github/dependabot.yml create mode 100644 .github/release.yml delete mode 100644 .github/workflows/codeql-analysis.yml create mode 100644 .github/workflows/codeql.yml delete mode 100644 .github/workflows/gosec.yml create mode 100644 .github/workflows/pr.yml create mode 100644 .golangci.yaml delete mode 100644 .gosec.json create mode 120000 AGENTS.md create mode 100644 additions_test.go create mode 100644 doc.go create mode 100644 docs/README.md create mode 100644 docs/concurrency.md create mode 100644 docs/containers.md create mode 100644 docs/faq.md create mode 100644 docs/getting-started.md create mode 100644 docs/iteration.md create mode 100644 docs/json.md create mode 100644 docs/migration.md create mode 100644 docs/operations.md create mode 100644 docs/performance.md delete mode 100644 godoc.go diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml new file mode 100644 index 0000000..93833f2 --- /dev/null +++ b/.github/codeql/codeql-config.yml @@ -0,0 +1,5 @@ +name: CodeQL config + +paths-ignore: + - mocks + - testdata diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..4a9c775 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,98 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) and other AI +assistants when working with code in this repository. Follow these guidelines +precisely to ensure consistency and maintainability. + +## Stack + +- Language: Go (Go 1.26+) +- Framework: Go standard library **only** — this library is intentionally + dependency-free +- Testing: Go's built-in testing package +- Dependency Management: Go modules +- Version Control: Git +- Documentation: `go doc` / pkg.go.dev, and Markdown files under `docs/` +- Code Review: Pull requests on GitHub +- CI/CD: GitHub Actions + +## What this library is + +`RamStorage (r9e)` is a small, dependency-free Go library of **thread-safe, +generic in-memory key-value containers**. It exposes two containers built on Go +generics: + +- `MapKeyValue[K comparable, T any]` — backed by a native map guarded by a + `sync.RWMutex`; best for read-heavy or mixed workloads and consistent bulk + snapshots. +- `SMapKeyValue[K comparable, T any]` — backed by `sync.Map` with an atomic + size counter; best for disjoint-key writes and write-once/read-many workloads. + +The focus is usability and simplicity without sacrificing performance. + +## Key Conventions + +- **Style:** Follow the Google Go Style Guide and Effective Go: + - + - + - + - +- Keep functions small and focused on a single task. +- Use meaningful names for variables, functions, and packages. +- Use comments to explain complex logic or decisions. +- Use `any`, never `interface{}`. +- Prefer `for b.Loop()` over `for i := 0; i < b.N; i++` in benchmarks (Go 1.24+). +- Never hold a lock while calling a method that takes the same lock (no + recursive `RLock`); build results directly under a single lock, or take a + snapshot first. +- **Tests:** Table-driven tests where practical. Test files are co-located with + source (`*_test.go`). Executable examples live in `example_test.go` and are + verified by `go test`. +- **No external dependencies:** Do not add third-party modules without explicit + approval. `go.sum` should not exist unless a dependency is intentionally + introduced. + +## Project Structure + +This is a single-package Go library; source files live in the repository root. + +- `*.go` — library source code. +- `*_test.go` — unit tests and benchmarks. +- `example_test.go` — executable examples rendered by pkg.go.dev. +- `doc.go` — package-level documentation. +- `docs/` — extensive usage documentation in Markdown. +- `.github/` — GitHub Actions workflows, CodeQL, Dependabot, release metadata. +- `.golangci.yaml` — optional local golangci-lint configuration. +- `.vscode/` — editor settings. +- `LICENSE` — Apache License 2.0. +- `README.md` — project overview and usage guide. +- `SECURITY.md` — vulnerability reporting policy. +- `go.mod` — module definition with no external requirements. + +## Post-Change Checklist + +Use these standard Go commands after making changes, before committing: + +```bash +go fix ./... +go fmt ./... +go vet ./... +betteralign -apply ./... +go test -race -coverprofile=/tmp/r9e-coverage.txt -covermode=atomic ./... +go build ./... +``` + +Do not add external module dependencies without explicit approval; this project +is intentionally standard-library-only. + +## Commit Message & Pull Request Guidelines + +- Always work on a new branch unless explicitly told to use the current one. +- Use semantic (Conventional Commits) messages, e.g. `feat:`, `fix:`, `docs:`, + `test:`, `chore:`, `refactor:`. +- Keep the commit subject under 72 characters and the whole message concise. +- Group related changes into focused commits rather than one large commit. +- Open pull requests against `main` with a semantic title and a description that + summarizes the changes and references any related issues. +- Keep changes small, idiomatic, tested, documented, and dependency-free unless + there is a clear reason to expand the project scope. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..d163711 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: + - package-ecosystem: gomod + directory: / + schedule: + interval: weekly + + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000..a032ef7 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,16 @@ +changelog: + categories: + - title: Breaking Changes + labels: + - Semver-Major + - breaking-change + - title: New Features + labels: + - Semver-Minor + - enhancement + - title: Security + labels: + - security + - title: Other Changes + labels: + - "*" diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index bcbd7e8..0000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,72 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" - -on: - push: - branches: [ main ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ main ] - schedule: - - cron: '26 9 * * 3' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'go' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] - # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - - # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality - - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v2 - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - # If the Autobuild fails above, remove it and uncomment the following three lines. - # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. - - # - run: | - # echo "Run, Build Application using script" - # ./location_of_script_within_repo/buildscript.sh - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..a8aab4a --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,44 @@ +name: CodeQL Advanced + +on: + push: + branches: + - main + pull_request: + branches: + - main + schedule: + - cron: "10 12 * * 3" + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + packages: read + security-events: write + strategy: + fail-fast: false + matrix: + include: + - language: actions + build-mode: none + - language: go + build-mode: autobuild + steps: + - name: Checkout repository + uses: actions/checkout@v7 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + config-file: ./.github/codeql/codeql-config.yml + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 + with: + category: /language:${{ matrix.language }} diff --git a/.github/workflows/gosec.yml b/.github/workflows/gosec.yml deleted file mode 100644 index 3b80cab..0000000 --- a/.github/workflows/gosec.yml +++ /dev/null @@ -1,44 +0,0 @@ ---- -name: Run Gosec -on: - push: - branches: - - main - - pull_request: - branches: - - main - - workflow_dispatch: - -env: - GO_VERSION: 1.19 - -jobs: - tests: - runs-on: ubuntu-latest - env: - GOROOT: $(go env GOROOT) - steps: - - name: Set up Go 1.x - id: go - uses: actions/setup-go@v2 - with: - go-version: ${{ env.GO_VERSION }} - - - name: Check out code - uses: actions/checkout@v3 - - - name: Show project files after make - run: tree . - - - name: Set Go environment variables - id: goroot - run: | - echo "GOROOT=$(go env GOROOT)" >> $GITHUB_ENV - - - name: Run Gosec Security Scanner - run: | - export PATH=$PATH:$(go env GOPATH)/bin - go install github.com/securego/gosec/v2/cmd/gosec@latest - gosec -conf .gosec.json ./... \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a091ca8..72092f5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,53 +1,81 @@ -name: "Main" +name: Main on: push: branches: - main - # pull_request: - # branches: - # - main - - workflow_dispatch: - -env: - GO_VERSION: 1.19 - permissions: - security-events: write - actions: read contents: read - pull-requests: read jobs: - test: - name: Test + build: runs-on: ubuntu-latest steps: - - name: Set up Go 1.x - id: go - uses: actions/setup-go@v2 + - name: Checkout + uses: actions/checkout@v7 + + - name: Set up Go + uses: actions/setup-go@v7 with: - go-version: ${{ env.GO_VERSION }} + go-version-file: ./go.mod + cache: true - - name: Check out code - uses: actions/checkout@v3 + - name: Summary Information + run: | + echo "# Push Summary" > "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "**Repository:** ${{ github.repository }}" >> "$GITHUB_STEP_SUMMARY" + echo "**Push:** ${{ github.event.head_commit.message }}" >> "$GITHUB_STEP_SUMMARY" + echo "**Author:** ${{ github.event.head_commit.author.name }}" >> "$GITHUB_STEP_SUMMARY" + echo "**Branch:** ${{ github.ref }}" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" - - name: Show project files before make - run: tree . + - name: Tools and versions + run: | + echo "## Tools and versions" >> "$GITHUB_STEP_SUMMARY" + echo "**Ubuntu Version:** $(lsb_release -ds)" >> "$GITHUB_STEP_SUMMARY" + echo "**Bash Version:** $(bash --version | head -n 1 | awk '{print $4}')" >> "$GITHUB_STEP_SUMMARY" + echo "**Git Version:** $(git --version | awk '{print $3}')" >> "$GITHUB_STEP_SUMMARY" + echo "**Go Version:** $(go version | awk '{print $3}')" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" - - name: Test - run: make test + - name: Format check + run: | + echo "## Format check" >> "$GITHUB_STEP_SUMMARY" + files=$(gofmt -l .) + if [ -n "$files" ]; then + echo "$files" + echo "The files above need gofmt." >> "$GITHUB_STEP_SUMMARY" + exit 1 + fi + echo "All Go files are gofmt-formatted." >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" - - name: Show project files after make - run: tree . + - name: Vet + run: | + echo "## Vet" >> "$GITHUB_STEP_SUMMARY" + go vet ./... | tee -a "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" - - name: codecov coverage report - uses: codecov/codecov-action@v2 - with: - token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos - files: ./coverage.out + - name: Test + run: | + echo "## Test report" >> "$GITHUB_STEP_SUMMARY" + go test -race -coverprofile=coverage.txt -covermode=atomic ./... | tee -a "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + + - name: Test coverage + run: | + echo "## Test coverage" >> "$GITHUB_STEP_SUMMARY" + echo '```' >> "$GITHUB_STEP_SUMMARY" + go tool cover -func=coverage.txt | tee -a "$GITHUB_STEP_SUMMARY" + echo '```' >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + total_coverage=$(go tool cover -func=coverage.txt | grep total | awk '{print $3}') + echo "**Total Coverage:** $total_coverage" >> "$GITHUB_STEP_SUMMARY" - - name: Remove artifacts - run: make clean \ No newline at end of file + - name: Build + run: | + echo "## Build" >> "$GITHUB_STEP_SUMMARY" + go build ./... | tee -a "$GITHUB_STEP_SUMMARY" + echo "Build completed successfully." >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml new file mode 100644 index 0000000..dbc01e7 --- /dev/null +++ b/.github/workflows/pr.yml @@ -0,0 +1,58 @@ +name: Pull Request + +on: + pull_request: + branches: + - main + +permissions: + contents: read + pull-requests: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Set up Go + uses: actions/setup-go@v7 + with: + go-version-file: ./go.mod + cache: true + + - name: Summary Information + run: | + echo "# Pull Request Summary" > "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "**Repository:** ${{ github.repository }}" >> "$GITHUB_STEP_SUMMARY" + echo "**Pull Request:** ${{ github.event.pull_request.title }}" >> "$GITHUB_STEP_SUMMARY" + echo "**Author:** ${{ github.event.pull_request.user.login }}" >> "$GITHUB_STEP_SUMMARY" + echo "**Branch:** ${{ github.event.pull_request.head.ref }}" >> "$GITHUB_STEP_SUMMARY" + echo "**Base:** ${{ github.event.pull_request.base.ref }}" >> "$GITHUB_STEP_SUMMARY" + echo "**Commits:** ${{ github.event.pull_request.commits }}" >> "$GITHUB_STEP_SUMMARY" + echo "**Changed Files:** ${{ github.event.pull_request.changed_files }}" >> "$GITHUB_STEP_SUMMARY" + echo "**Additions:** ${{ github.event.pull_request.additions }}" >> "$GITHUB_STEP_SUMMARY" + echo "**Deletions:** ${{ github.event.pull_request.deletions }}" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + + - name: Format check + run: | + files=$(gofmt -l .) + if [ -n "$files" ]; then + echo "$files" + exit 1 + fi + + - name: Vet + run: go vet ./... + + - name: Test + run: go test -race -coverprofile=coverage.txt -covermode=atomic ./... + + - name: Test coverage + run: go tool cover -func=coverage.txt + + - name: Build + run: go build ./... diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 291596f..360fa3c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,64 +1,69 @@ ---- -name: "Release" +name: Release -# https://help.github.com/es/actions/reference/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet on: push: tags: - - v[0-9].[0-9]+.[0-9]* - -env: - GO_VERSION: 1.19 + - "v*.*.*" permissions: - id-token: write - security-events: write - actions: read + actions: write contents: write + id-token: write + packages: write pull-requests: read + security-events: write jobs: - test: - name: Test + release: + name: Release runs-on: ubuntu-latest steps: - - name: Set up Go 1.x - uses: actions/setup-go@v2 + - name: Check out code + uses: actions/checkout@v7 + + - name: Set up Go + uses: actions/setup-go@v7 with: - go-version: ${{ env.GO_VERSION }} - id: go + go-version-file: ./go.mod + cache: true - - name: Check out code - uses: actions/checkout@v3 + - name: Summary Information + run: | + echo "# Release Summary" > "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "**Repository:** ${{ github.repository }}" >> "$GITHUB_STEP_SUMMARY" + echo "**Actor:** ${{ github.triggering_actor }}" >> "$GITHUB_STEP_SUMMARY" + echo "**Commit ID:** ${{ github.sha }}" >> "$GITHUB_STEP_SUMMARY" + echo "**Tag:** ${{ github.ref_name }}" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" - - name: Test - run: make test + - name: Format check + run: | + files=$(gofmt -l .) + if [ -n "$files" ]; then + echo "$files" + exit 1 + fi - create_github_release: - name: Create Github Release - needs: test - runs-on: ubuntu-latest - steps: - - name: Set up Go 1.x - uses: actions/setup-go@v2 - with: - go-version: ${{ env.GO_VERSION }} - id: go + - name: Vet + run: go vet ./... - - name: Check out code - uses: actions/checkout@v3 + - name: Test + run: go test -race -coverprofile=coverage.txt -covermode=atomic ./... - - name: Show workspace files - run: tree . + - name: Test coverage + run: | + echo "## Test coverage" >> "$GITHUB_STEP_SUMMARY" + echo '```' >> "$GITHUB_STEP_SUMMARY" + go tool cover -func=coverage.txt | tee -a "$GITHUB_STEP_SUMMARY" + echo '```' >> "$GITHUB_STEP_SUMMARY" - - name: Create Release - id: create_github_release - uses: softprops/action-gh-release@v1 + - name: Release + uses: softprops/action-gh-release@v3 with: tag_name: ${{ github.ref_name }} name: ${{ github.ref_name }} - # body: | - # See the file: CHANGELOG.md draft: false prerelease: false - token: ${{ secrets.GITHUB_TOKEN }} + generate_release_notes: true + make_latest: true diff --git a/.gitignore b/.gitignore index 86a8fdb..8ee458e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# # Binaries for programs and plugins *.exe *.exe~ @@ -8,10 +11,26 @@ # Test binary, built with `go test -c` *.test -# Output of the go coverage tool, specifically when used with LiteIDE +# Code coverage profiles and other test artifacts *.out +coverage.txt +coverage.* +*.coverprofile +profile.cov # Dependency directories (remove the comment below to include it) # vendor/ -*.log \ No newline at end of file +# Go workspace file +go.work +go.work.sum + +# env file +.env + +# Editor/IDE +# .idea/ +# .vscode/ + +# Build and coverage output +build/ diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..e4af670 --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,21 @@ +version: "2" +linters: + enable: + - errcheck + - ineffassign + - staticcheck + - unused + + settings: + errcheck: + check-type-assertions: false + check-blank: false + disable-default-exclusions: true + exclude-functions: + - (*os.File).Close + - (io.Closer).Close + - fmt.Print + - fmt.Printf + - fmt.Println + - fmt.Fprint + - fmt.Fprintf diff --git a/.gosec.json b/.gosec.json deleted file mode 100644 index e104170..0000000 --- a/.gosec.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "G101": { - "pattern": "(?i)passwd|pass|password|pwd|private_key", - "ignore_entropy": false, - "entropy_threshold": "80.0", - "per_char_threshold": "3.0", - "truncate": "32" - } -} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 81d8736..10e6913 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,14 +1,16 @@ { "cSpell.words": [ - "codecov", - "Doakes", - "Donato", - "Gosec", - "keyval", - "Println", - "Ramstorag", - "Ricupero", - "Sixpack", - "struct" - ] -} \ No newline at end of file + "betteralign", + "comparable", + "Errorf", + "gomod", + "nolint", + "RamStorage", + "slashdevops" + ], + "chat.tools.terminal.autoApprove": { + "gofmt": true, + "test": true, + "actionlint": true + } +} diff --git a/AGENTS.md b/AGENTS.md new file mode 120000 index 0000000..02dd134 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1 @@ +.github/copilot-instructions.md \ No newline at end of file diff --git a/Makefile b/Makefile index c1e2539..785d110 100644 --- a/Makefile +++ b/Makefile @@ -2,42 +2,174 @@ EXECUTABLES = go K := $(foreach exec,$(EXECUTABLES),\ - $(if $(shell which $(exec)),some string,$(error "No $(exec) in PATH))) + $(if $(shell command -v $(exec)),some string,$(error "No $(exec) in PATH"))) +# Ensure shell errors are propagated. +.SHELLFLAGS := -e -c + +PROJECT_NAME ?= $(shell grep '^module' go.mod | cut -d '/' -f 3) +PROJECT_NAMESPACE ?= $(shell grep '^module' go.mod | cut -d '/' -f 2) PROJECT_DEPENDENCIES := $(shell go list -m -f '{{if not (or .Indirect .Main)}}{{.Path}}{{end}}' all) -# avoid mocks in tests -GO_FILES := $(shell go list ./...) +BUILD_DIR ?= ./build + +PROJECT_COVERAGE_FILE ?= $(BUILD_DIR)/coverage.txt +PROJECT_COVERAGE_MODE ?= atomic + +######## Functions ######## +# exec_cmd runs a command with friendly output. +# MAKE_DEBUG=true print the command instead of running it. +# MAKE_STOP_ON_ERRORS=true abort the whole run when a command fails (useful in CI). +MAKE_STOP_ON_ERRORS ?= false +MAKE_DEBUG ?= false + +define exec_cmd +$(if $(filter $(MAKE_DEBUG),true),\ + ${1} \ +, \ + $(if $(filter $(MAKE_STOP_ON_ERRORS),true),\ + $(if $(findstring >, $1),\ + @${1} 2>/dev/null && printf " 🤞 ${1} ✅\n" || (printf " ${1} ❌\n"; exit 1) \ + , \ + @${1} > /dev/null && printf " 🤞 ${1} ✅\n" || (printf " ${1} ❌\n"; exit 1) \ + ) \ + , \ + $(if $(findstring >, $1),\ + @${1} 2>/dev/null; _exit_code=$$?; if [ $$_exit_code -eq 0 ]; then printf " 🤞 ${1} ✅\n"; else printf " ${1} ❌\n"; fi; exit $$_exit_code \ + , \ + @${1} > /dev/null 2>&1; _exit_code=$$?; if [ $$_exit_code -eq 0 ]; then printf ' 🤞 ${1} ✅\n'; else printf ' ${1} ❌\n'; fi; exit $$_exit_code \ + ) \ + ) \ +) + +endef # don't remove the white line before endef + +############################################################################### +######## Targets ############################################################## +##@ Default command +.PHONY: all +all: clean check ## Clean and run the full quality gate (default target). + +############################################################################### +##@ Golang commands +.PHONY: go-fmt +go-fmt: ## Format go code. + @printf "👉 Formatting go code...\n" + $(call exec_cmd, go fmt ./... ) + +.PHONY: go-vet +go-vet: ## Vet go code. + @printf "👉 Vet go code...\n" + $(call exec_cmd, go vet ./... ) + +.PHONY: go-fix +go-fix: go-fmt go-vet ## Apply modernizations, then fmt and vet. + @printf "👉 Fixing go code...\n" + $(call exec_cmd, go fix ./... ) + +.PHONY: go-betteralign +go-betteralign: install-betteralign ## Align struct fields for optimal memory layout. + @printf "👉 Aligning struct fields with betteralign...\n" + $(call exec_cmd, betteralign -apply ./... ) + +.PHONY: go-mod-tidy +go-mod-tidy: ## Clean go.mod and go.sum. + @printf "👉 Cleaning go.mod and go.sum...\n" + $(call exec_cmd, go mod tidy) + +.PHONY: go-mod-update +go-mod-update: go-mod-tidy ## Update all direct dependencies. + @printf "👉 Updating dependencies...\n" + $(foreach DEP, $(PROJECT_DEPENDENCIES), \ + $(call exec_cmd, go get -u $(DEP)) \ + ) + $(call exec_cmd, go mod tidy) + +.PHONY: go-mod-verify +go-mod-verify: ## Verify go.mod and go.sum. + @printf "👉 Verifying modules...\n" + $(call exec_cmd, go mod verify) + +############################################################################### +##@ Test commands +$(PROJECT_COVERAGE_FILE): + @printf "👉 Creating coverage file...\n" + $(call exec_cmd, mkdir -p $(BUILD_DIR) ) + $(call exec_cmd, touch $(PROJECT_COVERAGE_FILE) ) + +.PHONY: test +test: $(PROJECT_COVERAGE_FILE) ## Run tests with the race detector and coverage. + @printf "👉 Running tests...\n" + $(call exec_cmd, go test \ + -race \ + -coverprofile=$(PROJECT_COVERAGE_FILE) \ + -covermode=$(PROJECT_COVERAGE_MODE) \ + ./... \ + ) + +.PHONY: test-coverage +test-coverage: test ## Open the HTML coverage report in the browser. + @printf "👉 Opening coverage report...\n" + $(call exec_cmd, go tool cover -html=$(PROJECT_COVERAGE_FILE)) -all: clean test +.PHONY: cover-func +cover-func: test ## Print per-function and total coverage. + @printf "👉 Coverage summary...\n" + $(call exec_cmd, go tool cover -func=$(PROJECT_COVERAGE_FILE)) -mod-update: tidy - $(foreach dep, $(PROJECT_DEPENDENCIES), $(shell go get -u $(dep))) - go mod tidy +.PHONY: bench +bench: ## Run all benchmarks with allocation stats. + @printf "👉 Running benchmarks...\n" + $(call exec_cmd, go test -run '^$$' -bench . -benchmem ./... ) -tidy: - go mod tidy +############################################################################### +##@ Build & Check commands +.PHONY: build +build: ## Verify the package builds. + @printf "👉 Building...\n" + $(call exec_cmd, go build ./... ) -fmt: - @go fmt $(GO_FILES) +.PHONY: lint +lint: install-golangci-lint ## Lint go code with golangci-lint. + @printf "👉 Linting...\n" + $(call exec_cmd, golangci-lint run ./... ) -vet: - go vet $(GO_FILES) +.PHONY: vulncheck +vulncheck: install-govulncheck ## Check for known vulnerabilities. + @printf "👉 Checking vulnerabilities...\n" + $(call exec_cmd, govulncheck ./... ) -lint: - golangci-lint run +.PHONY: check +check: go-fix test build ## Run the local quality gate (matches CI): fix, test, build. -generate: - go generate $(GO_FILES) +############################################################################### +##@ Tools commands +.PHONY: install-golangci-lint +install-golangci-lint: ## Install golangci-lint (https://golangci-lint.run/). + @printf "👉 Installing golangci-lint...\n" + $(call exec_cmd, go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2 ) -test: tidy fmt vet - go test -race -covermode=atomic -coverprofile coverage.out -tags=unit $(GO_FILES) +.PHONY: install-betteralign +install-betteralign: ## Install betteralign (https://github.com/dkorunic/betteralign). + @printf "👉 Installing betteralign...\n" + $(call exec_cmd, go install github.com/dkorunic/betteralign/cmd/betteralign@latest ) -test-coverage: test - go tool cover -html=coverage.out +.PHONY: install-govulncheck +install-govulncheck: ## Install govulncheck (https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck). + @printf "👉 Installing govulncheck...\n" + $(call exec_cmd, go install golang.org/x/vuln/cmd/govulncheck@latest ) -bench: - go test -bench=. -benchmem -benchtime=3s +############################################################################### +##@ Support commands +.PHONY: clean +clean: ## Remove build and coverage artifacts. + @printf "👉 Cleaning environment...\n" + $(call exec_cmd, rm -rf $(BUILD_DIR) ./*.out ) -clean: - rm -rf ./*.out +.PHONY: help +help: ## Display this help. + @awk 'BEGIN {FS = ":.*##"; \ + printf "Usage: make \033[36m\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ \ + { printf " \033[36m%-20s\033[0m %s\n", $$1, $$2 } /^##@/ \ + { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' \ + $(MAKEFILE_LIST) diff --git a/README.md b/README.md index 96909c4..4ffad37 100644 --- a/README.md +++ b/README.md @@ -1,164 +1,256 @@ -# RamStorage (r9e) +# 🧠 RamStorage (r9e) -[![CodeQL Analysis](https://github.com/slashdevops/r9e/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/slashdevops/r9e/actions/workflows/codeql-analysis.yml) -[![Gosec](https://github.com/slashdevops/r9e/actions/workflows/gosec.yml/badge.svg)](https://github.com/slashdevops/r9e/actions/workflows/gosec.yml) -[![Unit Test](https://github.com/slashdevops/r9e/actions/workflows/main.yml/badge.svg)](https://github.com/slashdevops/r9e/actions/workflows/main.yml) -[![Release](https://github.com/slashdevops/r9e/actions/workflows/release.yml/badge.svg)](https://github.com/slashdevops/r9e/actions/workflows/release.yml) +[![main branch](https://github.com/slashdevops/r9e/actions/workflows/main.yml/badge.svg)](https://github.com/slashdevops/r9e/actions/workflows/main.yml) ![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/slashdevops/r9e?style=plastic) +[![Go Reference](https://pkg.go.dev/badge/github.com/slashdevops/r9e.svg)](https://pkg.go.dev/github.com/slashdevops/r9e) +[![Go Report Card](https://goreportcard.com/badge/github.com/slashdevops/r9e)](https://goreportcard.com/report/github.com/slashdevops/r9e) +[![CodeQL](https://github.com/slashdevops/r9e/actions/workflows/codeql.yml/badge.svg)](https://github.com/slashdevops/r9e/actions/workflows/codeql.yml) [![license](https://img.shields.io/github/license/slashdevops/r9e.svg)](https://github.com/slashdevops/r9e/blob/main/LICENSE) -[![codecov](https://codecov.io/gh/slashdevops/r9e/branch/main/graph/badge.svg?token=UNTP5C1P6C)](https://codecov.io/gh/slashdevops/r9e) +[![Release](https://github.com/slashdevops/r9e/actions/workflows/release.yml/badge.svg)](https://github.com/slashdevops/r9e/actions/workflows/release.yml) [![release](https://img.shields.io/github/release/slashdevops/r9e/all.svg)](https://github.com/slashdevops/r9e/releases) -[![Go Reference](https://pkg.go.dev/badge/github.com/slashdevops/r9e.svg)](https://pkg.go.dev/github.com/slashdevops/r9e) - -**RamStorage (r9e)** is a `Thread-Safe` [Golang](https://go.dev/) library used for memory storage with convenient methods to store and retrieve data. - -This is focused on `usability and simplicity` rather than performance, but it doesn't mean that it's not fast. [Discover it for yourself](#how-fast) -## Overview +**RamStorage (`r9e`)** is a small, **thread-safe**, **generic**, dependency-free +[Go](https://go.dev/) library of in-memory key-value containers with a rich set +of convenient methods to store, retrieve, transform, and iterate data. + +It is focused on **usability and simplicity** — without giving up performance. +Only the Go standard library is used; there are **zero third-party +dependencies**. + +## ✨ Features + +- 🔒 **Thread-safe** — every operation is safe for concurrent use. +- 🧬 **Generic** — `MapKeyValue[K comparable, T any]` stores any comparable key + and any value type. +- 🧱 **Two containers, one API** — a `sync.RWMutex`-backed map and a `sync.Map`-backed + store expose the same method set, so you can switch by workload. +- 🔁 **Range-over-func iterators** — `for k, v := range kv.All()` (Go 1.23+). +- 🧰 **Functional helpers** — `Map`, `Filter`, `Partition`, `Merge`, `Clone`, + `GetOrSet`, `SortKeys`, `SortValues` — all non-mutating where it matters. +- 🗄️ **JSON-ready** — implements `json.Marshaler` / `json.Unmarshaler`. +- ⚡ **O(1) `Size()`** on both containers. +- 🚫 **Zero dependencies** — standard library only. +- 📄 **Apache-2.0 licensed**. + +## 🧭 Overview + +`r9e` takes advantage of [Go generics](https://go.dev/blog/intro-generics) and +the standard library's own concurrency primitives to provide a simple way to +store and retrieve data from memory. Two containers share the **same method +set**: + +```mermaid +flowchart TD + API["Shared API
Set · Get · GetOrSet · Delete · All · Map · Filter · Partition · Merge · JSON"] + API --> M["MapKeyValue[K, T]
native map + sync.RWMutex"] + API --> S["SMapKeyValue[K, T]
sync.Map + atomic counter"] + M --> M1["Best for read-heavy / mixed
consistent bulk snapshots"] + S --> S1["Best for disjoint-key writes
write-once / read-many"] +``` -Taking advantage of the [Golang Generics](https://go.dev/blog/intro-generics) and internal golang data structures, `RamStorage (r9e)` provides a simple way to store and retrieve data. +### Available Containers -The goal is to provide an easy way to use a library to store and retrieve data from memory using an API and data structures simples and. +| Container | Backing | Best for | +| --------- | ------- | -------- | +| [`MapKeyValue[K, T]`](https://pkg.go.dev/github.com/slashdevops/r9e#MapKeyValue) | `map` + [`sync.RWMutex`](https://pkg.go.dev/sync#RWMutex) | Read-heavy / mixed workloads, consistent snapshots | +| [`SMapKeyValue[K, T]`](https://pkg.go.dev/github.com/slashdevops/r9e#SMapKeyValue) | [`sync.Map`](https://pkg.go.dev/sync#Map) + atomic counter | Disjoint-key writes, write-once/read-many | -This package doesn't have any dependencies, so it's easy to use and maintain, only golang standard library is required. +> **Not sure which to use?** Start with `MapKeyValue`. Switch to `SMapKeyValue` +> only if profiling shows the `sync.Map` access pattern fits your workload +> better. See [docs/containers.md](docs/containers.md). -### Available Containers +## 📋 Requirements -* [MapKeyValue[K comparable, T any]](https://pkg.go.dev/github.com/slashdevops/r9e#MapKeyValue) using [sync.RWMutex](https://pkg.go.dev/sync#RWMutex) -* [SMapKeyValue[K comparable, T any]](https://pkg.go.dev/github.com/slashdevops/r9e#SMapKeyValue) using [sync.Map](https://pkg.go.dev/sync#Map) +- **Go 1.26 or newer** +- No external Go modules -### Documentation +## 📦 Installation -Official documentation is available on [pkg.go.dev -> slashdevops/r9e](https://pkg.go.dev/github.com/slashdevops/r9e) +Add the latest release to your module: -## Installing +```bash +go get github.com/slashdevops/r9e@latest +``` -Latest release: +Pin a specific version: ```bash -go get -u github.com/slashdevops/r9e@latest +go get github.com/slashdevops/r9e@vX.Y.Z ``` -Specific release: +Update to the newest available version later: ```bash -go get -u github.com/slashdevops/r9e@vx.y.z +go get -u github.com/slashdevops/r9e ``` -Adding it to your project: +Then import it: ```go import "github.com/slashdevops/r9e" ``` -## Example +## 🚀 Quick Start ```go package main import ( - "fmt" + "fmt" - "github.com/slashdevops/r9e" + "github.com/slashdevops/r9e" ) func main() { - type MathematicalConstants struct { + kv := r9e.NewMapKeyValue[string, int]() + + kv.Set("answer", 42) + + if v, ok := kv.GetAndCheck("answer"); ok { + fmt.Println("answer =", v) // answer = 42 + } + + // Range-over-func iteration (Go 1.23+). + for k, v := range kv.All() { + fmt.Printf("%s = %d\n", k, v) + } +} +``` + +### A richer example + +```go +type Constant struct { Name string Value float64 - } - - // With Capacity allocated - // kv := r9e.NewMapKeyValue[string, MathematicalConstants](r9e.WithCapacity(5)) - kv := r9e.NewMapKeyValue[string, MathematicalConstants]() - - kv.Set("pi", MathematicalConstants{"Archimedes' constant", 3.141592}) - kv.Set("e", MathematicalConstants{"Euler number, Napier's constant", 2.718281}) - kv.Set("γ", MathematicalConstants{"Euler number, Napier's constant", 0.577215}) - kv.Set("Φ", MathematicalConstants{"Golden ratio constant", 1.618033}) - kv.Set("ρ", MathematicalConstants{"Plastic number ρ (or silver constant)", 2.414213}) - - kvFilteredValues := kv.FilterValue(func(value MathematicalConstants) bool { - return value.Value > 2.0 - }) - - fmt.Println("Mathematical Constants:") - kvFilteredValues.ForEach(func(key string, value MathematicalConstants) { - fmt.Printf("Key: %v, Name: %v, Value: %v\n", key, value.Name, value.Value) - }) - - fmt.Printf("\n") - fmt.Printf("The most famous mathematical constant:\n") - fmt.Printf("Name: %v, Value: %v\n", kv.Get("pi").Name, kv.Get("pi").Value) - - lst := kv.SortValues(func(value1, value2 MathematicalConstants) bool { - return value1.Value > value2.Value - }) - - fmt.Printf("\n") - fmt.Printf("The most famous mathematical constant sorted by value:\n") - for i, value := range lst { - fmt.Printf("i: %v, Name: %v, Value: %v\n", i, value.Name, value.Value) - } - - kvHigh, kvLow := kv.Partition(func(key string, value MathematicalConstants) bool { - return value.Value > 2.5 - }) - - fmt.Printf("\n") - fmt.Printf("Mathematical constants which value is greater than 2.5:\n") - kvHigh.ForEach(func(key string, value MathematicalConstants) { - fmt.Printf("Key: %v, Name: %v, Value: %v\n", key, value.Name, value.Value) - }) - - fmt.Printf("\n") - fmt.Printf("Mathematical constants which value is less than 2.5:\n") - kvLow.ForEach(func(key string, value MathematicalConstants) { - fmt.Printf("Key: %v, Name: %v, Value: %v\n", key, value.Name, value.Value) - }) } -``` -Output: +kv := r9e.NewMapKeyValue[string, Constant](r9e.WithCapacity(8)) -```bash -Mathematical Constants: -Key: ρ, Name: Plastic number ρ (or silver constant), Value: 2.414213 -Key: e, Name: Euler number, Napier's constant, Value: 2.718281 -Key: pi, Name: Archimedes' constant, Value: 3.141592 - -The most famous mathematical constant: -Name: Archimedes' constant, Value: 3.141592 - -The most famous mathematical constants sorted by value: -i: 0, Name: Archimedes' constant, Value: 3.141592 -i: 1, Name: Euler number, Napier's constant, Value: 2.718281 -i: 2, Name: Plastic number ρ (or silver constant), Value: 2.414213 -i: 3, Name: Golden ratio constant, Value: 1.618033 -i: 4, Name: Euler number, Napier's constant, Value: 0.577215 - -Mathematical constants which value is greater than 2.5: -Key: pi, Name: Archimedes' constant, Value: 3.141592 -Key: e, Name: Euler number, Napier's constant, Value: 2.718281 - -Mathematical constants which value is less than 2.5: -Key: Φ, Name: Golden ratio constant, Value: 1.618033 -Key: ρ, Name: Plastic number ρ (or silver constant), Value: 2.414213 -Key: γ, Name: Euler number, Napier's constant, Value: 0.577215 +kv.Set("pi", Constant{"Archimedes' constant", 3.141592}) +kv.Set("e", Constant{"Euler's number", 2.718281}) +kv.Set("phi", Constant{"Golden ratio", 1.618033}) + +// Keep only the "large" constants (returns a NEW container). +large := kv.FilterValue(func(c Constant) bool { return c.Value > 2.0 }) + +// Sort the survivors by value. +sorted := large.SortValues(func(a, b Constant) bool { return a.Value > b.Value }) +for _, c := range sorted { + fmt.Printf("%s = %v\n", c.Name, c.Value) +} ``` -## How Fast? +## 🧩 API at a glance -Discover it for yourself: +Both containers implement the same methods: + +| Group | Methods | +| ----- | ------- | +| Access | `Set`, `Get`, `GetAndCheck`, `GetOrSet`, `GetAndDelete`, `Delete`, `Clear` | +| Query | `Size`, `IsEmpty`, `ContainsKey`, `ContainsValue` | +| Bulk | `Keys`, `Values`, `All` (iterator), `ForEach`, `ForEachKey`, `ForEachValue` | +| Copy/Combine | `Clone`, `CloneAndClear`, `Merge`, `DeepEqual` | +| Transform | `Map`, `MapKey`, `MapValue`, `Filter`, `FilterKey`, `FilterValue` | +| Split | `Partition`, `PartitionKey`, `PartitionValue` | +| Sort | `SortKeys`, `SortValues` | +| Serialize | `MarshalJSON`, `UnmarshalJSON` | + +Full reference: [pkg.go.dev/github.com/slashdevops/r9e](https://pkg.go.dev/github.com/slashdevops/r9e) + +## 📚 Documentation + +Extensive guides with runnable examples and diagrams live in [`docs/`](docs/): + +| Guide | What it covers | +| ----- | -------------- | +| [Getting Started](docs/getting-started.md) | Install, first program, core types. | +| [Containers](docs/containers.md) | `MapKeyValue` vs `SMapKeyValue`, and how to choose. | +| [Concurrency](docs/concurrency.md) | Thread-safety model and locking rules. | +| [Operations](docs/operations.md) | Map / Filter / Partition / Merge / Clone / sorting. | +| [Iteration](docs/iteration.md) | `All()` iterators and the `ForEach` family. | +| [JSON](docs/json.md) | Marshaling containers to and from JSON. | +| [Performance](docs/performance.md) | Cost model, benchmarks, and tuning. | +| [Migration](docs/migration.md) | **What's New in v1.0.0** and breaking changes. | +| [FAQ](docs/faq.md) | Common questions and gotchas. | + +Runnable examples also live in [`example_test.go`](example_test.go). + +## 🆕 What's New in v1.0.0 + +`v1.0.0` is the first stable release. It modernizes the library for **Go 1.26** +and adds several long-requested capabilities. + +- 🔁 **Range-over-func iterators** — `All()` returns an `iter.Seq2[K, T]`. +- 🧰 **`GetOrSet`** — atomic get-or-insert. +- 🔗 **`Merge`** — combine one container into another. +- 🗄️ **JSON support** — `MarshalJSON` / `UnmarshalJSON` on both containers. +- 🐞 **Correctness fixes**: + - Fixed potential **deadlocks** in `MapKeyValue` caused by recursive read + locking in `Clone`, `Map`, `Filter`, `Partition`, `DeepEqual`, and sorting. + - Fixed `SMapKeyValue.Size()` **over-counting** when overwriting existing keys. + - Fixed a **non-atomic** counter update race in `SMapKeyValue`. +- 🧹 **Modern internals** — uses `maps`, `slices`, the `clear` builtin, and + `sync.Map.Clear`. + +### 💥 Breaking Changes + +| Before | After | Why | +| ------ | ----- | --- | +| `go 1.19` | `go 1.26` | Iterators, `maps`/`slices`, `clear`, `sync.Map.Clear`. | +| `GetAnDelete(...)` | `GetAndDelete(...)` | Fixes a spelling typo in the public API. | +| `IsFull() bool` | *removed* — use `!IsEmpty()` | The old method meant "not empty", which was misleading. | +| `Key(k) K` | *removed* — use `ContainsKey(k)` | The old method returned the key or a zero value; `ContainsKey` is clearer. | +| `SortKeys(...) []*K` | `SortKeys(...) []K` | Returning pointers into internal state was unsafe and unidiomatic. | +| `SortValues(...) []*T` | `SortValues(...) []T` | Same as above. | + +Migration guide with before/after snippets: [docs/migration.md](docs/migration.md). + +## ⚡ Performance + +`r9e` keeps the common operations cheap: + +- `Set`, `Get`, `GetAndCheck`, `GetOrSet`, `Delete`, `Size` are **O(1)**. +- `ContainsValue` and the bulk `Map` / `Filter` / `Partition` / sort operations + are **O(n)**. +- `WithCapacity(n)` presizes `MapKeyValue` to avoid incremental map growth. + +Run the benchmarks yourself: ```bash git clone git@github.com:slashdevops/r9e.git cd r9e/ make bench +# or: +go test -run '^$' -bench . -benchmem ./... +``` + +See [docs/performance.md](docs/performance.md) for the cost model and tuning tips. + +## ✅ Local Quality Gate + +The same checks CI runs: + +```bash +make check # fmt + vet + race tests + build +# individually: +go fmt ./... +go vet ./... +go test -race -covermode=atomic -coverprofile=coverage.txt ./... +go build ./... ``` -## License +## 🤝 Contributing + +Issues and pull requests are welcome at +[github.com/slashdevops/r9e](https://github.com/slashdevops/r9e). Please keep +changes small, idiomatic, tested, documented, and dependency-free unless there +is a clear reason to expand the project scope. See +[.github/copilot-instructions.md](.github/copilot-instructions.md) (also linked +as `AGENTS.md`) for conventions. + +## 📄 License -RamStorage (r9e) is released under the Apache License Version 2.0: +`RamStorage (r9e)` is released under the [Apache License 2.0](LICENSE): -* [http://www.apache.org/licenses/LICENSE-2.0.html](http://www.apache.org/licenses/LICENSE-2.0.html) +- diff --git a/SECURITY.md b/SECURITY.md index e3e124d..217a446 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,19 +1,15 @@ # Security Policy -## Supported Versions +This project uses GitHub CodeQL to scan for security vulnerabilities. -Use this section to tell people about which versions of your project are -currently being supported with security updates. +[![CodeQL Advanced](https://github.com/slashdevops/r9e/actions/workflows/codeql.yml/badge.svg)](https://github.com/slashdevops/r9e/actions/workflows/codeql.yml) -| Version | Supported | -| ------- | ------------------ | -| 0.0.x | :white_check_mark: | +## Supported Versions +| Version | Supported | +| ------- | --------- | +| 1.0.x | Yes | ## Reporting a Vulnerability -Use this section to tell people how to report a vulnerability. - -Tell them where to go, how often they can expect to get an update on a -reported vulnerability, what to expect if the vulnerability is accepted or -declined, etc. +Please report vulnerabilities through GitHub issues or the repository security advisory flow when available. Avoid posting sensitive exploit details publicly before maintainers have had time to respond. diff --git a/additions_test.go b/additions_test.go new file mode 100644 index 0000000..76a3077 --- /dev/null +++ b/additions_test.go @@ -0,0 +1,208 @@ +package r9e + +import ( + "encoding/json" + "maps" + "testing" +) + +func TestGetOrSet_MapKeyValue(t *testing.T) { + kv := NewMapKeyValue[string, int]() + + if v, loaded := kv.GetOrSet("a", 1); v != 1 || loaded { + t.Fatalf("first GetOrSet = (%d, %v), want (1, false)", v, loaded) + } + if v, loaded := kv.GetOrSet("a", 99); v != 1 || !loaded { + t.Fatalf("second GetOrSet = (%d, %v), want (1, true)", v, loaded) + } + if kv.Size() != 1 { + t.Fatalf("Size = %d, want 1", kv.Size()) + } +} + +func TestGetOrSet_SMapKeyValue(t *testing.T) { + kv := NewSMapKeyValue[string, int]() + + if v, loaded := kv.GetOrSet("a", 1); v != 1 || loaded { + t.Fatalf("first GetOrSet = (%d, %v), want (1, false)", v, loaded) + } + if v, loaded := kv.GetOrSet("a", 99); v != 1 || !loaded { + t.Fatalf("second GetOrSet = (%d, %v), want (1, true)", v, loaded) + } + if kv.Size() != 1 { + t.Fatalf("Size = %d, want 1", kv.Size()) + } +} + +func TestSetOverwriteSize_SMapKeyValue(t *testing.T) { + kv := NewSMapKeyValue[string, int]() + + kv.Set("a", 1) + kv.Set("a", 2) + kv.Set("a", 3) + + if kv.Size() != 1 { + t.Fatalf("Size after overwriting same key = %d, want 1", kv.Size()) + } + if got := kv.Get("a"); got != 3 { + t.Fatalf("Get = %d, want 3", got) + } + + kv.Delete("a") + if kv.Size() != 0 { + t.Fatalf("Size after delete = %d, want 0", kv.Size()) + } +} + +func TestMerge_MapKeyValue(t *testing.T) { + a := NewMapKeyValue[string, int]() + a.Set("x", 1) + a.Set("y", 2) + + b := NewMapKeyValue[string, int]() + b.Set("y", 20) + b.Set("z", 30) + + a.Merge(b) + + want := map[string]int{"x": 1, "y": 20, "z": 30} + if got := maps.Collect(a.All()); !maps.Equal(got, want) { + t.Fatalf("after Merge = %v, want %v", got, want) + } + + // nil and self merges are no-ops. + a.Merge(nil) + a.Merge(a) + if a.Size() != 3 { + t.Fatalf("Size after nil/self merge = %d, want 3", a.Size()) + } +} + +func TestMerge_SMapKeyValue(t *testing.T) { + a := NewSMapKeyValue[string, int]() + a.Set("x", 1) + + b := NewSMapKeyValue[string, int]() + b.Set("y", 2) + + a.Merge(b) + if a.Size() != 2 { + t.Fatalf("Size = %d, want 2", a.Size()) + } + a.Merge(nil) + a.Merge(a) + if a.Size() != 2 { + t.Fatalf("Size after nil/self merge = %d, want 2", a.Size()) + } +} + +func TestAll_MapKeyValue(t *testing.T) { + kv := NewMapKeyValue[string, int]() + kv.Set("a", 1) + kv.Set("b", 2) + kv.Set("c", 3) + + got := maps.Collect(kv.All()) + want := map[string]int{"a": 1, "b": 2, "c": 3} + if !maps.Equal(got, want) { + t.Fatalf("All collected = %v, want %v", got, want) + } + + // Early break must stop iteration without panicking. + count := 0 + for range kv.All() { + count++ + break + } + if count != 1 { + t.Fatalf("early-break visited %d entries, want 1", count) + } +} + +func TestAll_SMapKeyValue(t *testing.T) { + kv := NewSMapKeyValue[string, int]() + kv.Set("a", 1) + kv.Set("b", 2) + + got := maps.Collect(kv.All()) + want := map[string]int{"a": 1, "b": 2} + if !maps.Equal(got, want) { + t.Fatalf("All collected = %v, want %v", got, want) + } +} + +func TestJSON_MapKeyValue(t *testing.T) { + kv := NewMapKeyValue[string, int]() + kv.Set("a", 1) + kv.Set("b", 2) + + encoded, err := json.Marshal(kv) + if err != nil { + t.Fatalf("Marshal error: %v", err) + } + if string(encoded) != `{"a":1,"b":2}` { + t.Fatalf("Marshal = %s, want {\"a\":1,\"b\":2}", encoded) + } + + decoded := NewMapKeyValue[string, int]() + if err := json.Unmarshal([]byte(`{"x":10,"y":20}`), decoded); err != nil { + t.Fatalf("Unmarshal error: %v", err) + } + if decoded.Get("x") != 10 || decoded.Get("y") != 20 { + t.Fatalf("decoded = %v, want x=10 y=20", maps.Collect(decoded.All())) + } + + // Empty container marshals to an empty object, not null. + empty := NewMapKeyValue[string, int]() + encoded, _ = json.Marshal(empty) + if string(encoded) != `{}` { + t.Fatalf("empty Marshal = %s, want {}", encoded) + } +} + +func TestJSON_SMapKeyValue(t *testing.T) { + kv := NewSMapKeyValue[string, int]() + kv.Set("a", 1) + + encoded, err := json.Marshal(kv) + if err != nil { + t.Fatalf("Marshal error: %v", err) + } + if string(encoded) != `{"a":1}` { + t.Fatalf("Marshal = %s, want {\"a\":1}", encoded) + } + + decoded := NewSMapKeyValue[string, int]() + if err := json.Unmarshal([]byte(`{"x":10}`), decoded); err != nil { + t.Fatalf("Unmarshal error: %v", err) + } + if decoded.Get("x") != 10 || decoded.Size() != 1 { + t.Fatalf("decoded x=%d size=%d, want x=10 size=1", decoded.Get("x"), decoded.Size()) + } +} + +func TestDeepEqualNil_MapKeyValue(t *testing.T) { + empty := NewMapKeyValue[string, int]() + if !empty.DeepEqual(nil) { + t.Fatal("empty.DeepEqual(nil) = false, want true") + } + + nonEmpty := NewMapKeyValue[string, int]() + nonEmpty.Set("a", 1) + if nonEmpty.DeepEqual(nil) { + t.Fatal("nonEmpty.DeepEqual(nil) = true, want false") + } +} + +func TestDeepEqualNil_SMapKeyValue(t *testing.T) { + empty := NewSMapKeyValue[string, int]() + if !empty.DeepEqual(nil) { + t.Fatal("empty.DeepEqual(nil) = false, want true") + } + + nonEmpty := NewSMapKeyValue[string, int]() + nonEmpty.Set("a", 1) + if nonEmpty.DeepEqual(nil) { + t.Fatal("nonEmpty.DeepEqual(nil) = true, want false") + } +} diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..3d9c658 --- /dev/null +++ b/doc.go @@ -0,0 +1,68 @@ +// Package r9e (RamStorage) provides thread-safe, generic in-memory key-value +// containers built only on the Go standard library. +// +// r9e takes advantage of Go generics and the standard library's own concurrency +// primitives to offer a small, dependency-free API for storing and retrieving +// data from memory. The focus is usability and simplicity without giving up +// performance. +// +// # Containers +// +// The package exposes two containers with the same method set, so code can be +// written against either and switched based on the workload: +// +// - [MapKeyValue] is backed by a native Go map guarded by a [sync.RWMutex]. +// Reads run concurrently under a read lock; writes take the exclusive lock. +// It is the best default for read-heavy or mixed workloads and for +// operations that need a consistent snapshot (Keys, Values, Clone, Map, +// Filter, Partition). +// +// - [SMapKeyValue] is backed by a [sync.Map] with an atomic size counter, so +// Size is O(1). It suits workloads where many goroutines write disjoint +// keys, or where each key is written once and read many times. +// +// # Quick Start +// +// kv := r9e.NewMapKeyValue[string, int]() +// kv.Set("answer", 42) +// +// if v, ok := kv.GetAndCheck("answer"); ok { +// fmt.Println(v) // 42 +// } +// +// for k, v := range kv.All() { +// fmt.Printf("%s=%d\n", k, v) +// } +// +// # Iteration +// +// Both containers implement range-over-func iteration via [MapKeyValue.All] and +// [SMapKeyValue.All], which return an [iter.Seq2] over key-value pairs: +// +// for k, v := range kv.All() { +// // ... +// } +// +// For MapKeyValue the read lock is held for the duration of the loop, so the +// body must not call methods that mutate the same container. Break out of the +// loop early to stop iterating. +// +// # Functional Helpers +// +// Map, MapKey, MapValue, Filter, FilterKey, FilterValue, Partition, +// PartitionKey, and PartitionValue each return new, independent containers and +// never mutate the receiver. Clone, CloneAndClear, and Merge provide snapshot +// and combination semantics. SortKeys and SortValues return sorted slices. +// +// # Choosing a Container +// +// Prefer [MapKeyValue] unless profiling shows that [sync.Map]'s access pattern +// (write-once/read-many, or disjoint keys across many goroutines) fits your +// workload better. When in doubt, benchmark both with your own key and value +// types. +// +// # Dependencies +// +// r9e has zero third-party dependencies. It uses only the Go standard library +// (sync, sync/atomic, iter, maps, slices, reflect, encoding/json). +package r9e diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..3ee7eae --- /dev/null +++ b/docs/README.md @@ -0,0 +1,59 @@ +# 📚 r9e Documentation + +Welcome to the full documentation for **`r9e`** (*RamStorage*) — a thread-safe, +generic, dependency-free in-memory key-value library for Go. + +Everything here is built on the Go standard library only (`sync`, +`sync/atomic`, `iter`, `maps`, `slices`, `reflect`, `encoding/json`). r9e +requires **Go 1.26 or newer**. If you are just getting started, read the guides +in order; otherwise jump straight to the topic you need. + +## 🗂️ Table of Contents + +| Guide | What it covers | +| ----- | -------------- | +| [Getting Started](getting-started.md) | Installation, your first program, the core types, and `WithCapacity`. | +| [Containers](containers.md) | `MapKeyValue` vs `SMapKeyValue`, their internals, and a decision tree. | +| [Concurrency](concurrency.md) | The thread-safety model, read vs write locks, and the iteration deadlock rule. | +| [Operations](operations.md) | Functional helpers: Map, Filter, Partition, Merge, Clone, GetOrSet, sorting. | +| [Iteration](iteration.md) | `All()` range-over-func, the ForEach family, and deterministic ordering. | +| [JSON](json.md) | `MarshalJSON`/`UnmarshalJSON`, struct fields, and key-type constraints. | +| [Performance](performance.md) | Cost model, presizing, container trade-offs, and running benchmarks. | +| [Migration](migration.md) | What's new in v1.0.0 and how to move off older releases. | +| [FAQ](faq.md) | Common questions, gotchas, and thread-safety of returned values. | + +## ⚡ Quick Links + +- Package reference: [pkg.go.dev/github.com/slashdevops/r9e](https://pkg.go.dev/github.com/slashdevops/r9e) +- Runnable examples: [`example_test.go`](../example_test.go) +- Source: [`mapkeyvalue.go`](../mapkeyvalue.go), [`smapkeyvalue.go`](../smapkeyvalue.go) + +## 🧭 Mental Model + +r9e ships **two containers that expose the exact same method set**. You write +your code against one API and pick the backing implementation that fits your +workload — no other code changes. + +```mermaid +flowchart TD + API["Shared API surface
Set · Get · GetOrSet · Delete · Clear
Keys · Values · All · ForEach
Map · Filter · Partition · Merge · Clone
SortKeys · SortValues · MarshalJSON"] + API --> M["MapKeyValue[K, T]
native map + sync.RWMutex"] + API --> S["SMapKeyValue[K, T]
sync.Map + atomic counter"] + M --> MU["Best for: read-heavy / mixed
consistent bulk snapshots"] + S --> SU["Best for: disjoint-key writes
write-once / read-many"] +``` + +Both containers are: + +- **Generic** — `MapKeyValue[K comparable, T any]` and + `SMapKeyValue[K comparable, T any]`. +- **Thread-safe** — safe for concurrent use from many goroutines with no + external locking. +- **Dependency-free** — standard library only. + +When in doubt, start with [`MapKeyValue`](containers.md) and +[benchmark both](performance.md) with your own key and value types. + +## ➡️ Next + +Start with [Getting Started](getting-started.md). diff --git a/docs/concurrency.md b/docs/concurrency.md new file mode 100644 index 0000000..6ff53b8 --- /dev/null +++ b/docs/concurrency.md @@ -0,0 +1,134 @@ +# 🔒 Concurrency + +Both containers are safe for concurrent use by multiple goroutines with **no +external locking**. This guide explains the model, the one rule you must not +break, and the O(1) size counter. + +## 🧵 Thread-safety model + +| Container | Mechanism | +| --------- | --------- | +| `MapKeyValue` | A single `sync.RWMutex` guards the map. Reads take `RLock` (shared); writes take `Lock` (exclusive). | +| `SMapKeyValue` | A `sync.Map` handles concurrency internally; an `atomic.Int64` tracks size. No external lock is ever held. | + +You never call `Lock`/`RLock` yourself — every method acquires whatever it +needs and releases it before returning. + +## 🔁 Shared reads vs exclusive writes (`MapKeyValue`) + +`sync.RWMutex` lets any number of readers proceed together, but a writer must +wait for all readers to finish and then holds the lock alone. + +```mermaid +flowchart TD + subgraph Shared["RLock — shared (reads)"] + R1["Get"] + R2["ContainsKey"] + R3["Keys / Values"] + R4["Size"] + end + subgraph Exclusive["Lock — exclusive (writes)"] + W1["Set"] + W2["Delete"] + W3["Clear"] + W4["Merge (write half)"] + end + Shared -->|"many at once"| OK["proceed concurrently"] + Exclusive -->|"one at a time"| SOLO["all others wait"] +``` + +Read methods (`Get`, `GetAndCheck`, `ContainsKey`, `ContainsValue`, `Size`, +`Keys`, `Values`, `All`, `ForEach*`, `DeepEqual`, the `Map`/`Filter`/`Partition` +family, `MarshalJSON`) use the read lock. Write methods (`Set`, `GetOrSet`, +`GetAndDelete`, `Delete`, `Clear`, `Merge`, `CloneAndClear`, `UnmarshalJSON`) +take the exclusive lock. + +## ⛔ The iteration rule: do not mutate during `All`/`ForEach` + +For **`MapKeyValue`**, `All()` and the `ForEach*` methods hold the **read lock +for the entire duration of the loop**. If the loop body calls a mutating method +on the *same* container, that method blocks forever trying to acquire the +exclusive lock, which the iteration will not release until the body returns — +a classic self-deadlock. + +```mermaid +sequenceDiagram + participant L as Loop body + participant KV as MapKeyValue + participant MU as sync.RWMutex + + KV->>MU: RLock() (held for whole loop) + activate MU + KV->>L: yield (k, v) + L->>KV: Set(k2, v2) ❌ + KV->>MU: Lock() — waits for RUnlock + Note over MU,L: RLock is still held by
the iteration → deadlock + deactivate MU +``` + +**Rule:** inside `All`/`ForEach*` on a `MapKeyValue`, never call `Set`, +`Delete`, `Clear`, `Merge`, `GetOrSet`, `GetAndDelete`, `CloneAndClear`, or +`UnmarshalJSON` on the container being iterated. + +### Safe patterns instead + +Collect the changes first, then apply them after the loop ends: + +```go +// Collect keys to delete during iteration, delete after. +var stale []string +for k, v := range kv.All() { + if v.Expired() { + stale = append(stale, k) + } +} +for _, k := range stale { + kv.Delete(k) // loop is over → read lock released +} +``` + +Or build a new container with a non-mutating helper, which returns a fresh, +independent instance: + +```go +fresh := kv.FilterValue(func(v Session) bool { return !v.Expired() }) +``` + +### `SMapKeyValue` iteration is different + +`SMapKeyValue.All`/`ForEach*` delegate to `sync.Map.Range`, which holds **no +lock across the callback**. Mutating during iteration does not deadlock, but +the iteration reflects a **moment-in-time view**: concurrent writes may or may +not be visited, per `sync.Map.Range` semantics. Do not rely on seeing (or not +seeing) entries written during the scan. + +## 🔢 The atomic size counter + +`SMapKeyValue.Size()` is O(1) because `sync.Map` itself has no length. r9e keeps +an `atomic.Int64` that is adjusted on every mutation: + +```mermaid +flowchart LR + SET["Set (new key)"] -->|"count.Add(+1)"| C[("atomic.Int64")] + GOS["GetOrSet (stored)"] -->|"count.Add(+1)"| C + DEL["Delete / GetAndDelete"] -->|"count.Add(-1)"| C + CLR["Clear"] -->|"count.Store(0)"| C + C -->|"count.Load()"| SIZE["Size() → int"] +``` + +Overwrites are counted correctly: `Set` only increments when the key was newly +inserted, so setting an existing key leaves `Size()` unchanged. + +## 🧱 Consistency of returned values + +- `Keys()`, `Values()`, `SortKeys`, `SortValues` return **fresh slices** you + own — mutating them never affects the container. +- `Clone`, `CloneAndClear`, and the `Map`/`Filter`/`Partition` family return + **new independent containers**. +- Copies are **shallow**: if `T` is a pointer or contains reference types, + copies share the pointed-to data. See the [FAQ](faq.md) for details. + +## ➡️ Next + +- Put the shared API to work in [Operations](operations.md). +- Iterate safely in [Iteration](iteration.md). diff --git a/docs/containers.md b/docs/containers.md new file mode 100644 index 0000000..98793e3 --- /dev/null +++ b/docs/containers.md @@ -0,0 +1,109 @@ +# 🧱 Containers + +r9e provides two containers with the **same method set** but different internal +machinery. This guide explains how each is built and how to choose. + +## 🔬 `MapKeyValue` internals + +`MapKeyValue` wraps a native Go `map` behind a `sync.RWMutex`. Many readers can +hold the read lock at once; a writer takes the lock exclusively. `Size()` is a +plain `len()` of the map under the read lock. + +```mermaid +flowchart LR + subgraph MKV["MapKeyValue[K, T]"] + direction TB + MU["sync.RWMutex
(RLock shared / Lock exclusive)"] + MAP["map[K]T
native Go map"] + MU -. guards .-> MAP + end + R1["reader goroutine"] -->|RLock| MU + R2["reader goroutine"] -->|RLock| MU + W1["writer goroutine"] -->|Lock| MU +``` + +Because reads and writes are serialized through one mutex, bulk operations +(`Keys`, `Values`, `Clone`, `Map`, `Filter`, `Partition`, `All`) observe a +**consistent snapshot** of the map — no entry is half-updated mid-scan. + +## 🔬 `SMapKeyValue` internals + +`SMapKeyValue` wraps a `sync.Map` alongside an `atomic.Int64` entry counter. +`sync.Map` keeps its own internal read/dirty structures and never needs an +external lock. The atomic counter is adjusted on every insert/delete so that +`Size()` is **O(1)** instead of requiring a full `Range`. + +```mermaid +flowchart LR + subgraph SKV["SMapKeyValue[K, T]"] + direction TB + SM["sync.Map
lock-free reads, sharded writes"] + CNT["atomic.Int64
entry counter"] + end + G1["goroutine"] -->|"Set / LoadOrStore"| SM + G1 -->|"Add(+1) on new key"| CNT + G2["goroutine"] -->|"Delete"| SM + G2 -->|"Add(-1)"| CNT + Q["Size()"] -->|"Load() O(1)"| CNT +``` + +The counter is kept accurate across overwrites: `Set` uses `Swap` and only +increments when the key was **not** already present, so overwriting an existing +key does not inflate `Size()`. + +## 📊 Comparison + +| Aspect | `MapKeyValue` | `SMapKeyValue` | +| ------ | ------------- | -------------- | +| Backing store | native `map` + `sync.RWMutex` | `sync.Map` + `atomic.Int64` | +| Constructor | `NewMapKeyValue[K, T](opts...)` | `NewSMapKeyValue[K, T]()` | +| Presizing | `WithCapacity(n)` | not available | +| `Size()` cost | O(1) (`len` under RLock) | O(1) (atomic load) | +| Concurrent reads | shared read lock | lock-free | +| Concurrent writes | serialized by exclusive lock | sharded, low contention on disjoint keys | +| Bulk snapshot consistency | strong (lock held during scan) | moment-in-time (`sync.Map.Range`) | +| Iteration + mutation | ❌ deadlocks (lock held) | ⚠️ allowed, but reflects a live view | +| Best for | read-heavy / mixed; consistent bulk ops | disjoint-key writes; write-once/read-many | + +## 🧭 Which container should I use? + +```mermaid +flowchart TD + Start(["Choosing a container"]) --> Q1{"Many goroutines writing
mostly disjoint keys?"} + Q1 -->|Yes| S["Try SMapKeyValue"] + Q1 -->|No| Q2{"Write-once,
read-many keys?"} + Q2 -->|Yes| S + Q2 -->|No| Q3{"Need consistent bulk
snapshots (Keys, Clone,
Filter, Partition)?"} + Q3 -->|Yes| M["Use MapKeyValue"] + Q3 -->|No| M + S --> B["Benchmark both
with your real types"] + M --> B + B --> Done(["Ship the faster one"]) +``` + +**Default to `MapKeyValue`.** `sync.Map` is optimized for two specific access +patterns — write-once/read-many, and many goroutines updating disjoint key +sets. Outside those patterns a plain mutex-guarded map is usually simpler and +faster. Whichever you lean toward, [benchmark both](performance.md) with your +own key and value types before committing. + +## 🔀 Switching between them + +Since the method sets are identical, swapping is a one-line change: + +```go +// Before +kv := r9e.NewMapKeyValue[string, int]() + +// After — every call site below still compiles unchanged +kv := r9e.NewSMapKeyValue[string, int]() +``` + +If you want to keep call sites backend-agnostic, program against a small local +interface that lists the methods you actually use, and accept either concrete +type. + +## ➡️ Next + +- Understand locking and the iteration rules in [Concurrency](concurrency.md). +- Explore the shared method set in [Operations](operations.md). diff --git a/docs/faq.md b/docs/faq.md new file mode 100644 index 0000000..1113021 --- /dev/null +++ b/docs/faq.md @@ -0,0 +1,115 @@ +# ❓ FAQ + +Common questions, gotchas, and thread-safety notes. If something here is +unclear, the [pkg.go.dev reference](https://pkg.go.dev/github.com/slashdevops/r9e) +has the authoritative per-method documentation. + +## Which container should I use? + +Default to **`MapKeyValue`**. It is a native map behind a `sync.RWMutex`: +simple, fast for read-heavy and mixed workloads, and it gives consistent +snapshots for bulk operations. Reach for **`SMapKeyValue`** only when your +access pattern matches what `sync.Map` is optimized for — many goroutines +writing **disjoint** keys, or keys written **once and read many** times. When +unsure, [benchmark both](performance.md). See [Containers](containers.md) for +the full decision tree. + +## Is it safe for concurrent use? + +Yes. Both containers are safe for use by multiple goroutines with **no external +locking**. Every method acquires whatever synchronization it needs internally. +See [Concurrency](concurrency.md). + +## Can I iterate and modify at the same time? + +**Not on `MapKeyValue`.** `All()` and `ForEach*` hold the read lock for the +whole loop, so calling a mutating method (`Set`, `Delete`, `Clear`, `Merge`, +`GetOrSet`, `GetAndDelete`, `CloneAndClear`) on the *same* container from inside +the loop **deadlocks**. Collect the changes and apply them after the loop, or +build a new container with `Filter`/`Map`. + +**On `SMapKeyValue`** mutation during iteration does not deadlock, but the scan +reflects a *moment-in-time* view (`sync.Map.Range` semantics): concurrent writes +may or may not be visited. Don't rely on either outcome. See +[Iteration](iteration.md). + +## What key and value types are allowed? + +- **Keys** — any `comparable` type: strings, all integer/float kinds, `bool`, + pointers, and comparable structs/arrays. +- **Values** — literally `any` type, including structs, slices, maps, pointers, + and interfaces. + +For **JSON** there is one extra restriction: `encoding/json` only supports map +keys that are strings, integers, or `encoding.TextMarshaler` implementations. A +container with, say, a struct key stores and retrieves fine but cannot be +JSON-encoded. See [JSON](json.md). + +## Does it persist to disk? + +**No.** r9e is purely in-memory — the name stands for *RamStorage*. Data lives +for the lifetime of the process and disappears when it exits. To persist, encode +the container (e.g. with [JSON](json.md)) and write the bytes wherever you like; +r9e itself never touches the filesystem, the network, or any external store. + +## Are the slices and containers returned by methods safe to use? + +Yes — they are independent copies you fully own: + +- `Keys()`, `Values()`, `SortKeys`, `SortValues` return **fresh slices**. + Mutating them never affects the source container. +- `Clone`, `CloneAndClear`, and the `Map`/`Filter`/`Partition` family return + **new independent containers**. The receiver is unchanged (except + `CloneAndClear`, which clears it). + +One caveat: copies are **shallow**. If `T` is a pointer or contains reference +types (slices, maps, pointers), the copy shares the pointed-to data with the +original. Mutating that shared data is visible through both. + +```go +type Bag struct{ Items []string } + +kv := r9e.NewMapKeyValue[string, *Bag]() +kv.Set("a", &Bag{Items: []string{"x"}}) + +clone := kv.Clone() +clone.Get("a").Items[0] = "MUTATED" // both see it — same *Bag +``` + +Store values instead of pointers, or deep-copy inside a `MapValue`, if you need +full isolation. + +## Why is `Get` returning zero for a key I never set? + +`Get` returns the **zero value** of `T` for absent keys, which is +indistinguishable from a stored zero. Use `GetAndCheck` to tell them apart: + +```go +v, ok := kv.GetAndCheck("maybe") +if !ok { + // truly absent +} +``` + +## Why is iteration order different every run? + +Both containers are unordered, and Go randomizes map iteration order on purpose. +For stable output, sort first with `SortKeys` / `SortValues`. See +[Iteration](iteration.md#-deterministic-iteration). + +## How do I count entries cheaply? + +`Size()` is **O(1)** on both containers — a `len()` under the read lock for +`MapKeyValue`, and an atomic load for `SMapKeyValue`. `IsEmpty()` is just +`Size() == 0`. Avoid `ContainsValue`, which is O(n). + +## Does r9e have any third-party dependencies? + +No. It uses only the Go standard library (`sync`, `sync/atomic`, `iter`, `maps`, +`slices`, `reflect`, `encoding/json`) and requires **Go 1.26+**. + +## ➡️ Next + +- Back to the [documentation index](README.md). +- Deep-dive the internals in [Containers](containers.md) and + [Concurrency](concurrency.md). diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..d6faca8 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,134 @@ +# 🚀 Getting Started + +## 📦 Installation + +```bash +go get github.com/slashdevops/r9e +``` + +Update to the latest available version: + +```bash +go get -u github.com/slashdevops/r9e +``` + +`r9e` requires **Go 1.26 or newer** and has **zero third-party dependencies** — +only the Go standard library. + +## 👋 Your First Program + +```go +package main + +import ( + "fmt" + + "github.com/slashdevops/r9e" +) + +func main() { + kv := r9e.NewMapKeyValue[string, int]() + + kv.Set("answer", 42) + + if v, ok := kv.GetAndCheck("answer"); ok { + fmt.Println(v) // 42 + } + + for k, v := range kv.All() { + fmt.Printf("%s=%d\n", k, v) + } +} +``` + +`NewMapKeyValue[string, int]()` constructs a container whose keys are `string` +and whose values are `int`. Everything is type-checked at compile time — there +are no `interface{}`/`any` casts in your code and no runtime type assertions. + +## 🧱 Core Types + +r9e exposes two containers with an **identical method set**: + +| Type | Backed by | Constructor | +| ---- | --------- | ----------- | +| `MapKeyValue[K comparable, T any]` | native Go `map` + `sync.RWMutex` | `r9e.NewMapKeyValue[K, T](opts...)` | +| `SMapKeyValue[K comparable, T any]` | `sync.Map` + atomic size counter | `r9e.NewSMapKeyValue[K, T]()` | + +Because the API is the same, you can prototype against `MapKeyValue` and switch +to `SMapKeyValue` later without touching your call sites. See +[Containers](containers.md) for how to choose. + +### The type parameters + +- **`K comparable`** — the key type. Any comparable type works: `string`, + every integer/float kind, `bool`, pointers, and comparable structs/arrays. +- **`T any`** — the value type. Any type at all, including structs, slices, + maps, pointers, and interfaces. + +```go +users := r9e.NewMapKeyValue[int, User]() // int keys, struct values +config := r9e.NewMapKeyValue[string, string]() // string → string +tallies := r9e.NewSMapKeyValue[string, int]() // sync.Map-backed counter +``` + +## 🔁 A Set → Get Round Trip + +The following sequence shows what happens inside a `MapKeyValue` on a `Set` +followed by a `Get`. Writes take the exclusive lock; reads take a shared read +lock (covered in depth in [Concurrency](concurrency.md)). + +```mermaid +sequenceDiagram + participant C as Caller + participant KV as MapKeyValue + participant MU as sync.RWMutex + participant M as map[K]T + + C->>KV: Set("answer", 42) + KV->>MU: Lock() (exclusive) + KV->>M: data["answer"] = 42 + KV->>MU: Unlock() + KV-->>C: (done) + + C->>KV: Get("answer") + KV->>MU: RLock() (shared) + KV->>M: read data["answer"] + M-->>KV: 42 + KV->>MU: RUnlock() + KV-->>C: 42 +``` + +## 🎛️ Presizing with `WithCapacity` + +When you know roughly how many entries you will store, presize the underlying +map at construction time to avoid incremental reallocation as it grows. This is +a `MapKeyValue`-only option. + +```go +// Presize for ~10,000 entries. +kv := r9e.NewMapKeyValue[string, int](r9e.WithCapacity(10_000)) + +for i := range 10_000 { + kv.Set(fmt.Sprintf("key-%d", i), i) +} +``` + +`WithCapacity` is a **performance hint** only — it changes nothing about +behavior or semantics, and the container still grows on demand if you exceed +the hint. `SMapKeyValue` takes no options; `sync.Map` manages its own storage. + +## 🧪 Distinguishing "absent" from "zero" + +`Get` returns the zero value of `T` when a key is absent, which is +indistinguishable from a stored zero. Use `GetAndCheck` when you need to know +whether the key existed: + +```go +v := kv.Get("missing") // 0 — but was it stored as 0, or absent? +v, ok := kv.GetAndCheck("missing") // ok == false means truly absent +``` + +## ➡️ Next + +- Learn the difference between the two backends in [Containers](containers.md). +- Understand the locking model in [Concurrency](concurrency.md). diff --git a/docs/iteration.md b/docs/iteration.md new file mode 100644 index 0000000..3db1b8e --- /dev/null +++ b/docs/iteration.md @@ -0,0 +1,112 @@ +# 🔁 Iteration + +There are three ways to walk a container: the modern `All()` range-over-func +iterator, the `ForEach*` callbacks, and materialized `Keys()`/`Values()` +slices. All of them produce entries in **unspecified order**. + +## ✨ `All()` — range-over-func (preferred) + +`All()` returns an `iter.Seq2[K, T]`, so you can range over it directly with +the Go 1.23+ range-over-func feature: + +```go +for k, v := range kv.All() { + fmt.Printf("%s = %d\n", k, v) +} +``` + +Break early to stop; the iterator stops yielding as soon as the loop exits. + +```go +for k, v := range kv.All() { + if v > threshold { + fmt.Println("found", k) + break // stops iteration cleanly + } +} +``` + +### How the yield loop works + +A range-over-func iterator is a function that calls `yield(k, v)` for each pair. +Ranging over it drives that function; returning `false` from `yield` (which the +`for` loop does on `break`/`return`) tells it to stop. + +```mermaid +flowchart TD + Start(["for k, v := range kv.All()"]) --> Next{"more entries?"} + Next -->|no| Done(["loop ends"]) + Next -->|yes| Yield["yield(k, v) → run loop body"] + Yield --> Cont{"loop continued?
(no break/return)"} + Cont -->|yes| Next + Cont -->|no| Stop["yield returns false"] + Stop --> Done +``` + +> ⛔ **`MapKeyValue` only:** the read lock is held for the entire loop, so the +> body must not mutate the same container. See the deadlock rule in +> [Concurrency](concurrency.md). `SMapKeyValue` iteration reflects a +> moment-in-time view and does not lock across the body. + +## 📞 The `ForEach` family + +Older callback-style iteration. Prefer `All()` in new code, but these remain +useful when you already have a function to apply. + +```go +kv.ForEach(func(k string, v int) { + fmt.Printf("%s=%d\n", k, v) +}) + +kv.ForEachKey(func(k string) { /* keys only */ }) +kv.ForEachValue(func(v int) { /* values only */ }) +``` + +The same "don't mutate during iteration" rule applies to `MapKeyValue`. + +## 📦 `Keys()` and `Values()` — materialized snapshots + +When you want a slice you fully own (safe to mutate, sort, or pass around) and +you do not need to iterate lazily: + +```go +keys := kv.Keys() // []K, unordered snapshot +values := kv.Values() // []T, unordered snapshot +``` + +These allocate a new slice each call. Because they return a snapshot rather than +holding a lock, you can freely mutate the container afterward. + +## 🔢 Deterministic iteration + +Map order is randomized in Go, so none of the above is stable across runs. For +reproducible output, sort first with `SortKeys` and look values up as you go: + +```go +kv := r9e.NewMapKeyValue[string, int]() +kv.Set("c", 3); kv.Set("a", 1); kv.Set("b", 2) + +for _, k := range kv.SortKeys(func(a, b string) bool { return a < b }) { + fmt.Printf("%s=%d\n", k, kv.Get(k)) +} +// a=1 +// b=2 +// c=3 +``` + +This pattern reads from the container inside the loop, which is a **read**, so +it is safe on both containers — no mutation occurs. + +## 🧭 Choosing an iteration style + +| Need | Use | +| ---- | --- | +| Idiomatic loop, lazy, early break | `All()` | +| Apply an existing callback | `ForEach` / `ForEachKey` / `ForEachValue` | +| A slice you own to sort or pass on | `Keys()` / `Values()` | +| Stable, reproducible order | `SortKeys` / `SortValues` | + +## ➡️ Next + +- Serialize the collection with [JSON](json.md). +- Understand the cost of each walk in [Performance](performance.md). diff --git a/docs/json.md b/docs/json.md new file mode 100644 index 0000000..3bb49ea --- /dev/null +++ b/docs/json.md @@ -0,0 +1,109 @@ +# 🗄️ JSON + +Both containers implement `json.Marshaler` and `json.Unmarshaler`, so they +encode to and decode from a plain JSON **object** — exactly like the underlying +`map[K]T` would. + +## 📤 Marshalling + +```go +kv := r9e.NewMapKeyValue[string, int]() +kv.Set("a", 1) +kv.Set("b", 2) + +encoded, _ := json.Marshal(kv) +fmt.Println(string(encoded)) // {"a":1,"b":2} +``` + +`MarshalJSON` takes a consistent snapshot (under the read lock for +`MapKeyValue`) and encodes it, so it is safe to call while other goroutines +read the container. + +```mermaid +flowchart LR + KV["MapKeyValue / SMapKeyValue"] -->|"MarshalJSON"| MAP["map[K]T snapshot"] + MAP -->|"json.Marshal"| JSON["{\"a\":1,\"b\":2}"] + JSON -->|"UnmarshalJSON"| KV2["container
(entries merged in)"] +``` + +## 📥 Unmarshalling + +`UnmarshalJSON` decodes the JSON object and **merges** the decoded entries over +any existing ones (it does not clear the container first). Construct the +container before unmarshalling into it. + +```go +decoded := r9e.NewMapKeyValue[string, int]() +_ = json.Unmarshal([]byte(`{"x":10,"y":20}`), decoded) + +fmt.Println(decoded.Get("x"), decoded.Get("y")) // 10 20 +``` + +## 🧩 As a struct field + +Because the container marshals like a map, you can embed it directly in a struct +and it serializes as a nested object: + +```go +type Server struct { + Name string `json:"name"` + Labels *r9e.MapKeyValue[string, string] `json:"labels"` +} + +s := Server{ + Name: "web-1", + Labels: r9e.NewMapKeyValue[string, string](), +} +s.Labels.Set("env", "prod") +s.Labels.Set("tier", "frontend") + +out, _ := json.Marshal(s) +// {"name":"web-1","labels":{"env":"prod","tier":"frontend"}} +``` + +When decoding into such a struct, make sure the field is non-nil first (e.g. +initialize it, or the surrounding decode allocates it) so `UnmarshalJSON` has a +container to merge into. + +## 🔑 Key-type constraints + +JSON object keys are always strings, so `encoding/json` only accepts key types +it can turn into (and back from) a string. Marshalling **succeeds only** when +`K` is one of: + +| Key type `K` | JSON key example | Works? | +| ------------ | ---------------- | ------ | +| `string` | `"env"` | ✅ | +| integer kinds (`int`, `int64`, `uint`, ...) | `"42"` | ✅ (quoted) | +| type implementing `encoding.TextMarshaler` | its `MarshalText` output | ✅ | +| `bool`, `float`, struct, pointer, ... | — | ❌ marshalling errors | + +This is a constraint of `encoding/json`, not of r9e — the same rules apply to a +raw `map[K]T`. Value type `T` may be anything JSON can encode. + +```go +// int keys marshal as quoted strings, per encoding/json +ports := r9e.NewMapKeyValue[int, string]() +ports.Set(80, "http") +ports.Set(443, "https") + +b, _ := json.Marshal(ports) +fmt.Println(string(b)) // {"443":"https","80":"http"} +``` + +## ⚠️ Error handling + +Do not ignore the error from `json.Marshal`/`json.Unmarshal` in production code +— an unsupported key type, or malformed input, is reported there: + +```go +b, err := json.Marshal(kv) +if err != nil { + return fmt.Errorf("encode store: %w", err) +} +``` + +## ➡️ Next + +- Tune throughput and memory in [Performance](performance.md). +- Browse common questions in the [FAQ](faq.md). diff --git a/docs/migration.md b/docs/migration.md new file mode 100644 index 0000000..aee2ab3 --- /dev/null +++ b/docs/migration.md @@ -0,0 +1,157 @@ +# 🚦 Migration + +This guide covers what changed in **v1.0.0** and how to move existing code onto +it. v1.0.0 is a cleanup-and-modernize release: the API is smaller, more +consistent, and built on newer standard-library facilities. + +## ✨ What's new in v1.0.0 + +- **Range-over-func iteration** — `All()` returns an `iter.Seq2[K, T]` so you + can write `for k, v := range kv.All() { ... }`. See [Iteration](iteration.md). +- **`GetOrSet`** — atomic get-or-insert on both containers. +- **`Merge`** — copy entries from another container, overwriting, with + nil/self-merge treated as a safe no-op. +- **JSON support** — `MarshalJSON`/`UnmarshalJSON` on both containers; use a + container directly as a struct field. See [JSON](json.md). +- **Deadlock fixes in the `MapKeyValue` (RWMutex) container** — cross-container + operations (`Merge`, `DeepEqual`, `CloneAndClear`) now take a snapshot instead + of locking two containers at once, removing lock-ordering deadlocks. +- **Modern stdlib internals** — `maps` (Copy), `slices` (SortFunc), the builtin + `clear`, and `sync.Map.Clear` power the implementation. +- **Accurate `SMapKeyValue.Size()`** — an atomic counter keeps `Size()` O(1) + and correct across overwrites. + +## 💥 Breaking changes + +| Change | Before | After | +| ------ | ------ | ----- | +| **Go version** | Go 1.19 | **Go 1.26+** required | +| **Typo fixed** | `GetAnDelete` | `GetAndDelete` | +| **Removed `IsFull()`** | `kv.IsFull()` | use `!kv.IsEmpty()` | +| **Removed `Key()`** | `kv.Key()` | removed — use `Keys()` / `ContainsKey` | +| **`SortKeys` return type** | `[]*K` | `[]K` | +| **`SortValues` return type** | `[]*T` | `[]T` | +| **`SMapKeyValue.Size()`** | double-counted overwrites | accurate on overwrite | + +### 1. Requires Go 1.26+ + +Update your toolchain and `go.mod`: + +```go +// go.mod +go 1.26.0 +``` + +The library uses generics, range-over-func iterators (`iter.Seq2`), +`sync.Map.Clear`, and the `maps`/`slices` packages, which require a recent +toolchain. + +### 2. `GetAnDelete` → `GetAndDelete` + +A spelling fix. Update call sites: + +```go +// Before +v, ok := kv.GetAnDelete("token") + +// After +v, ok := kv.GetAndDelete("token") +``` + +### 3. `IsFull()` removed + +There was never a capacity ceiling to be "full" against. Invert the emptiness +check instead: + +```go +// Before +if kv.IsFull() { ... } + +// After +if !kv.IsEmpty() { ... } +``` + +### 4. `Key()` removed + +Use `Keys()` for the full snapshot, or `ContainsKey` to test membership: + +```go +// Before +ks := kv.Key() + +// After +ks := kv.Keys() // []K snapshot +present := kv.ContainsKey("a") // membership test +``` + +### 5. `SortKeys` / `SortValues` return values, not pointers + +They now return `[]K` / `[]T` directly — no more dereferencing. + +```go +// Before +for _, kp := range kv.SortKeys(less) { + fmt.Println(*kp) +} + +// After +for _, k := range kv.SortKeys(func(a, b string) bool { return a < b }) { + fmt.Println(k) +} +``` + +### 6. `SMapKeyValue.Size()` is now accurate on overwrite + +Previously, re-setting an existing key inflated the reported size. Now `Set` +only increments the counter for genuinely new keys. + +```go +sm := r9e.NewSMapKeyValue[string, int]() +sm.Set("a", 1) +sm.Set("a", 2) // overwrite +sm.Set("b", 3) +fmt.Println(sm.Size()) // 2 (was 3 in older versions) +``` + +If any code compensated for the old double-counting bug, remove that +workaround. + +## 🔁 Before / after at a glance + +```mermaid +flowchart LR + subgraph OLD["Pre-1.0.0"] + A1["GetAnDelete"] + A2["IsFull()"] + A3["Key()"] + A4["SortKeys → []*K"] + A5["Size double-counts"] + end + subgraph NEW["v1.0.0"] + B1["GetAndDelete"] + B2["!IsEmpty()"] + B3["Keys() / ContainsKey"] + B4["SortKeys → []K"] + B5["Size accurate + All(), GetOrSet, Merge, JSON"] + end + A1 --> B1 + A2 --> B2 + A3 --> B3 + A4 --> B4 + A5 --> B5 +``` + +## ✅ Migration checklist + +1. Bump the toolchain to **Go 1.26+** and update `go.mod`. +2. Rename `GetAnDelete` → `GetAndDelete`. +3. Replace `IsFull()` with `!IsEmpty()`. +4. Replace `Key()` with `Keys()` or `ContainsKey`. +5. Drop pointer dereferences on `SortKeys`/`SortValues` results. +6. Remove any workaround for the old `SMapKeyValue.Size()` overcount. +7. `go build ./...` and `go test ./...` to confirm the tree is clean. + +## ➡️ Next + +- Adopt the new iterators in [Iteration](iteration.md). +- Try `GetOrSet` and `Merge` in [Operations](operations.md). diff --git a/docs/operations.md b/docs/operations.md new file mode 100644 index 0000000..1224762 --- /dev/null +++ b/docs/operations.md @@ -0,0 +1,162 @@ +# 🧰 Operations + +Beyond `Set`/`Get`, both containers share a set of functional helpers. A key +property runs through all of them: + +> **Non-mutating helpers return new, independent containers.** `Map`, `MapKey`, +> `MapValue`, `Filter`, `FilterKey`, `FilterValue`, `Partition`, +> `PartitionKey`, `PartitionValue`, `Clone`, and `CloneAndClear` never modify +> the receiver. + +## 🔎 Reads and existence + +```go +kv := r9e.NewMapKeyValue[string, int]() +kv.Set("a", 1) + +v := kv.Get("a") // 1 (zero value if absent) +v, ok := kv.GetAndCheck("a") // 1, true +has := kv.ContainsKey("a") // true +hasV := kv.ContainsValue(1) // true — O(n), uses reflect.DeepEqual +n := kv.Size() // 1 +empty := kv.IsEmpty() // false +``` + +## ➕ `GetOrSet` — atomic get-or-insert + +Returns the existing value if the key is present, otherwise stores and returns +the new one. The check-and-store happens atomically under a single lock, so it +is race-free even when many goroutines call it at once. + +```go +v, loaded := kv.GetOrSet("hits", 1) +// first call: v == 1, loaded == false (stored) +// later call: v == 1, loaded == true (existing value returned, arg ignored) +``` + +## 🗑️ `GetAndDelete` — remove and return + +```go +value, loaded := kv.GetAndDelete("token") +// loaded reports whether the key was present; the entry is now gone +``` + +## 🧬 `Merge` — copy entries in + +Copies every entry from `other` into the receiver, **overwriting** keys that +already exist. `other` is read via a consistent snapshot, so the two containers +are never locked at the same time. A `nil` `other`, or merging a container into +itself, is a safe no-op. + +```go +a := r9e.NewMapKeyValue[string, int]() +a.Set("x", 1); a.Set("y", 2) + +b := r9e.NewMapKeyValue[string, int]() +b.Set("y", 20); b.Set("z", 30) + +a.Merge(b) // a is now {x:1, y:20, z:30} — b is unchanged +``` + +## 🪞 `Clone` and `CloneAndClear` + +```go +snapshot := kv.Clone() // new independent copy; kv unchanged +drained := kv.CloneAndClear() // copy returned, receiver left empty (atomically for MapKeyValue) +``` + +`CloneAndClear` on `MapKeyValue` copies and clears under a single exclusive +lock. On `SMapKeyValue` the copy and clear are two steps — see the doc comment +if concurrent writers may race the transition. + +## 🔧 `Map`, `MapKey`, `MapValue` — transform + +Each returns a new container; the receiver is untouched. + +```go +kv := r9e.NewMapKeyValue[string, int]() +kv.Set("a", 1); kv.Set("b", 2) + +doubled := kv.MapValue(func(v int) int { return v * 2 }) // {a:2, b:4} +upper := kv.MapKey(func(k string) string { return strings.ToUpper(k) }) // {A:1, B:2} +both := kv.Map(func(k string, v int) (string, int) { // {a!:10, b!:20} + return k + "!", v * 10 +}) +``` + +> ⚠️ With `Map`/`MapKey`, if your function produces the same key for two inputs, +> the later write wins and the result has fewer entries — exactly like assigning +> to a Go map. + +## 🧹 `Filter`, `FilterKey`, `FilterValue` — select + +Keep only the pairs for which the predicate returns `true`. + +```go +kv := r9e.NewMapKeyValue[string, int]() +kv.Set("one", 1); kv.Set("two", 2); kv.Set("three", 3); kv.Set("four", 4) + +even := kv.FilterValue(func(v int) bool { return v%2 == 0 }) // {two:2, four:4} +long := kv.FilterKey(func(k string) bool { return len(k) > 3 }) // {three:3, four:4} +``` + +## ✂️ `Partition`, `PartitionKey`, `PartitionValue` — split in two + +Returns **two** new containers: `match` (predicate true) and `others` (the +rest). Every entry lands in exactly one of them. + +```mermaid +flowchart LR + KV["MapKeyValue
{a:10, b:20, c:30}"] --> P{"Partition:
v >= 20 ?"} + P -->|true| MATCH["match
{b:20, c:30}"] + P -->|false| OTHERS["others
{a:10}"] +``` + +```go +kv := r9e.NewMapKeyValue[string, int]() +kv.Set("a", 10); kv.Set("b", 20); kv.Set("c", 30) + +big, small := kv.Partition(func(_ string, v int) bool { return v >= 20 }) +// big: {b:20, c:30} +// small: {a:10} +``` + +## 🔀 `SortKeys` and `SortValues` — ordered slices + +Because the containers are unordered, sorting produces a **slice** (not a new +container) ordered by a `less` function. `SortKeys` returns `[]K`; +`SortValues` returns `[]T`. + +```go +kv := r9e.NewMapKeyValue[string, float64]() +kv.Set("pi", 3.14); kv.Set("e", 2.71); kv.Set("phi", 1.61) + +keys := kv.SortKeys(func(a, b string) bool { return a < b }) // [e phi pi] +vals := kv.SortValues(func(a, b float64) bool { return a < b }) // [1.61 2.71 3.14] +``` + +## 🟰 `DeepEqual` — structural comparison + +Reports whether two containers hold the same keys mapped to deeply equal values +(`reflect.DeepEqual`). The two containers are never locked simultaneously. + +```go +same := kv.DeepEqual(other) // true only if keys and values all match +``` + +## 📋 Cheat sheet + +| Method | Mutates receiver? | Returns | +| ------ | ----------------- | ------- | +| `Set`, `Delete`, `Clear`, `Merge` | ✅ yes | — | +| `GetOrSet`, `GetAndDelete` | ✅ yes | value (+ bool) | +| `CloneAndClear` | ✅ clears | new container | +| `Get`, `GetAndCheck`, `ContainsKey/Value`, `Size`, `IsEmpty` | ❌ no | value / bool / int | +| `Clone`, `Map*`, `Filter*`, `Partition*` | ❌ no | new container(s) | +| `Keys`, `Values`, `SortKeys`, `SortValues` | ❌ no | new slice | +| `DeepEqual` | ❌ no | bool | + +## ➡️ Next + +- Walk the collection with [Iteration](iteration.md). +- Serialize it with [JSON](json.md). diff --git a/docs/performance.md b/docs/performance.md new file mode 100644 index 0000000..5f4fc01 --- /dev/null +++ b/docs/performance.md @@ -0,0 +1,116 @@ +# ⚡ Performance + +r9e is a thin, allocation-conscious layer over the standard library's own map +and `sync.Map`. Its cost is dominated by those primitives plus the +synchronization needed to make them safe. This guide covers the cost model, how +to keep it low, and how to measure. + +## 🧮 Cost model + +| Operation | Cost | +| --------- | ---- | +| `Set`, `Get`, `GetAndCheck`, `GetOrSet`, `Delete`, `GetAndDelete`, `ContainsKey` | **O(1)** | +| `Size`, `IsEmpty` | **O(1)** (`len` under RLock, or an atomic load) | +| `Clear` | O(1)–O(n) depending on the runtime | +| `ContainsValue` | **O(n)** — scans values with `reflect.DeepEqual` | +| `Keys`, `Values`, `All`, `ForEach*`, `Clone`, `Merge`, `DeepEqual` | **O(n)** | +| `Map*`, `Filter*`, `Partition*` | **O(n)** + allocates a new container | +| `SortKeys`, `SortValues` | **O(n log n)** | +| `MarshalJSON`, `UnmarshalJSON` | **O(n)** | + +The single-key operations are constant time. Everything that touches every +entry is linear and allocates a fresh slice or container. + +## 🔐 Where the time goes + +The overhead on top of the raw map is synchronization. The two backends trade +off differently under contention: + +```mermaid +flowchart TD + subgraph MKV["MapKeyValue — one RWMutex"] + RA["reader"] --> MU["RWMutex"] + RB["reader"] --> MU + WA["writer"] --> MU + MU --> Note1["reads share · writes serialize
a hot writer blocks all readers"] + end + subgraph SKV["SMapKeyValue — sync.Map"] + GA["goroutine A → key set 1"] --> SM["sync.Map"] + GB["goroutine B → key set 2"] --> SM + SM --> Note2["lock-free reads · sharded writes
shines on disjoint keys"] + end +``` + +- **`MapKeyValue`** is excellent for read-heavy and mixed workloads, and gives + strong snapshot consistency for bulk ops. A single frequently-writing + goroutine can become a bottleneck because writes are serialized. +- **`SMapKeyValue`** avoids that serialization when goroutines touch disjoint + keys or when keys are written once and read many times — the patterns + `sync.Map` is built for. Outside those, its bookkeeping overhead can make it + slower than a plain mutexed map. + +## 📏 Presizing with `WithCapacity` + +If you know the approximate final size of a `MapKeyValue`, presize it once to +avoid repeated rehashing as it grows: + +```go +kv := r9e.NewMapKeyValue[string, int](r9e.WithCapacity(100_000)) +``` + +This is a hint, not a hard cap — the map still grows past it if needed. It has +no effect on `SMapKeyValue`, which manages its own storage. + +## 💡 Tuning levers + +| Lever | Effect | +| ----- | ------ | +| Presize with `WithCapacity` | Avoids incremental map growth for `MapKeyValue`. | +| Prefer O(1) lookups over `ContainsValue` | Key an index instead of scanning values. | +| Reuse a container; call `Clear` | Retains capacity, avoids re-allocating. | +| Batch reads, minimize write frequency | Fewer exclusive-lock acquisitions on `MapKeyValue`. | +| Choose the backend that fits the access pattern | See [Containers](containers.md). | +| Avoid bulk ops in hot paths | `Keys`/`Values`/`Clone`/`Map`/`Filter` all allocate O(n). | + +## 🏁 Benchmarks + +The test suite includes benchmarks for both containers. Run them with: + +```bash +go test -run '^$' -bench . ./... +``` + +Add memory allocation stats: + +```bash +go test -run '^$' -bench . -benchmem ./... +``` + +The `-run '^$'` selects no unit tests, so only benchmarks execute. Benchmarks +use the Go 1.24+ `for b.Loop()` form, which excludes per-benchmark setup from +the timed region automatically: + +```go +func BenchmarkSet(b *testing.B) { + kv := r9e.NewMapKeyValue[int, int]() + for b.Loop() { + kv.Set(1, 1) + } +} +``` + +## 🧭 Practical guidance + +1. **Default to `MapKeyValue`.** Switch to `SMapKeyValue` only when its access + pattern (disjoint-key writes, or write-once/read-many) fits. +2. **Benchmark both** with your real key and value types before committing — + micro-benchmark intuition rarely survives contact with a real workload. +3. **Presize** with `WithCapacity` when the size is known. +4. **Index, don't scan.** Replace `ContainsValue` (O(n)) with a secondary + container keyed by the value you look up. +5. **Keep bulk operations out of hot loops** — they allocate O(n) each call. + +## ➡️ Next + +- Understand the backend trade-offs in [Containers](containers.md). +- Common questions and gotchas: [FAQ](faq.md). diff --git a/example_test.go b/example_test.go index 3ac52de..b620de6 100644 --- a/example_test.go +++ b/example_test.go @@ -1,117 +1,174 @@ package r9e_test import ( + "encoding/json" "fmt" + "maps" "github.com/slashdevops/r9e" ) -func ExampleMapKeyValue_basic() { - type MathematicalConstants struct { - Name string - Value float64 - } +// Store and read back a value. +func ExampleMapKeyValue() { + kv := r9e.NewMapKeyValue[string, int]() - // With Capacity allocated - // kv := r9e.NewMapKeyValue[string, MathematicalConstants](r9e.WithCapacity(5)) - kv := r9e.NewMapKeyValue[string, MathematicalConstants]() - - kv.Set("pi", MathematicalConstants{"Archimedes' constant", 3.141592}) - kv.Set("e", MathematicalConstants{"Euler number, Napier's constant", 2.718281}) - kv.Set("γ", MathematicalConstants{"Euler number, Napier's constant", 0.577215}) - kv.Set("Φ", MathematicalConstants{"Golden ratio constant", 1.618033}) - kv.Set("ρ", MathematicalConstants{"Plastic number ρ (or silver constant)", 2.414213}) - - kvFilteredValues := kv.FilterValue(func(value MathematicalConstants) bool { - return value.Value > 2.0 - }) - - fmt.Println("Mathematical Constants:") - kvFilteredValues.ForEach(func(key string, value MathematicalConstants) { - fmt.Printf("Key: %v, Name: %v, Value: %v\n", key, value.Name, value.Value) - }) - - fmt.Printf("\n") - fmt.Printf("The most famous mathematical constant:\n") - fmt.Printf("Name: %v, Value: %v\n", kv.Get("pi").Name, kv.Get("pi").Value) - - lst := kv.SortValues(func(value1, value2 MathematicalConstants) bool { - return value1.Value > value2.Value - }) - - fmt.Printf("\n") - fmt.Printf("The most famous mathematical constant sorted by value:\n") - for i, value := range lst { - fmt.Printf("i: %v, Name: %v, Value: %v\n", i, value.Name, value.Value) + kv.Set("answer", 42) + + if v, ok := kv.GetAndCheck("answer"); ok { + fmt.Println(v) } + // Output: 42 +} - kvHigh, kvLow := kv.Partition(func(key string, value MathematicalConstants) bool { - return value.Value > 2.5 - }) - - fmt.Printf("\n") - fmt.Printf("Mathematical constants which value is greater than 2.5:\n") - kvHigh.ForEach(func(key string, value MathematicalConstants) { - fmt.Printf("Key: %v, Name: %v, Value: %v\n", key, value.Name, value.Value) - }) - - fmt.Printf("\n") - fmt.Printf("Mathematical constants which value is less than 2.5:\n") - kvLow.ForEach(func(key string, value MathematicalConstants) { - fmt.Printf("Key: %v, Name: %v, Value: %v\n", key, value.Name, value.Value) - }) +// GetOrSet returns the existing value or stores and returns a new one. +func ExampleMapKeyValue_GetOrSet() { + kv := r9e.NewMapKeyValue[string, int]() + + v1, loaded1 := kv.GetOrSet("hits", 1) + fmt.Printf("first: value=%d loaded=%v\n", v1, loaded1) + + v2, loaded2 := kv.GetOrSet("hits", 999) + fmt.Printf("second: value=%d loaded=%v\n", v2, loaded2) + // Output: + // first: value=1 loaded=false + // second: value=1 loaded=true } -func ExampleSMapKeyValue_basic() { - type MathematicalConstants struct { - Name string - Value float64 +// GetAndDelete removes a key and returns its former value. +func ExampleMapKeyValue_GetAndDelete() { + kv := r9e.NewMapKeyValue[string, string]() + kv.Set("token", "abc123") + + value, loaded := kv.GetAndDelete("token") + fmt.Printf("value=%q loaded=%v size=%d\n", value, loaded, kv.Size()) + // Output: value="abc123" loaded=true size=0 +} + +// Iterate deterministically by sorting the keys first. +func ExampleMapKeyValue_All() { + kv := r9e.NewMapKeyValue[string, int]() + kv.Set("c", 3) + kv.Set("a", 1) + kv.Set("b", 2) + + // All() yields in map order; maps.Collect drains the iterator into a plain + // map that we can look up while iterating the sorted keys for stable output. + snapshot := maps.Collect(kv.All()) + + for _, k := range kv.SortKeys(func(a, b string) bool { return a < b }) { + fmt.Printf("%s=%d\n", k, snapshot[k]) } + // Output: + // a=1 + // b=2 + // c=3 +} + +// FilterValue returns a new container with the matching entries. +func ExampleMapKeyValue_FilterValue() { + kv := r9e.NewMapKeyValue[string, int]() + kv.Set("one", 1) + kv.Set("two", 2) + kv.Set("three", 3) + kv.Set("four", 4) - kv := r9e.NewSMapKeyValue[string, MathematicalConstants]() + even := kv.FilterValue(func(v int) bool { return v%2 == 0 }) - kv.Set("pi", MathematicalConstants{"Archimedes' constant", 3.141592}) - kv.Set("e", MathematicalConstants{"Euler number, Napier's constant", 2.718281}) - kv.Set("γ", MathematicalConstants{"Euler number, Napier's constant", 0.577215}) - kv.Set("Φ", MathematicalConstants{"Golden ratio constant", 1.618033}) - kv.Set("ρ", MathematicalConstants{"Plastic number ρ (or silver constant)", 2.414213}) + fmt.Println(even.SortValues(func(a, b int) bool { return a < b })) + // Output: [2 4] +} - kvFilteredValues := kv.FilterValue(func(value MathematicalConstants) bool { - return value.Value > 2.0 - }) +// Partition splits a container into matching and non-matching halves. +func ExampleMapKeyValue_Partition() { + kv := r9e.NewMapKeyValue[string, int]() + kv.Set("a", 10) + kv.Set("b", 20) + kv.Set("c", 30) - fmt.Println("Mathematical Constants:") - kvFilteredValues.ForEach(func(key string, value MathematicalConstants) { - fmt.Printf("Key: %v, Name: %v, Value: %v\n", key, value.Name, value.Value) - }) + big, small := kv.Partition(func(_ string, v int) bool { return v >= 20 }) - fmt.Printf("\n") - fmt.Printf("The most famous mathematical constant:\n") - fmt.Printf("Name: %v, Value: %v\n", kv.Get("pi").Name, kv.Get("pi").Value) + fmt.Println("big: ", big.SortValues(func(a, b int) bool { return a < b })) + fmt.Println("small:", small.SortValues(func(a, b int) bool { return a < b })) + // Output: + // big: [20 30] + // small: [10] +} - lst := kv.SortValues(func(value1, value2 MathematicalConstants) bool { - return value1.Value > value2.Value - }) +// SortValues returns values ordered by a custom comparison. +func ExampleMapKeyValue_SortValues() { + kv := r9e.NewMapKeyValue[string, float64]() + kv.Set("pi", 3.14) + kv.Set("e", 2.71) + kv.Set("phi", 1.61) - fmt.Printf("\n") - fmt.Printf("The most famous mathematical constant sorted by value:\n") - for i, value := range lst { - fmt.Printf("i: %v, Name: %v, Value: %v\n", i, value.Name, value.Value) + fmt.Println(kv.SortValues(func(a, b float64) bool { return a < b })) + // Output: [1.61 2.71 3.14] +} + +// Merge copies entries from another container, overwriting existing keys. +func ExampleMapKeyValue_Merge() { + a := r9e.NewMapKeyValue[string, int]() + a.Set("x", 1) + a.Set("y", 2) + + b := r9e.NewMapKeyValue[string, int]() + b.Set("y", 20) + b.Set("z", 30) + + a.Merge(b) + + for _, k := range a.SortKeys(func(a, b string) bool { return a < b }) { + fmt.Printf("%s=%d\n", k, a.Get(k)) } + // Output: + // x=1 + // y=20 + // z=30 +} + +// A MapKeyValue marshals to and from a JSON object. +func ExampleMapKeyValue_json() { + kv := r9e.NewMapKeyValue[string, int]() + kv.Set("a", 1) + kv.Set("b", 2) + + encoded, _ := json.Marshal(kv) + fmt.Println(string(encoded)) + + decoded := r9e.NewMapKeyValue[string, int]() + _ = json.Unmarshal([]byte(`{"x":10,"y":20}`), decoded) + fmt.Println(decoded.Get("x"), decoded.Get("y")) + // Output: + // {"a":1,"b":2} + // 10 20 +} + +// SMapKeyValue has the same API, backed by sync.Map. +func ExampleSMapKeyValue() { + sm := r9e.NewSMapKeyValue[string, int]() + + sm.Set("a", 1) + sm.Set("a", 2) // overwrite: Size stays 1 + sm.Set("b", 3) + + fmt.Println("size:", sm.Size()) + fmt.Println("a:", sm.Get("a")) + // Output: + // size: 2 + // a: 2 +} + +// Using a struct value type. +func ExampleMapKeyValue_struct() { + type Constant struct { + Name string + Value float64 + } + + kv := r9e.NewMapKeyValue[string, Constant]() + kv.Set("pi", Constant{"Archimedes' constant", 3.141592}) - kvHigh, kvLow := kv.Partition(func(key string, value MathematicalConstants) bool { - return value.Value > 2.5 - }) - - fmt.Printf("\n") - fmt.Printf("Mathematical constants which value is greater than 2.5:\n") - kvHigh.ForEach(func(key string, value MathematicalConstants) { - fmt.Printf("Key: %v, Name: %v, Value: %v\n", key, value.Name, value.Value) - }) - - fmt.Printf("\n") - fmt.Printf("Mathematical constants which value is less than 2.5:\n") - kvLow.ForEach(func(key string, value MathematicalConstants) { - fmt.Printf("Key: %v, Name: %v, Value: %v\n", key, value.Name, value.Value) - }) + c := kv.Get("pi") + fmt.Printf("%s = %v\n", c.Name, c.Value) + // Output: Archimedes' constant = 3.141592 } diff --git a/go.mod b/go.mod index 9db387a..0b8404e 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module github.com/slashdevops/r9e -go 1.19 +go 1.26.0 diff --git a/godoc.go b/godoc.go deleted file mode 100644 index 9f2c039..0000000 --- a/godoc.go +++ /dev/null @@ -1,9 +0,0 @@ -/* -Package r9e provides a collection of memory store containers. - -# Available Containers - -* [MapKeyValue[K comparable, T any]](https://pkg.go.dev/github.com/slashdevops/r9e#MapKeyValue) using sync.RWMutex -* [SMapKeyValue[K comparable, T any]](https://pkg.go.dev/github.com/slashdevops/r9e#SMapKeyValue) using sync.Map -*/ -package r9e diff --git a/mapkeyvalue.go b/mapkeyvalue.go index aa585d4..be4987e 100644 --- a/mapkeyvalue.go +++ b/mapkeyvalue.go @@ -1,8 +1,11 @@ package r9e import ( + "encoding/json" + "iter" + "maps" "reflect" - "sort" + "slices" "sync" ) @@ -10,43 +13,59 @@ type mapKeyValueOptions struct { size int } -// MapKeyValueOptions are the options for MapKeyValue container. +// MapKeyValueOptions configures a [MapKeyValue] container at construction time. type MapKeyValueOptions func(*mapKeyValueOptions) -// WithCapacity sets the initial capacity allocation of the MapKeyValue container. +// WithCapacity presizes the underlying map to hold at least size entries +// without reallocating. It is a performance hint: when the approximate number +// of entries is known up front, presizing avoids incremental map growth. func WithCapacity(size int) MapKeyValueOptions { - return func(kv *mapKeyValueOptions) { - kv.size = size + return func(o *mapKeyValueOptions) { + o.size = size } } -// MapKeyValue is a generic key-value store container that is thread-safe. -// This use a golang native map data structure as underlying data structure and a mutex to -// protect the data. +// MapKeyValue is a thread-safe, generic key-value container backed by a native +// Go map guarded by a [sync.RWMutex]. Reads are served concurrently under a +// read lock; writes take the exclusive lock. +// +// Prefer MapKeyValue when the workload is read-heavy or mixed and callers want +// predictable, snapshot-consistent bulk operations (Keys, Values, Clone, Map, +// Filter, Partition). For workloads dominated by disjoint keys written from +// many goroutines, see [SMapKeyValue]. +// +// The zero value is not ready for use; construct one with [NewMapKeyValue]. type MapKeyValue[K comparable, T any] struct { mu sync.RWMutex data map[K]T } -// kv is a helper struct to sort the values of the MapKeyValue container. -type kv[K comparable, T any] struct { - key K - value T -} - -// NewMapKeyValue returns a new MapKeyValue container. +// NewMapKeyValue returns a ready-to-use MapKeyValue. Pass [WithCapacity] to +// presize the container. func NewMapKeyValue[K comparable, T any](options ...MapKeyValueOptions) *MapKeyValue[K, T] { - kvo := mapKeyValueOptions{} + var o mapKeyValueOptions for _, opt := range options { - opt(&kvo) + opt(&o) } return &MapKeyValue[K, T]{ - data: make(map[K]T, kvo.size), + data: make(map[K]T, o.size), } } -// Set sets the value associated with the key. +// snapshot returns a shallow copy of the underlying data taken under the read +// lock. It is the building block for operations that must not hold the lock +// while touching another container (avoiding lock-ordering deadlocks). +func (r *MapKeyValue[K, T]) snapshot() map[K]T { + r.mu.RLock() + defer r.mu.RUnlock() + + out := make(map[K]T, len(r.data)) + maps.Copy(out, r.data) + return out +} + +// Set stores value under key, replacing any existing value. func (r *MapKeyValue[K, T]) Set(key K, value T) { r.mu.Lock() defer r.mu.Unlock() @@ -54,8 +73,18 @@ func (r *MapKeyValue[K, T]) Set(key K, value T) { r.data[key] = value } -// GetAndCheck returns the value associated with the key if this exist also a -// boolean value if this exist of not. +// Get returns the value stored under key, or the zero value of T if the key is +// absent. Use [MapKeyValue.GetAndCheck] to distinguish an absent key from a +// stored zero value. +func (r *MapKeyValue[K, T]) Get(key K) T { + r.mu.RLock() + defer r.mu.RUnlock() + + return r.data[key] +} + +// GetAndCheck returns the value stored under key and a boolean reporting whether +// the key was present. func (r *MapKeyValue[K, T]) GetAndCheck(key K) (T, bool) { r.mu.RLock() defer r.mu.RUnlock() @@ -64,28 +93,34 @@ func (r *MapKeyValue[K, T]) GetAndCheck(key K) (T, bool) { return value, ok } -// Get returns the value associated with the key. -// If the key does not exist, return zero value of the type. -func (r *MapKeyValue[K, T]) Get(key K) T { - r.mu.RLock() - defer r.mu.RUnlock() +// GetOrSet returns the existing value for key if present. Otherwise it stores +// and returns value. The loaded result is true if the value was already +// present. The lookup and store are performed atomically under a single lock. +func (r *MapKeyValue[K, T]) GetOrSet(key K, value T) (actual T, loaded bool) { + r.mu.Lock() + defer r.mu.Unlock() - return r.data[key] + if existing, ok := r.data[key]; ok { + return existing, true + } + r.data[key] = value + return value, false } -// GetAnDelete returns the value associated with the key and delete it if the key exist -// if the key doesn't exist return the given key value false -func (r *MapKeyValue[K, T]) GetAnDelete(key K) (T, bool) { +// GetAndDelete returns the value stored under key and deletes it. The loaded +// result reports whether the key was present. +func (r *MapKeyValue[K, T]) GetAndDelete(key K) (value T, loaded bool) { r.mu.Lock() defer r.mu.Unlock() - current, loaded := r.data[key] + + value, loaded = r.data[key] if loaded { delete(r.data, key) } - return current, loaded + return value, loaded } -// Delete deletes the value associated with the key. +// Delete removes key from the container. Deleting an absent key is a no-op. func (r *MapKeyValue[K, T]) Delete(key K) { r.mu.Lock() defer r.mu.Unlock() @@ -93,15 +128,15 @@ func (r *MapKeyValue[K, T]) Delete(key K) { delete(r.data, key) } -// Clear deletes all key-value pairs stored in the container. +// Clear removes all entries, retaining the allocated capacity for reuse. func (r *MapKeyValue[K, T]) Clear() { r.mu.Lock() defer r.mu.Unlock() - r.data = make(map[K]T, 0) + clear(r.data) } -// Size returns the number of key-value pairs stored in the container. +// Size returns the number of entries stored. func (r *MapKeyValue[K, T]) Size() int { r.mu.RLock() defer r.mu.RUnlock() @@ -109,17 +144,12 @@ func (r *MapKeyValue[K, T]) Size() int { return len(r.data) } -// IsEmpty returns true if the container is empty. +// IsEmpty reports whether the container has no entries. func (r *MapKeyValue[K, T]) IsEmpty() bool { return r.Size() == 0 } -// IsFull returns true if the container has elements. -func (r *MapKeyValue[K, T]) IsFull() bool { - return r.Size() != 0 -} - -// ContainsKey returns true if the key is in the container. +// ContainsKey reports whether key is present. func (r *MapKeyValue[K, T]) ContainsKey(key K) bool { r.mu.RLock() defer r.mu.RUnlock() @@ -128,7 +158,8 @@ func (r *MapKeyValue[K, T]) ContainsKey(key K) bool { return ok } -// ContainsValue returns true if the value is in the container. +// ContainsValue reports whether any stored value is deeply equal to value, +// using [reflect.DeepEqual]. This is O(n) in the number of entries. func (r *MapKeyValue[K, T]) ContainsValue(value T) bool { r.mu.RLock() defer r.mu.RUnlock() @@ -138,23 +169,10 @@ func (r *MapKeyValue[K, T]) ContainsValue(value T) bool { return true } } - return false } -// Get returns the key value associated with the key. -func (r *MapKeyValue[K, T]) Key(key K) K { - r.mu.RLock() - defer r.mu.RUnlock() - - if _, ok := r.data[key]; ok { - return key - } - var empty K - return empty -} - -// Keys returns all keys stored in the container. +// Keys returns a snapshot slice of all keys. The order is unspecified. func (r *MapKeyValue[K, T]) Keys() []K { r.mu.RLock() defer r.mu.RUnlock() @@ -166,7 +184,7 @@ func (r *MapKeyValue[K, T]) Keys() []K { return keys } -// Values returns all values stored in the container. +// Values returns a snapshot slice of all values. The order is unspecified. func (r *MapKeyValue[K, T]) Values() []T { r.mu.RLock() defer r.mu.RUnlock() @@ -178,7 +196,32 @@ func (r *MapKeyValue[K, T]) Values() []T { return values } -// ForEach calls the given function for each key-value pair in the container. +// All returns an iterator over all key-value pairs, suitable for use with a +// range-over-func loop: +// +// for k, v := range kv.All() { +// // ... +// } +// +// The read lock is held for the duration of the iteration, so the callback must +// not call methods that mutate the same container (Set, Delete, Clear, ...); +// doing so deadlocks. Break out of the loop early to stop iterating. +func (r *MapKeyValue[K, T]) All() iter.Seq2[K, T] { + return func(yield func(K, T) bool) { + r.mu.RLock() + defer r.mu.RUnlock() + + for key, value := range r.data { + if !yield(key, value) { + return + } + } + } +} + +// ForEach calls fn for every key-value pair. The read lock is held for the +// duration; fn must not mutate the same container. Prefer [MapKeyValue.All] +// with a range-over-func loop in new code. func (r *MapKeyValue[K, T]) ForEach(fn func(key K, value T)) { r.mu.RLock() defer r.mu.RUnlock() @@ -188,7 +231,8 @@ func (r *MapKeyValue[K, T]) ForEach(fn func(key K, value T)) { } } -// ForEachKey calls the given function for each key in the container. +// ForEachKey calls fn for every key. See [MapKeyValue.ForEach] for locking +// semantics. func (r *MapKeyValue[K, T]) ForEachKey(fn func(key K)) { r.mu.RLock() defer r.mu.RUnlock() @@ -198,7 +242,8 @@ func (r *MapKeyValue[K, T]) ForEachKey(fn func(key K)) { } } -// ForEachValue calls the given function for each value in the container. +// ForEachValue calls fn for every value. See [MapKeyValue.ForEach] for locking +// semantics. func (r *MapKeyValue[K, T]) ForEachValue(fn func(value T)) { r.mu.RLock() defer r.mu.RUnlock() @@ -208,221 +253,254 @@ func (r *MapKeyValue[K, T]) ForEachValue(fn func(value T)) { } } -// Clone returns a new MapKeyValue with a copy of the underlying data. +// Clone returns a new independent container holding a shallow copy of the data. func (r *MapKeyValue[K, T]) Clone() *MapKeyValue[K, T] { - r.mu.RLock() - defer r.mu.RUnlock() - - clone := NewMapKeyValue[K, T](WithCapacity(r.Size())) - for key, value := range r.data { - clone.Set(key, value) - } - return clone + return &MapKeyValue[K, T]{data: r.snapshot()} } -// CloneAndClear returns a new MapKeyValue with a copy of the underlying data and clears the container. +// CloneAndClear atomically copies the data into a new container and clears the +// receiver. func (r *MapKeyValue[K, T]) CloneAndClear() *MapKeyValue[K, T] { - r.mu.RLock() - defer r.mu.RUnlock() + r.mu.Lock() + defer r.mu.Unlock() - clone := NewMapKeyValue[K, T](WithCapacity(r.Size())) - for key, value := range r.data { - clone.Set(key, value) + out := make(map[K]T, len(r.data)) + maps.Copy(out, r.data) + clear(r.data) + return &MapKeyValue[K, T]{data: out} +} + +// Merge copies every entry from other into the receiver, overwriting existing +// keys. other is read via a consistent snapshot, so the two containers are +// never locked simultaneously. Merging a container into itself is a no-op-safe +// operation. A nil other is ignored. +func (r *MapKeyValue[K, T]) Merge(other *MapKeyValue[K, T]) { + if other == nil || other == r { + return } - r.data = make(map[K]T) - return clone + src := other.snapshot() + + r.mu.Lock() + defer r.mu.Unlock() + maps.Copy(r.data, src) } -// DeepEqual returns true if the given kv is deep equal to the MapKeyValue container -func (r *MapKeyValue[K, T]) DeepEqual(kv *MapKeyValue[K, T]) bool { +// DeepEqual reports whether the receiver and other hold the same keys mapped to +// deeply equal values ([reflect.DeepEqual]). A nil other equals an empty +// receiver only when the receiver is also empty. The two containers are never +// locked at the same time. +func (r *MapKeyValue[K, T]) DeepEqual(other *MapKeyValue[K, T]) bool { + var otherData map[K]T + if other != nil { + otherData = other.snapshot() + } + r.mu.RLock() defer r.mu.RUnlock() - if r.Size() != kv.Size() { + if len(r.data) != len(otherData) { return false } - for key, value := range r.data { - if !reflect.DeepEqual(value, kv.Get(key)) { + ov, ok := otherData[key] + if !ok || !reflect.DeepEqual(value, ov) { return false } } - return true } -// Map returns a new MapKeyValue after applying the given function fn to each key-value pair. -func (r *MapKeyValue[K, T]) Map(fn func(key K, value T) (newKey K, newValue T)) *MapKeyValue[K, T] { +// Map returns a new container produced by applying fn to every pair. +func (r *MapKeyValue[K, T]) Map(fn func(key K, value T) (K, T)) *MapKeyValue[K, T] { r.mu.RLock() defer r.mu.RUnlock() - m := NewMapKeyValue[K, T](WithCapacity(r.Size())) + out := make(map[K]T, len(r.data)) for key, value := range r.data { - newKey, newValue := fn(key, value) - m.Set(newKey, newValue) + nk, nv := fn(key, value) + out[nk] = nv } - return m + return &MapKeyValue[K, T]{data: out} } -// MapKey returns a new MapKeyValue after applying the given function fn to each key. +// MapKey returns a new container with each key transformed by fn. func (r *MapKeyValue[K, T]) MapKey(fn func(key K) K) *MapKeyValue[K, T] { r.mu.RLock() defer r.mu.RUnlock() - m := NewMapKeyValue[K, T](WithCapacity(r.Size())) - for key := range r.data { - newKey := fn(key) - m.Set(newKey, r.data[key]) + out := make(map[K]T, len(r.data)) + for key, value := range r.data { + out[fn(key)] = value } - return m + return &MapKeyValue[K, T]{data: out} } -// MapValue returns a new MapKeyValue after applying the given function fn to each value. +// MapValue returns a new container with each value transformed by fn. func (r *MapKeyValue[K, T]) MapValue(fn func(value T) T) *MapKeyValue[K, T] { r.mu.RLock() defer r.mu.RUnlock() - m := NewMapKeyValue[K, T](WithCapacity(r.Size())) + out := make(map[K]T, len(r.data)) for key, value := range r.data { - newValue := fn(value) - m.Set(key, newValue) + out[key] = fn(value) } - return m + return &MapKeyValue[K, T]{data: out} } -// Filter returns a new MapKeyValue after applying the given function fn to each key-value pair. +// Filter returns a new container with the pairs for which fn reports true. func (r *MapKeyValue[K, T]) Filter(fn func(key K, value T) bool) *MapKeyValue[K, T] { r.mu.RLock() defer r.mu.RUnlock() - m := NewMapKeyValue[K, T](WithCapacity(r.Size())) + out := make(map[K]T) for key, value := range r.data { if fn(key, value) { - m.Set(key, value) + out[key] = value } } - return m + return &MapKeyValue[K, T]{data: out} } -// FilterKey returns a new MapKeyValue after applying the given function fn to each key. +// FilterKey returns a new container with the pairs whose key satisfies fn. func (r *MapKeyValue[K, T]) FilterKey(fn func(key K) bool) *MapKeyValue[K, T] { r.mu.RLock() defer r.mu.RUnlock() - m := NewMapKeyValue[K, T](WithCapacity(r.Size())) - for key := range r.data { + out := make(map[K]T) + for key, value := range r.data { if fn(key) { - m.Set(key, r.data[key]) + out[key] = value } } - return m + return &MapKeyValue[K, T]{data: out} } -// FilterValue returns a new MapKeyValue after applying the given function fn to each value. +// FilterValue returns a new container with the pairs whose value satisfies fn. func (r *MapKeyValue[K, T]) FilterValue(fn func(value T) bool) *MapKeyValue[K, T] { r.mu.RLock() defer r.mu.RUnlock() - m := NewMapKeyValue[K, T](WithCapacity(r.Size())) + out := make(map[K]T) for key, value := range r.data { if fn(value) { - m.Set(key, value) + out[key] = value } } - return m + return &MapKeyValue[K, T]{data: out} } -// Partition returns two new MapKeyValue. One with all the elements that satisfy the predicate and -// another with the rest. The predicate is applied to each element. +// Partition splits the container into match (pairs for which fn is true) and +// others (the rest), returning two new containers. func (r *MapKeyValue[K, T]) Partition(fn func(key K, value T) bool) (match, others *MapKeyValue[K, T]) { r.mu.RLock() defer r.mu.RUnlock() - match = NewMapKeyValue[K, T](WithCapacity(r.Size())) - others = NewMapKeyValue[K, T](WithCapacity(r.Size())) + m := make(map[K]T) + o := make(map[K]T) for key, value := range r.data { if fn(key, value) { - match.Set(key, value) + m[key] = value } else { - others.Set(key, value) + o[key] = value } } - return + return &MapKeyValue[K, T]{data: m}, &MapKeyValue[K, T]{data: o} } -// PartitionKey returns two new MapKeyValue. One with all the elements that satisfy the predicate and -// another with the rest. The predicate is applied to each key. +// PartitionKey splits the container by applying fn to each key. func (r *MapKeyValue[K, T]) PartitionKey(fn func(key K) bool) (match, others *MapKeyValue[K, T]) { r.mu.RLock() defer r.mu.RUnlock() - match = NewMapKeyValue[K, T](WithCapacity(r.Size())) - others = NewMapKeyValue[K, T](WithCapacity(r.Size())) - for key := range r.data { + m := make(map[K]T) + o := make(map[K]T) + for key, value := range r.data { if fn(key) { - match.Set(key, r.data[key]) + m[key] = value } else { - others.Set(key, r.data[key]) + o[key] = value } } - return + return &MapKeyValue[K, T]{data: m}, &MapKeyValue[K, T]{data: o} } -// PartitionValue returns two new MapKeyValue. One with all the elements that satisfy the predicate and -// another with the rest. The predicate is applied to each value. +// PartitionValue splits the container by applying fn to each value. func (r *MapKeyValue[K, T]) PartitionValue(fn func(value T) bool) (match, others *MapKeyValue[K, T]) { r.mu.RLock() defer r.mu.RUnlock() - match = NewMapKeyValue[K, T](WithCapacity(r.Size())) - others = NewMapKeyValue[K, T](WithCapacity(r.Size())) + m := make(map[K]T) + o := make(map[K]T) for key, value := range r.data { if fn(value) { - match.Set(key, value) + m[key] = value } else { - others.Set(key, value) + o[key] = value } } - return + return &MapKeyValue[K, T]{data: m}, &MapKeyValue[K, T]{data: o} } -// SortKeys returns a []*K (keys) after sorting the keys using the given sortFn function. -func (r *MapKeyValue[K, T]) SortKeys(sortFn func(key1, key2 K) bool) []*K { - r.mu.RLock() - defer r.mu.RUnlock() - +// SortKeys returns all keys sorted by the less function, which must report +// whether a should sort before b. +func (r *MapKeyValue[K, T]) SortKeys(less func(a, b K) bool) []K { keys := r.Keys() - - sort.Slice(keys, func(i, j int) bool { - return sortFn(keys[i], keys[j]) + slices.SortFunc(keys, func(a, b K) int { + switch { + case less(a, b): + return -1 + case less(b, a): + return 1 + default: + return 0 + } }) + return keys +} - m := make([]*K, len(keys)) - for i, key := range keys { - k := key - m[i] = &k - } - return m +// SortValues returns all values sorted by the less function, which must report +// whether a should sort before b. +func (r *MapKeyValue[K, T]) SortValues(less func(a, b T) bool) []T { + values := r.Values() + slices.SortFunc(values, func(a, b T) int { + switch { + case less(a, b): + return -1 + case less(b, a): + return 1 + default: + return 0 + } + }) + return values } -// SortValues returns a []*T (values) after sorting the values using given function sortFn. -func (r *MapKeyValue[K, T]) SortValues(sortFn func(value1, value2 T) bool) []*T { +// MarshalJSON encodes the container as a JSON object, so a MapKeyValue can be +// used directly as a struct field. Encoding succeeds only for key types that +// encoding/json accepts as object keys (strings, integers, and +// encoding.TextMarshaler implementations). +func (r *MapKeyValue[K, T]) MarshalJSON() ([]byte, error) { r.mu.RLock() defer r.mu.RUnlock() - kvs := make([]*kv[K, T], 0, r.Size()) - for key, value := range r.data { - kvs = append(kvs, &kv[K, T]{key, value}) - } - - sort.Slice(kvs, func(i, j int) bool { - return sortFn(kvs[i].value, kvs[j].value) - }) + out := make(map[K]T, len(r.data)) + maps.Copy(out, r.data) + return json.Marshal(out) +} - m := make([]*T, len(kvs)) - for i, pair := range kvs { - m[i] = &pair.value +// UnmarshalJSON decodes a JSON object into the container, merging the decoded +// entries over any existing ones. +func (r *MapKeyValue[K, T]) UnmarshalJSON(data []byte) error { + var m map[K]T + if err := json.Unmarshal(data, &m); err != nil { + return err } - return m + r.mu.Lock() + defer r.mu.Unlock() + if r.data == nil { + r.data = make(map[K]T, len(m)) + } + maps.Copy(r.data, m) + return nil } diff --git a/mapkeyvalue_test.go b/mapkeyvalue_test.go index 591b6ea..c9892ba 100644 --- a/mapkeyvalue_test.go +++ b/mapkeyvalue_test.go @@ -32,18 +32,18 @@ func init() { rand.Seed(time.Now().UnixNano()) // fill the kv_int_int - for i := 0; i < kvSize; i++ { + for range kvSize { kv_int_int.Set(rand.Intn(kvSize), rand.Intn(kvSize)) } // fill the kv_string_string - for i := 0; i < kvSize; i++ { + for range kvSize { keyval := fmt.Sprintf("%x", md5.Sum([]byte(strconv.Itoa(rand.Intn(kvSize))))) kv_string_string.Set(keyval, keyval) } // fill the kv_string_struct - for i := 0; i < kvSize; i++ { + for range kvSize { keyval := fmt.Sprintf("%x", md5.Sum([]byte(strconv.Itoa(rand.Intn(kvSize))))) s := TestStruct{ a: keyval, @@ -69,15 +69,15 @@ func TestNewMapKeyValue(t *testing.T) { t.Errorf("Expected size to be %v, got %v", 1, kv.Size()) } - value := kv.Get(1) - VKind := reflect.TypeOf(value).Kind().String() + _ = kv.Get(1) + VKind := reflect.TypeFor[int]().Kind().String() if VKind != "int" { t.Errorf("Expected type to be %s, got %s", "int", VKind) } - key := kv.Keys()[0] - kKind := reflect.TypeOf(key).Kind().String() + _ = kv.Keys()[0] + kKind := reflect.TypeFor[int]().Kind().String() if kKind != "int" { t.Errorf("Expected type to be %s, got %s", "int", kKind) @@ -97,15 +97,15 @@ func TestNewMapKeyValue(t *testing.T) { t.Errorf("Expected size to be %v, got %v", 1, kv.Size()) } - value := kv.Get(1) - VKind := reflect.TypeOf(value).Kind().String() + _ = kv.Get(1) + VKind := reflect.TypeFor[int]().Kind().String() if VKind != "int" { t.Errorf("Expected type to be %s, got %s", "int", VKind) } - key := kv.Keys()[0] - kKind := reflect.TypeOf(key).Kind().String() + _ = kv.Keys()[0] + kKind := reflect.TypeFor[int]().Kind().String() if kKind != "int" { t.Errorf("Expected type to be %s, got %s", "int", kKind) @@ -125,15 +125,15 @@ func TestNewMapKeyValue(t *testing.T) { t.Errorf("Expected size to be %v, got %v", 1, kv.Size()) } - value := kv.Get(1) - VKind := reflect.TypeOf(value).Kind().String() + _ = kv.Get(1) + VKind := reflect.TypeFor[string]().Kind().String() if VKind != "string" { t.Errorf("Expected type to be %s, got %s", "string", VKind) } - key := kv.Keys()[0] - kKind := reflect.TypeOf(key).Kind().String() + _ = kv.Keys()[0] + kKind := reflect.TypeFor[float64]().Kind().String() if kKind != "float64" { t.Errorf("Expected type to be %s, got %s", "float64", kKind) @@ -157,8 +157,8 @@ func TestNewMapKeyValue(t *testing.T) { t.Errorf("Expected size to be %v, got %v", 1, kv.Size()) } - value := kv.Get(1) - typeOf := reflect.TypeOf(value) + _ = kv.Get(1) + typeOf := reflect.TypeFor[testStruct]() kind := typeOf.Kind().String() if kind != "struct" { @@ -169,8 +169,8 @@ func TestNewMapKeyValue(t *testing.T) { t.Errorf("Expected type to be %s, got %s", "testStruct", kind) } - key := kv.Keys()[0] - kKind := reflect.TypeOf(key).Kind().String() + _ = kv.Keys()[0] + kKind := reflect.TypeFor[int]().Kind().String() if kKind != "int" { t.Errorf("Expected type to be %s, got %s", "int", kKind) @@ -294,8 +294,8 @@ func TestGet_MapKeyValue(t *testing.T) { }) } -func TestGetAnDelete_MapKeyValue(t *testing.T) { - t.Run("test GetAnDelete for NewMapKeyValue[string, struct] key exist", func(t *testing.T) { +func TestGetAndDelete_MapKeyValue(t *testing.T) { + t.Run("test GetAndDelete for NewMapKeyValue[string, struct] key exist", func(t *testing.T) { type testStruct struct { Name string value float64 @@ -310,9 +310,9 @@ func TestGetAnDelete_MapKeyValue(t *testing.T) { t.Errorf("Expected size to be %v, got %v", 3, kv.Size()) } - value, ok := kv.GetAnDelete("Archimedes") + value, ok := kv.GetAndDelete("Archimedes") if !ok { - t.Errorf("Expected GetAnDelete to return true, got %v", ok) + t.Errorf("Expected GetAndDelete to return true, got %v", ok) } if value.Name != "This is Archimedes' Constant (Pi)" { @@ -327,7 +327,7 @@ func TestGetAnDelete_MapKeyValue(t *testing.T) { } }) - t.Run("test GetAnDelete for NewMapKeyValue[string, struct] key doesn't exist", func(t *testing.T) { + t.Run("test GetAndDelete for NewMapKeyValue[string, struct] key doesn't exist", func(t *testing.T) { type testStruct struct { Name string value float64 @@ -340,9 +340,9 @@ func TestGetAnDelete_MapKeyValue(t *testing.T) { t.Errorf("Expected size to be %v, got %v", 1, kv.Size()) } - value, ok := kv.GetAnDelete("Euler") + value, ok := kv.GetAndDelete("Euler") if ok { - t.Errorf("Expected GetAnDelete to return true, got %v", ok) + t.Errorf("Expected GetAndDelete to return true, got %v", ok) } if value.value != 0 { @@ -548,34 +548,6 @@ func TestIsEmpty_MapKeyValue(t *testing.T) { }) } -func TestIsFull_MapKeyValue(t *testing.T) { - t.Run("test IsFull for NewMapKeyValue[string, struct] with keys", func(t *testing.T) { - type testStruct struct { - Name string - value float64 - } - kv := NewMapKeyValue[string, testStruct]() - - kv.Set("Archimedes", testStruct{"This is Archimedes' Constant (Pi)", 3.1415}) - kv.Set("Euler", testStruct{"This is Euler's Number (e)", 2.7182}) - kv.Set("Golden Ratio", testStruct{"This is The Golden Ratio", 1.6180}) - - if kv.Size() != 3 { - t.Errorf("Expected size to be %v, got %v", 3, kv.Size()) - } - - if kv.IsFull() != true { - t.Errorf("Expected IsFull to be %v, got %v", false, kv.IsFull()) - } - - kv.Clear() - - if kv.IsFull() != false { - t.Errorf("Expected IsFull to be %v, got %v", true, kv.IsFull()) - } - }) -} - func TestContainsKey_MapKeyValue(t *testing.T) { t.Run("test ContainsKey for NewMapKeyValue[string, struct] with keys", func(t *testing.T) { type testStruct struct { @@ -648,32 +620,6 @@ func TestContainsValue_MapKeyValue(t *testing.T) { }) } -func TestKey_MapKeyValue(t *testing.T) { - t.Run("test Key for NewMapKeyValue[string, struct] with keys", func(t *testing.T) { - type testStruct struct { - Name string - value float64 - } - kv := NewMapKeyValue[string, testStruct]() - - kv.Set("Archimedes", testStruct{"This is Archimedes' Constant (Pi)", 3.1415}) - kv.Set("Euler", testStruct{"This is Euler's Number (e)", 2.7182}) - kv.Set("Golden Ratio", testStruct{"This is The Golden Ratio", 1.6180}) - - if kv.Size() != 3 { - t.Errorf("Expected size to be %v, got %v", 3, kv.Size()) - } - - if kv.Key("Archimedes") != "Archimedes" { - t.Errorf("Expected key to be %v, got %v", "Archimedes", kv.Key("Archimedes")) - } - - if kv.Key("Do Not Exist") != "" { - t.Errorf("Expected key to be %v, got %v", "Archimedes", kv.Key("Do Not Exist")) - } - }) -} - func TestKeys_MapKeyValue(t *testing.T) { t.Run("test Keys for NewMapKeyValue[string, struct] with keys", func(t *testing.T) { type testStruct struct { @@ -1157,7 +1103,7 @@ func TestMap_MapKeyValue(t *testing.T) { }) newKv.ForEach(func(key string, value testStruct) { - if kv.Key(key) != key { + if !kv.ContainsKey(key) { t.Errorf("Expected key to be uppercase, want: %v, got %v", strings.ToUpper(key), key) } if strings.ToUpper(kv.Get(key).Name) != value.Name { @@ -1212,8 +1158,8 @@ func TestMapKey_MapKeyValue(t *testing.T) { }) newKv.ForEach(func(key string, value testStruct) { - if strings.ToUpper(kv.Key(strings.Title(strings.ToLower(key)))) != key { - t.Errorf("Expected key to be uppercase, want: %v, got %v", kv.Key(strings.Title(strings.ToLower(key))), key) + if !kv.ContainsKey(strings.Title(strings.ToLower(key))) { + t.Errorf("Expected key to be uppercase, want: %v, got %v", strings.Title(strings.ToLower(key)), key) } if kv.Get(strings.Title(strings.ToLower(key))).Name != value.Name { t.Errorf("Expected value.Name to be uppercase, want: %v, got %v", kv.Get(strings.Title(strings.ToLower(key))).Name, value.Name) @@ -1267,8 +1213,8 @@ func TestMapValue_MapKeyValue(t *testing.T) { }) newKv.ForEach(func(key string, value testStruct) { - if kv.Key(key) != key { - t.Errorf("Expected key to be uppercase, want: %v, got %v", kv.Key(key), key) + if !kv.ContainsKey(key) { + t.Errorf("Expected key to be uppercase, want: %v, got %v", key, key) } if strings.ToUpper(kv.Get(key).Name) != value.Name { t.Errorf("Expected value.Name to be uppercase, want: %v, got %v", kv.Get(key).Name, value.Name) @@ -1323,7 +1269,7 @@ func TestFilter_MapKeyValue(t *testing.T) { newKv.ForEach(func(key string, value testStruct) { if key != "Archimedes" { - t.Errorf("Expected key to be uppercase, want: %v, got %v", kv.Key(key), key) + t.Errorf("Expected key to be uppercase, want: %v, got %v", key, key) } if value.Name != "This is Archimedes' Constant (Pi)" { t.Errorf("Expected value.Name to be uppercase, want: %v, got %v", "This is Archimedes' Constant (Pi)", value.Name) @@ -1376,7 +1322,7 @@ func TestFilterKey_MapKeyValue(t *testing.T) { newKv.ForEach(func(key string, value testStruct) { if key != "Archimedes" { - t.Errorf("Expected key to be uppercase, want: %v, got %v", kv.Key(key), key) + t.Errorf("Expected key to be uppercase, want: %v, got %v", key, key) } if value.Name != "This is Archimedes' Constant (Pi)" { t.Errorf("Expected value.Name to be uppercase, want: %v, got %v", "This is Archimedes' Constant (Pi)", value.Name) @@ -1433,7 +1379,7 @@ func TestFilterValue_MapKeyValue(t *testing.T) { newKv.ForEach(func(key string, value testStruct) { if key != "Archimedes" { - t.Errorf("Expected key to be uppercase, want: %v, got %v", kv.Key(key), key) + t.Errorf("Expected key to be uppercase, want: %v, got %v", key, key) } if value.Name != "This is Archimedes' Constant (Pi)" { t.Errorf("Expected value.Name to be uppercase, want: %v, got %v", "This is Archimedes' Constant (Pi)", value.Name) @@ -1493,7 +1439,7 @@ func TestPartition_MapKeyValue(t *testing.T) { grp1Kv.ForEach(func(key string, value testStruct) { if key != "Archimedes" { - t.Errorf("Expected key to be uppercase, want: %v, got %v", kv.Key(key), key) + t.Errorf("Expected key to be uppercase, want: %v, got %v", key, key) } if value.Name != "This is Archimedes' Constant (Pi)" { t.Errorf("Expected value.Name to be uppercase, want: %v, got %v", "This is Archimedes' Constant (Pi)", value.Name) @@ -1505,7 +1451,7 @@ func TestPartition_MapKeyValue(t *testing.T) { grp2Kv.ForEach(func(key string, value testStruct) { if key != "Euler" && key != "Golden Ratio" { - t.Errorf("Expected key to be uppercase, want: %v, got %v", kv.Key(key), key) + t.Errorf("Expected key to be uppercase, want: %v, got %v", key, key) } if value.Name != "This is Euler's Number (e)" && value.Name != "This is The Golden Ratio" { t.Errorf("Expected value.Name to be uppercase, want: %v, got %v", "This is Euler's Number (e)", value.Name) @@ -1569,7 +1515,7 @@ func TestPartitionKey_MapKeyValue(t *testing.T) { grp1Kv.ForEach(func(key string, value testStruct) { if key != "Archimedes" { - t.Errorf("Expected key to be uppercase, want: %v, got %v", kv.Key(key), key) + t.Errorf("Expected key to be uppercase, want: %v, got %v", key, key) } if value.Name != "This is Archimedes' Constant (Pi)" { t.Errorf("Expected value.Name to be uppercase, want: %v, got %v", "This is Archimedes' Constant (Pi)", value.Name) @@ -1581,7 +1527,7 @@ func TestPartitionKey_MapKeyValue(t *testing.T) { grp2Kv.ForEach(func(key string, value testStruct) { if key != "Euler" && key != "Golden Ratio" { - t.Errorf("Expected key to be uppercase, want: %v, got %v", kv.Key(key), key) + t.Errorf("Expected key to be uppercase, want: %v, got %v", key, key) } if value.Name != "This is Euler's Number (e)" && value.Name != "This is The Golden Ratio" { t.Errorf("Expected value.Name to be uppercase, want: %v, got %v", "This is Euler's Number (e)", value.Name) @@ -1645,7 +1591,7 @@ func TestPartitionValue_MapKeyValue(t *testing.T) { grp1Kv.ForEach(func(key string, value testStruct) { if key != "Archimedes" { - t.Errorf("Expected key to be uppercase, want: %v, got %v", kv.Key(key), key) + t.Errorf("Expected key to be uppercase, want: %v, got %v", key, key) } if value.Name != "This is Archimedes' Constant (Pi)" { t.Errorf("Expected value.Name to be uppercase, want: %v, got %v", "This is Archimedes' Constant (Pi)", value.Name) @@ -1657,7 +1603,7 @@ func TestPartitionValue_MapKeyValue(t *testing.T) { grp2Kv.ForEach(func(key string, value testStruct) { if key != "Euler" && key != "Golden Ratio" { - t.Errorf("Expected key to be uppercase, want: %v, got %v", kv.Key(key), key) + t.Errorf("Expected key to be uppercase, want: %v, got %v", key, key) } if value.Name != "This is Euler's Number (e)" && value.Name != "This is The Golden Ratio" { t.Errorf("Expected value.Name to be uppercase, want: %v, got %v", "This is Euler's Number (e)", value.Name) @@ -1716,8 +1662,8 @@ func TestSortKeys_MapKeyValue(t *testing.T) { t.Errorf("Expected size to be %v, got %v", 3, len(kSorted)) } - if *kSorted[0] != "Archimedes" { - t.Errorf("Expected key to be uppercase, want: %v, got %v", "Archimedes", *kSorted[0]) + if kSorted[0] != "Archimedes" { + t.Errorf("Expected key to be uppercase, want: %v, got %v", "Archimedes", kSorted[0]) } }) diff --git a/smapkeyvalue.go b/smapkeyvalue.go index 45cc556..933ae4b 100644 --- a/smapkeyvalue.go +++ b/smapkeyvalue.go @@ -1,162 +1,168 @@ package r9e import ( + "encoding/json" + "iter" "reflect" - "sort" + "slices" "sync" "sync/atomic" ) -// SMapKeyValue is a generic key-value store container that is thread-safe. -// This use a golang native sync.Map data structure as underlying data structure. +// SMapKeyValue is a thread-safe, generic key-value container backed by a +// [sync.Map]. It keeps an atomic entry counter so [SMapKeyValue.Size] is O(1). +// +// Prefer SMapKeyValue for workloads where many goroutines write disjoint keys, +// or where a key is written once and read many times, matching the cases +// sync.Map is optimized for. For read-heavy or mixed workloads that also need +// consistent bulk snapshots, [MapKeyValue] is usually simpler and faster. +// +// The zero value is not ready for use; construct one with [NewSMapKeyValue]. type SMapKeyValue[K comparable, T any] struct { - count atomic.Uint64 + count atomic.Int64 data sync.Map } -// skv is a helper struct to sort the values of the SMapKeyValue container. -type skv[K comparable, T any] struct { - key K - value T +// NewSMapKeyValue returns a ready-to-use SMapKeyValue. +func NewSMapKeyValue[K comparable, T any]() *SMapKeyValue[K, T] { + return &SMapKeyValue[K, T]{} } -// NewSMapKeyValue returns a new SMapKeyValue container. -func NewSMapKeyValue[K comparable, T any]() *SMapKeyValue[K, T] { - return &SMapKeyValue[K, T]{ - data: sync.Map{}, +// cast converts a value loaded from the underlying sync.Map back to T, +// returning the zero value of T when the stored value is absent or of an +// unexpected type. +func cast[T any](v any) T { + if t, ok := v.(T); ok { + return t } + var zero T + return zero } -// Set sets the value associated with the key. +// Set stores value under key, replacing any existing value. func (r *SMapKeyValue[K, T]) Set(key K, value T) { - r.count.Add(1) - r.data.Store(key, value) -} - -// GetAndCheck returns the value associated with the key if this exist also a -// boolean value if this exist of not. -func (r *SMapKeyValue[K, T]) GetAndCheck(key K) (T, bool) { - value, ok := r.data.Load(key) - - switch value := value.(type) { - case T: - return value, ok - default: - var t T - return t, ok + if _, loaded := r.data.Swap(key, value); !loaded { + r.count.Add(1) } } -// Get returns the value associated with the key. -// If the key does not exist, return zero value of the type. +// Get returns the value stored under key, or the zero value of T if the key is +// absent. func (r *SMapKeyValue[K, T]) Get(key K) T { value, _ := r.data.Load(key) - - switch value := value.(type) { - case T: - return value - default: - var t T - return t - } + return cast[T](value) } -// GetAnDelete returns the value associated with the key and delete it if the key exist -// if the key doesn't exist return the given key value false -func (r *SMapKeyValue[K, T]) GetAnDelete(key K) (T, bool) { - value, ok := r.data.LoadAndDelete(key) - if ok { - r.count.Swap(r.count.Load() - 1) +// GetAndCheck returns the value stored under key and a boolean reporting whether +// the key was present. +func (r *SMapKeyValue[K, T]) GetAndCheck(key K) (T, bool) { + value, ok := r.data.Load(key) + return cast[T](value), ok +} +// GetOrSet returns the existing value for key if present. Otherwise it stores +// and returns value. The loaded result reports whether the value was already +// present. The operation is atomic. +func (r *SMapKeyValue[K, T]) GetOrSet(key K, value T) (actual T, loaded bool) { + v, loaded := r.data.LoadOrStore(key, value) + if !loaded { + r.count.Add(1) } + return cast[T](v), loaded +} - switch value := value.(type) { - case T: - return value, ok - default: - var t T - return t, ok +// GetAndDelete returns the value stored under key and deletes it. The loaded +// result reports whether the key was present. +func (r *SMapKeyValue[K, T]) GetAndDelete(key K) (value T, loaded bool) { + v, loaded := r.data.LoadAndDelete(key) + if loaded { + r.count.Add(-1) } + return cast[T](v), loaded } -// Delete deletes the value associated with the key. +// Delete removes key from the container. Deleting an absent key is a no-op. func (r *SMapKeyValue[K, T]) Delete(key K) { - if _, ok := r.data.LoadAndDelete(key); ok { - r.count.Swap(r.count.Load() - 1) + if _, loaded := r.data.LoadAndDelete(key); loaded { + r.count.Add(-1) } } -// Clear deletes all key-value pairs stored in the container. +// Clear removes all entries. func (r *SMapKeyValue[K, T]) Clear() { - r.data = sync.Map{} - r.count.Swap(0) + r.data.Clear() + r.count.Store(0) } -// Size returns the number of key-value pairs stored in the container. +// Size returns the number of entries stored. It is O(1). func (r *SMapKeyValue[K, T]) Size() int { return int(r.count.Load()) } -// IsEmpty returns true if the container is empty. +// IsEmpty reports whether the container has no entries. func (r *SMapKeyValue[K, T]) IsEmpty() bool { return r.Size() == 0 } -// IsFull returns true if the container has elements. -func (r *SMapKeyValue[K, T]) IsFull() bool { - return r.Size() != 0 -} - -// ContainsKey returns true if the key is in the container. +// ContainsKey reports whether key is present. func (r *SMapKeyValue[K, T]) ContainsKey(key K) bool { _, ok := r.data.Load(key) return ok } -// ContainsValue returns true if the value is in the container. +// ContainsValue reports whether any stored value is deeply equal to value, +// using [reflect.DeepEqual]. This is O(n) in the number of entries. func (r *SMapKeyValue[K, T]) ContainsValue(value T) bool { - var ret bool - - r.data.Range(func(key, v any) bool { + found := false + r.data.Range(func(_, v any) bool { if reflect.DeepEqual(v, value) { - ret = true + found = true return false } return true }) - return ret -} - -// Get returns the key value associated with the key. -func (r *SMapKeyValue[K, T]) Key(key K) K { - if _, ok := r.data.Load(key); ok { - return key - } - var empty K - return empty + return found } -// Keys returns all keys stored in the container. +// Keys returns a snapshot slice of all keys. The order is unspecified. func (r *SMapKeyValue[K, T]) Keys() []K { keys := make([]K, 0, r.Size()) - r.data.Range(func(key, value any) bool { + r.data.Range(func(key, _ any) bool { keys = append(keys, key.(K)) return true }) return keys } -// Values returns all values stored in the container. +// Values returns a snapshot slice of all values. The order is unspecified. func (r *SMapKeyValue[K, T]) Values() []T { values := make([]T, 0, r.Size()) - r.data.Range(func(key, value any) bool { + r.data.Range(func(_, value any) bool { values = append(values, value.(T)) return true }) return values } -// ForEach calls the given function for each key-value pair in the container. +// All returns an iterator over all key-value pairs, suitable for use with a +// range-over-func loop: +// +// for k, v := range sm.All() { +// // ... +// } +// +// Iteration reflects a moment-in-time view of the map; concurrent writes may or +// may not be observed. Break out of the loop early to stop iterating. +func (r *SMapKeyValue[K, T]) All() iter.Seq2[K, T] { + return func(yield func(K, T) bool) { + r.data.Range(func(key, value any) bool { + return yield(key.(K), value.(T)) + }) + } +} + +// ForEach calls fn for every key-value pair. Prefer [SMapKeyValue.All] with a +// range-over-func loop in new code. func (r *SMapKeyValue[K, T]) ForEach(fn func(key K, value T)) { r.data.Range(func(key, value any) bool { fn(key.(K), value.(T)) @@ -164,141 +170,146 @@ func (r *SMapKeyValue[K, T]) ForEach(fn func(key K, value T)) { }) } -// ForEachKey calls the given function for each key in the container. +// ForEachKey calls fn for every key. func (r *SMapKeyValue[K, T]) ForEachKey(fn func(key K)) { - r.data.Range(func(key, value any) bool { + r.data.Range(func(key, _ any) bool { fn(key.(K)) return true }) } -// ForEachValue calls the given function for each value in the container. +// ForEachValue calls fn for every value. func (r *SMapKeyValue[K, T]) ForEachValue(fn func(value T)) { - r.data.Range(func(key, value any) bool { + r.data.Range(func(_, value any) bool { fn(value.(T)) return true }) } -// Clone returns a new SMapKeyValue with a copy of the underlying data. +// Clone returns a new independent container holding a copy of the data. func (r *SMapKeyValue[K, T]) Clone() *SMapKeyValue[K, T] { clone := NewSMapKeyValue[K, T]() - r.data.Range(func(key, value any) bool { clone.Set(key.(K), value.(T)) return true }) - return clone } -// CloneAndClear returns a new SMapKeyValue with a copy of the underlying data and clears the container. +// CloneAndClear copies the data into a new container and clears the receiver. +// The two operations are not a single atomic step: concurrent writes that land +// between the copy and the clear are observed by neither container reliably. func (r *SMapKeyValue[K, T]) CloneAndClear() *SMapKeyValue[K, T] { - clone := NewSMapKeyValue[K, T]() - r.data.Range(func(key, value any) bool { - clone.Set(key.(K), value.(T)) - return true - }) + clone := r.Clone() r.Clear() return clone } -// DeepEqual returns true if the given kv is deep equal to the SMapKeyValue container -func (r *SMapKeyValue[K, T]) DeepEqual(kv *SMapKeyValue[K, T]) bool { - if r.Size() != kv.Size() { - return false +// Merge copies every entry from other into the receiver, overwriting existing +// keys. A nil other, or merging a container into itself, is ignored. +func (r *SMapKeyValue[K, T]) Merge(other *SMapKeyValue[K, T]) { + if other == nil || other == r { + return } - if (r.Size() == kv.Size()) && r.Size() == 0 { + other.data.Range(func(key, value any) bool { + r.Set(key.(K), value.(T)) return true + }) +} + +// DeepEqual reports whether the receiver and other hold the same keys mapped to +// deeply equal values ([reflect.DeepEqual]). A nil other equals the receiver +// only when the receiver is empty. +func (r *SMapKeyValue[K, T]) DeepEqual(other *SMapKeyValue[K, T]) bool { + otherSize := 0 + if other != nil { + otherSize = other.Size() + } + if r.Size() != otherSize { + return false } - var ret bool + equal := true r.data.Range(func(key, value any) bool { - kk, ok := kv.GetAndCheck(key.(K)) - if !ok { - ret = false + ov, ok := other.GetAndCheck(key.(K)) + if !ok || !reflect.DeepEqual(value.(T), ov) { + equal = false return false - } else { - ret = reflect.DeepEqual(kk, value.(T)) - return true } + return true }) - - return ret + return equal } -// Map returns a new SMapKeyValue after applying the given function fn to each key-value pair. -func (r *SMapKeyValue[K, T]) Map(fn func(key K, value T) (newKey K, newValue T)) *SMapKeyValue[K, T] { - m := NewSMapKeyValue[K, T]() +// Map returns a new container produced by applying fn to every pair. +func (r *SMapKeyValue[K, T]) Map(fn func(key K, value T) (K, T)) *SMapKeyValue[K, T] { + out := NewSMapKeyValue[K, T]() r.data.Range(func(key, value any) bool { - newKey, newValue := fn(key.(K), value.(T)) - m.Set(newKey, newValue) + nk, nv := fn(key.(K), value.(T)) + out.Set(nk, nv) return true }) - - return m + return out } -// MapKey returns a new SMapKeyValue after applying the given function fn to each key. +// MapKey returns a new container with each key transformed by fn. func (r *SMapKeyValue[K, T]) MapKey(fn func(key K) K) *SMapKeyValue[K, T] { - m := NewSMapKeyValue[K, T]() + out := NewSMapKeyValue[K, T]() r.data.Range(func(key, value any) bool { - newKey := fn(key.(K)) - m.Set(newKey, value.(T)) + out.Set(fn(key.(K)), value.(T)) return true }) - return m + return out } -// MapValue returns a new SMapKeyValue after applying the given function fn to each value. +// MapValue returns a new container with each value transformed by fn. func (r *SMapKeyValue[K, T]) MapValue(fn func(value T) T) *SMapKeyValue[K, T] { - m := NewSMapKeyValue[K, T]() + out := NewSMapKeyValue[K, T]() r.data.Range(func(key, value any) bool { - newValue := fn(value.(T)) - m.Set(key.(K), newValue) + out.Set(key.(K), fn(value.(T))) return true }) - return m + return out } -// Filter returns a new SMapKeyValue after applying the given function fn to each key-value pair. +// Filter returns a new container with the pairs for which fn reports true. func (r *SMapKeyValue[K, T]) Filter(fn func(key K, value T) bool) *SMapKeyValue[K, T] { - m := NewSMapKeyValue[K, T]() + out := NewSMapKeyValue[K, T]() r.data.Range(func(key, value any) bool { if fn(key.(K), value.(T)) { - m.Set(key.(K), value.(T)) + out.Set(key.(K), value.(T)) } return true }) - return m + return out } -// FilterKey returns a new SMapKeyValue after applying the given function fn to each key. +// FilterKey returns a new container with the pairs whose key satisfies fn. func (r *SMapKeyValue[K, T]) FilterKey(fn func(key K) bool) *SMapKeyValue[K, T] { - m := NewSMapKeyValue[K, T]() + out := NewSMapKeyValue[K, T]() r.data.Range(func(key, value any) bool { if fn(key.(K)) { - m.Set(key.(K), value.(T)) + out.Set(key.(K), value.(T)) } return true }) - return m + return out } -// FilterValue returns a new SMapKeyValue after applying the given function fn to each value. +// FilterValue returns a new container with the pairs whose value satisfies fn. func (r *SMapKeyValue[K, T]) FilterValue(fn func(value T) bool) *SMapKeyValue[K, T] { - m := NewSMapKeyValue[K, T]() + out := NewSMapKeyValue[K, T]() r.data.Range(func(key, value any) bool { if fn(value.(T)) { - m.Set(key.(K), value.(T)) + out.Set(key.(K), value.(T)) } return true }) - return m + return out } -// Partition returns two new SMapKeyValue. One with all the elements that satisfy the predicate and -// another with the rest. The predicate is applied to each element. +// Partition splits the container into match (pairs for which fn is true) and +// others (the rest), returning two new containers. func (r *SMapKeyValue[K, T]) Partition(fn func(key K, value T) bool) (match, others *SMapKeyValue[K, T]) { match = NewSMapKeyValue[K, T]() others = NewSMapKeyValue[K, T]() @@ -310,12 +321,10 @@ func (r *SMapKeyValue[K, T]) Partition(fn func(key K, value T) bool) (match, oth } return true }) - - return + return match, others } -// PartitionKey returns two new SMapKeyValue. One with all the elements that satisfy the predicate and -// another with the rest. The predicate is applied to each key. +// PartitionKey splits the container by applying fn to each key. func (r *SMapKeyValue[K, T]) PartitionKey(fn func(key K) bool) (match, others *SMapKeyValue[K, T]) { match = NewSMapKeyValue[K, T]() others = NewSMapKeyValue[K, T]() @@ -327,12 +336,10 @@ func (r *SMapKeyValue[K, T]) PartitionKey(fn func(key K) bool) (match, others *S } return true }) - - return + return match, others } -// PartitionValue returns two new SMapKeyValue. One with all the elements that satisfy the predicate and -// another with the rest. The predicate is applied to each value. +// PartitionValue splits the container by applying fn to each value. func (r *SMapKeyValue[K, T]) PartitionValue(fn func(value T) bool) (match, others *SMapKeyValue[K, T]) { match = NewSMapKeyValue[K, T]() others = NewSMapKeyValue[K, T]() @@ -344,41 +351,64 @@ func (r *SMapKeyValue[K, T]) PartitionValue(fn func(value T) bool) (match, other } return true }) - return + return match, others } -// SortKeys returns a []*K (keys) after sorting the keys using the given sortFn function. -func (r *SMapKeyValue[K, T]) SortKeys(sortFn func(key1, key2 K) bool) []*K { +// SortKeys returns all keys sorted by the less function, which must report +// whether a should sort before b. +func (r *SMapKeyValue[K, T]) SortKeys(less func(a, b K) bool) []K { keys := r.Keys() - - sort.Slice(keys, func(i, j int) bool { - return sortFn(keys[i], keys[j]) + slices.SortFunc(keys, func(a, b K) int { + switch { + case less(a, b): + return -1 + case less(b, a): + return 1 + default: + return 0 + } }) + return keys +} - m := make([]*K, len(keys)) - for i, key := range keys { - k := key - m[i] = &k - } - return m +// SortValues returns all values sorted by the less function, which must report +// whether a should sort before b. +func (r *SMapKeyValue[K, T]) SortValues(less func(a, b T) bool) []T { + values := r.Values() + slices.SortFunc(values, func(a, b T) int { + switch { + case less(a, b): + return -1 + case less(b, a): + return 1 + default: + return 0 + } + }) + return values } -// SortValues returns a []*T (values) after sorting the values using given function sortFn. -func (r *SMapKeyValue[K, T]) SortValues(sortFn func(value1, value2 T) bool) []*T { - kvs := make([]*skv[K, T], 0, r.Size()) +// MarshalJSON encodes the container as a JSON object. Encoding succeeds only for +// key types that encoding/json accepts as object keys (strings, integers, and +// encoding.TextMarshaler implementations). +func (r *SMapKeyValue[K, T]) MarshalJSON() ([]byte, error) { + out := make(map[K]T, r.Size()) r.data.Range(func(key, value any) bool { - kvs = append(kvs, &skv[K, T]{key.(K), value.(T)}) + out[key.(K)] = value.(T) return true }) + return json.Marshal(out) +} - sort.Slice(kvs, func(i, j int) bool { - return sortFn(kvs[i].value, kvs[j].value) - }) - - m := make([]*T, len(kvs)) - for i, pair := range kvs { - m[i] = &pair.value +// UnmarshalJSON decodes a JSON object into the container, merging the decoded +// entries over any existing ones. +func (r *SMapKeyValue[K, T]) UnmarshalJSON(data []byte) error { + var m map[K]T + if err := json.Unmarshal(data, &m); err != nil { + return err } - - return m + for key, value := range m { + r.Set(key, value) + } + return nil } diff --git a/smapkeyvalue_test.go b/smapkeyvalue_test.go index 0f3171f..975cbe3 100644 --- a/smapkeyvalue_test.go +++ b/smapkeyvalue_test.go @@ -32,18 +32,18 @@ func init() { rand.Seed(time.Now().UnixNano()) // fill the skv_int_int - for i := 0; i < skvSize; i++ { + for range skvSize { skv_int_int.Set(rand.Intn(skvSize), rand.Intn(skvSize)) } // fill the skv_string_string - for i := 0; i < skvSize; i++ { + for range skvSize { keyval := fmt.Sprintf("%x", md5.Sum([]byte(strconv.Itoa(rand.Intn(skvSize))))) skv_string_string.Set(keyval, keyval) } // fill the skv_string_struct - for i := 0; i < skvSize; i++ { + for range skvSize { keyval := fmt.Sprintf("%x", md5.Sum([]byte(strconv.Itoa(rand.Intn(skvSize))))) s := STestStruct{ a: keyval, @@ -69,15 +69,15 @@ func TestNewSMapKeyValue(t *testing.T) { t.Errorf("Expected size to be %v, got %v", 1, kv.Size()) } - value := kv.Get(1) - VKind := reflect.TypeOf(value).Kind().String() + _ = kv.Get(1) + VKind := reflect.TypeFor[int]().Kind().String() if VKind != "int" { t.Errorf("Expected type to be %s, got %s", "int", VKind) } - key := kv.Keys()[0] - kKind := reflect.TypeOf(key).Kind().String() + _ = kv.Keys()[0] + kKind := reflect.TypeFor[int]().Kind().String() if kKind != "int" { t.Errorf("Expected type to be %s, got %s", "int", kKind) @@ -97,15 +97,15 @@ func TestNewSMapKeyValue(t *testing.T) { t.Errorf("Expected size to be %v, got %v", 1, kv.Size()) } - value := kv.Get(1) - VKind := reflect.TypeOf(value).Kind().String() + _ = kv.Get(1) + VKind := reflect.TypeFor[int]().Kind().String() if VKind != "int" { t.Errorf("Expected type to be %s, got %s", "int", VKind) } - key := kv.Keys()[0] - kKind := reflect.TypeOf(key).Kind().String() + _ = kv.Keys()[0] + kKind := reflect.TypeFor[int]().Kind().String() if kKind != "int" { t.Errorf("Expected type to be %s, got %s", "int", kKind) @@ -125,15 +125,15 @@ func TestNewSMapKeyValue(t *testing.T) { t.Errorf("Expected size to be %v, got %v", 1, kv.Size()) } - value := kv.Get(1) - VKind := reflect.TypeOf(value).Kind().String() + _ = kv.Get(1) + VKind := reflect.TypeFor[string]().Kind().String() if VKind != "string" { t.Errorf("Expected type to be %s, got %s", "string", VKind) } - key := kv.Keys()[0] - kKind := reflect.TypeOf(key).Kind().String() + _ = kv.Keys()[0] + kKind := reflect.TypeFor[float64]().Kind().String() if kKind != "float64" { t.Errorf("Expected type to be %s, got %s", "float64", kKind) @@ -157,8 +157,8 @@ func TestNewSMapKeyValue(t *testing.T) { t.Errorf("Expected size to be %v, got %v", 1, kv.Size()) } - value := kv.Get(1) - typeOf := reflect.TypeOf(value) + _ = kv.Get(1) + typeOf := reflect.TypeFor[STestStruct]() kind := typeOf.Kind().String() if kind != "struct" { @@ -169,8 +169,8 @@ func TestNewSMapKeyValue(t *testing.T) { t.Errorf("Expected type to be %s, got %s", "STestStruct", kind) } - key := kv.Keys()[0] - kKind := reflect.TypeOf(key).Kind().String() + _ = kv.Keys()[0] + kKind := reflect.TypeFor[int]().Kind().String() if kKind != "int" { t.Errorf("Expected type to be %s, got %s", "int", kKind) @@ -294,8 +294,8 @@ func TestGet_SMapKeyValue(t *testing.T) { }) } -func TestGetAnDelete_SMapKeyValue(t *testing.T) { - t.Run("test GetAnDelete for NewSMapKeyValue[string, struct] key exist", func(t *testing.T) { +func TestGetAndDelete_SMapKeyValue(t *testing.T) { + t.Run("test GetAndDelete for NewSMapKeyValue[string, struct] key exist", func(t *testing.T) { type STestStruct struct { Name string value float64 @@ -310,9 +310,9 @@ func TestGetAnDelete_SMapKeyValue(t *testing.T) { t.Errorf("Expected size to be %v, got %v", 3, kv.Size()) } - value, ok := kv.GetAnDelete("Archimedes") + value, ok := kv.GetAndDelete("Archimedes") if !ok { - t.Errorf("Expected GetAnDelete to return true, got %v", ok) + t.Errorf("Expected GetAndDelete to return true, got %v", ok) } if value.Name != "This is Archimedes' Constant (Pi)" { @@ -327,7 +327,7 @@ func TestGetAnDelete_SMapKeyValue(t *testing.T) { } }) - t.Run("test GetAnDelete for NewSMapKeyValue[string, struct] key doesn't exist", func(t *testing.T) { + t.Run("test GetAndDelete for NewSMapKeyValue[string, struct] key doesn't exist", func(t *testing.T) { type STestStruct struct { Name string value float64 @@ -340,9 +340,9 @@ func TestGetAnDelete_SMapKeyValue(t *testing.T) { t.Errorf("Expected size to be %v, got %v", 1, kv.Size()) } - value, ok := kv.GetAnDelete("Euler") + value, ok := kv.GetAndDelete("Euler") if ok { - t.Errorf("Expected GetAnDelete to return true, got %v", ok) + t.Errorf("Expected GetAndDelete to return true, got %v", ok) } if value.value != 0 { @@ -548,34 +548,6 @@ func TestIsEmpty_SMapKeyValue(t *testing.T) { }) } -func TestIsFull_SMapKeyValue(t *testing.T) { - t.Run("test IsFull for NewSMapKeyValue[string, struct] with keys", func(t *testing.T) { - type STestStruct struct { - Name string - value float64 - } - kv := NewSMapKeyValue[string, STestStruct]() - - kv.Set("Archimedes", STestStruct{"This is Archimedes' Constant (Pi)", 3.1415}) - kv.Set("Euler", STestStruct{"This is Euler's Number (e)", 2.7182}) - kv.Set("Golden Ratio", STestStruct{"This is The Golden Ratio", 1.6180}) - - if kv.Size() != 3 { - t.Errorf("Expected size to be %v, got %v", 3, kv.Size()) - } - - if kv.IsFull() != true { - t.Errorf("Expected IsFull to be %v, got %v", false, kv.IsFull()) - } - - kv.Clear() - - if kv.IsFull() != false { - t.Errorf("Expected IsFull to be %v, got %v", true, kv.IsFull()) - } - }) -} - func TestContainsKey_SMapKeyValue(t *testing.T) { t.Run("test ContainsKey for NewSMapKeyValue[string, struct] with keys", func(t *testing.T) { type STestStruct struct { @@ -648,32 +620,6 @@ func TestContainsValue_SMapKeyValue(t *testing.T) { }) } -func TestKey_SMapKeyValue(t *testing.T) { - t.Run("test Key for NewSMapKeyValue[string, struct] with keys", func(t *testing.T) { - type STestStruct struct { - Name string - value float64 - } - kv := NewSMapKeyValue[string, STestStruct]() - - kv.Set("Archimedes", STestStruct{"This is Archimedes' Constant (Pi)", 3.1415}) - kv.Set("Euler", STestStruct{"This is Euler's Number (e)", 2.7182}) - kv.Set("Golden Ratio", STestStruct{"This is The Golden Ratio", 1.6180}) - - if kv.Size() != 3 { - t.Errorf("Expected size to be %v, got %v", 3, kv.Size()) - } - - if kv.Key("Archimedes") != "Archimedes" { - t.Errorf("Expected key to be %v, got %v", "Archimedes", kv.Key("Archimedes")) - } - - if kv.Key("Do Not Exist") != "" { - t.Errorf("Expected key to be %v, got %v", "Archimedes", kv.Key("Do Not Exist")) - } - }) -} - func TestKeys_SMapKeyValue(t *testing.T) { t.Run("test Keys for NewSMapKeyValue[string, struct] with keys", func(t *testing.T) { type STestStruct struct { @@ -930,7 +876,7 @@ func TestClone_SMapKeyValue(t *testing.T) { } for _, kvValue := range kvSortedValues { - if kvClone.ContainsValue(*kvValue) == false { + if kvClone.ContainsValue(kvValue) == false { t.Errorf("Expected Clone to contain value, got %v", true) } } @@ -949,7 +895,7 @@ func TestClone_SMapKeyValue(t *testing.T) { t.Errorf("Expected size to be %v, got %v", 3, kvClone.Size()) } - if reflect.DeepEqual(kv, kvClone) == false { + if kv.DeepEqual(kvClone) == false { t.Errorf("Expected Clone to be equal to original, got %v", true) } }) @@ -1013,7 +959,7 @@ func TestCloneAndClear_SMapKeyValue(t *testing.T) { t.Errorf("Expected size to be %v, got %v", 3, kvClone.Size()) } - if reflect.DeepEqual(kv, kvClone) == false { + if kv.DeepEqual(kvClone) == false { t.Errorf("Expected Clone to be equal to original, got %v", true) } }) @@ -1162,7 +1108,7 @@ func TestMap_SMapKeyValue(t *testing.T) { }) newKv.ForEach(func(key string, value STestStruct) { - if kv.Key(key) != key { + if !kv.ContainsKey(key) { t.Errorf("Expected key to be uppercase, want: %v, got %v", strings.ToUpper(key), key) } if strings.ToUpper(kv.Get(key).Name) != value.Name { @@ -1217,8 +1163,8 @@ func TestMapKey_SMapKeyValue(t *testing.T) { }) newKv.ForEach(func(key string, value STestStruct) { - if strings.ToUpper(kv.Key(strings.Title(strings.ToLower(key)))) != key { - t.Errorf("Expected key to be uppercase, want: %v, got %v", kv.Key(strings.Title(strings.ToLower(key))), key) + if !kv.ContainsKey(strings.Title(strings.ToLower(key))) { + t.Errorf("Expected key to be uppercase, want: %v, got %v", strings.Title(strings.ToLower(key)), key) } if kv.Get(strings.Title(strings.ToLower(key))).Name != value.Name { t.Errorf("Expected value.Name to be uppercase, want: %v, got %v", kv.Get(strings.Title(strings.ToLower(key))).Name, value.Name) @@ -1272,8 +1218,8 @@ func TestMapValue_SMapKeyValue(t *testing.T) { }) newKv.ForEach(func(key string, value STestStruct) { - if kv.Key(key) != key { - t.Errorf("Expected key to be uppercase, want: %v, got %v", kv.Key(key), key) + if !kv.ContainsKey(key) { + t.Errorf("Expected key to be uppercase, want: %v, got %v", key, key) } if strings.ToUpper(kv.Get(key).Name) != value.Name { t.Errorf("Expected value.Name to be uppercase, want: %v, got %v", kv.Get(key).Name, value.Name) @@ -1328,7 +1274,7 @@ func TestFilter_SMapKeyValue(t *testing.T) { newKv.ForEach(func(key string, value STestStruct) { if key != "Archimedes" { - t.Errorf("Expected key to be uppercase, want: %v, got %v", kv.Key(key), key) + t.Errorf("Expected key to be uppercase, want: %v, got %v", key, key) } if value.Name != "This is Archimedes' Constant (Pi)" { t.Errorf("Expected value.Name to be uppercase, want: %v, got %v", "This is Archimedes' Constant (Pi)", value.Name) @@ -1381,7 +1327,7 @@ func TestFilterKey_SMapKeyValue(t *testing.T) { newKv.ForEach(func(key string, value STestStruct) { if key != "Archimedes" { - t.Errorf("Expected key to be uppercase, want: %v, got %v", kv.Key(key), key) + t.Errorf("Expected key to be uppercase, want: %v, got %v", key, key) } if value.Name != "This is Archimedes' Constant (Pi)" { t.Errorf("Expected value.Name to be uppercase, want: %v, got %v", "This is Archimedes' Constant (Pi)", value.Name) @@ -1438,7 +1384,7 @@ func TestFilterValue_SMapKeyValue(t *testing.T) { newKv.ForEach(func(key string, value STestStruct) { if key != "Archimedes" { - t.Errorf("Expected key to be uppercase, want: %v, got %v", kv.Key(key), key) + t.Errorf("Expected key to be uppercase, want: %v, got %v", key, key) } if value.Name != "This is Archimedes' Constant (Pi)" { t.Errorf("Expected value.Name to be uppercase, want: %v, got %v", "This is Archimedes' Constant (Pi)", value.Name) @@ -1498,7 +1444,7 @@ func TestPartition_SMapKeyValue(t *testing.T) { grp1Kv.ForEach(func(key string, value STestStruct) { if key != "Archimedes" { - t.Errorf("Expected key to be uppercase, want: %v, got %v", kv.Key(key), key) + t.Errorf("Expected key to be uppercase, want: %v, got %v", key, key) } if value.Name != "This is Archimedes' Constant (Pi)" { t.Errorf("Expected value.Name to be uppercase, want: %v, got %v", "This is Archimedes' Constant (Pi)", value.Name) @@ -1510,7 +1456,7 @@ func TestPartition_SMapKeyValue(t *testing.T) { grp2Kv.ForEach(func(key string, value STestStruct) { if key != "Euler" && key != "Golden Ratio" { - t.Errorf("Expected key to be uppercase, want: %v, got %v", kv.Key(key), key) + t.Errorf("Expected key to be uppercase, want: %v, got %v", key, key) } if value.Name != "This is Euler's Number (e)" && value.Name != "This is The Golden Ratio" { t.Errorf("Expected value.Name to be uppercase, want: %v, got %v", "This is Euler's Number (e)", value.Name) @@ -1574,7 +1520,7 @@ func TestPartitionKey_SMapKeyValue(t *testing.T) { grp1Kv.ForEach(func(key string, value STestStruct) { if key != "Archimedes" { - t.Errorf("Expected key to be uppercase, want: %v, got %v", kv.Key(key), key) + t.Errorf("Expected key to be uppercase, want: %v, got %v", key, key) } if value.Name != "This is Archimedes' Constant (Pi)" { t.Errorf("Expected value.Name to be uppercase, want: %v, got %v", "This is Archimedes' Constant (Pi)", value.Name) @@ -1586,7 +1532,7 @@ func TestPartitionKey_SMapKeyValue(t *testing.T) { grp2Kv.ForEach(func(key string, value STestStruct) { if key != "Euler" && key != "Golden Ratio" { - t.Errorf("Expected key to be uppercase, want: %v, got %v", kv.Key(key), key) + t.Errorf("Expected key to be uppercase, want: %v, got %v", key, key) } if value.Name != "This is Euler's Number (e)" && value.Name != "This is The Golden Ratio" { t.Errorf("Expected value.Name to be uppercase, want: %v, got %v", "This is Euler's Number (e)", value.Name) @@ -1650,7 +1596,7 @@ func TestPartitionValue_SMapKeyValue(t *testing.T) { grp1Kv.ForEach(func(key string, value STestStruct) { if key != "Archimedes" { - t.Errorf("Expected key to be uppercase, want: %v, got %v", kv.Key(key), key) + t.Errorf("Expected key to be uppercase, want: %v, got %v", key, key) } if value.Name != "This is Archimedes' Constant (Pi)" { t.Errorf("Expected value.Name to be uppercase, want: %v, got %v", "This is Archimedes' Constant (Pi)", value.Name) @@ -1662,7 +1608,7 @@ func TestPartitionValue_SMapKeyValue(t *testing.T) { grp2Kv.ForEach(func(key string, value STestStruct) { if key != "Euler" && key != "Golden Ratio" { - t.Errorf("Expected key to be uppercase, want: %v, got %v", kv.Key(key), key) + t.Errorf("Expected key to be uppercase, want: %v, got %v", key, key) } if value.Name != "This is Euler's Number (e)" && value.Name != "This is The Golden Ratio" { t.Errorf("Expected value.Name to be uppercase, want: %v, got %v", "This is Euler's Number (e)", value.Name) @@ -1721,8 +1667,8 @@ func TestSortKeys_SMapKeyValue(t *testing.T) { t.Errorf("Expected size to be %v, got %v", 3, len(kSorted)) } - if *kSorted[0] != "Archimedes" { - t.Errorf("Expected key to be uppercase, want: %v, got %v", "Archimedes", *kSorted[0]) + if kSorted[0] != "Archimedes" { + t.Errorf("Expected key to be uppercase, want: %v, got %v", "Archimedes", kSorted[0]) } })