Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
8 changes: 6 additions & 2 deletions rust/platform-app-client/graphql/ApplicationsList.graphql
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
query ApplicationsList($status: ApplicationStatusFilter) {
applications(status: $status) {
query ApplicationsList($status: ApplicationStatusFilter, $first: Int, $after: String) {
applications(status: $status, first: $first, after: $after) {
edges {
node {
id
Expand All @@ -11,5 +11,9 @@ query ApplicationsList($status: ApplicationStatusFilter) {
proxyUrl
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
5 changes: 4 additions & 1 deletion rust/platform-app-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,8 @@ mod tests {
"edges": [
{ "node": { "id": "a1", "label": "Active", "name": "active-app", "description": null, "status": "ACTIVE", "url": null, "proxyUrl": null } },
{ "node": { "id": "a2", "label": "Inactive", "name": "inactive-app", "description": null, "status": "INACTIVE", "url": null, "proxyUrl": null } }
]
],
"pageInfo": { "hasNextPage": false, "endCursor": null }
}
}
}));
Expand All @@ -254,6 +255,8 @@ mod tests {
in_: Some(statuses),
ne: None,
}),
first: None,
after: None,
};

let data = client_for(&server)
Expand Down
2 changes: 1 addition & 1 deletion rust/src/api/guides/api-explorer.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ gddy api parameter list --operation <operationId>
gddy api response list --operation <operationId>
```

Both support the standard `--limit`/`--offset` flags if there are more rows than fit on one page. To see one parameter or response's full detail on its own:
Both support the standard `--limit`/`--continue` flags if there are more rows than fit on one page (or `--offset` to jump directly to a position). To see one parameter or response's full detail on its own:

```
gddy api parameter get <name> --operation <operationId>
Expand Down
94 changes: 67 additions & 27 deletions rust/src/api/parameter.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
//! `api parameter list` and `api parameter get`.

use cli_engine::{
CommandResult, CommandSpec, NextActionParam, PaginationConfig, RuntimeCommandSpec, TableColumn,
CommandResult, CommandSpec, CursorConfig, NextActionParam, RuntimeCommandSpec, TableColumn,
Tier,
};
use serde_json::json;

use crate::next_action::{next_action, required_value};
use crate::pagination_token;

use super::catalog::{
catalog, graphql_operation_redirect_error, resolve_graphql_operation, resolve_operation,
Expand All @@ -21,10 +22,14 @@ struct ParameterListArgs {
/// Operation ID to list parameters for (see `api operation list`).
#[arg(long, value_name = "OPERATION")]
operation: String,

/// Jump directly to an offset in the collection. Ignored when `--continue` is given.
#[arg(long, value_name = "N")]
offset: Option<i64>,
Comment thread
jpage-godaddy marked this conversation as resolved.
Resolved
}

pub(super) fn list_command() -> RuntimeCommandSpec {
RuntimeCommandSpec::new_typed::<ParameterListArgs, _, _, _>(
RuntimeCommandSpec::new_typed_with_context::<ParameterListArgs, _, _, _>(
CommandSpec::from_args::<ParameterListArgs>("list", "List an operation's parameters")
.with_long(
"Summarizes every parameter for an operation — query/path/header/cookie/body. \
Expand All @@ -42,18 +47,29 @@ pub(super) fn list_command() -> RuntimeCommandSpec {
.with_system("api")
.with_tier(Tier::Read)
.no_auth(true)
.with_pagination(PaginationConfig::new(
.with_cursor(CursorConfig::new(
OPERATION_CHILD_LIST_DEFAULT_LIMIT as i64,
OPERATION_CHILD_LIST_MAX_LIMIT,
)),
|_cred, args: ParameterListArgs| async move {
|ctx, args: ParameterListArgs| async move {
let operation = args.operation.as_str();
let catalog = catalog();
if resolve_graphql_operation(catalog, operation).is_some() {
return Err(graphql_operation_redirect_error(operation, "graphql get"));
}
let (domain, ep) = resolve_operation(catalog, operation, None)?;
let all = summarize_parameters(ep, &domain.defs);
let items = to_json(all)?.as_array().cloned().unwrap_or_default();
let continue_token = pagination_token::resolve_continue_token(
args.offset,
ctx.middleware.continue_token.as_deref(),
);
let (page, continuation) = pagination_token::paginate_known_collection(
pagination_token::filter_known_collection(items, &ctx.middleware.filter)?,
ctx.middleware.cursor_limit,
OPERATION_CHILD_LIST_MAX_LIMIT,
continue_token.as_deref(),
);
let next_actions = vec![
next_action(
"api parameter get <name> --operation <operation>",
Expand All @@ -62,7 +78,9 @@ pub(super) fn list_command() -> RuntimeCommandSpec {
.with_param("name", NextActionParam::required())
.with_param("operation", required_value(ep.operation_id.clone())),
];
Ok(CommandResult::new(json!(all)).with_next_actions(next_actions))
Ok(CommandResult::new(json!(page))
.with_cursor(continuation)
.with_next_actions(next_actions))
},
)
}
Expand Down Expand Up @@ -215,31 +233,31 @@ mod tests {
let rendered: serde_json::Value =
serde_json::from_str(&output.rendered).expect("valid json");
assert_eq!(rendered["data"].as_array().expect("data array").len(), 5);
let pagination = &rendered["pagination"];
assert_eq!(pagination["total"], json!(26));
assert_eq!(pagination["offset"], json!(20));
assert_eq!(pagination["limit"], json!(5));
assert_eq!(pagination["count"], json!(5));
let cursor = &rendered["cursor"];
assert_eq!(cursor["total"], json!(26));
assert_eq!(cursor["limit"], json!(5));
assert_eq!(cursor["count"], json!(5));
// 20 + 5 = 25 < 26, so one more remains.
assert_eq!(pagination["has_more"], json!(true));
assert_eq!(cursor["has_more"], json!(true));
assert_eq!(cursor["remaining"], json!(1));
let next_actions = rendered["next_actions"].as_array().expect("next_actions");
assert!(
next_actions
.iter()
.any(|a| a["command"].as_str().unwrap_or("").contains("--offset 25")),
next_actions.iter().any(|a| a["command"]
.as_str()
.unwrap_or("")
.contains("--continue \"limit:5,offset:25\"")),
"expected an auto-generated next-page hint: {}",
output.rendered
);
}

/// `--limit 0` means "all" per cli-engine's own generated help text for
/// `with_pagination` — must show every item, not zero. With both
/// `--limit`/`--offset` at their "disabled" value (0), cli-engine 0.8's
/// pipeline skips pagination entirely rather than reporting a page of
/// everything, so there's no `pagination` envelope field at all here.
/// The next-page action the engine advertises after a direct `--offset`
/// jump replays that `--offset` next to the new `--continue`. Executing it
/// exactly as printed must advance to the following page, not fail to
/// parse and not land on the same page again.
#[tokio::test]
async fn parameter_list_limit_zero_means_all() {
let output = operation_cli()
async fn parameter_list_next_action_after_an_offset_jump_advances() {
let first = operation_cli()
.run([
"gddy",
"api",
Expand All @@ -248,16 +266,38 @@ mod tests {
"--operation",
"get_transaction_disputes",
"--limit",
"0",
"5",
"--offset",
"20",
"--output",
"json",
])
.await;
assert_eq!(output.exit_code, 0, "{}", output.rendered);
let rendered: serde_json::Value =
serde_json::from_str(&output.rendered).expect("valid json");
assert_eq!(rendered["data"].as_array().expect("data array").len(), 26);
assert!(rendered.get("pagination").is_none(), "{}", output.rendered);
assert_eq!(first.exit_code, 0, "{}", first.rendered);
let first: serde_json::Value = serde_json::from_str(&first.rendered).expect("valid json");
let command = first["next_actions"]
.as_array()
.and_then(|actions| {
actions
.iter()
.filter_map(|a| a["command"].as_str())
.find(|c| c.contains("--continue"))
})
.expect("a next-page action");

let argv: Vec<&str> = command
.split_whitespace()
.map(|part| part.trim_matches('"'))
.chain(["--output", "json"])
.collect();
let second = operation_cli().run(argv).await;
assert_eq!(second.exit_code, 0, "{command}: {}", second.rendered);
let second: serde_json::Value = serde_json::from_str(&second.rendered).expect("valid json");

// 26 rows: offsets 20..25 were page one, so only offset 25 remains.
assert_eq!(second["data"].as_array().expect("data array").len(), 1);
assert_eq!(second["cursor"]["has_more"], json!(false));
assert_ne!(first["data"], second["data"]);
}

/// `max_limit` (`OPERATION_CHILD_LIST_MAX_LIMIT`) rejects an
Expand Down
28 changes: 23 additions & 5 deletions rust/src/api/response.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
//! `api response list` and `api response get`.

use cli_engine::{
CommandResult, CommandSpec, NextActionParam, PaginationConfig, RuntimeCommandSpec, TableColumn,
CommandResult, CommandSpec, CursorConfig, NextActionParam, RuntimeCommandSpec, TableColumn,
Tier,
};
use serde_json::json;

use crate::next_action::{next_action, required_value};
use crate::pagination_token;

use super::catalog::{catalog, resolve_operation};
use super::summary::{
Expand All @@ -18,10 +19,14 @@ struct ResponseListArgs {
/// Operation ID to list responses for (see `api operation list`).
#[arg(long, value_name = "OPERATION")]
operation: String,

/// Jump directly to an offset in the collection. Ignored when `--continue` is given.
#[arg(long, value_name = "N")]
offset: Option<i64>,
}

pub(super) fn list_command() -> RuntimeCommandSpec {
RuntimeCommandSpec::new_typed::<ResponseListArgs, _, _, _>(
RuntimeCommandSpec::new_typed_with_context::<ResponseListArgs, _, _, _>(
CommandSpec::from_args::<ResponseListArgs>("list", "List an operation's responses")
.with_long(
"Lists every response on one operation by status code. Use \
Expand All @@ -37,15 +42,26 @@ pub(super) fn list_command() -> RuntimeCommandSpec {
.with_system("api")
.with_tier(Tier::Read)
.no_auth(true)
.with_pagination(PaginationConfig::new(
.with_cursor(CursorConfig::new(
OPERATION_CHILD_LIST_DEFAULT_LIMIT as i64,
OPERATION_CHILD_LIST_MAX_LIMIT,
)),
|_cred, args: ResponseListArgs| async move {
|ctx, args: ResponseListArgs| async move {
let operation = args.operation.as_str();
let catalog = catalog();
let (domain, ep) = resolve_operation(catalog, operation, None)?;
let all = response_rows(ep, &domain.defs);
let items = to_json(all)?.as_array().cloned().unwrap_or_default();
let continue_token = pagination_token::resolve_continue_token(
args.offset,
ctx.middleware.continue_token.as_deref(),
);
let (page, continuation) = pagination_token::paginate_known_collection(
pagination_token::filter_known_collection(items, &ctx.middleware.filter)?,
ctx.middleware.cursor_limit,
OPERATION_CHILD_LIST_MAX_LIMIT,
continue_token.as_deref(),
);
let next_actions = vec![
next_action(
"api response get <status> --operation <operation>",
Expand All @@ -54,7 +70,9 @@ pub(super) fn list_command() -> RuntimeCommandSpec {
.with_param("status", NextActionParam::required())
.with_param("operation", required_value(ep.operation_id.clone())),
];
Ok(CommandResult::new(json!(all)).with_next_actions(next_actions))
Ok(CommandResult::new(json!(page))
.with_cursor(continuation)
.with_next_actions(next_actions))
},
)
}
Expand Down
Loading
Loading