Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 70 additions & 1 deletion CONFORMANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Verification tiers:
the all-adapters-boot guard on every CI run; no SDK suite drives it yet.
- Every adapter additionally documents its behavior in depth in its README.

**98 adapters** — 2 SDK+VM, 34 SDK-only, 59 VM-only, 3 boot-tier.
**99 adapters** — 2 SDK+VM, 34 SDK-only, 60 VM-only, 3 boot-tier.

**45 adapters carry derived provider-surface coverage**: their real-API route totals come from the route tables embedded in the pinned official SDKs (Google Discovery docs inside `google-api-go-client`; generated tables inside the Node clients) or from official specs vendored under `conformance/surfaces/` (refreshed by `just surfaces-fetch`) — mechanical and network-free at generation time. For those rows the derived not-implemented list supplements the curated Missing column; adapters without one have no trustworthy machine-readable surface and stay fully curated.

Expand Down Expand Up @@ -71,6 +71,7 @@ Behavior columns come in two kinds: **verified** (an official SDK was driven aga
| [github-style](adapters/github-style/) | GitHub REST + GraphQL API `2022-11-28` | 26 +GQL | SDK | go-github/v89 @ v89.0.0<br>octokit @ 5.0.5 (floor) | 6 | [7](#github-style) | [4](#github-style) |
| [gmail-style](adapters/gmail-style/) | Gmail API `v1` | 16 | SDK | google-api-go-client @ v0.293.0 | 9 | [6](#gmail-style) | [3](#gmail-style) |
| [google-admin-style](adapters/google-admin-style/) | Google Admin SDK Directory API `directory_v1` | 13 | SDK | google-api-go-client @ v0.293.0 | 8 | [4](#google-admin-style) | [4](#google-admin-style) |
| [google-cloudtasks-style](adapters/google-cloudtasks-style/) | Google Cloud Tasks API `v2` | 16 | VM | — | — | [7](#google-cloudtasks-style) | [7](#google-cloudtasks-style) |
| [google-iam-style](adapters/google-iam-style/) | Google Cloud IAM API + Service Accounts `v1` | 10 | SDK | google-api-go-client @ v0.293.0 | 7 | [4](#google-iam-style) | [2](#google-iam-style) |
| [google-style](adapters/google-style/) | Google OAuth2 API `v2` | 4 | SDK | x/oauth2 @ v0.36.0<br>google-api-go-client/idtoken @ v0.293.0 | 7 | [2](#google-style) | [2](#google-style) |
| [gsearchconsole-style](adapters/gsearchconsole-style/) | Google Search Console API `v1` | 11 | SDK | google-api-go-client @ v0.293.0 | 7 | [2](#gsearchconsole-style) | [4](#gsearchconsole-style) |
Expand Down Expand Up @@ -5045,6 +5046,74 @@ behavior notes live in each adapter's README.

</details>

### google-cloudtasks-style

**Covered** — 16 routes

<details><summary>Routes</summary>

| Method | Route |
|---|---|
| GET | `/v2/projects/{project}/locations` |
| GET | `/v2/projects/{project}/locations/{location}` |
| GET | `/v2/projects/{project}/locations/{location}/cmekConfig` |
| PATCH | `/v2/projects/{project}/locations/{location}/cmekConfig` |
| POST | `/v2/projects/{project}/locations/{location}/queues` |
| GET | `/v2/projects/{project}/locations/{location}/queues` |
| GET | `/v2/projects/{project}/locations/{location}/queues/{queue}` |
| PATCH | `/v2/projects/{project}/locations/{location}/queues/{queue}` |
| DELETE | `/v2/projects/{project}/locations/{location}/queues/{queue}` |
| POST | `/v2/projects/{project}/locations/{location}/queues/{queue_verb}` |
| POST | `/v2/projects/{project}/locations/{location}/queues/{queue}/tasks` |
| GET | `/v2/projects/{project}/locations/{location}/queues/{queue}/tasks` |
| POST | `/v2/projects/{project}/locations/{location}/queues/{queue}/tasks:buffer` |
| POST | `/v2/projects/{project}/locations/{location}/queues/{queue}/tasks/{task_verb}` |
| GET | `/v2/projects/{project}/locations/{location}/queues/{queue}/tasks/{task}` |
| DELETE | `/v2/projects/{project}/locations/{location}/queues/{queue}/tasks/{task}` |

</details>

**Missing** (7)

- No background dispatch loop — tasks are never auto-delivered at scheduleTime; only tasks.run drives attempts
- No gRPC transport (REST only); google-cloud-* client libraries default to gRPC
- oauthToken/oidcToken are stored verbatim — no token minting at dispatch time
- stackdriverLoggingConfig is stored but no logs are written
- IAM policies are stored but not enforced — testIamPermissions always grants
- No App Engine queue.yaml/xml semantics (DISABLED state, queue-file overrides)
- CMEK config is metadata-only; nothing is encrypted

**Deviations** (7)

- Any OAuth2 bearer token is accepted; real Google validates tokens and IAM permissions
- tasks.run models the worker instead of calling it — success completes and deletes the task; the failing-worker profile simulates a 500 with retryConfig backoff
- queues.list filter supports only name/state with =, !=, and : containment (subset of the real filter grammar)
- tasks.list returns creation order; the real service defines no order
- Generated task IDs are counter-based 19-digit decimals, not random
- Purge is synchronous; real purge can take up to 60s to complete
- Deleted-task tombstones hold names exactly 24h (real retention is up to 24h, 9 days for queue.yaml queues)

<details><summary>Derived behavior tags (static — from scripts/*.star, not SDK-verified)</summary>

- `GET` `/v2/projects/{project}/locations` — query, params, paginate
- `GET` `/v2/projects/{project}/locations/{location}` — params
- `GET` `/v2/projects/{project}/locations/{location}/cmekConfig` — params, stateful
- `PATCH` `/v2/projects/{project}/locations/{location}/cmekConfig` — body, params, stateful
- `POST` `/v2/projects/{project}/locations/{location}/queues` — body, query, params, stateful
- `GET` `/v2/projects/{project}/locations/{location}/queues` — query, params, stateful, paginate
- `GET` `/v2/projects/{project}/locations/{location}/queues/{queue}` — params, stateful
- `PATCH` `/v2/projects/{project}/locations/{location}/queues/{queue}` — body, query, params, stateful
- `DELETE` `/v2/projects/{project}/locations/{location}/queues/{queue}` — params, stateful
- `POST` `/v2/projects/{project}/locations/{location}/queues/{queue_verb}` — body, params, stateful, clock
- `POST` `/v2/projects/{project}/locations/{location}/queues/{queue}/tasks` — body, query, params, stateful, clock
- `GET` `/v2/projects/{project}/locations/{location}/queues/{queue}/tasks` — query, params, stateful, paginate
- `POST` `/v2/projects/{project}/locations/{location}/queues/{queue}/tasks:buffer` — body, params, stateful, clock
- `POST` `/v2/projects/{project}/locations/{location}/queues/{queue}/tasks/{task_verb}` — body, query, params, stateful, clock
- `GET` `/v2/projects/{project}/locations/{location}/queues/{queue}/tasks/{task}` — query, params, stateful
- `DELETE` `/v2/projects/{project}/locations/{location}/queues/{queue}/tasks/{task}` — params, stateful, clock

</details>

### google-iam-style

**Covered** — 10 routes
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ stunt adapter test ./myapi-style # conformance vs your local real t
stunt catalog search stripe # browse the adapter registry
```

**Reference adapters in this repo** — 98 of them (Stripe, Salesforce, Discord, Twilio,
**Reference adapters in this repo** — 99 of them (Stripe, Salesforce, Discord, Twilio,
Square, Adyen, AWS S3, Google/Microsoft/Apple families, blockchain RPCs, …; all unofficial,
synthetic-data-only, with a DISCLAIMER). Browse them with `stunt catalog search`. Every one
passes an adversarial input-safety sweep (garbage params, null/malformed bodies, ~30 tampered
Expand Down
17 changes: 17 additions & 0 deletions adapters/google-cloudtasks-style/DISCLAIMER
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# DISCLAIMER

This adapter is **not affiliated with, endorsed by, or sponsored by** Google.
"Google" and related marks are trademarks of their respective owners.

This is a **local development and testing simulator** provided by the `stunt` project.

- It **does not** call the real Google API.
- It runs entirely on your local machine and returns **synthetic, fake data only**.
- It contains **no real Google data, no recorded responses, and no proprietary
documentation**. All fixtures and templates are generated by fakers and pass
`stunt adapter lint`.
- It reproduces the *structure* of a Google-style API solely so you can develop and
test your client code locally without creating remote accounts or hitting the network.

Use of this adapter is at your own risk. If you are the provider and believe this
adapter should be changed or removed, please open an issue.
97 changes: 97 additions & 0 deletions adapters/google-cloudtasks-style/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# google-cloudtasks-style

A stunt adapter simulating the **Google Cloud Tasks API** (`cloudtasks.googleapis.com/v2`) — queues, tasks, rate/retry configuration, and the IAM/location surface — for local testing.

## Simulated API

- **Name:** Google Cloud Tasks API
- **Version:** `v2`

## Endpoints

All routes live under `/v2/projects/{project}/locations/{location}` and require an `Authorization: Bearer <token>` header (any non-empty token — the sim does not validate Google OAuth).

### projects.locations

| Method | Route | Description |
|--------|-------|-------------|
| GET | `/v2/projects/{project}/locations` | List the regions the service operates in (the canonical GCP set; `pageSize`/`pageToken`). |
| GET | `/v2/projects/{project}/locations/{location}` | Get one location. |
| GET | `.../locations/{location}/cmekConfig` | Get the CMEK config (`{name, kmsKey}`; empty key by default, like the real service). |
| PATCH | `.../locations/{location}/cmekConfig` | Set the CMEK key. |

### projects.locations.queues

| Method | Route | Description |
|--------|-------|-------------|
| POST | `.../queues?queueId={id}` | Create a queue (body = `Queue`). Unset `rateLimits`/`retryConfig` get the service defaults (500 disp/s, burst 100, 1000 concurrent; 100 attempts, 0.1s min / 3600s max backoff, 16 doublings). `maxBurstSize` is derived (output only). |
| GET | `.../queues` | List queues — lexicographical, `pageSize` (max 9800), `pageToken`, and a `filter` subset (`name`/`state` with `=`, `!=`, `:` containment). |
| GET | `.../queues/{queue}` | Get a queue. |
| PATCH | `.../queues/{queue}?updateMask=...` | Patch a queue (AIP-134: bare path replaces the message, dotted path sets a leaf; `state` is rejected — use pause/resume). |
| DELETE | `.../queues/{queue}` | Delete a queue and its tasks. |
| POST | `.../queues/{queue}:pause` | Pause (idempotent). |
| POST | `.../queues/{queue}:resume` | Resume (idempotent). |
| POST | `.../queues/{queue}:purge` | Purge tasks created before the purge moment; stamps `purgeTime`. |
| POST | `.../queues/{queue}:getIamPolicy` | Get the queue's IAM policy. |
| POST | `.../queues/{queue}:setIamPolicy` | Set the policy (etag-checked; mismatch → `409 ABORTED`). |
| POST | `.../queues/{queue}:testIamPermissions` | Echo the asked permissions (the sim grants all). |

### projects.locations.queues.tasks

| Method | Route | Description |
|--------|-------|-------------|
| POST | `.../queues/{queue}/tasks` | Create a task (body `{task, responseView?}`). Validations mirror the real API: exactly one of `httpRequest`/`appEngineHttpRequest`, `url` must start `http(s)://`, `body` only with POST/PUT/PATCH (POST/PUT for App Engine), `relativeUri` must start `/`, ID charset rules, past/absent `scheduleTime` clamped to now. |
| GET | `.../queues/{queue}/tasks` | List tasks in creation order (`responseView`, `pageSize` max 1000, `pageToken`). |
| GET | `.../queues/{queue}/tasks/{task}` | Get a task (`?responseView=BASIC\|FULL`). |
| DELETE | `.../queues/{queue}/tasks/{task}` | Delete a task. |
| POST | `.../queues/{queue}/tasks/{task}:run` | Force a run now — ignores `scheduleTime`, queue state, and rate limits. |
| POST | `.../queues/{queue}/tasks:buffer` | Buffer a task whose payload is the request `HttpBody` (queue must have an `httpTarget` with `uriOverride.host`; URL built from scheme/host/port/path/query overrides). Generated task ID. |
| POST | `.../queues/{queue}/tasks/{taskId}:buffer` | Same, with a caller-chosen task ID. |

## Key semantics

- **Views.** `responseView` defaults to `BASIC`, which omits the request
`body` (both HTTP and App Engine payloads); `FULL` returns everything.
- **Task de-duplication.** Creating a task whose ID exists (or was
deleted/executed recently) fails with `409 ALREADY_EXISTS` — the real
service holds deleted IDs for up to 24h, and so does the sim (tombstones).
`stunt reset <service>` clears them.
- **`tasks.run` outcome model.** The sim never opens outbound connections —
the worker is never actually called. By default the unseen worker
"succeeds": the returned `Task` carries the post-dispatch status
(`dispatchTime` set, no `responseStatus`), then the task is deleted —
which is also why re-running a completed task returns `404 NOT_FOUND`,
exactly like the real API.
- **Authored profile — `failing-worker`.** `stunt profile activate
failing-worker` flips the worker model: every `tasks.run` records a 500
`responseStatus`, increments the attempt counters, and reschedules the
task per the queue's `retryConfig` (min/max backoff, doublings, and
`maxAttempts`/`maxRetryDuration` exhaustion → permanent failure). Use it
to exercise client retry/backoff paths deterministically.
- **Errors.** Canonical Google shape: `{"error": {code, message, status}}`
with `INVALID_ARGUMENT` / `NOT_FOUND` / `ALREADY_EXISTS` / `ABORTED` /
`UNAUTHENTICATED`.
- **Ordering.** Queues list lexicographically (documented); tasks list in
creation order (the real service defines no order — this keeps runs
reproducible).

## Usage

```bash
stunt init
# Add to your stunt.yaml:
# cloudtasks:
# adapter: embedded:google-cloudtasks-style
stunt up
```

```bash
Q=projects/demo/locations/us-central1/queues/orders
curl -X POST "http://127.0.0.1:8000/v2/projects/demo/locations/us-central1/queues?queueId=orders" \
-H "Authorization: Bearer anything" -H "Content-Type: application/json" -d '{}'
curl -X POST "http://127.0.0.1:8000/$Q/tasks" \
-H "Authorization: Bearer anything" -H "Content-Type: application/json" \
-d '{"task": {"httpRequest": {"url": "https://worker.example/handler", "httpMethod": "POST", "body": "eyJvayI6dHJ1ZX0="}}}'
```

All data is synthetic. See [DISCLAIMER](DISCLAIMER).
140 changes: 140 additions & 0 deletions adapters/google-cloudtasks-style/adapter.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# stunt adapter manifest — Google Cloud Tasks API simulator (unofficial)
# Docs: https://stuntapi.com/stunt
#
# This adapter mimics the Google Cloud Tasks API (v2, REST surface at
# cloudtasks.googleapis.com) for LOCAL TESTING ONLY. It does not call the
# real Google API. All data is synthetic. See DISCLAIMER.
id: google-cloudtasks-style
name: "Google Cloud Tasks API simulator (unofficial)"
version: "0.1.0"

api:
name: "Google Cloud Tasks API"
version: "v2"

# Endpoints — each maps a route + method to a Starlark handler, in
# declaration order (literals before parameterized routes where they could
# shadow). Colon verbs (:pause, :run, ...) ride the last path segment, so
# they are captured in a {name_verb} param and split in the handler.
endpoints:
# --- projects.locations ---
- route: /v2/projects/{project}/locations
method: GET
handler: scripts/locations.star#on_list_locations
- route: /v2/projects/{project}/locations/{location}
method: GET
handler: scripts/locations.star#on_get_location
- route: /v2/projects/{project}/locations/{location}/cmekConfig
method: GET
handler: scripts/locations.star#on_get_cmek
- route: /v2/projects/{project}/locations/{location}/cmekConfig
method: PATCH
handler: scripts/locations.star#on_update_cmek
concurrency_key: location

# --- projects.locations.queues ---
- route: /v2/projects/{project}/locations/{location}/queues
method: POST
handler: scripts/queues.star#on_create_queue
concurrency_key: location
- route: /v2/projects/{project}/locations/{location}/queues
method: GET
handler: scripts/queues.star#on_list_queues
- route: /v2/projects/{project}/locations/{location}/queues/{queue}
method: GET
handler: scripts/queues.star#on_get_queue
- route: /v2/projects/{project}/locations/{location}/queues/{queue}
method: PATCH
handler: scripts/queues.star#on_patch_queue
concurrency_key: queue
- route: /v2/projects/{project}/locations/{location}/queues/{queue}
method: DELETE
handler: scripts/queues.star#on_delete_queue
concurrency_key: queue
# :pause :resume :purge :getIamPolicy :setIamPolicy :testIamPermissions
# NOTE: the concurrency key is the raw "<queueId>:<verb>" param — the
# engine keys on the route param value, so these verbs serialize among
# themselves but not with the sibling {queue} routes.
- route: /v2/projects/{project}/locations/{location}/queues/{queue_verb}
method: POST
handler: scripts/queues.star#on_queue_verb
concurrency_key: queue_verb

# --- projects.locations.queues.tasks ---
- route: /v2/projects/{project}/locations/{location}/queues/{queue}/tasks
method: POST
handler: scripts/tasks.star#on_create_task
concurrency_key: queue
- route: /v2/projects/{project}/locations/{location}/queues/{queue}/tasks
method: GET
handler: scripts/tasks.star#on_list_tasks
# tasks:buffer — generated-ID form (taskId in the body only).
- route: /v2/projects/{project}/locations/{location}/queues/{queue}/tasks:buffer
method: POST
handler: scripts/tasks.star#on_buffer_task
concurrency_key: queue
# {task}:run and {taskId}:buffer — verb rides the last segment. The
# concurrency key is the raw "<taskId>:<verb>" param (same caveat as the
# queue verbs above).
- route: /v2/projects/{project}/locations/{location}/queues/{queue}/tasks/{task_verb}
method: POST
handler: scripts/tasks.star#on_task_verb
concurrency_key: task_verb
- route: /v2/projects/{project}/locations/{location}/queues/{queue}/tasks/{task}
method: GET
handler: scripts/tasks.star#on_get_task
- route: /v2/projects/{project}/locations/{location}/queues/{queue}/tasks/{task}
method: DELETE
handler: scripts/tasks.star#on_delete_task
concurrency_key: task

# Backing stores — collections for stateful data (no seeds: like the real
# service, a fresh project has no queues until you create them).
resources:
- name: queues
kind: collection
- name: tasks
kind: collection
# Deleted/executed task names are not immediately reusable (the real
# service holds them for up to 24h) — see tasks.create de-duplication.
- name: tombstones
kind: collection
- name: iam_policies
kind: collection

# Adapter-authored behavior modes. Handlers read the active one via the
# profile_active() builtin; activation is runtime-only (see the stunt
# profile command / dashboard profiles panel).
profiles:
failing-worker: "tasks.run simulates a worker answering HTTP 500 — the task stays queued and reschedules per the queue's retryConfig, exercising client retry/backoff paths"

# Auth scheme metadata (mock: any OAuth2 bearer token is accepted, matching
# the cloud-platform / cloud-tasks scopes the real API requires).
identity:
token_scheme: bearer

# Catch-all: any unmatched route returns a 404 in the Google error shape.
rules:
- name: catchall-404
match: { path: "/**" }
respond:
status: 404
body:
inline:
error:
code: 404
message: "Method not found."
status: "NOT_FOUND"

# Resources notes:
# - queues collection: {id: full queue name, project, location, queue_id,
# state, rate_limits, retry_config,
# http_target?, app_engine_routing_override?,
# stackdriver_logging_config?, purge_time_unix?}
# - tasks collection: {id: full task name, queue, task_id, seq,
# create_unix, create_time, schedule_time,
# dispatch_deadline?, dispatch_count, response_count,
# first_attempt?, last_attempt?, message_type,
# http_request? | app_engine_http_request?}
# - tombstones: {id: task name, until_unix}
# - iam_policies: {id: queue name, bindings, version, etag}
Loading
Loading