-
Notifications
You must be signed in to change notification settings - Fork 63
Add ApiSpecFetcher for Fetching and Comparing API Specifications #900
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
owaiskazi19
merged 7 commits into
opensearch-project:main
from
junweid62:api-consistency-test
Oct 8, 2024
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
f2a0c4e
Added ApiSpecFetcher with test
65b5303
remove duplication license
87a23a9
Add more test to pass test coverage check
6329da9
Merge branch 'main' into api-consistency-test
junweid62 8f0010f
new commit address all comments
f021ef8
new commit address all comments
af15a00
Addressed all comments
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
48 changes: 48 additions & 0 deletions
48
src/main/java/org/opensearch/flowframework/exception/ApiSpecParseException.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| /* | ||
| * Copyright OpenSearch Contributors | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| * | ||
| * The OpenSearch Contributors require contributions made to | ||
| * this file be licensed under the Apache-2.0 license or a | ||
| * compatible open source license. | ||
| */ | ||
| package org.opensearch.flowframework.exception; | ||
|
|
||
| import org.opensearch.OpenSearchException; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| /** | ||
| * Custom exception to be thrown when an error occurs during the parsing of an API specification. | ||
| */ | ||
| public class ApiSpecParseException extends OpenSearchException { | ||
dbwiddis marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| /** | ||
| * Constructor with message. | ||
| * | ||
| * @param message The detail message. | ||
| */ | ||
| public ApiSpecParseException(String message) { | ||
| super(message); | ||
| } | ||
|
|
||
| /** | ||
| * Constructor with message and cause. | ||
| * | ||
| * @param message The detail message. | ||
| * @param cause The cause of the exception. | ||
| */ | ||
| public ApiSpecParseException(String message, Throwable cause) { | ||
| super(message, cause); | ||
| } | ||
|
|
||
| /** | ||
| * Constructor with message and list of detailed errors. | ||
| * | ||
| * @param message The detail message. | ||
| * @param details The list of errors encountered during the parsing process. | ||
| */ | ||
| public ApiSpecParseException(String message, List<String> details) { | ||
| super(message + ": " + String.join(", ", details)); | ||
| } | ||
| } | ||
120 changes: 120 additions & 0 deletions
120
src/main/java/org/opensearch/flowframework/util/ApiSpecFetcher.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| /* | ||
| * Copyright OpenSearch Contributors | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| * | ||
| * The OpenSearch Contributors require contributions made to | ||
| * this file be licensed under the Apache-2.0 license or a | ||
| * compatible open source license. | ||
| */ | ||
| package org.opensearch.flowframework.util; | ||
|
|
||
| import org.apache.logging.log4j.LogManager; | ||
| import org.apache.logging.log4j.Logger; | ||
| import org.opensearch.common.xcontent.XContentType; | ||
| import org.opensearch.flowframework.exception.ApiSpecParseException; | ||
| import org.opensearch.rest.RestRequest; | ||
|
|
||
| import java.util.HashSet; | ||
| import java.util.List; | ||
|
|
||
| import io.swagger.v3.oas.models.OpenAPI; | ||
| import io.swagger.v3.oas.models.Operation; | ||
| import io.swagger.v3.oas.models.PathItem; | ||
| import io.swagger.v3.oas.models.media.Content; | ||
| import io.swagger.v3.oas.models.media.MediaType; | ||
| import io.swagger.v3.oas.models.media.Schema; | ||
| import io.swagger.v3.oas.models.parameters.RequestBody; | ||
| import io.swagger.v3.parser.OpenAPIV3Parser; | ||
| import io.swagger.v3.parser.core.models.ParseOptions; | ||
| import io.swagger.v3.parser.core.models.SwaggerParseResult; | ||
|
|
||
| /** | ||
| * Utility class for fetching and parsing OpenAPI specifications. | ||
| */ | ||
| public class ApiSpecFetcher { | ||
| private static final Logger logger = LogManager.getLogger(ApiSpecFetcher.class); | ||
| private static final ParseOptions PARSE_OPTIONS = new ParseOptions(); | ||
| private static final OpenAPIV3Parser OPENAPI_PARSER = new OpenAPIV3Parser(); | ||
|
|
||
| static { | ||
| PARSE_OPTIONS.setResolve(true); | ||
| PARSE_OPTIONS.setResolveFully(true); | ||
| } | ||
|
|
||
| /** | ||
| * Parses the OpenAPI specification directly from the URI. | ||
| * | ||
| * @param apiSpecUri URI to the API specification (can be file path or web URI). | ||
| * @return Parsed OpenAPI object. | ||
| * @throws ApiSpecParseException If parsing fails. | ||
| */ | ||
| public static OpenAPI fetchApiSpec(String apiSpecUri) { | ||
| logger.info("Parsing API spec from URI: {}", apiSpecUri); | ||
| SwaggerParseResult result = OPENAPI_PARSER.readLocation(apiSpecUri, null, PARSE_OPTIONS); | ||
| OpenAPI openApi = result.getOpenAPI(); | ||
|
|
||
| if (openApi == null) { | ||
| throw new ApiSpecParseException("Unable to parse spec from URI: " + apiSpecUri, result.getMessages()); | ||
| } | ||
|
|
||
| return openApi; | ||
| } | ||
|
|
||
| /** | ||
| * Compares the required fields in the API spec with the required enum parameters. | ||
| * | ||
| * @param requiredEnumParams List of required parameters from the enum. | ||
| * @param apiSpecUri URI of the API spec to fetch and compare. | ||
| * @param path The API path to check. | ||
| * @param method The HTTP method (POST, GET, etc.). | ||
| * @return boolean indicating if the required fields match. | ||
| */ | ||
| public static boolean compareRequiredFields(List<String> requiredEnumParams, String apiSpecUri, String path, RestRequest.Method method) | ||
| throws IllegalArgumentException, ApiSpecParseException { | ||
| OpenAPI openAPI = fetchApiSpec(apiSpecUri); | ||
|
|
||
| PathItem pathItem = openAPI.getPaths().get(path); | ||
| Content content = getContent(method, pathItem); | ||
| MediaType mediaType = content.get(XContentType.JSON.mediaTypeWithoutParameters()); | ||
| if (mediaType != null) { | ||
| Schema<?> schema = mediaType.getSchema(); | ||
|
|
||
| List<String> requiredApiParams = schema.getRequired(); | ||
| if (requiredApiParams != null && !requiredApiParams.isEmpty()) { | ||
| return new HashSet<>(requiredEnumParams).equals(new HashSet<>(requiredApiParams)); | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| private static Content getContent(RestRequest.Method method, PathItem pathItem) throws IllegalArgumentException, ApiSpecParseException { | ||
junweid62 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| Operation operation; | ||
| switch (method) { | ||
| case POST: | ||
| operation = pathItem.getPost(); | ||
| break; | ||
| case GET: | ||
| operation = pathItem.getGet(); | ||
| break; | ||
| case PUT: | ||
| operation = pathItem.getPut(); | ||
| break; | ||
| case DELETE: | ||
| operation = pathItem.getDelete(); | ||
| break; | ||
| default: | ||
| throw new IllegalArgumentException("Unsupported HTTP method: " + method); | ||
| } | ||
|
|
||
| if (operation == null) { | ||
| throw new IllegalArgumentException("No operation found for the specified method: " + method); | ||
| } | ||
|
|
||
| RequestBody requestBody = operation.getRequestBody(); | ||
| if (requestBody == null) { | ||
| throw new ApiSpecParseException("No requestBody defined for this operation."); | ||
| } | ||
|
|
||
| return requestBody.getContent(); | ||
| } | ||
| } | ||
42 changes: 42 additions & 0 deletions
42
src/test/java/org/opensearch/flowframework/exception/ApiSpecParseExceptionTests.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| /* | ||
| * Copyright OpenSearch Contributors | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| * | ||
| * The OpenSearch Contributors require contributions made to | ||
| * this file be licensed under the Apache-2.0 license or a | ||
| * compatible open source license. | ||
| */ | ||
| package org.opensearch.flowframework.exception; | ||
|
|
||
| import org.opensearch.OpenSearchException; | ||
| import org.opensearch.test.OpenSearchTestCase; | ||
|
|
||
| import java.util.Arrays; | ||
| import java.util.List; | ||
|
|
||
| public class ApiSpecParseExceptionTests extends OpenSearchTestCase { | ||
|
|
||
| public void testApiSpecParseException() { | ||
| ApiSpecParseException exception = new ApiSpecParseException("API spec parsing failed"); | ||
| assertTrue(exception instanceof OpenSearchException); | ||
| assertEquals("API spec parsing failed", exception.getMessage()); | ||
| } | ||
|
|
||
| public void testApiSpecParseExceptionWithCause() { | ||
| Throwable cause = new RuntimeException("Underlying issue"); | ||
| ApiSpecParseException exception = new ApiSpecParseException("API spec parsing failed", cause); | ||
| assertTrue(exception instanceof OpenSearchException); | ||
| assertEquals("API spec parsing failed", exception.getMessage()); | ||
| assertEquals(cause, exception.getCause()); | ||
| } | ||
|
|
||
| public void testApiSpecParseExceptionWithDetailedErrors() { | ||
| String message = "API spec parsing failed"; | ||
| List<String> details = Arrays.asList("Missing required field", "Invalid type"); | ||
| ApiSpecParseException exception = new ApiSpecParseException(message, details); | ||
| assertTrue(exception instanceof OpenSearchException); | ||
| String expectedMessage = "API spec parsing failed: Missing required field, Invalid type"; | ||
| assertEquals(expectedMessage, exception.getMessage()); | ||
| } | ||
|
|
||
| } |
130 changes: 130 additions & 0 deletions
130
src/test/java/org/opensearch/flowframework/util/ApiSpecFetcherTests.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,130 @@ | ||
| /* | ||
| * Copyright OpenSearch Contributors | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| * | ||
| * The OpenSearch Contributors require contributions made to | ||
| * this file be licensed under the Apache-2.0 license or a | ||
| * compatible open source license. | ||
| */ | ||
| package org.opensearch.flowframework.util; | ||
|
|
||
| import org.opensearch.flowframework.exception.ApiSpecParseException; | ||
| import org.opensearch.rest.RestRequest; | ||
junweid62 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| import org.opensearch.test.OpenSearchTestCase; | ||
| import org.junit.Before; | ||
|
|
||
| import java.util.Arrays; | ||
| import java.util.List; | ||
|
|
||
| import io.swagger.v3.oas.models.OpenAPI; | ||
|
|
||
| import static org.opensearch.flowframework.common.CommonValue.ML_COMMONS_API_SPEC_YAML_URI; | ||
| import static org.opensearch.rest.RestRequest.Method.DELETE; | ||
| import static org.opensearch.rest.RestRequest.Method.PATCH; | ||
| import static org.opensearch.rest.RestRequest.Method.POST; | ||
| import static org.opensearch.rest.RestRequest.Method.PUT; | ||
|
|
||
| public class ApiSpecFetcherTests extends OpenSearchTestCase { | ||
|
|
||
| private ApiSpecFetcher apiSpecFetcher; | ||
|
|
||
| @Before | ||
| public void setUp() throws Exception { | ||
| super.setUp(); | ||
| } | ||
|
|
||
| public void testFetchApiSpecSuccess() throws Exception { | ||
|
|
||
| OpenAPI result = ApiSpecFetcher.fetchApiSpec(ML_COMMONS_API_SPEC_YAML_URI); | ||
|
|
||
| assertNotNull("The fetched OpenAPI spec should not be null", result); | ||
| } | ||
|
|
||
| public void testFetchApiSpecThrowsException() throws Exception { | ||
| String invalidUri = "http://invalid-url.com/fail.yaml"; | ||
|
|
||
| ApiSpecParseException exception = expectThrows(ApiSpecParseException.class, () -> { ApiSpecFetcher.fetchApiSpec(invalidUri); }); | ||
|
|
||
| assertNotNull("Exception should be thrown for invalid URI", exception); | ||
| assertTrue(exception.getMessage().contains("Unable to parse spec")); | ||
| } | ||
|
|
||
| public void testCompareRequiredFieldsSuccess() throws Exception { | ||
|
|
||
| String path = "/_plugins/_ml/agents/_register"; | ||
| RestRequest.Method method = POST; | ||
|
|
||
| // Assuming REGISTER_AGENT step in the enum has these required fields | ||
| List<String> expectedRequiredParams = Arrays.asList("name", "type"); | ||
|
|
||
| boolean comparisonResult = ApiSpecFetcher.compareRequiredFields(expectedRequiredParams, ML_COMMONS_API_SPEC_YAML_URI, path, method); | ||
|
|
||
| assertTrue("The required fields should match between API spec and enum", comparisonResult); | ||
| } | ||
|
|
||
| public void testCompareRequiredFieldsFailure() throws Exception { | ||
|
|
||
| String path = "/_plugins/_ml/agents/_register"; | ||
| RestRequest.Method method = POST; | ||
|
|
||
| List<String> wrongRequiredParams = Arrays.asList("nonexistent_param"); | ||
|
|
||
| boolean comparisonResult = ApiSpecFetcher.compareRequiredFields(wrongRequiredParams, ML_COMMONS_API_SPEC_YAML_URI, path, method); | ||
|
|
||
| assertFalse("The required fields should not match for incorrect input", comparisonResult); | ||
| } | ||
|
|
||
| public void testCompareRequiredFieldsThrowsException() throws Exception { | ||
| String invalidUri = "http://invalid-url.com/fail.yaml"; | ||
| String path = "/_plugins/_ml/agents/_register"; | ||
| RestRequest.Method method = PUT; | ||
|
|
||
| Exception exception = expectThrows( | ||
| Exception.class, | ||
| () -> { ApiSpecFetcher.compareRequiredFields(List.of(), invalidUri, path, method); } | ||
| ); | ||
|
|
||
| assertNotNull("An exception should be thrown for an invalid API spec Uri", exception); | ||
| assertTrue(exception.getMessage().contains("Unable to parse spec")); | ||
| } | ||
|
|
||
| public void testUnsupportedMethodException() throws IllegalArgumentException { | ||
| Exception exception = expectThrows(Exception.class, () -> { | ||
| ApiSpecFetcher.compareRequiredFields( | ||
| List.of("name", "type"), | ||
| ML_COMMONS_API_SPEC_YAML_URI, | ||
| "/_plugins/_ml/agents/_register", | ||
| PATCH | ||
| ); | ||
| }); | ||
|
|
||
| assertEquals("Unsupported HTTP method: PATCH", exception.getMessage()); | ||
| } | ||
|
|
||
| public void testNoOperationFoundException() throws Exception { | ||
| Exception exception = expectThrows(IllegalArgumentException.class, () -> { | ||
| ApiSpecFetcher.compareRequiredFields( | ||
| List.of("name", "type"), | ||
| ML_COMMONS_API_SPEC_YAML_URI, | ||
| "/_plugins/_ml/agents/_register", | ||
| DELETE | ||
| ); | ||
| }); | ||
|
|
||
| assertEquals("No operation found for the specified method: DELETE", exception.getMessage()); | ||
| } | ||
|
|
||
| public void testNoRequestBodyDefinedException() throws ApiSpecParseException { | ||
| Exception exception = expectThrows(ApiSpecParseException.class, () -> { | ||
| ApiSpecFetcher.compareRequiredFields( | ||
| List.of("name", "type"), | ||
| ML_COMMONS_API_SPEC_YAML_URI, | ||
| "/_plugins/_ml/model_groups/{model_group_id}", | ||
| RestRequest.Method.GET | ||
| ); | ||
| }); | ||
|
|
||
| assertEquals("No requestBody defined for this operation.", exception.getMessage()); | ||
| } | ||
|
|
||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.