# API Schema Discovery (`routeros.introspect`)

`routeros.discover_api()` and `routeros.load_api_schema()` let you discover,
cache, and reload the full RouterOS API tree as a structured JSON schema.

The output format is **identical to the MikroTik REST-API `inspect.json`** — a
nested tree of `_type: "dir"`, `"cmd"`, and `"arg"` nodes.

---

## Functions

### `routeros.discover_api(client, *, data_dir, force, logger) → dict`

Walk the RouterOS API tree and persist the result to disk.

| Parameter | Type | Default | Description |
|---|---|---|---|
| `client` | `Client` | — | A connected, logged-in `Client` |
| `data_dir` | `str \| Path \| None` | `data/mikrotik/api/` | Directory for cached JSON files |
| `force` | `bool` | `False` | Re-discover even if a cached file exists |
| `logger` | `logging.Logger \| None` | `None` | Custom logger |

**Returns:** `dict` with keys `version`, `generated_at`, `endpoint_count`, `tree`

**Raises:** `RouterOSError` if the device version cannot be determined

```python
import routeros

with routeros.dial("192.168.1.1:8728", "admin", "secret") as c:
    schema = routeros.discover_api(c)

print(schema["version"])         # "7.20.8"
print(schema["endpoint_count"])  # 541
```

---

### `routeros.load_api_schema(version, *, data_dir) → dict | None`

Load a previously saved schema from disk without a live device connection.

| Parameter | Type | Default | Description |
|---|---|---|---|
| `version` | `str` | — | RouterOS version string, e.g. `"7.20.8"` |
| `data_dir` | `str \| Path \| None` | `data/mikrotik/api/` | Directory to search |

**Returns:** schema `dict` or `None` if no file exists for that version.

```python
schema = routeros.load_api_schema("7.20.8")
if schema is None:
    print("No cached schema — run discover_api() first")
```

---

## Schema format

The saved file is `routeros_<version>.json` with the following top-level structure:

```json
{
  "version": "7.20.8",
  "generated_at": "2026-03-21T14:00:00+00:00",
  "endpoint_count": 541,
  "tree": { }
}
```

### `tree` — nested API tree

`tree` mirrors the MikroTik REST-API `inspect.json` format exactly.
Every node carries a `_type` field:

| `_type` | Meaning | Contains |
|---|---|---|
| `"dir"` | Navigable submenu / path | other `dir`, `cmd` nodes |
| `"path"` | Navigable path (alias for `dir`) | other `dir`, `cmd` nodes |
| `"cmd"` | Executable command | `arg` nodes (one per parameter) |
| `"arg"` | Command parameter | optional `"desc"` string |

`"desc"` is present only when the router provides a non-empty description.

### Example tree snippet

```json
{
  "ip": {
    "_type": "dir",
    "address": {
      "_type": "dir",
      "add": {
        "_type": "cmd",
        "address":   {"_type": "arg", "desc": "A.B.C.D    (IP address)"},
        "interface": {"_type": "arg", "desc": "string value"},
        "disabled":  {"_type": "arg"},
        "comment":   {"_type": "arg", "desc": "string value"}
      },
      "print": {
        "_type": "cmd",
        "brief":          {"_type": "arg"},
        "detail":         {"_type": "arg"},
        "count-only":     {"_type": "arg"},
        "without-paging": {"_type": "arg"},
        "where":          {"_type": "arg"},
        "interval":       {"_type": "arg", "desc": "00:00:00.200..    (time interval)"}
      },
      "remove": {
        "_type": "cmd",
        "numbers": {"_type": "arg", "desc": "see documentation"}
      }
    },
    "hotspot": {
      "_type": "dir",
      "user": {
        "_type": "dir",
        "add": {
          "_type": "cmd",
          "name":     {"_type": "arg", "desc": "string value"},
          "password": {"_type": "arg", "desc": "string value"},
          "profile":  {"_type": "arg"},
          "address":  {"_type": "arg", "desc": "A.B.C.D    (IP address)"}
        },
        "print":   {"_type": "cmd", "brief": {"_type": "arg"}, "detail": {"_type": "arg"}},
        "remove":  {"_type": "cmd", "numbers": {"_type": "arg", "desc": "see documentation"}},
        "enable":  {"_type": "cmd", "numbers": {"_type": "arg", "desc": "see documentation"}},
        "disable": {"_type": "cmd", "numbers": {"_type": "arg", "desc": "see documentation"}}
      }
    }
  }
}
```

### Navigating the tree

```python
import routeros

with routeros.dial("192.168.1.1:8728", "admin", "secret") as c:
    schema = routeros.discover_api(c)

tree = schema["tree"]

# Check if a path exists
if "ip" in tree and "firewall" in tree["ip"]:
    fw = tree["ip"]["firewall"]

# List all commands at a path
ip_addr = tree["ip"]["address"]
commands = [
    k for k, v in ip_addr.items()
    if isinstance(v, dict) and v.get("_type") == "cmd"
]
print(commands)
# ['add', 'comment', 'disable', 'enable', 'find', 'get', 'print', 'remove', 'set', ...]

# List parameters of the 'add' command with their descriptions
add_cmd = ip_addr["add"]
for name, node in add_cmd.items():
    if name == "_type":
        continue
    desc = node.get("desc", "")
    print(f"  {name}: {desc}" if desc else f"  {name}")
```

---

## Discovery strategies

Two strategies are attempted in order:

### 1. REST-API `inspect.json` (preferred)

If MikroTik REST-API files are available under
`data/mikrotik/rest-api/<version>/`, they are loaded and **deep-merged**:

```
data/mikrotik/rest-api/
└── 7.20.7/
    ├── inspect.json        ← main tree
    └── extra/
        └── inspect.json    ← extended tree (caps-man, container, …)
```

Both files are deep-merged into a single tree. The `extra/inspect.json`
adds paths like `caps-man`, `container`, `dude`, `iot`, `lora` that are
absent from the main file, giving the richest possible result.

**Partial version matching:** if `"7.17"` is not an exact directory name,
the first directory starting with `"7.17."` (e.g. `"7.17.1"`) is used.

This strategy requires no live device interaction beyond version detection
and produces the most complete output (full parameter descriptions sourced
from MikroTik data).

### 2. Binary-API BFS (fallback)

When no `inspect.json` is available, the library walks the live device's
`/console/inspect` endpoint using **comma-separated paths**:

```
/console/inspect request=child path=
/console/inspect request=child path=ip
/console/inspect request=child path=ip,address
/console/inspect request=syntax path=ip,address,add
/console/inspect request=syntax path=ip,address,print
...
```

Each `request=syntax` call returns the parameter list with `symbol` (name)
and `text` (description) for that specific command. Empty `desc` values
are omitted from the output.

> **Note:** some RouterOS builds close the connection after a `request=syntax`
> call on certain paths (`!fatal`). The library catches both `RouterOSError`
> and `OSError`, records the command with an empty arg list, and continues.

---

## Caching

The schema is saved to `<data_dir>/routeros_<version>.json` after the first
successful discovery. Subsequent calls return the cached file immediately —
only the version-detection call (`/system/resource/print`) is made.

```python
# First call — discovers and saves to disk
schema = routeros.discover_api(c)

# Second call — loads from disk (fast, no BFS)
schema = routeros.discover_api(c)

# Force fresh discovery regardless of cache
schema = routeros.discover_api(c, force=True)
```

---

## Custom data directory

```python
from pathlib import Path
import routeros

data_dir = Path("/var/cache/routeros-schemas")

with routeros.dial("192.168.1.1:8728", "admin", "secret") as c:
    schema = routeros.discover_api(c, data_dir=data_dir)

# Load later without a connection
schema = routeros.load_api_schema("7.20.8", data_dir=data_dir)
```

---

## Logging

Set `logging.DEBUG` to see step-by-step discovery progress:

```python
import logging
import routeros

logging.basicConfig(level=logging.DEBUG)

with routeros.dial("192.168.1.1:8728", "admin", "secret") as c:
    schema = routeros.discover_api(c)
```

Example output:

```
DEBUG routeros.introspect: RouterOS version: 7.20.8
INFO  routeros.introspect: Loaded REST-API inspect.json: 541 dir nodes
INFO  routeros.introspect: Saved 541 endpoints to .../routeros_7.20.8.json
```
