Skip to content

Repository files navigation

TaskResolver

HTTP service that accepts a job (a list of shell tasks with optional requires dependencies), topologically sorts them into a valid execution order, and returns either sorted JSON or an executable bash script.

Built with Elixir 1.20, Plug, and Bandit — no Phoenix, no database.

Prerequisites

  • Native: asdf with Erlang 27 and Elixir 1.20 (see .tool-versions), or any Elixir ~> 1.20 install
  • Docker: Docker Engine + Docker Compose v2

Local setup

mix deps.get
mix run --no-halt

The API listens on http://localhost:4000.

Run the test suite:

mix test
mix format --check-formatted
mix credo --strict

Docker setup

Build and start the app plus Swagger UI:

docker compose up --build
Service URL Purpose
app http://localhost:4000 TaskResolver HTTP API
swagger-ui http://localhost:8080 Interactive OpenAPI docs

Swagger UI mounts ./docs/openapi.yaml from the host, so spec changes are reflected on container restart.

Stop everything:

docker compose down

API

GET /health

Liveness check — returns 200 ok.

curl http://localhost:4000/health

POST /resolve

Sort a job's tasks. Requires:

  • Content-Type: application/json
  • Accept: application/json or text/plain / text/x-shellscript

Challenge example — JSON response

curl -s -X POST http://localhost:4000/resolve \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "tasks": [
      {"name": "task-1", "command": "touch /tmp/file1"},
      {"name": "task-2", "command": "cat /tmp/file1", "requires": ["task-3"]},
      {"name": "task-3", "command": "echo '\''Hello World!'\'' > /tmp/file1", "requires": ["task-1"]},
      {"name": "task-4", "command": "rm /tmp/file1", "requires": ["task-2", "task-3"]}
    ]
  }'

Expected order: task-1task-3task-2task-4. The requires field is omitted from the output.

Challenge example — bash script response

curl -s -X POST http://localhost:4000/resolve \
  -H "Content-Type: application/json" \
  -H "Accept: text/plain" \
  -d '{
    "tasks": [
      {"name": "task-1", "command": "touch /tmp/file1"},
      {"name": "task-2", "command": "cat /tmp/file1", "requires": ["task-3"]},
      {"name": "task-3", "command": "echo '\''Hello World!'\'' > /tmp/file1", "requires": ["task-1"]},
      {"name": "task-4", "command": "rm /tmp/file1", "requires": ["task-2", "task-3"]}
    ]
  }'

Expected output:

#!/usr/bin/env bash
touch /tmp/file1
echo 'Hello World!' > /tmp/file1
cat /tmp/file1
rm /tmp/file1

Error examples

Missing Content-Type415:

curl -s -X POST http://localhost:4000/resolve \
  -H "Accept: application/json" \
  -d '{"tasks": []}'

Malformed JSON → 400:

curl -s -X POST http://localhost:4000/resolve \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{not json'

Unhandled Accept406:

curl -s -X POST http://localhost:4000/resolve \
  -H "Content-Type: application/json" \
  -H "Accept: application/xml" \
  -d '{"tasks": [{"name": "a", "command": "echo a"}]}'

Cyclic dependency → 422:

curl -s -X POST http://localhost:4000/resolve \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{"tasks": [{"name": "a", "command": "echo a", "requires": ["b"]}, {"name": "b", "command": "echo b", "requires": ["a"]}]}'

OpenAPI

The full API specification lives at docs/openapi.yaml.

Browse it interactively via Swagger UI at http://localhost:8080 when running docker compose up.

Project structure

lib/task_resolver/
├── core/                  # Phase 2 — validation & topological sort
│   ├── job_validator/     # Ecto embedded schemas
│   └── sorter.ex          # Kahn's algorithm
└── web/                   # Phase 3 — HTTP layer
    ├── router.ex          # POST /resolve, content negotiation
    ├── bash_formatter.ex
    └── error_mapper.ex

Design rationale and trade-offs are documented in DECISIONS.md. The implementation roadmap is in PLAN.md.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages