Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,6 @@ unique_names_generator-*.tar

# Temporary files, for example, from tests.
/tmp/

.DS_Store
.serena
2 changes: 2 additions & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
erlang 27.2
elixir 1.18.3-otp-27
124 changes: 124 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

UniqueNamesGenerator is an Elixir library for generating random and unique names using PRNG (pseudo-random number generation). It supports both fully random and seeded name generation using configurable dictionaries.

**Key Architecture Pattern**: Dictionary data is loaded at compile time into module attributes using `@external_resource` markers, enabling automatic recompilation when dictionary files change. This provides memory efficiency and fast runtime access.

## Essential Commands

### Development Workflow
```bash
# Get dependencies
mix deps.get

# Run tests
mix test

# Run single test file
mix test test/path/to/test_file.exs

# Run specific test line
mix test test/path/to/test_file.exs:42

# Code formatting (auto-runs on pre-push)
mix format

# Linting with Credo (auto-runs on pre-push)
mix credo --strict

# Type checking with Dialyzer
mix dialyzer

# Generate documentation
mix docs

# Update dictionary statistics
mix update_info
```

### Git Hooks
The project uses `git_hooks` configured in `config/config.exs`:
- **pre-commit**: Runs `mix update_info_if_data_changed` to update dictionary statistics if data files changed
- **pre-push**: Runs `mix credo --strict` and `mix format` to enforce code quality

## Architecture

### Core Module Structure

**Entry Point**: `UniqueNamesGenerator` (lib/unique_names_generator.ex)
- Delegates to `UniqueNamesGenerator.Impl.Dictionaries`
- Public API: `generate/2` and `available_dictionaries/0`

**Dictionary Loading**: `UniqueNamesGenerator.Dictionaries.Loader` (lib/dictionaries/loader.ex)
- Loads all `.txt` files from `lib/dictionaries/data/` at compile time
- Uses `@external_resource` to mark dictionary files for recompilation tracking
- Dictionary terms stored in module attribute `@dictionaries` as a map
- File name becomes dictionary atom (e.g., `fruits.txt` → `:fruits`)

**Generation Logic**: `UniqueNamesGenerator.Impl.Dictionaries` (lib/impl/dictionaries.ex)
- Handles word selection, formatting, and concatenation
- Supports custom dictionaries (lists of strings) alongside built-in dictionaries
- Style options: `:capital`, `:titlecase`, `:uppercase`, `:lowercase`
- Configurable separator (default: `"_"`)

**Seeding**: `UniqueNamesGenerator.Impl.Seed` (lib/impl/seed.ex)
- Deterministic generation using `:rand.seed(:exro928ss, {a, b, c})`
- Converts string/integer seeds to consistent random state

### Dictionary System Design

Dictionary discovery is fully automatic. To add new dictionaries:

1. Create `lib/dictionaries/data/dictionary_name.txt`
2. Add one term per line (newline-separated)
3. Recompile - the dictionary becomes immediately available

The `@external_resource` markers ensure that changes to dictionary files trigger automatic recompilation, keeping the compiled dictionary map in sync with file contents.

### Mix Tasks

**mix update_info**: Generates `dictionaries_info.json` with statistics
- Word counts per dictionary
- Permutation calculations for common combinations
- Used for documentation and project metrics

**mix update_info_if_data_changed**: Conditional version for pre-commit hook
- Only updates info file if dictionary data files changed
- Prevents unnecessary updates on non-dictionary commits

## Testing Strategy

Tests organized by module:
- `test/unique_names_generator_test.exs` - Public API tests
- `test/impl/dictionaries_test.exs` - Internal generation logic
- `test/impl/seed_test.exs` - Seeding behavior

DocTests are embedded in module documentation and run automatically with `mix test`.

## Code Quality Standards

- **Credo**: Enforces Elixir style guide and code consistency
- **Dialyxir**: Static type analysis using Dialyzer
- **ExDoc**: Documentation must include `@moduledoc` and `@doc` annotations
- **Formatting**: Use `mix format` (`.formatter.exs` configuration)

## Important Implementation Details

**Random Number Generation**:
- Unseeded: Uses `:crypto.strong_rand_bytes(12)` for entropy
- Seeded: Deterministic via `:rand.seed(:exro928ss, {a, b, c})`
- Same seed + same dictionaries = identical output

**Compile-Time vs Runtime**:
- Dictionary loading happens at compile time (build artifact includes all terms)
- Runtime generation only involves random selection and formatting
- Adding new dictionaries requires recompilation (automatic via `@external_resource`)

**Dictionary Data Sources**:
- Original dictionaries: Custom curated
- Extended dictionaries: [aziele/unique-namer](https://github.com/aziele/unique-namer)
- Cities: [SimpleMaps World Cities Database](https://simplemaps.com/data/world-cities)
72 changes: 71 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,29 @@ Then run `mix deps.get`.

## Usage

In a nutshell, you can begin generating randon names with UniqueNamesGenerator by simply specifying a list of one or more dictionaries via [generate/2](`UniqueNamesGenerator.generate/2`). Available dictionary types are `t:UniqueNamesGenerator.Impl.Dictionaries.dictionaries/0`.
In a nutshell, you can begin generating randon names with UniqueNamesGenerator by simply specifying a list of one or more dictionaries via [generate/2](`UniqueNamesGenerator.generate/2`). The system automatically discovers all available dictionaries from text files.

You can see all available dictionaries at any time:

```elixir
UniqueNamesGenerator.available_dictionaries()
# => [:adjectives, :animals, :architecture, :cities, :colors, :countries, :food, :languages, :names, :numbers, :scientists, :star_wars, :technology]
```

Built-in dictionaries include:
- `:adjectives` - Descriptive adjectives
- `:animals` - Animal names
- `:architecture` - Architecture and building terms
- `:cities` - City names from around the world
- `:colors` - Color names
- `:countries` - Country names
- `:food` - Food and culinary terms
- `:languages` - Programming and spoken languages
- `:names` - Person names
- `:numbers` - Numbers 1-999
- `:scientists` - Famous scientist names
- `:star_wars` - Star Wars character names
- `:technology` - Technology and computing terms

```elixir
UniqueNamesGenerator.generate([:adjectives, :animals])
Expand All @@ -33,6 +55,15 @@ UniqueNamesGenerator.generate([:adjectives, :colors, :animals])

UniqueNamesGenerator.generate([:adjectives, :names, :numbers])
# => Generates ex: "doubtful_wanda_979"

UniqueNamesGenerator.generate([:scientists, :architecture])
# => Generates ex: "einstein_cathedral"

UniqueNamesGenerator.generate([:countries, :food])
# => Generates ex: "france_croissant"

UniqueNamesGenerator.generate([:adjectives, :cities])
# => Generates ex: "amazing_tokyo"
```

To use custom dictionaries, simply include your list of strings as part of the dictionaries list:
Expand Down Expand Up @@ -63,6 +94,45 @@ UniqueNamesGenerator.generate([:colors, :star_wars, :numbers], %{ seed: "03bf070
# => Seed "03bf0706-b7e9-33b8-aee5-c6142a816478" always generates: "brown_dooku_247"
```

## Adding New Dictionaries

UniqueNamesGenerator makes it extremely easy to add new dictionaries. Simply create a new text file in the `lib/dictionaries/data/` directory with one term per line:

**Example: Creating a fruits dictionary**

1. Create `lib/dictionaries/data/fruits.txt`:
```
apple
banana
orange
grape
strawberry
mango
kiwi
```

2. The dictionary becomes immediately available:
```elixir
UniqueNamesGenerator.available_dictionaries()
# => [:adjectives, :animals, ..., :cities, ..., :fruits, ...]

UniqueNamesGenerator.generate([:colors, :fruits])
# => Generates ex: "purple_mango"
```

**Requirements:**
- File must be in `lib/dictionaries/data/` directory
- File must have `.txt` extension
- One term per line (newline-separated)
- File name becomes the dictionary atom (e.g., `fruits.txt` → `:fruits`)

## Data Sources

Dictionary data sourced from:
- Original dictionaries: Custom curated lists
- Extended dictionaries: [aziele/unique-namer](https://github.com/aziele/unique-namer) repository
- Cities: [SimpleMaps World Cities Database](https://simplemaps.com/data/world-cities)

## License
This project is licensed under the MIT License - see the [LICENSE file](https://github.com/jongirard/unique_names_generator/blob/development/LICENSE) for details.

20 changes: 20 additions & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Config

if Mix.env() == :dev do
config :git_hooks,
auto_install: true,
verbose: true,
hooks: [
pre_commit: [
tasks: [
{:cmd, "mix update_info_if_data_changed"}
]
],
pre_push: [
tasks: [
{:cmd, "mix credo --strict"},
{:cmd, "mix format"}
]
]
]
end
Loading