From 10fc4858d895ff898dd21db6c8b2f22a74af5311 Mon Sep 17 00:00:00 2001 From: Felix Caceres Date: Wed, 29 Apr 2026 11:44:29 +0200 Subject: [PATCH 1/4] OpenAPI 3.2 Compatibility Layer + Java 17 Alignment and Parser Upgrade - Upgrade runtime to Java 17 (Dockerfile openjdk base) - Bump swagger-parser from 2.1.19 to 2.1.41 - Add OpenAPI 3.2 normalization layer in SerializedDataUtils * Normalizes 3.2 specs to 3.1-compatible structure before parsing * Transforms querystring parameter location to query * Remaps 3.2-specific fields to extension-safe keys - Improve SoapUI generation resilience * Support querystring parameter handling * Centralize and simplify readOnly HTTP method filtering * Gracefully skip unsupported HTTP methods with logging - Convert OpenAPI 3.2 tests from placeholder to concrete behavior * Add real 3.2 parsing and generation tests * Add querystring parameter compatibility test - Update documentation to reflect official 3.2 support strategy * OPENAPI_VERSIONS.md: 3.2 now marked as officially supported * README.md: Updated compatibility matrix and feature table Impact: OpenAPI 3.2 specs can now be parsed and reliably converted to SoapUI projects via normalization layer, safer handling of edge-case parameter/method types, and clearer product messaging. Closes: OpenAPI 3.2 support feature --- Dockerfile | 2 +- OPENAPI_VERSIONS.md | 55 +++++------ README.md | 8 +- pom.xml | 2 +- .../openapi2soapui/model/SoapUIProject.java | 33 ++++--- .../util/SerializedDataUtils.java | 93 ++++++++++++++++++- .../model/OpenAPIVersionSupportTest.java | 66 +++++++++---- 7 files changed, 198 insertions(+), 61 deletions(-) diff --git a/Dockerfile b/Dockerfile index f355110..cb77621 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM openjdk:11-jdk +FROM openjdk:17-jdk ARG JAR_FILE COPY ${JAR_FILE} app.jar diff --git a/OPENAPI_VERSIONS.md b/OPENAPI_VERSIONS.md index 78c0137..de60fb1 100644 --- a/OPENAPI_VERSIONS.md +++ b/OPENAPI_VERSIONS.md @@ -80,35 +80,36 @@ paths: type: [string, null] # Nullable in 3.1 ``` -### OpenAPI 3.2.x ⏳ -- **Status:** Not yet released -- **ETA:** Future (as of April 2026) -- **Planned Support:** When OpenAPI 3.2 is officially released and swagger-parser is updated -- **Expected Changes:** Refinements to JSON Schema integration, possible webhooks improvements +### OpenAPI 3.2.x ✅ +- **Status:** Officially released and supported +- **Release:** OpenAPI 3.2.0 (September 2025) +- **Parser:** swagger-parser 2.1.41+ (current project version) +- **Current Compatibility:** Parsed and generated through project-level normalization of 3.2-specific fields +- **Highlights:** Additional HTTP method support, richer parameter modeling, and improved response metadata ## Version Detection The OpenAPI version is automatically detected from the `openapi` field in your spec: ```yaml -openapi: 3.1.0 # Detected and handled appropriately +openapi: 3.2.0 # Detected and handled appropriately ``` No configuration is needed — just submit your spec and the tool will parse it correctly. ## Feature Compatibility Across Versions -All openapi2soapui features work with both OpenAPI 3.0 and 3.1: +All openapi2soapui features work with OpenAPI 3.0, 3.1, and 3.2: -| Feature | 3.0.x | 3.1.x | -|---------|-------|-------| -| `readOnly` | ✅ | ✅ | -| `serverPattern` | ✅ | ✅ | -| `minimalEndpoints` | ✅ | ✅ | -| `microcksHeaders` | ✅ | ✅ | -| `generateOneOfAnyOf` | ✅ | ✅ | -| `examples` | ✅ | ✅ | -| `validateSchema` | ✅ | ✅ | +| Feature | 3.0.x | 3.1.x | 3.2.x | +|---------|-------|-------|-------| +| `readOnly` | ✅ | ✅ | ✅ | +| `serverPattern` | ✅ | ✅ | ✅ | +| `minimalEndpoints` | ✅ | ✅ | ✅ | +| `microcksHeaders` | ✅ | ✅ | ✅ | +| `generateOneOfAnyOf` | ✅ | ✅ | ✅ | +| `examples` | ✅ | ✅ | ✅ | +| `validateSchema` | ✅ | ✅ | ✅ | ## Test Coverage @@ -137,9 +138,9 @@ Comprehensive tests ensure compatibility across versions: - Arrays of objects - Schema composition (allOf) -- **Forward Compatibility Tests:** 2 tests - - 3.2 status documentation - - Graceful handling of future versions +- **OpenAPI 3.2.x Tests:** 2 tests + - Basic 3.2.0 parsing and generation + - Querystring parameter compatibility **Total:** 15 version support tests, all passing ✅ @@ -194,9 +195,10 @@ If you're migrating from 3.0 to 3.1: ``` -- **Version 2.1.19+** supports OpenAPI 3.0.x and 3.1.x +- **Version 2.1.41+** supports OpenAPI 3.0.x and 3.1.x natively +- **OpenAPI 3.2.x** is supported in this project through a compatibility normalization layer - **Version 2.0.x** supports OpenAPI 3.0.x only (legacy) -- **Version 3.x** (future) may support OpenAPI 3.2.x +- **Version 2.1.x/3.x** can be evaluated for future parser enhancements ## Error Handling @@ -217,26 +219,27 @@ If your OpenAPI spec has version-specific issues: ### "Parser returned null" error - Verify your YAML/JSON is valid -- Ensure OpenAPI version is one of: 3.0.0, 3.0.1, 3.0.2, 3.0.3, 3.1.0+ +- Ensure OpenAPI version is one of: 3.0.0, 3.0.1, 3.0.2, 3.0.3, 3.1.0+, 3.2.x - Check that required fields (info, paths) are present ### "Unknown schema property" errors - For OpenAPI 3.0: Some JSON Schema 2020-12 features not supported -- For OpenAPI 3.1: Verify you're using 3.1-compatible schemas +- For OpenAPI 3.1/3.2: Verify you're using 3.1+ compatible schemas ### Performance with large specs -- Both 3.0 and 3.1 handle large specs efficiently +- OpenAPI 3.0, 3.1, and 3.2 handle large specs efficiently - No performance difference between versions ## References - [OpenAPI 3.0 Specification](https://spec.openapis.org/oas/v3.0.3) - [OpenAPI 3.1 Specification](https://spec.openapis.org/oas/v3.1.0) +- [OpenAPI 3.2 Specification](https://spec.openapis.org/oas/v3.2.0) - [swagger-parser Releases](https://github.com/swagger-api/swagger-parser/releases) - [JSON Schema 2020-12](https://json-schema.org/draft/2020-12/json-schema-core.html) --- -**Last Updated:** 2026-04-27 -**Swagger Parser Version:** 2.1.19+ +**Last Updated:** 2026-04-29 +**Swagger Parser Version:** 2.1.41+ (with OpenAPI 3.2 normalization layer) **Test Coverage:** 15 tests (100% passing) diff --git a/README.md b/README.md index e54824c..5449978 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Given an OpenAPI Specification (v3.0.x, v3.1.x, or v3.2.x), a SoapUI project is generated with the _requests_ for each resource operation and a _test suite_. The response is the content of the SoapUI project in XML format to save as file and import into the SoapUI application. -**Supported OpenAPI Versions:** 3.0.0, 3.0.1, 3.0.2, 3.0.3, 3.1.0+, 3.2.x (forward compatible) +**Supported OpenAPI Versions:** 3.0.0, 3.0.1, 3.0.2, 3.0.3, 3.1.0+, 3.2.x (officially released and supported) ### This repository is intended for :octocat: **community** use, it can be modified and adapted without commercial use. If you need a version, support or help for your **enterprise** or project, please contact us 📧 devrel@apiaddicts.org @@ -197,7 +197,7 @@ OpenAPI2SoapUI supports 7 optional parameters to customize SoapUI project genera |[Hibernate Validator](https://hibernate.org/validator/)|Express validation rules in a standardized way using annotation-based constraints and benefit from transparent integration with a wide variety of frameworks.| |[Springdoc OpenAPI UI](https://springdoc.org/)|OpenAPI 3 Library for spring boot projects. Is based on swagger-ui, to display the OpenAPI description.| |[SoapUI core module](https://www.soapui.org/open-source/)|SoapUI is the world's leading Functional Testing tool for SOAP and REST testing.| -|[Swagger Parser 2.1.19+](https://github.com/swagger-api/swagger-parser)|Parses OpenAPI definitions (3.0.x, 3.1.x) in JSON or YAML format into swagger-core representation as Java POJO. Supports JSON Schema 2020-12 and nullable types.| +|[Swagger Parser 2.1.41+](https://github.com/swagger-api/swagger-parser)|Parses OpenAPI definitions (3.0.x, 3.1.x) in JSON or YAML format into swagger-core representation as Java POJO. OpenAPI 3.2 compatibility is provided in this project through normalization of 3.2-specific fields before parsing.| # 📑 Getting started @@ -399,7 +399,7 @@ OpenAPI2SoapUI supports multiple OpenAPI specification versions with full featur | **OpenAPI 3.0.2** | ✅ Fully Supported | All features | | **OpenAPI 3.0.3** | ✅ Fully Supported | All features | | **OpenAPI 3.1.0+** | ✅ Fully Supported | All features + JSON Schema 2020-12, nullable types | -| **OpenAPI 3.2.x** | ✅ Forward Compatible | Ready for future releases | +| **OpenAPI 3.2.x** | ✅ Fully Supported | Officially released and supported | ### Key Features by Version @@ -432,7 +432,7 @@ For detailed information, see [OpenAPI Version Support Documentation](./OPENAPI_ The project includes comprehensive test coverage with **88 tests** covering: - ✅ 7 feature options -- ✅ OpenAPI 3.0.x and 3.1.x support +- ✅ OpenAPI 3.0.x, 3.1.x, and 3.2.x support - ✅ All HTTP methods - ✅ Parameter handling (path, query, header) - ✅ Complex schema handling diff --git a/pom.xml b/pom.xml index f87b2b4..3e5f59c 100644 --- a/pom.xml +++ b/pom.xml @@ -61,7 +61,7 @@ 2.0.2 5.6.0 - 2.1.19 + 2.1.41 diff --git a/src/main/java/org/apiaddicts/apitools/openapi2soapui/model/SoapUIProject.java b/src/main/java/org/apiaddicts/apitools/openapi2soapui/model/SoapUIProject.java index 52ca104..775b024 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/model/SoapUIProject.java +++ b/src/main/java/org/apiaddicts/apitools/openapi2soapui/model/SoapUIProject.java @@ -267,12 +267,22 @@ private void setParameterProperties(RestParamProperty parameter, Parameter openA parameter.setStyle(ParameterStyle.HEADER); } else if (openAPIParameter.getIn().equalsIgnoreCase(PATH)) { parameter.setStyle(ParameterStyle.TEMPLATE); - } else if (openAPIParameter.getIn().equalsIgnoreCase(QUERY)) { + } else if (openAPIParameter.getIn().equalsIgnoreCase(QUERY) || openAPIParameter.getIn().equalsIgnoreCase("querystring")) { parameter.setStyle(ParameterStyle.QUERY); } } } + /** + * Determine if an HTTP method should be skipped when readOnly option is enabled. + * @param httpMethod OpenAPI HTTP method + * @return true when method is considered write operation + */ + private boolean isWriteOperation(HttpMethod httpMethod) { + String method = httpMethod.name(); + return method.equals("POST") || method.equals("PUT") || method.equals("PATCH") || method.equals("DELETE"); + } + /** * Get OpenAPI Parameter Example * Validate if the parameter has the examples, example or x-example property and if so, it returns its value @@ -361,15 +371,17 @@ private void setResourceMethods(RestResource restResource, Map { // Feature 1: readOnly - if (options.isReadOnly()) { - String method = httpMethod.name(); - if (method.equals("POST") || method.equals("PUT") || method.equals("PATCH") || method.equals("DELETE")) { - return; - } + if (options.isReadOnly() && isWriteOperation(httpMethod)) { + return; } RestMethod restMethod = restResource.addNewMethod((operation.getOperationId() != null) ? operation.getOperationId() : httpMethod.name()); - restMethod.setMethod(RestRequestInterface.HttpMethod.valueOf(httpMethod.name())); + try { + restMethod.setMethod(RestRequestInterface.HttpMethod.valueOf(httpMethod.name())); + } catch (IllegalArgumentException ex) { + log.warn("HTTP method {} is not supported by current SoapUI version and will be skipped", httpMethod.name()); + return; + } restMethod.setDescription((operation.getDescription() != null) ? operation.getDescription() : ""); if (operation.getRequestBody() != null) { @@ -438,11 +450,8 @@ private void setMethodsRequests(String pathName, PathItem pathItem) { if (restResource != null) { pathItem.readOperationsMap().forEach((httpMethod, operation) -> { // Feature 1: readOnly - if (options.isReadOnly()) { - String method = httpMethod.name(); - if (method.equals("POST") || method.equals("PUT") || method.equals("PATCH") || method.equals("DELETE")) { - return; - } + if (options.isReadOnly() && isWriteOperation(httpMethod)) { + return; } RestMethod restMethod = restResource.getRestMethodByName((operation.getOperationId() != null) ? operation.getOperationId() : httpMethod.name()); diff --git a/src/main/java/org/apiaddicts/apitools/openapi2soapui/util/SerializedDataUtils.java b/src/main/java/org/apiaddicts/apitools/openapi2soapui/util/SerializedDataUtils.java index 6396990..4967b77 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/util/SerializedDataUtils.java +++ b/src/main/java/org/apiaddicts/apitools/openapi2soapui/util/SerializedDataUtils.java @@ -1,6 +1,8 @@ package org.apiaddicts.apitools.openapi2soapui.util; import java.util.Base64; +import java.util.List; +import java.util.Map; import lombok.extern.slf4j.Slf4j; import org.apiaddicts.apitools.openapi2soapui.error.exceptions.DecodeBase64Exception; @@ -19,6 +21,8 @@ */ @Slf4j public class SerializedDataUtils { + private static final String QUERY = "query"; + private static final String GET = "get"; private SerializedDataUtils() { // Intentional blank @@ -78,10 +82,11 @@ public static boolean isYAMLValid(String content) { */ public static OpenAPI parseOpenAPIContent(String openAPIContent) { try { + String normalizedContent = normalizeOpenAPI32Content(openAPIContent); ParseOptions parseOptions = new ParseOptions(); parseOptions.setResolve(true); parseOptions.setResolveFully(true); - OpenAPI openAPI = new OpenAPIParser().readContents(openAPIContent, null, parseOptions).getOpenAPI(); + OpenAPI openAPI = new OpenAPIParser().readContents(normalizedContent, null, parseOptions).getOpenAPI(); validateRequiredOpenAPIProperties(openAPI); return openAPI; } catch (Exception e) { @@ -90,6 +95,92 @@ public static OpenAPI parseOpenAPIContent(String openAPIContent) { } } + /** + * Normalize OpenAPI 3.2 content so it can be parsed by the current parser stack. + * This keeps the runtime compatible while parser-level 3.2 support evolves. + * @param openAPIContent OpenAPI content as string + * @return normalized content for parser consumption + */ + private static String normalizeOpenAPI32Content(String openAPIContent) { + try { + Yaml yaml = new Yaml(); + Object parsed = yaml.load(openAPIContent); + if (!(parsed instanceof Map)) { + return openAPIContent; + } + + Map root = (Map) parsed; + Object version = root.get("openapi"); + if (!(version instanceof String) || !((String) version).startsWith("3.2")) { + return openAPIContent; + } + + root.put("openapi", "3.1.0"); + normalizeNode(root); + return yaml.dump(root); + } catch (Exception e) { + log.debug("OpenAPI 3.2 normalization skipped", e); + return openAPIContent; + } + } + + /** + * Recursively normalize known OpenAPI 3.2-only fields to 3.1-compatible fields. + * @param node current structure node + */ + @SuppressWarnings("unchecked") + private static void normalizeNode(Object node) { + if (node instanceof Map) { + Map map = (Map) node; + normalizeParameterLocation(map); + normalizeTopLevel32Fields(map); + normalizeQueryOperation(map); + normalizeComponentsMediaTypes(map); + map.values().forEach(SerializedDataUtils::normalizeNode); + } else if (node instanceof List) { + ((List) node).forEach(SerializedDataUtils::normalizeNode); + } + } + + private static void normalizeParameterLocation(Map map) { + Object inValue = map.get("in"); + if (inValue instanceof String && "querystring".equalsIgnoreCase((String) inValue)) { + map.put("in", QUERY); + } + } + + private static void normalizeTopLevel32Fields(Map map) { + if (map.containsKey("$self")) { + map.put("x-oas32-self", map.remove("$self")); + } + + if (map.containsKey("additionalOperations")) { + map.put("x-oas32-additionalOperations", map.remove("additionalOperations")); + } + } + + private static void normalizeQueryOperation(Map map) { + if (map.containsKey(QUERY)) { + Object queryOp = map.remove(QUERY); + if (!map.containsKey(GET)) { + map.put(GET, queryOp); + } else { + map.put("x-oas32-query-operation", queryOp); + } + } + } + + @SuppressWarnings("unchecked") + private static void normalizeComponentsMediaTypes(Map map) { + Object componentsObj = map.get("components"); + if (componentsObj instanceof Map) { + Map components = (Map) componentsObj; + if (components.containsKey("mediaTypes")) { + components.put("x-oas32-mediaTypes", components.remove("mediaTypes")); + } + } + } + /** * Validates the mandatory properties of an Open API Spec * @param openAPI instance of OpenAPI diff --git a/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/OpenAPIVersionSupportTest.java b/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/OpenAPIVersionSupportTest.java index 04748dc..74843c2 100644 --- a/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/OpenAPIVersionSupportTest.java +++ b/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/OpenAPIVersionSupportTest.java @@ -8,6 +8,7 @@ import io.swagger.v3.oas.models.OpenAPI; import org.apiaddicts.apitools.openapi2soapui.request.SoapUIProjectOptions; +import org.apiaddicts.apitools.openapi2soapui.util.SerializedDataUtils; import com.eviware.soapui.support.SoapUIException; import java.io.IOException; @@ -405,43 +406,76 @@ void testServerPatternWith310() throws IOException, XmlException, SoapUIExceptio } @Nested - @DisplayName("OpenAPI 3.2 Forward Compatibility") + @DisplayName("OpenAPI 3.2.x Support") class OpenAPI32Support { @Test - @DisplayName("Note: OpenAPI 3.2 is not officially released yet") - void testOpenAPI32Status() { - String message = "OpenAPI 3.2 support: Not yet released as of 2026-04-27. " + - "When released, swagger-parser will need to be updated to support it. " + - "Current version (2.0.24) supports OpenAPI 3.0.x and 3.1.x."; - assertTrue(message.contains("3.2"), "Documentation note for 3.2 support"); + @DisplayName("Should parse OpenAPI 3.2.0 spec") + void testParseOpenAPI320() throws IOException, XmlException, SoapUIException { + String spec = "openapi: 3.2.0\n" + + "info:\n" + + " title: OpenAPI 32 API\n" + + " version: 1.0.0\n" + + "servers:\n" + + " - url: http://api.example.com/v1\n" + + "paths:\n" + + " /status:\n" + + " get:\n" + + " operationId: getStatus\n" + + " responses:\n" + + " '200':\n" + + " description: OK\n" + + " content:\n" + + " application/json:\n" + + " schema:\n" + + " type: object\n"; + + OpenAPI openAPI = SerializedDataUtils.parseOpenAPIContent(spec); + assertNotNull(openAPI, "OpenAPI 3.2.0 spec should parse successfully"); + assertEquals("3.1.0", openAPI.getOpenapi(), "3.2 spec should be normalized to parser-compatible version"); + + SoapUIProject project = new SoapUIProject("OpenAPI32API", openAPI, null, null, null, null); + String xml = project.getFileContent(); + project.deleteTemporaryFile(); + + assertTrue(xml.contains("getStatus"), "Generated XML should contain operation"); + assertFalse(xml.isEmpty(), "Should generate valid XML"); } @Test - @DisplayName("Should gracefully handle future OpenAPI versions") - void testFutureVersionHandling() throws IOException, XmlException, SoapUIException { - String spec = "openapi: 3.1.0\n" + + @DisplayName("Should handle OpenAPI 3.2 querystring parameter") + void testOpenAPI32QuerystringParameterSupport() throws IOException, XmlException, SoapUIException { + String spec = "openapi: 3.2.0\n" + "info:\n" + - " title: Test API\n" + + " title: Querystring API\n" + " version: 1.0.0\n" + "servers:\n" + " - url: http://api.example.com\n" + "paths:\n" + " /test:\n" + " get:\n" + - " operationId: test\n" + + " operationId: testQuerystring\n" + + " parameters:\n" + + " - name: rawQuery\n" + + " in: querystring\n" + + " required: false\n" + + " content:\n" + + " application/x-www-form-urlencoded:\n" + + " schema:\n" + + " type: string\n" + " responses:\n" + " '200':\n" + " description: OK\n"; - OpenAPI openAPI = parser.readContents(spec).getOpenAPI(); - assertNotNull(openAPI, "Parser should handle current versions"); + OpenAPI openAPI = SerializedDataUtils.parseOpenAPIContent(spec); + assertNotNull(openAPI, "OpenAPI 3.2 with querystring parameter should parse successfully"); - SoapUIProject project = new SoapUIProject("TestAPI", openAPI, null, null, null, null); + SoapUIProject project = new SoapUIProject("QuerystringAPI", openAPI, null, null, null, null); String xml = project.getFileContent(); project.deleteTemporaryFile(); - assertTrue(xml.length() > 0, "Should generate valid XML"); + assertTrue(xml.contains("testQuerystring"), "Generated XML should contain operation"); + assertFalse(xml.isEmpty(), "Should generate valid XML"); } } From f79191d37d1000bd6c6cc1beed3e72de5219b5db Mon Sep 17 00:00:00 2001 From: FelixCaceres17 Date: Mon, 25 May 2026 09:16:09 +0200 Subject: [PATCH 2/4] Update pom.xml Co-authored-by: Rafael Goterris Perales --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 3e5f59c..e07791f 100644 --- a/pom.xml +++ b/pom.xml @@ -61,7 +61,7 @@ 2.0.2 5.6.0 - 2.1.41 + 2.1.42 From 4ace374ec74f5e1e531e3f19eb593fc58df70c37 Mon Sep 17 00:00:00 2001 From: FelixCaceres17 Date: Mon, 25 May 2026 10:13:53 +0200 Subject: [PATCH 3/4] Address PR review: bump version, springdoc, and resolve Sonar issues - pom.xml: bump openapi2soapui to 2.0.0 and springdoc to 2.6.0 (Spring Boot 3.3 compatible) - SoapUIProject.java: extract configureResourceMethod / configureMethodRequest helpers to reduce cognitive complexity (S3776) on setResourceMethods and setMethodsRequests - SerializedDataUtils.java: use pattern matching for instanceof (S6201) - OpenAPIVersionSupportTest.java: replace YAML string concatenations with text blocks (S6126) --- pom.xml | 4 +- .../openapi2soapui/model/SoapUIProject.java | 105 ++++++++++-------- .../util/SerializedDataUtils.java | 4 +- .../model/OpenAPIVersionSupportTest.java | 80 ++++++------- 4 files changed, 107 insertions(+), 86 deletions(-) diff --git a/pom.xml b/pom.xml index e07791f..657387f 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ net.cloudappi openapi2soapui - 1.0.3 + 2.0.0 ${packaging.type} openapi2soapui @@ -59,7 +59,7 @@ UTF-8 17 - 2.0.2 + 2.6.0 5.6.0 2.1.42 diff --git a/src/main/java/org/apiaddicts/apitools/openapi2soapui/model/SoapUIProject.java b/src/main/java/org/apiaddicts/apitools/openapi2soapui/model/SoapUIProject.java index 775b024..f987b19 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/model/SoapUIProject.java +++ b/src/main/java/org/apiaddicts/apitools/openapi2soapui/model/SoapUIProject.java @@ -368,29 +368,34 @@ private void setMethodParameters(RestMethod restMethod, List openAPIP * @param operations list of path operations */ private void setResourceMethods(RestResource restResource, Map operations) { - if (operations != null && !operations.isEmpty()) { - operations.forEach((httpMethod, operation) -> { - // Feature 1: readOnly - if (options.isReadOnly() && isWriteOperation(httpMethod)) { - return; - } + if (operations == null || operations.isEmpty()) { + return; + } + operations.forEach((httpMethod, operation) -> configureResourceMethod(restResource, httpMethod, operation)); + } - RestMethod restMethod = restResource.addNewMethod((operation.getOperationId() != null) ? operation.getOperationId() : httpMethod.name()); - try { - restMethod.setMethod(RestRequestInterface.HttpMethod.valueOf(httpMethod.name())); - } catch (IllegalArgumentException ex) { - log.warn("HTTP method {} is not supported by current SoapUI version and will be skipped", httpMethod.name()); - return; - } - restMethod.setDescription((operation.getDescription() != null) ? operation.getDescription() : ""); + private void configureResourceMethod(RestResource restResource, HttpMethod httpMethod, Operation operation) { + // Feature 1: readOnly + if (options.isReadOnly() && isWriteOperation(httpMethod)) { + return; + } - if (operation.getRequestBody() != null) { - setMethodRequestRepresentations(restMethod, operation.getRequestBody()); - } + String methodName = (operation.getOperationId() != null) ? operation.getOperationId() : httpMethod.name(); + RestMethod restMethod = restResource.addNewMethod(methodName); + try { + restMethod.setMethod(RestRequestInterface.HttpMethod.valueOf(httpMethod.name())); + } catch (IllegalArgumentException ex) { + log.warn("HTTP method {} is not supported by current SoapUI version and will be skipped", httpMethod.name()); + return; + } + String description = (operation.getDescription() != null) ? operation.getDescription() : ""; + restMethod.setDescription(description); - setMethodResponseRepresentations(restMethod, operation.getResponses()); - }); + if (operation.getRequestBody() != null) { + setMethodRequestRepresentations(restMethod, operation.getRequestBody()); } + + setMethodResponseRepresentations(restMethod, operation.getResponses()); } /** @@ -446,38 +451,50 @@ private void setMethodResponseRepresentations(RestMethod restMethod, ApiResponse */ private void setMethodsRequests(String pathName, PathItem pathItem) { RestResource restResource = restService.getResourceByFullPath(restService.getBasePath() + pathName); + if (restResource == null) { + return; + } + pathItem.readOperationsMap().forEach((httpMethod, operation) -> + configureMethodRequest(restResource, pathItem, httpMethod, operation)); + } - if (restResource != null) { - pathItem.readOperationsMap().forEach((httpMethod, operation) -> { - // Feature 1: readOnly - if (options.isReadOnly() && isWriteOperation(httpMethod)) { - return; - } + private void configureMethodRequest(RestResource restResource, PathItem pathItem, HttpMethod httpMethod, Operation operation) { + // Feature 1: readOnly + if (options.isReadOnly() && isWriteOperation(httpMethod)) { + return; + } - RestMethod restMethod = restResource.getRestMethodByName((operation.getOperationId() != null) ? operation.getOperationId() : httpMethod.name()); - if (restMethod == null) return; - RestRequest restRequest = restMethod.addNewRequest(DEFAULT_REQUEST_NAME); - RestRequestConfig restRequestConfig = restRequest.getConfig(); + String methodName = (operation.getOperationId() != null) ? operation.getOperationId() : httpMethod.name(); + RestMethod restMethod = restResource.getRestMethodByName(methodName); + if (restMethod == null) { + return; + } - restRequestConfig.setOriginalUri(restService.getEndpoints()[0] + restResource.getFullPath(true)); - setRequestAuthProfile(restRequestConfig); - setRequestJMSConfig(restRequestConfig); + RestRequest restRequest = restMethod.addNewRequest(DEFAULT_REQUEST_NAME); + RestRequestConfig restRequestConfig = restRequest.getConfig(); - restRequest.setEndpoint(restService.getEndpoints()[0]); - setRequestMediaType(restRequest, operation); + restRequestConfig.setOriginalUri(restService.getEndpoints()[0] + restResource.getFullPath(true)); + setRequestAuthProfile(restRequestConfig); + setRequestJMSConfig(restRequestConfig); - setResourceParameters(restResource, pathItem.getParameters()); - setMethodParameters(restMethod, operation.getParameters()); + restRequest.setEndpoint(restService.getEndpoints()[0]); + setRequestMediaType(restRequest, operation); - if (operation.getRequestBody() != null) { - Content content = operation.getRequestBody().getContent(); - if (content != null && !content.isEmpty()) { - setRequestContent(restRequest, content); - } - } + setResourceParameters(restResource, pathItem.getParameters()); + setMethodParameters(restMethod, operation.getParameters()); - setRequestHeaders(restRequest, operation); - }); + applyRequestBodyContent(restRequest, operation); + + setRequestHeaders(restRequest, operation); + } + + private void applyRequestBodyContent(RestRequest restRequest, Operation operation) { + if (operation.getRequestBody() == null) { + return; + } + Content content = operation.getRequestBody().getContent(); + if (content != null && !content.isEmpty()) { + setRequestContent(restRequest, content); } } diff --git a/src/main/java/org/apiaddicts/apitools/openapi2soapui/util/SerializedDataUtils.java b/src/main/java/org/apiaddicts/apitools/openapi2soapui/util/SerializedDataUtils.java index 4967b77..dd0184e 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/util/SerializedDataUtils.java +++ b/src/main/java/org/apiaddicts/apitools/openapi2soapui/util/SerializedDataUtils.java @@ -111,7 +111,7 @@ private static String normalizeOpenAPI32Content(String openAPIContent) { Map root = (Map) parsed; Object version = root.get("openapi"); - if (!(version instanceof String) || !((String) version).startsWith("3.2")) { + if (!(version instanceof String versionStr) || !versionStr.startsWith("3.2")) { return openAPIContent; } @@ -144,7 +144,7 @@ private static void normalizeNode(Object node) { private static void normalizeParameterLocation(Map map) { Object inValue = map.get("in"); - if (inValue instanceof String && "querystring".equalsIgnoreCase((String) inValue)) { + if (inValue instanceof String inStr && "querystring".equalsIgnoreCase(inStr)) { map.put("in", QUERY); } } diff --git a/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/OpenAPIVersionSupportTest.java b/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/OpenAPIVersionSupportTest.java index 74843c2..656ba4b 100644 --- a/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/OpenAPIVersionSupportTest.java +++ b/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/OpenAPIVersionSupportTest.java @@ -412,23 +412,25 @@ class OpenAPI32Support { @Test @DisplayName("Should parse OpenAPI 3.2.0 spec") void testParseOpenAPI320() throws IOException, XmlException, SoapUIException { - String spec = "openapi: 3.2.0\n" + - "info:\n" + - " title: OpenAPI 32 API\n" + - " version: 1.0.0\n" + - "servers:\n" + - " - url: http://api.example.com/v1\n" + - "paths:\n" + - " /status:\n" + - " get:\n" + - " operationId: getStatus\n" + - " responses:\n" + - " '200':\n" + - " description: OK\n" + - " content:\n" + - " application/json:\n" + - " schema:\n" + - " type: object\n"; + String spec = """ + openapi: 3.2.0 + info: + title: OpenAPI 32 API + version: 1.0.0 + servers: + - url: http://api.example.com/v1 + paths: + /status: + get: + operationId: getStatus + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + """; OpenAPI openAPI = SerializedDataUtils.parseOpenAPIContent(spec); assertNotNull(openAPI, "OpenAPI 3.2.0 spec should parse successfully"); @@ -445,27 +447,29 @@ void testParseOpenAPI320() throws IOException, XmlException, SoapUIException { @Test @DisplayName("Should handle OpenAPI 3.2 querystring parameter") void testOpenAPI32QuerystringParameterSupport() throws IOException, XmlException, SoapUIException { - String spec = "openapi: 3.2.0\n" + - "info:\n" + - " title: Querystring API\n" + - " version: 1.0.0\n" + - "servers:\n" + - " - url: http://api.example.com\n" + - "paths:\n" + - " /test:\n" + - " get:\n" + - " operationId: testQuerystring\n" + - " parameters:\n" + - " - name: rawQuery\n" + - " in: querystring\n" + - " required: false\n" + - " content:\n" + - " application/x-www-form-urlencoded:\n" + - " schema:\n" + - " type: string\n" + - " responses:\n" + - " '200':\n" + - " description: OK\n"; + String spec = """ + openapi: 3.2.0 + info: + title: Querystring API + version: 1.0.0 + servers: + - url: http://api.example.com + paths: + /test: + get: + operationId: testQuerystring + parameters: + - name: rawQuery + in: querystring + required: false + content: + application/x-www-form-urlencoded: + schema: + type: string + responses: + '200': + description: OK + """; OpenAPI openAPI = SerializedDataUtils.parseOpenAPIContent(spec); assertNotNull(openAPI, "OpenAPI 3.2 with querystring parameter should parse successfully"); From 92001872b4df3096c8ae2f89a2196640fb714e43 Mon Sep 17 00:00:00 2001 From: FelixCaceres17 Date: Mon, 25 May 2026 10:33:57 +0200 Subject: [PATCH 4/4] Match project style for new helpers - Rename configureResourceMethod -> setResourceMethod - Rename configureMethodRequest -> setMethodRequest - Rename applyRequestBodyContent -> setRequestBodyContent - Add Javadoc to each new private helper to match the rest of the class --- .../openapi2soapui/model/SoapUIProject.java | 36 +++++++++++++++---- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/apiaddicts/apitools/openapi2soapui/model/SoapUIProject.java b/src/main/java/org/apiaddicts/apitools/openapi2soapui/model/SoapUIProject.java index f987b19..d3c941f 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/model/SoapUIProject.java +++ b/src/main/java/org/apiaddicts/apitools/openapi2soapui/model/SoapUIProject.java @@ -371,10 +371,19 @@ private void setResourceMethods(RestResource restResource, Map configureResourceMethod(restResource, httpMethod, operation)); + operations.forEach((httpMethod, operation) -> setResourceMethod(restResource, httpMethod, operation)); } - private void configureResourceMethod(RestResource restResource, HttpMethod httpMethod, Operation operation) { + /** + * Set Resource Method + * Add a single Method to Resource from an OpenAPI Operation + * Skip write operations if readOnly mode is enabled + * Skip method if the HTTP method is not supported by current SoapUI version + * @param restResource instance of Resource + * @param httpMethod OpenAPI HTTP method + * @param operation OpenAPI Operation + */ + private void setResourceMethod(RestResource restResource, HttpMethod httpMethod, Operation operation) { // Feature 1: readOnly if (options.isReadOnly() && isWriteOperation(httpMethod)) { return; @@ -455,10 +464,19 @@ private void setMethodsRequests(String pathName, PathItem pathItem) { return; } pathItem.readOperationsMap().forEach((httpMethod, operation) -> - configureMethodRequest(restResource, pathItem, httpMethod, operation)); + setMethodRequest(restResource, pathItem, httpMethod, operation)); } - private void configureMethodRequest(RestResource restResource, PathItem pathItem, HttpMethod httpMethod, Operation operation) { + /** + * Set Method Request + * Find the Method matching the Operation and create/configure its Request + * Skip write operations if readOnly mode is enabled + * @param restResource instance of Resource + * @param pathItem instance of OpenAPI Path + * @param httpMethod OpenAPI HTTP method + * @param operation OpenAPI Operation + */ + private void setMethodRequest(RestResource restResource, PathItem pathItem, HttpMethod httpMethod, Operation operation) { // Feature 1: readOnly if (options.isReadOnly() && isWriteOperation(httpMethod)) { return; @@ -483,12 +501,18 @@ private void configureMethodRequest(RestResource restResource, PathItem pathItem setResourceParameters(restResource, pathItem.getParameters()); setMethodParameters(restMethod, operation.getParameters()); - applyRequestBodyContent(restRequest, operation); + setRequestBodyContent(restRequest, operation); setRequestHeaders(restRequest, operation); } - private void applyRequestBodyContent(RestRequest restRequest, Operation operation) { + /** + * Set Request Body Content + * Extract the Operation Request Body content and set it on the Request when present + * @param restRequest instance of Method Request + * @param operation OpenAPI Operation + */ + private void setRequestBodyContent(RestRequest restRequest, Operation operation) { if (operation.getRequestBody() == null) { return; }