From 73a60fa82c861754ec1ad6811f8914c8d1d85a41 Mon Sep 17 00:00:00 2001 From: Michael Primeaux Date: Mon, 28 Oct 2024 18:10:28 -0500 Subject: [PATCH] FEATURE: Optimized implementation. (#4) * Updated README * Optimized to reduce all allocs/op to 2 * Added test for Generate() --- .golangci.yaml | 48 +----- CHANGELOG/CHANGELOG-1.x.md | 17 +- CODE_OF_CONDUCT.md | 133 +++++++++++++++ CONTRIBUTING.md | 146 +++++++++++++++++ README.md | 265 ++++++++++++++++++------------ nanoid.go | 223 +++++++++++++++++--------- nanoid_benchmark_test.go | 321 +++++++++++++++++++++++++++++-------- nanoid_test.go | 289 +++++++++++++++------------------ 8 files changed, 1001 insertions(+), 441 deletions(-) create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md diff --git a/.golangci.yaml b/.golangci.yaml index da6e1c7..944fbfa 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -15,6 +15,12 @@ run: # Default: []. build-tags: [] + # Exclude certain directories from linting + exclude-dirs: + - "vendor" + - "third_party" + - "generated" + # Which dirs to skip: issues from them won't be reported. # Can use regexp here: `generated.*`, regexp is applied on full path, # including the path prefix if one is set. @@ -49,48 +55,6 @@ run: # If false (default) - golangci-lint acquires file lock on start. allow-parallel-runners: false -linters: - enable: - - asciicheck - - bodyclose - - cyclop - - dogsled - - dupl - - durationcheck - - errname - - errorlint - - exhaustive - - exportloopref - - forbidigo - - forcetypeassert - - funlen - - gochecknoinits - - goconst - - gocritic - - godot - - godox - - gofmt - - goimports - - gomnd - - gosec - - lll - - makezero - - misspell - - nakedret - - nestif - - nilerr - - nlreturn - - noctx - - nolintlint - - predeclared - - revive - - stylecheck - - tagliatelle - - unconvert - - unparam - - wastedassign - - whitespace - output: sort-results: true diff --git a/CHANGELOG/CHANGELOG-1.x.md b/CHANGELOG/CHANGELOG-1.x.md index 937c66d..30432e2 100644 --- a/CHANGELOG/CHANGELOG-1.x.md +++ b/CHANGELOG/CHANGELOG-1.x.md @@ -1,4 +1,5 @@ # Changelog + All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), @@ -14,6 +15,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed ### Security +--- +## [1.5.0] - 2024-OCT-28 + +### Added +- **FEATURE**: Added Code of Conduct +- **FEATURE**: Added Contribution Guidelines +### Changed +- **DEBT:** Optimized overall implementation to reduce the allocations per operation to 2. +### Deprecated +### Removed +### Fixed +### Security + --- ## [1.4.0] - 2024-OCT-26 @@ -65,7 +79,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed ### Security -[Unreleased]: https://github.com/scriptures-social/platform/compare/v1.4.0...HEAD +[Unreleased]: https://github.com/scriptures-social/platform/compare/v1.5.0...HEAD +[1.5.0]: https://github.com/sixafter/nanoid/compare/v1.4.0...v1.5.0 [1.4.0]: https://github.com/sixafter/nanoid/compare/v1.3.0...v1.4.0 [1.3.0]: https://github.com/sixafter/nanoid/compare/v1.2.0...v1.3.0 [1.2.0]: https://github.com/sixafter/nanoid/compare/v1.0.0...v1.2.0 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..20bcc4f --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,133 @@ + +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official email address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +[INSERT CONTACT METHOD]. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..471e0fb --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,146 @@ +# Contributing + +Thank you for your interest in contributing to **NanoID for Go**! Your contributions help make this project better for everyone. This guide outlines the process for contributing to the project, including reporting issues, submitting pull requests, and adhering to the project's code of conduct. + +## 📜 Table of Contents + +- [Code of Conduct](#code-of-conduct) +- [How to Contribute](#how-to-contribute) + - [Reporting Bugs](#reporting-bugs) + - [Requesting Features](#requesting-features) + - [Submitting Changes](#submitting-changes) +- [Coding Guidelines](#coding-guidelines) + - [Style Guidelines](#style-guidelines) +- [Security Considerations](#-security-considerations) +- [Pull Request Process](#pull-request-process) +--- + +## 🛡️ Code of Conduct + +This project adheres to the [Contributor Covenant Code of Conduct](https://github.com/sixafter/nanoid/blob/main/CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to [osint@sixafter.com](mailto:osint@sixafter.com). + +--- + +## 🤝 How to Contribute + +There are several ways you can contribute to NanoID for Go: + +### 🐛 Reporting Bugs + +If you encounter a bug or unexpected behavior: + +1. **Search Existing Issues**: Check if the issue has already been reported [here](https://github.com/sixafter/nanoid/issues). +2. **Open a New Issue**: If not, open a new issue describing the bug in detail. + - **Provide a Clear Title**: Summarize the problem. + - **Describe the Behavior**: Explain what you expected to happen versus what actually happened. + - **Steps to Reproduce**: Include code snippets or commands that can help reproduce the issue. + - **Environment Details**: Mention your Go version, operating system, and any other relevant information. + +### 🌟 Requesting Features + +To suggest new features or improvements: + +1. **Search Existing Features**: Ensure your idea hasn't been discussed [here](https://github.com/sixafter/nanoid/issues?q=is%3Aissue+is%3Aopen+label%3Afeature). +2. **Open a New Feature Request**: Provide a detailed description of the feature. + - **Describe the Feature**: Explain what the feature is and why it's needed. + - **Use Cases**: Provide examples of how the feature would be used. + - **Potential Implementation**: If possible, suggest how it could be implemented. + +### ✨ Submitting Changes + +Contributions in the form of bug fixes, improvements, or new features are welcome! + +#### 1. Fork the Repository + +Fork the repository to your GitHub account by clicking the **Fork** button at the top right of the repository page. + +#### 2. Clone Your Fork + +Clone the forked repository to your local machine: + +```bash +git clone https://github.com/sixafter/nanoid.git +cd nanoid +``` + +#### 3. Create a New Branch + +Create a new branch for your changes: + +```bash +git checkout -b feature/your-feature-name +``` + +#### 4. Make Your Changes + +#### 5. Run Tests and Linters + +Ensure all tests pass and the code adheres to the style guidelines: + +```bash +make lint +make test +``` + +#### 6. Commit Your Changes + +Commit your changes with a clear and descriptive message: + +```bash +git add . +git commit -m "Add descriptive commit message" +``` + +#### 7. Push to Your Fork + +Push your changes to your forked repository: + +```bash +git push origin feature/your-feature-name +``` + +#### 8. Open a Pull Request + +Navigate to the original repository and click New Pull Request. Provide a clear description of your changes and link any related issues. + +## 🎨 Coding Guidelines + +Adhering to consistent coding standards ensures the codebase remains clean, readable, and maintainable. + +### 🖋️ Style Guidelines + +* **Formatting**: Use `make fmt` to format your code. +* **Linting**: Follow the linting rules defined in `.golangci.yaml`. Ensure that your code passes all linters before submitting. +* **Documentation**: Document public functions, types, and methods using Go's standard documentation conventions. +* **Error Handling**: Handle errors gracefully and consistently. Use the predefined error types where applicable. + +## 🔒 Security Considerations + +* **Randomness**: Ensure that all randomness sources use cryptographically secure methods (crypto/rand). +* **Data Sanitization**: Avoid exposing sensitive data through IDs or logs. + +## 🚀 Pull Request Process + +Follow these steps to create a successful pull request (PR): + +1. Ensure Your Branch is Up-to-Date + * Before opening a PR, make sure your branch is based on the latest main branch. +2. Create a Pull Request +3. Address Feedback + * **Respond Promptly**: Engage with reviewers by responding to comments and making necessary changes. + * **Update Your PR**: Push additional commits to your branch to address feedback. +4. Merge the PR + * Once approved and all checks pass, your PR will be merged by a maintainer. + +## 📝 Additional Resources + +* [Go Documentation](https://go.dev/doc/) +* [GolangCI-Lint Documentation](https://golangci-lint.run) +* [GitHub Flow](https://docs.github.com/en/get-started/using-github/github-flow) +* [Contributor Covenant Code of Conduct](https://github.com/sixafter/nanoid/blob/main/CODE_OF_CONDUCT.md) + +## 🙏 Thank You! + +We appreciate your interest in contributing to NanoID for Go! Your efforts help improve the project and support the community. If you have any questions or need assistance, feel free to reach out by opening an issue or contacting the maintainers. + +Happy coding! 🎉 \ No newline at end of file diff --git a/README.md b/README.md index e038f89..b2b85ab 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # NanoID +[![Go](https://img.shields.io/github/go-mod/go-version/sixafter/nanoid)](https://img.shields.io/github/go-mod/go-version/sixafter/nanoid) [![Go Reference](https://pkg.go.dev/badge/github.com/sixafter/nanoid.svg)](https://pkg.go.dev/github.com/sixafter/nanoid) [![Go Report Card](https://goreportcard.com/badge/github.com/sixafter/nanoid)](https://goreportcard.com/report/github.com/sixafter/nanoid) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) @@ -8,18 +9,13 @@ A simple, fast, and efficient Go implementation of [NanoID](https://github.com/a ## Features -* **Stateless Design**: Each function operates independently without relying on global state or caches, eliminating the need for synchronization primitives like mutexes. This design ensures predictable behavior and simplifies usage in various contexts. -* **Cryptographically Secure**: Utilizes Go's crypto/rand package for generating cryptographically secure random numbers. This guarantees that the generated IDs are both unpredictable and suitable for security-sensitive applications. -* **High Performance**: Optimized algorithms and efficient memory management techniques ensure rapid ID generation. Whether you're generating a few IDs or millions, the library maintains consistent speed and responsiveness. -* **Memory Efficient**: Implements sync.Pool to reuse byte slices, minimizing memory allocations and reducing garbage collection overhead. This approach significantly enhances performance, especially in high-throughput scenarios. -* **Thread-Safe**: Designed for safe concurrent use in multi-threaded applications. Multiple goroutines can generate IDs simultaneously without causing race conditions or requiring additional synchronization. -* **Customizable**: Offers flexibility to specify custom ID lengths and alphabets. Whether you need short, compact IDs or longer, more complex ones, the library can accommodate your specific requirements. -* **User-Friendly API**: Provides a simple and intuitive API with sensible defaults, making integration straightforward. Developers can start generating IDs with minimal configuration and customize as needed. -* **Zero External Dependencies**: Relies solely on Go's standard library, ensuring ease of use, compatibility, and minimal footprint within your projects. -* **Comprehensive Testing**: Includes a robust suite of unit tests and concurrency tests to ensure reliability, correctness, and thread safety. This commitment to quality guarantees consistent performance across different use cases. -* **Detailed Documentation**: Accompanied by clear and thorough documentation, including examples and usage guidelines. New users can quickly understand how to implement and customize the library to fit their needs. -* **Efficient Error Handling**: Employs predefined errors to avoid unnecessary allocations, enhancing both performance and clarity in error management. -* **Optimized for Low Allocations**: Carefully structured to minimize heap allocations, reducing memory overhead and improving cache locality. This optimization is crucial for applications where performance and resource usage are critical. +- **Short & Unique IDs**: Generates compact and collision-resistant identifiers. +- **Cryptographically Secure**: Utilizes Go's crypto/rand package for generating cryptographically secure random numbers. This guarantees that the generated IDs are both unpredictable and suitable for security-sensitive applications. +- **Customizable Alphabet**: Define your own set of characters for ID generation. +- **Concurrency Safe**: Designed to be safe for use in concurrent environments. +- **High Performance**: Optimized with buffer pooling to minimize allocations and enhance speed. +- **Zero Dependencies**: Lightweight implementation with no external dependencies beyond the standard library. +- **Optimized for Low Allocations**: Carefully structured to minimize heap allocations, reducing memory overhead and improving cache locality. This optimization is crucial for applications where performance and resource usage are critical. ## Installation @@ -44,11 +40,20 @@ import "github.com/sixafter/nanoid" Generate a Nano ID using the default size (21 characters) and default alphabet: ```go -id, err := nanoid.New() -if err != nil { - log.Fatal(err) +package main + +import ( + "fmt" + "github.com/sixafter/nanoid" +) + +func main() { + id, err := nanoid.Generate() + if err != nil { + panic(err) + } + fmt.Println("Generated ID:", id) } -fmt.Println("Generated Nano ID:", id) ``` ### Generating a NanoID with Custom Size @@ -56,98 +61,148 @@ fmt.Println("Generated Nano ID:", id) Generate a NanoID with a custom length: ```go -id, err := nanoid.NewSize(32) -if err != nil { - log.Fatal(err) +package main + +import ( + "fmt" + "github.com/sixafter/nanoid" +) + +func main() { + id, err := nanoid.GenerateSize(10) + if err != nil { + panic(err) + } + fmt.Println("Generated ID:", id) } -fmt.Println("Generated Nano ID of size 32:", id) ``` ### Generate a Nano ID with Custom Alphabet -Generate a Nano ID using a custom alphabet: +Create a custom generator with a specific alphabet and use it to generate IDs: ```go -alphabet := "abcdef123456" -id, err := nanoid.NewCustom(16, alphabet) -if err != nil { - log.Fatal(err) +package main + +import ( + "fmt" + "github.com/sixafter/nanoid" +) + +func main() { + // Define a custom alphabet + alphabet := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + + // Create a new generator + generator, err := nanoid.New(alphabet, nil) // nil uses crypto/rand as the default + if err != nil { + panic(err) + } + + // Generate a Nano ID + id, err := generator.Generate(10) // Custom length: 10 + if err != nil { + panic(err) + } + fmt.Println("Generated ID:", id) } -fmt.Println("Generated Nano ID with custom alphabet:", id) ``` -### Generate a Nano ID with Custom Random Source +## Functions + +### `Generate` -Generate a Nano ID using a custom random source that implements io.Reader: +Generates a Nano ID with the specified length using the default generator. ```go -// Example custom random source (for demonstration purposes) -var myRandomSource io.Reader = myCustomRandomReader{} +func Generate(length int) (string, error) +``` -id, err := nanoid.NewCustomReader(21, nanoid.DefaultAlphabet, myRandomSource) -if err != nil { - log.Fatal(err) -} -fmt.Println("Generated Nano ID with custom random source:", id) +* Parameters: + * `length` (`int`): The desired length of the Nano ID. Must be a positive integer. +* Returns: + * `string`: The generated Nano ID. + * `error`: An error if the generation fails. + +### `New` + +Creates a new Nano ID generator with a custom alphabet and random source. + +```go +func New(alphabet string, randReader io.Reader) (Generator, error) ``` -**Note:** Replace `myCustomRandomReader{}` with your actual implementation of `io.Reader`. +* Parameters: + * `alphabet` (`string`): The set of characters to use for generating IDs. Must not be empty, too short, or contain duplicate characters. + * `randReader` (`io.Reader`): The source of randomness. If `nil`, `crypto/rand` is used by default. +* Returns: + * `Generator`: A new Nano ID generator. + * `error`: An error if the configuration is invalid. -## Thread Safety +### `Generator` Interface -All functions provided by this package are safe for concurrent use by multiple goroutines. Here's an example of generating Nano IDs concurrently: +Defines the method to generate Nano IDs. ```go -package main +type Generator interface { + Generate(size int) (string, error) +} +``` -import ( - "fmt" - "log" - "sync" +### `Configuration` Interface - "github.com/sixafter/nanoid" -) +Provides access to the generator's configuration. -func main() { - const numGoroutines = 10 - const idSize = 21 - - var wg sync.WaitGroup - wg.Add(numGoroutines) - - for i := 0; i < numGoroutines; i++ { - go func() { - defer wg.Done() - id, err := nanoid.New() - if err != nil { - log.Fatal(err) - } - fmt.Println("Generated Nano ID:", id) - }() - } - - wg.Wait() +```go +type Configuration interface { + GetConfig() Config } ``` -## Functions +### `Config` Struct + +Holds the configuration details for the generator. + +```go +type Config struct { + Alphabet []byte + AlphabetLen int + Mask byte + Step int +} +``` + +## Error Handling -* `func New() (string, error)`: Generates a Nano ID with the default size (21 characters) and default alphabet. -* `func NewSize(size int) (string, error)`: Generates a Nano ID with a specified size using the default alphabet. -* `func NewCustom(size int, alphabet string) (string, error)`: Generates a Nano ID with a specified size and custom alphabet. -* `func NewCustomReader(size int, alphabet string, rnd io.Reader) (string, error)`: Generates a Nano ID with a specified size, custom alphabet, and custom random source. +The nanoid module defines several error types to handle various failure scenarios: +* `ErrInvalidLength`: Returned when a non-positive length is specified. +* `ErrExceededMaxAttempts`: Returned when the generation process exceeds the maximum number of attempts. +* `ErrEmptyAlphabet`: Returned when an empty alphabet is provided. +* `ErrAlphabetTooShort`: Returned when the alphabet is shorter than required. +* `ErrAlphabetTooLong`: Returned when the alphabet exceeds the maximum allowed length. +* `ErrDuplicateCharacters`: Returned when the alphabet contains duplicate characters. ## Constants * `DefaultAlphabet`: The default alphabet used for ID generation: `-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz` * `DefaultSize`: The default size of the generated ID: `21` -## Performance +## Performance Optimizations + +### Buffer Pooling with `sync.Pool` + +The nanoid generator utilizes sync.Pool to manage byte slice buffers efficiently. This approach minimizes memory allocations and enhances performance, especially in high-concurrency scenarios. + +How It Works: +* Storing Pointers: `sync.Pool` stores pointers to `[]byte` slices (`*[]byte`) instead of the slices themselves. This avoids unnecessary allocations and aligns with best practices for using `sync.Pool`. +* Zeroing Buffers: Before returning buffers to the pool, they are zeroed out to prevent data leaks. -The package is optimized for performance and low memory consumption: -* **Efficient Random Byte Consumption**: Uses bitwise operations to extract random bits efficiently. -* **Avoids `math/big`**: Does not use `math/big`, relying on built-in integer types for calculations. -* **Minimized System Calls**: Reads random bytes in batches to reduce the number of system calls. +### Struct Optimization + +The `generator` struct is optimized for memory alignment and size by: + +* Removing Embedded Interfaces: Interfaces like `Generator` and `Configuration` are implemented explicitly without embedding, reducing the struct's size and preventing unnecessary padding. +* Ordering Fields by Alignment: Fields are ordered from largest to smallest alignment requirements to minimize padding and optimize memory usage. ## Execute Benchmarks: @@ -166,28 +221,30 @@ go test -bench=. -benchmem ./... goos: darwin goarch: arm64 pkg: github.com/sixafter/nanoid -cpu: Apple M3 Max -BenchmarkNew-16 6329498 189.2 ns/op 40 B/op 3 allocs/op -BenchmarkNewSize/Size10-16 11600679 102.4 ns/op 24 B/op 2 allocs/op -BenchmarkNewSize/Size21-16 6384469 186.7 ns/op 40 B/op 3 allocs/op -BenchmarkNewSize/Size50-16 2680179 448.2 ns/op 104 B/op 6 allocs/op -BenchmarkNewSize/Size100-16 1387914 863.3 ns/op 192 B/op 11 allocs/op -BenchmarkNewCustom/Size10_CustomASCIIAlphabet-16 9306187 128.8 ns/op 24 B/op 2 allocs/op -BenchmarkNewCustom/Size21_CustomASCIIAlphabet-16 5062975 239.4 ns/op 40 B/op 3 allocs/op -BenchmarkNewCustom/Size50_CustomASCIIAlphabet-16 2322037 515.3 ns/op 101 B/op 5 allocs/op -BenchmarkNewCustom/Size100_CustomASCIIAlphabet-16 1235755 972.0 ns/op 182 B/op 9 allocs/op -BenchmarkNew_Concurrent/Concurrency1-16 2368245 513.1 ns/op 40 B/op 3 allocs/op -BenchmarkNew_Concurrent/Concurrency2-16 1940826 609.5 ns/op 40 B/op 3 allocs/op -BenchmarkNew_Concurrent/Concurrency4-16 1986049 585.6 ns/op 40 B/op 3 allocs/op -BenchmarkNew_Concurrent/Concurrency8-16 1999959 602.2 ns/op 40 B/op 3 allocs/op -BenchmarkNew_Concurrent/Concurrency16-16 2018793 595.6 ns/op 40 B/op 3 allocs/op -BenchmarkNewCustom_Concurrent/Concurrency1-16 1960315 611.7 ns/op 40 B/op 3 allocs/op -BenchmarkNewCustom_Concurrent/Concurrency2-16 1790460 673.7 ns/op 40 B/op 3 allocs/op -BenchmarkNewCustom_Concurrent/Concurrency4-16 1766841 670.7 ns/op 40 B/op 3 allocs/op -BenchmarkNewCustom_Concurrent/Concurrency8-16 1768189 677.4 ns/op 40 B/op 3 allocs/op -BenchmarkNewCustom_Concurrent/Concurrency16-16 1765303 689.5 ns/op 40 B/op 3 allocs/op +cpu: Apple M2 Ultra +BenchmarkGenerateDefault-24 3985082 300.7 ns/op 48 B/op 2 allocs/op +BenchmarkGenerateCustomAlphabet-24 3429874 346.0 ns/op 32 B/op 2 allocs/op +BenchmarkGenerateShortID-24 3646383 327.2 ns/op 10 B/op 2 allocs/op +BenchmarkGenerateLongID-24 2557196 468.1 ns/op 128 B/op 2 allocs/op +BenchmarkGenerateMaxAlphabet-24 4532246 263.8 ns/op 32 B/op 2 allocs/op +BenchmarkGenerateMinAlphabet-24 2507995 479.8 ns/op 32 B/op 2 allocs/op +BenchmarkGenerateWithBufferPool-24 3468786 343.9 ns/op 32 B/op 2 allocs/op +BenchmarkGenerateDefaultParallel-24 1530394 790.9 ns/op 48 B/op 2 allocs/op +BenchmarkGenerateCustomAlphabetParallel-24 1386268 861.6 ns/op 32 B/op 2 allocs/op +BenchmarkGenerateShortIDParallel-24 1421832 842.7 ns/op 10 B/op 2 allocs/op +BenchmarkGenerateLongIDParallel-24 1000000 1050 ns/op 128 B/op 2 allocs/op +BenchmarkGenerateExtremeConcurrency-24 1530957 785.7 ns/op 48 B/op 2 allocs/op +BenchmarkGenerateDifferentLengths/Length_5-24 3659472 327.7 ns/op 10 B/op 2 allocs/op +BenchmarkGenerateDifferentLengths/Length_10-24 3436932 346.0 ns/op 32 B/op 2 allocs/op +BenchmarkGenerateDifferentLengths/Length_20-24 3140282 381.1 ns/op 48 B/op 2 allocs/op +BenchmarkGenerateDifferentLengths/Length_50-24 2580222 470.5 ns/op 128 B/op 2 allocs/op +BenchmarkGenerateDifferentLengths/Length_100-24 1936257 617.2 ns/op 224 B/op 2 allocs/op +BenchmarkGenerateDifferentAlphabets/Alphabet_2-24 2510594 479.6 ns/op 32 B/op 2 allocs/op +BenchmarkGenerateDifferentAlphabets/Alphabet_6-24 3452442 346.3 ns/op 32 B/op 2 allocs/op +BenchmarkGenerateDifferentAlphabets/Alphabet_26-24 3901122 308.0 ns/op 32 B/op 2 allocs/op +BenchmarkGenerateDifferentAlphabets/Alphabet_38-24 3562468 336.3 ns/op 32 B/op 2 allocs/op PASS -ok github.com/sixafter/nanoid 33.279s +ok github.com/sixafter/nanoid 34.903s ``` * `ns/op` (Nanoseconds per Operation): @@ -200,17 +257,19 @@ ok github.com/sixafter/nanoid 33.279s * Represents the average number of memory allocations per operation. * `0 allocs/op` is ideal as it indicates no heap allocations. -## Contributing +## Nano ID Generation -Contributions are welcome! Please feel free to submit issues or pull requests. +Nano ID generates unique identifiers based on the following: + +1. Random Byte Generation: Nano ID generates a sequence of random bytes using a secure random source (e.g., crypto/rand.Reader). +2. Mapping to Alphabet: Each random byte is mapped to a character in a predefined alphabet to form the final ID. +3. Uniform Distribution: To ensure that each character in the alphabet has an equal probability of being selected, Nano ID employs techniques to avoid bias, especially when the alphabet size isn't a power of two. + +## Contributing -* Fork the repository. -* Create a new branch for your feature or bugfix. -* Write tests for your changes. -* Ensure all tests pass. -* Submit a pull request. +Contributions are welcome. See [CONTRIBUTING](CODE_OF_CONDUCT) ## License -This project is licensed under the [MIT License](https://choosealicense.com/licenses/mit/). +This project is licensed under the [MIT License](https://choosealicense.com/licenses/mit/). See [LICENSE](LICENSE) file. diff --git a/nanoid.go b/nanoid.go index 33e3778..4c36601 100644 --- a/nanoid.go +++ b/nanoid.go @@ -2,118 +2,189 @@ // // This source code is licensed under the MIT License found in the // LICENSE file in the root directory of this source tree. - // nanoid.go + package nanoid import ( "crypto/rand" "errors" + "fmt" + "io" "math/bits" "sync" ) -// Constants for default settings. -const ( - DefaultAlphabet = "-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz" - DefaultSize = 21 - MaxUintSize = 1024 // Adjust as needed -) +// DefaultGenerator is a global, shared instance of a Nano ID generator. It is safe for concurrent use. +var DefaultGenerator Generator + +// Generate generates a Nano ID using the default generator and the default size. +func Generate() (string, error) { + return GenerateSize(DefaultSize) +} + +// GenerateSize generates a Nano ID using the default generator. +func GenerateSize(length int) (string, error) { + return DefaultGenerator.Generate(length) +} + +func init() { + var err error + DefaultGenerator, err = New(DefaultAlphabet, nil) + if err != nil { + panic(fmt.Sprintf("failed to initialize DefaultGenerator: %v", err)) + } +} -// Predefined errors to avoid allocations on each call. var ( - ErrInvalidSize = errors.New("size must be greater than zero") - ErrSizeExceedsMaxUint = errors.New("size exceeds maximum allowed value") - ErrEmptyAlphabet = errors.New("alphabet must not be empty") - ErrRandomSourceNoData = errors.New("random source returned no data") + ErrInvalidLength = errors.New("length must be positive") + ErrExceededMaxAttempts = errors.New("generate method exceeded maximum attempts, possibly due to invalid mask or alphabet") + ErrEmptyAlphabet = errors.New("alphabet must not be empty") + ErrAlphabetTooShort = errors.New("alphabet length must be at least 2") + ErrAlphabetTooLong = errors.New("alphabet length must not exceed 256") + ErrDuplicateCharacters = errors.New("alphabet contains duplicate characters") ) -// Byte pool to reuse byte slices and minimize allocations. -var bytePool = sync.Pool{ - New: func() interface{} { - b := make([]byte, MaxUintSize) // Non-zero length and capacity +const ( + // DefaultAlphabet Default alphabet as per Nano ID specification. + DefaultAlphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-" + + // DefaultSize Default size of the generated Nano ID: 21. + DefaultSize = 21 +) + +// Generator holds the configuration for the Nano ID generator. +type Generator interface { + Generate(size int) (string, error) +} - return &b - }, +type Configuration interface { + GetConfig() Config } -// New generates a Nano ID with the default size and alphabet using crypto/rand as the random source. -func New() (string, error) { - return NewSize(DefaultSize) +type Config struct { + Alphabet []byte + AlphabetLen int + Mask byte + Step int } -// NewSize generates a Nano ID with a specified size and the default alphabet using crypto/rand as the random source. -func NewSize(size int) (string, error) { - return NewCustom(size, DefaultAlphabet) +type generator struct { + randReader io.Reader + bufferPool *sync.Pool + config Config } -// NewCustom generates a Nano ID with a specified size and custom ASCII alphabet using crypto/rand as the random source. -func NewCustom(size int, alphabet string) (string, error) { - if size <= 0 { - return "", ErrInvalidSize +// New creates a new Generator with buffer pooling enabled. +func New(alphabet string, randReader io.Reader) (Generator, error) { + if len(alphabet) == 0 { + return nil, ErrEmptyAlphabet } - if size > MaxUintSize { - return "", ErrSizeExceedsMaxUint + + if randReader == nil { + randReader = rand.Reader // Initialize here } - if len(alphabet) == 0 { - return "", ErrEmptyAlphabet + + alphabetBytes := []byte(alphabet) + alphabetLen := len(alphabetBytes) + + if alphabetLen < 2 { + return nil, ErrAlphabetTooShort } - return generateASCIIID(size, alphabet) -} + if alphabetLen > 256 { + return nil, ErrAlphabetTooLong + } -// generateASCIIID generates an ID using a byte-based (ASCII) alphabet. -func generateASCIIID(size int, alphabet string) (string, error) { - //nolint:gosec // G115: conversion from int to uint is safe due to prior bounds checking - bitsPerChar := bits.Len(uint(len(alphabet) - 1)) - if bitsPerChar == 0 { - bitsPerChar = 1 + // Check for duplicate characters + seen := make(map[byte]struct{}, alphabetLen) + for _, b := range alphabetBytes { + if _, exists := seen[b]; exists { + return nil, ErrDuplicateCharacters + } + seen[b] = struct{}{} } - // Acquire a pointer to a byte slice from the pool - bufPtr, ok := bytePool.Get().(*[]byte) - if !ok { - panic("bytePool.Get() did not return a *[]byte") + // Calculate mask using power-of-two approach + k := bits.Len(uint(alphabetLen - 1)) + if k == 0 { + return nil, ErrAlphabetTooShort } - buf := *bufPtr - buf = buf[:size] // Slice to desired size + mask := byte((1 << k) - 1) - defer func() { - // Reset the slice back to MaxUintSize before putting it back - *bufPtr = (*bufPtr)[:MaxUintSize] - bytePool.Put(bufPtr) - }() + // Calculate step based on mask + step := (8 * 128) / bits.OnesCount8(mask) - var bitBuffer uint64 - var bitsInBuffer int + // Initialize buffer pool as a pointer + bufferPool := &sync.Pool{ + New: func() interface{} { + b := make([]byte, step) + return &b // Store pointer to slice + }, + } - for i := 0; i < size; { - if bitsInBuffer < bitsPerChar { - var b [8]byte - n, err := rand.Read(b[:]) - if err != nil { - return "", err - } - if n == 0 { - return "", ErrRandomSourceNoData - } - for j := 0; j < n; j++ { - bitBuffer |= uint64(b[j]) << bitsInBuffer - bitsInBuffer += 8 - } + return &generator{ + config: Config{ + Alphabet: alphabetBytes, + AlphabetLen: alphabetLen, + Mask: mask, + Step: step, + }, + randReader: randReader, + bufferPool: bufferPool, // Always assigned + }, nil +} + +// GenerateSize creates a new Nano ID of the specified length. +// It ensures that each character in the ID is selected uniformly from the alphabet. +// Pre-allocated errors are used to minimize memory allocations. +func (g *generator) Generate(length int) (string, error) { + if length <= 0 { + return "", ErrInvalidLength + } + + id := make([]byte, length) + cursor := 0 + maxAttempts := length * 10 // Prevent infinite loops + attempts := 0 + + // Retrieve a pointer to the buffer from the pool + bufferPtr := g.bufferPool.Get().(*[]byte) + buffer := *bufferPtr + defer func() { + for i := range buffer { + buffer[i] = 0 } + g.bufferPool.Put(bufferPtr) // Return the pointer to the pool + }() - mask := uint64((1 << bitsPerChar) - 1) - idx := bitBuffer & mask - bitBuffer >>= bitsPerChar - bitsInBuffer -= bitsPerChar + for cursor < length { + if attempts >= maxAttempts { + return "", ErrExceededMaxAttempts + } + attempts++ - //nolint:gosec // G115: conversion from int to uint is safe due to prior bounds checking - if int(idx) < len(alphabet) { - buf[i] = alphabet[idx] - i++ + n, err := g.randReader.Read(buffer) + if err != nil { + return "", err + } + buffer = buffer[:n] + + for _, rnd := range buffer { + if int(rnd&g.config.Mask) < g.config.AlphabetLen { + id[cursor] = g.config.Alphabet[rnd&g.config.Mask] + cursor++ + if cursor == length { + break + } + } } } - return string(buf), nil + return string(id), nil +} + +// GetConfig returns the configuration for the generator. +func (g *generator) GetConfig() Config { + return g.config } diff --git a/nanoid_benchmark_test.go b/nanoid_benchmark_test.go index 08b9ded..d984ac0 100644 --- a/nanoid_benchmark_test.go +++ b/nanoid_benchmark_test.go @@ -3,95 +3,290 @@ // This source code is licensed under the MIT License found in the // LICENSE file in the root directory of this source tree. -package nanoid_test +package nanoid import ( - "fmt" - "github.com/sixafter/nanoid" - "runtime" + "strconv" "testing" ) -// BenchmarkNew benchmarks the New function with default settings. -func BenchmarkNew(b *testing.B) { +// BenchmarkGenerateDefault benchmarks the default generator with default alphabet and ID length. +func BenchmarkGenerateDefault(b *testing.B) { + b.ReportAllocs() + gen, err := New(DefaultAlphabet, nil) + if err != nil { + b.Fatalf("Failed to create generator: %v", err) + } + + idLength := DefaultSize // Default Nano ID length for i := 0; i < b.N; i++ { - _, err := nanoid.New() + _, err := gen.Generate(idLength) if err != nil { - b.Fatal(err) + b.Fatalf("GenerateSize failed: %v", err) } } } -// BenchmarkNewSize benchmarks the NewSize function with various sizes. -func BenchmarkNewSize(b *testing.B) { - sizes := []int{10, 21, 50, 100} - for _, size := range sizes { - size := size // Capture range variable - b.Run(fmt.Sprintf("Size%d", size), func(b *testing.B) { - for i := 0; i < b.N; i++ { - _, err := nanoid.NewSize(size) - if err != nil { - b.Fatal(err) - } +// BenchmarkGenerateCustomAlphabet benchmarks a custom alphabet generator with ID length 10. +func BenchmarkGenerateCustomAlphabet(b *testing.B) { + b.ReportAllocs() + alphabet := "ABCDEF" + gen, err := New(alphabet, nil) + if err != nil { + b.Fatalf("Failed to create generator: %v", err) + } + + idLength := 10 + for i := 0; i < b.N; i++ { + _, err := gen.Generate(idLength) + if err != nil { + b.Fatalf("GenerateSize failed: %v", err) + } + } +} + +// BenchmarkGenerateShortID benchmarks the generator with a short ID length. +func BenchmarkGenerateShortID(b *testing.B) { + b.ReportAllocs() + gen, err := New("abcdef", nil) + if err != nil { + b.Fatalf("Failed to create generator: %v", err) + } + + idLength := 5 + for i := 0; i < b.N; i++ { + _, err := gen.Generate(idLength) + if err != nil { + b.Fatalf("GenerateSize failed: %v", err) + } + } +} + +// BenchmarkGenerateLongID benchmarks the generator with a long ID length. +func BenchmarkGenerateLongID(b *testing.B) { + b.ReportAllocs() + gen, err := New("abcdef", nil) + if err != nil { + b.Fatalf("Failed to create generator: %v", err) + } + + idLength := 50 + for i := 0; i < b.N; i++ { + _, err := gen.Generate(idLength) + if err != nil { + b.Fatalf("GenerateSize failed: %v", err) + } + } +} + +// BenchmarkGenerateMaxAlphabet benchmarks the generator with the maximum allowed alphabet size (256 characters). +func BenchmarkGenerateMaxAlphabet(b *testing.B) { + b.ReportAllocs() + // Create an alphabet of 256 unique characters + alphabet := make([]byte, 256) + for i := 0; i < 256; i++ { + alphabet[i] = byte(i) + } + gen, err := New(string(alphabet), nil) + if err != nil { + b.Fatalf("Failed to create generator: %v", err) + } + + idLength := 10 + for i := 0; i < b.N; i++ { + _, err := gen.Generate(idLength) + if err != nil { + b.Fatalf("GenerateSize failed: %v", err) + } + } +} + +// BenchmarkGenerateMinAlphabet benchmarks the generator with the minimum allowed alphabet size (2 characters). +func BenchmarkGenerateMinAlphabet(b *testing.B) { + b.ReportAllocs() + alphabet := "AB" + gen, err := New(alphabet, nil) + if err != nil { + b.Fatalf("Failed to create generator: %v", err) + } + + idLength := 10 + for i := 0; i < b.N; i++ { + _, err := gen.Generate(idLength) + if err != nil { + b.Fatalf("GenerateSize failed: %v", err) + } + } +} + +// BenchmarkGenerateWithBufferPool benchmarks the generator with buffer pooling enabled. +func BenchmarkGenerateWithBufferPool(b *testing.B) { + b.ReportAllocs() + gen, err := New("abcdef", nil) + if err != nil { + b.Fatalf("Failed to create generator: %v", err) + } + + idLength := 10 + for i := 0; i < b.N; i++ { + _, err := gen.Generate(idLength) + if err != nil { + b.Fatalf("GenerateSize failed: %v", err) + } + } +} + +// BenchmarkGenerateDefaultParallel benchmarks the default generator in a parallel/concurrent setting. +func BenchmarkGenerateDefaultParallel(b *testing.B) { + gen, err := New(DefaultAlphabet, nil) + if err != nil { + b.Fatalf("Failed to create generator: %v", err) + } + + idLength := DefaultSize // Default Nano ID length + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + _, err := gen.Generate(idLength) + if err != nil { + b.Fatalf("GenerateSize failed: %v", err) } - }) + } + }) +} + +// BenchmarkGenerateCustomAlphabetParallel benchmarks a custom alphabet generator in parallel. +func BenchmarkGenerateCustomAlphabetParallel(b *testing.B) { + gen, err := New("ABCDEF", nil) + if err != nil { + b.Fatalf("Failed to create generator: %v", err) + } + + idLength := 10 + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + _, err := gen.Generate(idLength) + if err != nil { + b.Fatalf("GenerateSize failed: %v", err) + } + } + }) +} + +// BenchmarkGenerateShortIDParallel benchmarks the generator with short IDs in parallel. +func BenchmarkGenerateShortIDParallel(b *testing.B) { + gen, err := New("abcdef", nil) + if err != nil { + b.Fatalf("Failed to create generator: %v", err) + } + + idLength := 5 + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + _, err := gen.Generate(idLength) + if err != nil { + b.Fatalf("GenerateSize failed: %v", err) + } + } + }) +} + +// BenchmarkGenerateLongIDParallel benchmarks the generator with long IDs in parallel. +func BenchmarkGenerateLongIDParallel(b *testing.B) { + gen, err := New("abcdef", nil) + if err != nil { + b.Fatalf("Failed to create generator: %v", err) + } + + idLength := 50 + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + _, err := gen.Generate(idLength) + if err != nil { + b.Fatalf("GenerateSize failed: %v", err) + } + } + }) +} + +// BenchmarkGenerateExtremeConcurrency benchmarks the generator under extreme concurrency. +func BenchmarkGenerateExtremeConcurrency(b *testing.B) { + gen, err := New(DefaultAlphabet, nil) + if err != nil { + b.Fatalf("Failed to create generator: %v", err) } + + idLength := 21 // Default Nano ID length + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + _, err := gen.Generate(idLength) + if err != nil { + b.Fatalf("GenerateSize failed: %v", err) + } + } + }) } -// BenchmarkNewCustom benchmarks the NewCustom function with a custom ASCII alphabet and various sizes. -func BenchmarkNewCustom(b *testing.B) { - sizes := []int{10, 21, 50, 100} - customASCIIAlphabet := "abcdef123456" - for _, size := range sizes { - size := size // Capture range variable - b.Run(fmt.Sprintf("Size%d_CustomASCIIAlphabet", size), func(b *testing.B) { +// BenchmarkGenerateDifferentLengths benchmarks the generator with varying ID lengths. +func BenchmarkGenerateDifferentLengths(b *testing.B) { + alphabet := "abcdef" + lengths := []int{5, 10, 20, 50, 100} + + for _, length := range lengths { + // Correctly format the benchmark name + name := "Length_" + strconv.Itoa(length) + b.Run(name, func(b *testing.B) { + b.ReportAllocs() + gen, err := New(alphabet, nil) + if err != nil { + b.Fatalf("Failed to create generator: %v", err) + } + for i := 0; i < b.N; i++ { - _, err := nanoid.NewCustom(size, customASCIIAlphabet) + _, err := gen.Generate(length) if err != nil { - b.Fatal(err) + b.Fatalf("GenerateSize failed: %v", err) } } }) } } -// BenchmarkNew_Concurrent benchmarks the New function under concurrent load. -func BenchmarkNew_Concurrent(b *testing.B) { - concurrencyLevels := []int{1, 2, 4, 8, runtime.NumCPU()} - - for _, concurrency := range concurrencyLevels { - concurrency := concurrency // Capture range variable - b.Run(fmt.Sprintf("Concurrency%d", concurrency), func(b *testing.B) { - b.SetParallelism(concurrency) - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - _, err := nanoid.New() - if err != nil { - b.Fatal(err) - } - } - }) - }) +// BenchmarkGenerateDifferentAlphabets benchmarks the generator with different alphabet sizes. +func BenchmarkGenerateDifferentAlphabets(b *testing.B) { + alphabets := []string{ + "AB", // 2 characters + "ABCDEF", // 6 characters + "abcdefghijklmnopqrstuvwxyz", // 26 characters + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-", // 64 characters } -} -// BenchmarkNewCustom_Concurrent benchmarks the NewCustom function with a custom ASCII alphabet under concurrent load. -func BenchmarkNewCustom_Concurrent(b *testing.B) { - concurrencyLevels := []int{1, 2, 4, 8, runtime.NumCPU()} - customASCIIAlphabet := "abcdef123456" - - for _, concurrency := range concurrencyLevels { - concurrency := concurrency // Capture range variable - b.Run(fmt.Sprintf("Concurrency%d", concurrency), func(b *testing.B) { - b.SetParallelism(concurrency) - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - _, err := nanoid.NewCustom(21, customASCIIAlphabet) - if err != nil { - b.Fatal(err) - } + idLength := 10 + for _, alphabet := range alphabets { + // Correctly format the benchmark name + name := "Alphabet_" + strconv.Itoa(len(alphabet)) + b.Run(name, func(b *testing.B) { + b.ReportAllocs() + gen, err := New(alphabet, nil) + if err != nil { + b.Fatalf("Failed to create generator: %v", err) + } + + for i := 0; i < b.N; i++ { + _, err := gen.Generate(idLength) + if err != nil { + b.Fatalf("GenerateSize failed: %v", err) } - }) + } }) } } diff --git a/nanoid_test.go b/nanoid_test.go index 58ae094..ce3d204 100644 --- a/nanoid_test.go +++ b/nanoid_test.go @@ -3,222 +3,199 @@ // This source code is licensed under the MIT License found in the // LICENSE file in the root directory of this source tree. -// nanoid_test.go -package nanoid_test +package nanoid import ( + "bytes" + "errors" "fmt" - "github.com/sixafter/nanoid" - "github.com/stretchr/testify/assert" "sync" "testing" + "time" + + "github.com/stretchr/testify/assert" ) -// TestNew verifies that the New function generates an ID of the default size and alphabet. -func TestNew(t *testing.T) { +func TestNewGenerator(t *testing.T) { t.Parallel() - is := assert.New(t) - id, err := nanoid.New() - is.NoError(err, "New() should not return an error") - is.Equal(nanoid.DefaultSize, len(id), "ID length should match the default size") + alphabet := "abc123" + gen, err := New(alphabet, nil) + is.NoError(err, "Expected no error when creating a new generator") + is.NotNil(gen, "Generator should not be nil") - // Check that all characters are within the default alphabet - for _, char := range id { - is.Contains(nanoid.DefaultAlphabet, string(char), "Character '%c' should be in the default alphabet", char) - } + conf, ok := gen.(Configuration) + is.True(ok, "Expected a Configuration") + + is.Equal(6, conf.GetConfig().AlphabetLen, "AlphabetLen should be 6") + is.Equal([]byte(alphabet), conf.GetConfig().Alphabet, "Alphabet should match the input") + is.Equal(byte(7), conf.GetConfig().Mask, "Mask should be 7 for alphabetLen=6") + is.Equal(341, conf.GetConfig().Step, "Step should be 341 for mask=7 and alphabetLen=6") } -// TestNewSize verifies that the NewSize function generates IDs of specified sizes. -func TestNewSize(t *testing.T) { +func TestGenerateSize(t *testing.T) { t.Parallel() - is := assert.New(t) - testCases := []struct { - name string - size int - }{ - {"Size1", 1}, - {"Size10", 10}, - {"Size21", 21}, - {"Size50", 50}, - {"Size100", 100}, - } - - for _, tc := range testCases { - tc := tc // Capture range variable - t.Run(tc.name, func(t *testing.T) { - t.Parallel() + id, err := GenerateSize(10) + is.NoError(err, "GenerateSize should not return an error") + is.Len(id, 10, "Generated ID should have length 10") +} - id, err := nanoid.NewSize(tc.size) - is.NoError(err, "NewSize(%d) should not return an error", tc.size) - is.Equal(tc.size, len(id), "ID length should match the specified size") +func TestGenerate(t *testing.T) { + t.Parallel() + is := assert.New(t) - // Check that all characters are within the default alphabet - for _, char := range id { - is.Contains(nanoid.DefaultAlphabet, string(char), "Character '%c' should be in the default alphabet", char) - } - }) - } + id, err := Generate() + is.NoError(err, "GenerateSize should not return an error") + is.Len(id, DefaultSize, fmt.Sprintf("Generated ID should have length %v", DefaultSize)) } -// TestNewCustom verifies that the NewCustom function generates IDs using a custom ASCII alphabet. -func TestNewCustom(t *testing.T) { +func TestGenerateCustomAlphabet(t *testing.T) { t.Parallel() - is := assert.New(t) - customASCIIAlphabet := "abcdef123456" - testCases := []struct { - name string - size int - alphabet string - }{ - {"Size10_CustomASCIIAlphabet", 10, customASCIIAlphabet}, - {"Size21_CustomASCIIAlphabet", 21, customASCIIAlphabet}, - {"Size50_CustomASCIIAlphabet", 50, customASCIIAlphabet}, - {"Size100_CustomASCIIAlphabet", 100, customASCIIAlphabet}, - {"Size10_SingleCharacter", 10, "x"}, // Single-character alphabet - {"Size5_SingleCharacter", 5, "A"}, // Single-character alphabet - } - - for _, tc := range testCases { - tc := tc // Capture range variable - t.Run(tc.name, func(t *testing.T) { - t.Parallel() + alphabet := "ABCDEF" + gen, err := New(alphabet, nil) + is.NoError(err, "Creating generator with custom alphabet should not error") + is.NotNil(gen, "Generator should not be nil") - id, err := nanoid.NewCustom(tc.size, tc.alphabet) - is.NoError(err, "NewCustom(%d, %s) should not return an error", tc.size, tc.alphabet) - is.Equal(tc.size, len(id), "ID length should match the specified size") + id, err := gen.Generate(5) + is.NoError(err, "GenerateSize should not return an error") + is.Len(id, 5, "Generated ID should have length 5") - // Check that all characters are within the custom alphabet - for _, char := range id { - is.Contains(tc.alphabet, string(char), "Character '%c' should be in the custom alphabet", char) - } - }) + for _, c := range id { + is.True(bytes.Contains([]byte(alphabet), []byte{byte(c)}), "Character %c should be in the custom alphabet", c) } } -// TestErrorHandling verifies that functions return errors for invalid inputs. -func TestErrorHandling(t *testing.T) { +func TestGenerateZeroLength(t *testing.T) { t.Parallel() - is := assert.New(t) - testCases := []struct { - name string - function func() (string, error) - }{ - { - name: "NewSize with zero size", - function: func() (string, error) { - return nanoid.NewSize(0) - }, - }, - { - name: "NewSize with negative size", - function: func() (string, error) { - return nanoid.NewSize(-10) - }, - }, - { - name: "NewCustom with empty alphabet", - function: func() (string, error) { - return nanoid.NewCustom(10, "") - }, - }, - { - name: "NewCustom with size exceeding MaxUintSize", - function: func() (string, error) { - return nanoid.NewCustom(nanoid.MaxUintSize+1, "abcdef") - }, - }, - } + id, err := GenerateSize(0) + is.ErrorIs(err, ErrInvalidLength, "Generating ID with zero length should return ErrInvalidLength") + is.Empty(id, "Generated ID should be empty when length is zero") +} - for _, tc := range testCases { - tc := tc // Capture range variable - t.Run(tc.name, func(t *testing.T) { - t.Parallel() +func TestGenerateWithCustomReader(t *testing.T) { + t.Parallel() + is := assert.New(t) - id, err := tc.function() - is.Error(err, "Expected an error for test case '%s'", tc.name) - is.Empty(id, "Expected empty ID for test case '%s'", tc.name) - }) + // Initialize mockReader with sufficient data + mockReader := &mockReader{ + data: []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05}, + reads: 0, } + alphabet := "ABCDEF" + gen, err := New(alphabet, mockReader) + is.NoError(err, "Creating generator with custom reader should not error") + is.NotNil(gen, "Generator should not be nil") + + id, err := gen.Generate(6) + is.NoError(err, "GenerateSize should not return an error") + is.Equal("ABCDEF", id, "Generated ID should match expected value 'ABCDEF'") } -// TestUniqueness verifies that multiple generated IDs are unique. -func TestUniqueness(t *testing.T) { - // Note: Due to the high memory consumption and execution time, - // this test can be marked as skipped unless specifically needed. - t.Skip("Skipping TestUniqueness to save resources during regular test runs") - +func TestGenerateInsufficientRandomBytes(t *testing.T) { t.Parallel() - is := assert.New(t) - const sampleSize = 100000 - ids := make(map[string]struct{}, sampleSize) - - for i := 0; i < sampleSize; i++ { - id, err := nanoid.New() - is.NoError(err, "New() should not return an error") - - if _, exists := ids[id]; exists { - is.FailNow(fmt.Sprintf("Duplicate ID found: %s", id)) - } - ids[id] = struct{}{} + // Reader that provides only a few bytes + mockReader := &mockReader{ + data: []byte{0x00, 0x01}, // Only 2 bytes + reads: 0, } + alphabet := "ABCDEF" + gen, err := New(alphabet, mockReader) + is.NoError(err, "Creating generator with custom reader should not error") + is.NotNil(gen, "Generator should not be nil") + + id, err := gen.Generate(3) // Requires 3 valid bytes + is.ErrorIs(err, ErrNoMoreData, "Generating ID with insufficient random bytes should return ErrNoMoreData") + is.Empty(id, "Generated ID should be empty on error") } -// TestConcurrencySafety verifies that concurrent ID generation does not produce errors or duplicates. -func TestConcurrencySafety(t *testing.T) { +func TestGenerateConcurrency(t *testing.T) { t.Parallel() - is := assert.New(t) - const ( - concurrency = 100 - perGoroutine = 1000 - totalSample = concurrency * perGoroutine - ) - ids := make(chan string, totalSample) - errs := make(chan error, totalSample) + gen, err := New(DefaultAlphabet, nil) + is.NoError(err, "Creating generator should not error") + is.NotNil(gen, "Generator should not be nil") - var wg sync.WaitGroup - wg.Add(concurrency) + if gen == nil { + return + } + + const goroutines = 100 + const idsPerGoroutine = 1000 + ids := make(chan string, goroutines*idsPerGoroutine) - for i := 0; i < concurrency; i++ { + var wg sync.WaitGroup + wg.Add(goroutines) + for i := 0; i < goroutines; i++ { go func() { defer wg.Done() - for j := 0; j < perGoroutine; j++ { - id, err := nanoid.New() + for j := 0; j < idsPerGoroutine; j++ { + id, err := gen.Generate(10) if err != nil { - errs <- err - continue + t.Errorf("GenerateSize failed: %v", err) + return } ids <- id } }() } - wg.Wait() close(ids) - close(errs) - // Check for errors - for err := range errs { - is.NoError(err, "New() should not return an error in concurrent execution") + // Verify uniqueness + idMap := make(map[string]struct{}) + for id := range ids { + idMap[id] = struct{}{} } + is.Equal(goroutines*idsPerGoroutine, len(idMap), "All generated IDs should be unique") +} - // Check for duplicates - uniqueIDs := make(map[string]struct{}, totalSample) - for id := range ids { - if _, exists := uniqueIDs[id]; exists { - is.FailNow(fmt.Sprintf("Duplicate ID found: %s", id)) +var ErrNoMoreData = errors.New("no more data") + +// mockReader is a mock implementation of io.Reader for testing purposes. +type mockReader struct { + data []byte + reads int +} + +func (m *mockReader) Read(p []byte) (int, error) { + if m.reads >= len(m.data) { + return 0, ErrNoMoreData + } + p[0] = m.data[m.reads] + m.reads++ + return 1, nil +} + +// TestGenerateDoesNotHang tests that GenerateSize does not hang indefinitely. +func TestGenerateDoesNotHang(t *testing.T) { + gen, err := New("abcdef", nil) + if err != nil { + t.Fatalf("Failed to create generator: %v", err) + } + + done := make(chan struct{}) + go func() { + _, err := gen.Generate(50) + if err != nil { + t.Errorf("GenerateSize failed: %v", err) } - uniqueIDs[id] = struct{}{} + close(done) + }() + + select { + case <-done: + // Test passed + case <-time.After(5 * time.Second): + t.Error("GenerateSize method is hanging") } }