poolster-plugin-java 0.5.0-alpha.1

Native Java SDK generator for Poolster
Documentation
use super::*;

#[test]
fn emits_cursor_pagination_through_the_normal_operation() {
    let mut source = contact_api();
    source.operations[0].id = "listContacts".into();
    source.operations[0].path = "/v1/contacts".into();
    source.operations[0].request_body = None;
    source.operations[0].parameters = vec![OperationParameter {
        name: "cursor".into(),
        location: "query".into(),
        required: false,
        schema: Some(string()),
        description: None,
        annotations: BTreeMap::new(),
    }];
    source.operations[0].annotations.insert(
        "x-poolster-pagination".into(),
        serde_json::json!({
            "type": "cursor",
            "inputs": [{ "name": "cursor", "in": "parameters", "type": "cursor" }],
            "outputs": { "nextCursor": "$.data[-1].nextCursor" }
        }),
    );
    let tree = render_test_sdk(&source, "java", None).unwrap();
    let client = rendered_java(&tree);
    assert!(client.contains(
        "public java.lang.Iterable<Contact> listContactsPages(ListContactsRequest input)"
    ));
    assert!(client.contains("var page = listContacts(current);"));
    assert!(client.contains("current = ListContactsRequestWithCursor(current, cursor.asText());"));
    assert!(client.contains("protected static JsonNode poolsterJsonPath"));
    assert!(client.contains("$.data[-1].nextCursor"));
    assert!(client.contains("Integer.parseInt"));

    let namespaced =
        render_sdk(&source, "java-namespaced", None, SdkClientStyle::Namespaced).unwrap();
    let resource = namespaced
        .get("java-namespaced/src/main/java/io/poolster/poolsteremailapi/ContactsResource.java")
        .unwrap();
    assert!(resource.contains("extends ContactsResourcePart000"));
    let sources = rendered_java(&namespaced);
    assert!(sources.contains(
        "public java.lang.Iterable<Contact> listPages(Client.ListContactsRequest input)"
    ));
    assert!(sources.contains("return client.listContactsPages(input);"));
}

#[test]
fn declared_page_results_have_defaults_bounds_and_validated_selectors() {
    let mut source = contact_api();
    let operation = &mut source.operations[0];
    operation.request_body = None;
    operation.parameters = vec![OperationParameter {
        name: "page".into(),
        location: "query".into(),
        required: false,
        schema: Some(integer()),
        description: None,
        annotations: BTreeMap::new(),
    }];
    operation.responses = vec![poolster_core::OperationResponse::json(
        "200",
        SchemaValue::new(SchemaKind::Array {
            items: Box::new(SchemaValue::new(SchemaKind::String)),
        }),
    )];
    operation.annotations.insert("x-poolster-pagination".into(), serde_json::json!({"type":"page", "inputs":[{"name":"page","type":"page"}], "outputs":{"results":"$"}}));
    let rendered = rendered_java(&render_test_sdk(&source, "java", None).unwrap());
    assert!(rendered.contains("input.page() == null ?"));
    assert!(rendered.contains("(input, 1L) : input"));
    assert!(rendered.contains("results.size() == 0"));
    assert!(rendered.contains("current.page() == Long.MAX_VALUE"));
    assert!(rendered.contains("++pageCount >= 10000"));
    source.operations[0]
        .annotations
        .get_mut("x-poolster-pagination")
        .unwrap()["outputs"]["results"] = serde_json::json!("$.missing");
    assert!(render_test_sdk(&source, "java", None).is_err());
}

#[test]
fn emits_page_and_offset_pagination_only_for_safe_query_parameters() {
    let mut page = contact_api();
    page.operations[0].id = "listContactPages".into();
    page.operations[0].path = "/v1/contacts".into();
    page.operations[0].request_body = None;
    page.operations[0].parameters = vec![OperationParameter {
        name: "page".into(),
        location: "query".into(),
        required: false,
        schema: Some(integer()),
        description: None,
        annotations: BTreeMap::new(),
    }];
    page.operations[0].annotations.insert(
        "x-speakeasy-pagination".into(),
        serde_json::json!({
            "type": "offsetLimit",
            "inputs": [{ "name": "page", "in": "parameters", "type": "page" }],
            "outputs": { "numPages": "$.meta.pages" }
        }),
    );
    let tree = render_test_sdk(&page, "java", None).unwrap();
    let client = rendered_java(&tree);
    assert!(client.contains("listContactPagesPages(ListContactPagesRequest input)"));
    assert!(
        client.contains(
            "var numPages = poolsterJsonPath(mapper.valueToTree(page), \"$.meta.pages\")"
        )
    );
    assert!(client.contains("current = ListContactPagesRequestWithPage(current, nextValue);"));

    let mut offset = page;
    offset.operations[0].id = "listContactOffsets".into();
    offset.operations[0].parameters = vec![
        OperationParameter {
            name: "offset".into(),
            location: "query".into(),
            required: false,
            schema: Some(integer()),
            description: None,
            annotations: BTreeMap::new(),
        },
        OperationParameter {
            name: "limit".into(),
            location: "query".into(),
            required: false,
            schema: Some(integer()),
            description: None,
            annotations: BTreeMap::new(),
        },
    ];
    offset.operations[0].annotations.insert(
        "x-speakeasy-pagination".into(),
        serde_json::json!({
            "type": "offsetLimit",
            "inputs": [
                { "name": "offset", "in": "parameters", "type": "offset" },
                { "name": "limit", "in": "parameters", "type": "limit" }
            ],
            "outputs": { "results": "$.contacts" }
        }),
    );
    let tree = render_test_sdk(&offset, "java", None).unwrap();
    let client = rendered_java(&tree);
    assert!(
        client.contains("var results = poolsterJsonPath(mapper.valueToTree(page), \"$.contacts\")")
    );
    assert!(client.contains(
        "current = ListContactOffsetsRequestWithOffset(current, currentValue + resultCount);"
    ));
}

#[test]
fn follows_declared_url_pagination_on_the_generated_client_origin() {
    let mut source = contact_api();
    source.operations[0].id = "listContacts".into();
    source.operations[0].annotations.insert(
        "x-speakeasy-pagination".into(),
        serde_json::json!({
            "type": "url",
            "inputs": [],
            "outputs": { "nextUrl": "$.links.next" }
        }),
    );
    let tree = render_test_sdk(&source, "java", None).unwrap();
    let client = rendered_java(&tree);
    assert!(client.contains(
        "public java.lang.Iterable<Contact> listContactsPages(ListContactsRequest input)"
    ));
    assert!(client.contains("listContactsFromPaginationUrl(input, nextUrl)"));
    assert!(client.contains("requestPaginationUrlWithRetry"));
    assert!(client.contains("Poolster pagination URL must remain on the configured API origin"));
    assert!(client.contains("\"$.links.next\""));
}

#[test]
fn supports_safe_header_and_rejects_invalid_path_java_pagination_contracts() {
    let mut source = contact_api();
    source.operations[0].id = "listContacts".into();
    source.operations[0].request_body = None;
    source.operations[0].parameters = vec![OperationParameter {
        name: "cursor".into(),
        location: "header".into(),
        required: false,
        schema: Some(string()),
        description: None,
        annotations: BTreeMap::new(),
    }];
    source.operations[0].annotations.insert(
        "x-poolster-pagination".into(),
        serde_json::json!({
            "type": "cursor",
            "inputs": [{ "name": "cursor", "in": "parameters", "type": "cursor" }],
            "outputs": { "nextCursor": "$.next" }
        }),
    );
    let tree = render_test_sdk(&source, "java", None).unwrap();
    let client = rendered_java(&tree);
    assert!(client.contains("listContactsPages("));

    source.operations[0].parameters[0].location = "path".into();
    source.operations[0].parameters[0].required = false;
    source.operations[0].path = "/contacts/{cursor}".into();
    let tree = render_test_sdk(&source, "java", None).unwrap();
    let client = rendered_java(&tree);
    assert!(!client.contains("listContactsPages("));

    source.operations[0].parameters[0].location = "query".into();
    source.operations[0].annotations.insert(
        "x-poolster-pagination".into(),
        serde_json::json!({
            "type": "offsetLimit",
            "inputs": [{ "name": "cursor", "in": "parameters", "type": "offset" }],
            "outputs": { "results": "$.items" }
        }),
    );
    let tree = render_test_sdk(&source, "java", None).unwrap();
    let client = rendered_java(&tree);
    assert!(!client.contains("listContactsPages("));
}