diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/docs/OperationDocProvider.java b/codegen/src/main/java/software/amazon/awssdk/codegen/docs/OperationDocProvider.java index d9c2052b595c..653f7434eefa 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/docs/OperationDocProvider.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/docs/OperationDocProvider.java @@ -15,13 +15,16 @@ package software.amazon.awssdk.codegen.docs; +import static software.amazon.awssdk.codegen.internal.Constant.EXAMPLE_META_PATH; import static software.amazon.awssdk.codegen.internal.DocumentationUtils.createLinkToServiceDocumentation; import static software.amazon.awssdk.codegen.internal.DocumentationUtils.stripHtmlTags; import com.squareup.javapoet.ClassName; import java.util.Collections; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; +import software.amazon.awssdk.codegen.internal.ExampleMetadataProvider; import software.amazon.awssdk.codegen.model.intermediate.DocumentationModel; import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel; import software.amazon.awssdk.codegen.model.intermediate.OperationModel; @@ -51,6 +54,9 @@ abstract class OperationDocProvider { "this method will throw an exception. If the file is not writable by the current user then " + "an exception will be thrown. "; + private static final ExampleMetadataProvider EXAMPLE_PROVIDER = + ExampleMetadataProvider.getInstance(EXAMPLE_META_PATH); + protected final IntermediateModel model; protected final OperationModel opModel; protected final DocConfiguration config; @@ -86,6 +92,10 @@ String getDocs() { if (!crosslink.isEmpty()) { docBuilder.see(crosslink); } + + Optional codeExampleLink = EXAMPLE_PROVIDER + .createLinkToCodeExample(model.getMetadata(), opModel.getOperationName()); + codeExampleLink.ifPresent(docBuilder::see); return docBuilder.build().replace("$", "$"); } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/emitters/tasks/PackageInfoGeneratorTasks.java b/codegen/src/main/java/software/amazon/awssdk/codegen/emitters/tasks/PackageInfoGeneratorTasks.java index 9ccd8f488f81..2bd9968432ba 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/emitters/tasks/PackageInfoGeneratorTasks.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/emitters/tasks/PackageInfoGeneratorTasks.java @@ -15,19 +15,41 @@ package software.amazon.awssdk.codegen.emitters.tasks; +import static software.amazon.awssdk.codegen.internal.Constant.EXAMPLE_META_PATH; + import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import software.amazon.awssdk.codegen.emitters.GeneratorTask; import software.amazon.awssdk.codegen.emitters.GeneratorTaskParams; import software.amazon.awssdk.codegen.emitters.SimpleGeneratorTask; +import software.amazon.awssdk.codegen.internal.ExampleMetadataProvider; import software.amazon.awssdk.codegen.model.intermediate.Metadata; /** * Emits the package-info.java for the base service package. Includes the service - * level documentation. + * level documentation and code examples for that service organized by category. */ public final class PackageInfoGeneratorTasks extends BaseGeneratorTasks { + /** + * Mapping from internal category names to user-friendly display names. + * This defines the preferred order and display format for code example categories. + */ + private static final Map CATEGORY_DISPLAY_MAPPING; + + static { + Map mapping = new LinkedHashMap<>(); + mapping.put("Hello", "Getting Started"); + mapping.put("Basics", "Basics"); + mapping.put("Api", "API Actions"); + mapping.put("Scenarios", "Scenarios"); + mapping.put("Serverless examples", "Serverless Examples"); + CATEGORY_DISPLAY_MAPPING = Collections.unmodifiableMap(mapping); + } + private final String baseDirectory; PackageInfoGeneratorTasks(GeneratorTaskParams dependencies) { @@ -38,17 +60,150 @@ public final class PackageInfoGeneratorTasks extends BaseGeneratorTasks { @Override protected List createTasks() throws Exception { Metadata metadata = model.getMetadata(); - String packageInfoContents = - String.format("/**%n" - + " * %s%n" - + "*/%n" - + "package %s;", - metadata.getDocumentation(), - metadata.getFullClientPackageName()); + String packageInfoContents = buildPackageInfoContent(metadata, EXAMPLE_META_PATH); + return Collections.singletonList(new SimpleGeneratorTask(baseDirectory, "package-info.java", model.getFileHeader(), () -> packageInfoContents)); } + /** + * Builds the complete package-info.java content including Javadoc and package declaration. + * + * @param metadata the service metadata containing documentation and package information + * @param exampleMetaPath the path to the example metadata JSON file + * @return the complete package-info.java file content + */ + String buildPackageInfoContent(Metadata metadata, String exampleMetaPath) { + String baseDocumentation = metadata.getDocumentation(); + String codeExamples = getCodeExamplesWithPath(metadata, exampleMetaPath); + String javadocContent = buildJavadocContent(baseDocumentation, codeExamples); + + return javadocContent + System.lineSeparator() + "package " + metadata.getFullClientPackageName() + ";"; + } + + /** + * Builds the Javadoc comment content for the package-info.java file. + * + * @param baseDocumentation the base service documentation + * @param codeExamples the formatted code examples content, or empty string if none + * @return the complete Javadoc comment including opening and closing markers + */ + private String buildJavadocContent(String baseDocumentation, String codeExamples) { + StringBuilder javadoc = new StringBuilder(); + javadoc.append("/**").append(System.lineSeparator()); + javadoc.append(" * ").append(baseDocumentation).append(System.lineSeparator()); + + if (!codeExamples.isEmpty()) { + javadoc.append(" *").append(System.lineSeparator()); + javadoc.append(" * ").append(codeExamples).append(System.lineSeparator()); + } + + javadoc.append(" */"); + return javadoc.toString(); + } + + /** + * Gets code examples using a custom example metadata path. + */ + private String getCodeExamplesWithPath(Metadata metadata, String exampleMetaPath) { + ExampleMetadataProvider exampleProvider = ExampleMetadataProvider.getInstance(exampleMetaPath); + List examples = exampleProvider.getServiceCodeExamples(metadata); + + if (examples.isEmpty()) { + return ""; + } + + return generateCodeExamplesJavadoc(examples); + } + + private String generateCodeExamplesJavadoc(List examples) { + Map> categorizedExamples = + examples.stream().collect(Collectors.groupingBy(ExampleMetadataProvider.ExampleData::getCategory, + LinkedHashMap::new, + Collectors.toList())); + + StringBuilder javadoc = new StringBuilder(); + javadoc.append("

Code Examples

").append(System.lineSeparator()); + javadoc.append("

The following code examples show how to use this service with the AWS SDK for Java v2:

") + .append(System.lineSeparator()); + + appendPredefinedCategories(javadoc, categorizedExamples, CATEGORY_DISPLAY_MAPPING); + appendRemainingCategories(javadoc, categorizedExamples, CATEGORY_DISPLAY_MAPPING); + + return formatAsJavadocLines(javadoc.toString()); + } + + /** + * Formats HTML content as properly indented Javadoc comment lines. + * Each non-empty line gets prefixed with appropriate Javadoc comment formatting. + * + * @param htmlContent the HTML content to format for Javadoc + * @return formatted string ready for inclusion in Javadoc comments + */ + private String formatAsJavadocLines(String htmlContent) { + StringBuilder result = new StringBuilder(); + String[] lines = htmlContent.split(System.lineSeparator()); + + for (int i = 0; i < lines.length; i++) { + String line = lines[i]; + if (!line.trim().isEmpty()) { + result.append(line); + if (i < lines.length - 1) { + result.append(System.lineSeparator()).append(" * "); + } + } + } + return result.toString(); + } + + /** + * Appends predefined categories to the Javadoc in the preferred order. + * Only includes categories that exist in the categorized examples and have content. + */ + private void appendPredefinedCategories(StringBuilder javadoc, + Map> categorizedExamples, + Map categoryMapping) { + categoryMapping.forEach((category, displayName) -> + appendCategoryIfExists(javadoc, categorizedExamples, category, displayName)); + } + + /** + * Appends any remaining categories that weren't in the predefined mapping. + */ + private void appendRemainingCategories(StringBuilder javadoc, + Map> categorizedExamples, + Map categoryMapping) { + categorizedExamples.entrySet().stream() + .filter(entry -> !categoryMapping.containsKey(entry.getKey())) + .forEach(entry -> appendCategoryIfExists(javadoc, categorizedExamples, entry.getKey(), + entry.getKey())); + } + + /** + * Appends a category section if examples exist for the given category. + */ + private void appendCategoryIfExists(StringBuilder javadoc, + Map> categorizedExamples, + String category, + String displayName) { + List categoryExamples = categorizedExamples.get(category); + if (categoryExamples != null && !categoryExamples.isEmpty()) { + appendCategorySection(javadoc, displayName, categoryExamples); + } + } + + private void appendCategorySection(StringBuilder javadoc, String displayName, + List categoryExamples) { + javadoc.append("

").append(displayName).append("

").append(System.lineSeparator()); + javadoc.append("
    ").append(System.lineSeparator()); + + for (ExampleMetadataProvider.ExampleData example : categoryExamples) { + javadoc.append("
  • ") + .append(example.getTitle()).append("
  • ").append(System.lineSeparator()); + } + javadoc.append("
").append(System.lineSeparator()); + } + } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/internal/Constant.java b/codegen/src/main/java/software/amazon/awssdk/codegen/internal/Constant.java index 548df37ff885..5384975f49fb 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/internal/Constant.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/internal/Constant.java @@ -98,6 +98,8 @@ public final class Constant { public static final String AWS_DOCS_HOST = "docs.aws.amazon.com"; + public static final String EXAMPLE_META_PATH = "software/amazon/awssdk/codegen/example-meta.json"; + public static final String APPROVED_SIMPLE_METHOD_VERBS = "(get|list|describe|lookup|batchGet).*"; public static final String ASYNC_STREAMING_INPUT_PARAM = "requestBody"; diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/internal/DocumentationUtils.java b/codegen/src/main/java/software/amazon/awssdk/codegen/internal/DocumentationUtils.java index 877ecef95ea2..c6f11f3043fe 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/internal/DocumentationUtils.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/internal/DocumentationUtils.java @@ -177,4 +177,4 @@ public static String defaultFluentReturn() { public static String defaultExistenceCheck() { return DEFAULT_EXISTENCE_CHECK; } -} +} \ No newline at end of file diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/internal/ExampleMetadataProvider.java b/codegen/src/main/java/software/amazon/awssdk/codegen/internal/ExampleMetadataProvider.java new file mode 100644 index 000000000000..93113a5c0163 --- /dev/null +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/internal/ExampleMetadataProvider.java @@ -0,0 +1,277 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.codegen.internal; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import software.amazon.awssdk.codegen.model.intermediate.Metadata; +import software.amazon.awssdk.protocols.jsoncore.JsonNode; +import software.amazon.awssdk.protocols.jsoncore.JsonNodeParser; +import software.amazon.awssdk.utils.Lazy; +import software.amazon.awssdk.utils.Logger; + +/** + * Class for loading and caching code example metadata from JSON files. + */ +public final class ExampleMetadataProvider { + + private static final Logger log = Logger.loggerFor(ExampleMetadataProvider.class); + + private static final ConcurrentMap INSTANCE_CACHE = new ConcurrentHashMap<>(); + + private final String exampleMetaPath; + private final JsonNodeParser jsonParser; + private final Lazy> serviceNodeCache; + private final Lazy> normalizedServiceKeyMap; + + private ExampleMetadataProvider(String exampleMetaPath) { + this.exampleMetaPath = exampleMetaPath; + this.jsonParser = JsonNodeParser.create(); + this.serviceNodeCache = new Lazy<>(this::buildServiceNodeCache); + this.normalizedServiceKeyMap = new Lazy<>(this::buildNormalizedServiceKeyMap); + } + + /** + * Creates an ExampleMetadataProvider instance for the given JSON file path. + * + * @param exampleMetaPath path to the example metadata JSON file + * @return ExampleMetadataProvider instance for this path + */ + public static ExampleMetadataProvider getInstance(String exampleMetaPath) { + if (exampleMetaPath == null) { + throw new IllegalArgumentException("exampleMetaPath cannot be null"); + } + return INSTANCE_CACHE.computeIfAbsent(exampleMetaPath, ExampleMetadataProvider::new); + } + + /** + * Clears the instance cache. + */ + public static void clearCache() { + INSTANCE_CACHE.clear(); + } + + /** + * Creates a link to a code example for the given operation. + * + * @param metadata the service metadata + * @param operationName the name of the operation to find an example for + * @return Optional containing the HTML link to the code example, or empty if no example found + */ + public Optional createLinkToCodeExample(Metadata metadata, String operationName) { + try { + if (metadata == null || operationName == null) { + return Optional.empty(); + } + + String actualServiceKey = resolveActualServiceKey(metadata); + if (actualServiceKey == null) { + return Optional.empty(); + } + + String url = findExampleUrl(actualServiceKey, operationName); + + if (url != null) { + return Optional.of(String.format("Code Example", url)); + } + + return Optional.empty(); + } catch (RuntimeException e) { + log.debug(() -> "Failed to create code example link for " + + metadata.getServiceName() + "." + operationName, e); + return Optional.empty(); + } + } + + private String resolveActualServiceKey(Metadata metadata) { + String normalizedServiceName = metadata.getServiceName().toLowerCase(Locale.ROOT); + return normalizedServiceKeyMap.getValue().get(normalizedServiceName); + } + + /** + * Finds the URL for a code example given the service key and operation name. + */ + private String findExampleUrl(String serviceKey, String operationName) { + JsonNode serviceNode = serviceNodeCache.getValue().get(serviceKey); + + if (serviceNode != null) { + String targetExampleId = serviceKey + "_" + operationName; + return findOperationUrl(serviceNode, targetExampleId); + } + + return null; + } + + /** + * Gets all code examples for a specific service. + * + * @param metadata the service metadata + * @return a list of examples for the service + */ + public List getServiceCodeExamples(Metadata metadata) { + List examples = new ArrayList<>(); + + try { + String normalizedServiceName = metadata.getServiceName().toLowerCase(Locale.ROOT); + String actualServiceKey = normalizedServiceKeyMap.getValue().get(normalizedServiceName); + + if (actualServiceKey != null) { + JsonNode serviceNode = serviceNodeCache.getValue().get(actualServiceKey); + if (serviceNode != null) { + examples = parseServiceExamples(serviceNode); + } + } + } catch (Exception e) { + log.debug(() -> "Failed to load examples for " + metadata.getServiceName(), e); + } + + return examples; + } + + /** + * Builds the service node cache from the example metadata file. + * @return map of service keys to their JSON node data + */ + private Map buildServiceNodeCache() { + Map nodeCache = new HashMap<>(); + + try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(exampleMetaPath)) { + if (inputStream == null) { + log.debug(() -> exampleMetaPath + " not found in classpath"); + } else { + JsonNode root = jsonParser.parse(inputStream); + JsonNode servicesNode = root.field("services").orElse(null); + + if (servicesNode != null && servicesNode.isObject()) { + Map servicesMap = servicesNode.asObject(); + servicesMap.forEach((serviceKey, serviceNode) -> { + if (serviceNode != null) { + nodeCache.put(serviceKey, serviceNode); + } + }); + } + } + } catch (IOException | RuntimeException e) { + log.warn(() -> "Failed to load " + exampleMetaPath, e); + } + + return nodeCache; + } + + /** + * Builds the normalized service key mapping from the example metadata file. + * @return map of normalized service names to actual service keys + */ + private Map buildNormalizedServiceKeyMap() { + Map normalizedMap = new HashMap<>(); + + serviceNodeCache.getValue().keySet().forEach(serviceKey -> { + String normalizedKey = serviceKey.replace("-", "").toLowerCase(Locale.ROOT); + normalizedMap.put(normalizedKey, serviceKey); + }); + + return normalizedMap; + } + + /** + * Finds the URL for a specific operation ID within a service node. + */ + private String findOperationUrl(JsonNode serviceNode, String targetExampleId) { + JsonNode examplesNode = serviceNode.field("examples").orElse(null); + if (examplesNode != null && examplesNode.isArray()) { + for (JsonNode example : examplesNode.asArray()) { + JsonNode idNode = example.field("id").orElse(null); + JsonNode urlNode = example.field("url").orElse(null); + + if (idNode != null && urlNode != null) { + String id = idNode.asString(); + if (targetExampleId.equals(id)) { + return urlNode.asString(); + } + } + } + } + return null; + } + + /** + * Parses examples from a service node in the JSON. + */ + private List parseServiceExamples(JsonNode serviceNode) { + List examples = new ArrayList<>(); + JsonNode examplesNode = serviceNode.field("examples").orElse(null); + + if (examplesNode != null && examplesNode.isArray()) { + for (JsonNode example : examplesNode.asArray()) { + JsonNode idNode = example.field("id").orElse(null); + JsonNode titleNode = example.field("title").orElse(null); + JsonNode categoryNode = example.field("category").orElse(null); + JsonNode urlNode = example.field("url").orElse(null); + + if (idNode != null && titleNode != null && urlNode != null) { + String id = idNode.asString(); + String title = titleNode.asString(); + String category = categoryNode != null ? categoryNode.asString() : "Api"; + String url = urlNode.asString(); + + if (!id.isEmpty() && !title.isEmpty() && !url.isEmpty()) { + examples.add(new ExampleData(id, title, category, url)); + } + } + } + } + + return examples; + } + + public static final class ExampleData { + private final String id; + private final String title; + private final String category; + private final String url; + + public ExampleData(String id, String title, String category, String url) { + this.id = id; + this.title = title; + this.category = category; + this.url = url; + } + + public String getId() { + return id; + } + + public String getTitle() { + return title; + } + + public String getCategory() { + return category; + } + + public String getUrl() { + return url; + } + } +} diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/docs/OperationDocProviderTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/docs/OperationDocProviderTest.java new file mode 100644 index 000000000000..734aa2169a31 --- /dev/null +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/docs/OperationDocProviderTest.java @@ -0,0 +1,101 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.codegen.docs; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.Optional; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.codegen.internal.ExampleMetadataProvider; +import software.amazon.awssdk.codegen.model.intermediate.Metadata; + +public class OperationDocProviderTest { + + private static final String TEST_EXAMPLE_META_PATH = "software/amazon/awssdk/codegen/test-example-meta.json"; + + @AfterEach + void cleanupCache() { + ExampleMetadataProvider.clearCache(); + } + + @Test + public void exampleMetadataService_createLinkToCodeExample_withValidExample_returnsCorrectLink() { + ExampleMetadataProvider provider = ExampleMetadataProvider.getInstance(TEST_EXAMPLE_META_PATH); + + Optional result = provider.createLinkToCodeExample(createTestMetadata("s3"), "GetObject"); + + assertThat(result).isPresent(); + assertThat(result.get()).isEqualTo("Code Example"); + } + + @Test + public void exampleMetadataService_createLinkToCodeExample_withNonExistentExample_returnsEmpty() { + ExampleMetadataProvider provider = ExampleMetadataProvider.getInstance(TEST_EXAMPLE_META_PATH); + + Optional result = provider.createLinkToCodeExample(createTestMetadata("s3"), "NonExistentOperation"); + + assertThat(result).isEmpty(); + } + + @Test + public void registryPattern_withMultiplePaths_maintainsSeparateInstances() { + ExampleMetadataProvider provider1 = ExampleMetadataProvider.getInstance(TEST_EXAMPLE_META_PATH); + ExampleMetadataProvider provider2 = ExampleMetadataProvider.getInstance("nonexistent/path.json"); + + assertThat(provider1).isNotSameAs(provider2); + + ExampleMetadataProvider provider1Again = ExampleMetadataProvider.getInstance(TEST_EXAMPLE_META_PATH); + assertThat(provider1).isSameAs(provider1Again); + } + + @Test + public void registryPattern_threadSafety_handlesNullPath() { + assertThatThrownBy(() -> ExampleMetadataProvider.getInstance(null)) + .as("getInstance should reject null paths") + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("exampleMetaPath cannot be null"); + } + + @Test + public void exampleMetadataService_createLinkToCodeExample_withMedicalImagingService_returnsCorrectLink() { + ExampleMetadataProvider provider = ExampleMetadataProvider.getInstance(TEST_EXAMPLE_META_PATH); + + Optional result = provider.createLinkToCodeExample(createTestMetadata("medicalimaging"), "GetImageSet"); + + assertThat(result).isPresent(); + assertThat(result.get()).isEqualTo("Code Example"); + } + + @Test + public void exampleMetadataService_createLinkToCodeExample_withNonExistentService_returnsEmpty() { + ExampleMetadataProvider provider = ExampleMetadataProvider.getInstance(TEST_EXAMPLE_META_PATH); + + Optional result = provider.createLinkToCodeExample(createTestMetadata("nonexistent-service"), "GetObject"); + + assertThat(result).isEmpty(); + } + + private Metadata createTestMetadata(String serviceName) { + Metadata metadata = new Metadata(); + metadata.setServiceName(serviceName); + metadata.setDocumentation("Test service documentation."); + metadata.setRootPackageName("software.amazon.awssdk.services"); + metadata.setClientPackageName("testservice"); + return metadata; + } +} diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/emitters/tasks/PackageInfoGeneratorTasksTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/emitters/tasks/PackageInfoGeneratorTasksTest.java new file mode 100644 index 000000000000..38a94ea15bc3 --- /dev/null +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/emitters/tasks/PackageInfoGeneratorTasksTest.java @@ -0,0 +1,151 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.codegen.emitters.tasks; + +import static org.assertj.core.api.Assertions.assertThat; +import static software.amazon.awssdk.codegen.poet.ClientTestModels.restJsonServiceModels; + +import java.io.InputStream; +import java.util.List; +import java.util.Scanner; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.codegen.emitters.GeneratorTaskParams; +import software.amazon.awssdk.codegen.internal.ExampleMetadataProvider; +import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel; +import software.amazon.awssdk.codegen.model.intermediate.Metadata; + +public class PackageInfoGeneratorTasksTest { + + private static final String TEST_EXAMPLE_META_PATH = "software/amazon/awssdk/codegen/test-example-meta.json"; + + @AfterEach + void cleanupCache() { + ExampleMetadataProvider.clearCache(); + } + + @Test + public void exampleMetadataService_withExamples_returnsCorrectExamples() { + ExampleMetadataProvider provider = ExampleMetadataProvider.getInstance(TEST_EXAMPLE_META_PATH); + + List result = provider.getServiceCodeExamples(createTestMetadata("s3")); + + assertThat(result).hasSize(5); + assertThat(result.get(0).getTitle()).isEqualTo("Get an object from a bucket"); + assertThat(result.get(0).getCategory()).isEqualTo("Api"); + assertThat(result.get(0).getUrl()).contains("s3_example_s3_GetObject_section.html"); + } + + @Test + public void exampleMetadataService_withoutExamples_returnsEmptyList() { + ExampleMetadataProvider provider = ExampleMetadataProvider.getInstance(TEST_EXAMPLE_META_PATH); + + List result = provider.getServiceCodeExamples(createTestMetadata("empty-service")); + + assertThat(result).isEmpty(); + } + + @Test + public void exampleMetadataService_withNonExistentService_returnsEmptyList() { + ExampleMetadataProvider provider = ExampleMetadataProvider.getInstance(TEST_EXAMPLE_META_PATH); + + List result = provider.getServiceCodeExamples(createTestMetadata("nonexistent")); + + assertThat(result).isEmpty(); + } + + @Test + public void exampleMetadataService_withMissingExampleFile_returnsEmptyList() { + ExampleMetadataProvider provider = ExampleMetadataProvider.getInstance("nonexistent/path.json"); + + List result = provider.getServiceCodeExamples(createTestMetadata("s3")); + + assertThat(result).isEmpty(); + } + + @Test + public void exampleMetadataService_withMedicalImagingService_returnsCorrectExamples() { + ExampleMetadataProvider provider = ExampleMetadataProvider.getInstance(TEST_EXAMPLE_META_PATH); + + List result = provider.getServiceCodeExamples(createTestMetadata("medicalimaging")); + + assertThat(result).hasSize(1); + assertThat(result.get(0).getTitle()).isEqualTo("Get image set properties"); + assertThat(result.get(0).getCategory()).isEqualTo("Api"); + assertThat(result.get(0).getUrl()).contains("medical-imaging_example_medical-imaging_GetImageSet_section.html"); + } + + @Test + public void buildPackageInfoContent_withS3Examples_generatesExpectedContent() { + String actualContent = generatePackageInfoContent("s3"); + String expectedContent = loadFixtureFile("s3-package-info.java"); + + assertThat(actualContent).isEqualToIgnoringWhitespace(expectedContent); + } + + @Test + public void buildPackageInfoContent_withMedicalImagingExamples_generatesExpectedContent() { + String actualContent = generatePackageInfoContent("medicalimaging"); + String expectedContent = loadFixtureFile("medical-imaging-package-info.java"); + + assertThat(actualContent).isEqualToIgnoringWhitespace(expectedContent); + } + + @Test + public void buildPackageInfoContent_withNoExamples_generatesContentWithoutCodeExamples() { + String actualContent = generatePackageInfoContent("empty-service"); + String expectedContent = loadFixtureFile("empty-service-package-info.java"); + + assertThat(actualContent).isEqualToIgnoringWhitespace(expectedContent); + } + + @Test + public void buildPackageInfoContent_withNonExistentService_generatesContentWithoutCodeExamples() { + String actualContent = generatePackageInfoContent("nonexistent"); + String expectedContent = loadFixtureFile("empty-service-package-info.java"); + + assertThat(actualContent).isEqualToIgnoringWhitespace(expectedContent); + } + + private PackageInfoGeneratorTasks createTestGenerator() { + IntermediateModel model = restJsonServiceModels(); + GeneratorTaskParams dependencies = GeneratorTaskParams.create(model, "sources/", "tests/", "resources/"); + return new PackageInfoGeneratorTasks(dependencies); + } + + private Metadata createTestMetadata(String serviceName) { + Metadata metadata = new Metadata(); + metadata.setServiceName(serviceName); + metadata.setDocumentation("Test service documentation."); + metadata.setRootPackageName("software.amazon.awssdk.services"); + metadata.setClientPackageName("testservice"); + return metadata; + } + + private String generatePackageInfoContent(String serviceName) { + PackageInfoGeneratorTasks generator = createTestGenerator(); + Metadata metadata = createTestMetadata(serviceName); + return generator.buildPackageInfoContent(metadata, TEST_EXAMPLE_META_PATH); + } + + private String loadFixtureFile(String filename) { + InputStream is = getClass().getResourceAsStream(filename); + if (is == null) { + throw new RuntimeException("Fixture file not found: " + filename); + } + return new Scanner(is).useDelimiter("\\A").next(); + } +} diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/emitters/tasks/empty-service-package-info.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/emitters/tasks/empty-service-package-info.java new file mode 100644 index 000000000000..c9e2ac26375a --- /dev/null +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/emitters/tasks/empty-service-package-info.java @@ -0,0 +1,4 @@ +/** + * Test service documentation. + */ +package software.amazon.awssdk.services.testservice; diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/emitters/tasks/medical-imaging-package-info.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/emitters/tasks/medical-imaging-package-info.java new file mode 100644 index 000000000000..b7ecbd75a4b3 --- /dev/null +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/emitters/tasks/medical-imaging-package-info.java @@ -0,0 +1,11 @@ +/** + * Test service documentation. + * + *

Code Examples

+ *

The following code examples show how to use this service with the AWS SDK for Java v2:

+ *

API Actions

+ * + */ +package software.amazon.awssdk.services.testservice; diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/emitters/tasks/s3-package-info.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/emitters/tasks/s3-package-info.java new file mode 100644 index 000000000000..e5ef0539dcb8 --- /dev/null +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/emitters/tasks/s3-package-info.java @@ -0,0 +1,24 @@ +/** + * Test service documentation. + * + *

Code Examples

+ *

The following code examples show how to use this service with the AWS SDK for Java v2:

+ *

Getting Started

+ * + *

Basics

+ * + *

API Actions

+ * + *

Scenarios

+ * + */ +package software.amazon.awssdk.services.testservice; diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/test-example-meta.json b/codegen/src/test/resources/software/amazon/awssdk/codegen/test-example-meta.json new file mode 100644 index 000000000000..b69129cecab9 --- /dev/null +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/test-example-meta.json @@ -0,0 +1,51 @@ +{ + "services": { + "s3": { + "examples": [ + { + "id": "s3_GetObject", + "title": "Get an object from a bucket", + "category": "Api", + "url": "https://docs.aws.amazon.com/code-library/latest/ug/s3_example_s3_GetObject_section.html" + }, + { + "id": "s3_PutObject", + "title": "Upload an object to a bucket", + "category": "Api", + "url": "https://docs.aws.amazon.com/code-library/latest/ug/s3_example_s3_PutObject_section.html" + }, + { + "id": "s3_HelloS3", + "title": "Hello Amazon S3", + "category": "Hello", + "url": "https://docs.aws.amazon.com/code-library/latest/ug/s3_example_s3_HelloS3_section.html" + }, + { + "id": "s3_S3Basics", + "title": "Learn the basics", + "category": "Basics", + "url": "https://docs.aws.amazon.com/code-library/latest/ug/s3_example_s3_Basics_section.html" + }, + { + "id": "s3_S3Scenario", + "title": "Create a presigned URL", + "category": "Scenarios", + "url": "https://docs.aws.amazon.com/code-library/latest/ug/s3_example_s3_Scenario_section.html" + } + ] + }, + "medical-imaging": { + "examples": [ + { + "id": "medical-imaging_GetImageSet", + "title": "Get image set properties", + "category": "Api", + "url": "https://docs.aws.amazon.com/code-library/latest/ug/medical-imaging_example_medical-imaging_GetImageSet_section.html" + } + ] + }, + "empty-service": { + "examples": [] + } + } +} \ No newline at end of file