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
9 changes: 5 additions & 4 deletions docs/reference/cli-command-catalogue.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Auto-generated from `specs/*.json.gz` via `scripts/generate_cli_command_catalogu
Lists every operation the CLI dispatches. Deprecated operations and the `internal` resource are excluded. Each row corresponds to a concrete `--api-scope` / `--api-version` combination.

**Services:** 24
**Operations:** 1961
**Operations:** 1962

## achievement

Expand Down Expand Up @@ -102,7 +102,7 @@ Lists every operation the CLI dispatches. Deprecated operations and the `interna

- Spec name: `basic`
- Resources: 5
- Operations: 57
- Operations: 58

| Resource | Method | Scope | Version | HTTP | Path | Summary |
|----------|--------|-------|---------|------|------|---------|
Expand Down Expand Up @@ -160,6 +160,7 @@ Lists every operation the CLI dispatches. Deprecated operations and the `interna
| `profiles` | `update-custom-attributes` | public | v1 | PUT | `/basic/v1/public/namespaces/{namespace}/users/{userId}/profiles/customAttributes` | Updates custom attributes for a user |
| `profiles` | `update-my` | public | v1 | PUT | `/basic/v1/public/namespaces/{namespace}/users/me/profiles` | Updates my profile |
| `profiles` | `update-my-private-custom-attributes` | public | v1 | PUT | `/basic/v1/public/namespaces/{namespace}/users/me/profiles/privateCustomAttributes` | Updates my private custom attributes |
| `profiles` | `update-my-zip-code` | public | v1 | PATCH | `/basic/v1/public/namespaces/{namespace}/users/me/profiles/zipCode` | Updates my zip code |
| `profiles` | `update-private-custom-attributes` | admin | v1 | PUT | `/basic/v1/admin/namespaces/{namespace}/users/{userId}/profiles/privateCustomAttributes` | Updates private custom attributes for a user |
| `profiles` | `update-status` | admin | v1 | PATCH | `/basic/v1/admin/namespaces/{namespace}/users/{userId}/profiles/status` | Updates user profile status |
| `profiles` | `update-status` | public | v1 | PATCH | `/basic/v1/public/namespaces/{namespace}/users/{userId}/profiles/status` | Updates user profile status |
Expand Down Expand Up @@ -1120,7 +1121,7 @@ Lists every operation the CLI dispatches. Deprecated operations and the `interna

| Resource | Method | Scope | Version | HTTP | Path | Summary |
|----------|--------|-------|---------|------|------|---------|
| `blocks` | `symc` | public | v1 | PATCH | `/lobby/sync/namespaces/{namespace}/me/block` | Syncs the current user's blocked list with linked native first-party platform accounts |
| `blocks` | `sync` | public | v1 | PATCH | `/lobby/sync/namespaces/{namespace}/me/block` | Syncs the current user's blocked list with linked native first-party platform accounts |
| `config` | `export` | admin | v1 | GET | `/lobby/v1/admin/config/namespaces/{namespace}/export` | Exports the lobby configuration to a JSON file |
| `config` | `get` | admin | v1 | GET | `/lobby/v1/admin/config/namespaces/{namespace}` | Retrieves the configuration for a specified namespace |
| `config` | `get-log` | admin | v1 | GET | `/lobby/v1/admin/config/log` | Retrieves the current log configuration |
Expand Down Expand Up @@ -1963,8 +1964,8 @@ Lists every operation the CLI dispatches. Deprecated operations and the `interna
| `stat-cycles` | `create` | admin | v1 | POST | `/social/v1/admin/namespaces/{namespace}/statCycles` | Creates a stat cycle |
| `stat-cycles` | `delete` | admin | v1 | DELETE | `/social/v1/admin/namespaces/{namespace}/statCycles/{cycleId}` | Deletes stat cycle |
| `stat-cycles` | `export` | admin | v1 | GET | `/social/v1/admin/namespaces/{namespace}/statCycles/export` | Exports all stat cycle configurations |
| `stat-cycles` | `export` | public | v1 | GET | `/social/v1/public/namespaces/{namespace}/statCycles/{cycleId}` | Retrieves a stat cycle |
| `stat-cycles` | `get` | admin | v1 | GET | `/social/v1/admin/namespaces/{namespace}/statCycles/{cycleId}` | Retrieves a stat cycle |
| `stat-cycles` | `get` | public | v1 | GET | `/social/v1/public/namespaces/{namespace}/statCycles/{cycleId}` | Retrieves a stat cycle |
| `stat-cycles` | `import` | admin | v1 | POST | `/social/v1/admin/namespaces/{namespace}/statCycles/import` | Imports stat cycle configurations |
| `stat-cycles` | `list` | admin | v1 | GET | `/social/v1/admin/namespaces/{namespace}/statCycles` | Lists stat cycles |
| `stat-cycles` | `list` | public | v1 | GET | `/social/v1/public/namespaces/{namespace}/statCycles` | Lists stat cycles |
Expand Down
Binary file modified specs/basic.json.gz
Binary file not shown.
Binary file modified specs/lobby.json.gz
Binary file not shown.
Binary file modified specs/social.json.gz
Binary file not shown.
115 changes: 105 additions & 10 deletions src/catalogue/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,20 @@ pub fn parse_spec(service_name: &str, spec: &SwaggerSpec) -> ServiceSchema {
continue;
};

let parts: Vec<&str> = x_operation_id.splitn(5, '/').collect();
if parts.len() < 5 {
// Expect exactly 5 segments: service/scope/resource/version/method.
// Both underflow (too few) and overflow (a stray slash that pushes
// the method into a 6th segment) are spec bugs. Without this
// check, `splitn` would silently fold the overflow into `parts[4]`
// and the version-parse below would fail-and-continue, dropping
// the operation from the CLI surface with no signal.
let parts: Vec<&str> = x_operation_id.split('/').collect();
if parts.len() != 5 {
debug_assert!(
false,
"x-operationId must have exactly 5 segments \
(service/scope/resource/version/method), got {} in '{x_operation_id}'",
parts.len(),
);
continue;
}
let scope = parts[1].to_string();
Expand All @@ -62,14 +74,22 @@ pub fn parse_spec(service_name: &str, spec: &SwaggerSpec) -> ServiceSchema {
continue;
}
let method_name = parts[4].to_string();
// Parse the version integer from "v1", "v2", etc. Skip the
// operation entirely on a non-numeric segment rather than
// coercing to 0 — a `0` here would create a ghost contract
// selectable as `--api-version v0` and could collide with a
// sibling on the duplicate-key panic.
// Parse the version integer from "v1", "v2", etc. A non-numeric
// segment here means the spec is malformed — assert in debug so it
// surfaces during development, skip in release so one bad op does
// not poison the whole service. Coercing to 0 would create a
// ghost contract selectable as `--api-version v0`.
let api_version: ApiVersion = match parts[3].parse() {
Ok(v) => v,
Err(_) => continue,
Err(_) => {
debug_assert!(
false,
"x-operationId version segment must be numeric (e.g. 'v1' style is allowed \
only if ApiVersion parses it), got '{}' in '{x_operation_id}'",
parts[3],
);
continue;
}
};

let http_method_upper = http_method_name.to_uppercase();
Expand Down Expand Up @@ -749,9 +769,21 @@ mod tests {
assert!(names.contains(&"prefs"));
}

/// Malformed `x-operationId` values should be skipped instead of panicking.
/// An `x-operationId` with fewer than 5 segments is malformed and should
/// trip the debug assertion so spec drift surfaces during development.
#[test]
#[cfg(debug_assertions)]
#[should_panic(expected = "x-operationId must have exactly 5 segments")]
fn test_parser_underflow_x_operation_id_asserts_in_debug() {
let spec = spec_from(&[("/api/v1/bans", "get", "too/few/parts", false, "")]);
let _ = parse_spec("test", &spec);
}

/// In release builds, underflow x-operationId values are skipped so one
/// bad entry does not poison the whole service.
#[test]
fn test_skips_malformed_x_operation_id() {
#[cfg(not(debug_assertions))]
fn test_parser_underflow_x_operation_id_skips_in_release() {
let spec = spec_from(&[
("/api/v1/bans", "get", "too/few/parts", false, ""),
("/api/v1/users", "get", "svc/admin/users/v1/list", false, ""),
Expand All @@ -761,6 +793,69 @@ mod tests {
assert_eq!(service.resources[0].name, "users");
}

/// An `x-operationId` with more than 5 segments — a real spec bug seen in
/// `basic/public/profiles/update/v1/my-zip-code` where a stray slash split
/// the method name — must trip the debug assertion. Previously this was
/// silently dropped because `splitn(5, '/')` folded the overflow into the
/// last segment and the version-parse fail path skipped the op.
#[test]
#[cfg(debug_assertions)]
#[should_panic(expected = "x-operationId must have exactly 5 segments")]
fn test_parser_overflow_x_operation_id_asserts_in_debug() {
let spec = spec_from(&[(
"/basic/v1/public/namespaces/{namespace}/users/me/profiles/zipCode",
"put",
"basic/public/profiles/update/v1/my-zip-code",
false,
"",
)]);
let _ = parse_spec("basic", &spec);
}

/// In release builds, overflow x-operationId values are skipped (the
/// operation is omitted from the CLI surface) rather than panicking.
#[test]
#[cfg(not(debug_assertions))]
fn test_parser_overflow_x_operation_id_skips_in_release() {
let spec = spec_from(&[
(
"/basic/v1/public/namespaces/{namespace}/users/me/profiles/zipCode",
"put",
"basic/public/profiles/update/v1/my-zip-code",
false,
"",
),
("/api/v1/users", "get", "svc/admin/users/v1/list", false, ""),
]);
let service = parse_spec("basic", &spec);
assert_eq!(service.resources.len(), 1);
assert_eq!(service.resources[0].name, "users");
}

/// A non-numeric version segment in an otherwise well-shaped (5-segment)
/// `x-operationId` should trip the debug assertion as a spec bug.
#[test]
#[cfg(debug_assertions)]
#[should_panic(expected = "x-operationId version segment must be numeric")]
fn test_parser_non_numeric_version_asserts_in_debug() {
let spec = spec_from(&[("/api/v1/bans", "get", "svc/admin/bans/vX/list", false, "")]);
let _ = parse_spec("test", &spec);
}

/// In release builds, a non-numeric version segment skips the operation
/// instead of panicking — the rest of the service still loads.
#[test]
#[cfg(not(debug_assertions))]
fn test_parser_non_numeric_version_skips_in_release() {
let spec = spec_from(&[
("/api/v1/bans", "get", "svc/admin/bans/vX/list", false, ""),
("/api/v1/users", "get", "svc/admin/users/v1/list", false, ""),
]);
let service = parse_spec("test", &spec);
assert_eq!(service.resources.len(), 1);
assert_eq!(service.resources[0].name, "users");
}

/// All non-deprecated versions of a contract should be preserved and the
/// highest version should become the default.
#[test]
Expand Down
15 changes: 15 additions & 0 deletions tests/fixtures/baselines/basic_input_contract.json
Original file line number Diff line number Diff line change
Expand Up @@ -986,6 +986,21 @@
"summary": "Updates my private custom attributes",
"x_operation_id": "basic/public/profiles/v1/update-my-private-custom-attributes"
},
{
"http_method": "PATCH",
"method": "update-my-zip-code",
"parameters": [
{
"location": "path",
"name": "namespace",
"required": true
}
],
"path": "/basic/v1/public/namespaces/{namespace}/users/me/profiles/zipCode",
"permissions": [],
"summary": "Updates my zip code",
"x_operation_id": "basic/public/profiles/v1/update-my-zip-code"
},
{
"http_method": "PUT",
"method": "update-private-custom-attributes",
Expand Down
4 changes: 2 additions & 2 deletions tests/fixtures/baselines/lobby_input_contract.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"blocks": [
{
"http_method": "PATCH",
"method": "symc",
"method": "sync",
"parameters": [
{
"location": "path",
Expand All @@ -13,7 +13,7 @@
"path": "/lobby/sync/namespaces/{namespace}/me/block",
"permissions": [],
"summary": "Syncs the current user's blocked list with linked native first-party platform accounts",
"x_operation_id": "lobby/public/blocks/v1/symc"
"x_operation_id": "lobby/public/blocks/v1/sync"
}
],
"config": [
Expand Down
10 changes: 5 additions & 5 deletions tests/fixtures/baselines/social_input_contract.json
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@
},
{
"http_method": "GET",
"method": "export",
"method": "get",
"parameters": [
{
"location": "path",
Expand All @@ -217,10 +217,10 @@
"required": true
}
],
"path": "/social/v1/public/namespaces/{namespace}/statCycles/{cycleId}",
"path": "/social/v1/admin/namespaces/{namespace}/statCycles/{cycleId}",
"permissions": [],
"summary": "Retrieves a stat cycle",
"x_operation_id": "social/public/stat-cycles/v1/export"
"x_operation_id": "social/admin/stat-cycles/v1/get"
},
{
"http_method": "GET",
Expand All @@ -237,10 +237,10 @@
"required": true
}
],
"path": "/social/v1/admin/namespaces/{namespace}/statCycles/{cycleId}",
"path": "/social/v1/public/namespaces/{namespace}/statCycles/{cycleId}",
"permissions": [],
"summary": "Retrieves a stat cycle",
"x_operation_id": "social/admin/stat-cycles/v1/get"
"x_operation_id": "social/public/stat-cycles/v1/get"
},
{
"http_method": "POST",
Expand Down
Loading