diff --git a/.github/workflows/service-noise-generation-service.yml b/.github/workflows/service-noise-generation-service.yml
new file mode 100644
index 0000000..511e660
--- /dev/null
+++ b/.github/workflows/service-noise-generation-service.yml
@@ -0,0 +1,131 @@
+name: noise-generation-service
+
+on:
+ push:
+ branches: ["master"]
+ paths:
+ - "services/noise-generation-service/**"
+ pull_request:
+ branches: ["master"]
+ paths:
+ - "services/noise-generation-service/**"
+
+defaults:
+ run:
+ working-directory: services/noise-generation-service
+
+jobs:
+ lint:
+ name: Lint
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Setup Rust
+ uses: dtolnay/rust-toolchain@stable
+ with:
+ components: rustfmt, clippy
+ - name: Cache dependencies
+ uses: Swatinem/rust-cache@v2
+ with:
+ workspaces: services/noise-generation-service
+ - name: Formatting check
+ run: cargo fmt --check
+ - name: Clippy linting
+ run: cargo clippy --all-targets --all-features -- -D warnings
+
+ test:
+ name: Test
+ runs-on: ubuntu-latest
+ needs: lint
+ steps:
+ - uses: actions/checkout@v4
+ - name: Setup Rust
+ uses: dtolnay/rust-toolchain@stable
+ - name: Cache dependencies
+ uses: Swatinem/rust-cache@v2
+ with:
+ workspaces: services/noise-generation-service
+ - name: Run tests
+ run: |
+ # Start server in background for integration tests
+ cargo run -- server &
+ SERVER_PID=$!
+
+ # Wait for server to be ready
+ for i in $(seq 1 30); do
+ if curl -s http://localhost:8000/v1/algorithms > /dev/null 2>&1; then
+ echo "Server is ready"
+ break
+ fi
+ echo "Waiting for server... ($i/30)"
+ sleep 1
+ done
+
+ # Run all tests
+ cargo test || EXIT_CODE=$?
+
+ # Stop the server
+ kill $SERVER_PID 2>/dev/null || true
+ wait $SERVER_PID 2>/dev/null || true
+
+ exit ${EXIT_CODE:-0}
+ - name: Upload coverage to Codecov
+ uses: codecov/codecov-action@v4
+ with:
+ files: services/noise-generation-service/coverage/lcov.info
+ flags: noise-generation-service
+ token: ${{ secrets.CODECOV_TOKEN }}
+ fail_ci_if_error: false
+ if: always()
+
+ generate-openapi:
+ name: Generate OpenAPI Spec
+ runs-on: ubuntu-latest
+ needs: test
+ steps:
+ - uses: actions/checkout@v4
+ - name: Setup Rust
+ uses: dtolnay/rust-toolchain@stable
+ - name: Cache dependencies
+ uses: Swatinem/rust-cache@v2
+ with:
+ workspaces: services/noise-generation-service
+ - name: Generate OpenAPI spec
+ run: cargo run -- openapi --output noise-generation-service.openapi.json
+ - name: Upload OpenAPI spec
+ uses: actions/upload-artifact@v4
+ with:
+ name: openapi-spec-noise-generation-service
+ path: services/noise-generation-service/noise-generation-service.openapi.json
+
+ build:
+ name: Build & Push Docker Image
+ runs-on: ubuntu-latest
+ needs: generate-openapi
+ if: github.event_name == 'push' && github.ref == 'refs/heads/master'
+ permissions:
+ contents: read
+ packages: write
+ steps:
+ - uses: actions/checkout@v4
+ - name: Log in to GitHub Container Registry
+ uses: docker/login-action@v3
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+ - name: Extract Docker metadata
+ id: meta
+ uses: docker/metadata-action@v5
+ with:
+ images: ghcr.io/${{ github.repository_owner }}/noise-generation-service
+ tags: |
+ type=sha,prefix=sha-
+ type=raw,value=latest
+ - name: Build and push Docker image
+ uses: docker/build-push-action@v6
+ with:
+ context: services/noise-generation-service
+ push: true
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
diff --git a/README.md b/README.md
index c94884c..7a53300 100644
--- a/README.md
+++ b/README.md
@@ -5,6 +5,7 @@
[](https://github.com/HTW-ALADIN/ALADIN-Services/actions/workflows/service-jsonpath-mapper-service.yml)
[](https://github.com/HTW-ALADIN/ALADIN-Services/actions/workflows/service-sql-assessment-service.yml)
[](https://github.com/HTW-ALADIN/ALADIN-Services/actions/workflows/service-fermentaladin-service.yml)
+[](https://github.com/HTW-ALADIN/ALADIN-Services/actions/workflows/service-noise-generation-service.yml)
[](https://github.com/HTW-ALADIN/ALADIN-Services/actions/workflows/service-llm-gateway-service.yml)
[](https://github.com/HTW-ALADIN/ALADIN-Services/actions/workflows/service-academic-search-service.yml)
@@ -17,6 +18,7 @@
| [jsonpath-mapper-service](services/jsonpath-mapper-service/README.md) | TypeScript | [](https://github.com/HTW-ALADIN/ALADIN-Services/actions/workflows/service-jsonpath-mapper-service.yml) | [](https://codecov.io/gh/HTW-ALADIN/ALADIN-Services?flags[0]=jsonpath-mapper-service) | JSONPath Mapper — JSON-to-JSON transformation utility exposed as a Fastify HTTP API |
| [sql-assessment-service](services/sql-assessment-service/README.md) | TypeScript | [](https://github.com/HTW-ALADIN/ALADIN-Services/actions/workflows/service-sql-assessment-service.yml) | [](https://codecov.io/gh/HTW-ALADIN/ALADIN-Services?flags[0]=sql-assessment-service) | SQL Assessment — schema analysis, SQL task generation, and query grading |
| [fermentaladin-service](services/fermentaladin-service/README.md) | Python | [](https://github.com/HTW-ALADIN/ALADIN-Services/actions/workflows/service-fermentaladin-service.yml) | — | Bioreactor fermentation simulation — ODE-based multi-phase kinetic model exposed as a FastAPI HTTP service |
+| [noise-generation-service](services/noise-generation-service/README.md) | Rust | [](https://github.com/HTW-ALADIN/ALADIN-Services/actions/workflows/service-noise-generation-service.yml) | — | Unified REST API for noise generation — 14 algorithms (Perlin, Simplex, FBM, Domain Warp, etc.) with multiple backends |
| [llm-gateway-service](services/llm-gateway-service/README.md) | TypeScript | [](https://github.com/HTW-ALADIN/ALADIN-Services/actions/workflows/service-llm-gateway-service.yml) | [](https://codecov.io/gh/HTW-ALADIN/ALADIN-Services?flags[0]=llm-gateway-service) | HTTP/CLI wrapper around a self-hosted LLM Gateway instance, using the Vercel AI SDK |
## For Developers:
diff --git a/services/noise-generation-service/.dockerignore b/services/noise-generation-service/.dockerignore
new file mode 100644
index 0000000..ceed026
--- /dev/null
+++ b/services/noise-generation-service/.dockerignore
@@ -0,0 +1,7 @@
+/target
+*.openapi.json
+.git
+.gitignore
+docker-compose*.yml
+Dockerfile
+README.md
diff --git a/services/noise-generation-service/.gitignore b/services/noise-generation-service/.gitignore
new file mode 100644
index 0000000..5065ec6
--- /dev/null
+++ b/services/noise-generation-service/.gitignore
@@ -0,0 +1,2 @@
+/target/
+*.openapi.json
\ No newline at end of file
diff --git a/services/noise-generation-service/Cargo.lock b/services/noise-generation-service/Cargo.lock
new file mode 100644
index 0000000..c8ab86f
--- /dev/null
+++ b/services/noise-generation-service/Cargo.lock
@@ -0,0 +1,2246 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "anstream"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
+
+[[package]]
+name = "anstyle-parse"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "async-trait"
+version = "0.1.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "autocfg"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
+
+[[package]]
+name = "axum"
+version = "0.6.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf"
+dependencies = [
+ "async-trait",
+ "axum-core 0.3.4",
+ "bitflags 1.3.2",
+ "bytes",
+ "futures-util",
+ "http 0.2.12",
+ "http-body 0.4.6",
+ "hyper 0.14.32",
+ "itoa",
+ "matchit",
+ "memchr",
+ "mime",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustversion",
+ "serde",
+ "serde_json",
+ "serde_path_to_error",
+ "serde_urlencoded",
+ "sync_wrapper 0.1.2",
+ "tokio",
+ "tower 0.4.13",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "axum"
+version = "0.7.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
+dependencies = [
+ "async-trait",
+ "axum-core 0.4.5",
+ "bytes",
+ "futures-util",
+ "http 1.4.2",
+ "http-body 1.1.0",
+ "http-body-util",
+ "hyper 1.10.1",
+ "hyper-util",
+ "itoa",
+ "matchit",
+ "memchr",
+ "mime",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustversion",
+ "serde",
+ "serde_json",
+ "serde_path_to_error",
+ "serde_urlencoded",
+ "sync_wrapper 1.0.2",
+ "tokio",
+ "tower 0.5.3",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "axum-core"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c"
+dependencies = [
+ "async-trait",
+ "bytes",
+ "futures-util",
+ "http 0.2.12",
+ "http-body 0.4.6",
+ "mime",
+ "rustversion",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "axum-core"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199"
+dependencies = [
+ "async-trait",
+ "bytes",
+ "futures-util",
+ "http 1.4.2",
+ "http-body 1.1.0",
+ "http-body-util",
+ "mime",
+ "pin-project-lite",
+ "rustversion",
+ "sync_wrapper 1.0.2",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "base64"
+version = "0.21.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
+
+[[package]]
+name = "block-buffer"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
+dependencies = [
+ "hybrid-array",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.20.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "bytes"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
+
+[[package]]
+name = "cc"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8"
+dependencies = [
+ "find-msvc-tools",
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "clap"
+version = "4.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "clap_lex"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
+
+[[package]]
+name = "const-oid"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
+
+[[package]]
+name = "core-foundation"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "cpufeatures"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
+
+[[package]]
+name = "crypto-common"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
+dependencies = [
+ "hybrid-array",
+]
+
+[[package]]
+name = "digest"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
+dependencies = [
+ "block-buffer",
+ "const-oid",
+ "crypto-common",
+]
+
+[[package]]
+name = "dirs"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "fastnoise-lite"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e5f3c3cc9081e5d0e18bcd50e80cd33cba47fc22f88a9da9c33ecd1c87ea5c0"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
+
+[[package]]
+name = "flate2"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
+
+[[package]]
+name = "futures-sink"
+version = "0.3.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307"
+
+[[package]]
+name = "futures-task"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
+
+[[package]]
+name = "futures-util"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+]
+
+[[package]]
+name = "h2"
+version = "0.3.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d"
+dependencies = [
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "http 0.2.12",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "http"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425"
+dependencies = [
+ "bytes",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
+dependencies = [
+ "bytes",
+ "http 0.2.12",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "http-body"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c"
+dependencies = [
+ "bytes",
+ "http 1.4.2",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http 1.4.2",
+ "http-body 1.1.0",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+[[package]]
+name = "hybrid-array"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c"
+dependencies = [
+ "typenum",
+]
+
+[[package]]
+name = "hyper"
+version = "0.14.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http 0.2.12",
+ "http-body 0.4.6",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "socket2 0.5.10",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "want",
+]
+
+[[package]]
+name = "hyper"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "http 1.4.2",
+ "http-body 1.1.0",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+]
+
+[[package]]
+name = "hyper-tls"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
+dependencies = [
+ "bytes",
+ "hyper 0.14.32",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
+dependencies = [
+ "bytes",
+ "http 1.4.2",
+ "http-body 1.1.0",
+ "hyper 1.10.1",
+ "pin-project-lite",
+ "tokio",
+ "tower-service",
+]
+
+[[package]]
+name = "icu_collections"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "utf8_iter",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
+dependencies = [
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
+
+[[package]]
+name = "icu_properties"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
+dependencies = [
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
+
+[[package]]
+name = "icu_provider"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "idna"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "js-sys"
+version = "0.3.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
+dependencies = [
+ "cfg-if",
+ "futures-util",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.186"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
+
+[[package]]
+name = "libredox"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+
+[[package]]
+name = "litemap"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
+
+[[package]]
+name = "log"
+version = "0.4.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
+
+[[package]]
+name = "matchit"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
+
+[[package]]
+name = "memchr"
+version = "2.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "mime_guess"
+version = "2.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
+dependencies = [
+ "mime",
+ "unicase",
+]
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
+[[package]]
+name = "mio"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "native-tls"
+version = "0.2.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2"
+dependencies = [
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
+[[package]]
+name = "noise"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6da45c8333f2e152fc665d78a380be060eb84fad8ca4c9f7ac8ca29216cff0cc"
+dependencies = [
+ "num-traits",
+ "rand",
+ "rand_xorshift",
+]
+
+[[package]]
+name = "noise-generation-service"
+version = "0.1.0"
+dependencies = [
+ "axum 0.7.9",
+ "clap",
+ "fastnoise-lite",
+ "noise",
+ "reqwest",
+ "serde",
+ "serde_json",
+ "serde_yaml",
+ "tokio",
+ "utoipa",
+ "utoipa-swagger-ui",
+ "uuid",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+
+[[package]]
+name = "openssl"
+version = "0.10.81"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45"
+dependencies = [
+ "bitflags 2.13.0",
+ "cfg-if",
+ "foreign-types",
+ "libc",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
+name = "pin-project"
+version = "1.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
+
+[[package]]
+name = "potential_utf"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+
+[[package]]
+name = "rand"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a"
+dependencies = [
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+
+[[package]]
+name = "rand_xorshift"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f"
+dependencies = [
+ "rand_core",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
+dependencies = [
+ "getrandom 0.2.17",
+ "libredox",
+ "thiserror",
+]
+
+[[package]]
+name = "regex"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
+
+[[package]]
+name = "reqwest"
+version = "0.11.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62"
+dependencies = [
+ "base64",
+ "bytes",
+ "encoding_rs",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http 0.2.12",
+ "http-body 0.4.6",
+ "hyper 0.14.32",
+ "hyper-tls",
+ "ipnet",
+ "js-sys",
+ "log",
+ "mime",
+ "native-tls",
+ "once_cell",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustls-pemfile",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper 0.1.2",
+ "system-configuration",
+ "tokio",
+ "tokio-native-tls",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "winreg",
+]
+
+[[package]]
+name = "rust-embed"
+version = "8.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e9e7760e252aaba7b09f4be00e36476cf585bdb68a53552ac954cdf504ab4bc9"
+dependencies = [
+ "rust-embed-impl",
+ "rust-embed-utils",
+ "walkdir",
+]
+
+[[package]]
+name = "rust-embed-impl"
+version = "8.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3bcfc4d6f53af43755f7a723e4b6b8794fcce052a178dd8c6c1dadc5f5343097"
+dependencies = [
+ "mime_guess",
+ "proc-macro2",
+ "quote",
+ "rust-embed-utils",
+ "shellexpand",
+ "syn 2.0.119",
+ "walkdir",
+]
+
+[[package]]
+name = "rust-embed-utils"
+version = "8.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42ffa149f6aa81b58a5b3011d01a857c4ed12c7a732d2c51947a4c7c692185f0"
+dependencies = [
+ "sha2",
+ "walkdir",
+]
+
+[[package]]
+name = "rustix"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
+dependencies = [
+ "bitflags 2.13.0",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustls-pemfile"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
+dependencies = [
+ "base64",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
+
+[[package]]
+name = "ryu"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "schannel"
+version = "0.1.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "security-framework"
+version = "3.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
+dependencies = [
+ "bitflags 2.13.0",
+ "core-foundation 0.10.1",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.150"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "serde_path_to_error"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
+dependencies = [
+ "itoa",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_yaml"
+version = "0.9.34+deprecated"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
+dependencies = [
+ "indexmap",
+ "itoa",
+ "ryu",
+ "serde",
+ "unsafe-libyaml",
+]
+
+[[package]]
+name = "sha2"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "shellexpand"
+version = "3.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32824fab5e16e6c4d86dc1ba84489390419a39f97699852b66480bb87d297ed8"
+dependencies = [
+ "dirs",
+]
+
+[[package]]
+name = "shlex"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
+
+[[package]]
+name = "simd-adler32"
+version = "0.3.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
+
+[[package]]
+name = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
+[[package]]
+name = "smallvec"
+version = "1.15.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
+
+[[package]]
+name = "socket2"
+version = "0.5.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "socket2"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.119"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sync_wrapper"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
+
+[[package]]
+name = "sync_wrapper"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+
+[[package]]
+name = "synstructure"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "system-configuration"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
+dependencies = [
+ "bitflags 1.3.2",
+ "core-foundation 0.9.4",
+ "system-configuration-sys",
+]
+
+[[package]]
+name = "system-configuration-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
+dependencies = [
+ "fastrand",
+ "getrandom 0.4.3",
+ "once_cell",
+ "rustix",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "tokio"
+version = "1.52.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
+dependencies = [
+ "bytes",
+ "libc",
+ "mio",
+ "pin-project-lite",
+ "socket2 0.6.5",
+ "tokio-macros",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "tokio-native-tls"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tower"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project",
+ "pin-project-lite",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project-lite",
+ "sync_wrapper 1.0.2",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+
+[[package]]
+name = "tower-service"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
+[[package]]
+name = "tracing"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+dependencies = [
+ "log",
+ "pin-project-lite",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
+[[package]]
+name = "typenum"
+version = "1.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
+
+[[package]]
+name = "unicase"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "unsafe-libyaml"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
+
+[[package]]
+name = "url"
+version = "2.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+]
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "utoipa"
+version = "4.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c5afb1a60e207dca502682537fefcfd9921e71d0b83e9576060f09abc6efab23"
+dependencies = [
+ "indexmap",
+ "serde",
+ "serde_json",
+ "utoipa-gen",
+]
+
+[[package]]
+name = "utoipa-gen"
+version = "4.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20c24e8ab68ff9ee746aad22d39b5535601e6416d1b0feeabf78be986a5c4392"
+dependencies = [
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "utoipa-swagger-ui"
+version = "4.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "154517adf0d0b6e22e8e1f385628f14fcaa3db43531dc74303d3edef89d6dfe5"
+dependencies = [
+ "axum 0.6.20",
+ "mime_guess",
+ "regex",
+ "rust-embed",
+ "serde",
+ "serde_json",
+ "utoipa",
+ "zip",
+]
+
+[[package]]
+name = "uuid"
+version = "1.23.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea5fab0d6c3c01ae70085a09cb03d4c7a1d6314e2b3e075392783396d724ca0a"
+dependencies = [
+ "getrandom 0.4.3",
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.76"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "winapi-util"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "winreg"
+version = "0.50.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
+dependencies = [
+ "cfg-if",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "writeable"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
+
+[[package]]
+name = "yoke"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
+dependencies = [
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "synstructure",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "synstructure",
+]
+
+[[package]]
+name = "zerotrie"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "zip"
+version = "0.6.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261"
+dependencies = [
+ "byteorder",
+ "crc32fast",
+ "crossbeam-utils",
+ "flate2",
+]
+
+[[package]]
+name = "zmij"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
diff --git a/services/noise-generation-service/Cargo.toml b/services/noise-generation-service/Cargo.toml
new file mode 100644
index 0000000..c59c49e
--- /dev/null
+++ b/services/noise-generation-service/Cargo.toml
@@ -0,0 +1,29 @@
+[package]
+name = "noise-generation-service"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+axum = "0.7.5"
+tokio = { version = "1.36", features = ["rt-multi-thread", "macros", "net"] }
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
+serde_yaml = "0.9"
+clap = { version = "4", features = ["derive"] }
+noise = "0.9"
+fastnoise-lite = "1.1.1"
+uuid = { version = "1.10", features = ["v4"] }
+utoipa = { version = "4", features = ["axum_extras"] }
+utoipa-swagger-ui = { version = "4", features = ["axum"] }
+
+# `axum`, `tokio`, and `utoipa` are intentionally NOT re-declared here:
+# `[dependencies]` entries are already available to integration tests and
+# doctests, and duplicating them risked silent version drift between the two
+# tables.
+[dev-dependencies]
+reqwest = { version = "0.11", features = ["json"] }
+
+[lints.clippy]
+# `fill_white_noise`'s 1D loop (src/generate.rs) reads clearer as an
+# explicit range loop than as an iterator chain.
+needless_range_loop = "allow"
diff --git a/services/noise-generation-service/Dockerfile b/services/noise-generation-service/Dockerfile
new file mode 100644
index 0000000..d314ab2
--- /dev/null
+++ b/services/noise-generation-service/Dockerfile
@@ -0,0 +1,44 @@
+# Stage 1: Build
+FROM rust:1.86-slim-bookworm AS builder
+
+# Install build dependencies
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ pkg-config build-essential libssl-dev \
+ && rm -rf /var/lib/apt/lists/*
+
+WORKDIR /app
+
+# Copy only the dependency manifest first and build a throwaway binary
+# against it, so the (slow) dependency-compilation layer is cached and only
+# invalidated when Cargo.toml/Cargo.lock actually change — not on every
+# source/README/test edit.
+COPY Cargo.toml Cargo.lock ./
+RUN mkdir src \
+ && echo "fn main() {}" > src/main.rs \
+ && echo "" > src/lib.rs \
+ && cargo build --release \
+ && rm -rf src
+
+# Now copy the real source and rebuild — only this layer is invalidated by
+# ordinary source changes.
+COPY src ./src
+RUN touch src/main.rs src/lib.rs && cargo build --release
+
+# Stage 2: Runtime
+FROM debian:bookworm-slim
+
+# Install SSL libraries for runtime
+RUN apt-get update && apt-get install -y --no-install-recommends libssl3 \
+ && rm -rf /var/lib/apt/lists/*
+
+# Copy the binary from the builder
+COPY --from=builder /app/target/release/noise-generation-service /usr/local/bin/noise-generation-service
+
+# Expose the HTTP port the server binds to by default
+EXPOSE 8000
+
+# Set the user
+USER nobody
+
+# Start the service (defaults to binding 0.0.0.0:8000)
+CMD ["/usr/local/bin/noise-generation-service", "server"]
diff --git a/services/noise-generation-service/Makefile b/services/noise-generation-service/Makefile
new file mode 100644
index 0000000..55053a1
--- /dev/null
+++ b/services/noise-generation-service/Makefile
@@ -0,0 +1,37 @@
+.PHONY: prep build test lint start clean docker-build generate-openapi dev prod smoke-test
+
+prep:
+ cargo fetch
+
+build:
+ cargo build --release
+
+test:
+ cargo test
+
+lint:
+ cargo fmt --check
+ cargo clippy --all-targets --all-features -- -D warnings
+
+start:
+ cargo run
+
+clean:
+ rm -rf target/
+
+docker-build:
+ docker build -t noise-generation-service .
+
+generate-openapi:
+ cargo run -- openapi --output noise-generation-service.openapi.json
+
+# Black-box smoke test against a *running* server (not run by `make test`).
+# Usage: make start & then, in another shell, make smoke-test
+smoke-test:
+ ./tests/test_all_algorithms.sh
+
+dev:
+ docker-compose -f docker-compose.dev.yml up --build
+
+prod:
+ docker-compose -f docker-compose.prod.yml up --build
diff --git a/services/noise-generation-service/README.md b/services/noise-generation-service/README.md
new file mode 100644
index 0000000..0da8df6
--- /dev/null
+++ b/services/noise-generation-service/README.md
@@ -0,0 +1,707 @@
+# Noise Generation Service
+
+
+
+Unified REST API for multiple noise generation libraries and algorithms.
+
+## Features
+
+
+
+### Supported Algorithms
+
+
+
+Default libraries are automatically assigned based on the selected algorithm.
+
+| Algorithm | Default Library | Description | Parameters |
+| --- | --- | --- | --- |
+| `perlin` | `fastnoise_lite`
| Classic Perlin noise with smooth gradients
+
+ | `seed`
|
+| `simplex` | `noise_rs`
| Improved noise with lower computational complexity
+
+ | `seed`
|
+| `opensimplex2` | `fastnoise_lite`
| Modern variant with better visual quality
+
+ | `seed`
|
+| `supersimplex` | `noise_rs`
| Higher-dimensional noise variant
+
+ | `seed`
|
+| `value` | `fastnoise_lite`
| Grid-based value noise (+cubic interpolation)
+
+ | `seed`
|
+| `cellular` | `fastnoise_lite`
| Cell-based patterns (Worley/Voronoi)
+
+ | `seed`
|
+| `fbm` | `noise_rs`
| Fractal Brownian Motion (multi-octave)
+
+ | `seed`, `octaves`, `frequency`, `lacunarity`, `persistence`
|
+| `billow` | `noise_rs`
| Billow noise (absolute value of Perlin)
+
+ | `seed`, `octaves`, `frequency`, `lacunarity`, `persistence`
|
+| `ridged_multi` | `fastnoise_lite`
| Ridge-like fractal patterns
+
+ | `seed`, `octaves`, `frequency`, `lacunarity`
|
+| `hybrid_multi` | `noise_rs`
| HybridMulti fractal noise
+
+ | `seed`, `octaves`, `frequency`, `lacunarity`
|
+| `pingpong` | `fastnoise_lite`
| PingPong fractal with wrapping
+
+ | `seed`, `strength`
|
+| `domain_warp` | `fastnoise_lite`
| Domain warping transformation
+
+ | `seed`, `amplitude`
|
+| `combinator` | `noise_rs`
| Generic combinators (Add/Multiply/Min/Max/Blend)
+
+ | `seed`, `op` (`add`/`multiply`/`min`/`max`/`blend`)
+
+ |
+| `utility` | `noise_rs`
| Utility generators (Constant/Cylinders)
+
+ | `kind` (`constant`/`cylinders`), `value` (for constant)
+
+ |
+| `white` | `native`
| Pure random white noise
+
+ | `seed`
|
+
+**Total: 15 unique algorithms with automatic library selection**
+
+### API Endpoints
+
+
+
+* `GET /v1/algorithms` — List all available algorithms with their default parameters
+
+
+* `POST /v1/noise` — Generate noise field and return the full result (including data grid)
+
+ The `sampling.size` field determines the dimensionality of the result:
+
+ - `[width]` — 1D array
+ - `[width, height]` — 2D grid (nested arrays)
+ - `[width, height, depth]` — 3D volume (triple-nested arrays)
+ - `[width, height, depth, time]` — 4D hypervolume (4×-nested arrays)
+
+ **4D support** is available for noise-rs algorithms (simplex, fbm, billow,
+ ridged_multi, hybrid_multi, combinator, utility) and white noise.
+ FNL-based algorithms (perlin, opensimplex2, value, cellular, pingpong,
+ domain_warp) and supersimplex are limited to 2D/3D.
+ White noise additionally supports 1D.
+ Requesting an unsupported dimension returns a 400 error with a descriptive message.
+
+ **Response includes `params_used`**: Every successful response contains a
+ `params_used` field that echoes all resolved parameters (including applied
+ defaults and the effective seed). This makes every generated noise field
+ fully reproducible — even when the caller omitted optional parameters or
+ left the seed unset (in which case the randomly generated seed is included
+ in the response).
+
+
+* `GET /api-docs/openapi.json` — OpenAPI specification
+
+
+
+---
+
+### Concrete `params` Object Per Algorithm
+
+
+
+`params` is **not** an arbitrary JSON blob. Its shape depends on `algorithm`.
+All fields below are optional and use server-side defaults when omitted.
+
+`perlin`, `simplex`, `opensimplex2`, `supersimplex`, `value`, `white`
+
+```json
+{ "seed": 42 }
+
+```
+
+`cellular`
+
+```json
+{
+ "seed": 42,
+ "distance_function": "euclidean",
+ "return_type": "cell_value",
+ "jitter": 0.45
+}
+
+```
+
+Allowed values:
+
+* `distance_function`: `euclidean`, `euclidean_sq`, `manhattan`, `hybrid`
+
+* `return_type`: `cell_value`, `distance`, `distance2`, `distance2_add`, `distance2_sub`, `distance2_mul`, `distance2_div`
+
+
+`fbm`, `billow`
+
+```json
+{
+ "seed": 42,
+ "octaves": 4,
+ "frequency": 0.1,
+ "lacunarity": 2.0,
+ "persistence": 0.5
+}
+
+```
+
+`ridged_multi`, `hybrid_multi`
+
+```json
+{
+ "seed": 42,
+ "octaves": 4,
+ "frequency": 0.1,
+ "lacunarity": 2.0,
+ "persistence": 0.5
+}
+
+```
+
+`pingpong`
+
+```json
+{
+ "seed": 42,
+ "strength": 2.0
+}
+
+```
+
+`domain_warp`
+
+```json
+{
+ "seed": 42,
+ "amplitude": 1.0
+}
+
+```
+
+`combinator`
+
+```json
+{
+ "seed": 42,
+ "op": "add",
+ "blend_factor": 0.5
+}
+
+```
+
+Allowed values:
+
+* `op`: `add`, `multiply`, `min`, `max`, `blend`
+
+
+`utility`
+
+```json
+{
+ "kind": "constant",
+ "value": 1.0
+}
+
+```
+
+Allowed values:
+
+* `kind`: `constant`, `cylinders`
+
+
+Defaults used by the service if omitted:
+
+* `seed`: randomly generated per request (based on current time) if omitted — not deterministic
+
+* `octaves`: `4`
+
+* `frequency`: `0.1`
+
+* `lacunarity`: `2.0`
+
+* `persistence`: `0.5` (Fbm, Billow), `0.5` (RidgedMulti), `0.25` (HybridMulti)
+
+* `strength`: `2.0`
+
+* `amplitude`: `1.0`
+
+* `op`: `add`
+
+* `kind`: `constant`
+
+* `value`: `1.0`
+
+> **Why are parameters still optional?** All fields remain optional so that the
+> service stays easily callable by AI agents or quick scripts with minimal context.
+> However, **reproducibility is now guaranteed** via two mechanisms:
+> 1. **`POST /v1/noise` response** includes a `params_used` field that echoes all
+> resolved parameter values (including the effective seed).
+> 2. **`GET /v1/algorithms`** lists every algorithm together with its server-side
+> default values, enabling clients to inspect defaults without reading docs.
+>
+> This way you get both convenience (optional fields) and full traceability
+> (resolved values in the response + discoverable defaults via the API).
+
+
+### GET /v1/algorithms Response
+
+The endpoint now returns an array of objects, each containing the algorithm
+name and its default parameter values:
+
+```json
+[
+ {
+ "name": "perlin",
+ "defaults": { "seed": null }
+ },
+ {
+ "name": "cellular",
+ "defaults": {
+ "seed": null,
+ "distance_function": "euclidean",
+ "return_type": "cell_value",
+ "jitter": 0.45
+ }
+ },
+ {
+ "name": "fbm",
+ "defaults": {
+ "seed": null,
+ "octaves": 4,
+ "frequency": 0.1,
+ "lacunarity": 2.0,
+ "persistence": 0.5
+ }
+ },
+ ...
+]
+```
+
+> The `seed` default shows as `null` because seeds are randomly generated per
+> request when omitted. The actual effective seed for a specific response is
+> always included in `params_used` in the `POST /v1/noise` response.
+
+**CLI output (`noise-generation-service list`)**:
+
+- **JSON format** (`--format json`, default): Returns the same structured array
+ as the REST endpoint.
+- **CSV format** (`--format csv`): Lists algorithm name and defaults
+ (as a compact JSON string in a second column).
+
+
+---
+
+### Example Requests
+
+
+
+#### List Available Algorithms
+
+
+
+```bash
+curl http://localhost:8000/v1/algorithms
+
+```
+
+#### Generate Perlin Noise
+
+
+
+```bash
+curl -s -X POST http://localhost:8000/v1/noise \
+ -H "Content-Type: application/json" \
+ -d '{
+ "algorithm": "perlin",
+ "params": {"seed": 42},
+ "sampling": {
+ "size": [4, 4]
+ }
+ }' | jq .
+
+```
+
+Response:
+
+```json
+{
+ "id": "nsf_abc123...",
+ "status": "completed",
+ "algorithm": "perlin",
+ "data": [[0.23, -0.45, ...], ...],
+ "size": [4, 4],
+ "params_used": {
+ "seed": 42
+ }
+}
+
+```
+
+#### Generate Perlin Noise Without Seed (auto-generated seed in response)
+
+```bash
+curl -s -X POST http://localhost:8000/v1/noise \
+ -H "Content-Type: application/json" \
+ -d '{
+ "algorithm": "perlin",
+ "params": {},
+ "sampling": {
+ "size": [4, 4]
+ }
+ }' | jq .
+
+```
+
+Response (seed is auto-generated but echoed back):
+
+```json
+{
+ "id": "nsf_def456...",
+ "status": "completed",
+ "algorithm": "perlin",
+ "data": [[-0.12, 0.34, ...], ...],
+ "size": [4, 4],
+ "params_used": {
+ "seed": 3714829405
+ }
+}
+```
+
+> The `params_used` field contains the effective seed even when none was provided.
+> Save this value to reproduce the identical noise field later.
+
+```
+
+#### Generate Fractal Brownian Motion
+
+
+
+```bash
+curl -X POST http://localhost:8000/v1/noise \
+ -H "Content-Type: application/json" \
+ -d '{
+ "algorithm": "fbm",
+ "params": {
+ "seed": 123,
+ "octaves": 6,
+ "frequency": 0.1,
+ "lacunarity": 2.0,
+ "persistence": 0.5
+ },
+ "sampling": {
+ "size": [128, 128]
+ }
+ }'
+
+```
+
+#### Generate Domain-Warped Noise
+
+
+
+```bash
+curl -X POST http://localhost:8000/v1/noise \
+ -H "Content-Type: application/json" \
+ -d '{
+ "algorithm": "domain_warp",
+ "params": {
+ "seed": 999,
+ "amplitude": 2.0
+ },
+ "sampling": {
+ "size": [64, 64]
+ }
+ }'
+
+```
+
+#### Generate Combinator Noise (Add two Perlin sources)
+
+
+
+```bash
+curl -X POST http://localhost:8000/v1/noise \
+ -H "Content-Type: application/json" \
+ -d '{
+ "algorithm": "combinator",
+ "params": {
+ "seed": 555,
+ "op": "add"
+ },
+ "sampling": {
+ "size": [32, 32]
+ }
+ }'
+
+```
+
+#### Generate Utility Noise (Constant field)
+
+
+
+```bash
+curl -X POST http://localhost:8000/v1/noise \
+ -H "Content-Type: application/json" \
+ -d '{
+ "algorithm": "utility",
+ "params": {
+ "kind": "constant",
+ "value": 0.5
+ },
+ "sampling": {
+ "size": [16, 16]
+ }
+ }'
+
+```
+
+---
+
+## CLI Usage (Mirroring REST API)
+
+
+
+The service includes a CLI for local generation, API inspection, and server startup.
+
+### List Available Algorithms
+
+
+
+```bash
+# JSON output (default) — returns structured array with name + defaults
+noise-generation-service list
+
+# CSV output — "algorithm","defaults" columns
+noise-generation-service list --format csv
+
+```
+
+### Generate Noise Locally (all 15 algorithms)
+
+Parameters are passed as a single JSON object via `--params`, and grid size via
+`--sampling-size` (or the deprecated alias `--size`; 1–4 comma-separated
+values: `width` for 1D, `width,height` for 2D, `width,height,depth` for 3D,
+`width,height,depth,time` for 4D; defaults to `64,64` if omitted).
+
+> **CLI-API mapping**: `--sampling-size` → `sampling.size`,
+> `--output-format` → `output.format`,
+> `--output-normalize` → `output.normalize`.
+> `--output-file` is CLI-only (not part of the API schema).
+> Deprecated aliases (`--size`, `--format`, `--normalize`) still work for
+> backward compatibility.
+
+```bash
+# Basic usage (if --params is omitted, seed is chosen at random by the service)
+noise-generation-service generate --algorithm perlin --sampling-size 64,64
+
+# With custom parameters (all algorithms supported)
+noise-generation-service generate \
+ --algorithm fbm \
+ --sampling-size 128,128 \
+ --params '{"seed": 42, "octaves": 6, "frequency": 0.1, "lacunarity": 2.0, "persistence": 0.5}' \
+ --output-file noise_field.json
+
+# CSV output
+noise-generation-service generate \
+ --algorithm white \
+ --params '{"seed": 999}' \
+ --sampling-size 32,32 \
+ --output-format csv \
+ --output-file noise.csv
+
+# Combinator noise with blend operation
+noise-generation-service generate \
+ --algorithm combinator \
+ --params '{"seed": 555, "op": "blend", "blend_factor": 0.5}' \
+ --sampling-size 32,32
+
+# Normalize output values to [0,1]
+noise-generation-service generate --algorithm perlin --output-normalize
+
+# CSV output is limited to 1D/2D data; 3D/4D requests will error:
+noise-generation-service generate --algorithm white --sampling-size 4,4,4 --output-format csv
+# → Error: CSV output only supports 1D/2D noise fields; use --output-format json for 3D data
+
+### Start HTTP Server
+
+
+
+```bash
+# Default (0.0.0.0:8000)
+noise-generation-service server
+
+# Custom host/port
+noise-generation-service server --host 0.0.0.0 --port 9000
+
+```
+
+### Generate OpenAPI Specification
+
+
+
+```bash
+# Output to stdout
+noise-generation-service openapi
+
+# Save to file
+noise-generation-service openapi --output api-spec.json
+
+# JSON format (default)
+noise-generation-service openapi --format json
+
+# YAML output is also supported
+noise-generation-service openapi --format yaml
+
+```
+
+### CLI Help
+
+
+
+```bash
+noise-generation-service --help # All commands
+noise-generation-service generate --help # Command-specific help
+
+```
+
+---
+
+## Development
+
+
+
+### Building
+
+
+
+```bash
+make build
+
+```
+
+### Testing
+
+
+
+```bash
+make test
+
+```
+
+### Running locally
+
+
+
+```bash
+make start # Direct execution via cargo run
+# OR
+cargo run # Same as make start
+# OR
+cargo run -- server --port 8001 # CLI with custom port
+
+```
+
+### Generating OpenAPI spec
+
+
+
+```bash
+make generate-openapi
+# OR
+cargo run -- openapi --output spec.json # Via CLI
+
+```
+
+> **Important (utoipa schema registration):** This service uses an explicit `components(schemas(...))` list in `src/lib.rs`. If you add or change nested request models (for example new `params` structs or enums used by `GenerateNoiseRequest`), you must also add those types to that schema list. Otherwise the generated OpenAPI can contain unresolved `$ref` entries.
+>
+>
+
+---
+
+## Technical Details
+
+
+
+* **Language**: Rust (Edition 2021)
+
+
+* **Web Framework**: Axum 0.7
+
+
+* **Noise Libraries**: `fastnoise-lite` v1.1.1, `noise` v0.9
+
+
+* **OpenAPI**: Auto-generated via `utoipa` v4
+
+
+* **Port**: 8000 (configurable)
+
+
+* **License**: MIT
+
+
+
+---
+
+## Algorithms Reference
+
+
+
+All rows use `POST /v1/noise` with the given `algorithm` tag; default libraries are selected automatically and the response includes the full noise field data grid in the `data` field.
+
+> **⚠️ Performance Note:** For large grids (e.g., 1024×1024 or larger), the noise generation will take longer and the response payload may become very large (several MB). Consider using smaller grid sizes for interactive use, or use the CLI's `generate` command for local generation without network overhead. A future enhancement may add HTTP streaming (e.g., `Transfer-Encoding: chunked`) for large grid responses.
+>
+> **📦 Caching Note:** An internal cache for previously generated noise fields is planned as a future update. This would allow re-requesting identical noise fields (same algorithm, parameters, and sampling configuration) without re-computation, improving response times for repeated or shared requests.
+>
+>
+
+### Core Noise Algorithms
+
+
+
+| `algorithm` tag | Canonical family | Default Library | Underlying function |
+| --- | --- | --- | --- |
+| `perlin` | Perlin noise | `fastnoise_lite` | `SetNoiseType(Perlin)` + `GetNoise(x, y[, z])` |
+| `simplex` | Simplex noise (classic) | `noise_rs` | `Simplex::new(seed).get(point)` |
+| `opensimplex2` | OpenSimplex2 / OpenSimplex2S | `fastnoise_lite` | `SetNoiseType(OpenSimplex2 | OpenSimplex2S)` + `GetNoise(x, y[, z])` |
+| `supersimplex` | SuperSimplex | `noise_rs` | `SuperSimplex::new(seed).get(point)` |
+| `value` | Value noise (+cubic) | `fastnoise_lite` | `SetNoiseType(Value | ValueCubic)` + `GetNoise(x, y[, z])` |
+| `cellular` | Cellular / Worley / Voronoi | `fastnoise_lite` | `SetNoiseType(Cellular)`, `SetCellularDistanceFunction(...)`, `SetCellularReturnType(...)`, `SetCellularJitter(...)` + `GetNoise(x, y[, z])` |
+
+### Fractal Algorithms
+
+
+
+| `algorithm` tag | Canonical family | Default Library | Underlying function |
+| --- | --- | --- | --- |
+| `fbm` | Fractal Brownian Motion | `noise_rs` | `Fbm::::new(seed).set_octaves/lacunarity/persistence(...).get(point)` |
+| `billow` | Billow noise | `noise_rs` | `Billow::::new(seed).set_octaves/...(...).get(point)` |
+| `ridged_multi` | Ridged multifractal | `fastnoise_lite` | `SetFractalType(Ridged)`, `SetFrequency(...)`, `SetFractalOctaves(...)`, `SetFractalLacunarity(...)`, `SetFractalGain(...)` |
+| `hybrid_multi` | HybridMulti fractal | `noise_rs` | `HybridMulti::::new(seed).set_octaves/...(...).get(point)` |
+| `pingpong` | PingPong fractal | `fastnoise_lite` | `SetFractalType(PingPong)`, `SetFractalPingPongStrength(...)` |
+
+### Advanced Algorithms
+
+
+
+| `algorithm` tag | Canonical family | Default Library | Underlying function |
+| --- | --- | --- | --- |
+| `domain_warp` | Domain warping | `fastnoise_lite` | `SetDomainWarpType(...)`, `SetDomainWarpAmp(...)` + `DomainWarp2D/3D(x, y[, z])` |
+| `combinator` | Generic combinators | `noise_rs` | `Add`/`Multiply`/`Min`/`Max`/`Blend` (selected via `op` sub-field), each wrapping two Perlin sources with seeds `seed` and `seed+1` |
+| `utility` | Utility / deterministic generators | `noise_rs` | `Constant::new(value)` / `Cylinders::new()` (selected via `kind` sub-field) |
+
+### Native White Noise
+
+
+
+White noise requires no coherence/interpolation logic and is implemented natively rather than via an external library. Because it is inherently uncorrelated, its `params` schema is minimal (`seed` only) — each grid cell is sampled independently, so generation is trivially parallelizable.
+
+| `algorithm` tag | Canonical family | Underlying function |
+| --- | --- | --- |
+| `white` | White noise | `seeded_prng(seed, x, y, z, w).next_f32() * 2.0 - 1.0` — uncorrelated per-cell/per-point noise, no interpolation |
\ No newline at end of file
diff --git a/services/noise-generation-service/docker-compose.dev.yml b/services/noise-generation-service/docker-compose.dev.yml
new file mode 100644
index 0000000..6f8d936
--- /dev/null
+++ b/services/noise-generation-service/docker-compose.dev.yml
@@ -0,0 +1,10 @@
+services:
+ noise-generation-service:
+ container_name: noise-generation-service
+ build: .
+ ports:
+ - "8000:8000"
+ environment:
+ - RUST_LOG=debug
+ stdin_open: true
+ tty: true
diff --git a/services/noise-generation-service/docker-compose.prod.yml b/services/noise-generation-service/docker-compose.prod.yml
new file mode 100644
index 0000000..a05cd57
--- /dev/null
+++ b/services/noise-generation-service/docker-compose.prod.yml
@@ -0,0 +1,9 @@
+services:
+ noise-generation-service:
+ container_name: noise-generation-service
+ build: .
+ ports:
+ - "8000:8000"
+ environment:
+ - RUST_LOG=info
+ restart: unless-stopped
diff --git a/services/noise-generation-service/src/algorithms.rs b/services/noise-generation-service/src/algorithms.rs
new file mode 100644
index 0000000..dc94d66
--- /dev/null
+++ b/services/noise-generation-service/src/algorithms.rs
@@ -0,0 +1,218 @@
+//! Single source of truth for the set of supported noise algorithms.
+//!
+//! The [`algorithms!`] macro below generates the wire-tagged
+//! [`AlgorithmParams`] enum, the algorithm name lookup, and the
+//! dimension-support table from one declarative list. `ALGORITHM_NAMES` and
+//! `AlgorithmParams::name`/`AlgorithmParams::dim_support` are then the only
+//! things any other code (HTTP handlers, the CLI, tests) needs to consult.
+
+use crate::dim::Dim;
+use crate::model::{
+ CellularParams, CombinatorParams, DomainWarpParams, FractalParams, PingPongParams, SeedParams,
+ UtilityParams,
+};
+use serde::{Deserialize, Serialize};
+use utoipa::ToSchema;
+
+/// Which sampling dimensionalities an algorithm family supports.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum DimSupport {
+ /// 2D and 3D only (most FastNoiseLite-backed algorithms, domain warp,
+ /// noise-crate's `SuperSimplex`).
+ D2D3,
+ /// 2D, 3D, and 4D (noise-crate types implementing `NoiseFn`).
+ D2D3D4,
+ /// 1D, 2D, 3D, and 4D (the native white-noise hash, which has no
+ /// dimensionality restriction from an external library).
+ All,
+}
+
+impl DimSupport {
+ pub fn supports(&self, dim: Dim) -> bool {
+ match self {
+ DimSupport::D2D3 => matches!(dim, Dim::D2 | Dim::D3),
+ DimSupport::D2D3D4 => matches!(dim, Dim::D2 | Dim::D3 | Dim::D4),
+ DimSupport::All => true,
+ }
+ }
+
+ /// Human-readable reason shown in dimension-rejection error messages.
+ /// Only ever called after `supports()` returns `false` (see
+ /// `crate::service::generate`), so `DimSupport::All` — which supports
+ /// every dimensionality and thus never rejects — can never reach here.
+ pub fn reason(&self) -> &'static str {
+ match self {
+ DimSupport::D2D3 => "only 2D/3D sampling is supported",
+ DimSupport::D2D3D4 => "1D sampling is not supported (requires at least 2D)",
+ DimSupport::All => unreachable!("DimSupport::All supports every dimension"),
+ }
+ }
+}
+
+macro_rules! algorithms {
+ ($(($variant:ident, $wire:literal, $params:ty, $dims:expr)),* $(,)?) => {
+ #[derive(Serialize, Deserialize, Debug, ToSchema)]
+ #[serde(tag = "algorithm", content = "params")]
+ pub enum AlgorithmParams {
+ $(
+ #[serde(rename = $wire)]
+ $variant(#[serde(default)] $params),
+ )*
+ }
+
+ impl AlgorithmParams {
+ /// The wire-format name of this algorithm, e.g. `"perlin"`.
+ pub fn name(&self) -> &'static str {
+ match self {
+ $(AlgorithmParams::$variant(..) => $wire,)*
+ }
+ }
+
+ /// Which sampling dimensionalities this algorithm supports.
+ pub fn dim_support(&self) -> DimSupport {
+ match self {
+ $(AlgorithmParams::$variant(..) => $dims,)*
+ }
+ }
+ }
+
+ /// Every algorithm's wire-format name, in declaration order. This is
+ /// the single source used by `GET /v1/algorithms` and by CLI
+ /// argument validation — neither maintains its own copy of the list.
+ pub const ALGORITHM_NAMES: &[&str] = &[$($wire),*];
+ };
+}
+
+algorithms! {
+ (Perlin, "perlin", SeedParams, DimSupport::D2D3),
+ (Simplex, "simplex", SeedParams, DimSupport::D2D3D4),
+ (OpenSimplex2, "opensimplex2", SeedParams, DimSupport::D2D3),
+ (SuperSimplex, "supersimplex", SeedParams, DimSupport::D2D3),
+ (Value, "value", SeedParams, DimSupport::D2D3),
+ (Cellular, "cellular", CellularParams, DimSupport::D2D3),
+ (Fbm, "fbm", FractalParams, DimSupport::D2D3D4),
+ (Billow, "billow", FractalParams, DimSupport::D2D3D4),
+ (RidgedMulti, "ridged_multi", FractalParams, DimSupport::D2D3D4),
+ (HybridMulti, "hybrid_multi", FractalParams, DimSupport::D2D3D4),
+ (PingPong, "pingpong", PingPongParams, DimSupport::D2D3),
+ (DomainWarp, "domain_warp", DomainWarpParams, DimSupport::D2D3),
+ (Combinator, "combinator", CombinatorParams, DimSupport::D2D3D4),
+ (Utility, "utility", UtilityParams, DimSupport::D2D3D4),
+ (White, "white", SeedParams, DimSupport::All),
+}
+
+/// Default persistence values, one per fractal-family algorithm (all four
+/// share the same [`FractalParams`] shape but differ in their default
+/// `persistence`).
+pub const DEFAULT_PERSISTENCE_FBM_BILLOW: f64 = 0.5;
+/// Deliberately 0.5, not 1.0: callers wanting the noise-crate library
+/// default of 1.0 for `ridged_multi` must pass `persistence` explicitly.
+pub const DEFAULT_PERSISTENCE_RIDGED: f64 = 0.5;
+/// HybridMulti combines octave amplitudes multiplicatively; a lower
+/// persistence prevents signal saturation (consistent with noise-crate
+/// examples).
+pub const DEFAULT_PERSISTENCE_HYBRID: f64 = 0.25;
+
+/// Builds the `AlgorithmParams` value for `name` with all params omitted
+/// (i.e. `{}`), the same way `main.rs`'s CLI `--algorithm`/`--params`
+/// handling does — reusing `AlgorithmParams`'s tag/content `Deserialize`
+/// impl instead of a second per-algorithm constructor.
+fn default_algorithm_params(name: &str) -> Option {
+ serde_json::from_value(serde_json::json!({ "algorithm": name, "params": {} })).ok()
+}
+
+/// Returns default parameter values for a given algorithm name, for display
+/// via `GET /v1/algorithms`.
+///
+/// This is derived from [`crate::resolve::resolve_params`] — the same
+/// resolution logic used for actual generation — rather than a second,
+/// independently-maintained name-keyed table, so the advertised defaults
+/// can never drift from what generation actually uses. `seed` is nulled out
+/// here (rather than echoing the concrete value `resolve_params` randomly
+/// generates for a request with no seed) since defaults are meant to show
+/// "no seed was requested", not a fabricated one.
+pub fn algorithm_defaults(name: &str) -> serde_json::Value {
+ let Some(algorithm) = default_algorithm_params(name) else {
+ return serde_json::json!({});
+ };
+ let request = crate::model::GenerateNoiseRequest {
+ algorithm,
+ sampling: crate::model::Sampling { size: None },
+ output: None,
+ };
+ let mut defaults = crate::resolve::resolve_params(&request).to_json();
+ if let Some(obj) = defaults.as_object_mut() {
+ if obj.contains_key("seed") {
+ obj.insert("seed".to_string(), serde_json::Value::Null);
+ }
+ }
+ defaults
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn algorithm_names_has_15_entries() {
+ assert_eq!(ALGORITHM_NAMES.len(), 15);
+ }
+
+ #[test]
+ fn name_and_dim_support_are_consistent_with_wire_tag() {
+ let perlin = AlgorithmParams::Perlin(SeedParams::default());
+ assert_eq!(perlin.name(), "perlin");
+ assert_eq!(perlin.dim_support(), DimSupport::D2D3);
+
+ let white = AlgorithmParams::White(SeedParams::default());
+ assert_eq!(white.name(), "white");
+ assert_eq!(white.dim_support(), DimSupport::All);
+ }
+
+ #[test]
+ fn every_algorithm_name_has_defaults() {
+ for name in ALGORITHM_NAMES {
+ let defaults = algorithm_defaults(name);
+ assert!(defaults.is_object(), "{name} defaults should be an object");
+ }
+ }
+
+ #[test]
+ fn fbm_defaults_match_resolve_params_constants() {
+ let defaults = algorithm_defaults("fbm");
+ assert!(defaults["seed"].is_null());
+ assert_eq!(defaults["octaves"], crate::resolve::DEFAULT_OCTAVES);
+ assert_eq!(defaults["frequency"], crate::resolve::DEFAULT_FREQUENCY);
+ assert_eq!(defaults["lacunarity"], crate::resolve::DEFAULT_LACUNARITY);
+ assert_eq!(defaults["persistence"], DEFAULT_PERSISTENCE_FBM_BILLOW);
+ }
+
+ #[test]
+ fn ridged_multi_and_hybrid_multi_defaults_have_distinct_persistence() {
+ assert_eq!(
+ algorithm_defaults("ridged_multi")["persistence"],
+ DEFAULT_PERSISTENCE_RIDGED
+ );
+ assert_eq!(
+ algorithm_defaults("hybrid_multi")["persistence"],
+ DEFAULT_PERSISTENCE_HYBRID
+ );
+ }
+
+ #[test]
+ fn utility_defaults_have_no_seed_field() {
+ // Utility noise is deterministic and takes no seed, unlike every
+ // other algorithm family.
+ let defaults = algorithm_defaults("utility");
+ assert!(defaults.get("seed").is_none());
+ assert_eq!(defaults["kind"], "constant");
+ }
+
+ #[test]
+ fn unknown_algorithm_name_returns_empty_defaults() {
+ assert_eq!(
+ algorithm_defaults("not-a-real-algorithm"),
+ serde_json::json!({})
+ );
+ }
+}
diff --git a/services/noise-generation-service/src/cli.rs b/services/noise-generation-service/src/cli.rs
new file mode 100644
index 0000000..99c4499
--- /dev/null
+++ b/services/noise-generation-service/src/cli.rs
@@ -0,0 +1,213 @@
+//! `clap` CLI argument definitions.
+//!
+//! `--algorithm` is validated at parse time against
+//! `crate::algorithms::ALGORITHM_NAMES` (the same table the HTTP API and
+//! `GET /v1/algorithms` use) and stored as the wire-name `String` directly,
+//! so `main.rs` can hand it straight to `serde_json` to build an
+//! `AlgorithmParams` value.
+
+use clap::{Args, Parser, Subcommand, ValueEnum};
+use serde_json::Value;
+use std::fmt;
+use std::path::PathBuf;
+
+use crate::algorithms::ALGORITHM_NAMES;
+
+/// Parses and validates an `--algorithm` value against `ALGORITHM_NAMES`,
+/// producing a clap-friendly error listing the valid options on failure.
+///
+/// Also accepts kebab-case spellings of multi-word algorithm names (e.g.
+/// `ridged-multi`, `open-simplex2`, `ping-pong`), which differ from the
+/// wire-format snake_case names used everywhere else (`ridged_multi`,
+/// `opensimplex2`, `pingpong`). Matching against the input with hyphens
+/// both converted to underscores *and* stripped entirely covers every
+/// accepted spelling (`ridged-multi` -> `ridged_multi`, `ping-pong` ->
+/// `pingpong`, `open-simplex2` -> `opensimplex2`) without hardcoding a
+/// per-name alias table.
+fn parse_algorithm(s: &str) -> Result {
+ let underscored = s.replace('-', "_");
+ let collapsed = s.replace('-', "");
+ if let Some(name) = ALGORITHM_NAMES
+ .iter()
+ .find(|n| **n == s || **n == underscored || **n == collapsed)
+ {
+ Ok(name.to_string())
+ } else {
+ Err(format!(
+ "invalid algorithm '{s}' (expected one of: {})",
+ ALGORITHM_NAMES.join(", ")
+ ))
+ }
+}
+
+// ─── Format Enums ─────────────────────────────────────────────────────────────
+
+/// Output format shared by the `list` and `generate` CLI commands. This is a
+/// CLI-only rendering choice — unrelated to `model::OutputFormat`, which
+/// describes the HTTP API's (JSON-only) response format.
+#[derive(ValueEnum, Clone, Debug, PartialEq)]
+pub enum Format {
+ Json,
+ Csv,
+}
+
+impl fmt::Display for Format {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Format::Json => write!(f, "json"),
+ Format::Csv => write!(f, "csv"),
+ }
+ }
+}
+
+#[derive(ValueEnum, Clone, Debug, PartialEq)]
+pub enum OpenApiFormat {
+ Json,
+ Yaml,
+}
+
+impl fmt::Display for OpenApiFormat {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ OpenApiFormat::Json => write!(f, "json"),
+ OpenApiFormat::Yaml => write!(f, "yaml"),
+ }
+ }
+}
+
+// ─── Custom Parsers ───────────────────────────────────────────────────────────
+
+fn parse_params_json(s: &str) -> Result {
+ serde_json::from_str(s).map_err(|e| format!("invalid JSON params: {}", e))
+}
+
+// ─── CLI Structure ────────────────────────────────────────────────────────────
+
+#[derive(Parser)]
+#[command(author, version, about, long_about = None)]
+#[command(name = "noise-generation-service")]
+#[command(about = "Unified CLI for noise generation algorithms")]
+pub struct Cli {
+ #[command(subcommand)]
+ pub command: Option,
+}
+
+#[derive(Subcommand, Debug, PartialEq)]
+pub enum Commands {
+ /// List all available algorithms
+ #[command(alias = "ls")]
+ List {
+ /// Output format: json (default) or csv
+ #[arg(short, long, default_value = "json")]
+ format: Format,
+ },
+
+ /// Generate noise using specified algorithm (local)
+ #[command(alias = "gen")]
+ Generate(GenerateArgs),
+
+ /// Start the HTTP server
+ #[command(alias = "serve")]
+ Server {
+ /// Port to listen on
+ #[arg(short, long, default_value = "8000")]
+ port: u16,
+
+ /// Host to bind to
+ #[arg(long, default_value = "0.0.0.0")]
+ host: String,
+ },
+
+ /// Generate OpenAPI specification
+ #[command(name = "openapi")]
+ OpenApi {
+ /// Output format (json, yaml)
+ #[arg(short, long, default_value = "json")]
+ format: OpenApiFormat,
+
+ /// Output file (stdout if not specified)
+ #[arg(short, long)]
+ output: Option,
+ },
+}
+
+#[derive(Args, Debug, PartialEq, Clone)]
+pub struct GenerateArgs {
+ /// Algorithm to use (see `list` for the full set of valid names)
+ #[arg(short, long, value_parser = parse_algorithm)]
+ pub algorithm: String,
+
+ /// Size of noise field (comma-separated, e.g. "64,64" or "64" for square).
+ /// Maps to `sampling.size` in the API.
+ ///
+ /// `default_value` must be a string literal (clap's attribute macro
+ /// can't reference `crate::limits::DEFAULT_SAMPLING_SIZE` directly), so
+ /// this is pinned against that constant by
+ /// `tests/cli_tests.rs::test_cli_parse_generate` instead — update both
+ /// if the default ever changes.
+ #[arg(
+ short = 's', long = "sampling-size",
+ default_value = "64,64", value_delimiter = ',', num_args = 1..=4,
+ visible_alias = "size"
+ )]
+ pub sampling_size: Vec,
+
+ /// Output format: json (default) or csv. Maps to `output.format` in the API.
+ #[arg(
+ short = 'f',
+ long = "output-format",
+ default_value = "json",
+ visible_alias = "format"
+ )]
+ pub output_format: Format,
+
+ /// Output file (stdout if not specified). CLI-only, not part of the API schema.
+ #[arg(short = 'o', long = "output-file", visible_alias = "output")]
+ pub output_file: Option,
+
+ /// Algorithm parameters as JSON object (e.g. '{"seed": 42}')
+ #[arg(long, default_value = "{}", value_parser = parse_params_json)]
+ pub params: serde_json::Value,
+
+ /// Normalize output values to [0,1] range. Maps to `output.normalize` in the API.
+ #[arg(long = "output-normalize", visible_alias = "normalize")]
+ pub output_normalize: bool,
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn rejects_unknown_algorithm_name() {
+ assert!(parse_algorithm("not-a-real-algorithm").is_err());
+ }
+
+ #[test]
+ fn accepts_every_known_algorithm_name() {
+ for name in ALGORITHM_NAMES {
+ assert_eq!(parse_algorithm(name), Ok(name.to_string()));
+ }
+ }
+
+ /// Kebab-case spellings of multi-word algorithm names must keep working
+ /// so existing scripts/CLI invocations don't silently break.
+ #[test]
+ fn accepts_legacy_kebab_case_aliases() {
+ let cases = [
+ ("open-simplex2", "opensimplex2"),
+ ("super-simplex", "supersimplex"),
+ ("ridged-multi", "ridged_multi"),
+ ("hybrid-multi", "hybrid_multi"),
+ ("ping-pong", "pingpong"),
+ ("domain-warp", "domain_warp"),
+ ];
+ for (legacy, canonical) in cases {
+ assert_eq!(
+ parse_algorithm(legacy),
+ Ok(canonical.to_string()),
+ "legacy alias '{legacy}' should map to '{canonical}'"
+ );
+ }
+ }
+}
diff --git a/services/noise-generation-service/src/dim.rs b/services/noise-generation-service/src/dim.rs
new file mode 100644
index 0000000..7bd4e41
--- /dev/null
+++ b/services/noise-generation-service/src/dim.rs
@@ -0,0 +1,247 @@
+//! Dimensionality of a sampling grid, and shared per-cell iteration helpers.
+//!
+//! `Dim` makes the set of supported dimensionalities a compile-time-checked
+//! enum, so every generation kernel can match on it exhaustively instead of
+//! branching on a `mode: &str` value.
+
+use std::fmt;
+
+/// The coordinate scaling factor applied to noise-crate/FastNoiseLite sample
+/// positions. Named so every generation kernel shares one definition instead
+/// of repeating the bare literal `0.1` at each call site.
+pub const COORD_STEP: f64 = 0.1;
+
+/// Supported sampling dimensionalities (1D-4D). 5D+ is rejected by every
+/// algorithm and is therefore not representable by this type.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum Dim {
+ D1,
+ D2,
+ D3,
+ D4,
+}
+
+impl Dim {
+ /// Maps a `sampling.size` length to a `Dim`, or `None` for unsupported
+ /// (0 or 5+) lengths.
+ pub fn from_len(len: usize) -> Option {
+ match len {
+ 1 => Some(Dim::D1),
+ 2 => Some(Dim::D2),
+ 3 => Some(Dim::D3),
+ 4 => Some(Dim::D4),
+ _ => None,
+ }
+ }
+
+ pub fn as_usize(&self) -> usize {
+ match self {
+ Dim::D1 => 1,
+ Dim::D2 => 2,
+ Dim::D3 => 3,
+ Dim::D4 => 4,
+ }
+ }
+}
+
+impl fmt::Display for Dim {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{}D", self.as_usize())
+ }
+}
+
+/// Row-major cell iteration for a 2D grid: yields `(flat_index, [x, y])`.
+///
+/// Implemented as an explicit `Iterator` (incrementing plain coordinate/index
+/// counters) rather than a nested `flat_map`/`map` chain: the flat_map form
+/// recomputes each flat index from scratch per cell (e.g. `y * w + x`),
+/// which is measurably slower than a running increment for the cheaper
+/// kernels (white noise, `Utility::Constant`) where index bookkeeping is a
+/// larger fraction of the per-cell cost.
+pub fn iter_2d(size: &[usize]) -> Cells2D {
+ Cells2D {
+ w: size[0],
+ h: size[1],
+ x: 0,
+ y: 0,
+ idx: 0,
+ }
+}
+
+pub struct Cells2D {
+ w: usize,
+ h: usize,
+ x: usize,
+ y: usize,
+ idx: usize,
+}
+
+impl Iterator for Cells2D {
+ type Item = (usize, [usize; 2]);
+
+ fn next(&mut self) -> Option {
+ if self.y >= self.h {
+ return None;
+ }
+ let item = (self.idx, [self.x, self.y]);
+ self.idx += 1;
+ self.x += 1;
+ if self.x >= self.w {
+ self.x = 0;
+ self.y += 1;
+ }
+ Some(item)
+ }
+}
+
+/// Row-major cell iteration for a 3D grid: yields `(flat_index, [x, y, z])`.
+pub fn iter_3d(size: &[usize]) -> Cells3D {
+ Cells3D {
+ w: size[0],
+ h: size[1],
+ d: size[2],
+ x: 0,
+ y: 0,
+ z: 0,
+ idx: 0,
+ }
+}
+
+pub struct Cells3D {
+ w: usize,
+ h: usize,
+ d: usize,
+ x: usize,
+ y: usize,
+ z: usize,
+ idx: usize,
+}
+
+impl Iterator for Cells3D {
+ type Item = (usize, [usize; 3]);
+
+ fn next(&mut self) -> Option {
+ if self.z >= self.d {
+ return None;
+ }
+ let item = (self.idx, [self.x, self.y, self.z]);
+ self.idx += 1;
+ self.x += 1;
+ if self.x >= self.w {
+ self.x = 0;
+ self.y += 1;
+ if self.y >= self.h {
+ self.y = 0;
+ self.z += 1;
+ }
+ }
+ Some(item)
+ }
+}
+
+/// Row-major cell iteration for a 4D grid: yields `(flat_index, [x, y, z, w])`.
+pub fn iter_4d(size: &[usize]) -> Cells4D {
+ Cells4D {
+ w: size[0],
+ h: size[1],
+ d: size[2],
+ t: size[3],
+ x: 0,
+ y: 0,
+ z: 0,
+ w4: 0,
+ idx: 0,
+ }
+}
+
+pub struct Cells4D {
+ w: usize,
+ h: usize,
+ d: usize,
+ t: usize,
+ x: usize,
+ y: usize,
+ z: usize,
+ w4: usize,
+ idx: usize,
+}
+
+impl Iterator for Cells4D {
+ type Item = (usize, [usize; 4]);
+
+ fn next(&mut self) -> Option {
+ if self.w4 >= self.t {
+ return None;
+ }
+ let item = (self.idx, [self.x, self.y, self.z, self.w4]);
+ self.idx += 1;
+ self.x += 1;
+ if self.x >= self.w {
+ self.x = 0;
+ self.y += 1;
+ if self.y >= self.h {
+ self.y = 0;
+ self.z += 1;
+ if self.z >= self.d {
+ self.z = 0;
+ self.w4 += 1;
+ }
+ }
+ }
+ Some(item)
+ }
+}
+
+/// Converts an integer grid coordinate into the scaled `f64` coordinate used
+/// by noise-crate/FastNoiseLite sample positions.
+pub fn scaled(coord: usize) -> f64 {
+ coord as f64 * COORD_STEP
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn from_len_maps_1_to_4() {
+ assert_eq!(Dim::from_len(1), Some(Dim::D1));
+ assert_eq!(Dim::from_len(2), Some(Dim::D2));
+ assert_eq!(Dim::from_len(3), Some(Dim::D3));
+ assert_eq!(Dim::from_len(4), Some(Dim::D4));
+ assert_eq!(Dim::from_len(0), None);
+ assert_eq!(Dim::from_len(5), None);
+ }
+
+ #[test]
+ fn iter_2d_row_major_order() {
+ let size = [2usize, 3usize];
+ let cells: Vec<(usize, [usize; 2])> = iter_2d(&size).collect();
+ assert_eq!(cells.len(), 6);
+ assert_eq!(cells[0], (0, [0, 0]));
+ assert_eq!(cells[1], (1, [1, 0]));
+ assert_eq!(cells[2], (2, [0, 1]));
+ assert_eq!(cells[5], (5, [1, 2]));
+ }
+
+ #[test]
+ fn iter_3d_covers_every_cell_exactly_once() {
+ let size = [2usize, 2usize, 2usize];
+ let mut seen = vec![false; 8];
+ for (idx, _coords) in iter_3d(&size) {
+ assert!(!seen[idx], "index {idx} visited twice");
+ seen[idx] = true;
+ }
+ assert!(seen.into_iter().all(|v| v));
+ }
+
+ #[test]
+ fn iter_4d_covers_every_cell_exactly_once() {
+ let size = [2usize, 2usize, 2usize, 2usize];
+ let mut seen = vec![false; 16];
+ for (idx, _coords) in iter_4d(&size) {
+ assert!(!seen[idx], "index {idx} visited twice");
+ seen[idx] = true;
+ }
+ assert!(seen.into_iter().all(|v| v));
+ }
+}
diff --git a/services/noise-generation-service/src/error.rs b/services/noise-generation-service/src/error.rs
new file mode 100644
index 0000000..afe1718
--- /dev/null
+++ b/services/noise-generation-service/src/error.rs
@@ -0,0 +1,82 @@
+//! Structured errors for noise-field generation.
+//!
+//! `NoiseError` centralizes the failure cases; the HTTP handler
+//! (`crate::http`) is the only place that turns one into the response body,
+//! and the CLI can match on it directly instead of parsing a string.
+
+use axum::http::StatusCode;
+
+use crate::dim::Dim;
+use crate::model::NoiseFieldResult;
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum NoiseError {
+ /// `sampling.size` has a length that maps to no supported dimensionality
+ /// (0 or 5+ entries).
+ UnsupportedSizeLength { len: usize },
+ /// The algorithm does not support the requested (but otherwise valid)
+ /// dimensionality.
+ UnsupportedDimension {
+ algorithm: &'static str,
+ dim: Dim,
+ reason: &'static str,
+ },
+ /// `output.format: csv` was requested against the HTTP API, which only
+ /// ever returns JSON (CSV rendering is CLI-only).
+ UnsupportedCsv,
+ /// A sampling dimension is zero or exceeds `MAX_SAMPLING_DIM`.
+ DimensionOutOfRange { size: Vec, max: usize },
+ /// The total cell count (product of all dimensions) exceeds
+ /// `MAX_SAMPLING_CELLS`.
+ TooManyCells { size: Vec, max: usize },
+}
+
+impl NoiseError {
+ /// All current failure cases map to 400 Bad Request — they are all
+ /// client input problems — but centralizing the mapping here means a
+ /// future error variant (e.g. an internal generation failure) can pick a
+ /// different code without touching every call site.
+ pub fn status_code(&self) -> StatusCode {
+ StatusCode::BAD_REQUEST
+ }
+
+ /// The human-readable description of this error, without the `"error: "`
+ /// prefix used in the HTTP response body's `status` field.
+ pub fn message(&self) -> String {
+ match self {
+ NoiseError::UnsupportedSizeLength { len } => {
+ format!("sampling.size has {len} dimensions; only 1D-4D are supported")
+ }
+ NoiseError::UnsupportedDimension {
+ algorithm,
+ dim,
+ reason,
+ } => format!("algorithm '{algorithm}' does not support {dim} sampling ({reason})"),
+ NoiseError::UnsupportedCsv => {
+ "output.format 'csv' is not supported by POST /v1/noise; use the CLI's \
+ --output-format csv, or omit output.format for JSON"
+ .to_string()
+ }
+ NoiseError::DimensionOutOfRange { size, max } => {
+ format!("each sampling dimension must be between 1 and {max} (got {size:?})")
+ }
+ NoiseError::TooManyCells { size, max } => {
+ format!("requested sampling size {size:?} exceeds the maximum of {max} total cells")
+ }
+ }
+ }
+
+ /// Renders this error as the same `NoiseFieldResult` shape historically
+ /// returned on failure: `status` is `"error: "`, `data` and
+ /// `params_used` are `null`.
+ pub fn into_result(self, id: String, algorithm: String, size: Vec) -> NoiseFieldResult {
+ NoiseFieldResult {
+ id,
+ status: format!("error: {}", self.message()),
+ algorithm,
+ data: serde_json::Value::Null,
+ size,
+ params_used: serde_json::Value::Null,
+ }
+ }
+}
diff --git a/services/noise-generation-service/src/generate.rs b/services/noise-generation-service/src/generate.rs
new file mode 100644
index 0000000..8f77098
--- /dev/null
+++ b/services/noise-generation-service/src/generate.rs
@@ -0,0 +1,476 @@
+//! Per-algorithm noise generation into a flat `Vec` buffer.
+//!
+//! `size` is the grid extent: `[width]` for 1D, `[width, height]` for 2D,
+//! `[width, height, depth]` for 3D, `[width, height, depth, w]` for 4D. Cell
+//! iteration order matches `crate::shape::shape_data`, which reshapes the
+//! same flat buffer into nested JSON.
+
+use fastnoise_lite::FastNoiseLite;
+use noise::{
+ Add, Blend, Constant, Cylinders, HybridMulti, Max, Min, MultiFractal, Multiply, NoiseFn,
+ Perlin, Seedable, Simplex, SuperSimplex,
+};
+
+use crate::algorithms::AlgorithmParams;
+use crate::dim::{iter_2d, iter_3d, iter_4d, scaled, Dim};
+use crate::model::{
+ CellularDistanceFunction, CellularReturnType, CombinatorOp, GenerateNoiseRequest, UtilityKind,
+};
+use crate::resolve::ResolvedNoiseParams;
+
+/// Evaluates a per-cell closure over every 2D/3D/4D cell, using the shared
+/// `crate::dim` iterators for indexing (so the row-major flattening math
+/// lives in exactly one place: `crate::dim`). The coordinate-array
+/// construction is still repeated once per dimensionality because `noise`'s
+/// `NoiseFn` trait is generic over a const array length that Rust cannot
+/// infer from a single closure shared across 2/3/4 element arrays — but no
+/// nested-loop or index-arithmetic duplication remains here. Shared by
+/// Combinator and Utility, which both call into `noise`-crate `NoiseFn`
+/// sources with no other algorithm-specific setup.
+macro_rules! dispatch_cells {
+ ($flat:expr, $size:expr, $dim:expr, |$pos:ident| $body:expr) => {
+ match $dim {
+ Dim::D2 => {
+ for (idx, [x, y]) in iter_2d($size) {
+ let $pos: [f64; 2] = [scaled(x), scaled(y)];
+ $flat[idx] = $body;
+ }
+ }
+ Dim::D3 => {
+ for (idx, [x, y, z]) in iter_3d($size) {
+ let $pos: [f64; 3] = [scaled(x), scaled(y), scaled(z)];
+ $flat[idx] = $body;
+ }
+ }
+ Dim::D4 => {
+ for (idx, [x, y, z, w]) in iter_4d($size) {
+ let $pos: [f64; 4] = [scaled(x), scaled(y), scaled(z), scaled(w)];
+ $flat[idx] = $body;
+ }
+ }
+ Dim::D1 => {
+ unreachable!("dimension already validated: Combinator/Utility require 2D-4D")
+ }
+ }
+ };
+}
+
+/// Generates noise into a flat `Vec` according to the requested
+/// dimensionality. `resolved` contains the already-resolved parameters
+/// (seed etc.) — this is the *only* place parameter resolution happens for
+/// generation, ensuring consistency with `params_used`.
+pub fn generate_flat(
+ flat: &mut [f64],
+ payload: &GenerateNoiseRequest,
+ resolved: &ResolvedNoiseParams,
+ size: &[usize],
+ dim: Dim,
+) {
+ match (&payload.algorithm, resolved) {
+ // ─── fastnoise-lite seed-only algorithms (2D/3D) ───────────────────
+ (AlgorithmParams::Perlin(..), ResolvedNoiseParams::SeedOnly { seed }) => {
+ fill_fnl_seed_only(flat, size, dim, *seed, fastnoise_lite::NoiseType::Perlin);
+ }
+ (AlgorithmParams::OpenSimplex2(..), ResolvedNoiseParams::SeedOnly { seed }) => {
+ fill_fnl_seed_only(
+ flat,
+ size,
+ dim,
+ *seed,
+ fastnoise_lite::NoiseType::OpenSimplex2,
+ );
+ }
+ (AlgorithmParams::Value(..), ResolvedNoiseParams::SeedOnly { seed }) => {
+ fill_fnl_seed_only(flat, size, dim, *seed, fastnoise_lite::NoiseType::Value);
+ }
+ (
+ AlgorithmParams::Cellular(..),
+ ResolvedNoiseParams::Cellular {
+ seed,
+ distance_function,
+ return_type,
+ jitter,
+ },
+ ) => {
+ let mut noise = FastNoiseLite::with_seed(*seed as i32);
+ noise.set_noise_type(Some(fastnoise_lite::NoiseType::Cellular));
+ noise.set_cellular_distance_function(Some(distance_function.into()));
+ noise.set_cellular_return_type(Some(return_type.into()));
+ noise.set_cellular_jitter(Some(*jitter as f32));
+ fill_fnl(flat, size, dim, &noise);
+ }
+ // ─── PingPong: fastnoise-lite fractal ──────────────────────────────
+ (AlgorithmParams::PingPong(..), ResolvedNoiseParams::PingPong { seed, strength }) => {
+ let mut noise = FastNoiseLite::with_seed(*seed as i32);
+ noise.set_fractal_type(Some(fastnoise_lite::FractalType::PingPong));
+ noise.set_fractal_ping_pong_strength(Some(*strength as f32));
+ noise.set_noise_type(Some(fastnoise_lite::NoiseType::Perlin));
+ fill_fnl(flat, size, dim, &noise);
+ }
+ // ─── DomainWarp: fastnoise-lite domain warp ────────────────────────
+ (AlgorithmParams::DomainWarp(..), ResolvedNoiseParams::DomainWarp { seed, amplitude }) => {
+ fill_domain_warp(flat, size, dim, *seed as i32, *amplitude);
+ }
+ // ─── noise-crate algorithms (2D/3D/4D) ─────────────────────────────
+ (AlgorithmParams::Simplex(..), ResolvedNoiseParams::SeedOnly { seed }) => {
+ let simplex = Simplex::new(*seed);
+ fill_noise_rs_4d::(flat, size, dim, &simplex);
+ }
+ (AlgorithmParams::SuperSimplex(..), ResolvedNoiseParams::SeedOnly { seed }) => {
+ let s = SuperSimplex::new(*seed);
+ fill_noise_rs::(flat, size, dim, &s);
+ }
+ // ─── Fractal family (Fbm, Billow, RidgedMulti, HybridMulti) ────────
+ (
+ AlgorithmParams::Fbm(..),
+ ResolvedNoiseParams::Fractal {
+ seed,
+ octaves,
+ frequency,
+ lacunarity,
+ persistence,
+ },
+ ) => fill_fractal::>(
+ flat,
+ size,
+ dim,
+ *seed,
+ *octaves,
+ *frequency,
+ *lacunarity,
+ *persistence,
+ ),
+ (
+ AlgorithmParams::Billow(..),
+ ResolvedNoiseParams::Fractal {
+ seed,
+ octaves,
+ frequency,
+ lacunarity,
+ persistence,
+ },
+ ) => fill_fractal::>(
+ flat,
+ size,
+ dim,
+ *seed,
+ *octaves,
+ *frequency,
+ *lacunarity,
+ *persistence,
+ ),
+ (
+ AlgorithmParams::RidgedMulti(..),
+ ResolvedNoiseParams::Fractal {
+ seed,
+ octaves,
+ frequency,
+ lacunarity,
+ persistence,
+ },
+ ) => fill_fractal::>(
+ flat,
+ size,
+ dim,
+ *seed,
+ *octaves,
+ *frequency,
+ *lacunarity,
+ *persistence,
+ ),
+ (
+ AlgorithmParams::HybridMulti(..),
+ ResolvedNoiseParams::Fractal {
+ seed,
+ octaves,
+ frequency,
+ lacunarity,
+ persistence,
+ },
+ ) => fill_fractal::>(
+ flat,
+ size,
+ dim,
+ *seed,
+ *octaves,
+ *frequency,
+ *lacunarity,
+ *persistence,
+ ),
+ // ─── Combinator ─────────────────────────────────────────────────────
+ (
+ AlgorithmParams::Combinator(..),
+ ResolvedNoiseParams::Combinator {
+ seed,
+ op,
+ blend_factor,
+ },
+ ) => {
+ let source1 = Perlin::new(*seed);
+ let source2 = Perlin::new(seed.wrapping_add(1));
+ let bf = *blend_factor;
+ dispatch_cells!(flat, size, dim, |pos| match op {
+ CombinatorOp::Add => Add::new(source1, source2).get(pos),
+ CombinatorOp::Multiply => Multiply::new(source1, source2).get(pos),
+ CombinatorOp::Min => Min::new(source1, source2).get(pos),
+ CombinatorOp::Max => Max::new(source1, source2).get(pos),
+ CombinatorOp::Blend => Blend::new(source1, source2, Constant::new(bf)).get(pos),
+ });
+ }
+ // ─── Utility ────────────────────────────────────────────────────────
+ (AlgorithmParams::Utility(..), ResolvedNoiseParams::Utility { kind, value }) => {
+ let val = *value;
+ dispatch_cells!(flat, size, dim, |pos| match kind {
+ UtilityKind::Constant => Constant::new(val).get(pos),
+ UtilityKind::Cylinders => Cylinders::new().get(pos),
+ });
+ }
+ // ─── White noise ────────────────────────────────────────────────────
+ (AlgorithmParams::White(..), ResolvedNoiseParams::SeedOnly { seed }) => {
+ fill_white_noise(flat, size, dim, *seed as u64);
+ }
+ // Safety: payload variant always matches resolved variant — enforced
+ // by `resolve_params` producing exactly one `ResolvedNoiseParams`
+ // shape per `AlgorithmParams` variant.
+ _ => unreachable!("payload/resolved type mismatch — this is a programming error"),
+ }
+}
+
+impl From<&CellularDistanceFunction> for fastnoise_lite::CellularDistanceFunction {
+ fn from(value: &CellularDistanceFunction) -> Self {
+ match value {
+ CellularDistanceFunction::Euclidean => Self::Euclidean,
+ CellularDistanceFunction::EuclideanSq => Self::EuclideanSq,
+ CellularDistanceFunction::Manhattan => Self::Manhattan,
+ CellularDistanceFunction::Hybrid => Self::Hybrid,
+ }
+ }
+}
+
+impl From<&CellularReturnType> for fastnoise_lite::CellularReturnType {
+ fn from(value: &CellularReturnType) -> Self {
+ match value {
+ CellularReturnType::CellValue => Self::CellValue,
+ CellularReturnType::Distance => Self::Distance,
+ CellularReturnType::Distance2 => Self::Distance2,
+ CellularReturnType::Distance2Add => Self::Distance2Add,
+ CellularReturnType::Distance2Sub => Self::Distance2Sub,
+ CellularReturnType::Distance2Mul => Self::Distance2Mul,
+ CellularReturnType::Distance2Div => Self::Distance2Div,
+ }
+ }
+}
+
+/// Wraps filler for noise-rs fractal types (Fbm, Billow, RidgedMulti, HybridMulti).
+#[allow(clippy::too_many_arguments)]
+fn fill_fractal(
+ flat: &mut [f64],
+ size: &[usize],
+ dim: Dim,
+ seed: u32,
+ octaves: usize,
+ frequency: f64,
+ lacunarity: f64,
+ persistence: f64,
+) where
+ T: NoiseFn + NoiseFn + NoiseFn + MultiFractal + Seedable + Default,
+{
+ let n = T::default()
+ .set_seed(seed)
+ .set_octaves(octaves)
+ .set_frequency(frequency)
+ .set_lacunarity(lacunarity)
+ .set_persistence(persistence);
+ fill_noise_rs_4d::(flat, size, dim, &n);
+}
+
+/// Wraps fill_fnl for seed-only FNL algorithms (Perlin, OpenSimplex2, Value).
+fn fill_fnl_seed_only(
+ flat: &mut [f64],
+ size: &[usize],
+ dim: Dim,
+ seed: u32,
+ noise_type: fastnoise_lite::NoiseType,
+) {
+ let mut noise = FastNoiseLite::with_seed(seed as i32);
+ noise.set_noise_type(Some(noise_type));
+ fill_fnl(flat, size, dim, &noise);
+}
+
+/// Fills `flat` using fastnoise-lite (2D or 3D only — fastnoise-lite has no
+/// 1D or 4D sampling API).
+fn fill_fnl(flat: &mut [f64], size: &[usize], dim: Dim, noise: &FastNoiseLite) {
+ match dim {
+ Dim::D2 => {
+ for (idx, [x, y]) in iter_2d(size) {
+ flat[idx] = noise.get_noise_2d(x as f32, y as f32) as f64;
+ }
+ }
+ Dim::D3 => {
+ for (idx, [x, y, z]) in iter_3d(size) {
+ flat[idx] = noise.get_noise_3d(x as f32, y as f32, z as f32) as f64;
+ }
+ }
+ Dim::D1 | Dim::D4 => {
+ unreachable!("dimension already validated: fastnoise-lite is 2D/3D only")
+ }
+ }
+}
+
+/// Fills `flat` using a noise-crate `NoiseFn` source (2D or 3D only).
+/// Used by `SuperSimplex`, which does not implement `NoiseFn`.
+fn fill_noise_rs(flat: &mut [f64], size: &[usize], dim: Dim, noise: &T)
+where
+ T: NoiseFn + NoiseFn,
+{
+ match dim {
+ Dim::D2 => {
+ for (idx, [x, y]) in iter_2d(size) {
+ flat[idx] = noise.get([scaled(x), scaled(y)]);
+ }
+ }
+ Dim::D3 => {
+ for (idx, [x, y, z]) in iter_3d(size) {
+ flat[idx] = noise.get([scaled(x), scaled(y), scaled(z)]);
+ }
+ }
+ Dim::D1 | Dim::D4 => {
+ unreachable!("dimension already validated: SuperSimplex is 2D/3D only")
+ }
+ }
+}
+
+/// Fills `flat` using a noise-crate `NoiseFn` source (2D, 3D, or 4D).
+/// Used by most noise-rs generators, which implement `NoiseFn` for all three.
+fn fill_noise_rs_4d(flat: &mut [f64], size: &[usize], dim: Dim, noise: &T)
+where
+ T: NoiseFn + NoiseFn + NoiseFn,
+{
+ match dim {
+ Dim::D2 => {
+ for (idx, [x, y]) in iter_2d(size) {
+ flat[idx] = noise.get([scaled(x), scaled(y)]);
+ }
+ }
+ Dim::D3 => {
+ for (idx, [x, y, z]) in iter_3d(size) {
+ flat[idx] = noise.get([scaled(x), scaled(y), scaled(z)]);
+ }
+ }
+ Dim::D4 => {
+ for (idx, [x, y, z, w]) in iter_4d(size) {
+ flat[idx] = noise.get([scaled(x), scaled(y), scaled(z), scaled(w)]);
+ }
+ }
+ Dim::D1 => {
+ unreachable!("dimension already validated: noise-crate types require at least 2D")
+ }
+ }
+}
+
+/// Fills `flat` using fastnoise-lite domain warping (2D or 3D only).
+fn fill_domain_warp(flat: &mut [f64], size: &[usize], dim: Dim, seed: i32, amplitude: f64) {
+ let mut warp = FastNoiseLite::with_seed(seed);
+ warp.set_domain_warp_type(Some(fastnoise_lite::DomainWarpType::OpenSimplex2));
+ warp.set_domain_warp_amp(Some(amplitude as f32));
+ // `base` depends only on `seed`, not on the loop position — build it once
+ // outside the loop instead of once per cell.
+ let mut base = FastNoiseLite::with_seed(seed.wrapping_add(1));
+ base.set_noise_type(Some(fastnoise_lite::NoiseType::Perlin));
+
+ match dim {
+ Dim::D2 => {
+ for (idx, [x, y]) in iter_2d(size) {
+ let (wx, wy) = warp.domain_warp_2d(x as f32, y as f32);
+ flat[idx] = base.get_noise_2d(wx, wy) as f64;
+ }
+ }
+ Dim::D3 => {
+ for (idx, [x, y, z]) in iter_3d(size) {
+ let (wx, wy, wz) = warp.domain_warp_3d(x as f32, y as f32, z as f32);
+ flat[idx] = base.get_noise_3d(wx, wy, wz) as f64;
+ }
+ }
+ Dim::D1 | Dim::D4 => unreachable!("dimension already validated: domain warp is 2D/3D only"),
+ }
+}
+
+// ─── White noise ────────────────────────────────────────────────────────────
+
+/// PCG-style hash constants. `MUL`/`INC` are the standard PCG32 LCG
+/// multiplier/increment; `AVALANCHE` and the per-axis mixing constants are
+/// arbitrary large odd numbers chosen to decorrelate each coordinate axis
+/// before the final hash — their exact values don't matter for correctness,
+/// only that they differ per axis and are odd (for good bit mixing).
+const PCG_MUL: u64 = 6364136223846793005;
+const PCG_INC: u64 = 1442695040888963407;
+const AVALANCHE_MUL: u64 = 12741261754838537793;
+const AXIS_MUL: [u64; 4] = [374761393, 668265263, 941568331, 1221221227];
+
+/// Uncorrelated per-cell hash noise: each cell's value depends only on the
+/// seed and its own integer coordinates, with no interpolation between
+/// neighbors (unlike every other algorithm in this service). Supports 1D-4D
+/// natively since it has no external-library dimensionality restriction.
+fn white_noise(seed: u64, coords: &[usize]) -> f64 {
+ let mut state = seed.wrapping_mul(PCG_MUL).wrapping_add(PCG_INC);
+ for (axis, &c) in coords.iter().enumerate() {
+ state ^= (c as u64).wrapping_mul(AXIS_MUL[axis]);
+ }
+ state = state.wrapping_mul(AVALANCHE_MUL);
+ let hash = state ^ (state >> 31);
+ (hash as f64 / u64::MAX as f64) * 2.0 - 1.0
+}
+
+fn fill_white_noise(flat: &mut [f64], size: &[usize], dim: Dim, seed: u64) {
+ match dim {
+ Dim::D1 => {
+ for x in 0..size[0] {
+ flat[x] = white_noise(seed, &[x]);
+ }
+ }
+ Dim::D2 => {
+ for (idx, [x, y]) in iter_2d(size) {
+ flat[idx] = white_noise(seed, &[x, y]);
+ }
+ }
+ Dim::D3 => {
+ for (idx, [x, y, z]) in iter_3d(size) {
+ flat[idx] = white_noise(seed, &[x, y, z]);
+ }
+ }
+ Dim::D4 => {
+ for (idx, [x, y, z, w]) in iter_4d(size) {
+ flat[idx] = white_noise(seed, &[x, y, z, w]);
+ }
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn white_noise_is_deterministic_for_same_seed_and_coords() {
+ assert_eq!(white_noise(42, &[1, 2]), white_noise(42, &[1, 2]));
+ }
+
+ #[test]
+ fn white_noise_differs_across_coordinates() {
+ assert_ne!(white_noise(42, &[1, 2]), white_noise(42, &[1, 3]));
+ assert_ne!(white_noise(42, &[1, 2]), white_noise(42, &[2, 2]));
+ }
+
+ #[test]
+ fn white_noise_differs_across_seeds() {
+ assert_ne!(white_noise(1, &[1, 2]), white_noise(2, &[1, 2]));
+ }
+
+ #[test]
+ fn white_noise_is_bounded_to_unit_range() {
+ for x in 0..50 {
+ let v = white_noise(7, &[x, x * 3]);
+ assert!((-1.0..=1.0).contains(&v), "value {v} out of range");
+ }
+ }
+}
diff --git a/services/noise-generation-service/src/http.rs b/services/noise-generation-service/src/http.rs
new file mode 100644
index 0000000..b35c4e1
--- /dev/null
+++ b/services/noise-generation-service/src/http.rs
@@ -0,0 +1,78 @@
+//! Axum HTTP handlers. Both handlers are thin adapters over
+//! `crate::algorithms`/`crate::service`; no domain logic lives here.
+
+use axum::{http::StatusCode, Json};
+
+use crate::algorithms::{algorithm_defaults, ALGORITHM_NAMES};
+use crate::limits::DEFAULT_SAMPLING_SIZE;
+use crate::model::{AlgorithmInfo, GenerateNoiseRequest, NoiseFieldResult};
+use crate::service;
+
+#[utoipa::path(
+ get,
+ path = "/v1/algorithms",
+ tag = "noise",
+ responses(
+ (status = 200, description = "List of algorithms with their default parameters", body = Vec)
+ )
+)]
+pub async fn list_algorithms() -> Json> {
+ let entries: Vec = ALGORITHM_NAMES
+ .iter()
+ .map(|name| AlgorithmInfo {
+ name: name.to_string(),
+ defaults: algorithm_defaults(name),
+ })
+ .collect();
+ Json(entries)
+}
+
+#[utoipa::path(
+ post,
+ path = "/v1/noise",
+ tag = "noise",
+ request_body = GenerateNoiseRequest,
+ responses(
+ (status = 201, description = "Noise field created", body = NoiseFieldResult)
+ )
+)]
+pub async fn generate_noise(
+ Json(payload): Json,
+) -> (StatusCode, Json) {
+ let algorithm_name = payload.algorithm.name().to_string();
+ let field_id = format!("nsf_{}", uuid::Uuid::new_v4());
+ // Must match the default `crate::service::generate` uses for the success
+ // path, so a rejected request that omitted `sampling.size` echoes the
+ // same size the service would actually have used, rather than `[]`.
+ let size_for_error = payload
+ .sampling_size()
+ .unwrap_or_else(|| DEFAULT_SAMPLING_SIZE.to_vec());
+
+ // Run the CPU-bound validation/generation/shaping work on a blocking
+ // thread so a large request doesn't stall the async executor for other
+ // in-flight requests.
+ let result = tokio::task::spawn_blocking(move || service::generate(&payload))
+ .await
+ .expect("noise generation task panicked");
+
+ match result {
+ Ok(field) => (
+ StatusCode::CREATED,
+ Json(NoiseFieldResult {
+ id: field_id,
+ status: "completed".to_string(),
+ algorithm: algorithm_name,
+ data: field.data,
+ size: field.size,
+ params_used: field.params_used,
+ }),
+ ),
+ Err(err) => {
+ let status = err.status_code();
+ (
+ status,
+ Json(err.into_result(field_id, algorithm_name, size_for_error)),
+ )
+ }
+ }
+}
diff --git a/services/noise-generation-service/src/lib.rs b/services/noise-generation-service/src/lib.rs
new file mode 100644
index 0000000..d17f25d
--- /dev/null
+++ b/services/noise-generation-service/src/lib.rs
@@ -0,0 +1,173 @@
+//! Noise generation service library.
+//!
+//! Module layout:
+//! - [`algorithms`]: the single source of truth for the set of supported
+//! algorithms — wire names, the tagged [`AlgorithmParams`] enum, and
+//! per-algorithm dimension support.
+//! - [`model`]: request/response DTOs and per-algorithm parameter structs.
+//! - [`dim`]: the `Dim` enum and shared grid-iteration helpers.
+//! - [`resolve`]: turns optional request parameters into concrete resolved
+//! values, used for both generation and the `params_used` echo.
+//! - [`generate`]: the noise generation kernels (one per algorithm family).
+//! - [`shape`]: reshapes a flat generation buffer into nested response JSON.
+//! - [`error`]: structured request-validation errors.
+//! - [`service`]: the HTTP-free core `generate` function shared by the HTTP
+//! handler and the CLI.
+//! - [`http`]: thin axum handlers.
+//! - [`openapi`]: the `utoipa` `ApiDoc` schema registration.
+//! - [`cli`]: `clap` CLI argument definitions, shared by the binary and by
+//! `tests/cli_tests.rs` via this public module.
+
+// Range loops and casts are clearer for noise generation — allowed via the
+// `[lints.clippy]` table in Cargo.toml, which applies uniformly to the lib
+// and bin targets instead of a per-crate `#![allow(...)]`.
+
+pub mod algorithms;
+pub mod cli;
+pub mod dim;
+pub mod error;
+pub mod generate;
+pub mod http;
+pub mod limits;
+pub mod model;
+pub mod openapi;
+pub mod render;
+pub mod resolve;
+pub mod service;
+pub mod shape;
+
+// Re-export the most commonly used items at the crate root so callers
+// (the binary, integration tests) don't need to know the internal module
+// layout.
+pub use algorithms::{AlgorithmParams, ALGORITHM_NAMES};
+pub use error::NoiseError;
+pub use http::{generate_noise, list_algorithms};
+pub use model::{
+ AlgorithmInfo, CellularDistanceFunction, CellularParams, CellularReturnType, CombinatorOp,
+ CombinatorParams, DomainWarpParams, FractalParams, GenerateNoiseRequest, NoiseFieldResult,
+ Output, OutputFormat, PingPongParams, Sampling, SeedParams, UtilityKind, UtilityParams,
+};
+pub use openapi::ApiDoc;
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::collections::BTreeSet;
+
+ /// Verify that the JSON wire format matches the expected layout:
+ /// { "algorithm": "perlin", "params": {...}, "sampling": {...}, "output": {...} }
+ /// The order of keys in the serialized JSON is determined by serde's flatten
+ /// behavior — the only requirement is that the four keys exist with correct values.
+ #[test]
+ fn test_serialization_perlin() {
+ let req = GenerateNoiseRequest {
+ algorithm: AlgorithmParams::Perlin(SeedParams { seed: Some(42) }),
+ sampling: Sampling {
+ size: Some(vec![64, 64]),
+ },
+ output: Some(Output {
+ format: OutputFormat::Json,
+ normalize: false,
+ }),
+ };
+ let json = serde_json::to_value(&req).unwrap();
+ assert_eq!(json["algorithm"], "perlin");
+ assert_eq!(json["params"]["seed"], 42);
+ assert_eq!(json["sampling"]["size"], serde_json::json!([64, 64]));
+ assert_eq!(json["output"]["format"], "json");
+ assert_eq!(json["output"]["normalize"], false);
+ let keys: BTreeSet<&str> = json
+ .as_object()
+ .unwrap()
+ .keys()
+ .map(|k| k.as_str())
+ .collect();
+ let expected: BTreeSet<&str> = ["algorithm", "params", "sampling", "output"].into();
+ assert_eq!(keys, expected);
+ }
+
+ #[test]
+ fn test_serialization_fbm() {
+ let req = GenerateNoiseRequest {
+ algorithm: AlgorithmParams::Fbm(FractalParams {
+ seed: Some(42),
+ octaves: Some(4),
+ frequency: Some(0.1),
+ lacunarity: Some(2.0),
+ persistence: Some(0.5),
+ }),
+ sampling: Sampling {
+ size: Some(vec![32, 32]),
+ },
+ output: None,
+ };
+ let json = serde_json::to_value(&req).unwrap();
+ assert_eq!(json["algorithm"], "fbm");
+ assert_eq!(json["params"]["seed"], 42);
+ assert_eq!(json["params"]["octaves"], 4);
+ assert_eq!(json["sampling"]["size"], serde_json::json!([32, 32]));
+ assert!(json["output"].is_null());
+ }
+
+ #[test]
+ fn test_serialization_cellular() {
+ let req = GenerateNoiseRequest {
+ algorithm: AlgorithmParams::Cellular(CellularParams {
+ seed: Some(123),
+ distance_function: Some(CellularDistanceFunction::EuclideanSq),
+ return_type: Some(CellularReturnType::Distance2),
+ jitter: Some(0.6),
+ }),
+ sampling: Sampling {
+ size: Some(vec![16, 16]),
+ },
+ output: Some(Output {
+ format: OutputFormat::Csv,
+ normalize: true,
+ }),
+ };
+ let json = serde_json::to_value(&req).unwrap();
+ assert_eq!(json["algorithm"], "cellular");
+ assert_eq!(json["params"]["seed"], 123);
+ assert_eq!(json["params"]["distance_function"], "euclidean_sq");
+ assert_eq!(json["params"]["return_type"], "distance2");
+ assert_eq!(json["params"]["jitter"], 0.6);
+ assert_eq!(json["output"]["format"], "csv");
+ assert_eq!(json["output"]["normalize"], true);
+ }
+
+ #[test]
+ fn test_deserialization_roundtrip() {
+ let json_str = r#"{"algorithm":"perlin","params":{"seed":42},"sampling":{"size":[64,64]},"output":{"format":"json","normalize":false}}"#;
+ let req: GenerateNoiseRequest = serde_json::from_str(json_str).unwrap();
+ assert_eq!(req.algorithm.name(), "perlin");
+ assert_eq!(req.sampling_size(), Some(vec![64, 64]));
+ assert!(!req.should_normalize());
+ let json = serde_json::to_value(&req).unwrap();
+ assert_eq!(json["algorithm"], "perlin");
+ assert_eq!(json["params"]["seed"], 42);
+ }
+
+ #[test]
+ fn test_deserialization_fbm_roundtrip() {
+ let json_str = r#"{"algorithm":"fbm","params":{"seed":42,"octaves":4,"frequency":0.1,"lacunarity":2.0,"persistence":0.5},"sampling":{"size":[32,32]},"output":null}"#;
+ let req: GenerateNoiseRequest = serde_json::from_str(json_str).unwrap();
+ assert_eq!(req.algorithm.name(), "fbm");
+ assert_eq!(req.sampling_size(), Some(vec![32, 32]));
+ let json = serde_json::to_value(&req).unwrap();
+ assert_eq!(json["algorithm"], "fbm");
+ assert_eq!(json["params"]["seed"], 42);
+ }
+
+ #[test]
+ fn test_deserialization_cellular_roundtrip() {
+ let json_str = r#"{"algorithm":"cellular","params":{"seed":123,"distance_function":"euclidean_sq","return_type":"distance2","jitter":0.6},"sampling":{"size":[16,16]},"output":{"format":"csv","normalize":true}}"#;
+ let req: GenerateNoiseRequest = serde_json::from_str(json_str).unwrap();
+ assert_eq!(req.algorithm.name(), "cellular");
+ assert_eq!(req.sampling_size(), Some(vec![16, 16]));
+ assert!(req.should_normalize());
+ let json = serde_json::to_value(&req).unwrap();
+ assert_eq!(json["algorithm"], "cellular");
+ assert_eq!(json["params"]["distance_function"], "euclidean_sq");
+ }
+}
diff --git a/services/noise-generation-service/src/limits.rs b/services/noise-generation-service/src/limits.rs
new file mode 100644
index 0000000..1fb4ec1
--- /dev/null
+++ b/services/noise-generation-service/src/limits.rs
@@ -0,0 +1,16 @@
+//! Request-size limits shared by validation in `crate::service`.
+
+/// Maximum extent allowed for any single sampling dimension. Bounds per-request
+/// memory/CPU cost and prevents pathological requests from exhausting the host.
+pub const MAX_SAMPLING_DIM: usize = 4096;
+
+/// Maximum total number of cells (product of all dimensions) allowed per
+/// request. Chosen to keep the largest response well under typical memory
+/// limits (16M f64 cells ~ 128 MB flat buffer before JSON shaping).
+pub const MAX_SAMPLING_CELLS: usize = 16 * 1024 * 1024;
+
+/// Default grid size used when a request omits `sampling.size`. Large enough
+/// to show visible noise structures, small enough to keep response payload
+/// manageable (~32 KB for f64 values). Matches common examples in noise
+/// library documentation.
+pub const DEFAULT_SAMPLING_SIZE: [usize; 2] = [64, 64];
diff --git a/services/noise-generation-service/src/main.rs b/services/noise-generation-service/src/main.rs
new file mode 100644
index 0000000..828f539
--- /dev/null
+++ b/services/noise-generation-service/src/main.rs
@@ -0,0 +1,176 @@
+use axum::{
+ response::IntoResponse,
+ routing::{get, post},
+ Json, Router,
+};
+use clap::{CommandFactory, Parser};
+use noise_generation_service::algorithms::{algorithm_defaults, AlgorithmParams, ALGORITHM_NAMES};
+use noise_generation_service::cli::{Cli, Commands, Format, GenerateArgs, OpenApiFormat};
+use noise_generation_service::model::{
+ AlgorithmInfo, GenerateNoiseRequest, Output, OutputFormat, Sampling,
+};
+use noise_generation_service::{generate_noise, list_algorithms, render, service, ApiDoc};
+use std::path::PathBuf;
+use tokio::net::TcpListener;
+use utoipa::OpenApi;
+
+#[tokio::main]
+async fn main() {
+ let cli = Cli::parse();
+ let Some(command) = cli.command else {
+ // No subcommand — show help
+ Cli::command().print_help().unwrap();
+ println!();
+ std::process::exit(1);
+ };
+
+ match command {
+ Commands::List { format } => handle_list_command(format),
+ Commands::Generate(args) => handle_generate_command(args),
+ Commands::OpenApi { format, output } => handle_openapi_command(format, output),
+ Commands::Server { port, host } => {
+ let bind_addr = format!("{}:{}", host, port);
+ println!("Starting server on {}", bind_addr);
+ start_server(bind_addr).await;
+ }
+ }
+}
+
+// ─── Server ──────────────────────────────────────────────────────────────────
+
+async fn start_server(bind_addr: String) {
+ let app = Router::new()
+ .route("/v1/algorithms", get(list_algorithms))
+ .route("/v1/noise", post(generate_noise))
+ .route(
+ "/api-docs/openapi.json",
+ get(|| async { Json(ApiDoc::openapi()).into_response() }),
+ );
+
+ let listener = match TcpListener::bind(&bind_addr).await {
+ Ok(listener) => listener,
+ Err(err) => {
+ eprintln!("Error: could not bind to {bind_addr} ({err})");
+ std::process::exit(1);
+ }
+ };
+ println!("Listening on {}", bind_addr);
+ if let Err(err) = axum::serve(listener, app).await {
+ eprintln!("Error: server stopped unexpectedly ({err})");
+ std::process::exit(1);
+ }
+}
+
+// ─── CLI: list ───────────────────────────────────────────────────────────────
+
+fn handle_list_command(format: Format) {
+ let entries: Vec = ALGORITHM_NAMES
+ .iter()
+ .map(|name| AlgorithmInfo {
+ name: name.to_string(),
+ defaults: algorithm_defaults(name),
+ })
+ .collect();
+
+ match format {
+ Format::Csv => print!("{}", render::list_csv(&entries)),
+ Format::Json => {
+ let text = serde_json::to_string_pretty(&entries)
+ .unwrap_or_else(|e| unreachable!("AlgorithmInfo always serializes: {e}"));
+ println!("{text}");
+ }
+ }
+}
+
+// ─── CLI: generate (local) ───────────────────────────────────────────────────
+
+/// Builds an `AlgorithmParams` value from the CLI's `--algorithm` name and
+/// `--params` JSON by round-tripping through the same tag/content
+/// deserialization the HTTP API uses — no per-algorithm mapping match needed
+/// on the CLI side.
+fn build_algorithm(name: &str, params: serde_json::Value) -> AlgorithmParams {
+ let params = if params.is_null() || params.as_object().is_some_and(|o| o.is_empty()) {
+ serde_json::json!({})
+ } else {
+ params
+ };
+ serde_json::from_value(serde_json::json!({ "algorithm": name, "params": params }))
+ .unwrap_or_else(|err| {
+ eprintln!("Error: invalid params for algorithm '{name}' ({err})");
+ std::process::exit(1);
+ })
+}
+
+fn handle_generate_command(args: GenerateArgs) {
+ let size = args.sampling_size.clone();
+ let algorithm = build_algorithm(&args.algorithm, args.params);
+ let request = GenerateNoiseRequest {
+ algorithm,
+ sampling: Sampling {
+ size: Some(size.clone()),
+ },
+ output: Some(Output {
+ format: OutputFormat::Json,
+ normalize: args.output_normalize,
+ }),
+ };
+
+ let field = match service::generate(&request) {
+ Ok(field) => field,
+ Err(err) => {
+ eprintln!("Error: {}", err.message());
+ std::process::exit(1);
+ }
+ };
+
+ match args.output_format {
+ Format::Json => {
+ let output = serde_json::json!({
+ "status": 201,
+ "result": {
+ "id": format!("nsf_{}", uuid::Uuid::new_v4()),
+ "algorithm": args.algorithm,
+ "status": "completed",
+ "data": field.data,
+ "size": field.size,
+ "params_used": field.params_used,
+ }
+ });
+ let text = serde_json::to_string_pretty(&output).unwrap();
+ write_or_print(text, args.output_file);
+ }
+ Format::Csv => match render::generate_csv(&field.data, size.len()) {
+ Ok(csv) => write_or_print(csv, args.output_file),
+ Err(msg) => {
+ eprintln!("Error: {msg}");
+ std::process::exit(1);
+ }
+ },
+ }
+}
+
+// ─── CLI: openapi ────────────────────────────────────────────────────────────
+
+fn handle_openapi_command(format: OpenApiFormat, output: Option) {
+ let spec = ApiDoc::openapi();
+
+ let text = match format {
+ OpenApiFormat::Json => serde_json::to_string_pretty(&spec).unwrap(),
+ OpenApiFormat::Yaml => serde_yaml::to_string(&spec).unwrap(),
+ };
+
+ write_or_print(text, output);
+}
+
+// ─── Helper: write to file or stdout ─────────────────────────────────────────
+
+fn write_or_print(text: String, file: Option) {
+ if let Some(path) = file {
+ std::fs::write(&path, &text).unwrap_or_else(|e| {
+ eprintln!("Error: could not write to {} ({})", path.display(), e);
+ std::process::exit(1);
+ });
+ } else {
+ println!("{}", text);
+ }
+}
diff --git a/services/noise-generation-service/src/model.rs b/services/noise-generation-service/src/model.rs
new file mode 100644
index 0000000..1e75107
--- /dev/null
+++ b/services/noise-generation-service/src/model.rs
@@ -0,0 +1,166 @@
+//! Request/response DTOs and per-algorithm parameter structs.
+//!
+//! `AlgorithmParams`, its wire-name table, and its dimension-support table are
+//! defined in [`crate::algorithms`] via a single macro invocation instead of
+//! here, so that the set of supported algorithms has exactly one source of
+//! truth.
+
+use serde::{Deserialize, Serialize};
+use utoipa::ToSchema;
+
+#[derive(Serialize, Deserialize, Debug, Default, ToSchema)]
+pub struct SeedParams {
+ pub seed: Option,
+}
+
+#[derive(Serialize, Deserialize, Debug, Default, ToSchema)]
+pub struct CellularParams {
+ pub seed: Option,
+ pub distance_function: Option,
+ pub return_type: Option,
+ pub jitter: Option,
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone, ToSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum CellularDistanceFunction {
+ Euclidean,
+ EuclideanSq,
+ Manhattan,
+ Hybrid,
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone, ToSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum CellularReturnType {
+ CellValue,
+ Distance,
+ Distance2,
+ Distance2Add,
+ Distance2Sub,
+ Distance2Mul,
+ Distance2Div,
+}
+
+/// Shared parameter shape for every noise-crate fractal algorithm (Fbm,
+/// Billow, RidgedMulti, HybridMulti). Each algorithm applies its own default
+/// `persistence` (see `crate::algorithms::DEFAULT_PERSISTENCE_*`) when the
+/// field is omitted; the struct itself is identical across all four, so it is
+/// defined once rather than duplicated per algorithm.
+#[derive(Serialize, Deserialize, Debug, Default, ToSchema)]
+pub struct FractalParams {
+ pub seed: Option,
+ pub octaves: Option,
+ pub frequency: Option,
+ pub lacunarity: Option,
+ pub persistence: Option,
+}
+
+#[derive(Serialize, Deserialize, Debug, Default, ToSchema)]
+pub struct PingPongParams {
+ pub seed: Option,
+ pub strength: Option,
+}
+
+#[derive(Serialize, Deserialize, Debug, Default, ToSchema)]
+pub struct DomainWarpParams {
+ pub seed: Option,
+ pub amplitude: Option,
+}
+
+#[derive(Serialize, Deserialize, Debug, Default, ToSchema)]
+pub struct CombinatorParams {
+ pub seed: Option,
+ pub op: Option,
+ pub blend_factor: Option,
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone, Default, ToSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum CombinatorOp {
+ #[default]
+ Add,
+ Multiply,
+ Min,
+ Max,
+ Blend,
+}
+
+#[derive(Serialize, Deserialize, Debug, Default, ToSchema)]
+pub struct UtilityParams {
+ pub kind: Option,
+ pub value: Option,
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone, Default, ToSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum UtilityKind {
+ #[default]
+ Constant,
+ Cylinders,
+}
+
+#[derive(Serialize, Debug, ToSchema)]
+pub struct AlgorithmInfo {
+ /// Algorithm name / identifier (e.g. "perlin", "fbm")
+ pub name: String,
+ /// Default parameter values used when the corresponding field is omitted.
+ /// The `seed` is shown as `null` because it is randomly generated per request.
+ pub defaults: serde_json::Value,
+}
+
+#[derive(Serialize, Deserialize, Debug, ToSchema)]
+pub struct Sampling {
+ pub size: Option>,
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone, Default, ToSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum OutputFormat {
+ #[default]
+ Json,
+ Csv,
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone, ToSchema)]
+pub struct Output {
+ pub format: OutputFormat,
+ pub normalize: bool,
+}
+
+#[derive(Serialize, Deserialize, Debug, ToSchema)]
+pub struct GenerateNoiseRequest {
+ #[serde(flatten)]
+ pub algorithm: crate::algorithms::AlgorithmParams,
+ pub sampling: Sampling,
+ pub output: Option