-
Notifications
You must be signed in to change notification settings - Fork 970
Add code example links to individual method documentation in API refs guide #6626
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
base: master
Are you sure you want to change the base?
Changes from all commits
c306af1
700aec0
dd0c522
35e541b
6fb8b85
ac222ce
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,9 +17,11 @@ | |
|
|
||
| import java.util.Collections; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| 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.DocumentationUtils; | ||
| import software.amazon.awssdk.codegen.model.intermediate.Metadata; | ||
|
|
||
| /** | ||
|
|
@@ -38,17 +40,100 @@ public final class PackageInfoGeneratorTasks extends BaseGeneratorTasks { | |
| @Override | ||
| protected List<GeneratorTask> createTasks() throws Exception { | ||
| Metadata metadata = model.getMetadata(); | ||
| String packageInfoContents = | ||
| String.format("/**%n" | ||
| + " * %s%n" | ||
| + "*/%n" | ||
| + "package %s;", | ||
| metadata.getDocumentation(), | ||
| metadata.getFullClientPackageName()); | ||
|
|
||
| String baseDocumentation = metadata.getDocumentation(); | ||
|
|
||
| String codeExamples = getCodeExamples(metadata); | ||
|
|
||
| StringBuilder sb = new StringBuilder(); | ||
| sb.append(String.format("/**%n * %s%n", baseDocumentation)); | ||
| if (!codeExamples.isEmpty()) { | ||
| sb.append(String.format(" *%n * %s%n", codeExamples)); | ||
| } | ||
| sb.append(String.format("*/%npackage %s;", metadata.getFullClientPackageName())); | ||
| String packageInfoContents = sb.toString(); | ||
| return Collections.singletonList(new SimpleGeneratorTask(baseDirectory, | ||
| "package-info.java", | ||
| model.getFileHeader(), | ||
| () -> packageInfoContents)); | ||
| } | ||
|
|
||
| String getCodeExamples(Metadata metadata) { | ||
| String exampleMetaPath = "software/amazon/awssdk/codegen/example-meta.json"; | ||
| List<DocumentationUtils.ExampleData> examples = | ||
| DocumentationUtils.getServiceCodeExamples(metadata, exampleMetaPath); | ||
|
|
||
| if (examples.isEmpty()) { | ||
| return ""; | ||
| } | ||
|
|
||
| String codeExamplesJavadoc = generateCodeExamplesJavadoc(examples); | ||
|
|
||
| StringBuilder result = new StringBuilder(); | ||
| String[] lines = codeExamplesJavadoc.split("\n"); | ||
| for (String line : lines) { | ||
| if (!line.trim().isEmpty()) { | ||
| result.append(line); | ||
| if (!line.equals(lines[lines.length - 1])) { | ||
| result.append(System.lineSeparator()).append(" * "); | ||
| } | ||
| } | ||
| } | ||
|
Comment on lines
+72
to
+81
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not straighforward what this block is trying to do. Can we create a separate method with documentation? |
||
|
|
||
| return result.toString(); | ||
| } | ||
|
|
||
|
|
||
| private String generateCodeExamplesJavadoc(List<DocumentationUtils.ExampleData> examples) { | ||
| Map<String, List<DocumentationUtils.ExampleData>> categorizedExamples = new java.util.LinkedHashMap<>(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's replace qualified name with import. Can be shortened to the following: Map<String, List<DocumentationUtils.ExampleData>> categorizedExamples =
examples.stream().collect(Collectors.groupingBy(DocumentationUtils.ExampleData::getCategory,
LinkedHashMap::new,
Collectors.toList())); |
||
| for (DocumentationUtils.ExampleData example : examples) { | ||
| categorizedExamples.computeIfAbsent(example.getCategory(), k -> new java.util.ArrayList<>()).add(example); | ||
| } | ||
|
|
||
| StringBuilder javadoc = new StringBuilder(); | ||
| javadoc.append("<h2>Code Examples</h2>").append("\n"); | ||
| javadoc.append("<p>The following code examples show how to use this service with the AWS SDK for Java v2:</p>") | ||
| .append("\n"); | ||
|
|
||
| Map<String, String> categoryMapping = new java.util.LinkedHashMap<>(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we initialize this map in ctor? |
||
| categoryMapping.put("Hello", "Getting Started"); | ||
| categoryMapping.put("Basics", "Basics"); | ||
| categoryMapping.put("Api", "API Actions"); | ||
| categoryMapping.put("Scenarios", "Scenarios"); | ||
| categoryMapping.put("Serverless examples", "Serverless Examples"); | ||
|
|
||
| for (Map.Entry<String, String> entry : categoryMapping.entrySet()) { | ||
| String category = entry.getKey(); | ||
| String displayName = entry.getValue(); | ||
| List<DocumentationUtils.ExampleData> categoryExamples = categorizedExamples.get(category); | ||
| if (categoryExamples != null && !categoryExamples.isEmpty()) { | ||
| appendCategorySection(javadoc, displayName, categoryExamples); | ||
| } | ||
| } | ||
|
|
||
| for (Map.Entry<String, List<DocumentationUtils.ExampleData>> entry : categorizedExamples.entrySet()) { | ||
| String category = entry.getKey(); | ||
| if (!categoryMapping.containsKey(category)) { | ||
| List<DocumentationUtils.ExampleData> categoryExamples = entry.getValue(); | ||
| if (!categoryExamples.isEmpty()) { | ||
| appendCategorySection(javadoc, category, categoryExamples); | ||
| } | ||
| } | ||
| } | ||
|
Comment on lines
+105
to
+122
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not very straighforward what those two loop do. Can we create separat methods with descriptive method names (ideally with javadocs). |
||
|
|
||
| return javadoc.toString(); | ||
| } | ||
|
|
||
| private void appendCategorySection(StringBuilder javadoc, String displayName, | ||
| List<DocumentationUtils.ExampleData> categoryExamples) { | ||
| javadoc.append("<h3>").append(displayName).append("</h3>").append("\n"); | ||
| javadoc.append("<ul>").append("\n"); | ||
|
|
||
| for (DocumentationUtils.ExampleData example : categoryExamples) { | ||
| javadoc.append("<li><a href=\"").append(example.getUrl()).append("\" target=\"_top\">") | ||
| .append(example.getTitle()).append("</a></li>").append("\n"); | ||
| } | ||
| javadoc.append("</ul>").append("\n"); | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,12 +20,22 @@ | |
| import static software.amazon.awssdk.codegen.model.intermediate.ShapeType.Request; | ||
| import static software.amazon.awssdk.codegen.model.intermediate.ShapeType.Response; | ||
|
|
||
| import com.fasterxml.jackson.databind.JsonNode; | ||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||
| import java.io.IOException; | ||
| import java.io.InputStream; | ||
| import java.util.ArrayList; | ||
| import java.util.Arrays; | ||
| import java.util.HashMap; | ||
| import java.util.HashSet; | ||
| import java.util.List; | ||
| import java.util.Locale; | ||
| import java.util.Map; | ||
| import java.util.Set; | ||
| import java.util.regex.Pattern; | ||
| import software.amazon.awssdk.codegen.model.intermediate.Metadata; | ||
| import software.amazon.awssdk.codegen.model.intermediate.ShapeModel; | ||
| import software.amazon.awssdk.utils.Logger; | ||
|
|
||
| public final class DocumentationUtils { | ||
|
|
||
|
|
@@ -54,6 +64,11 @@ public final class DocumentationUtils { | |
| "iot", "data.iot", "machinelearning", "rekognition", "s3", "sdb", "swf" | ||
| )); | ||
| private static final Pattern COMMENT_DELIMITER = Pattern.compile("\\*\\/"); | ||
| private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); | ||
|
|
||
| private static final Logger log = Logger.loggerFor(DocumentationUtils.class); | ||
| private static Map<String, JsonNode> serviceNodeCache; | ||
| private static Map<String, String> normalizedServiceKeyMap; | ||
|
Comment on lines
+70
to
+71
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doesn't seem thread safe. Suggesting creating a class instead of using static util methods for the new functionalities. |
||
|
|
||
| private DocumentationUtils() { | ||
| } | ||
|
|
@@ -140,6 +155,39 @@ public static String createLinkToServiceDocumentation(Metadata metadata, ShapeMo | |
| : ""; | ||
| } | ||
|
|
||
| /** | ||
| * Create a link to a code example for the given operation. | ||
| * | ||
| * @param metadata the service metadata containing service name information | ||
| * @param operationName the name of the operation to find an example for | ||
| * @param exampleMetaPath the path to the example metadata JSON file | ||
| * @return a '@see also' HTML link to the code example, or empty string if no example found | ||
| */ | ||
| public static String createLinkToCodeExample(Metadata metadata, String operationName, String exampleMetaPath) { | ||
| try { | ||
| String normalizedServiceName = metadata.getServiceName().toLowerCase(Locale.ROOT); | ||
| Map<String, String> normalizedMap = getNormalizedServiceKeyMap(exampleMetaPath); | ||
| String actualServiceKey = normalizedMap.get(normalizedServiceName); | ||
|
|
||
| if (actualServiceKey != null) { | ||
| String targetExampleId = actualServiceKey + "_" + operationName; | ||
| JsonNode serviceNode = getServiceNode(actualServiceKey, exampleMetaPath); | ||
|
|
||
| if (serviceNode != null) { | ||
| String url = findOperationUrl(serviceNode, targetExampleId); | ||
| if (url != null) { | ||
| return String.format("<a href=\"%s\" target=\"_top\">Code Example</a>", url); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return ""; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we just use null to indicate there's no example, so here, we'd return
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, we're not using null here. we're returning an empty string ("") right, so based on the check we have in
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right, I was suggesting using null to denote lack of example instead of empty string |
||
| } catch (Exception e) { | ||
| log.debug(() -> "Failed to create code example link for " + metadata.getServiceName() + "." + operationName, e); | ||
| return ""; | ||
| } | ||
| } | ||
|
|
||
| public static String removeFromEnd(String string, String stringToRemove) { | ||
| return string.endsWith(stringToRemove) ? string.substring(0, string.length() - stringToRemove.length()) : string; | ||
| } | ||
|
|
@@ -177,4 +225,174 @@ public static String defaultFluentReturn() { | |
| public static String defaultExistenceCheck() { | ||
| return DEFAULT_EXISTENCE_CHECK; | ||
| } | ||
|
|
||
|
|
||
| /** | ||
| * Gets the cached service node | ||
| */ | ||
| private static JsonNode getServiceNode(String serviceKey, String exampleMetaPath) { | ||
| if (serviceNodeCache == null) { | ||
| buildServiceCache(exampleMetaPath); | ||
| } | ||
| return serviceNodeCache != null ? serviceNodeCache.get(serviceKey) : null; | ||
| } | ||
|
|
||
| /** | ||
| * Gets the cached normalized service key map for service name matching. | ||
| */ | ||
| private static Map<String, String> getNormalizedServiceKeyMap(String exampleMetaPath) { | ||
| if (normalizedServiceKeyMap == null) { | ||
| buildServiceCache(exampleMetaPath); | ||
| } | ||
| return normalizedServiceKeyMap != null ? normalizedServiceKeyMap : new HashMap<>(); | ||
| } | ||
|
|
||
| /** | ||
| * Builds the service node cache and normalized service key mapping from the specified example metadata file. | ||
| */ | ||
| private static void buildServiceCache(String exampleMetaPath) { | ||
| Map<String, JsonNode> nodeCache = new HashMap<>(); | ||
| Map<String, String> normalizedMap = new HashMap<>(); | ||
|
|
||
| try (InputStream inputStream = DocumentationUtils.class.getClassLoader() | ||
| .getResourceAsStream(exampleMetaPath)) { | ||
|
|
||
| if (inputStream == null) { | ||
| log.debug(() -> exampleMetaPath + " not found in classpath"); | ||
| } else { | ||
| JsonNode root = OBJECT_MAPPER.readTree(inputStream); | ||
| JsonNode servicesNode = root.get("services"); | ||
|
|
||
| if (servicesNode != null) { | ||
| servicesNode.fieldNames().forEachRemaining(serviceKey -> { | ||
| buildNormalizedMapping(serviceKey, normalizedMap); | ||
| nodeCache.put(serviceKey, servicesNode.get(serviceKey)); | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| } catch (IOException e) { | ||
| log.warn(() -> "Failed to load " + exampleMetaPath, e); | ||
| } | ||
|
|
||
| serviceNodeCache = nodeCache; | ||
| normalizedServiceKeyMap = normalizedMap; | ||
| } | ||
|
|
||
| /** | ||
| * Builds normalized mapping for a service key (e.g., "medical-imaging" -> "medicalimaging"). | ||
| */ | ||
| private static void buildNormalizedMapping(String serviceKey, Map<String, String> normalizedMap) { | ||
| String normalizedKey = serviceKey.replace("-", "").toLowerCase(Locale.ROOT); | ||
| normalizedMap.put(normalizedKey, serviceKey); | ||
| } | ||
|
|
||
| /** | ||
| * Finds the URL for a specific operation ID within a service node. | ||
| */ | ||
| private static String findOperationUrl(JsonNode serviceNode, String targetExampleId) { | ||
| JsonNode examplesNode = serviceNode.get("examples"); | ||
| if (examplesNode != null && examplesNode.isArray()) { | ||
| for (JsonNode example : examplesNode) { | ||
| JsonNode idNode = example.get("id"); | ||
| JsonNode urlNode = example.get("url"); | ||
|
|
||
| if (idNode != null && urlNode != null) { | ||
| String id = idNode.asText(); | ||
| if (targetExampleId.equals(id)) { | ||
| return urlNode.asText(); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| /** | ||
| * Gets all code examples for a specific service. | ||
| * | ||
| * @param metadata the service metadata containing service name information | ||
| * @param exampleMetaPath the path to the example metadata JSON file | ||
| * @return a list of examples for the service | ||
| */ | ||
| public static List<ExampleData> getServiceCodeExamples(Metadata metadata, String exampleMetaPath) { | ||
| List<ExampleData> examples = new ArrayList<>(); | ||
|
|
||
| try { | ||
| String normalizedServiceName = metadata.getServiceName().toLowerCase(Locale.ROOT); | ||
| Map<String, String> normalizedMap = getNormalizedServiceKeyMap(exampleMetaPath); | ||
| String actualServiceKey = normalizedMap.get(normalizedServiceName); | ||
|
|
||
| if (actualServiceKey != null) { | ||
| JsonNode serviceNode = getServiceNode(actualServiceKey, exampleMetaPath); | ||
| if (serviceNode != null) { | ||
| examples = parseServiceExamples(serviceNode); | ||
| } | ||
| } | ||
| } catch (Exception e) { | ||
| log.debug(() -> "Failed to load examples for " + metadata.getServiceName(), e); | ||
| } | ||
|
|
||
| return examples; | ||
| } | ||
|
|
||
| /** | ||
| * Parses examples from a service node in the JSON. | ||
| */ | ||
| private static List<ExampleData> parseServiceExamples(JsonNode serviceNode) { | ||
| List<ExampleData> examples = new ArrayList<>(); | ||
| JsonNode examplesNode = serviceNode.get("examples"); | ||
|
|
||
| if (examplesNode != null && examplesNode.isArray()) { | ||
| for (JsonNode example : examplesNode) { | ||
| JsonNode idNode = example.get("id"); | ||
| JsonNode titleNode = example.get("title"); | ||
| JsonNode categoryNode = example.get("category"); | ||
| JsonNode urlNode = example.get("url"); | ||
|
|
||
| if (idNode != null && titleNode != null && urlNode != null) { | ||
| String id = idNode.asText(); | ||
| String title = titleNode.asText(); | ||
| String category = categoryNode != null ? categoryNode.asText() : "Api"; | ||
| String url = urlNode.asText(); | ||
|
|
||
| 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; | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we create a constant?