poolster-plugin-java 0.5.0-alpha.1

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

#[test]
fn retry_headers_preserve_zero_precedence_dates_and_bounds() {
    let tree = render_test_sdk(&contact_api(), "java", Some("com.poolster.email")).unwrap();
    let client = rendered_java(&tree);
    assert!(client.contains("firstValue(\"retry-after-ms\")"));
    assert!(client.contains("retryAfterMillisDelay(retryAfterMillis)"));
    assert!(client.contains("retryAllowed(method, headers, null)"));
    assert!(client.contains("entry.getValue() != null && !entry.getValue().isBlank()"));
    assert!(!client.contains("retryAllowed(method, headers)"));
    assert!(!client.contains("retryDelay(attempt, null)"));
    assert!(!client.contains("retryDelay(attempt, error.retryAfter())"));
    assert!(client.contains("if (delay < 0) delay = retryAfterDelay(retryAfter)"));
    assert!(client.contains("delay = Math.min(delay, cap)"));
    assert!(client.contains("DateTimeFormatter.RFC_1123_DATE_TIME"));
    assert!(client.contains("!Double.isFinite(milliseconds) || milliseconds < 0"));
    let error = super::api_exception("example");
    assert!(error.contains("this(statusCode, responseBody, retryAfter, null)"));
    assert!(error.contains("public String retryAfterMillis()"));
}

#[test]
#[ignore = "requires a JDK 17 toolchain; executes emitted retry parsers without dependencies"]
fn generated_retry_parsers_execute_with_jdk() {
    use std::process::Command;
    let client = super::render_client_base(&contact_api(), "example")
        .replace(
            include_str!("../../templates/call_options_methods.java.tmpl"),
            "",
        )
        .replace(
            include_str!("../../templates/sequential_json.java.tmpl"),
            "",
        );
    let start = client
        .find("    protected static long retryAfterMillisDelay(")
        .unwrap();
    let end = client.rfind("\n}").unwrap();
    let eligibility_start = client
        .find("    protected static boolean retryAllowed(")
        .unwrap();
    let eligibility_end = client
        .find("    protected static boolean retryableStatus(")
        .unwrap();
    let parsers = format!(
        "{}\n{}",
        &client[eligibility_start..eligibility_end],
        &client[start..end]
    );
    let probe = format!(
        "import java.util.Map;\nimport java.util.Locale;\npublic class ParserProbe {{\n{parsers}\n{}\n}}",
        r#"
    static void check(boolean value) { if (!value) throw new AssertionError(); }
    public static void main(String[] args) {
        var closed = new java.util.concurrent.atomic.AtomicBoolean();
        try (var events = ssePayloads(java.util.stream.Stream.of(": keepalive", "event: update", "id: 1", "data: first", "data:  second", "", "retry: 100", "data:", "", "data:last").onClose(() -> closed.set(true)))) {
            check(events.toList().equals(java.util.List.of("first\n second", "", "last")));
        }
        check(closed.get());
        check(!retryAllowed("POST", Map.of("Idempotency-Key", ""), null));
        check(!retryAllowed("PATCH", Map.of("X-Key", ""), "X-Key"));
        check(!retryAllowed("POST", Map.of("Idempotency-Key", " "), null));
        check(!retryAllowed("PATCH", Map.of("X-Key", "\t"), "X-Key"));
        check(retryAllowed("POST", Map.of("idempotency-key", "provided"), null));
        check(retryAllowed("PATCH", Map.of("x-key", "provided"), "X-Key"));
        check(retryAfterMillisDelay("0") == 0);
        check(retryAfterMillisDelay("125.5") == 125);
        check(retryAfterMillisDelay("NaN") == -1);
        check(retryAfterMillisDelay("Infinity") == -1);
        check(retryAfterMillisDelay("-1") == -1);
        check(retryAfterMillisDelay("invalid") == -1);
        check(retryAfterMillisDelay("1e300") == Long.MAX_VALUE);
        check(retryAfterDelay("0") == 0);
        check(retryAfterDelay("2") == 2000);
        check(retryAfterDelay("NaN") == -1);
        check(retryAfterDelay("invalid") == -1);
        check(retryAfterDelay("Thu, 01 Jan 1970 00:00:00 GMT") == 0);
        var future = java.time.ZonedDateTime.now(java.time.ZoneOffset.UTC).plusSeconds(5).format(java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME);
        var delay = retryAfterDelay(future);
        check(delay >= 3000 && delay <= 5000);
    }
"#
    );
    let output = tempfile::tempdir().unwrap();
    std::fs::write(output.path().join("ParserProbe.java"), probe).unwrap();
    for (tool, args) in [
        ("javac", vec!["ParserProbe.java"]),
        ("java", vec!["-cp", ".", "ParserProbe"]),
    ] {
        let result = Command::new(tool)
            .args(args)
            .current_dir(output.path())
            .output()
            .unwrap();
        assert!(
            result.status.success(),
            "{}",
            String::from_utf8_lossy(&result.stderr)
        );
    }
}

#[test]
#[ignore = "requires Maven and a JDK 17 toolchain"]
fn generated_package_compiles_with_maven() {
    use std::process::Command;

    let mut source = contact_api();
    let mut page = source.operations[0].clone();
    page.id = "listItemPages".into();
    page.path = "/items".into();
    page.request_body = None;
    page.parameters = vec![OperationParameter {
        name: "page".into(),
        location: "query".into(),
        required: false,
        schema: Some(integer()),
        description: None,
        annotations: BTreeMap::new(),
    }];
    page.responses = vec![poolster_core::OperationResponse::json(
        "200",
        SchemaValue::new(SchemaKind::Array {
            items: Box::new(SchemaValue::new(SchemaKind::String)),
        }),
    )];
    page.annotations.insert("x-poolster-pagination".into(), serde_json::json!({"type":"page","inputs":[{"name":"page","type":"page"}],"outputs":{"results":"$"}}));
    source.operations.push(page);
    let mut keyed = source.operations[0].clone();
    keyed.id = "createKeyedItem".into();
    keyed.method = poolster_core::HttpMethod::Post;
    keyed.annotations.insert(
        "x-poolster-idempotency".into(),
        serde_json::json!({"header":"X-Request-Key","auto_generate":true}),
    );
    source.operations.push(keyed);
    let source = poolster_core::idempotency::prepare_api(&source, &Default::default()).unwrap();
    let root = tempfile::tempdir().unwrap();
    render_sdk(
        &source,
        "sdk",
        Some("com.poolster.email"),
        SdkClientStyle::Namespaced,
    )
    .unwrap()
    .write_to(root.path())
    .unwrap();
    let output = Command::new("mvn")
        .args([
            "--batch-mode",
            "--no-transfer-progress",
            "-q",
            "-DskipTests",
            "compile",
        ])
        .current_dir(root.path().join("sdk"))
        .output()
        .expect("Maven must be available when this test is selected");
    assert!(
        output.status.success(),
        "generated Java package failed to compile:\nstdout:\n{}\nstderr:\n{}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr),
    );
    generated_retry_parsers_execute_with_jdk();
}