Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cc474b1ab1 | |||
| c6bfb966b7 | |||
| de842fdc2d | |||
| 39cb53ea8a | |||
| 3a037b5fd3 | |||
| fade4a17dc | |||
| 1443369f6c | |||
| ba24005bfb | |||
| ac2d30510c | |||
| 3803787fe8 | |||
| 8c74efcedb | |||
| 89a41262af | |||
| 772ee0a106 | |||
| e034565ea2 | |||
| a6cbc49401 |
@@ -66,6 +66,15 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
|
# Cache the extracted Odin install so only the first run downloads the
|
||||||
|
# ~60 MB tarball; keyed on the pinned version (cache paths live outside
|
||||||
|
# the checkout, so no sharing between jobs/refs).
|
||||||
|
- name: Cache Odin
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: /tmp/odin
|
||||||
|
key: odin-${{ env.ODIN_VERSION }}-${{ runner.arch }}
|
||||||
|
|
||||||
- name: Install Odin ${{ env.ODIN_VERSION }}
|
- name: Install Odin ${{ env.ODIN_VERSION }}
|
||||||
run: scripts/install_odin.sh
|
run: scripts/install_odin.sh
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,12 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Cache Odin
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: /tmp/odin
|
||||||
|
key: odin-${{ env.ODIN_VERSION }}-${{ runner.arch }}
|
||||||
|
|
||||||
- name: Install Odin ${{ env.ODIN_VERSION }}
|
- name: Install Odin ${{ env.ODIN_VERSION }}
|
||||||
run: scripts/install_odin.sh
|
run: scripts/install_odin.sh
|
||||||
|
|
||||||
@@ -76,6 +82,12 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Cache Odin
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: /tmp/odin
|
||||||
|
key: odin-${{ env.ODIN_VERSION }}-${{ runner.arch }}
|
||||||
|
|
||||||
- name: Install Odin ${{ env.ODIN_VERSION }}
|
- name: Install Odin ${{ env.ODIN_VERSION }}
|
||||||
run: scripts/install_odin.sh
|
run: scripts/install_odin.sh
|
||||||
|
|
||||||
@@ -139,6 +151,12 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Cache Odin
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: /tmp/odin
|
||||||
|
key: odin-${{ env.ODIN_VERSION }}-${{ runner.arch }}
|
||||||
|
|
||||||
- name: Install Odin ${{ env.ODIN_VERSION }}
|
- name: Install Odin ${{ env.ODIN_VERSION }}
|
||||||
run: scripts/install_odin.sh
|
run: scripts/install_odin.sh
|
||||||
|
|
||||||
|
|||||||
@@ -7,3 +7,6 @@ resources/ai/sessions
|
|||||||
infra/Pulumi.*.yaml.backup
|
infra/Pulumi.*.yaml.backup
|
||||||
infra/desi-explorer-infra
|
infra/desi-explorer-infra
|
||||||
api/target/
|
api/target/
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
*.env
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ The root `Makefile` is a lean delegator: base commands (`run`, `build`, `test`,
|
|||||||
- Odin code lives in `gui/src/`; external deps go in `gui/lib/` and are wired via `-collection:lib=lib/local` (or a git submodule imported by relative path).
|
- Odin code lives in `gui/src/`; external deps go in `gui/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.
|
- Everything is plain `make` — no Taskfile — so CI (Gitea Actions) can call `make` directly.
|
||||||
- Keep the renderer (gui/), API (api/), and infra (infra/) logically separated; each owns its own Makefile, and the root Makefile is the only place that ties them together.
|
- Keep the renderer (gui/), API (api/), and infra (infra/) logically separated; each owns its own Makefile, and the root Makefile is the only place that ties them together.
|
||||||
|
- **Diagrams in this repo's documentation are Mermaid flowcharts.** Gitea renders ` ```mermaid ` fenced blocks natively. Prefer a Mermaid flowchart over ASCII art / box-drawing diagrams; if a diagram genuinely can't be expressed as a flowchart, fall back to a plain text-based markdown diagram (e.g. a code block or table) rather than hand-rawn ASCII boxes.
|
||||||
|
|
||||||
## Gotchas
|
## 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.
|
- 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.
|
||||||
|
|||||||
@@ -9,8 +9,36 @@ GUI := gui
|
|||||||
API := api
|
API := api
|
||||||
INFRA := infra
|
INFRA := infra
|
||||||
|
|
||||||
|
# Local dev/test assets live under resources/dev. These are the defaults for
|
||||||
|
# the run/*-web targets; override any of them on the command line, e.g.
|
||||||
|
# make run GUI_ENV_FILE=/path/to/gui.env API_DESI_DATA=/path/to/data.json
|
||||||
|
RESOURCE_DIR := $(CURDIR)/resources/dev
|
||||||
|
GUI_ENV_FILE ?= $(RESOURCE_DIR)/gui.env.example
|
||||||
|
API_ENV_FILE ?= $(RESOURCE_DIR)/api.env.example
|
||||||
|
API_DESI_DATA ?= $(RESOURCE_DIR)/desi_subset.json
|
||||||
|
# The API binds here for local dev (see api/src/config.rs); `make run` waits
|
||||||
|
# for $(API_HEALTH_URL) to respond before launching the GUI, so the renderer
|
||||||
|
# never races the API on startup. A bind host of 0.0.0.0 is probed via
|
||||||
|
# 127.0.0.1. Adjust API_WAIT_TIMEOUT (seconds) if the API takes longer than
|
||||||
|
# 60s to become healthy on a given machine.
|
||||||
|
#
|
||||||
|
# API_BIND_ADDR is only forwarded to the API when the user sets it explicitly
|
||||||
|
# (command line / environment). When it is left unset, the API reads it from
|
||||||
|
# $(API_ENV_FILE) (dotenvy applies env-file values that aren't already in the
|
||||||
|
# process environment), so the Makefile must not inject its own default here
|
||||||
|
# or it would shadow the env file. The effective bind address is read back
|
||||||
|
# from $(API_ENV_FILE) so the health check always polls the right port.
|
||||||
|
API_BIND_ADDR ?= 127.0.0.1:8080
|
||||||
|
API_BIND_EXPLICIT := $(filter command line environment,$(origin API_BIND_ADDR))
|
||||||
|
API_BIND_FROM_FILE := $(if $(API_BIND_EXPLICIT),,$(shell \
|
||||||
|
grep -E '^[[:space:]]*API_BIND_ADDR[[:space:]]*=' '$(API_ENV_FILE)' 2>/dev/null | \
|
||||||
|
tail -n 1 | sed 's/^[[:space:]]*API_BIND_ADDR[[:space:]]*=[[:space:]]*//; s/[[:space:]]*#.*$$//'))
|
||||||
|
API_BIND_EFFECTIVE := $(if $(API_BIND_EXPLICIT),$(API_BIND_ADDR),$(or $(API_BIND_FROM_FILE),$(API_BIND_ADDR)))
|
||||||
|
API_HEALTH_URL := http://$(subst 0.0.0.0,127.0.0.1,$(API_BIND_EFFECTIVE))/health
|
||||||
|
API_WAIT_TIMEOUT ?= 60
|
||||||
|
|
||||||
.PHONY: help setup run run-web build build-debug build-web test clean fmt \
|
.PHONY: help setup run run-web build build-debug build-web test clean fmt \
|
||||||
renovate-validate
|
api-wait renovate-validate
|
||||||
|
|
||||||
help: ## List available targets
|
help: ## List available targets
|
||||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
|
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
|
||||||
@@ -31,10 +59,11 @@ setup: ## Setup all sub-projects (submodules, gui deps, api deps, infra deps)
|
|||||||
|
|
||||||
## ---- Renderer (Odin) -----------------------------------------------------
|
## ---- Renderer (Odin) -----------------------------------------------------
|
||||||
|
|
||||||
run: ## Run the native app (gui/)
|
run: ## Run the native app (gui/); waits for the API to be healthy first
|
||||||
@$(MAKE) -C $(API) run & api_pid=$$!; \
|
@API_ENV_FILE="$(API_ENV_FILE)" API_DESI_DATA="$(API_DESI_DATA)" $(if $(API_BIND_EXPLICIT),API_BIND_ADDR="$(API_BIND_ADDR)",) $(MAKE) -C $(API) run & api_pid=$$!; \
|
||||||
trap 'kill $$api_pid 2>/dev/null' INT TERM EXIT; \
|
trap 'kill $$api_pid 2>/dev/null' INT TERM EXIT; \
|
||||||
$(MAKE) -C $(GUI) run; \
|
$(MAKE) api-wait || { kill $$api_pid 2>/dev/null; exit 1; }; \
|
||||||
|
GUI_ENV_FILE="$(GUI_ENV_FILE)" $(MAKE) -C $(GUI) run; \
|
||||||
kill $$api_pid 2>/dev/null
|
kill $$api_pid 2>/dev/null
|
||||||
|
|
||||||
build: ## Release build (gui/ + api/)
|
build: ## Release build (gui/ + api/)
|
||||||
@@ -48,12 +77,28 @@ build-debug: ## Debug build (gui/ + api/)
|
|||||||
build-web: ## WebAssembly build -> build/web (gui/, needs emscripten)
|
build-web: ## WebAssembly build -> build/web (gui/, needs emscripten)
|
||||||
@$(MAKE) -C $(GUI) build-web
|
@$(MAKE) -C $(GUI) build-web
|
||||||
|
|
||||||
run-web: ## Start WASM build + API server for web dev
|
run-web: ## Start WASM build + API server for web dev (waits for API health)
|
||||||
@$(MAKE) -C $(API) run & api_pid=$$!; \
|
@API_ENV_FILE="$(API_ENV_FILE)" API_DESI_DATA="$(API_DESI_DATA)" $(if $(API_BIND_EXPLICIT),API_BIND_ADDR="$(API_BIND_ADDR)",) $(MAKE) -C $(API) run & api_pid=$$!; \
|
||||||
trap 'kill $$api_pid 2>/dev/null' INT TERM EXIT; \
|
trap 'kill $$api_pid 2>/dev/null' INT TERM EXIT; \
|
||||||
$(MAKE) -C $(GUI) build-web; \
|
$(MAKE) api-wait || { kill $$api_pid 2>/dev/null; exit 1; }; \
|
||||||
|
GUI_ENV_FILE="$(GUI_ENV_FILE)" $(MAKE) -C $(GUI) build-web; \
|
||||||
kill $$api_pid 2>/dev/null
|
kill $$api_pid 2>/dev/null
|
||||||
|
|
||||||
|
## ---- Dev helpers ----------------------------------------------------------
|
||||||
|
|
||||||
|
api-wait: ## Poll the API health endpoint until it responds (or times out)
|
||||||
|
@echo "Waiting for API at $(API_HEALTH_URL) ..."; \
|
||||||
|
elapsed=0; \
|
||||||
|
while ! curl -sf "$(API_HEALTH_URL)" >/dev/null 2>&1; do \
|
||||||
|
elapsed=$$((elapsed + 1)); \
|
||||||
|
if [ "$${elapsed}" -ge "$(API_WAIT_TIMEOUT)" ]; then \
|
||||||
|
echo "Error: API at $(API_HEALTH_URL) not healthy after $(API_WAIT_TIMEOUT)s" >&2; \
|
||||||
|
exit 1; \
|
||||||
|
fi; \
|
||||||
|
sleep 1; \
|
||||||
|
done; \
|
||||||
|
echo "API is up."
|
||||||
|
|
||||||
## ---- Aggregates ----------------------------------------------------------
|
## ---- Aggregates ----------------------------------------------------------
|
||||||
|
|
||||||
test: ## Test all projects (odin + cargo + go)
|
test: ## Test all projects (odin + cargo + go)
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ A fun personal project with three interlocking goals:
|
|||||||
Early scaffolding. The application currently:
|
Early scaffolding. The application currently:
|
||||||
|
|
||||||
- Opens a resizable 3D raylib window with an orbital camera (zoom + rotate + pan).
|
- Opens a resizable 3D raylib window with an orbital camera (zoom + rotate + pan).
|
||||||
|
- Starts on a launch screen where you configure the API URL, pick a region, hit **Refresh catalogs** to pull the catalog list, and press **Explore** to enter the 3D view (TAB returns to the launch screen).
|
||||||
- Renders a procedurally generated point cloud standing in for the galaxy catalog (real DESI data ingestion is the next milestone).
|
- Renders a procedurally generated point cloud standing in for the galaxy catalog (real DESI data ingestion is the next milestone).
|
||||||
- Compiles natively, to WebAssembly, and is deployed to an on-prem Kubernetes cluster as a placeholder web service.
|
- Compiles natively, to WebAssembly, and is deployed to an on-prem Kubernetes cluster as a placeholder web service.
|
||||||
|
|
||||||
@@ -79,6 +80,30 @@ make clean # remove build artifacts from all projects
|
|||||||
make fmt # format all projects
|
make fmt # format all projects
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`make run` / `make run-web` configure the API and renderer from the example
|
||||||
|
assets under `resources/dev/` (see the dotenv lib in `gui/lib/local/dotenv`).
|
||||||
|
Override any of them on the command line:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make run \
|
||||||
|
GUI_ENV_FILE=/path/to/gui.env \
|
||||||
|
API_ENV_FILE=/path/to/api.env \
|
||||||
|
API_DESI_DATA=/path/to/desi_data.json
|
||||||
|
```
|
||||||
|
|
||||||
|
The defaults point at `resources/dev/gui.env.example` (renderer's `API_URL`),
|
||||||
|
`resources/dev/api.env.example` (API `API_BIND_ADDR`), and
|
||||||
|
`resources/dev/desi_subset.json` (a small JSON catalog subset served by the
|
||||||
|
API's `/api/v1/catalogs` and `/api/v1/objects` endpoints).
|
||||||
|
|
||||||
|
`make run` and `make run-web` start the API in the background and then wait for
|
||||||
|
its `/health` endpoint before launching the GUI (`make api-wait`), so the
|
||||||
|
renderer never races the API on startup. Override the bind address and worst
|
||||||
|
case wait with `API_BIND_ADDR` (default `127.0.0.1:8080`) and
|
||||||
|
`API_WAIT_TIMEOUT` (default 60s). The earlier you click **Refresh** on the GUI's
|
||||||
|
launch screen, the more likely you are to catch the API mid-boot; a slow API can
|
||||||
|
also be re-polled by pressing **Refresh** again.
|
||||||
|
|
||||||
Project-specific targets live in their own `Makefile` and are reached with
|
Project-specific targets live in their own `Makefile` and are reached with
|
||||||
`make -C <dir> <target>`:
|
`make -C <dir> <target>`:
|
||||||
|
|
||||||
|
|||||||
Generated
+10
-2
@@ -99,6 +99,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"axum",
|
"axum",
|
||||||
|
"dotenvy",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -108,6 +109,12 @@ dependencies = [
|
|||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dotenvy"
|
||||||
|
version = "0.15.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "errno"
|
name = "errno"
|
||||||
version = "0.3.14"
|
version = "0.3.14"
|
||||||
@@ -558,14 +565,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tower-http"
|
name = "tower-http"
|
||||||
version = "0.6.11"
|
version = "0.7.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
|
checksum = "08a05a66a4fdd61cbbe0a1d755ffe0ca6aba159dd4820936a0ff8a8278245b9c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"bytes",
|
"bytes",
|
||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
|
|||||||
+5
-4
@@ -5,17 +5,18 @@ edition = "2021"
|
|||||||
description = "Backend API for DESI Explorer — serves DESI survey catalog data"
|
description = "Backend API for DESI Explorer — serves DESI survey catalog data"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
anyhow = "1"
|
||||||
axum = "0.8"
|
axum = "0.8"
|
||||||
tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal"] }
|
dotenvy = "0.15"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal"] }
|
||||||
|
tower-http = { version = "0.7", features = ["cors", "trace"] }
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
tower-http = { version = "0.6", features = ["cors", "trace"] }
|
|
||||||
anyhow = "1"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tower = { version = "0.5", features = ["util"] }
|
tower = { version = "0.5", features = ["util"] }
|
||||||
serde_json = "1"
|
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = true
|
lto = true
|
||||||
|
|||||||
@@ -4,6 +4,20 @@
|
|||||||
# `api-*` convenience targets).
|
# `api-*` convenience targets).
|
||||||
|
|
||||||
CARGO ?= cargo
|
CARGO ?= cargo
|
||||||
|
ROOT := ..
|
||||||
|
|
||||||
|
# Normalize API_ENV_FILE / API_DESI_DATA (given relative to the repo root) to
|
||||||
|
# absolute paths so the API process can open them regardless of its working
|
||||||
|
# directory. "override" is required because they are usually passed as
|
||||||
|
# command-line/env vars, which would otherwise override any assignment here.
|
||||||
|
define normalize_path
|
||||||
|
ifdef $1
|
||||||
|
ifneq ($(abspath $($1)),$($1))
|
||||||
|
override $1 := $(abspath $(ROOT)/$($1))
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
endef
|
||||||
|
$(foreach v,API_ENV_FILE API_DESI_DATA,$(eval $(call normalize_path,$v)))
|
||||||
|
|
||||||
.PHONY: help setup run build test check fmt clean
|
.PHONY: help setup run build test check fmt clean
|
||||||
|
|
||||||
|
|||||||
+14
-1
@@ -1,5 +1,10 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub bind_addr: String,
|
pub bind_addr: String,
|
||||||
|
/// Path to a DESI data file (JSON) to serve; `None` falls back to the
|
||||||
|
/// built-in placeholder catalogs.
|
||||||
|
pub desi_data: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
@@ -7,6 +12,14 @@ impl Config {
|
|||||||
let bind_addr =
|
let bind_addr =
|
||||||
std::env::var("API_BIND_ADDR").unwrap_or_else(|_| "0.0.0.0:8080".to_string());
|
std::env::var("API_BIND_ADDR").unwrap_or_else(|_| "0.0.0.0:8080".to_string());
|
||||||
|
|
||||||
Ok(Self { bind_addr })
|
let desi_data = std::env::var("API_DESI_DATA")
|
||||||
|
.ok()
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.map(PathBuf::from);
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
bind_addr,
|
||||||
|
desi_data,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod models;
|
pub mod models;
|
||||||
pub mod routes;
|
pub mod routes;
|
||||||
|
pub mod store;
|
||||||
|
|||||||
+40
-2
@@ -1,4 +1,7 @@
|
|||||||
use desi_explorer_api::{config, routes};
|
use std::path::Path;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use desi_explorer_api::{config, routes, store};
|
||||||
|
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
@@ -11,8 +14,22 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
)
|
)
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
|
load_env_file()?;
|
||||||
|
|
||||||
let config = config::Config::from_env()?;
|
let config = config::Config::from_env()?;
|
||||||
let app = routes::app();
|
|
||||||
|
let catalog_store = match &config.desi_data {
|
||||||
|
Some(path) => {
|
||||||
|
tracing::info!(path = %path.display(), "loading DESI data");
|
||||||
|
store::CatalogStore::load(Path::new(path))?
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
tracing::warn!("API_DESI_DATA not set, serving placeholder catalogs");
|
||||||
|
store::CatalogStore::placeholder()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let app = routes::app_with_state(Arc::new(catalog_store));
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind(&config.bind_addr).await?;
|
let listener = tokio::net::TcpListener::bind(&config.bind_addr).await?;
|
||||||
tracing::info!("DESI Explorer API listening on {}", config.bind_addr);
|
tracing::info!("DESI Explorer API listening on {}", config.bind_addr);
|
||||||
@@ -24,6 +41,27 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Loads the env file named by `API_ENV_FILE` (if set) into the process
|
||||||
|
/// environment. Existing env vars are not overridden, so values passed
|
||||||
|
/// directly on the command line or by the Makefile take precedence.
|
||||||
|
fn load_env_file() -> anyhow::Result<()> {
|
||||||
|
let path = std::env::var("API_ENV_FILE").unwrap_or_default();
|
||||||
|
if path.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
match dotenvy::from_path(&path) {
|
||||||
|
Ok(_) => tracing::info!(%path, "loaded env file"),
|
||||||
|
Err(err) => {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"failed to load API_ENV_FILE {path:?}: {err}"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn shutdown_signal() {
|
async fn shutdown_signal() {
|
||||||
let _ = tokio::signal::ctrl_c().await;
|
let _ = tokio::signal::ctrl_c().await;
|
||||||
tracing::info!("shutting down");
|
tracing::info!("shutting down");
|
||||||
|
|||||||
+4
-4
@@ -1,17 +1,17 @@
|
|||||||
use serde::Serialize;
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// Catalog metadata for a DESI data release/survey.
|
/// Catalog metadata for a DESI data release/survey.
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
pub struct Catalog {
|
pub struct Catalog {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub release: String,
|
pub release: String,
|
||||||
pub description: &'static str,
|
pub description: String,
|
||||||
pub object_count: Option<u64>,
|
pub object_count: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A single catalog object (galaxy / quasar / star) with its survey
|
/// A single catalog object (galaxy / quasar / star) with its survey
|
||||||
/// coordinates. `ra` and `dec` are in degrees; `redshift` is dimensionless.
|
/// coordinates. `ra` and `dec` are in degrees; `redshift` is dimensionless.
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
pub struct CatalogObject {
|
pub struct CatalogObject {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub catalog: String,
|
pub catalog: String,
|
||||||
|
|||||||
+24
-30
@@ -1,34 +1,16 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::Query,
|
extract::{Query, State},
|
||||||
http::StatusCode,
|
|
||||||
response::{IntoResponse, Response},
|
|
||||||
Json,
|
Json,
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::sync::LazyLock;
|
|
||||||
|
|
||||||
use crate::models::{Catalog, CatalogObject};
|
use crate::models::{Catalog, CatalogObject};
|
||||||
|
use crate::store::CatalogStore;
|
||||||
|
|
||||||
/// Placeholder catalogs until real DESI EDR/DR1 ingestion lands.
|
pub async fn list_catalogs(State(state): State<Arc<CatalogStore>>) -> Json<Vec<Catalog>> {
|
||||||
static CATALOGS: LazyLock<Vec<Catalog>> = LazyLock::new(|| {
|
Json(state.catalogs.clone())
|
||||||
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<Vec<Catalog>> {
|
|
||||||
Json(CATALOGS.clone())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
@@ -38,17 +20,29 @@ pub struct ObjectQuery {
|
|||||||
limit: Option<usize>,
|
limit: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Placeholder object query. Real implementation will page through the
|
pub async fn list_objects(
|
||||||
/// centralized DESI catalog store rather than return an empty result set.
|
State(state): State<Arc<CatalogStore>>,
|
||||||
pub async fn list_objects(Query(query): Query<ObjectQuery>) -> Response {
|
Query(query): Query<ObjectQuery>,
|
||||||
|
) -> Json<Vec<CatalogObject>> {
|
||||||
let limit = query.limit.unwrap_or(100).min(10_000);
|
let limit = query.limit.unwrap_or(100).min(10_000);
|
||||||
|
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
%limit,
|
%limit,
|
||||||
catalog = query.catalog.as_deref().unwrap_or("all"),
|
catalog = query.catalog.as_deref().unwrap_or("all"),
|
||||||
"querying catalog objects (placeholder)"
|
objects = state.objects.len(),
|
||||||
|
"querying catalog objects"
|
||||||
);
|
);
|
||||||
|
|
||||||
let objects: Vec<CatalogObject> = Vec::new();
|
let objects: Vec<CatalogObject> = match &query.catalog {
|
||||||
(StatusCode::OK, Json(objects)).into_response()
|
Some(catalog) => state
|
||||||
|
.objects
|
||||||
|
.iter()
|
||||||
|
.filter(|o| &o.catalog == catalog)
|
||||||
|
.take(limit)
|
||||||
|
.cloned()
|
||||||
|
.collect(),
|
||||||
|
None => state.objects.iter().take(limit).cloned().collect(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Json(objects)
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-2
@@ -1,13 +1,23 @@
|
|||||||
pub mod catalogs;
|
pub mod catalogs;
|
||||||
pub mod health;
|
pub mod health;
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use axum::{routing::get, Router};
|
use axum::{routing::get, Router};
|
||||||
|
|
||||||
/// Builds the application router. Kept separate from `main` so tests can
|
use crate::store::CatalogStore;
|
||||||
/// construct it without binding a socket.
|
|
||||||
|
/// Builds the application router with a static placeholder store. Kept
|
||||||
|
/// separate from `main` so tests can construct it without binding a socket.
|
||||||
pub fn app() -> Router {
|
pub fn app() -> Router {
|
||||||
|
app_with_state(Arc::new(CatalogStore::placeholder()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds the application router serving the given catalog store.
|
||||||
|
pub fn app_with_state(state: Arc<CatalogStore>) -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/health", get(health::health))
|
.route("/health", get(health::health))
|
||||||
.route("/api/v1/catalogs", get(catalogs::list_catalogs))
|
.route("/api/v1/catalogs", get(catalogs::list_catalogs))
|
||||||
.route("/api/v1/objects", get(catalogs::list_objects))
|
.route("/api/v1/objects", get(catalogs::list_objects))
|
||||||
|
.with_state(state)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use crate::models::{Catalog, CatalogObject};
|
||||||
|
|
||||||
|
/// In-memory catalog store, shared (via `Arc`) across routes. Populated either
|
||||||
|
/// from a DESI data file loaded at startup or from `placeholder`.
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct CatalogStore {
|
||||||
|
pub catalogs: Vec<Catalog>,
|
||||||
|
pub objects: Vec<CatalogObject>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// JSON layout of the DESI data file referenced by `API_DESI_DATA`.
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct DataFile {
|
||||||
|
pub catalogs: Vec<Catalog>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub objects: Vec<CatalogObject>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CatalogStore {
|
||||||
|
/// Static fallback catalogs used when no `API_DESI_DATA` file is given
|
||||||
|
/// (and by the `routes::app()` test helper).
|
||||||
|
pub fn placeholder() -> Self {
|
||||||
|
Self {
|
||||||
|
catalogs: vec![
|
||||||
|
Catalog {
|
||||||
|
name: "edr".to_string(),
|
||||||
|
release: "EDR".to_string(),
|
||||||
|
description: "DESI Early Data Release".to_string(),
|
||||||
|
object_count: None,
|
||||||
|
},
|
||||||
|
Catalog {
|
||||||
|
name: "dr1".to_string(),
|
||||||
|
release: "DR1".to_string(),
|
||||||
|
description: "DESI Data Release 1".to_string(),
|
||||||
|
object_count: None,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
objects: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads catalogs and objects from a JSON data file. Errors on unreadable
|
||||||
|
/// files or malformed JSON so the caller can fail loudly instead of
|
||||||
|
/// silently serving empty data.
|
||||||
|
pub fn load(path: &Path) -> anyhow::Result<Self> {
|
||||||
|
let text = std::fs::read_to_string(path)?;
|
||||||
|
let file: DataFile = serde_json::from_str(&text)?;
|
||||||
|
Ok(Self {
|
||||||
|
catalogs: file.catalogs,
|
||||||
|
objects: file.objects,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
use axum::body::{to_bytes, Body};
|
||||||
|
use axum::http::{Request, StatusCode};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tower::ServiceExt;
|
||||||
|
|
||||||
|
use desi_explorer_api::models::CatalogObject;
|
||||||
|
use desi_explorer_api::routes;
|
||||||
|
use desi_explorer_api::store::CatalogStore;
|
||||||
|
|
||||||
|
fn sample_store() -> CatalogStore {
|
||||||
|
CatalogStore {
|
||||||
|
catalogs: Vec::new(),
|
||||||
|
objects: vec![
|
||||||
|
CatalogObject {
|
||||||
|
id: "o1".to_string(),
|
||||||
|
catalog: "edr".to_string(),
|
||||||
|
object_type: "GALAXY".to_string(),
|
||||||
|
ra: 1.5,
|
||||||
|
dec: 2.5,
|
||||||
|
redshift: 0.8,
|
||||||
|
},
|
||||||
|
CatalogObject {
|
||||||
|
id: "o2".to_string(),
|
||||||
|
catalog: "dr1".to_string(),
|
||||||
|
object_type: "STAR".to_string(),
|
||||||
|
ra: 3.5,
|
||||||
|
dec: 4.5,
|
||||||
|
redshift: 0.0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn objects_returns_all_when_no_filter() {
|
||||||
|
let app = routes::app_with_state(Arc::new(sample_store()));
|
||||||
|
|
||||||
|
let response = app
|
||||||
|
.oneshot(
|
||||||
|
Request::builder()
|
||||||
|
.uri("/api/v1/objects")
|
||||||
|
.body(Body::empty())
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
let body = to_bytes(response.into_body(), usize::MAX).await.unwrap();
|
||||||
|
let objects: Vec<CatalogObject> = serde_json::from_slice(&body).unwrap();
|
||||||
|
assert_eq!(objects.len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn objects_filters_by_catalog() {
|
||||||
|
let app = routes::app_with_state(Arc::new(sample_store()));
|
||||||
|
|
||||||
|
let response = app
|
||||||
|
.oneshot(
|
||||||
|
Request::builder()
|
||||||
|
.uri("/api/v1/objects?catalog=edr")
|
||||||
|
.body(Body::empty())
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
let body = to_bytes(response.into_body(), usize::MAX).await.unwrap();
|
||||||
|
let objects: Vec<CatalogObject> = serde_json::from_slice(&body).unwrap();
|
||||||
|
assert_eq!(objects.len(), 1);
|
||||||
|
assert_eq!(objects[0].id, "o1");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn objects_respects_limit() {
|
||||||
|
let app = routes::app_with_state(Arc::new(sample_store()));
|
||||||
|
|
||||||
|
let response = app
|
||||||
|
.oneshot(
|
||||||
|
Request::builder()
|
||||||
|
.uri("/api/v1/objects?limit=1")
|
||||||
|
.body(Body::empty())
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
let body = to_bytes(response.into_body(), usize::MAX).await.unwrap();
|
||||||
|
let objects: Vec<CatalogObject> = serde_json::from_slice(&body).unwrap();
|
||||||
|
assert_eq!(objects.len(), 1);
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
use desi_explorer_api::store::CatalogStore;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_parses_catalogs_and_objects() {
|
||||||
|
let json = r#"{
|
||||||
|
"catalogs": [
|
||||||
|
{"name":"edr","release":"EDR","description":"test","object_count":2}
|
||||||
|
],
|
||||||
|
"objects": [
|
||||||
|
{"id":"o1","catalog":"edr","object_type":"GALAXY","ra":1.5,"dec":2.5,"redshift":0.8}
|
||||||
|
]
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
let dir = std::env::temp_dir().join(format!(
|
||||||
|
"desi_explorer_store_{}_{}",
|
||||||
|
std::process::id(),
|
||||||
|
line!()
|
||||||
|
));
|
||||||
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
|
let path = dir.join("data.json");
|
||||||
|
std::fs::write(&path, json).unwrap();
|
||||||
|
|
||||||
|
let store = CatalogStore::load(&path).unwrap();
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
|
||||||
|
assert_eq!(store.catalogs.len(), 1);
|
||||||
|
assert_eq!(store.catalogs[0].name, "edr");
|
||||||
|
assert_eq!(store.catalogs[0].object_count, Some(2));
|
||||||
|
assert_eq!(store.objects.len(), 1);
|
||||||
|
assert_eq!(store.objects[0].id, "o1");
|
||||||
|
assert_eq!(store.objects[0].ra, 1.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_rejects_malformed_json() {
|
||||||
|
let dir = std::env::temp_dir().join(format!(
|
||||||
|
"desi_explorer_store_{}_{}",
|
||||||
|
std::process::id(),
|
||||||
|
line!()
|
||||||
|
));
|
||||||
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
|
let path = dir.join("data.json");
|
||||||
|
std::fs::write(&path, "not json").unwrap();
|
||||||
|
|
||||||
|
let result = CatalogStore::load(&path);
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
+27
-2
@@ -8,13 +8,28 @@
|
|||||||
# output dirs live at the repo root and are referenced through `$(ROOT)`.
|
# output dirs live at the repo root and are referenced through `$(ROOT)`.
|
||||||
|
|
||||||
ODIN ?= odin
|
ODIN ?= odin
|
||||||
|
GDB ?= gdb
|
||||||
ROOT := ..
|
ROOT := ..
|
||||||
BIN := $(ROOT)/bin
|
BIN := $(ROOT)/bin
|
||||||
BINARY := $(BIN)/desi_explorer
|
BINARY := $(BIN)/desi_explorer
|
||||||
ODIN_FLAGS := -collection:lib=lib/local
|
ODIN_FLAGS := -collection:lib=lib/local
|
||||||
WASM_DEFINE := RAYLIB_WASM_LIB=env.o
|
WASM_DEFINE := RAYLIB_WASM_LIB=env.o
|
||||||
|
|
||||||
.PHONY: help setup add-dep run build build-debug build-web test clean fmt
|
# Normalize GUI_ENV_FILE (given relative to the repo root) to an absolute path
|
||||||
|
# so the Odin process can open it regardless of its working directory.
|
||||||
|
# "override" is required because GUI_ENV_FILE is usually set on the command
|
||||||
|
# line (or passed as an env var to this sub-make), which would otherwise
|
||||||
|
# override any assignment made here.
|
||||||
|
ifdef GUI_ENV_FILE
|
||||||
|
ifneq ($(abspath $(GUI_ENV_FILE)),$(GUI_ENV_FILE))
|
||||||
|
override GUI_ENV_FILE := $(abspath $(ROOT)/$(GUI_ENV_FILE))
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
# "override" on a command-line variable silently drops it from the recipe
|
||||||
|
# environment; re-export it so the app can find its env file (run + gdb).
|
||||||
|
export GUI_ENV_FILE
|
||||||
|
|
||||||
|
.PHONY: help setup add-dep run build build-debug gdb build-web test clean fmt
|
||||||
|
|
||||||
help: ## List available targets
|
help: ## List available targets
|
||||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
|
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
|
||||||
@@ -41,12 +56,22 @@ build-debug: ## Debug build -> bin/desi_explorer
|
|||||||
@mkdir -p lib/local $(BIN)
|
@mkdir -p lib/local $(BIN)
|
||||||
$(ODIN) build src $(ODIN_FLAGS) -o:none -debug -out:$(BINARY)
|
$(ODIN) build src $(ODIN_FLAGS) -o:none -debug -out:$(BINARY)
|
||||||
|
|
||||||
|
gdb: build-debug ## Run the native app under gdb (type 'run', then 'bt' on a crash)
|
||||||
|
$(GDB) -q --args $(BINARY) $(ARGS)
|
||||||
|
|
||||||
build-web: ## WebAssembly build -> build/web (needs emscripten)
|
build-web: ## WebAssembly build -> build/web (needs emscripten)
|
||||||
@scripts/build_web.sh
|
@scripts/build_web.sh
|
||||||
|
|
||||||
test: ## Run Odin unit tests
|
test: ## Run Odin unit tests
|
||||||
@mkdir -p lib/local
|
@mkdir -p lib/local
|
||||||
$(ODIN) test src $(ODIN_FLAGS)
|
@if ls test/*.odin >/dev/null 2>&1; then \
|
||||||
|
echo "== gui/test =="; \
|
||||||
|
$(ODIN) test test $(ODIN_FLAGS); \
|
||||||
|
fi
|
||||||
|
@for dir in $$(find lib/local -name '*_test.odin' -exec dirname {} \; | sort -u); do \
|
||||||
|
echo "== $$dir =="; \
|
||||||
|
$(ODIN) test "$$dir" $(ODIN_FLAGS); \
|
||||||
|
done
|
||||||
|
|
||||||
clean: ## Remove build artifacts
|
clean: ## Remove build artifacts
|
||||||
rm -rf $(BIN) build
|
rm -rf $(BIN) build
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
package dotenv
|
||||||
|
|
||||||
|
import "base:runtime"
|
||||||
|
import "core:os"
|
||||||
|
import "core:reflect"
|
||||||
|
import "core:strconv"
|
||||||
|
import "core:strings"
|
||||||
|
|
||||||
|
// parse parses dotenv-format source (KEY=VALUE lines) into a map allocated
|
||||||
|
// with allocator. Blank lines, lines starting with '#', and lines without a
|
||||||
|
// '=' are skipped. Keys and values are trimmed; values may be wrapped in
|
||||||
|
// double quotes. Real process environment variables take precedence over the
|
||||||
|
// file. The returned map owns its keys/values; release it with destroy.
|
||||||
|
@(private)
|
||||||
|
parse :: proc(src: string, allocator := context.allocator) -> map[string]string {
|
||||||
|
result := make(map[string]string, allocator)
|
||||||
|
|
||||||
|
it := src
|
||||||
|
for line in strings.split_lines_iterator(&it) {
|
||||||
|
tr := strings.trim_space(line)
|
||||||
|
if len(tr) == 0 || strings.has_prefix(tr, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
eq := strings.index_byte(tr, '=')
|
||||||
|
if eq < 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
key := strings.trim_space(tr[:eq])
|
||||||
|
if key == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
value := strings.trim_space(tr[eq + 1:])
|
||||||
|
if len(value) >= 2 && value[0] == '"' && value[len(value) - 1] == '"' {
|
||||||
|
value = value[1:len(value) - 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// real process env vars win over the file
|
||||||
|
if override, found := os.lookup_env(key, allocator); found {
|
||||||
|
result[strings.clone(key, allocator)] = override
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// clone so the map outlives the source buffer (e.g. a freed file read)
|
||||||
|
result[strings.clone(key, allocator)] = strings.clone(value, allocator)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse_file reads a dotenv file from disk and parses it into a map. It
|
||||||
|
// returns (nil, false) when the file cannot be read (e.g. it does not exist).
|
||||||
|
parse_file :: proc(filename: string, allocator := context.allocator) -> (map[string]string, bool) {
|
||||||
|
data, err := os.read_entire_file(filename, allocator)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
defer delete(data, allocator)
|
||||||
|
|
||||||
|
return parse(string(data), allocator), true
|
||||||
|
}
|
||||||
|
|
||||||
|
// destroy frees the cloned keys/values and the map itself. Use it to release
|
||||||
|
// a map returned by parse/parse_file (plain delete does not free the strings).
|
||||||
|
// Any allocator passed to parse/parse_file must be passed here too.
|
||||||
|
destroy :: proc(env: map[string]string, allocator := context.allocator) {
|
||||||
|
for key, value in env {
|
||||||
|
delete(key, allocator)
|
||||||
|
delete(value, allocator)
|
||||||
|
}
|
||||||
|
delete(env)
|
||||||
|
}
|
||||||
|
|
||||||
|
// env_key returns the env key that should bind to a struct field. It prefers
|
||||||
|
// an explicit `env:"NAME"` tag; when the tag is absent or empty it falls
|
||||||
|
// back to the field's name. Matching against the parsed map is
|
||||||
|
// case-insensitive, so API_URL maps onto api_url (or an `env:"API_URL"` tag).
|
||||||
|
@(private)
|
||||||
|
env_key_for_field :: proc(field: reflect.Struct_Field) -> string {
|
||||||
|
if tag_key, ok := reflect.struct_tag_lookup(field.tag, "env"); ok && tag_key != "" {
|
||||||
|
return tag_key
|
||||||
|
}
|
||||||
|
return field.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// decode populates dest's fields from env, matching each field by name
|
||||||
|
// (or by an `env:"NAME"` struct tag). Values are converted to the field's
|
||||||
|
// type: string is cloned as-is into allocator, integers are parsed with
|
||||||
|
// strconv.parse_int (decimal/hex/negative), booleans with strconv.parse_bool,
|
||||||
|
// and floats with strconv.parse_f64. Keys missing from env leave the field
|
||||||
|
// at its zero value. It returns false if a present value cannot be converted
|
||||||
|
// to the field's type.
|
||||||
|
decode :: proc(env: map[string]string, dest: ^$T, allocator := context.allocator) -> bool {
|
||||||
|
ti := reflect.type_info_base(type_info_of(T))
|
||||||
|
fields, ok := ti.variant.(runtime.Type_Info_Struct)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
value: string
|
||||||
|
field_ptr := rawptr(dest)
|
||||||
|
st: reflect.Struct_Field
|
||||||
|
for _, i in fields.names[:fields.field_count] {
|
||||||
|
st = reflect.struct_field_at(T, i)
|
||||||
|
name := env_key_for_field(st)
|
||||||
|
value = ""
|
||||||
|
found := false
|
||||||
|
for key, v in env {
|
||||||
|
if key == name || strings.equal_fold(key, name) {
|
||||||
|
value, found = v, true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
field_ptr = rawptr(uintptr(dest) + fields.offsets[i])
|
||||||
|
field_ti := reflect.type_info_base(fields.types[i])
|
||||||
|
#partial switch variant in field_ti.variant {
|
||||||
|
case runtime.Type_Info_String:
|
||||||
|
(^string)(field_ptr)^ = strings.clone(value, allocator)
|
||||||
|
case runtime.Type_Info_Integer:
|
||||||
|
parsed, err := strconv.parse_int(value)
|
||||||
|
if !err {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch field_ti.size {
|
||||||
|
case 1:
|
||||||
|
(^i8)(field_ptr)^ = cast(i8)parsed
|
||||||
|
case 2:
|
||||||
|
(^i16)(field_ptr)^ = cast(i16)parsed
|
||||||
|
case 4:
|
||||||
|
(^i32)(field_ptr)^ = cast(i32)parsed
|
||||||
|
case 8:
|
||||||
|
(^i64)(field_ptr)^ = cast(i64)parsed
|
||||||
|
case:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case runtime.Type_Info_Boolean:
|
||||||
|
parsed, err := strconv.parse_bool(value)
|
||||||
|
if !err {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
(^bool)(field_ptr)^ = parsed
|
||||||
|
case runtime.Type_Info_Float:
|
||||||
|
parsed, err := strconv.parse_f64(value)
|
||||||
|
if !err {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch field_ti.size {
|
||||||
|
case 4:
|
||||||
|
(^f32)(field_ptr)^ = cast(f32)parsed
|
||||||
|
case 8:
|
||||||
|
(^f64)(field_ptr)^ = parsed
|
||||||
|
case:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case:
|
||||||
|
// unsupported field type (slices, pointers, ...) is left untouched
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
package dotenv_tests
|
||||||
|
|
||||||
|
import "core:os"
|
||||||
|
import "core:strings"
|
||||||
|
import "core:testing"
|
||||||
|
import dotenv "lib:dotenv/src"
|
||||||
|
|
||||||
|
Test_Config :: struct {
|
||||||
|
api_url: string,
|
||||||
|
debug: bool,
|
||||||
|
port: int,
|
||||||
|
ratio: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
Tagged_Config :: struct {
|
||||||
|
api_url: string `env:"API_URL"`,
|
||||||
|
port: int `env:"PORT"`,
|
||||||
|
debug: bool `env:"DEBUG"`,
|
||||||
|
}
|
||||||
|
|
||||||
|
Tagged_Empty :: struct {
|
||||||
|
api_url: string `env:""`,
|
||||||
|
}
|
||||||
|
|
||||||
|
// load_env writes src to a unique temp file and parses it via parse_file.
|
||||||
|
// The returned map owns its strings; callers must destroy it.
|
||||||
|
load_env :: proc(t: ^testing.T, src: string) -> map[string]string {
|
||||||
|
dir, err := os.make_directory_temp("", "dotenv_test_*", context.allocator)
|
||||||
|
testing.expect(t, err == nil, "expected temp dir to be created")
|
||||||
|
defer os.remove_all(dir)
|
||||||
|
defer delete(dir)
|
||||||
|
|
||||||
|
path := strings.concatenate({dir, "/.env"})
|
||||||
|
defer delete(path)
|
||||||
|
|
||||||
|
testing.expect(
|
||||||
|
t,
|
||||||
|
os.write_entire_file(path, src) == nil,
|
||||||
|
"expected file write to succeed",
|
||||||
|
)
|
||||||
|
|
||||||
|
env, ok := dotenv.parse_file(path)
|
||||||
|
testing.expect(t, ok, "expected parse_file to succeed")
|
||||||
|
return env
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_parse_basic :: proc(t: ^testing.T) {
|
||||||
|
env := load_env(t, "API_URL=http://127.0.0.1:8080\nDEBUG=true\nPORT=8080\n")
|
||||||
|
defer dotenv.destroy(env)
|
||||||
|
|
||||||
|
testing.expect(t, env["API_URL"] == "http://127.0.0.1:8080")
|
||||||
|
testing.expect(t, env["DEBUG"] == "true")
|
||||||
|
testing.expect(t, env["PORT"] == "8080")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_parse_ignores_comments_and_blank_lines :: proc(t: ^testing.T) {
|
||||||
|
env := load_env(t, "# leading comment\n\n \nFOO=bar \nBAZ = qux \n")
|
||||||
|
defer dotenv.destroy(env)
|
||||||
|
|
||||||
|
testing.expect(t, env["FOO"] == "bar", "value should be trimmed")
|
||||||
|
testing.expect(
|
||||||
|
t,
|
||||||
|
env["BAZ"] == "qux",
|
||||||
|
"key and value should be trimmed around '='",
|
||||||
|
)
|
||||||
|
testing.expect(
|
||||||
|
t,
|
||||||
|
"API_URL" not_in env,
|
||||||
|
"comment-only lines should not be parsed",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_parse_quoted_values :: proc(t: ^testing.T) {
|
||||||
|
env := load_env(t, "GREETING=\"hello world\"\nEMPTY=\"\"\n")
|
||||||
|
defer dotenv.destroy(env)
|
||||||
|
|
||||||
|
testing.expect(
|
||||||
|
t,
|
||||||
|
env["GREETING"] == "hello world",
|
||||||
|
"quoted value with inner space",
|
||||||
|
)
|
||||||
|
testing.expect(t, env["EMPTY"] == "", "double-quoted empty value")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_parse_skips_lines_without_equals :: proc(t: ^testing.T) {
|
||||||
|
env := load_env(t, "not-an-assignment\nOK=yep\n")
|
||||||
|
defer dotenv.destroy(env)
|
||||||
|
|
||||||
|
testing.expect(t, env["OK"] == "yep")
|
||||||
|
testing.expect(
|
||||||
|
t,
|
||||||
|
"not-an-assignment" not_in env,
|
||||||
|
"line without '=' should be skipped",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_parse_missing_file :: proc(t: ^testing.T) {
|
||||||
|
env, ok := dotenv.parse_file("/nonexistent/dotenv_test_does_not_exist.env")
|
||||||
|
testing.expect(t, !ok, "missing file should report failure")
|
||||||
|
testing.expect(t, env == nil, "missing file should return nil map")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_real_env_overrides_file :: proc(t: ^testing.T) {
|
||||||
|
testing.expect(t, os.set_env("DESI_EXPLORER_TEST_FOO", "from_env") == nil)
|
||||||
|
defer os.unset_env("DESI_EXPLORER_TEST_FOO")
|
||||||
|
|
||||||
|
env := load_env(t, "DESI_EXPLORER_TEST_FOO=from_file\n")
|
||||||
|
defer dotenv.destroy(env)
|
||||||
|
|
||||||
|
testing.expect(
|
||||||
|
t,
|
||||||
|
env["DESI_EXPLORER_TEST_FOO"] == "from_env",
|
||||||
|
"real env var should win over file",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_decode_maps_fields_case_insensitively :: proc(t: ^testing.T) {
|
||||||
|
env := load_env(
|
||||||
|
t,
|
||||||
|
"API_URL=http://127.0.0.1:8080\nDEBUG=true\nPORT=8080\nRATIO=0.5\n",
|
||||||
|
)
|
||||||
|
defer dotenv.destroy(env)
|
||||||
|
|
||||||
|
cfg := Test_Config{}
|
||||||
|
testing.expect(t, dotenv.decode(env, &cfg))
|
||||||
|
defer delete(cfg.api_url)
|
||||||
|
|
||||||
|
testing.expect(
|
||||||
|
t,
|
||||||
|
cfg.api_url == "http://127.0.0.1:8080",
|
||||||
|
"API_URL maps onto api_url",
|
||||||
|
)
|
||||||
|
testing.expect(t, cfg.debug == true, "DEBUG=true should decode to true")
|
||||||
|
testing.expect(t, cfg.port == 8080, "PORT=8080 should decode to int 8080")
|
||||||
|
testing.expect(t, cfg.ratio == 0.5, "RATIO=0.5 should decode to f64 0.5")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_decode_matches_exact_and_lowercase_keys :: proc(t: ^testing.T) {
|
||||||
|
env := load_env(t, "api_url=http://exact\nPort=9090\n")
|
||||||
|
defer dotenv.destroy(env)
|
||||||
|
|
||||||
|
cfg := Test_Config{}
|
||||||
|
testing.expect(t, dotenv.decode(env, &cfg))
|
||||||
|
defer delete(cfg.api_url)
|
||||||
|
|
||||||
|
testing.expect(
|
||||||
|
t,
|
||||||
|
cfg.api_url == "http://exact",
|
||||||
|
"exact-case key should match",
|
||||||
|
)
|
||||||
|
testing.expect(t, cfg.port == 9090, "mixed-case key should match field")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_decode_missing_keys_leave_zero_values :: proc(t: ^testing.T) {
|
||||||
|
env := load_env(t, "UNRELATED=value\n")
|
||||||
|
defer dotenv.destroy(env)
|
||||||
|
|
||||||
|
cfg := Test_Config{}
|
||||||
|
testing.expect(t, dotenv.decode(env, &cfg))
|
||||||
|
|
||||||
|
testing.expect(t, cfg.api_url == "")
|
||||||
|
testing.expect(t, !cfg.debug)
|
||||||
|
testing.expect(t, cfg.port == 0)
|
||||||
|
testing.expect(t, cfg.ratio == 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_decode_unparsable_int_fails :: proc(t: ^testing.T) {
|
||||||
|
env := load_env(t, "PORT=oops\n")
|
||||||
|
defer dotenv.destroy(env)
|
||||||
|
|
||||||
|
cfg := Test_Config{}
|
||||||
|
testing.expect(
|
||||||
|
t,
|
||||||
|
!dotenv.decode(env, &cfg),
|
||||||
|
"unparsable int should make decode fail",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_decode_unparsable_bool_fails :: proc(t: ^testing.T) {
|
||||||
|
env := load_env(t, "DEBUG=maybe\nAPI_URL=http://127.0.0.1:8080\n")
|
||||||
|
defer dotenv.destroy(env)
|
||||||
|
|
||||||
|
cfg := Test_Config{}
|
||||||
|
testing.expect(
|
||||||
|
t,
|
||||||
|
!dotenv.decode(env, &cfg),
|
||||||
|
"unparsable bool should make decode fail",
|
||||||
|
)
|
||||||
|
defer delete(cfg.api_url)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_decode_hex_and_negative_ints :: proc(t: ^testing.T) {
|
||||||
|
env := load_env(t, "PORT=0x1F\n")
|
||||||
|
defer dotenv.destroy(env)
|
||||||
|
|
||||||
|
cfg := Test_Config{}
|
||||||
|
testing.expect(t, dotenv.decode(env, &cfg))
|
||||||
|
testing.expect(t, cfg.port == 31, "hex int should decode")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_decode_honors_env_tags :: proc(t: ^testing.T) {
|
||||||
|
env := load_env(t, "API_URL=http://127.0.0.1:8080\nPORT=9090\nDEBUG=true\n")
|
||||||
|
defer dotenv.destroy(env)
|
||||||
|
|
||||||
|
cfg := Tagged_Config{}
|
||||||
|
testing.expect(t, dotenv.decode(env, &cfg))
|
||||||
|
defer delete(cfg.api_url)
|
||||||
|
|
||||||
|
testing.expect(
|
||||||
|
t,
|
||||||
|
cfg.api_url == "http://127.0.0.1:8080",
|
||||||
|
"env tag should bind API_URL",
|
||||||
|
)
|
||||||
|
testing.expect(t, cfg.port == 9090, "env tag should bind PORT")
|
||||||
|
testing.expect(t, cfg.debug == true, "env tag should bind DEBUG")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_decode_empty_env_tag_falls_back_to_field_name :: proc(t: ^testing.T) {
|
||||||
|
env := load_env(t, "api_url=http://fallback\n")
|
||||||
|
defer dotenv.destroy(env)
|
||||||
|
|
||||||
|
cfg := Tagged_Empty{}
|
||||||
|
testing.expect(t, dotenv.decode(env, &cfg))
|
||||||
|
defer delete(cfg.api_url)
|
||||||
|
|
||||||
|
testing.expect(
|
||||||
|
t,
|
||||||
|
cfg.api_url == "http://fallback",
|
||||||
|
"empty env tag should use field name",
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,327 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import "core:fmt"
|
||||||
|
import "core:log"
|
||||||
|
import "core:net"
|
||||||
|
import "core:strconv"
|
||||||
|
import "core:strings"
|
||||||
|
import "core:time"
|
||||||
|
|
||||||
|
DEFAULT_HTTP_PORT :: 80
|
||||||
|
DEFAULT_HTTPS_PORT :: 443
|
||||||
|
MAX_REDIRECTS :: 5
|
||||||
|
HTTP_TIMEOUT :: 5 * time.Second
|
||||||
|
|
||||||
|
Response :: struct {
|
||||||
|
status: int,
|
||||||
|
location: string,
|
||||||
|
body: string,
|
||||||
|
content_length: int,
|
||||||
|
chunked: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
Error :: struct {
|
||||||
|
code: int,
|
||||||
|
message: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
Error_Code :: enum int {
|
||||||
|
Invalid_Url,
|
||||||
|
Dial_Failed,
|
||||||
|
Send_Failed,
|
||||||
|
Invalid_Response,
|
||||||
|
Invalid_Redirect,
|
||||||
|
Invalid_Chunked,
|
||||||
|
Too_Many_Redirects,
|
||||||
|
}
|
||||||
|
|
||||||
|
error_make :: proc(code: Error_Code, msg: string = "") -> ^Error {
|
||||||
|
err := new(Error)
|
||||||
|
err.code = int(code)
|
||||||
|
err.message = msg
|
||||||
|
|
||||||
|
if msg == "" {
|
||||||
|
switch code {
|
||||||
|
case .Invalid_Url:
|
||||||
|
err.message = "invalid url"
|
||||||
|
case .Dial_Failed:
|
||||||
|
err.message = "could not connect to host"
|
||||||
|
case .Send_Failed:
|
||||||
|
err.message = "could not send request"
|
||||||
|
case .Invalid_Response:
|
||||||
|
err.message = "could not parse response"
|
||||||
|
case .Invalid_Redirect:
|
||||||
|
err.message = "invalid redirect"
|
||||||
|
case .Invalid_Chunked:
|
||||||
|
err.message = "invalid chunked response"
|
||||||
|
case .Too_Many_Redirects:
|
||||||
|
err.message = "too many redirects"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
http_get :: proc(url: string) -> (resp: Response, err: ^Error) {
|
||||||
|
current := url
|
||||||
|
owns := false
|
||||||
|
|
||||||
|
for _ in 0 ..= MAX_REDIRECTS {
|
||||||
|
next_url, next_resp, request_err := perform_request(current)
|
||||||
|
if owns do delete(current)
|
||||||
|
|
||||||
|
if request_err != nil {
|
||||||
|
return {}, request_err
|
||||||
|
}
|
||||||
|
|
||||||
|
if next_url != "" {
|
||||||
|
current = next_url
|
||||||
|
owns = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return next_resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if owns do delete(current)
|
||||||
|
return {}, error_make(.Too_Many_Redirects)
|
||||||
|
}
|
||||||
|
|
||||||
|
perform_request :: proc(url: string) -> (next_url: string, resp: Response, err: ^Error) {
|
||||||
|
_, host, port, path, parse_ok := parse_http_url(url)
|
||||||
|
if !parse_ok {
|
||||||
|
return "", {}, error_make(.Invalid_Url)
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, dial_err := net.dial_tcp_from_host_or_endpoint(net.Host{host, port})
|
||||||
|
if dial_err != nil {
|
||||||
|
return "", {}, error_make(.Dial_Failed)
|
||||||
|
}
|
||||||
|
defer net.close(conn)
|
||||||
|
|
||||||
|
net.set_option(conn, .Receive_Timeout, HTTP_TIMEOUT)
|
||||||
|
net.set_option(conn, .Send_Timeout, HTTP_TIMEOUT)
|
||||||
|
|
||||||
|
host_buf: [256]byte
|
||||||
|
host_head := host
|
||||||
|
if port != DEFAULT_HTTP_PORT {
|
||||||
|
host_head = fmt.bprintf(host_buf[:], "%s:%d", host, port)
|
||||||
|
}
|
||||||
|
|
||||||
|
request_buf: [1024]byte
|
||||||
|
request := fmt.bprintf(
|
||||||
|
request_buf[:],
|
||||||
|
"GET %s HTTP/1.1\r\nHost: %s\r\nConnection: close\r\nUser-Agent: desi-explorer-http\r\nAccept: */*\r\n\r\n",
|
||||||
|
path,
|
||||||
|
host_head,
|
||||||
|
)
|
||||||
|
|
||||||
|
if _, send_err := net.send_tcp(conn, transmute([]u8)request); send_err != nil {
|
||||||
|
return "", {}, error_make(.Send_Failed)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf: [dynamic]u8
|
||||||
|
defer delete(buf)
|
||||||
|
scratch: [4096]byte
|
||||||
|
for {
|
||||||
|
n, recv_err := net.recv_tcp(conn, scratch[:])
|
||||||
|
if n > 0 do append(&buf, ..scratch[:n])
|
||||||
|
if recv_err != nil || n == 0 do break
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed, response_ok := parse_response(buf[:])
|
||||||
|
if !response_ok {
|
||||||
|
return "", {}, error_make(.Invalid_Response)
|
||||||
|
}
|
||||||
|
resp = parsed
|
||||||
|
|
||||||
|
if resp.status >= 300 && resp.status < 400 && resp.location != "" {
|
||||||
|
resolved := resolve_redirect(url, resp.location)
|
||||||
|
if resolved == "" {
|
||||||
|
return "", {}, error_make(.Invalid_Redirect)
|
||||||
|
}
|
||||||
|
resp.body = ""
|
||||||
|
return resolved, resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.chunked {
|
||||||
|
decoded, decode_ok := decode_chunked(resp.body)
|
||||||
|
if !decode_ok {
|
||||||
|
return "", {}, error_make(.Invalid_Chunked)
|
||||||
|
}
|
||||||
|
resp.body = decoded
|
||||||
|
return "", resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.content_length >= 0 && resp.content_length < len(resp.body) {
|
||||||
|
resp.body = resp.body[:resp.content_length]
|
||||||
|
}
|
||||||
|
resp.body = strings.clone(resp.body)
|
||||||
|
return "", resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_response :: proc(raw: []byte) -> (resp: Response, ok: bool) {
|
||||||
|
resp.content_length = -1
|
||||||
|
raw_str := string(raw)
|
||||||
|
|
||||||
|
header_end := strings.index(raw_str, "\r\n\r\n")
|
||||||
|
if header_end < 0 {
|
||||||
|
return resp, false
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.split(raw_str[:header_end], "\r\n")
|
||||||
|
defer delete(lines)
|
||||||
|
if len(lines) == 0 {
|
||||||
|
return resp, false
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.status = parse_status_code(lines[0])
|
||||||
|
if resp.status == 0 {
|
||||||
|
return resp, false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, i in lines {
|
||||||
|
if i == 0 do continue
|
||||||
|
colon := strings.index_byte(lines[i], ':')
|
||||||
|
if colon < 0 do continue
|
||||||
|
key := strings.trim_space(lines[i][:colon])
|
||||||
|
value := strings.trim_space(lines[i][colon + 1:])
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case fold_eq(key, "content-length"):
|
||||||
|
if n, number_ok := strconv.parse_int(value); number_ok {
|
||||||
|
resp.content_length = n
|
||||||
|
}
|
||||||
|
case fold_eq(key, "transfer-encoding"):
|
||||||
|
if strings.contains(value, "chunked") {
|
||||||
|
resp.chunked = true
|
||||||
|
}
|
||||||
|
case fold_eq(key, "location"):
|
||||||
|
resp.location = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body_start := header_end + 4
|
||||||
|
if body_start <= len(raw_str) {
|
||||||
|
resp.body = raw_str[body_start:]
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, true
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_status_code :: proc(status_line: string) -> int {
|
||||||
|
parts := strings.fields(status_line)
|
||||||
|
defer delete(parts)
|
||||||
|
if len(parts) < 2 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
code, ok := strconv.parse_int(parts[1])
|
||||||
|
return code if ok else 0
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_http_url :: proc(
|
||||||
|
url: string,
|
||||||
|
) -> (
|
||||||
|
scheme: string,
|
||||||
|
host: string,
|
||||||
|
port: int,
|
||||||
|
path: string,
|
||||||
|
ok: bool,
|
||||||
|
) {
|
||||||
|
if !strings.has_prefix(url, "https://") && !strings.has_prefix(url, "http://") {
|
||||||
|
return "", "", 0, "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
https := strings.has_prefix(url, "https://")
|
||||||
|
rest := url[https ? len("https://") : len("http://"):]
|
||||||
|
host_and_port := rest
|
||||||
|
if slash := strings.index_byte(rest, '/'); slash >= 0 {
|
||||||
|
host_and_port = rest[:slash]
|
||||||
|
path = rest[slash:]
|
||||||
|
} else {
|
||||||
|
path = "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
host = host_and_port
|
||||||
|
port = https ? DEFAULT_HTTPS_PORT : DEFAULT_HTTP_PORT
|
||||||
|
if colon := strings.last_index_byte(host_and_port, ':'); colon >= 0 {
|
||||||
|
parsed, p_ok := strconv.parse_int(host_and_port[colon + 1:])
|
||||||
|
if !p_ok {
|
||||||
|
return "", "", 0, "", false
|
||||||
|
}
|
||||||
|
host = host_and_port[:colon]
|
||||||
|
port = parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
if host == "" {
|
||||||
|
return "", "", 0, "", false
|
||||||
|
}
|
||||||
|
return https ? "https" : "http", host, port, path, true
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve_redirect :: proc(base_url, location: string) -> string {
|
||||||
|
if strings.has_prefix(location, "http://") || strings.has_prefix(location, "https://") {
|
||||||
|
return strings.clone(location)
|
||||||
|
}
|
||||||
|
if !strings.has_prefix(location, "/") {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
rest := base_url[len("http://"):]
|
||||||
|
if slash := strings.index_byte(rest, '/'); slash >= 0 {
|
||||||
|
rest = rest[:slash]
|
||||||
|
}
|
||||||
|
return strings.concatenate([]string{"http://", rest, location})
|
||||||
|
}
|
||||||
|
|
||||||
|
decode_chunked :: proc(data: string) -> (body: string, ok: bool) {
|
||||||
|
decoded: [dynamic]u8
|
||||||
|
defer delete(decoded)
|
||||||
|
|
||||||
|
at := 0
|
||||||
|
for at < len(data) {
|
||||||
|
nl := strings.index_byte(data[at:], '\n')
|
||||||
|
if nl < 0 do return "", false
|
||||||
|
size_line := strings.trim_space(data[at:at + nl])
|
||||||
|
at += nl + 1
|
||||||
|
|
||||||
|
if semi := strings.index_byte(size_line, ';'); semi >= 0 {
|
||||||
|
size_line = size_line[:semi]
|
||||||
|
}
|
||||||
|
size, size_ok := strconv.parse_int(size_line, 16)
|
||||||
|
if !size_ok || size < 0 {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
if size == 0 {
|
||||||
|
at += 2
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if at + size > len(data) {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
append(&decoded, ..transmute([]u8)data[at:at + size])
|
||||||
|
at += size + 2
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(decoded) == 0 {
|
||||||
|
return "", true
|
||||||
|
}
|
||||||
|
return strings.clone(string(decoded[:])), true
|
||||||
|
}
|
||||||
|
|
||||||
|
fold_eq :: proc(a, b: string) -> bool {
|
||||||
|
if len(a) != len(b) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i in 0 ..< len(a) {
|
||||||
|
ca := a[i]
|
||||||
|
cb := b[i]
|
||||||
|
if ca >= 'A' && ca <= 'Z' do ca += 'a' - 'A'
|
||||||
|
if cb >= 'A' && cb <= 'Z' do cb += 'a' - 'A'
|
||||||
|
if ca != cb do return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
package http_test
|
||||||
|
|
||||||
|
import "core:fmt"
|
||||||
|
import "core:log"
|
||||||
|
import "core:net"
|
||||||
|
import "core:testing"
|
||||||
|
import "core:thread"
|
||||||
|
|
||||||
|
import http "../src"
|
||||||
|
|
||||||
|
SERVER_BODY :: "hello world"
|
||||||
|
SERVER_RESPONSE :=
|
||||||
|
"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n" +
|
||||||
|
"Content-Length: 11\r\n" +
|
||||||
|
"Connection: close\r\n\r\n" +
|
||||||
|
SERVER_BODY
|
||||||
|
|
||||||
|
Serve_Args :: struct {
|
||||||
|
listener: net.TCP_Socket,
|
||||||
|
}
|
||||||
|
|
||||||
|
serve_proc :: proc(data: rawptr) {
|
||||||
|
args := (^Serve_Args)(data)
|
||||||
|
|
||||||
|
conn, _, err := net.accept_tcp(args.listener)
|
||||||
|
if err != nil {
|
||||||
|
log.errorf("test server: accept failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer net.close(conn)
|
||||||
|
|
||||||
|
request: [4096]byte
|
||||||
|
if _, err := net.recv_tcp(conn, request[:]); err != nil {
|
||||||
|
log.errorf("test server: recv failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := net.send_tcp(conn, transmute([]u8)SERVER_RESPONSE); err != nil {
|
||||||
|
log.errorf("test server: send failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_http_get_request :: proc(t: ^testing.T) {
|
||||||
|
listener, err := net.listen_tcp({net.IP4_Loopback, 0})
|
||||||
|
if err != nil {
|
||||||
|
log.errorf("could not start test server: %v", err)
|
||||||
|
testing.fail(t)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer net.close(listener)
|
||||||
|
|
||||||
|
bound, berr := net.bound_endpoint(listener)
|
||||||
|
if berr != nil {
|
||||||
|
log.errorf("could not read test server port: %v", berr)
|
||||||
|
testing.fail(t)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
args: Serve_Args = {
|
||||||
|
listener = listener,
|
||||||
|
}
|
||||||
|
server_thread := thread.create_and_start_with_data(&args, serve_proc)
|
||||||
|
defer thread.destroy(server_thread)
|
||||||
|
|
||||||
|
url_buf: [64]byte
|
||||||
|
url := fmt.bprintf(url_buf[:], "http://127.0.0.1:%d/", bound.port)
|
||||||
|
|
||||||
|
log.infof("requesting %s", url)
|
||||||
|
resp, req_err := http.http_get(url)
|
||||||
|
if !testing.expectf(t, req_err == nil, "http_get returned an error: %w", req_err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer delete(resp.body)
|
||||||
|
|
||||||
|
testing.expectf(t, resp.status == 200, "expected status 200, got %d", resp.status)
|
||||||
|
testing.expectf(t, resp.body == SERVER_BODY, "expected %q, got %q", SERVER_BODY, resp.body)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_http_parse_url :: proc(t: ^testing.T) {
|
||||||
|
testURL :: struct {
|
||||||
|
url: string,
|
||||||
|
expectedScheme: string,
|
||||||
|
expectedOK: bool,
|
||||||
|
expectedHost: string,
|
||||||
|
expectedPort: int,
|
||||||
|
expectedPath: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
testURLs := []testURL {
|
||||||
|
{"http://localhost:8080", "http", true, "localhost", 8080, "/"},
|
||||||
|
{"http://localhost", "http", true, "localhost", 80, "/"},
|
||||||
|
{"https://localhost:443", "https", true, "localhost", 443, "/"},
|
||||||
|
{"https://localhost", "https", true, "localhost", 443, "/"},
|
||||||
|
{"http://test:6969", "http", true, "test", 6969, "/"},
|
||||||
|
{
|
||||||
|
"http://localhost:3000/api/v1/catalogs",
|
||||||
|
"http",
|
||||||
|
true,
|
||||||
|
"localhost",
|
||||||
|
3000,
|
||||||
|
"/api/v1/catalogs",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
for tu in testURLs {
|
||||||
|
scheme, host, port, path, ok := http.parse_http_url(tu.url)
|
||||||
|
|
||||||
|
testing.expectf(
|
||||||
|
t,
|
||||||
|
ok == tu.expectedOK,
|
||||||
|
"expected ok from parsing to be %v but got %v",
|
||||||
|
tu.expectedOK,
|
||||||
|
ok,
|
||||||
|
)
|
||||||
|
testing.expectf(
|
||||||
|
t,
|
||||||
|
host == tu.expectedHost,
|
||||||
|
"expected host url to be '%s' but got '%s'",
|
||||||
|
tu.expectedHost,
|
||||||
|
host,
|
||||||
|
)
|
||||||
|
testing.expectf(
|
||||||
|
t,
|
||||||
|
scheme == tu.expectedScheme,
|
||||||
|
"expected scheme to be '%s' but got '%s'",
|
||||||
|
tu.expectedScheme,
|
||||||
|
scheme,
|
||||||
|
)
|
||||||
|
testing.expectf(
|
||||||
|
t,
|
||||||
|
port == tu.expectedPort,
|
||||||
|
"expected port to be %d but got %d",
|
||||||
|
tu.expectedPort,
|
||||||
|
port,
|
||||||
|
)
|
||||||
|
testing.expectf(
|
||||||
|
t,
|
||||||
|
path == tu.expectedPath,
|
||||||
|
"expected path to be '%s' but got '%s'",
|
||||||
|
tu.expectedPath,
|
||||||
|
path,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "core:log"
|
||||||
|
import "core:os"
|
||||||
|
import dotenv "lib:dotenv/src"
|
||||||
|
|
||||||
|
Config :: struct {
|
||||||
|
api_url: string `env:"API_URL"`,
|
||||||
|
}
|
||||||
|
|
||||||
|
get_config :: proc(env_file: ^string = nil) -> (^Config, ^Error) {
|
||||||
|
path := ".env"
|
||||||
|
if env_file != nil && env_file^ != "" {
|
||||||
|
path = env_file^
|
||||||
|
} else if from_env, ok := os.lookup_env("GUI_ENV_FILE", context.temp_allocator); ok {
|
||||||
|
path = from_env
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debugf("getting config from file: %s", path)
|
||||||
|
|
||||||
|
env, _ := dotenv.parse_file(path, context.temp_allocator)
|
||||||
|
defer dotenv.destroy(env, context.temp_allocator)
|
||||||
|
|
||||||
|
c := new(Config)
|
||||||
|
|
||||||
|
if env == nil {
|
||||||
|
c.api_url = os.get_env("API_URL", context.temp_allocator)
|
||||||
|
} else if !dotenv.decode(env, c) {
|
||||||
|
return nil, new_clone(Error{.Config, "failed to decode .env into Config"})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validate_config(c); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_config :: proc(c: ^Config) -> (err: ^Error) {
|
||||||
|
if c.api_url == "" {
|
||||||
|
err = new_clone(Error{.Config, "'API_URL' is required"})
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
+196
-6
@@ -1,5 +1,11 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
|
import "core:encoding/json"
|
||||||
|
import "core:fmt"
|
||||||
|
import "core:log"
|
||||||
|
import "core:strings"
|
||||||
|
import http "lib:http/src"
|
||||||
|
|
||||||
Catalog :: struct {
|
Catalog :: struct {
|
||||||
name: string,
|
name: string,
|
||||||
release: string,
|
release: string,
|
||||||
@@ -21,21 +27,205 @@ APIError :: struct {
|
|||||||
message: string,
|
message: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
get_catalogs :: proc(url: string) -> ([dynamic]Catalog, ^APIError) {
|
endpoint :: proc(base_url, path: string) -> string {
|
||||||
return nil, nil
|
return fmt.tprintf("%s/%s", strings.trim_right(base_url, "/"), strings.trim_left(path, "/"))
|
||||||
}
|
}
|
||||||
|
|
||||||
get_catalog :: proc(url: string, name: string) -> (^Catalog, ^APIError) {
|
api_error :: proc(code: int, message: string) -> ^APIError {
|
||||||
return nil, nil
|
err := new(APIError)
|
||||||
|
err.code = code
|
||||||
|
err.message = message
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy_api_error :: proc(err: ^APIError) {
|
||||||
|
delete(err.message)
|
||||||
|
free(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy_catalogs :: proc(catalogs: [dynamic]Catalog) {
|
||||||
|
for &c in catalogs {
|
||||||
|
delete(c.name)
|
||||||
|
delete(c.release)
|
||||||
|
if c.description != nil {
|
||||||
|
delete(c.description^)
|
||||||
|
free(c.description)
|
||||||
|
}
|
||||||
|
if c.object_count != nil {
|
||||||
|
free(c.object_count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete(catalogs)
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy_catalog_objects :: proc(objects: [dynamic]CatalogObject) {
|
||||||
|
for &o in objects {
|
||||||
|
delete(o.id)
|
||||||
|
delete(o.catalog)
|
||||||
|
delete(o.object_type)
|
||||||
|
}
|
||||||
|
delete(objects)
|
||||||
|
}
|
||||||
|
|
||||||
|
request_json_array :: proc(
|
||||||
|
url: string,
|
||||||
|
) -> (
|
||||||
|
arr: json.Array,
|
||||||
|
val: json.Value,
|
||||||
|
err: ^APIError,
|
||||||
|
ok: bool,
|
||||||
|
) {
|
||||||
|
resp, req_err := http.http_get(url)
|
||||||
|
if req_err != nil {
|
||||||
|
log.errorf("failed getting response from API: %w", req_err)
|
||||||
|
defer free(req_err)
|
||||||
|
return nil, json.Null(nil), api_error(req_err.code, strings.clone(req_err.message)), false
|
||||||
|
}
|
||||||
|
defer delete(resp.body)
|
||||||
|
|
||||||
|
if resp.status != 200 {
|
||||||
|
log.errorf("response did not return with 200 => %d", resp.status)
|
||||||
|
return nil,
|
||||||
|
json.Null(nil),
|
||||||
|
api_error(resp.status, strings.clone(strings.trim_space(resp.body))),
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
v, perr := json.parse_string(resp.body)
|
||||||
|
if perr != .None {
|
||||||
|
log.errorf("parsing json response failed: %w", perr)
|
||||||
|
return nil, json.Null(nil), api_error(int(perr), "invalid JSON in API response"), false
|
||||||
|
}
|
||||||
|
|
||||||
|
a, is_arr := v.(json.Array)
|
||||||
|
if !is_arr {
|
||||||
|
log.error("API response was not in JSON array format")
|
||||||
|
json.destroy_value(v)
|
||||||
|
return nil, json.Null(nil), api_error(0, "API response was not a JSON array"), false
|
||||||
|
}
|
||||||
|
return a, v, nil, true
|
||||||
|
}
|
||||||
|
|
||||||
|
get_catalogs :: proc(base_url: string) -> ([dynamic]Catalog, ^APIError) {
|
||||||
|
url := endpoint(base_url, "/api/v1/catalogs")
|
||||||
|
defer delete(url)
|
||||||
|
|
||||||
|
log.infof("getting catalogs from url: %s", url)
|
||||||
|
|
||||||
|
arr, val, err, ok := request_json_array(url)
|
||||||
|
if !ok {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer json.destroy_value(val)
|
||||||
|
|
||||||
|
catalogs := make([dynamic]Catalog, 0, len(arr))
|
||||||
|
for item in arr {
|
||||||
|
if c, cat_ok := catalog_from_json(item); cat_ok {
|
||||||
|
append(&catalogs, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return catalogs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
get_catalog_objects :: proc(
|
get_catalog_objects :: proc(
|
||||||
url: string,
|
base_url: string,
|
||||||
catalog_name: string,
|
catalog_name: string,
|
||||||
|
region: string = "",
|
||||||
) -> (
|
) -> (
|
||||||
[dynamic]CatalogObject,
|
[dynamic]CatalogObject,
|
||||||
^APIError,
|
^APIError,
|
||||||
) {
|
) {
|
||||||
return nil, nil
|
sb := strings.builder_make()
|
||||||
|
defer strings.builder_destroy(&sb)
|
||||||
|
fmt.sbprintf(&sb, "%s?catalog=%s", endpoint(base_url, "/api/v1/objects"), catalog_name)
|
||||||
|
if region != "" {
|
||||||
|
fmt.sbprintf(&sb, "®ion=%s", region)
|
||||||
|
}
|
||||||
|
fmt.sbprintf(&sb, "&limit=%d", 10_000)
|
||||||
|
url := strings.to_string(sb)
|
||||||
|
|
||||||
|
|
||||||
|
arr, val, err, ok := request_json_array(url)
|
||||||
|
if !ok {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer json.destroy_value(val)
|
||||||
|
|
||||||
|
objects := make([dynamic]CatalogObject, 0, len(arr))
|
||||||
|
for item in arr {
|
||||||
|
if o, obj_ok := catalog_object_from_json(item); obj_ok {
|
||||||
|
append(&objects, o)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return objects, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
catalog_from_json :: proc(v: json.Value) -> (c: Catalog, ok: bool) {
|
||||||
|
obj, is_obj := v.(json.Object)
|
||||||
|
if !is_obj {
|
||||||
|
return {}, false
|
||||||
|
}
|
||||||
|
c = Catalog {
|
||||||
|
name = json_string(obj, "name"),
|
||||||
|
release = json_string(obj, "release"),
|
||||||
|
}
|
||||||
|
if d, found := obj["description"]; found {
|
||||||
|
if s, s_ok := d.(json.String); s_ok {
|
||||||
|
c.description = new_clone(strings.clone(s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if n, n_ok := json_u64(obj, "object_count"); n_ok {
|
||||||
|
c.object_count = new_clone(n)
|
||||||
|
}
|
||||||
|
return c, true
|
||||||
|
}
|
||||||
|
|
||||||
|
catalog_object_from_json :: proc(v: json.Value) -> (o: CatalogObject, ok: bool) {
|
||||||
|
obj, is_obj := v.(json.Object)
|
||||||
|
if !is_obj {
|
||||||
|
return {}, false
|
||||||
|
}
|
||||||
|
return CatalogObject {
|
||||||
|
id = json_string(obj, "id"),
|
||||||
|
catalog = json_string(obj, "catalog"),
|
||||||
|
object_type = json_string(obj, "object_type"),
|
||||||
|
ra = json_f64(obj, "ra"),
|
||||||
|
dec = json_f64(obj, "dec"),
|
||||||
|
redshift = json_f64(obj, "redshift"),
|
||||||
|
},
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
json_string :: proc(obj: json.Object, key: string) -> string {
|
||||||
|
if v, found := obj[key]; found {
|
||||||
|
if s, ok := v.(json.String); ok {
|
||||||
|
return strings.clone(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
json_f64 :: proc(obj: json.Object, key: string) -> f64 {
|
||||||
|
if v, found := obj[key]; found {
|
||||||
|
#partial switch n in v {
|
||||||
|
case json.Integer:
|
||||||
|
return f64(n)
|
||||||
|
case json.Float:
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
json_u64 :: proc(obj: json.Object, key: string) -> (u64, bool) {
|
||||||
|
if v, found := obj[key]; found {
|
||||||
|
#partial switch n in v {
|
||||||
|
case json.Integer:
|
||||||
|
return u64(n), true
|
||||||
|
case json.Float:
|
||||||
|
return u64(n), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "core:fmt"
|
||||||
|
import "core:strings"
|
||||||
|
ErrorType :: enum {
|
||||||
|
Config,
|
||||||
|
API,
|
||||||
|
}
|
||||||
|
|
||||||
|
Error :: struct {
|
||||||
|
type: ErrorType,
|
||||||
|
message: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
format_error :: proc(err: ^Error) -> string {
|
||||||
|
sb := strings.builder_make(context.temp_allocator)
|
||||||
|
|
||||||
|
return fmt.sbprintf(&sb, "[%s] => %s", err.type, err.message)
|
||||||
|
}
|
||||||
|
|
||||||
+114
-8
@@ -1,14 +1,19 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
|
import "base:runtime"
|
||||||
|
import "core:fmt"
|
||||||
|
import "core:log"
|
||||||
import "core:math"
|
import "core:math"
|
||||||
import "core:math/rand"
|
import "core:math/rand"
|
||||||
|
import "core:os"
|
||||||
|
import "core:strings"
|
||||||
import rl "vendor:raylib"
|
import rl "vendor:raylib"
|
||||||
|
|
||||||
WIDTH :: 1280
|
WIDTH :: 1280
|
||||||
HEIGHT :: 720
|
HEIGHT :: 720
|
||||||
|
|
||||||
// Number of procedurally generated points standing in for the DESI catalog.
|
// Number of procedurally generated points standing in for the DESI catalog.
|
||||||
// This is replaced by real survey data once ingestion lands.
|
// This is replaced by real survey data once a catalog is loaded.
|
||||||
POINT_COUNT :: 4_000
|
POINT_COUNT :: 4_000
|
||||||
WORLD_RADIUS :: f32(500.0)
|
WORLD_RADIUS :: f32(500.0)
|
||||||
|
|
||||||
@@ -17,13 +22,44 @@ Galaxy :: struct {
|
|||||||
color: rl.Color,
|
color: rl.Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Screen :: enum {
|
||||||
|
Start,
|
||||||
|
Universe,
|
||||||
|
}
|
||||||
|
|
||||||
|
App :: struct {
|
||||||
|
screen: Screen,
|
||||||
|
api_url: strings.Builder,
|
||||||
|
api_url_focused: bool,
|
||||||
|
region: strings.Builder,
|
||||||
|
region_focused: bool,
|
||||||
|
catalogs: [dynamic]Catalog,
|
||||||
|
selected_catalog: int,
|
||||||
|
catalog_scroll: int,
|
||||||
|
status: strings.Builder,
|
||||||
|
loaded_catalog: string,
|
||||||
|
loaded_region: string,
|
||||||
|
}
|
||||||
|
|
||||||
camera: rl.Camera3D
|
camera: rl.Camera3D
|
||||||
|
app: App
|
||||||
universe: [dynamic]Galaxy
|
universe: [dynamic]Galaxy
|
||||||
|
|
||||||
// Deterministic generator so the placeholder sky is stable between runs.
|
// Deterministic generator so the placeholder sky is stable between runs.
|
||||||
rng: rand.Default_Random_State
|
rng: rand.Default_Random_State
|
||||||
|
|
||||||
main :: proc() {
|
main :: proc() {
|
||||||
|
c: ^Config
|
||||||
|
err: ^Error
|
||||||
|
|
||||||
|
context.logger = log.create_console_logger()
|
||||||
|
defer log.destroy_console_logger(context.logger)
|
||||||
|
s := os.get_env("GUI_ENV_FILE", context.temp_allocator)
|
||||||
|
|
||||||
|
if c, err = get_config(&s); err != nil {
|
||||||
|
panic(format_error(err))
|
||||||
|
}
|
||||||
|
|
||||||
rng = rand.create(0xDE51_0000)
|
rng = rand.create(0xDE51_0000)
|
||||||
context.random_generator = rand.default_random_generator(&rng)
|
context.random_generator = rand.default_random_generator(&rng)
|
||||||
|
|
||||||
@@ -33,13 +69,45 @@ main :: proc() {
|
|||||||
rl.SetTargetFPS(60)
|
rl.SetTargetFPS(60)
|
||||||
|
|
||||||
camera = make_camera()
|
camera = make_camera()
|
||||||
make_universe(&universe, POINT_COUNT)
|
app = make_app(c)
|
||||||
defer delete(universe)
|
defer destroy_app(&app)
|
||||||
|
|
||||||
for !rl.WindowShouldClose() {
|
for !rl.WindowShouldClose() {
|
||||||
|
switch app.screen {
|
||||||
|
case .Start:
|
||||||
|
start_screen_update()
|
||||||
|
start_screen_draw()
|
||||||
|
case .Universe:
|
||||||
update()
|
update()
|
||||||
draw()
|
draw()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
make_app :: proc(c: ^Config) -> App {
|
||||||
|
a := App {
|
||||||
|
screen = .Start,
|
||||||
|
selected_catalog = -1,
|
||||||
|
}
|
||||||
|
a.api_url = strings.builder_make()
|
||||||
|
a.region = strings.builder_make()
|
||||||
|
a.status = strings.builder_make()
|
||||||
|
strings.write_string(&a.api_url, c.api_url)
|
||||||
|
strings.write_string(
|
||||||
|
&a.status,
|
||||||
|
"Set the API URL, press Refresh, pick a catalog, then Explore.",
|
||||||
|
)
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy_app :: proc(a: ^App) {
|
||||||
|
strings.builder_destroy(&a.api_url)
|
||||||
|
strings.builder_destroy(&a.region)
|
||||||
|
strings.builder_destroy(&a.status)
|
||||||
|
destroy_catalogs(a.catalogs)
|
||||||
|
delete(a.loaded_catalog)
|
||||||
|
delete(a.loaded_region)
|
||||||
|
delete(universe)
|
||||||
}
|
}
|
||||||
|
|
||||||
make_camera :: proc() -> rl.Camera3D {
|
make_camera :: proc() -> rl.Camera3D {
|
||||||
@@ -61,6 +129,29 @@ make_universe :: proc(u: ^[dynamic]Galaxy, count: int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
make_universe_from_objects :: proc(u: ^[dynamic]Galaxy, objects: []CatalogObject) {
|
||||||
|
clear(u)
|
||||||
|
reserve(u, len(objects))
|
||||||
|
for obj in objects {
|
||||||
|
pos := ra_dec_to_pos(f32(obj.ra), f32(obj.dec), f32(obj.redshift))
|
||||||
|
append(u, Galaxy{position = pos, color = color_for_position(pos)})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Maps an equatorial position (ra/dec in degrees) plus redshift to a point in
|
||||||
|
// the scene: shells telescope outward with redshift.
|
||||||
|
ra_dec_to_pos :: proc(ra, dec, redshift: f32) -> rl.Vector3 {
|
||||||
|
theta := math.to_radians(ra)
|
||||||
|
phi := math.to_radians(dec)
|
||||||
|
t := math.clamp(redshift * 0.6, 0.05, 1.0)
|
||||||
|
r := WORLD_RADIUS * t
|
||||||
|
return {
|
||||||
|
r * math.cos(phi) * math.cos(theta),
|
||||||
|
r * math.sin(phi),
|
||||||
|
r * math.cos(phi) * math.sin(theta),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Uniformly distributed random point inside the scene's bounding sphere.
|
// Uniformly distributed random point inside the scene's bounding sphere.
|
||||||
random_sphere_point :: proc(radius: f32) -> rl.Vector3 {
|
random_sphere_point :: proc(radius: f32) -> rl.Vector3 {
|
||||||
for {
|
for {
|
||||||
@@ -85,6 +176,11 @@ color_for_position :: proc(p: rl.Vector3) -> rl.Color {
|
|||||||
update :: proc() {
|
update :: proc() {
|
||||||
// Orbital camera: drag to rotate, scroll to zoom, right-drag / shift to pan.
|
// Orbital camera: drag to rotate, scroll to zoom, right-drag / shift to pan.
|
||||||
rl.UpdateCamera(&camera, .ORBITAL)
|
rl.UpdateCamera(&camera, .ORBITAL)
|
||||||
|
|
||||||
|
// Return to the start screen to switch catalog / region without restarting.
|
||||||
|
if rl.IsKeyPressed(.TAB) {
|
||||||
|
app.screen = .Start
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
draw :: proc() {
|
draw :: proc() {
|
||||||
@@ -103,11 +199,21 @@ draw :: proc() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
rl.DrawFPS(10, 10)
|
rl.DrawFPS(10, 10)
|
||||||
rl.DrawText(
|
ui_draw_text("DESI Explorer - drag to rotate, scroll to zoom", 10, 34, 18, rl.RAYWHITE)
|
||||||
"DESI Explorer — drag to rotate, scroll to zoom",
|
|
||||||
|
buf: [128]u8
|
||||||
|
if app.loaded_catalog != "" {
|
||||||
|
ui_draw_text(fmt.bprintf(buf[:], "Catalog: %s", app.loaded_catalog), 10, 60, 18, rl.YELLOW)
|
||||||
|
}
|
||||||
|
if app.loaded_region != "" {
|
||||||
|
ui_draw_text(fmt.bprintf(buf[:], "Region: %s", app.loaded_region), 10, 82, 18, rl.YELLOW)
|
||||||
|
}
|
||||||
|
ui_draw_text(
|
||||||
|
"Press TAB to return to the start screen",
|
||||||
10,
|
10,
|
||||||
34,
|
i32(rl.GetScreenHeight()) - 28,
|
||||||
18,
|
16,
|
||||||
rl.RAYWHITE,
|
{120, 120, 140, 255},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,196 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "core:fmt"
|
||||||
|
import "core:strings"
|
||||||
|
import rl "vendor:raylib"
|
||||||
|
|
||||||
|
CATALOG_ROW_H :: 30
|
||||||
|
|
||||||
|
start_screen_update :: proc() {
|
||||||
|
api_rect := rl.Rectangle{60, 112, 460, 34}
|
||||||
|
region_rect := rl.Rectangle{60, 186, 460, 34}
|
||||||
|
|
||||||
|
text_input_update(&app.api_url, &app.api_url_focused, api_rect)
|
||||||
|
text_input_update(&app.region, &app.region_focused, region_rect)
|
||||||
|
|
||||||
|
if ui_button_clicked(rl.Rectangle{60, 230, 170, 38}) {
|
||||||
|
refresh_catalogs()
|
||||||
|
}
|
||||||
|
|
||||||
|
list_rect := rl.Rectangle{60, 300, 520, 230}
|
||||||
|
if clicked := catalog_list_update(list_rect); clicked >= 0 {
|
||||||
|
app.selected_catalog = clicked
|
||||||
|
set_statusf("Selected catalog: %s", app.catalogs[clicked].name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ui_button_clicked(rl.Rectangle{60, 548, 150, 38}) {
|
||||||
|
if explore() {
|
||||||
|
app.screen = .Universe
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
start_screen_draw :: proc() {
|
||||||
|
rl.BeginDrawing()
|
||||||
|
defer rl.EndDrawing()
|
||||||
|
|
||||||
|
rl.ClearBackground({8, 10, 20, 255})
|
||||||
|
|
||||||
|
ui_draw_text("DESI Explorer", 60, 36, 40, rl.WHITE)
|
||||||
|
|
||||||
|
ui_draw_text("API URL", 60, 92, 16, {150, 160, 190, 255})
|
||||||
|
text_input_draw(&app.api_url, app.api_url_focused, rl.Rectangle{60, 112, 460, 34})
|
||||||
|
|
||||||
|
ui_draw_text("Region (optional)", 60, 166, 16, {150, 160, 190, 255})
|
||||||
|
text_input_draw(&app.region, app.region_focused, rl.Rectangle{60, 186, 460, 34})
|
||||||
|
|
||||||
|
ui_button(rl.Rectangle{60, 230, 170, 38}, "Refresh catalogs")
|
||||||
|
|
||||||
|
ui_draw_text("Catalogs", 60, 280, 16, {150, 160, 190, 255})
|
||||||
|
catalog_list_draw(rl.Rectangle{60, 300, 520, 230})
|
||||||
|
|
||||||
|
ui_button(rl.Rectangle{60, 548, 150, 38}, "Explore")
|
||||||
|
|
||||||
|
ui_draw_text(strings.to_string(app.status), 60, i32(rl.GetScreenHeight()) - 36, 16, {200, 200, 220, 255})
|
||||||
|
}
|
||||||
|
|
||||||
|
set_status :: proc(msg: string) {
|
||||||
|
strings.builder_reset(&app.status)
|
||||||
|
strings.write_string(&app.status, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
set_statusf :: proc(format: string, args: ..any) {
|
||||||
|
strings.builder_reset(&app.status)
|
||||||
|
fmt.sbprintf(&app.status, format, ..args)
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh_catalogs :: proc() {
|
||||||
|
destroy_catalogs(app.catalogs)
|
||||||
|
app.catalogs = nil
|
||||||
|
app.selected_catalog = -1
|
||||||
|
|
||||||
|
base := strings.trim_space(strings.to_string(app.api_url))
|
||||||
|
if base == "" {
|
||||||
|
set_status("Enter an API URL first.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
catalogs, err := get_catalogs(base)
|
||||||
|
if err != nil {
|
||||||
|
set_statusf("Failed to load catalogs: %s", err.message)
|
||||||
|
destroy_api_error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
app.catalogs = catalogs
|
||||||
|
set_statusf("Loaded %d catalog(s). Select one and press Explore.", len(catalogs))
|
||||||
|
}
|
||||||
|
|
||||||
|
explore :: proc() -> bool {
|
||||||
|
if app.selected_catalog < 0 || app.selected_catalog >= len(app.catalogs) {
|
||||||
|
set_status("Select a catalog first.")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
base := strings.trim_space(strings.to_string(app.api_url))
|
||||||
|
region := strings.trim_space(strings.to_string(app.region))
|
||||||
|
if base == "" {
|
||||||
|
set_status("Enter an API URL first.")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
c := app.catalogs[app.selected_catalog]
|
||||||
|
objects, err := get_catalog_objects(base, c.name, region)
|
||||||
|
if err != nil {
|
||||||
|
set_statusf("Failed to load objects: %s", err.message)
|
||||||
|
destroy_api_error(err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(app.loaded_catalog)
|
||||||
|
delete(app.loaded_region)
|
||||||
|
app.loaded_catalog = strings.clone(c.name)
|
||||||
|
app.loaded_region = strings.clone(region)
|
||||||
|
|
||||||
|
if len(objects) == 0 {
|
||||||
|
make_universe(&universe, POINT_COUNT)
|
||||||
|
set_status("No objects returned - showing placeholder sky.")
|
||||||
|
} else {
|
||||||
|
make_universe_from_objects(&universe, objects[:])
|
||||||
|
set_statusf("Loaded %d object(s).", len(objects))
|
||||||
|
}
|
||||||
|
destroy_catalog_objects(objects)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
visible_row_count :: proc(rec: rl.Rectangle) -> int {
|
||||||
|
return max(1, int(rec.height) / CATALOG_ROW_H)
|
||||||
|
}
|
||||||
|
|
||||||
|
row_rect :: proc(rec: rl.Rectangle, index, scroll_offset: int) -> rl.Rectangle {
|
||||||
|
y := rec.y + f32((index - scroll_offset) * CATALOG_ROW_H)
|
||||||
|
return rl.Rectangle{rec.x, y, rec.width, CATALOG_ROW_H}
|
||||||
|
}
|
||||||
|
|
||||||
|
catalog_list_update :: proc(rec: rl.Rectangle) -> int {
|
||||||
|
mouse := rl.GetMousePosition()
|
||||||
|
if rl.CheckCollisionPointRec(mouse, rec) {
|
||||||
|
wheel := rl.GetMouseWheelMove()
|
||||||
|
max_scroll := max(0, len(app.catalogs) - visible_row_count(rec))
|
||||||
|
app.catalog_scroll = max(0, min(app.catalog_scroll - int(wheel), max_scroll))
|
||||||
|
}
|
||||||
|
|
||||||
|
clicked := -1
|
||||||
|
if len(app.catalogs) == 0 {
|
||||||
|
return clicked
|
||||||
|
}
|
||||||
|
visible := visible_row_count(rec)
|
||||||
|
for i in app.catalog_scroll ..< min(len(app.catalogs), app.catalog_scroll + visible) {
|
||||||
|
row := row_rect(rec, i, app.catalog_scroll)
|
||||||
|
if rl.CheckCollisionPointRec(mouse, row) && rl.IsMouseButtonPressed(.LEFT) {
|
||||||
|
clicked = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return clicked
|
||||||
|
}
|
||||||
|
|
||||||
|
catalog_list_draw :: proc(rec: rl.Rectangle) {
|
||||||
|
rl.DrawRectangleRec(rec, {12, 14, 22, 255})
|
||||||
|
rl.DrawRectangleLinesEx(rec, 1, {56, 60, 82, 255})
|
||||||
|
|
||||||
|
if len(app.catalogs) == 0 {
|
||||||
|
ui_draw_text("No catalogs loaded - press Refresh", i32(rec.x) + 12, i32(rec.y) + 12, 16, {120, 120, 140, 255})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mouse := rl.GetMousePosition()
|
||||||
|
visible := visible_row_count(rec)
|
||||||
|
for i in app.catalog_scroll ..< min(len(app.catalogs), app.catalog_scroll + visible) {
|
||||||
|
row := row_rect(rec, i, app.catalog_scroll)
|
||||||
|
if i == app.selected_catalog {
|
||||||
|
rl.DrawRectangleRec(row, {44, 56, 92, 255})
|
||||||
|
rl.DrawRectangleLinesEx(row, 1, {120, 160, 235, 255})
|
||||||
|
} else if rl.CheckCollisionPointRec(mouse, row) {
|
||||||
|
rl.DrawRectangleRec(row, {26, 34, 56, 255})
|
||||||
|
}
|
||||||
|
|
||||||
|
buf: [256]u8
|
||||||
|
ui_draw_text(
|
||||||
|
catalog_display(buf[:], app.catalogs[i]),
|
||||||
|
i32(rec.x) + 12,
|
||||||
|
i32(row.y) + 6,
|
||||||
|
16,
|
||||||
|
rl.RAYWHITE,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
catalog_display :: proc(buf: []u8, c: Catalog) -> string {
|
||||||
|
if c.description != nil {
|
||||||
|
return fmt.bprintf(buf, "%s (%s)", c.name, c.description^)
|
||||||
|
}
|
||||||
|
if c.release != "" {
|
||||||
|
return fmt.bprintf(buf, "%s (%s)", c.name, c.release)
|
||||||
|
}
|
||||||
|
return fmt.bprintf(buf, "%s", c.name)
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import c "core:c"
|
||||||
|
import "core:strings"
|
||||||
|
import "core:unicode/utf8"
|
||||||
|
import rl "vendor:raylib"
|
||||||
|
|
||||||
|
MAX_INPUT_LEN :: 256
|
||||||
|
|
||||||
|
to_cstring_buf :: proc(buf: []u8, s: string) -> cstring {
|
||||||
|
n := min(len(s), len(buf) - 1)
|
||||||
|
copy(buf[:n], s[:n])
|
||||||
|
buf[n] = 0
|
||||||
|
return cstring(&buf[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
ui_draw_text :: proc(text: string, x, y, size: c.int, color: rl.Color) {
|
||||||
|
buf: [1024]u8
|
||||||
|
rl.DrawText(to_cstring_buf(buf[:], text), x, y, size, color)
|
||||||
|
}
|
||||||
|
|
||||||
|
text_input_update :: proc(b: ^strings.Builder, focused: ^bool, rec: rl.Rectangle) {
|
||||||
|
mouse := rl.GetMousePosition()
|
||||||
|
if rl.CheckCollisionPointRec(mouse, rec) && rl.IsMouseButtonPressed(.LEFT) {
|
||||||
|
focused^ = true
|
||||||
|
} else if rl.IsMouseButtonPressed(.LEFT) {
|
||||||
|
focused^ = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !focused^ {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for r := rl.GetCharPressed(); r != 0; {
|
||||||
|
if len(b.buf) < MAX_INPUT_LEN {
|
||||||
|
strings.write_rune(b, r)
|
||||||
|
}
|
||||||
|
r = rl.GetCharPressed()
|
||||||
|
}
|
||||||
|
|
||||||
|
if rl.IsKeyPressed(.BACKSPACE) {
|
||||||
|
s := strings.to_string(b^)
|
||||||
|
_, rune_len := utf8.decode_last_rune(s)
|
||||||
|
if rune_len > 0 && len(b.buf) >= rune_len {
|
||||||
|
resize(&b.buf, len(b.buf) - rune_len)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
text_input_draw :: proc(b: ^strings.Builder, focused: bool, rec: rl.Rectangle) {
|
||||||
|
text := strings.to_string(b^)
|
||||||
|
|
||||||
|
if focused {
|
||||||
|
rl.DrawRectangleRec(rec, {28, 34, 56, 255})
|
||||||
|
rl.DrawRectangleLinesEx(rec, 2, {90, 160, 240, 255})
|
||||||
|
} else {
|
||||||
|
rl.DrawRectangleRec(rec, {16, 18, 30, 255})
|
||||||
|
rl.DrawRectangleLinesEx(rec, 1, {70, 76, 96, 255})
|
||||||
|
}
|
||||||
|
|
||||||
|
buf: [MAX_INPUT_LEN + 1]u8
|
||||||
|
cstr := to_cstring_buf(buf[:], text)
|
||||||
|
rl.DrawText(cstr, i32(rec.x) + 8, i32(rec.y) + 8, 18, rl.RAYWHITE)
|
||||||
|
|
||||||
|
if focused {
|
||||||
|
cw := rl.MeasureText(cstr, 18)
|
||||||
|
rl.DrawRectangle(i32(rec.x) + 8 + cw + 1, i32(rec.y) + 8, 2, 20, rl.SKYBLUE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ui_button_clicked :: proc(rec: rl.Rectangle) -> bool {
|
||||||
|
mouse := rl.GetMousePosition()
|
||||||
|
return rl.CheckCollisionPointRec(mouse, rec) && rl.IsMouseButtonPressed(.LEFT)
|
||||||
|
}
|
||||||
|
|
||||||
|
ui_button :: proc(rec: rl.Rectangle, label: string) {
|
||||||
|
mouse := rl.GetMousePosition()
|
||||||
|
hovered := rl.CheckCollisionPointRec(mouse, rec)
|
||||||
|
if hovered {
|
||||||
|
rl.DrawRectangleRec(rec, {48, 66, 118, 255})
|
||||||
|
} else {
|
||||||
|
rl.DrawRectangleRec(rec, {30, 38, 66, 255})
|
||||||
|
}
|
||||||
|
rl.DrawRectangleLinesEx(rec, 1, {92, 104, 134, 255})
|
||||||
|
|
||||||
|
buf: [128]u8
|
||||||
|
cstr := to_cstring_buf(buf[:], label)
|
||||||
|
w := rl.MeasureText(cstr, 18)
|
||||||
|
rl.DrawText(cstr, i32(rec.x) + (i32(rec.width) - w) / 2, i32(rec.y) + 10, 18, rl.RAYWHITE)
|
||||||
|
}
|
||||||
+50
-37
@@ -1,17 +1,17 @@
|
|||||||
module desi-explorer-infra
|
module desi-explorer-infra
|
||||||
|
|
||||||
go 1.26
|
go 1.26.6
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/pulumi/pulumi-kubernetes/sdk/v4 v4.31.1
|
github.com/pulumi/pulumi-kubernetes/sdk/v4 v4.34.0
|
||||||
github.com/pulumi/pulumi/sdk/v3 v3.244.0
|
github.com/pulumi/pulumi/sdk/v3 v3.261.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
dario.cat/mergo v1.0.0 // indirect
|
dario.cat/mergo v1.0.0 // indirect
|
||||||
github.com/BurntSushi/toml v1.6.0 // indirect
|
github.com/BurntSushi/toml v1.6.0 // indirect
|
||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
github.com/ProtonMail/go-crypto v1.1.6 // indirect
|
github.com/ProtonMail/go-crypto v1.4.1 // indirect
|
||||||
github.com/agext/levenshtein v1.2.3 // indirect
|
github.com/agext/levenshtein v1.2.3 // indirect
|
||||||
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
|
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
|
||||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
|
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
|
||||||
@@ -24,7 +24,7 @@ require (
|
|||||||
github.com/charmbracelet/bubbletea v1.3.10 // indirect
|
github.com/charmbracelet/bubbletea v1.3.10 // indirect
|
||||||
github.com/charmbracelet/colorprofile v0.4.3 // indirect
|
github.com/charmbracelet/colorprofile v0.4.3 // indirect
|
||||||
github.com/charmbracelet/lipgloss v1.1.0 // indirect
|
github.com/charmbracelet/lipgloss v1.1.0 // indirect
|
||||||
github.com/charmbracelet/x/ansi v0.11.6 // indirect
|
github.com/charmbracelet/x/ansi v0.11.8 // indirect
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
|
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
|
||||||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||||
github.com/cheggaaa/pb v1.0.29 // indirect
|
github.com/cheggaaa/pb v1.0.29 // indirect
|
||||||
@@ -32,34 +32,42 @@ require (
|
|||||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||||
github.com/cloudflare/circl v1.6.3 // indirect
|
github.com/cloudflare/circl v1.6.3 // indirect
|
||||||
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||||
|
github.com/danieljoos/wincred v1.2.3 // indirect
|
||||||
|
github.com/deckarep/golang-set/v2 v2.5.0 // indirect
|
||||||
github.com/djherbis/times v1.5.0 // indirect
|
github.com/djherbis/times v1.5.0 // indirect
|
||||||
|
github.com/ebitengine/purego v0.10.2 // indirect
|
||||||
github.com/emirpasic/gods v1.18.1 // indirect
|
github.com/emirpasic/gods v1.18.1 // indirect
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||||
|
github.com/go-git/gcfg/v2 v2.0.2 // indirect
|
||||||
github.com/go-git/go-billy/v5 v5.9.0 // indirect
|
github.com/go-git/go-billy/v5 v5.9.0 // indirect
|
||||||
|
github.com/go-git/go-billy/v6 v6.0.0-alpha.1 // indirect
|
||||||
github.com/go-git/go-git/v5 v5.19.1 // indirect
|
github.com/go-git/go-git/v5 v5.19.1 // indirect
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-git/go-git/v6 v6.0.0-alpha.4 // indirect
|
||||||
|
github.com/go-logr/logr v1.4.4 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
|
github.com/godbus/dbus/v5 v5.2.2 // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/golang/glog v1.2.5 // indirect
|
github.com/golang/glog v1.2.5 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||||
|
github.com/google/go-tpm v0.9.8 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 // indirect
|
||||||
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 // indirect
|
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 // indirect
|
||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
github.com/hashicorp/go-version v1.8.0 // indirect
|
github.com/hashicorp/go-version v1.9.0 // indirect
|
||||||
github.com/hashicorp/hcl/v2 v2.22.0 // indirect
|
github.com/hashicorp/hcl/v2 v2.24.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
github.com/kevinburke/ssh_config v1.6.0 // indirect
|
||||||
github.com/klauspost/compress v1.18.0 // indirect
|
github.com/klauspost/compress v1.18.7 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.4.1 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.24 // indirect
|
||||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.21 // indirect
|
github.com/mattn/go-runewidth v0.0.28 // indirect
|
||||||
github.com/mitchellh/go-ps v1.0.0 // indirect
|
github.com/mitchellh/go-ps v1.0.0 // indirect
|
||||||
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
|
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
@@ -87,33 +95,38 @@ require (
|
|||||||
github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect
|
github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect
|
||||||
github.com/uber/jaeger-lib v2.4.1+incompatible // indirect
|
github.com/uber/jaeger-lib v2.4.1+incompatible // indirect
|
||||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v1.0.0 // indirect
|
||||||
github.com/zclconf/go-cty v1.13.2 // indirect
|
github.com/zalando/go-keyring v0.2.8 // indirect
|
||||||
|
github.com/zclconf/go-cty v1.16.3 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
go.opentelemetry.io/collector/featuregate v1.53.0 // indirect
|
go.opentelemetry.io/collector/featuregate v1.65.0 // indirect
|
||||||
go.opentelemetry.io/collector/pdata v1.53.0 // indirect
|
go.opentelemetry.io/collector/pdata v1.65.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.43.0 // indirect
|
go.opentelemetry.io/contrib/bridges/otelslog v0.20.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 // indirect
|
go.opentelemetry.io/otel v1.45.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.21.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0 // indirect
|
||||||
go.opentelemetry.io/otel/sdk v1.43.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.45.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
go.opentelemetry.io/otel/log v0.21.0 // indirect
|
||||||
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
|
go.opentelemetry.io/otel/metric v1.45.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/sdk v1.45.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/sdk/log v0.21.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/trace v1.45.0 // indirect
|
||||||
|
go.opentelemetry.io/proto/otlp v1.11.0 // indirect
|
||||||
go.uber.org/atomic v1.11.0 // indirect
|
go.uber.org/atomic v1.11.0 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
golang.org/x/crypto v0.50.0 // indirect
|
golang.org/x/crypto v0.55.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
|
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
|
||||||
golang.org/x/mod v0.35.0 // indirect
|
golang.org/x/mod v0.38.0 // indirect
|
||||||
golang.org/x/net v0.53.0 // indirect
|
golang.org/x/net v0.58.0 // indirect
|
||||||
golang.org/x/sync v0.20.0 // indirect
|
golang.org/x/sync v0.22.0 // indirect
|
||||||
golang.org/x/sys v0.43.0 // indirect
|
golang.org/x/sys v0.47.0 // indirect
|
||||||
golang.org/x/term v0.42.0 // indirect
|
golang.org/x/term v0.45.0 // indirect
|
||||||
golang.org/x/text v0.36.0 // indirect
|
golang.org/x/text v0.41.0 // indirect
|
||||||
golang.org/x/tools v0.44.0 // indirect
|
golang.org/x/tools v0.48.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260311181403-84a4fc48630c // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20260819154853-08b0e4226688 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 // indirect
|
||||||
google.golang.org/grpc v1.80.0 // indirect
|
google.golang.org/grpc v1.83.1 // indirect
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
google.golang.org/protobuf v1.36.12 // indirect
|
||||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
lukechampine.com/frand v1.5.1 // indirect
|
lukechampine.com/frand v1.5.1 // indirect
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
|
|||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
|
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
|
||||||
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
||||||
|
github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM=
|
||||||
|
github.com/ProtonMail/go-crypto v1.4.1/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo=
|
||||||
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
|
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
|
||||||
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
|
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||||
@@ -39,6 +41,8 @@ github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoF
|
|||||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||||
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
|
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
|
||||||
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
|
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
|
||||||
|
github.com/charmbracelet/x/ansi v0.11.8 h1:JMFwp0CgDC2+jcOB162HH5k7I3FVbgFSMMYg7dSPBQQ=
|
||||||
|
github.com/charmbracelet/x/ansi v0.11.8/go.mod h1:ZNN+3mXny/516oTQPLMPIBeSINvNJJQ8uQXDgbeJxY0=
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
|
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
|
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
|
||||||
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||||
@@ -54,11 +58,17 @@ github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJ
|
|||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
|
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
|
||||||
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
|
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
|
||||||
|
github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ=
|
||||||
|
github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/deckarep/golang-set/v2 v2.5.0 h1:hn6cEZtQ0h3J8kFrHR/NrzyOoTnjgW1+FmNJzQ7y/sA=
|
||||||
|
github.com/deckarep/golang-set/v2 v2.5.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
|
||||||
github.com/djherbis/times v1.5.0 h1:79myA211VwPhFTqUk8xehWrsEO+zcIZj0zT8mXPVARU=
|
github.com/djherbis/times v1.5.0 h1:79myA211VwPhFTqUk8xehWrsEO+zcIZj0zT8mXPVARU=
|
||||||
github.com/djherbis/times v1.5.0/go.mod h1:5q7FDLvbNg1L/KaBmPcWlVR9NmoKo3+ucqUA3ijQhA0=
|
github.com/djherbis/times v1.5.0/go.mod h1:5q7FDLvbNg1L/KaBmPcWlVR9NmoKo3+ucqUA3ijQhA0=
|
||||||
|
github.com/ebitengine/purego v0.10.2 h1:W809HbnvzAxgdm+aOvlSekrM16wGCdT/e76+9tS7gzE=
|
||||||
|
github.com/ebitengine/purego v0.10.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||||
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
||||||
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
|
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
|
||||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||||
@@ -72,17 +82,27 @@ github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
|||||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||||
|
github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo=
|
||||||
|
github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs=
|
||||||
github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA=
|
github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA=
|
||||||
github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw=
|
github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw=
|
||||||
|
github.com/go-git/go-billy/v6 v6.0.0-alpha.1 h1:xVjAR4oUvrKy7/Xuw/lLlV3gkxR3KO2H8W+MamuVVsQ=
|
||||||
|
github.com/go-git/go-billy/v6 v6.0.0-alpha.1/go.mod h1:eaCUpHbedW7//EwcYmUDfJe2N6sJC9O12AT0OTqJR1E=
|
||||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||||
github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00=
|
github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00=
|
||||||
github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ=
|
github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ=
|
||||||
|
github.com/go-git/go-git/v6 v6.0.0-alpha.4 h1:aDTc2UGanmaE7FkGLSlBEB9nohMnQ+RKXcfq/D+esDQ=
|
||||||
|
github.com/go-git/go-git/v6 v6.0.0-alpha.4/go.mod h1:4ODa/G7hPWrh4Y+7lmt59Ij3zW38IEfvRoAZxLYYBhc=
|
||||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
|
||||||
|
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
|
||||||
|
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
||||||
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
@@ -94,11 +114,15 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek
|
|||||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||||
|
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 h1:/Tnpcb2E0Pz/tN9s3bfEY2Q8ePCEX9iuS+cneUwncnw=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0/go.mod h1:zOBXOsUaBSjKgmH4OGzV1esUpR3oUSCPYVd2cUBjKYY=
|
||||||
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1:MJG/KsmcqMwFAkh8mTnAwhyKoB+sTAnY4CACC110tbU=
|
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1:MJG/KsmcqMwFAkh8mTnAwhyKoB+sTAnY4CACC110tbU=
|
||||||
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw=
|
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw=
|
||||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
@@ -108,8 +132,12 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l
|
|||||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||||
github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=
|
github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=
|
||||||
github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||||
|
github.com/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA=
|
||||||
|
github.com/hashicorp/go-version v1.9.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||||
github.com/hashicorp/hcl/v2 v2.22.0 h1:hkZ3nCtqeJsDhPRFz5EA9iwcG1hNWGePOTw6oyul12M=
|
github.com/hashicorp/hcl/v2 v2.22.0 h1:hkZ3nCtqeJsDhPRFz5EA9iwcG1hNWGePOTw6oyul12M=
|
||||||
github.com/hashicorp/hcl/v2 v2.22.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA=
|
github.com/hashicorp/hcl/v2 v2.22.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA=
|
||||||
|
github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE=
|
||||||
|
github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||||
@@ -118,11 +146,15 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
|
|||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
|
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
|
||||||
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||||
|
github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY=
|
||||||
|
github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
|
||||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
|
github.com/klauspost/compress v1.18.7 h1:aUyZsS4kH3QTKurYhAOwAHxllVPnOthb3vPfnF1Ehjw=
|
||||||
|
github.com/klauspost/compress v1.18.7/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
@@ -134,6 +166,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.4.1 h1:1EO+WB73+EH8EVbzlrG3KLAfEypQWVHIBqlTf+2hNss=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.4.1/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
@@ -141,11 +175,15 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
|
|||||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
|
||||||
|
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
|
||||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||||
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||||
github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w=
|
github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w=
|
||||||
github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||||
|
github.com/mattn/go-runewidth v0.0.28 h1:rPyg2ybwEKPebvpzVWe1gKBkH8EQFkxO4Y0hjBeLaBU=
|
||||||
|
github.com/mattn/go-runewidth v0.0.28/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8=
|
||||||
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
|
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
|
||||||
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
|
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
|
||||||
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
|
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
|
||||||
@@ -187,8 +225,12 @@ github.com/pulumi/esc v0.24.0 h1:sCtiB0qbyrlU1ZNzJn4dTLYiChl8xeCBFbHWl1YoXJg=
|
|||||||
github.com/pulumi/esc v0.24.0/go.mod h1:eCOOkcDJS6eooGwdE4/E0+pOsvUWG254+KBmPCFwJpA=
|
github.com/pulumi/esc v0.24.0/go.mod h1:eCOOkcDJS6eooGwdE4/E0+pOsvUWG254+KBmPCFwJpA=
|
||||||
github.com/pulumi/pulumi-kubernetes/sdk/v4 v4.31.1 h1:Hg9RK9zqIU9kFbD5KeiON06gPP7cLgS68jvsgMBmPgw=
|
github.com/pulumi/pulumi-kubernetes/sdk/v4 v4.31.1 h1:Hg9RK9zqIU9kFbD5KeiON06gPP7cLgS68jvsgMBmPgw=
|
||||||
github.com/pulumi/pulumi-kubernetes/sdk/v4 v4.31.1/go.mod h1:BAWI9R3JEEGOp1JlXLPSZKwBGANSrPGUWKtMnS5w5qw=
|
github.com/pulumi/pulumi-kubernetes/sdk/v4 v4.31.1/go.mod h1:BAWI9R3JEEGOp1JlXLPSZKwBGANSrPGUWKtMnS5w5qw=
|
||||||
|
github.com/pulumi/pulumi-kubernetes/sdk/v4 v4.34.0 h1:r00p17ecoDVWqBg38/Is/9jU/JsWLPxnWzhfYqKitx0=
|
||||||
|
github.com/pulumi/pulumi-kubernetes/sdk/v4 v4.34.0/go.mod h1:/klrd3ib4AMRlFMjrxkv5r76x3Bdr68wIMEKfRkRzHU=
|
||||||
github.com/pulumi/pulumi/sdk/v3 v3.244.0 h1:oyQ9bwDE58wrQOqS70JrojJSlNoLcVhVcQgK4yusgcg=
|
github.com/pulumi/pulumi/sdk/v3 v3.244.0 h1:oyQ9bwDE58wrQOqS70JrojJSlNoLcVhVcQgK4yusgcg=
|
||||||
github.com/pulumi/pulumi/sdk/v3 v3.244.0/go.mod h1:BPWWuYPXcPH5YbXGoyy9Rrfa+evrh6IdM51AjDhcDpM=
|
github.com/pulumi/pulumi/sdk/v3 v3.244.0/go.mod h1:BPWWuYPXcPH5YbXGoyy9Rrfa+evrh6IdM51AjDhcDpM=
|
||||||
|
github.com/pulumi/pulumi/sdk/v3 v3.261.0 h1:1C2V6bxzO9UBe91PRebO/zeiUIQ2C05eRNkVFvJuuqc=
|
||||||
|
github.com/pulumi/pulumi/sdk/v3 v3.261.0/go.mod h1:xlOo55i2hj5tDyMY8diTS5LfFAn2sFew+50d67sVNdg=
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
@@ -225,34 +267,66 @@ github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM
|
|||||||
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
|
github.com/xo/terminfo v1.0.0 h1:2ZpYzqWzyyytjk3TP6aJVDhkMAkc99/1xKQdA3TDTBY=
|
||||||
|
github.com/xo/terminfo v1.0.0/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/zalando/go-keyring v0.2.8 h1:6sD/Ucpl7jNq10rM2pgqTs0sZ9V3qMrqfIIy5YPccHs=
|
||||||
|
github.com/zalando/go-keyring v0.2.8/go.mod h1:tsMo+VpRq5NGyKfxoBVjCuMrG47yj8cmakZDO5QGii0=
|
||||||
github.com/zclconf/go-cty v1.13.2 h1:4GvrUxe/QUDYuJKAav4EYqdM47/kZa672LwmXFmEKT0=
|
github.com/zclconf/go-cty v1.13.2 h1:4GvrUxe/QUDYuJKAav4EYqdM47/kZa672LwmXFmEKT0=
|
||||||
github.com/zclconf/go-cty v1.13.2/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0=
|
github.com/zclconf/go-cty v1.13.2/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0=
|
||||||
|
github.com/zclconf/go-cty v1.16.3 h1:osr++gw2T61A8KVYHoQiFbFd1Lh3JOCXc/jFLJXKTxk=
|
||||||
|
github.com/zclconf/go-cty v1.16.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
go.opentelemetry.io/collector/featuregate v1.53.0 h1:cgjXdtl7jezWxq6V0eohe/JqjY4PBotZGb5+bTR2OJw=
|
go.opentelemetry.io/collector/featuregate v1.53.0 h1:cgjXdtl7jezWxq6V0eohe/JqjY4PBotZGb5+bTR2OJw=
|
||||||
go.opentelemetry.io/collector/featuregate v1.53.0/go.mod h1:PS7zY/zaCb28EqciePVwRHVhc3oKortTFXsi3I6ee4g=
|
go.opentelemetry.io/collector/featuregate v1.53.0/go.mod h1:PS7zY/zaCb28EqciePVwRHVhc3oKortTFXsi3I6ee4g=
|
||||||
|
go.opentelemetry.io/collector/featuregate v1.65.0 h1:Dh+uYVB+POc5DTebZRWjtKJolGhevkiIpbHn+zhkq2o=
|
||||||
|
go.opentelemetry.io/collector/featuregate v1.65.0/go.mod h1:4ga1QBMPEejXXmpyJS8lmaRpknJ3Lb9Bvk6e420bUFU=
|
||||||
go.opentelemetry.io/collector/internal/testutil v0.147.0 h1:DFlRxBRp23/sZnpTITK25yqe0d56yNvK+63IaWc6OsU=
|
go.opentelemetry.io/collector/internal/testutil v0.147.0 h1:DFlRxBRp23/sZnpTITK25yqe0d56yNvK+63IaWc6OsU=
|
||||||
go.opentelemetry.io/collector/internal/testutil v0.147.0/go.mod h1:Jkjs6rkqs973LqgZ0Fe3zrokQRKULYXPIf4HuqStiEE=
|
go.opentelemetry.io/collector/internal/testutil v0.147.0/go.mod h1:Jkjs6rkqs973LqgZ0Fe3zrokQRKULYXPIf4HuqStiEE=
|
||||||
go.opentelemetry.io/collector/pdata v1.53.0 h1:DlYDbRwammEZaxDZHINx5v0n8SEOVNniPbi6FRTlVkA=
|
go.opentelemetry.io/collector/pdata v1.53.0 h1:DlYDbRwammEZaxDZHINx5v0n8SEOVNniPbi6FRTlVkA=
|
||||||
go.opentelemetry.io/collector/pdata v1.53.0/go.mod h1:LRSYGNjKXaUrZEwZv3Yl+8/zV2HmRGKXW62zB2bysms=
|
go.opentelemetry.io/collector/pdata v1.53.0/go.mod h1:LRSYGNjKXaUrZEwZv3Yl+8/zV2HmRGKXW62zB2bysms=
|
||||||
|
go.opentelemetry.io/collector/pdata v1.65.0 h1:6bQ3sIrEzOdapetxYFjdCns90kKXg1qCoIZ3la1aR5E=
|
||||||
|
go.opentelemetry.io/collector/pdata v1.65.0/go.mod h1:r5vRY0p7nZcEif06twUW09Sf6vaNsyPzij+EpwI/xeI=
|
||||||
|
go.opentelemetry.io/contrib/bridges/otelslog v0.20.0 h1:oEl2Pw/i4OQwhAuda2pAHFAcOMivA+Xa+iTccBfab/g=
|
||||||
|
go.opentelemetry.io/contrib/bridges/otelslog v0.20.0/go.mod h1:yMSQaiiq5dpfrSJCYLBcqFeJkFFI67seT4ngvx6jfVo=
|
||||||
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||||
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||||
|
go.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU=
|
||||||
|
go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.21.0 h1:WseeVYf5dJZTsyPiyW5L14k5qsSibqXAMTSiFEDiWr0=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.21.0/go.mod h1:SiLZnQS6Qk2eCpvr2CH/XMAOa64TWGXxEZJZCpD2Lmc=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0/go.mod h1:J2pvYM5NGHofZ2/Ru6zw/TNWnEQp5crgyDeSrYpXkAw=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0/go.mod h1:J2pvYM5NGHofZ2/Ru6zw/TNWnEQp5crgyDeSrYpXkAw=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0 h1:QRefszxJmfPdjXUUm3j6iDzY03mTPXMjqErFqQ67vUg=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0/go.mod h1:Tiz03lTBVBrm7eWZBOidzEaYaJa8tjwGUGv6d8mlTyk=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 h1:zWWrB1U6nqhS/k6zYB74CjRpuiitRtLLi68VcgmOEto=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 h1:zWWrB1U6nqhS/k6zYB74CjRpuiitRtLLi68VcgmOEto=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0/go.mod h1:2qXPNBX1OVRC0IwOnfo1ljoid+RD0QK3443EaqVlsOU=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0/go.mod h1:2qXPNBX1OVRC0IwOnfo1ljoid+RD0QK3443EaqVlsOU=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.45.0 h1:fG5MCxGz8+2VtrN/WgqSpJFctVz24gpxj8CxkKmc8Ww=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.45.0/go.mod h1:BmAYTn+3ysbRe+IU2msxmf5Rx3g6DHvex+tWI3LdhYI=
|
||||||
|
go.opentelemetry.io/otel/log v0.21.0 h1:SLsVDGmtyBrdw8/a2Z0bOIxou/+bN4z56GebH7T0LvA=
|
||||||
|
go.opentelemetry.io/otel/log v0.21.0/go.mod h1:iReetQrZL9Wyg84cCkOoCmqDHS5RCFfyxC7J+r8fn8g=
|
||||||
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||||
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||||
|
go.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M=
|
||||||
|
go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s=
|
||||||
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||||
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.45.0 h1:4VVSMgQ83dUgW2aoX5f6JgLvHwIvzcuLnF9lUdCSpCw=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.45.0/go.mod h1:Sr40LgXV7DsKMMJMKOhUWOgMWTfAaqvm2kF0g7ilwuA=
|
||||||
|
go.opentelemetry.io/otel/sdk/log v0.21.0 h1:QsE7XSR0ktQdKmRKGnR+f1ObGF32WG+7MER/P9KgmYc=
|
||||||
|
go.opentelemetry.io/otel/sdk/log v0.21.0/go.mod h1:m9mApjCoD2/1QuKCAptjv+BrG9WKOvQLVdNx+iBldTo=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||||
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||||
|
go.opentelemetry.io/otel/trace v1.45.0 h1:l/mP6Uv7oNO7/TblbhpbgMidxhq1uO/rPsikOyVhxag=
|
||||||
|
go.opentelemetry.io/otel/trace v1.45.0/go.mod h1:qoJJA2xNMnxRrdISU/kLtfUH2wNeQbiv+jhs/CxI8bc=
|
||||||
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
||||||
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E=
|
||||||
go.opentelemetry.io/proto/slim/otlp v1.9.0 h1:fPVMv8tP3TrsqlkH1HWYUpbCY9cAIemx184VGkS6vlE=
|
go.opentelemetry.io/proto/slim/otlp v1.9.0 h1:fPVMv8tP3TrsqlkH1HWYUpbCY9cAIemx184VGkS6vlE=
|
||||||
go.opentelemetry.io/proto/slim/otlp v1.9.0/go.mod h1:xXdeJJ90Gqyll+orzUkY4bOd2HECo5JofeoLpymVqdI=
|
go.opentelemetry.io/proto/slim/otlp v1.9.0/go.mod h1:xXdeJJ90Gqyll+orzUkY4bOd2HECo5JofeoLpymVqdI=
|
||||||
go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.2.0 h1:o13nadWDNkH/quoDomDUClnQBpdQQ2Qqv0lQBjIXjE8=
|
go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.2.0 h1:o13nadWDNkH/quoDomDUClnQBpdQQ2Qqv0lQBjIXjE8=
|
||||||
@@ -272,6 +346,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
|||||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||||
|
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
|
||||||
|
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
|
||||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
||||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
|
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
|
||||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||||
@@ -280,6 +356,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
|
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
|
||||||
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
||||||
|
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
|
||||||
|
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
@@ -288,11 +366,15 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
|
|||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||||
|
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
|
||||||
|
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
|
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||||
|
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -309,14 +391,20 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||||
|
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
|
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
|
||||||
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
|
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
|
||||||
|
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||||
|
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||||
|
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
|
||||||
|
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
@@ -325,6 +413,8 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY
|
|||||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
|
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
|
||||||
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
|
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
|
||||||
|
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
|
||||||
|
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
@@ -333,12 +423,20 @@ gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
|||||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260311181403-84a4fc48630c h1:OyQPd6I3pN/9gDxz6L13kYGJgqkpdrAohJRBeXyxlgI=
|
google.golang.org/genproto/googleapis/api v0.0.0-20260311181403-84a4fc48630c h1:OyQPd6I3pN/9gDxz6L13kYGJgqkpdrAohJRBeXyxlgI=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260311181403-84a4fc48630c/go.mod h1:X2gu9Qwng7Nn009s/r3RUxqkzQNqOrAy79bluY7ojIg=
|
google.golang.org/genproto/googleapis/api v0.0.0-20260311181403-84a4fc48630c/go.mod h1:X2gu9Qwng7Nn009s/r3RUxqkzQNqOrAy79bluY7ojIg=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20260819154853-08b0e4226688 h1:ax2KzoSRIZU/M0cIxri3pKxy99vniH1PVxWC6si/eZI=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20260819154853-08b0e4226688/go.mod h1:1RJ9BQGyNdZwkGc1eTqkErfRZ6RJyYPHZo73BZ1vQqI=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 h1:cYNAzI2sUwhmCcoj9TxvihSrqsxt6uIkj3rDRhSDmW4=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688/go.mod h1:DjtHYE8FKJLivXcBEjGwndXfIC23G0VpXiXKqG179uA=
|
||||||
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
|
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
|
||||||
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
|
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
|
||||||
|
google.golang.org/grpc v1.83.1 h1:HIO0+BEtBP6soyqvqC8sNUjZ7bTs+0hFQuFF+RAy++Y=
|
||||||
|
google.golang.org/grpc v1.83.1/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
|
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
|
||||||
|
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
|||||||
@@ -59,11 +59,13 @@ get_catalog_objects :: proc(url: string, catalog_name: string) // nil
|
|||||||
|
|
||||||
## Data flow gap
|
## Data flow gap
|
||||||
|
|
||||||
```
|
```mermaid
|
||||||
[DESI catalog store] --(future)--> [Rust/axum API] --(nothing today)--> [Odin + raylib GUI]
|
flowchart LR
|
||||||
^ ^
|
A["DESI catalog store"]
|
||||||
| serde JSON models | hand-mirrored structs
|
B["Rust / axum API<br/><i>serde JSON models</i>"]
|
||||||
| | (stubs, never used)
|
C["Odin + raylib GUI<br/><i>hand-mirrored structs<br/>stubs, never used</i>"]
|
||||||
|
A -. "future ingestion" .-> B
|
||||||
|
B --x|"nothing today"| C
|
||||||
```
|
```
|
||||||
|
|
||||||
There is **no live data flow**. The API currently returns JSON placeholders; the
|
There is **no live data flow**. The API currently returns JSON placeholders; the
|
||||||
@@ -116,17 +118,16 @@ response protocol with a cheap binary payload would fit this well.
|
|||||||
|
|
||||||
## High-level target architecture
|
## High-level target architecture
|
||||||
|
|
||||||
```
|
```mermaid
|
||||||
catalog.fbs (single source of truth, checked into repo)
|
flowchart TB
|
||||||
|
|
S["catalog.fbs<br/><i>single source of truth<br/>checked into repo</i>"]
|
||||||
+--------+---------+
|
R["flatc --rust"]
|
||||||
| |
|
C["flatcc --c<br/><i>or hand-rolled Odin reader</i>"]
|
||||||
flatc --rust flatcc --c (or hand-rolled Odin reader)
|
API["api/ · Rust"]
|
||||||
| |
|
GUI["gui/ · Odin + raylib"]
|
||||||
api/ (Rust) gui/ (Odin + raylib)
|
S --> R --> API
|
||||||
| ^
|
S --> C --> GUI
|
||||||
| HTTP / WebSocket (framed FlatBuffer binary stream)
|
API <-->|"HTTP / WebSocket<br/>framed FlatBuffer binary stream"| GUI
|
||||||
+------------------+
|
|
||||||
```
|
```
|
||||||
|
|
||||||
- One schema file. Two generators. Byte-for-byte identical wire format.
|
- One schema file. Two generators. Byte-for-byte identical wire format.
|
||||||
|
|||||||
@@ -105,19 +105,28 @@ These are the "thou shalt" rules for keeping buffers compatible:
|
|||||||
|
|
||||||
## Reading a buffer (conceptual)
|
## Reading a buffer (conceptual)
|
||||||
|
|
||||||
|
Buffer layout:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
subgraph BUF["bytes: &[u8]"]
|
||||||
|
O["uoffset<br/>root table offset"]
|
||||||
|
FI["file_identifier<br/>(optional)"]
|
||||||
|
D["tables · vtables · data"]
|
||||||
|
end
|
||||||
|
O --> FI --> D
|
||||||
```
|
```
|
||||||
bytes: &[u8]
|
|
||||||
|
|
||||||
┌─────────────────────────────┐
|
Access sequence — each field is a few offset dereferences and a read:
|
||||||
│ uoffset (root table offset) │
|
|
||||||
│ file_identifier (optional) │
|
|
||||||
│ ... tables, vtables, data ...│
|
|
||||||
└─────────────────────────────┘
|
|
||||||
|
|
||||||
root = follow(bytes) // jump to root table via uoffset
|
```mermaid
|
||||||
vtable = root - root.vtable_off // locate vtable for this table
|
flowchart TD
|
||||||
field_ra = vtable.slot_ra != 0 // present?
|
R["root = follow(bytes)<br/>jump to root table via uoffset"]
|
||||||
if present: ra = read_f64(bytes, root + slot_ra)
|
V["vtable = root − root.vtable_off<br/>locate vtable for this table"]
|
||||||
|
Q{"field slot present?"}
|
||||||
|
R --> V --> Q
|
||||||
|
Q -- "no" --> DEF["use schema default"]
|
||||||
|
Q -- "yes" --> RD["ra = read_f64(bytes, root + slot_ra)"]
|
||||||
```
|
```
|
||||||
|
|
||||||
There is **no parsing loop**. Each accessor is a few offset dereferences and a
|
There is **no parsing loop**. Each accessor is a few offset dereferences and a
|
||||||
|
|||||||
@@ -196,6 +196,18 @@ Notes:
|
|||||||
|
|
||||||
## Building buffers efficiently (Rust specifics)
|
## Building buffers efficiently (Rust specifics)
|
||||||
|
|
||||||
|
Build order is **back-to-front** (children before parents):
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
A["create_string / create_vector<br/>children first"]
|
||||||
|
B["create nested child tables"]
|
||||||
|
C["create parent table<br/>ObjectBatch::create(&args)"]
|
||||||
|
D["builder.finish(root, Some("DESI"))"]
|
||||||
|
E["Bytes::copy_from_slice(fbb.finished_data())<br/>→ HTTP / WebSocket response"]
|
||||||
|
A --> B --> C --> D --> E
|
||||||
|
```
|
||||||
|
|
||||||
- `FlatBufferBuilder::with_capacity(n)` pre-allocates; `reset()` reuses the
|
- `FlatBufferBuilder::with_capacity(n)` pre-allocates; `reset()` reuses the
|
||||||
buffer across messages. In a loop streaming batches, create one builder, reuse
|
buffer across messages. In a loop streaming batches, create one builder, reuse
|
||||||
it — avoid repeated reallocation.
|
it — avoid repeated reallocation.
|
||||||
|
|||||||
@@ -17,6 +17,21 @@ has two C-related access points:
|
|||||||
schema plus a small `libflatccrt.a` runtime. Works via the C ABI, so Odin's
|
schema plus a small `libflatccrt.a` runtime. Works via the C ABI, so Odin's
|
||||||
`foreign import` can consume it.
|
`foreign import` can consume it.
|
||||||
|
|
||||||
|
Choosing a path:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
NAT{"primary target is native desktop?"}
|
||||||
|
NAT -- "yes" --> CGO{"want to avoid C in the build?"}
|
||||||
|
CGO -- "yes" --> PATHA["Path B · pure-Odin reader<br/>hand-rolled, no C dependency"]
|
||||||
|
CGO -- "no" --> PATHA
|
||||||
|
CGO -- "prefer proven lib / less maintenance" --> PATHC["Path A · FFI to FlatCC<br/>bind generated C headers"]
|
||||||
|
PATHC --> REUSE["Path C · OdinArrow reuse<br/>or borrow its decode patterns"]
|
||||||
|
NAT -- "no · browser/WASM" --> PATHD["Path D · TS/JS interop<br/>official JS lib → typed arrays into WASM"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Index of paths:
|
||||||
|
|
||||||
| Path | Effort | Zero-copy on reads | Notes |
|
| Path | Effort | Zero-copy on reads | Notes |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| A: FFI to FlatCC (C runtime) | Medium | ✅ | Bind generated C headers to Odin `foreign` |
|
| A: FFI to FlatCC (C runtime) | Medium | ✅ | Bind generated C headers to Odin `foreign` |
|
||||||
@@ -175,12 +190,14 @@ typed Odin slices.
|
|||||||
The rendering win only materializes if data stays zero-copy **into the frame
|
The rendering win only materializes if data stays zero-copy **into the frame
|
||||||
loop**:
|
loop**:
|
||||||
|
|
||||||
1. Fetch frame bytes → owned `[dynamic]u8` (or a slice pinned for the lifetime
|
```mermaid
|
||||||
of the frame).
|
flowchart LR
|
||||||
2. `verify` the buffer once.
|
A["fetch frame bytes → [dynamic]u8<br/>or a slice pinned for the frame"]
|
||||||
3. Get `ra_slice := ObjectBatch.ra(&buf)` → `[]f64` view.
|
B["verify the buffer once"]
|
||||||
4. Per object in `update()`/`draw()`: read `ra[i]`, `dec[i]`, `z[i]` straight
|
C["ObjectBatch.ra(&buf) → []f64 view"]
|
||||||
from that slice; build `rl.Vector3`; `DrawPoint3D`.
|
D["per object in update()/draw()<br/>ra[i] · dec[i] · z[i] → rl.Vector3 → DrawPoint3D"]
|
||||||
|
A --> B --> C --> D
|
||||||
|
```
|
||||||
|
|
||||||
No per-object allocation. The current `Galaxy { position, color }` dynamic array
|
No per-object allocation. The current `Galaxy { position, color }` dynamic array
|
||||||
in `main.odin` is the data structure you'd replace with *slices into the
|
in `main.odin` is the data structure you'd replace with *slices into the
|
||||||
|
|||||||
@@ -24,14 +24,17 @@ offers two built-in mechanisms plus the community pattern:
|
|||||||
|
|
||||||
### Option 1: Size-prefixed FlatBuffers (built-in)
|
### Option 1: Size-prefixed FlatBuffers (built-in)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
A["u32 LE<br/>total buffer len<br/><i>size prefix</i>"]
|
||||||
|
B["u32 LE<br/>root table offset"]
|
||||||
|
C["file identifier<br/>(4 bytes)"]
|
||||||
|
D["tables · vtables · data"]
|
||||||
|
A --> B --> C --> D
|
||||||
|
```
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
builder.finish_size_prefixed(root, Some("DESI"));
|
builder.finish_size_prefixed(root, Some("DESI"));
|
||||||
// +---------------------------+
|
|
||||||
// | u32 LE: total buffer len | <-- size prefix
|
|
||||||
// | u32 LE: root table offset |
|
|
||||||
// | file identifier (4 bytes) |
|
|
||||||
// | ... data ... |
|
|
||||||
// +---------------------------+
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Reader side:
|
Reader side:
|
||||||
@@ -53,8 +56,12 @@ message kinds).
|
|||||||
|
|
||||||
### Option 2: Custom length-prefix framing (like `flatstream`)
|
### Option 2: Custom length-prefix framing (like `flatstream`)
|
||||||
|
|
||||||
```
|
```mermaid
|
||||||
[ u32 LE: message_len ] [ optional checksum (e.g. u32 crc/xxhash) ] [ flatbuffer payload ]
|
flowchart LR
|
||||||
|
A["u32 LE<br/>message_len"]
|
||||||
|
B["optional checksum<br/>(u32 crc / xxhash)"]
|
||||||
|
C["FlatBuffer payload"]
|
||||||
|
A --> B --> C
|
||||||
```
|
```
|
||||||
|
|
||||||
- `flatstream-rs` (see `03-rust-integration.md`) is a reference implementation
|
- `flatstream-rs` (see `03-rust-integration.md`) is a reference implementation
|
||||||
@@ -260,6 +267,16 @@ FlatBuffers long-term (mmap-friendly, page-in-what-you-touch).
|
|||||||
|
|
||||||
## Decision summary for this repo
|
## Decision summary for this repo
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A["HTTP GET → one FlatBuffer body per batch<br/>validate Rust builder + Odin reader"]
|
||||||
|
B["WebSocket → one Binary message per batch<br/>interactive path · no custom framing"]
|
||||||
|
C["Self-identifying messages<br/>file_identifier "DESI""]
|
||||||
|
D["size-prefixed / flatstream-style framing<br/>or HTTP-range + mmap for static catalogs"]
|
||||||
|
A --> B --> C
|
||||||
|
C -. "later, if needed" .-> D
|
||||||
|
```
|
||||||
|
|
||||||
1. Start with **HTTP GET → one FlatBuffer body per batch** to validate the Rust
|
1. Start with **HTTP GET → one FlatBuffer body per batch** to validate the Rust
|
||||||
builder + Odin reader (no protocol work at all).
|
builder + Odin reader (no protocol work at all).
|
||||||
2. Then add **WebSocket** with one `Binary` message per batch (no custom framing)
|
2. Then add **WebSocket** with one `Binary` message per batch (no custom framing)
|
||||||
|
|||||||
@@ -108,6 +108,15 @@ message shape — see `10-performance-benchmarks.md`.)
|
|||||||
|
|
||||||
## Bottom line
|
## Bottom line
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
Q1{"zero-copy reads<br/>in the per-frame render loop?"}
|
||||||
|
Q1 -- "no" --> PB["Protobuf / gRPC<br/>decode once into draw buffers"]
|
||||||
|
Q1 -- "yes" --> Q2{"truly columnar?<br/>millions of rows"}
|
||||||
|
Q2 -- "yes" --> ARR["Apache Arrow IPC<br/>via OdinArrow"]
|
||||||
|
Q2 -- "no · batched vectors" --> FB["FlatBuffers · this proposal"]
|
||||||
|
```
|
||||||
|
|
||||||
- **FlatBuffers is the best default** for this project: the zero-copy read model
|
- **FlatBuffers is the best default** for this project: the zero-copy read model
|
||||||
matches the render loop, the wire format is compact for numeric vectors, schema
|
matches the render loop, the wire format is compact for numeric vectors, schema
|
||||||
evolution fits DESI's release cadence, and the Rust + WASM/JS official story
|
evolution fits DESI's release cadence, and the Rust + WASM/JS official story
|
||||||
|
|||||||
@@ -139,7 +139,23 @@ side / flatc* — see the "cross-language fixture" section below.
|
|||||||
|
|
||||||
### 4. Cross-language conformance suite (THE key integration test)
|
### 4. Cross-language conformance suite (THE key integration test)
|
||||||
|
|
||||||
This is the test that actually catches incompatibility. Design:
|
This is the test that actually catches incompatibility. Pipeline:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
S["schema/catalog.fbs"]
|
||||||
|
J["testdata/catalog_sample.json"]
|
||||||
|
S --> F["flatc --binary"]
|
||||||
|
J --> F
|
||||||
|
F --> BIN["committed .bin fixtures<br/>repo-checked-in"]
|
||||||
|
BIN --> OT["Odin tests<br/>assert identical values"]
|
||||||
|
BIN --> RT["Rust tests<br/>assert expected values"]
|
||||||
|
RT -. "deterministic builder" .-> PAR["byte-for-byte parity"]
|
||||||
|
OT -. "reads it" .-> PAR
|
||||||
|
PAR -. "catch drift" .-> F
|
||||||
|
```
|
||||||
|
|
||||||
|
Design:
|
||||||
|
|
||||||
1. **Static fixtures, committed to the repo** (`testdata/*.bin`):
|
1. **Static fixtures, committed to the repo** (`testdata/*.bin`):
|
||||||
- Built once by `flatc --binary <schema>.fbs <data>.json` (deterministic,
|
- Built once by `flatc --binary <schema>.fbs <data>.json` (deterministic,
|
||||||
|
|||||||
@@ -70,6 +70,16 @@ directly from the buffer each frame without allocations.
|
|||||||
|
|
||||||
## Immediate Next Steps (when you're ready to implement)
|
## Immediate Next Steps (when you're ready to implement)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A["Prototype schema<br/>catalog.fbs: Catalog · CatalogObject · ServerMessage union"]
|
||||||
|
B["Generate Rust code<br/>flatc --rust → api/build.rs · serve WS via axum"]
|
||||||
|
C["Prototype the Odin reader<br/>flatcc FFI · pure-Odin · OdinArrow"]
|
||||||
|
D["Static fixture files<br/>flatc --binary → committed .bin"]
|
||||||
|
E["Cross-language tests<br/>Rust + Odin read the same fixtures identically"]
|
||||||
|
A --> B --> C --> D --> E
|
||||||
|
```
|
||||||
|
|
||||||
1. **Prototype schema first.** Write `catalog.fbs` covering `Catalog`, `CatalogObject`,
|
1. **Prototype schema first.** Write `catalog.fbs` covering `Catalog`, `CatalogObject`,
|
||||||
and a `ServerMessage` union (handshake / catalog list / chunk of objects / end).
|
and a `ServerMessage` union (handshake / catalog list / chunk of objects / end).
|
||||||
2. **Generate Rust code** via `flatc --rust` in an `api/build.rs` (see
|
2. **Generate Rust code** via `flatc --rust` in an `api/build.rs` (see
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Example API environment — passed via API_ENV_FILE (see root Makefile).
|
||||||
|
#
|
||||||
|
# The API also reads API_DESI_DATA from the environment (set by the root
|
||||||
|
# Makefile to resources/dev/desi_subset.json by default), so it is not
|
||||||
|
# repeated here. Values here win unless the same key is already set in the
|
||||||
|
# real process environment.
|
||||||
|
|
||||||
|
API_BIND_ADDR=127.0.0.1:8080
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
{
|
||||||
|
"catalogs": [
|
||||||
|
{
|
||||||
|
"name": "edr",
|
||||||
|
"release": "EDR",
|
||||||
|
"description": "DESI Early Data Release (local dev subset)",
|
||||||
|
"object_count": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "dr1",
|
||||||
|
"release": "DR1",
|
||||||
|
"description": "DESI Data Release 1 (local dev subset)",
|
||||||
|
"object_count": 2
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"objects": [
|
||||||
|
{
|
||||||
|
"id": "DESI_EDR_000000001",
|
||||||
|
"catalog": "edr",
|
||||||
|
"object_type": "GALAXY",
|
||||||
|
"ra": 150.123456,
|
||||||
|
"dec": 2.345678,
|
||||||
|
"redshift": 0.5521
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "DESI_EDR_000000002",
|
||||||
|
"catalog": "edr",
|
||||||
|
"object_type": "GALAXY",
|
||||||
|
"ra": 254.987654,
|
||||||
|
"dec": -15.203041,
|
||||||
|
"redshift": 1.1045
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "DESI_EDR_000000003",
|
||||||
|
"catalog": "edr",
|
||||||
|
"object_type": "QSO",
|
||||||
|
"ra": 75.001234,
|
||||||
|
"dec": 38.765432,
|
||||||
|
"redshift": 2.8756
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "DESI_DR1_000000001",
|
||||||
|
"catalog": "dr1",
|
||||||
|
"object_type": "STAR",
|
||||||
|
"ra": 188.556677,
|
||||||
|
"dec": 47.112233,
|
||||||
|
"redshift": 0.0001
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "DESI_DR1_000000002",
|
||||||
|
"catalog": "dr1",
|
||||||
|
"object_type": "GALAXY",
|
||||||
|
"ra": 300.445566,
|
||||||
|
"dec": 12.778899,
|
||||||
|
"redshift": 0.7742
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Example GUI environment — passed via GUI_ENV_FILE (see root Makefile).
|
||||||
|
#
|
||||||
|
# Point the renderer at the local dev API: `make run` also starts the API
|
||||||
|
# server, so http://127.0.0.1:8080 is the default. Values here win unless
|
||||||
|
# the same key is already set in the real process environment.
|
||||||
|
|
||||||
|
API_URL=http://127.0.0.1:8080
|
||||||
+14
-5
@@ -18,10 +18,19 @@ case "$ARCH" in
|
|||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
curl -fL -o /tmp/odin.tar.gz \
|
# If a prior job restored the install from cache, reuse it instead of
|
||||||
|
# re-downloading. The extracted Odin binary already on PATH is authoritative;
|
||||||
|
# otherwise fetch the version (plus the raylib workaround) fresh.
|
||||||
|
if [ -x /tmp/odin/odin ]; then
|
||||||
|
echo "Odin ${VERSION} found in cache (host arch: ${ARCH}, release arch: ${OBJ_ARCH})"
|
||||||
|
BIN_DIR=/tmp/odin
|
||||||
|
else
|
||||||
|
curl -fL -o /tmp/odin.tar.gz \
|
||||||
"https://github.com/odin-lang/Odin/releases/download/${VERSION}/odin-linux-${OBJ_ARCH}-${VERSION}.tar.gz"
|
"https://github.com/odin-lang/Odin/releases/download/${VERSION}/odin-linux-${OBJ_ARCH}-${VERSION}.tar.gz"
|
||||||
mkdir -p /tmp/odin
|
mkdir -p /tmp/odin
|
||||||
tar -xzf /tmp/odin.tar.gz -C /tmp/odin --strip-components=1
|
tar -xzf /tmp/odin.tar.gz -C /tmp/odin --strip-components=1
|
||||||
|
BIN_DIR=/tmp/odin
|
||||||
|
fi
|
||||||
|
|
||||||
# Work around an Odin binding bug: for ODIN_ARCH == .arm64 the vendored
|
# Work around an Odin binding bug: for ODIN_ARCH == .arm64 the vendored
|
||||||
# raylib references `vendor/raylib/linux-arm/libraylib.a`, but the release
|
# raylib references `vendor/raylib/linux-arm/libraylib.a`, but the release
|
||||||
@@ -33,7 +42,7 @@ if [ "${OBJ_ARCH}" = "arm64" ] \
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -n "${GITHUB_PATH:-}" ]; then
|
if [ -n "${GITHUB_PATH:-}" ]; then
|
||||||
echo "/tmp/odin" >> "$GITHUB_PATH"
|
echo "${BIN_DIR}" >> "$GITHUB_PATH"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Installed Odin ${VERSION} (host arch: ${ARCH}, release arch: ${OBJ_ARCH})"
|
echo "Odin ${VERSION} ready (host arch: ${ARCH}, release arch: ${OBJ_ARCH})"
|
||||||
|
|||||||
Reference in New Issue
Block a user