diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index e57c4e3..f1e4706 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -28,6 +28,9 @@ jobs: with: go-version: '1.26' + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + - name: Install Odin ${{ env.ODIN_VERSION }} run: scripts/install_odin.sh @@ -61,3 +64,18 @@ jobs: run: | cd infra go vet ./... + + - name: Test Rust API + run: | + cd api + cargo test + + - name: Lint Rust API + run: | + cd api + cargo clippy --all-targets -- -D warnings + + - name: Format-check Rust API + run: | + cd api + cargo fmt --all --check diff --git a/.gitignore b/.gitignore index 2d8ed01..0753ab7 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ build/ resources/ai/sessions infra/Pulumi.*.yaml.backup infra/desi-explorer-infra +api/target/ diff --git a/AGENTS.md b/AGENTS.md index b388449..d2b56d9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,26 +1,28 @@ # AGENTS.md -Odin + raylib interactive 3D universe map (DESI data), with Go + Pulumi infra and Gitea Actions CI. A clean `odin build` + `go test` is the verification step. +Odin + raylib interactive 3D universe map (DESI data), a Rust + axum API, with Go + Pulumi infra and Gitea Actions CI. A clean `odin build` + `cargo test` + `go test` is the verification step. ## Commands (root Makefile) - Run: `make run` (`odin run src`); Build: `make build` (`odin build src -collection:lib=lib/local -out:bin/desi_explorer`) - Web: `make build-web` (Emscripten/WASM -> build/web); Debug: `make build-debug` -- Test: `make test` (runs `odin test` for src/ and `go test` for infra/); Clean: `make clean` +- Test: `make test` (runs `odin test` for src/, `go test` for infra/, and `cargo test` for api/); Clean: `make clean` +- API: `make api-run` (`cargo run` in api/), `make api-test`, `make api-check` (`cargo fmt --check` + `clippy -D warnings`) - Setup: `make setup` (submodules + `go mod tidy` in infra/) - Infra: `make infra-preview`, `make infra-up`, `make infra-down`, `make infra-refresh` (Pulumi, `--cwd infra`) - Renovate: `make renovate-validate` (npx renovate-config-validator) -- Format Odin with `odinfmt` (config `odinfmt.json`: tabs, width 80) +- Format Odin with `odinfmt` (config `odinfmt.json`: tabs, width 80); format Rust with `cargo fmt` ## Layout - `src/main.odin` — entrypoint; loop is `update()` → `draw()`; resizable window with an orbital `Camera3D`; point cloud generated procedurally in `make_universe`. - `lib/` — third-party Odin deps (submodules / vendored libs). raylib is **not** here: it ships vendored with Odin and is imported as `vendor:raylib`. `lib/local/` is the default `-collection:lib` target and is currently empty. +- `api/` — Rust + axum backend API that will serve DESI catalog data. `src/lib.rs` exposes `routes`/`models`/`config`; `src/main.rs` is the server entrypoint (binds `API_BIND_ADDR`, default `0.0.0.0:8080`). Crate name is `desi-explorer-api` (lib `desi_explorer_api`); Rust version pinned in `api/rust-toolchain.toml`. - `infra/` — Go + Pulumi (`runtime: go`) targeting the on-prem k3s cluster via the default kubeconfig, mirroring the `homelab` repo's pattern. Module name is `desi-explorer-infra`. -- `.gitea/workflows/` — `ci.yml` (PR: install Odin, `odin test` + build, `go test`) and `release.yml` (tag `v*`: build native + WASM + publish a Gitea release). +- `.gitea/workflows/` — `ci.yml` (PR: install Odin, Rust, `odin test` + build, `cargo test` + clippy, `go test`) and `release.yml` (tag `v*`: build native + WASM + publish a Gitea release). ## Conventions - Odin code lives in `src/`; external deps go in `lib/` and are wired via `-collection:lib=lib/local` (or a git submodule imported by relative path). - Everything is plain `make` — no Taskfile — so CI (Gitea Actions) can call `make` directly. -- Keep the renderer (src/) and infra (infra/) logically separated; the root Makefile is the only place that ties them together. +- Keep the renderer (src/), API (api/), and infra (infra/) logically separated; the root Makefile is the only place that ties them together. ## Gotchas - Odin version is pinned in `.gitea/workflows/*.yml` (`ODIN_VERSION`) and defaults in `scripts/install_odin.sh`; bump both together when tracking a new release. @@ -32,4 +34,4 @@ Odin + raylib interactive 3D universe map (DESI data), with Go + Pulumi infra an Export session information to `resources/ai/sessions` at the end of every session — for both plans and builds. (Gitignored.) ## Gitignored -`bin/`, `build/`, `*.bin`, `*.o`, `*.log`, `resources/ai/sessions`, `infra/Pulumi.*.yaml.backup`. +`bin/`, `build/`, `*.bin`, `*.o`, `*.log`, `resources/ai/sessions`, `infra/Pulumi.*.yaml.backup`, `api/target/`. diff --git a/Makefile b/Makefile index 9d139dc..ae8cca2 100644 --- a/Makefile +++ b/Makefile @@ -4,12 +4,14 @@ # (infra/). Intended to be called directly by Gitea Actions CI. ODIN ?= odin +CARGO ?= cargo BIN ?= bin BINARY := $(BIN)/desi_explorer ODIN_FLAGS := -collection:lib=lib/local WASM_DEFINE := RAYLIB_WASM_LIB=env.o .PHONY: help setup run build build-debug build-web test clean fmt \ + api-run api-build api-test api-check api-fmt \ infra-preview infra-up infra-down infra-refresh infra-static \ renovate-validate @@ -41,17 +43,36 @@ build-debug: ## Debug build -> bin/desi_explorer build-web: ## WebAssembly build -> build/web (needs emscripten) @scripts/build_web.sh -test: ## Run tests (Odin src/ + Go infra/) +test: ## Run tests (Odin src/ + Go infra/ + Rust api/) @mkdir -p lib/local $(ODIN) test src $(ODIN_FLAGS) @cd infra && go test ./... + @cd api && $(CARGO) test clean: ## Remove build artifacts rm -rf $(BIN) build + @cd api && $(CARGO) clean fmt: ## Format Odin sources with odinfmt odinfmt src +## ---- API (Rust + axum) --------------------------------------------------- + +api-run: ## Run the API server (dev) + @cd api && $(CARGO) run + +api-build: ## Release build the API -> api/target/release/ + @cd api && $(CARGO) build --release + +api-test: ## Run API unit/integration tests + @cd api && $(CARGO) test + +api-check: ## Typecheck + format-check + lint the API + @cd api && $(CARGO) fmt --all --check && $(CARGO) clippy --all-targets -- -D warnings + +api-fmt: ## Format Rust sources with rustfmt + @cd api && $(CARGO) fmt + ## ---- Infrastructure (Go + Pulumi) ---------------------------------------- infra-preview: ## Preview infra changes against the cluster @@ -72,4 +93,4 @@ infra-static: ## Typecheck + vet the Go infra without Pulumi ## ---- Tooling ------------------------------------------------------------- renovate-validate: ## Validate renovate.json - npx --yes renovate-config-validator + bunx --package renovate renovate-config-validator renovate.json --strict diff --git a/README.md b/README.md index 91c01b3..f9b07ad 100644 --- a/README.md +++ b/README.md @@ -44,21 +44,23 @@ Early scaffolding. The application currently: ``` src/ Odin source — the interactive 3D experience (entrypoint: src/main.odin) lib/ Third-party Odin dependencies (submodules / vendored libs) +api/ Rust backend API (axum) for serving DESI catalog data infra/ Go + Pulumi infrastructure-as-code (deploys the experience to k8s) scripts/ Build helpers (Odin install, WASM build) .gitea/ Gitea Actions CI/CD workflows ``` -The renderer (`src/`) and the infrastructure (`infra/`) are kept in separate -directories so their logic stays cleanly separated; both are orchestrated by the -root `Makefile`. +The renderer (`src/`), the API (`api/`), and the infrastructure (`infra/`) are +kept in separate directories so their logic stays cleanly separated; all are +orchestrated by the root `Makefile`. ## Building & Running Requires [Odin](https://odin-lang.org/docs/install) with the bundled raylib vendor bindings. The web build additionally requires [Emscripten](https://emscripten.org) -(`emcc`). The infrastructure requires [Go](https://go.dev) and -[Pulumi](https://www.pulumi.com). +(`emcc`). The API requires [Rust](https://www.rust-lang.org) (stable toolchain, +pinned via `api/rust-toolchain.toml`). The infrastructure requires +[Go](https://go.dev) and [Pulumi](https://www.pulumi.com). All project commands live in the root `Makefile` (`make help` lists them): @@ -68,8 +70,11 @@ make run # run the native app make build # -> bin/desi_explorer make build-debug # debug native build make build-web # -> build/web (wasm + html) -make test # odin test + go test +make test # odin test + go test + cargo test make clean # remove build artifacts +make api-run # run the Rust API server +make api-test # cargo test +make api-check # rustfmt check + clippy ``` ## Dependency Updates (Renovate) @@ -82,8 +87,9 @@ hour (`renovate` workflow in the `homelab` repo) with autodiscovery filtered to picked up automatically. Config for this repo lives in `renovate.json` and is validated with `make renovate-validate`. -It manages three categories: +It manages four categories: - **Infra dependencies** — `infra/go.mod` (`gomod` manager); minor/patch updates grouped into a single PR. +- **API dependencies** — `api/Cargo.toml` (`cargo` manager); minor/patch updates grouped into a single PR. - **Submodules** — `lib/` submodules via `.gitmodules` (`git-submodules` manager). - **Workflow actions** — action versions in `.gitea/workflows/*.yml` (`github-actions` manager, which understands Gitea's `.gitea` layout). diff --git a/api/.gitignore b/api/.gitignore new file mode 100644 index 0000000..f4537f5 --- /dev/null +++ b/api/.gitignore @@ -0,0 +1,2 @@ +# Cargo build artifacts +target/ diff --git a/api/Cargo.lock b/api/Cargo.lock new file mode 100644 index 0000000..db8074b --- /dev/null +++ b/api/Cargo.lock @@ -0,0 +1,686 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "desi-explorer-api" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum", + "serde", + "serde_json", + "tokio", + "tower", + "tower-http", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[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.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "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 = "hyper" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[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 = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +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 = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +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 = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[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.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys", +] + +[[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 = "syn" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[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", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "http", + "http-body", + "pin-project-lite", + "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-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[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.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/api/Cargo.toml b/api/Cargo.toml new file mode 100644 index 0000000..0a543ac --- /dev/null +++ b/api/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "desi-explorer-api" +version = "0.1.0" +edition = "2021" +description = "Backend API for DESI Explorer — serves DESI survey catalog data" + +[dependencies] +axum = "0.8" +tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal"] } +serde = { version = "1", features = ["derive"] } +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +tower-http = { version = "0.6", features = ["cors", "trace"] } +anyhow = "1" + +[dev-dependencies] +tower = { version = "0.5", features = ["util"] } +serde_json = "1" + +[profile.release] +lto = true +strip = true diff --git a/api/rust-toolchain.toml b/api/rust-toolchain.toml new file mode 100644 index 0000000..292fe49 --- /dev/null +++ b/api/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "stable" diff --git a/api/src/config.rs b/api/src/config.rs new file mode 100644 index 0000000..e168014 --- /dev/null +++ b/api/src/config.rs @@ -0,0 +1,12 @@ +pub struct Config { + pub bind_addr: String, +} + +impl Config { + pub fn from_env() -> anyhow::Result { + let bind_addr = + std::env::var("API_BIND_ADDR").unwrap_or_else(|_| "0.0.0.0:8080".to_string()); + + Ok(Self { bind_addr }) + } +} diff --git a/api/src/lib.rs b/api/src/lib.rs new file mode 100644 index 0000000..b77fb25 --- /dev/null +++ b/api/src/lib.rs @@ -0,0 +1,3 @@ +pub mod config; +pub mod models; +pub mod routes; diff --git a/api/src/main.rs b/api/src/main.rs new file mode 100644 index 0000000..fef0a0c --- /dev/null +++ b/api/src/main.rs @@ -0,0 +1,30 @@ +use desi_explorer_api::{config, routes}; + +use tracing_subscriber::EnvFilter; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + tracing_subscriber::fmt() + .with_env_filter( + EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new("desi_explorer_api=info")), + ) + .init(); + + let config = config::Config::from_env()?; + let app = routes::app(); + + let listener = tokio::net::TcpListener::bind(&config.bind_addr).await?; + tracing::info!("DESI Explorer API listening on {}", config.bind_addr); + + axum::serve(listener, app) + .with_graceful_shutdown(shutdown_signal()) + .await?; + + Ok(()) +} + +async fn shutdown_signal() { + let _ = tokio::signal::ctrl_c().await; + tracing::info!("shutting down"); +} diff --git a/api/src/models.rs b/api/src/models.rs new file mode 100644 index 0000000..0090bcd --- /dev/null +++ b/api/src/models.rs @@ -0,0 +1,22 @@ +use serde::Serialize; + +/// Catalog metadata for a DESI data release/survey. +#[derive(Debug, Clone, Serialize)] +pub struct Catalog { + pub name: String, + pub release: String, + pub description: &'static str, + pub object_count: Option, +} + +/// A single catalog object (galaxy / quasar / star) with its survey +/// coordinates. `ra` and `dec` are in degrees; `redshift` is dimensionless. +#[derive(Debug, Serialize)] +pub struct CatalogObject { + pub id: String, + pub catalog: String, + pub object_type: String, + pub ra: f64, + pub dec: f64, + pub redshift: f64, +} diff --git a/api/src/routes/catalogs.rs b/api/src/routes/catalogs.rs new file mode 100644 index 0000000..5e5d220 --- /dev/null +++ b/api/src/routes/catalogs.rs @@ -0,0 +1,54 @@ +use axum::{ + extract::Query, + http::StatusCode, + response::{IntoResponse, Response}, + Json, +}; +use serde::Deserialize; +use std::sync::LazyLock; + +use crate::models::{Catalog, CatalogObject}; + +/// Placeholder catalogs until real DESI EDR/DR1 ingestion lands. +static CATALOGS: LazyLock> = LazyLock::new(|| { + vec![ + Catalog { + name: "edr".to_string(), + release: "EDR".to_string(), + description: "DESI Early Data Release", + object_count: None, + }, + Catalog { + name: "dr1".to_string(), + release: "DR1".to_string(), + description: "DESI Data Release 1", + object_count: None, + }, + ] +}); + +pub async fn list_catalogs() -> Json> { + Json(CATALOGS.clone()) +} + +#[derive(Debug, Deserialize)] +pub struct ObjectQuery { + catalog: Option, + #[serde(default)] + limit: Option, +} + +/// Placeholder object query. Real implementation will page through the +/// centralized DESI catalog store rather than return an empty result set. +pub async fn list_objects(Query(query): Query) -> Response { + let limit = query.limit.unwrap_or(100).min(10_000); + + tracing::debug!( + %limit, + catalog = query.catalog.as_deref().unwrap_or("all"), + "querying catalog objects (placeholder)" + ); + + let objects: Vec = Vec::new(); + (StatusCode::OK, Json(objects)).into_response() +} diff --git a/api/src/routes/health.rs b/api/src/routes/health.rs new file mode 100644 index 0000000..8d49bb8 --- /dev/null +++ b/api/src/routes/health.rs @@ -0,0 +1,11 @@ +use axum::Json; +use serde::Serialize; + +#[derive(Serialize)] +pub struct Health { + pub status: &'static str, +} + +pub async fn health() -> Json { + Json(Health { status: "ok" }) +} diff --git a/api/src/routes/mod.rs b/api/src/routes/mod.rs new file mode 100644 index 0000000..2a7119f --- /dev/null +++ b/api/src/routes/mod.rs @@ -0,0 +1,13 @@ +pub mod catalogs; +pub mod health; + +use axum::{routing::get, Router}; + +/// Builds the application router. Kept separate from `main` so tests can +/// construct it without binding a socket. +pub fn app() -> Router { + Router::new() + .route("/health", get(health::health)) + .route("/api/v1/catalogs", get(catalogs::list_catalogs)) + .route("/api/v1/objects", get(catalogs::list_objects)) +} diff --git a/api/tests/health.rs b/api/tests/health.rs new file mode 100644 index 0000000..fa91a6a --- /dev/null +++ b/api/tests/health.rs @@ -0,0 +1,44 @@ +use axum::body::{to_bytes, Body}; +use axum::http::{Request, StatusCode}; +use tower::ServiceExt; + +use desi_explorer_api::routes; + +#[tokio::test] +async fn health_returns_ok() { + let app = routes::app(); + + let response = app + .oneshot( + Request::builder() + .uri("/health") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); +} + +#[tokio::test] +async fn catalogs_is_nonempty() { + let app = routes::app(); + + let response = app + .oneshot( + Request::builder() + .uri("/api/v1/catalogs") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + + let body = to_bytes(response.into_body(), usize::MAX).await.unwrap(); + let json: serde_json::Value = serde_json::from_slice(&body).unwrap(); + let catalogs = json.as_array().unwrap(); + assert!(!catalogs.is_empty()); +} diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..f7cfde8 --- /dev/null +++ b/bun.lock @@ -0,0 +1,7 @@ +{ + "lockfileVersion": 2, + "configVersion": 1, + "packages": { + "renovate-config-validator": ["renovate-config-validator@0.0.1-placeholder", "", {}, "sha512-m4CJJH+haatkFvNgMavpM8DrpppPhV8uGm/b//d0LdKVe7D+6aeYL1VLdy04Fm0kOBcVs71Rv5fQVDh2m4LJeA=="], + } +} diff --git a/renovate.json b/renovate.json index 44ca450..bd26775 100644 --- a/renovate.json +++ b/renovate.json @@ -2,7 +2,7 @@ "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": ["config:recommended"], "gitAuthor": "Renovate Bot ", - "enabledManagers": ["gomod", "git-submodules", "github-actions"], + "enabledManagers": ["gomod", "cargo", "git-submodules", "github-actions"], "git-submodules": { "enabled": true }, @@ -13,6 +13,12 @@ "matchManagers": ["gomod"], "matchUpdateTypes": ["minor", "patch"], "groupName": "infra go dependencies" + }, + { + "description": "Group non-breaking API (Rust) updates into one PR", + "matchManagers": ["cargo"], + "matchUpdateTypes": ["minor", "patch"], + "groupName": "api rust dependencies" } ] }