Files

203 lines
7.7 KiB
Markdown

# Mining Structured API Documentation from SPA Doc Sites
When researching third-party APIs, their docs are often served as single-page applications (apidoc-generated, Slate, ReadMe.io, SwaggerUI). The visible browser page is JS-rendered, but the *raw data payload* is typically available as static JSON/JS files. This reference documents techniques to find and extract them.
## Common SPA Doc Frameworks & Their Data Files
### 1. apidocjs (http://apidocjs.com)
**Framework**: Uses Handlebars templates + a JSON data file. The page is an SPA that loads all endpoint data into memory.
**Data files to look for**:
| File | Contents |
|------|----------|
| `api_data.json` | Array of endpoint objects with type, url, title, parameters, responses, permissions |
| `api_data.js` | Same data wrapped in `define({ "api": [...] })` (RequireJS/AMD) |
| `api_project.json` | Project metadata: name, version, URL, group ordering |
| `api_project.js` | Project metadata in AMD wrapper |
| `api_doc_collection.json` | Postman v2 collection with bodies, headers, URLs, test scripts |
**Extraction technique**:
```bash
# Try the JSON version first (cleaner)
curl -sL "https://apidocs.example.com/api_data.json" | python3 -m json.tool
# If 404, try the JS version (has trailing commas — needs cleanup)
curl -sL "https://apidocs.example.com/api_data.js" -o /tmp/data.js
# Strip the AMD wrapper and fix trailing commas
python3 -c "
import re, json
with open('/tmp/data.js') as f:
content = f.read()
# Extract JSON array from define({ 'api': [...] })
match = re.search(r'define\(\{ \"api\": (\[.*?\])\s*\}\);?\s*$', content, re.DOTALL)
if match:
raw = match.group(1)
else:
# Fallback: find outermost array
start = content.index('[{')
end = content.rindex('}]') + 2
raw = content[start:end]
# Fix trailing commas (json doesn't allow them)
raw = re.sub(r',\s*}', '}', raw)
raw = re.sub(r',\s*]', ']', raw)
data = json.loads(raw)
print(json.dumps(data, indent=2)[:5000])
"
```
**What you get**: Full endpoint enumeration with:
- HTTP method, URL template
- Required/optional parameters with types
- Response field schemas
- Permission/scoping levels
- Success/error response patterns
- Example request bodies
### 2. Postman Collections
Many doc sites offer a Postman v2 collection JSON as a downloadable test suite.
**Finding it**: Check for:
- `api_doc_collection.json`
- `postman_collection.json`
- A "Download Postman Collection" link in the docs
**Extraction**:
```python
import json
with open('/tmp/postman.json') as f:
data = json.load(f)
def find_all_items(items):
results = []
for item in items:
name = item.get('name', '')
if 'item' in item:
results.extend(find_all_items(item['item']))
else:
# Leaf node = actual API endpoint
results.append((name, item))
return results
items = find_all_items(data.get('item', []))
# Group endpoints by folder
folders = {}
for item in data.get('item', []):
folder_name = item.get('name', '')
if 'item' in item:
for ep in item['item']:
# Extract URL template
req = ep.get('request', {})
url = req.get('url', {})
if isinstance(url, dict):
url_template = url.get('raw', 'N/A')
else:
url_template = str(url)
# Extract form params
body = req.get('body', {})
params = []
if isinstance(body, dict):
for mode in ['formdata', 'urlencoded']:
for p in body.get(mode, []):
params.append(p.get('key'))
folders.setdefault(folder_name, [])
folders[folder_name].append({
'name': ep.get('name'),
'method': req.get('method', 'POST'),
'url': url_template,
'params': params
})
```
**What you get** from Postman collections that raw API JSON may not include:
- Actual request body templates with placeholder values
- Real URL patterns including base URL templates (`{{api_url}}`)
- Authorization header templates
- Response status codes and example bodies
- Pre-request scripts and test assertions
### 3. Other Doc Frameworks — Quick Reference
| Framework | Look for |
|-----------|----------|
| **Swagger/OpenAPI 2.0** | `swagger.json`, `api-docs`, `swagger.yaml` |
| **OpenAPI 3.0** | `openapi.json`, `openapi.yaml`, `v3/api-docs` |
| **Slate/Middleman** | No JSON payload — scrape rendered HTML or look for `source/includes/` |
| **ReadMe.io** | Fetch with `?format=json` param, or look for `__NEXT_DATA__` in page HTML |
| **Redoc/RapiDoc** | Usually loads an external OpenAPI spec URL — find the `<redoc spec-url="...">` in HTML |
| **Stoplight/Elements** | `<elements-api apiDescriptionUrl="...">` in page source |
## Parsing the Project Metadata
The `api_project.json` or `api_project.js` tells you the API's:
- Base URL (`url` field)
- API version
- Ordered endpoint groups (`order` array)
- Auth mechanism description (in `header.content`)
```python
# Sample extraction
with open('/tmp/api_project.json') as f:
project = json.load(f)
base_url = project.get('url', 'N/A')
api_name = project.get('name', 'N/A')
version = project.get('version', 'N/A')
groups_in_order = project.get('order', [])
```
## Grouping and Aggregation
Once you have the full endpoint list, aggregate by group to produce a high-level inventory:
```
=== Agent (6 endpoints)
POST ?object=agent&action=count | Count Agents | Scope: Reseller
POST ?object=agent&action=create | Create an Agent | Scope: Reseller
POST ?object=agent&action=delete | Delete Agents | Scope: Reseller
POST ?object=agent&action=read | Read Agents | Scope: OMP
POST ?object=agent&action=update | Update Agents | Scope: Reseller
=== Domain (6 endpoints)
POST ?object=domain&action=count | Count Domains | Scope: OMP
POST ?object=domain&action=create | Create Domain | Scope: OMP
POST ?object=domain&action=read | Read Domain | Scope: OMP
POST ?object=domain&action=read&billing=yes | Read Billing | Scope: OMP
POST ?object=domain&action=update | Update Domain | Scope: OMP
POST ?object=domain&action=delete | Delete Domain | Scope: OMP
```
## Auth & Scope Mapping
Most API doc sites define permission/scope levels per endpoint. Extract a scope matrix:
```python
scopes = {}
for entry in data:
perm_names = [p['name'] for p in entry.get('permission', [])]
group = entry.get('group', '')
for perm in perm_names:
scopes.setdefault(perm, set()).add(group)
for scope, groups in sorted(scopes.items()):
print(f"{scope}: {', '.join(sorted(groups))}")
```
This tells you which auth level can access which functional area — critical for integration planning.
## Pitfalls
- **api_data.json with trailing commas**: Not valid JSON. The JS AMD wrapper version (`api_data.js`) often has the same problem. Always fix trailing commas before parsing.
- **Huge files**: Some doc sites have 800K+ JSON files with 30K+ lines. Don't dump the whole thing at once — parse programmatically and extract the structured summary.
- **URL templates**: URLs in the JSON often have template variables like `{{api_url}}` or use relative paths (`?object=x&action=y`). The base URL is in `api_project.json`.
- **Missing groups**: Swagger/OpenAPI may have paths that don't correspond to clean groups — you'll need to aggregate by tag or path prefix.
- **Authentication**: Doc site data files are public and contain endpoint schemas but NOT real credentials. Don't confuse the two.
- **Version drift**: The Postman collection and api_data.json may be out of sync. Cross-reference when possible.