Capstone: Add the NASA Source (You Drive)
Maps to: Task 4.4. Kind: Capstone — specification only. No walkthrough, no test names handed to you, no build loop. You have built three sources; this is the one you build alone.
The claim to prove
The entire arc has been an argument that the Source trait absorbs a new data source with zero orchestrator changes. This capstone is where you cash that claim. You will add a fourth source — NASA — as a new leaf in etl-sources, wire it into the CLI exactly like the other three, and watch the DAG schedule it without a single edit to etl-orchestrate. If you find yourself opening executor.rs or graph.rs, stop: the design says you should not have to, and if you do, something is being done at the wrong layer.
What you are building
A new source that reads from NASA's open API at api.nasa.gov, normalizes its responses into Rows, and runs as a pipeline in the DAG alongside CelesTrak, Space-Track, and TheSpaceDevs.
You choose the endpoint and the mapping. NASA publishes many datasets behind one API key — APOD (astronomy picture of the day), NeoWs (near-Earth objects), DONKI (space-weather notifications), and more. Pick one whose shape appeals to you. The point of the capstone is not a specific endpoint; it is that any of them slots in through the same trait.
Hard requirements (the givens)
- New file:
crates/etl-sources/src/nasa.rs, exported from that crate'slib.rs. This is the only new source code the capstone requires. - API key from the environment, passed as a query parameter. Read
NASA_API_KEYfrom the environment (as the other credentialed sources read theirs) and attach it to the request as a query param — e.g.?api_key=…. It is a query parameter, not a header, for this API. Never hard-code the key; never commit it. - A
NasaSourceimplementingSource.name()returns a stable string like"nasa";extract()does the HTTP GET, maps transport errors and non-success statuses through the sameclassify_statushelper the other sources use, and returnsRawRecord(s). - A transform implementing
Transform. Parse the JSON payload into one or more of the existingRowvariants, or add a newRowvariant inetl-coreif your chosen endpoint genuinely needs one. You decide theRowmapping — that decision is the substance of the capstone. - Tested against
wiremock, never the live API. Follow the pattern from the CelesTrak build: a mock server, aquery_param("api_key", …)matcher, a canned body, assertions on the parsedRawRecord/Row. Watch for the silent-404 trap when your matcher chain is wrong. - Wired into the CLI like the other three. Add a
NasaSourcepipeline inbuild_executorinpanoptes-etl/src/main.rs, with an idempotent JSONL sink and the same freshness window.
The extension point
build_executor in crates/panoptes-etl/src/main.rs already marks exactly where your pipeline goes. The comment is there waiting for you:
// CAPSTONE (you drive, unassisted): add your NASA pipeline here.
// A `NasaSource` reads its key from `NASA_API_KEY` and passes it as a query
// parameter; wire it with a transform and a `JsonlSink` exactly like the
// three above. The Source trait is all the orchestrator needs — nothing in
// this file's structure has to change to admit a fourth source.
Adding your pipeline is three or four lines mirroring the CelesTrak block right above it: construct the source, wrap it in a Pipeline with a transform and an idempotent sink, set refresh_after, and add_pipeline("nasa", …).
The property to observe (this is the grade)
When you are done, take stock of what you did not touch:
etl-orchestrate/src/graph.rs— unchanged.etl-orchestrate/src/executor.rs— unchanged.etl-orchestrate/Cargo.toml— still depends onetl-coreonly. Re-runcargo tree -p etl-orchestrateand confirmetl-sourcesis still absent, even though a fourth source now runs through the executor.
The orchestrator scheduled a source that did not exist when the orchestrator was written, because it only ever knew the trait. That is dependency inversion delivering — not as a slogan, but as a diff you can measure by its emptiness. A new capability arrived by adding a leaf, not by editing the core.
Self-checks (you write the tests)
No test names are given — deciding what to assert is part of the exercise. But you are done when, at minimum:
cargo test -p etl-sources nasais green: the extract test hits a mock (asserting yourapi_keyquery param is sent), and the transform test turns a canned payload into theRow(s) you designed.panoptes-etl statuslists your new source (extendSOURCESif you want it in the status report), andpanoptes-etl runexecutes it in DAG order with the others.git diff --statshows changes inetl-sourcesandpanoptes-etl— and nothing inetl-orchestrate.