apate 0.2.0

API mocking server & rust unit tests library server to mock http endpoints.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
# Apate — AI Agent Reference

**Project version:** 0.2.0 | **Git:** 3b4b8c4

> This document is written **for AI agents** (and any tool that consumes it). It is a precise,
> unambiguous reference of the Apate project: its DSL (TOML specification), its scripting
> APIs (Jinja/minijinja and Rhai), its Rust test-library API,
> and how to run it as a Docker image.
> Prefer this file over `README.md`.

---

## 1. Basic overview

**Apate** is an API prototyping / mocking server and a Rust library.

Two main usage scenarios:

1. **Standalone server** — a single binary `apate` that serves HTTP and ships a small
   web UI for editing specs on fly. Also distributable as a Docker image.
2. **Rust test library** — in-process ApateTestServer for `#[test]` / `#[tokio::test]` to
   test client logic against a real local HTTP endpoint without touching the network.

### Core concepts (vocabulary you will see everywhere)

| Term         | Meaning                                                                                              |
|--------------|------------------------------------------------------------------------------------------------------|
| **Spec**     | A TOML document describing everything Apate should mock. Deserialized into `ApateSpecs`.             |
| **Deceit**   | One unit of a spec. Targets one or more URIs and holds matchers, headers, processors, and responses.  |
| **Response** | A candidate body under a `Deceit`. Has its own matchers, code, headers, processors, output type/text. |
| **Matcher**  | A boolean check on the incoming request. All must pass for a response to be selected.                 |
| **Output**   | The response body, produced from `output` text via a chosen `type` (string/jinja/hex/base64/rhai).   |
| **Processor**| Post-processing step that can rewrite the already-rendered response body (Rhai or embedded Rust).     |
| **Rhai registry** | Named, reusable Rhai scripts (`[[rhai]]`) referenced by id from matchers/outputs/processors.   |

### Request processing pipeline (exact order)

For every incoming HTTP request, Apate does the following:

1. Iterate `deceit[]` **in order**.
2. For each `Deceit`, try to match the request path against `uris[]` (supports path
   arguments like `/user/{user_id}`). No match → try next `Deceit`.
3. If the path matched, evaluate **deceit-level matchers**. If any fail → try next `Deceit`.
4. Evaluate **response-level matchers** for each `responses[]` in order. The **first**
   response whose matchers all pass (or that has no matchers) is selected. If none pass →
   try next `Deceit`.
5. Render the selected response body using its `type` and `output`.
6. Apply **processors** (deceit-level first, then response-level, in order). A processor
   may replace the body.
7. Return the body with the response `code` (or a code forced by a template/script), the
   merged headers (deceit-level then response-level), and default `200` if no code was set.
8. If no `Deceit` matched at all → `404`.

> **Important:** matcher evaluation is short-circuiting. The `and`/`or` combinators and the
> implicit "all must pass" rule both return a single boolean per matcher list.

### Default port & constants

- Default port: **`8228`** (`apate::DEFAULT_PORT`).
- Default log filter: **`info,apate=debug`** (`apate::DEFAULT_RUST_LOG`).
- Web UI + admin API base path: **`/apate`**.

### Crate features

- `default = ["server"]` — the `server` feature enables `getopt3` (CLI parsing) and
  `include_dir` (embedded web UI). The binary target requires `server`.
- To use Apate **purely as a library** (no web UI, smaller build), disable default features:
  `apate = { version = "0.2", default-features = false }`.

### Rust edition & toolchain

Apate uses **Rust edition 2024**. The project requires Rust **1.98+** (see `rust-toolchain.toml`).

---

## 2. DSL specification

The spec is a **TOML** document deserialized into `apate::ApateSpecs`. Top level has two
optional arrays: `deceit` (required in practice) and `rhai` (named reusable scripts).

```toml
# Top-level shape (all fields optional, default to empty arrays)
deceit = [ /* Deceit ... */ ]
rhai   = [ /* { id = "...", script = "..." } ... */ ]
```

### 2.1 `Deceit` (TOML: `[[deceit]]`)

```toml
[[deceit]]
uris      = ["/user/list", "/user/{user_id}"]        # required. One or more URIs / patterns
headers   = [["Content-Type", "application/json"]]   # optional. [key, value] pairs
matchers  = [ /* Matcher ... */ ]                     # optional. ALL must pass
processors = [ /* Processor ... */ ]                  # optional. run after body render
responses = [ /* DeceitResponse ... */ ]              # optional. first passing one is used
```

Field notes:
- `uris` — each entry is a literal path or a pattern with path arguments
  (`/user/{user_id}`). The first URI that captures-matches the request path wins.
- `headers` — an array of `[key, value]` string pairs. Applied to the response.
- `matchers` — deceit-level matchers. If any fails, this whole `Deceit` is skipped.
- `processors` — run after the selected response body is rendered (before response-level
  processors). See §2.4.
- `responses` — ordered list. The first one whose matchers pass is chosen. A response with
  **no** `matchers` always matches (acts as a fallback).

### 2.2 `DeceitResponse` (TOML: `[[deceit.responses]]`)

```toml
[[deceit.responses]]
code       = 200                    # optional. HTTP status for THIS response
matchers   = [ /* Matcher ... */ ]  # optional. ALL must pass; empty = always match
headers    = [["X-Custom", "1"]]    # optional. extra headers for this response
processors = [ /* Processor ... */ ]# optional. run after body render
type       = "string"               # optional. see Output types §2.3 (default "string")
output     = "..."                  # the body source, interpreted according to `type`
```

### 2.3 Matchers — full reference

TOML tag is `type`. All matchers except `rhai`/`rhai_ref` support an optional `negate`
boolean (default `false`). `and`/`or` are combinators.

| `type`      | Fields                                          | Description                                             |
|-------------|-------------------------------------------------|---------------------------------------------------------|
| `method`    | `eq`, `negate`?                                 | Exact HTTP method match (e.g. `eq = "GET"`) |
| `header`    | `key`, `value`, `negate`?                       | Exact match on a request header value                     |
| `query_arg` | `name`, `value`, `negate`?                      | Exact match on a URL query argument                       |
| `path_arg`  | `name`, `value`, `negate`?                      | Exact match on a path argument (`/user/{user_id}`)        |
| `json`      | `path`, `eq`, `negate`?                         | JSONPath must yield **exactly one** string equal to `eq` |
| `rhai`      | `script`                                        | Inline Rhai script; must return a **boolean**            |
| `rhai_ref`  | `id`, `args`?                                   | Reusable Rhai matcher by registry id; must return bool   |
| `and`       | `matchers` (array)                              | True only if **all** inner matchers are true             |
| `or`        | `matchers` (array)                              | True if **any** inner matcher is true                    |

Examples:

```toml
{ type = "method", eq = "GET" }
{ type = "header", key = "User-Agent", value = "curl" }
{ type = "query_arg", name = "name", value = "Ivan" }
{ type = "path_arg", name = "user_id", value = "740" }
{ type = "json", path = "$.name", eq = "Rajesh" }
{ type = "and", matchers = [ { type = "method", eq = "POST" }, { type = "json", path = "$.id", eq = "1" } ] }
{ type = "or",  matchers = [ { type = "json", path = "$.name", eq = "A" }, { type = "json", path = "$.name", eq = "B" } ] }

# Inline Rhai matcher (returns boolean)
{ type = "rhai", script = """
if ctx.load_query_args().foo == "none" { return false; }
return true;
""" }

# Reusable Rhai matcher (defined in [[rhai]] registry)
{ type = "rhai_ref", id = "top-level-script", args = ["arg1", "arg2"] }
```

> **JSON matcher constraint:** the JSONPath expression must return exactly one result and
> that result must be a string equal to `eq`, otherwise the matcher fails.

### 2.4 Output types (TOML: `type` on a response)

| `type`     | `output` is interpreted as                          | Notes                                                    |
|------------|-----------------------------------------------------|----------------------------------------------------------|
| `string`   | raw string, returned as-is (DEFAULT)                |                                                          |
| `jinja`    | minijinja template (Jinja2-compatible syntax)        | See §3 Jinja templates API                                |
| `hex`      | HEX string → decoded bytes (no `0x` prefix expected) | Optional `0x` prefix is stripped                          |
| `base64`   | Base64 string → decoded bytes                         | Standard alphabet                                          |
| `rhai`     | inline Rhai script → returns a Blob (response body)  | See §4 Rhai script API                                     |
| `rhai_ref` | reusable Rhai script by id → returns a Blob          | `id = "..."`, optional `args = [...]`                     |

Binary examples (`hex`, `base64`) are the way to return non-text payloads (e.g. PNG files).

### 2.5 Processors (TOML: `[[deceit.processors]]` or `[[deceit.responses.processors]]`)

A processor rewrites the **already rendered** response body. They run in order; each one
receives the body produced by the previous step.

| `type`     | Fields        | Description                                          |
|------------|---------------|------------------------------------------------------|
| `rhai`     | `script`      | Inline Rhai; returns Blob (new body) or empty (keep)  |
| `rhai_ref` | `id`, `args`? | Reusable Rhai script by registry id                   |
| `embedded` | `id`, `args`? | Custom **Rust** processor registered in your app      |

- `embedded` is only available when Apate is embedded in your own Rust application
  (see §5 and the `PostProcessor` trait). It references a processor registered by `id`.
- A processor that returns **no value** (empty) leaves the body unchanged.

### 2.6 Rhai script registry (TOML: `[[rhai]]`)

Named, reusable Rhai scripts referenced by `id` from matchers, outputs, and processors
(via `rhai_ref`).

```toml
[[rhai]]
id = "my-script"
script = """
// your Rhai code here
"""
```

The `id` is used by `rhai_ref` matchers/outputs/processors. Each reference may pass
`args` (array of strings) to the script.

---

## 3. Jinja (minijinja) templates API

Used when a response `type = "jinja"`. The template engine is [minijinja](https://docs.rs/minijinja/latest/minijinja)
with Jinja2-compatible syntax. Full syntax reference:
<https://docs.rs/minijinja/latest/minijinja/syntax> · filters:
<https://docs.rs/minijinja/latest/minijinja/filters>.

### 3.1 Global functions (no receiver)

| Function                              | Returns                                   |
|---------------------------------------|-------------------------------------------|
| `random_num()`                        | random number as **string**                |
| `random_num(max)`                     | random number in `[0, max)` as string      |
| `random_num(from, to)`                | random number in `[from, to)` as string    |
| `random_hex()`                        | random HEX string (32 bytes → 64 chars)    |
| `random_hex(bytes_len)`               | random HEX string of `bytes_len` bytes     |
| `uuid_v4()`                           | a random UUID v4 (string)                  |

> All `random_*`/`uuid_v4` return **strings** (not numbers) in Jinja.

### 3.2 `ctx` object (request/response context)

Exposed under the variable name `ctx`.

**Readable properties:**
- `ctx.method` → HTTP method of the request (e.g. `"GET"`)
- `ctx.path` → the matched path (e.g. `"/user/740"`)

**Methods:**
- `ctx.load_headers()` → map of request headers, **lowercase keys**
- `ctx.load_query_args()` → map of URL query arguments
- `ctx.load_path_args()` → map of path arguments from URI pattern (`/user/{user_id}`)
- `ctx.load_body_string()` → request body as a string (empty string if none)
- `ctx.load_body_json()` → request body parsed as JSON (errors if not valid JSON)
- `ctx.inc_counter("key")` → returns the **previous** counter value for `key`, then
  increments it by 1 (first call returns `0`). Counters are shared server-wide per key.
- `ctx.set_response_code(code)`**sets** the response status code (e.g. `ctx.set_response_code(503)`)

### 3.3 Jinja examples

```jinja
{# echo a request path argument #}
{"id": "{{ ctx.load_path_args().user_id }}"}

{# read a JSON request body field #}
{%- set input = ctx.load_body_json() %}
{ "name": "{{ input.name }}", "surname": "{{ input.surname }}" }

{# conditional + set response code #}
{%- if ctx.load_query_args().id %}
    {%- set id = ctx.load_query_args().id %}
{%- else %}
    {%- set _ = ctx.set_response_code(500) %}
{%- endif %}
{"id":"{{ id }}"}

{# random values #}
"token": "{{ random_hex(16) }}", "id": "{{ random_num(100, 999) }}", "uuid": "{{ uuid_v4() }}"
```

---

## 4. Rhai script API

Rhai (<https://rhai.rs>) is used in three roles, each with a different **context** and
**return contract**. The scripting language itself is standard Rhai — see the
[Rhai book](https://rhai.rs/book/ref/index.html) for syntax.

### 4.1 Where Rhai is used

| Role            | Triggered by                              | `ctx` type        | Must return            |
|-----------------|-------------------------------------------|-------------------|------------------------|
| **Matcher**     | `type = "rhai"` / `"rhai_ref"` in matchers | `RhaiRequestContext`  | a **boolean**            |
| **Output**      | `type = "rhai"` / `"rhai_ref"` on response | `RhaiResponseContext` | a **Blob** (body) or empty (empty body) |
| **Processor**   | `type = "rhai"` / `"rhai_ref"` in processors | `RhaiResponseContext` | a **Blob** (new body) or empty (keep) |

### 4.2 Scope variables available inside a Rhai script

| Variable | Matcher | Output | Processor | Description                                        |
|----------|---------|--------|-----------|----------------------------------------------------|
| `ctx`    |||| context object (see §4.3 / §4.4)                    |
| `args`   |||| array of strings; **empty for inline `rhai`**, populated for `rhai_ref` |
| `body`   |||| the current response body as a **Blob**             |

> **Important:** `args` is only non-empty when you reference a registry script via
> `rhai_ref` with an `args` list. Inline `rhai` scripts always see `args = []`.

### 4.3 `RhaiRequestContext``ctx` in **matchers**

| Member                        | Returns                              |
|-------------------------------|--------------------------------------|
| `ctx.method`                  | request method (string)              |
| `ctx.path`                    | matched path (string)                |
| `ctx.load_headers()`          | map of request headers               |
| `ctx.load_query_args()`       | map of URL query arguments           |
| `ctx.load_path_args()`        | map of path arguments                |
| `ctx.load_body()`             | request body as **Blob**             |

### 4.4 `RhaiResponseContext``ctx` in **outputs** and **processors**

Everything in §4.3, **plus**:

| Member                        | Returns / effect                        |
|-------------------------------|-----------------------------------------|
| `ctx.response_code`           | get **and set** the response status code |
| `ctx.inc_counter("key")`      | returns **previous** counter value, then increments (first call → `0`) |

### 4.5 Global functions (available in all Rhai contexts)

| Function                            | Returns                                  |
|-------------------------------------|-------------------------------------------|
| `random_num()`                      | random `i64`                               |
| `random_num(max)`                   | random `i64` in `[0, max)`                 |
| `random_num(from, to)`              | random `i64` in `[from, to)`               |
| `random_hex()`                      | random HEX string (32 bytes)               |
| `random_hex(bytes_len)`             | random HEX string of `bytes_len` bytes     |
| `uuid_v4()`                         | random UUID v4 (string)                    |
| `to_json_blob(value)`               | serialize any Rhai value → **Blob** (JSON) |
| `from_json_blob(blob)`              | deserialize **Blob** (JSON) → Rhai value   |
| `storage_read(key)`                 | read a value from the global in-memory KV store (default if absent) |
| `storage_write(key, value)`         | write a value to the global in-memory KV store |

> **Storage** is an in-memory key/value store that persists across requests for the
> lifetime of the server (shared by all Rhai scripts). It is the primary way to mimic
> database / stateful behaviour. Values are stored as JSON-serialized Rhai values.

### 4.6 Logging

Rhai scripts can log via the standard Rhai macros:
- `print!("...")` → logged at `info` level with prefix `RHAI:`
- `debug!("...")` → logged at `debug` level

### 4.7 Rhai examples

**Matcher (returns boolean):**
```rhai
if ctx.method == "GET" { return true; }
let q = ctx.load_query_args();
if q.foo == "none" { return false; }
return true;
```

**Output template (returns Blob):**
```rhai
let data = [
    #{ name: "Ivan", surname: "Ivanov" },
    #{ name: "John", surname: "Smith" },
];
let q = ctx.load_query_args();
if "name" in q { data = data.filter(|r| r.name == q["name"]); }
return to_json_blob(data);
```

**Processor (rewrites body, uses `body`, `ctx`, `args`):**
```rhai
let map = from_json_blob(body);
map.post = "hello from post processor";
map.args = args;
return map.to_json().to_blob();
```

**Stateful (mimic a DB via storage):**
```rhai
let users = storage_read("users") ?? [];
let rec = from_json_blob(ctx.load_body());
rec.id = ctx.inc_counter("user_id") + 1;
users.push(rec);
storage_write("users", users);
return to_json_blob(rec);
```

---

## 5. DSL usage examples

Full, runnable spec files live in `examples/`. Reference them when in doubt:

| File | Demonstrates |
|------|--------------|
| `examples/apate-specs.toml` | matchers (method/query/path/json, `and`/`or`), Jinja templates, multi-response fallback |
| `examples/apate-template-specs.toml` | Jinja context (`ctx`), template functions, `set_response_code`, counters |
| `examples/apate-specs-rhai.toml` | Rhai as matcher, output, processor, and reusable registry scripts |
| `examples/apate-specs-bin.toml` | binary responses via `base64` and `hex` |
| `examples/apate-specs-app.toml` | stateful behaviour via `storage_read`/`storage_write`, `rhai_ref` output |
| `examples/processors.rs` | embedded Rust `PostProcessor` (custom signing logic) |

### 5.1 Minimal spec (one static JSON endpoint)

```toml
[[deceit]]
uris = ["/user/check"]
headers = [["Content-Type", "application/json"]]
matchers = [{ type = "method", eq = "POST" }]

[[deceit.responses]]
output = """
{"message":"Success"}
"""
```

### 5.2 Multiple responses selected by query arg, with a fallback

```toml
[[deceit]]
uris = ["/user/list"]
matchers = [{ type = "method", eq = "GET" }]

[[deceit.responses]]
matchers = [{ type = "query_arg", name = "name", value = "Ivan" }]
output = "[{\"id\":42,\"name\":\"Ivan\"}]"

[[deceit.responses]]
matchers = [{ type = "query_arg", name = "name", value = "Rajesh" }]
code = 503
output = "{\"message\":\"Impossible to list them all\"}"

[[deceit.responses]]
# no matchers -> fallback (always matches)
output = "[{\"id\":42,\"name\":\"Ivan\"},{\"id\":740,\"name\":\"Adolph\"}]"
```

### 5.3 Jinja template that echoes the request

```toml
[[deceit]]
uris = ["/echo/{name}"]
headers = [["Content-Type", "application/json"]]

[[deceit.responses]]
type = "jinja"
output = """
{"name": "{{ ctx.load_path_args().name }}",
 "query": {{ ctx.load_query_args() | tojson }},
 "id": "{{ uuid_v4() }}"}
"""
```

### 5.4 Rhai output that filters a dataset by query args

```toml
[[deceit]]
uris = ["/rhai/list"]
headers = [["Content-Type", "application/json"]]

[[deceit.responses]]
type = "rhai"
output = """
let data = [
    #{ name: "Ivan", surname: "Ivanov" },
    #{ name: "John", surname: "Smith" },
];
let q = ctx.load_query_args();
if "name" in q    { data = data.filter(|r| r.name == q["name"]); }
if "surname" in q { data = data.filter(|r| r.surname == q["surname"]); }
return to_json_blob(data);
"""
```

### 5.5 Stateful "add then list" using the storage KV

```toml
[[deceit]]
uris = ["/app/user/add"]
matchers = [{ type = "method", eq = "POST" }]
headers = [["Content-Type", "application/json"]]

[[deceit.responses]]
type = "rhai"
output = """
let users = storage_read("users") ?? [];
let rec = from_json_blob(ctx.load_body());
rec.id = ctx.inc_counter("user_id") + 1;
users.push(rec);
storage_write("users", users);
return to_json_blob(rec);
"""

[[deceit]]
uris = ["/app/user/list"]
matchers = [{ type = "method", eq = "GET" }]

[[deceit.responses]]
type = "rhai"
output = """
return to_json_blob(storage_read("users") ?? []);
"""
```

### 5.6 Reusable script shared by a matcher and a processor

```toml
[[deceit]]
uris = ["/guarded"]

[[deceit.matchers]]
type = "rhai_ref"
id = "require-admin"
args = ["secret-token"]

[[deceit.responses]]
output = "{\"ok\":true}"

[[deceit.processors]]
type = "rhai_ref"
id = "stamp-args"

[[rhai]]
id = "require-admin"
script = """
let h = ctx.load_headers();
return "x-token" in h and h["x-token"] == args[0];
"""

[[rhai]]
id = "stamp-args"
script = """
let m = from_json_blob(body);
m.args = args;
return m.to_json().to_blob();
"""
```

### 5.7 Binary response (PNG) via base64

```toml
[[deceit]]
uris = ["/file/base64.png"]
headers = [["Content-Type", "image/png"]]

[[deceit.responses]]
type = "base64"
output = "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4Hs..."  # base64 payload
```

---

## 6. Using Apate as a Rust test library

Apate is a regular library. You start an in-process HTTP server inside your test, call it
with any HTTP client, and it is automatically shut down when the server handle is dropped.

### 6.1 Cargo dependency

```toml
[dev-dependencies]
apate = "0.2"          # test-only dependency
reqwest = { version = "0.12", features = ["blocking", "json"] }
serial_test = "3"      # to serialize tests that share the default port
```

### 6.2 Public API (module paths)

| Item | Path |
|------|------|
| `ApateConfig` | `apate::ApateConfig` |
| `ApateSpecs` | `apate::ApateSpecs` |
| `ApateConfigBuilder` | `apate::ApateConfigBuilder` |
| `apate_server_run(config)` (async) | `apate::apate_server_run` |
| `apate_init_server_config(port, log, files)` | `apate::apate_init_server_config` |
| `DEFAULT_PORT` (= 8228) | `apate::DEFAULT_PORT` (also `apate::test::DEFAULT_PORT`) |
| `DEFAULT_RUST_LOG` | `apate::DEFAULT_RUST_LOG` (also `apate::test::DEFAULT_RUST_LOG`) |
| `Deceit`, `DeceitResponse` | `apate::deceit::{Deceit, DeceitResponse}` |
| `DeceitBuilder` | `apate::deceit::DeceitBuilder` |
| `DeceitResponseBuilder` | `apate::deceit::DeceitResponseBuilder` |
| `DeceitResponseContext` | `apate::deceit::DeceitResponseContext` |
| `Matcher` | `apate::matchers::Matcher` |
| `OutputType` | `apate::output::OutputType` |
| `Processor`, `ApateProcessor`, `PostProcessor` (trait) | `apate::processors::{Processor, ApateProcessor, PostProcessor}` |
| `ApateTestServer` | `apate::test::ApateTestServer` |
| `init_env_logger(log)` | `apate::test::init_env_logger` |

### 6.3 `ApateTestServer`

```rust
apate::test::ApateTestServer::start(config: ApateConfig, delay_ms: usize) -> ApateTestServer
```

- Starts the server on `config.port` (default `8228`).
- `delay_ms` — a short sleep after start for slow environments (use `0` or `1` in CI).
- **Synchronous API** — works in both `#[test]` and `#[tokio::test]`.
- **Auto-cleanup**: the server is stopped when the returned value is dropped. Bind it to
  a variable that lives for the duration of the test, e.g. `let _apate = ...;`.

### 6.4 Simplest test (builder style, sync)

```rust
use apate::deceit::{DeceitBuilder, DeceitResponseBuilder};
use apate::test::ApateTestServer;

#[test]
fn my_api_test() {
    let config = DeceitBuilder::with_uris(&["/user/check"])
        .require_method("POST")
        .add_header("Content-Type", "application/json")
        .add_response(
            DeceitResponseBuilder::default()
                .code(200)
                .with_output(r#"{"message":"Success"}"#)
                .build(),
        )
        .to_app_config();          // wraps the single Deceit into an ApateConfig

    // Server stops automatically when `_apate` is dropped at end of test.
    let _apate = ApateTestServer::start(config, 0);

    let client = reqwest::blocking::Client::new();
    let resp = client
        .post("http://localhost:8228/user/check")
        .send()
        .expect("request failed");

    assert_eq!(resp.status(), 200);
    assert_eq!(resp.headers().get("Content-Type").unwrap(), "application/json");
    assert_eq!(resp.json::<serde_json::Value>().unwrap()["message"], "Success");
}
```

### 6.5 Builder reference

`DeceitBuilder` (entry point `DeceitBuilder::with_uris(&[...])`):
- `add_header(key, value)`, `add_processor(Processor)`, `add_matcher(Matcher)`
- `require_method(m)`, `require_header(k,v)`, `require_query_arg(n,v)`,
  `require_path_arg(n,v)`, `require_json_match(path, eq)`
- `with_matchers(Vec<Matcher>)`, `add_response(DeceitResponse)`, `with_responses(Vec<_>)`
- `build() -> Deceit`
- `to_app_config() -> ApateConfig` (default port) / `to_app_config_with_port(port)`

`DeceitResponseBuilder` (entry point `DeceitResponseBuilder::default()`):
- `code(u16)`, `add_header(k,v)`, `add_processor(Processor)`, `add_matcher(Matcher)`
- `with_output(&str)`, `with_output_type(OutputType)`
- `require_*` helpers (same as `DeceitBuilder`)
- `build() -> DeceitResponse`

`ApateConfigBuilder` (for multiple deceits / custom processors / named scripts):
- `with_port(u16)`
- `add_deceit(Deceit)`
- `register_processor(ApateProcessor)`
- `add_script(id, script)` (adds to the `[[rhai]]` registry)
- `build() -> ApateConfig`

### 6.6 Multiple deceits + custom port (async test)

```rust
use apate::ApateConfigBuilder;
use apate::deceit::{DeceitBuilder, DeceitResponseBuilder};
use apate::test::ApateTestServer;

#[tokio::test]
async fn multi_endpoint_test() {
    let config = ApateConfigBuilder::default()
        .with_port(9321)
        .add_deceit(
            DeceitBuilder::with_uris(&["/user/add"])
                .require_method("POST")
                .add_response(
                    DeceitResponseBuilder::default()
                        .code(200)
                        .with_output(r#"{"message":"Success"}"#)
                        .build(),
                )
                .build(),
        )
        .add_deceit(
            DeceitBuilder::with_uris(&["/user/{id}"])
                .require_method("GET")
                .add_response(
                    DeceitResponseBuilder::default()
                        .code(200)
                        .add_header("Content-Type", "application/json")
                        .with_output_type(apate::output::OutputType::Jinja)
                        .with_output(r#"{"id":"{{ ctx.load_path_args().id }}"}"#)
                        .build(),
                )
                .build(),
        )
        .build();

    let _apate = ApateTestServer::start(config, 0);
    let client = reqwest::Client::new();

    let r = client.get("http://localhost:9321/user/1133").send().await.unwrap();
    assert_eq!(r.json::<serde_json::Value>().await.unwrap()["id"], "1133");
}
```

### 6.7 Registering a custom Rust `PostProcessor`

```rust
use apate::ApateConfigBuilder;
use apate::deceit::{DeceitBuilder, DeceitResponseBuilder, DeceitResponseContext};
use apate::processors::{ApateProcessor, PostProcessor, Processor};
use apate::test::ApateTestServer;

struct MySigner;
impl PostProcessor for MySigner {
    fn process(
        &self,
        _input: &[&str],
        _ctx: &DeceitResponseContext,
        response: &[u8],
    ) -> Result<Option<Vec<u8>>, Box<dyn core::error::Error>> {
        // `response` is the rendered body as bytes. Return Some(new) to replace it,
        // or None to keep the original body.
        let mut v = response.to_vec();
        v.extend(b" SIGNED");
        Ok(Some(v))
    }
}

#[test]
fn embedded_processor_test() {
    let config = ApateConfigBuilder::default()
        .register_processor(ApateProcessor::post("signer", Box::new(MySigner)))
        .add_deceit(
            DeceitBuilder::with_uris(&["/tx"])
                .add_processor(Processor::Embedded { id: "signer".into(), args: vec![] })
                .add_response(DeceitResponseBuilder::default().with_output("body").build())
                .build(),
        )
        .build();

    let _apate = ApateTestServer::start(config, 0);
    let client = reqwest::blocking::Client::new();
    let r = client.get("http://localhost:8228/tx").send().unwrap();
    assert_eq!(r.text().unwrap(), "body SIGNED");
}
```

### 6.8 Test authoring notes (gotchas)

- **Port conflicts:** all tests default to port `8228`. If you run many tests in parallel,
  either give each test a **distinct port** (via `with_port` / `to_app_config_with_port`)
  or annotate with `#[serial]` from the `serial_test` crate (the repo's own tests use
  `#[serial]`).
- The server is bound to `0.0.0.0:PORT`, so `http://localhost:PORT` always works.
- To enable verbose Apate logging in a test: `apate::test::init_env_logger("debug,apate=trace");`
- The test server does **not** require an async runtime; it spawns its own.
- For a full working reference, see `tests/test-api.rs`, `tests/scripting.rs`,
  and `tests/processors.rs`.

---

## 7. Running the Apate server

### 7.1 Install & run locally (CLI)

Install the `apate` binary from crates.io, then run it directly:

```sh
cargo install apate            # puts the `apate` binary on your $PATH
apate                          # start on default port 8228, no specs
apate -p 8228 ./spec.toml      # start on port 8228 with a TOML spec file
```

CLI arguments (higher priority than env vars) and env configuration: see §7.3.

### 7.2 Run from Docker image

Official image: `ghcr.io/rustrum/apate`. The container runs the `apate` binary, listens on
port **8228**, and starts with **no specs** (add them via UI or API). Use tag `latest` for this version.

#### 7.2.1 Run an empty server

```sh
docker run --rm -t -p 8228:8228 ghcr.io/rustrum/apate:latest
```

#### 7.2.2 Run with mounted TOML specs

Mount your specs and expose their path(s) through `APATHE_SPECS_FILE_*` env variables:

```sh
# from the apate repo root (uses ./examples as the specs dir)
docker run --rm -t -p 8228:8228 \
  -v "$(pwd)/examples:/specs" \
  -e APATHE_SPECS_FILE_1=/specs/apate-specs.toml \
  -e APATHE_SPECS_FILE_2=/specs/apate-specs-rhai.toml \
  ghcr.io/rustrum/apate:latest
```

> **Any** env variable whose name starts with `APATHE_SPECS_FILE` is treated as a path to a
> spec file. You can therefore register many specs with
> `APATHE_SPECS_FILE_1`, `APATHE_SPECS_FILE_2`, …, `APATHE_SPECS_FILE_N`.

### 7.3 Server configuration

**Env variables:**
- `RUST_LOG`, `RUST_LOG_STYLE` — logging (e.g. `RUST_LOG=info,apate=debug`)
- `APATHE_PORT` — server port (default `8228`)
- `APATHE_SPECS_FILE*` — spec file paths (see above)

**CLI arguments (higher priority than env):**
```sh
apate -p 8080 -l warn ./spec.toml ./another.toml
#   -p <port>      port to listen on
#   -l <level>     log level / filter
#   positional args = paths to spec files
```

### 7.4 Web UI & admin REST API

Base path: `http://HOST:PORT/apate` (e.g. `http://localhost:8228/apate`).

| Method | Path | Description |
|--------|------|-------------|
| `GET`  | `/apate/info` | JSON with name + version |
| `GET`  | `/apate/specs` | current specs as TOML |
| `POST` | `/apate/specs/replace` | replace all specs with TOML in request body |
| `POST` | `/apate/specs/append` | append TOML specs (request body) after existing |
| `POST` | `/apate/specs/prepend` | prepend TOML specs (request body) before existing |
| `GET`  | `/apate` | the web UI (single page app) |

Example live spec update:
```sh
curl -X POST http://localhost:8228/apate/specs/replace -d @./new-specs.toml
curl http://localhost:8228/apate/specs        # dump current specs as TOML
curl http://localhost:8228/apate/info         # {"name":"Apate API mocking server","version":"0.2.0"}
```

All `POST` spec endpoints accept a TOML document (the same shape as §2) in the request body
and return a plain-text confirmation. The specs cache (Jinja + Rhai AST) is cleared and
rebuilt on every update, so changes take effect immediately.

### 7.5 MCP API (`/mcp`) — Model Context Protocol

Apate exposes an MCP server at `http://HOST:PORT/mcp` for LLM tool integration. Two tools are available:

- **`specs_get`** — returns the current active specification as TOML text (no input required)
- **`specs_replace`** — replaces the configuration TOML with provided text and returns the stored config (requires `toml` argument)

---

## 8. Important notes for AI agent

### 8.1 Quick decision guide

- **Static / fixed response**`type = "string"` (or omit `type`).
- **Echo request data / light conditionals / randoms**`type = "jinja"`.
- **Complex logic, data filtering, stateful behaviour**`type = "rhai"` (or registry `rhai_ref`).
- **Return binary bytes**`type = "base64"` or `type = "hex"`.
- **Modify the body after render** → add a `processor` (`rhai`/`rhai_ref`/`embedded`).
- **Share a script across endpoints** → define it once in `[[rhai]]`, reference by `id`.
- **Unit-test a client against a local API** → use `ApateTestServer::start(config, 0)`.
- **Custom logic depending on Rust libraries** → should embed Apate in your app (custom server) and register a
  `PostProcessor`, reference with `type = "embedded"`.

### 8.2 DSL syntax tweaks & WTFs

TOML triple quote string is multiline and should be treated as raw text.
No need to escape quotes inside triple quotes text block.

In the next example script string has 4 lines because triple quotes are on separate lines.
```toml
script = """
let a = 1; // line number 2
let b = 2; // line number 3
"""
```
Consider it while reading error messages.

Rhai JSON objects are using CUSTOM syntax `#{ name: "John", surname: "Smith" }` with `#` prefix.