poolster-plugin-java 0.5.0-alpha.1

Native Java SDK generator for Poolster
Documentation
//! Independent generated buffered-operation smoke tests; never contact an API.
use super::*;
use poolster_core::engine::{Handle, Meta, Plugin, PluginContext, Requirement};
use serde_json::{Value, json};
pub struct OperationTests {
    meta: Meta,
    sdk: Option<Handle<NativeSdk>>,
    max_operations: usize,
}
pub fn operation_tests() -> OperationTests {
    OperationTests {
        meta: Meta::new(),
        sdk: None,
        max_operations: 128,
    }
}
impl OperationTests {
    pub fn using_sdk(mut self, handle: Handle<NativeSdk>) -> Self {
        self.sdk = Some(handle);
        self
    }
    pub fn sdk_from(self, sdk: &Sdk) -> Self {
        self.using_sdk(sdk.contract())
    }
    pub fn max_operations(mut self, limit: usize) -> Self {
        self.max_operations = limit;
        self
    }
}
impl Plugin<Java> for OperationTests {
    fn kind(&self) -> &'static str {
        "java-operation-tests"
    }
    fn meta(&self) -> &Meta {
        &self.meta
    }
    fn requires(&self) -> Vec<Requirement> {
        vec![Requirement::on(self.sdk)]
    }
    fn generate(&self, cx: &mut PluginContext<'_, Java>) -> Result<()> {
        anyhow::ensure!(
            self.max_operations > 0 && self.max_operations <= 128,
            "operation test bound must be 1..128"
        );
        let sdk = cx.inputs.get::<NativeSdk>()?;
        let api = prepare_api(&operation_samples::clean_api(cx.api));
        let mut code = String::new();
        let mut cases = vec![];
        let mut unsupported = vec![];
        for (index, operation) in api.operations.iter().enumerate() {
            if index >= self.max_operations {
                unsupported.push(
                    json!({"operation":operation.id,"reason":"operation count bound exceeded"}),
                );
                continue;
            }
            match operation_samples::case(&api, operation) {
                Ok(mut fixture) => {
                    fixture["encoded_path"] = json!(operation_samples::encoded_path(&fixture));
                    let fixture_json = serde_json::to_string(&fixture)?;
                    let mut input = serde_json::Map::new();
                    for parameter in fixture["parameters"].as_array().unwrap() {
                        input.insert(
                            parameter_name(
                                operation
                                    .parameters
                                    .iter()
                                    .find(|candidate| {
                                        candidate.name == parameter["name"].as_str().unwrap()
                                            && candidate.location
                                                == parameter["location"].as_str().unwrap()
                                    })
                                    .unwrap(),
                            ),
                            parameter["value"].clone(),
                        );
                    }
                    if fixture["has_body"] == true {
                        input.insert("body".into(), fixture["body"].clone());
                    }
                    let arg = if input.is_empty() {
                        String::new()
                    } else {
                        format!(
                            "MAPPER.readValue({},Client.{}Request.class)",
                            serde_json::to_string(&Value::Object(input).to_string())?,
                            type_name(&operation.id)
                        )
                    };
                    code += &format!(
                        "        {{ var driver=new Driver(MAPPER.readTree({})); var client=new Client(new ClientConfig(\"https://poolster-test.invalid\",null,\"Authorization\",\"Bearer\",Map.of(),driver,Duration.ofSeconds(1),null,null)); var result=client.{}({arg}); assertResult(driver,result); }}\n",
                        serde_json::to_string(&fixture_json)?,
                        method_name(&operation.id)
                    );
                    cases.push(fixture);
                }
                Err(error) => {
                    unsupported.push(json!({"operation":operation.id,"reason":error.to_string()}))
                }
            }
        }
        let source = include_str!("../tests/fixtures/operation_driver.java")
            .replace("__PACKAGE__", &sdk.namespace)
            .replace("__CASES__", &code);
        cx.files.emit(GeneratedFile::new(
            format!(
                "src/test/java/{}/PoolsterOperationTests.java",
                sdk.namespace.replace('.', "/")
            ),
            source,
        )?)?;
        cx.files.emit(GeneratedFile::new(
            "operation-test-report.json",
            serde_json::to_string_pretty(
                &json!({"version":1,"supported":cases,"unsupported":unsupported}),
            )?,
        )?)?;
        cx.files.emit(GeneratedFile::new("OPERATION_TESTS.md",format!("# Generated operation smoke tests\n\nAll HTTP is fake and in memory. Run:\n\n~~~sh\nmvn -q test-compile org.codehaus.mojo:exec-maven-plugin:3.5.0:java -Dexec.mainClass={}.PoolsterOperationTests -Dexec.classpathScope=test\n~~~\n\noperation-test-report.json records supported cases and exclusions. Bounded structural samples strip source examples, defaults and annotations. These tests verify public operation calls, wire controls, bodies and decoded models; dedicated fixtures are still needed for constraints, recursive/compositional schemas, auth, pagination, retries, and streaming.\n",sdk.namespace))?)?;
        Ok(())
    }
}
#[cfg(test)]
mod tests {
    use super::*;
    use poolster_core::engine::Packages;
    use poolster_core::{
        AdditionalProperties, Field, HttpMethod, OperationMediaType, OperationParameter,
        OperationRequestBody, OperationResponse,
    };
    fn api() -> Api {
        let mut optional = SchemaValue::new(SchemaKind::String);
        optional.nullable = true;
        let mut body = SchemaValue::new(SchemaKind::Object {
            fields: vec![
                Field {
                    name: "id".into(),
                    value: SchemaValue::new(SchemaKind::String),
                    required: true,
                    annotations: Default::default(),
                },
                Field {
                    name: "enabled".into(),
                    value: SchemaValue::new(SchemaKind::Boolean),
                    required: true,
                    annotations: Default::default(),
                },
                Field {
                    name: "count".into(),
                    value: SchemaValue::new(SchemaKind::Integer),
                    required: true,
                    annotations: Default::default(),
                },
                Field {
                    name: "note".into(),
                    value: optional,
                    required: true,
                    annotations: Default::default(),
                },
            ],
            additional_properties: AdditionalProperties::Forbidden,
        });
        body.default = Some(json!("DO_NOT_COPY_SOURCE_SECRET"));
        body.extensions
            .insert("x-secret".into(), json!("DO_NOT_COPY_SOURCE_SECRET"));
        body.constraints
            .insert("examples".into(), json!(["DO_NOT_COPY_SOURCE_SECRET"]));
        let parameter =
            |name: &str, location: &str, kind: SchemaKind, required: bool| OperationParameter {
                name: name.into(),
                location: location.into(),
                required,
                schema: Some(SchemaValue::new(kind)),
                description: None,
                annotations: Default::default(),
            };
        let echo = Operation {
            id: "echoContact".into(),
            method: HttpMethod::Post,
            path: "/contacts/{contact_id}".into(),
            parameters: vec![
                parameter("contact_id", "path", SchemaKind::String, true),
                parameter("enabled", "query", SchemaKind::Boolean, false),
                parameter("count", "query", SchemaKind::Integer, false),
                parameter("x-label", "header", SchemaKind::String, false),
            ],
            request_body: Some(OperationRequestBody::json(
                SchemaValue::reference("#/components/schemas/Contact"),
                true,
            )),
            responses: vec![OperationResponse::json(
                "200",
                SchemaValue::reference("#/components/schemas/Contact"),
            )],
            ..Default::default()
        };
        let mut unsupported = echo.clone();
        unsupported.id = "streamContacts".into();
        unsupported.request_body = None;
        unsupported.responses[0].media_types = vec![OperationMediaType {
            content_type: "text/event-stream".into(),
            schema: None,
        }];
        let get = Operation {
            id: "getContact".into(),
            method: HttpMethod::Get,
            path: "/contacts".into(),
            responses: echo.responses.clone(),
            ..Default::default()
        };
        Api {
            name: "Test".into(),
            version: "1.0.0".into(),
            schemas: vec![Schema::new("Contact", body)],
            operations: vec![echo, get, unsupported],
            ..Default::default()
        }
    }
    fn generate() -> GeneratedTree {
        let sdk = crate::sdk();
        Packages::new()
            .package(
                crate::package("sdk")
                    .name("io.poolster.operationtest")
                    .with(operation_tests().sdk_from(&sdk))
                    .with(sdk),
            )
            .generate(&api(), None)
            .unwrap()
    }
    #[test]
    fn generated_smoke_tests_use_sdk_contract_and_sanitized_bounded_samples() {
        let tree = generate();
        let report: Value =
            serde_json::from_str(tree.get("sdk/operation-test-report.json").unwrap()).unwrap();
        assert_eq!(report["supported"].as_array().unwrap().len(), 2);
        assert_eq!(report["unsupported"][0]["operation"], "streamContacts");
        let report_text = tree.get("sdk/operation-test-report.json").unwrap();
        assert!(!report_text.contains("DO_NOT_COPY_SOURCE_SECRET"));
        assert!(report_text.contains("encoded_path"));
        let source = tree
            .get("sdk/src/test/java/io/poolster/operationtest/PoolsterOperationTests.java")
            .unwrap();
        assert!(source.contains("client.echoContact("));
        assert!(source.contains("request.bodyPublisher()"));
        assert!(source.contains("one native request required"));
        let error = Packages::new()
            .package(crate::package("sdk").with(operation_tests()))
            .generate(&api(), None)
            .unwrap_err();
        assert!(format!("{error:#}").contains("native-sdk"));
        assert!(
            Packages::new()
                .package(
                    crate::package("sdk")
                        .with(crate::sdk())
                        .with(operation_tests().max_operations(0))
                )
                .generate(&api(), None)
                .is_err()
        );
        let bounded = Packages::new()
            .package(
                crate::package("sdk")
                    .with(crate::sdk())
                    .with(operation_tests().max_operations(1)),
            )
            .generate(&api(), None)
            .unwrap();
        let report: Value =
            serde_json::from_str(bounded.get("sdk/operation-test-report.json").unwrap()).unwrap();
        assert_eq!(report["supported"].as_array().unwrap().len(), 1);
        assert!(
            report["unsupported"]
                .as_array()
                .unwrap()
                .iter()
                .any(|case| case["reason"] == "operation count bound exceeded")
        );
    }
    #[test]
    #[ignore = "requires Maven and JDK17; executes generated public operation calls through in-memory native HTTP driver"]
    fn native_generated_operation_tests_execute() {
        let tree = generate();
        let dir = tempfile::tempdir().unwrap();
        tree.write_to(dir.path()).unwrap();
        let result = std::process::Command::new("mvn")
            .args([
                "-q",
                "test-compile",
                "org.codehaus.mojo:exec-maven-plugin:3.5.0:java",
                "-Dexec.mainClass=io.poolster.operationtest.PoolsterOperationTests",
                "-Dexec.classpathScope=test",
            ])
            .current_dir(dir.path().join("sdk"))
            .output()
            .unwrap();
        assert!(
            result.status.success(),
            "{}{}",
            String::from_utf8_lossy(&result.stdout),
            String::from_utf8_lossy(&result.stderr)
        );
    }
}