diff --git a/docs/contributor/README.md b/docs/contributor/README.md index 9f9a26aa7405..1de726c5377a 100644 --- a/docs/contributor/README.md +++ b/docs/contributor/README.md @@ -28,6 +28,7 @@ If you are a **consumer** of the SDK looking for usage guidance, start at the [U | [Versioning](https://github.com/Azure/azure-sdk-for-java/blob/main/docs/contributor/versioning.md) | `version_client.txt`, dependency tags, incrementing versions | | [Adding a Module](https://github.com/Azure/azure-sdk-for-java/blob/main/docs/contributor/adding-a-module.md) | Create a new SDK module: dir structure, POM, versioning, CODEOWNERS | | [TypeSpec Quickstart](https://github.com/Azure/azure-sdk-for-java/blob/main/docs/contributor/typespec-quickstart.md) | End-to-end workflow: generate → build → test → release | +| [TypeSpec Java Customization](https://github.com/Azure/azure-sdk-for-java/blob/main/docs/contributor/typespec-java-customization.md) | Customize generated Java code when TypeSpec cannot express the required behavior | | [Writing Performance Tests](https://github.com/Azure/azure-sdk-for-java/blob/main/docs/contributor/performance-tests.md) | Set up and run `perf-test-core` benchmarks | | [JavaDoc & Code Snippets](https://github.com/Azure/azure-sdk-for-java/blob/main/docs/contributor/javadocs.md) | Javadoc standards and codesnippet-maven-plugin workflow | | [Access Helpers](https://github.com/Azure/azure-sdk-for-java/blob/main/docs/contributor/access-helpers.md) | Cross-package internal access without public APIs | diff --git a/docs/contributor/sdk-generation-pipeline-troubleshooting.md b/docs/contributor/sdk-generation-pipeline-troubleshooting.md index 6b54bcf646da..30e2d3ba8a06 100644 --- a/docs/contributor/sdk-generation-pipeline-troubleshooting.md +++ b/docs/contributor/sdk-generation-pipeline-troubleshooting.md @@ -131,7 +131,7 @@ please check whether it causes failure, and fix them before apiview. > - *generated code*: produced by the generator; API surface may change when the spec or generator changes, which can break compilation. > - *customization code*: maintained by SDK developers; commonly wired via `customization-class` and preserved during regeneration with `partial-update: true`. > -> Reference: autorest.java customization-base: https://github.com/Azure/autorest.java/tree/main/customization-base +> Reference: [TypeSpec Java Customization](https://github.com/Azure/azure-sdk-for-java/blob/main/docs/contributor/typespec-java-customization.md) **Prerequisites for customization:** When adding customization code, two options must be set in `tspconfig.yaml`: ```yaml diff --git a/docs/contributor/typespec-java-customization.md b/docs/contributor/typespec-java-customization.md new file mode 100644 index 000000000000..05c3f102c0c9 --- /dev/null +++ b/docs/contributor/typespec-java-customization.md @@ -0,0 +1,165 @@ +# TypeSpec Java Customization + +The `azure-autorest-customization` package provides APIs to safely customize generated Java code for cases that +TypeSpec Java cannot represent directly. Customizations use JavaParser ASTs through `ClassCustomization.customizeAst`. + +## Before you customize + +First consider whether the change belongs in TypeSpec (`client.tsp`). TypeSpec customizations are cleaner and persist +through regeneration. See the [TypeSpec Client Customizations Reference](https://github.com/Azure/azure-sdk-for-java/blob/main/eng/common/knowledge/customizing-client-tsp.md) for decorators such as `@@clientName` and `@@access`. + +Use Java code customizations only when TypeSpec cannot express the required behavior. + +## Set up a customization project + +Create a Maven project that depends on `azure-autorest-customization`: + +```xml + + com.azure.tools + azure-autorest-customization + 1.0.0-beta.11 + +``` + +Create a class that extends `com.azure.autorest.customization.Customization` and overrides +`void customize(LibraryCustomization, Logger)`. The `LibraryCustomization` parameter is the entry point for changing +generated Java code before it is written to disk. + +```java +@Override +public void customize(LibraryCustomization customization, Logger logger) { + customization.getClass("com.azure.myservice.models", "Foo") + .customizeAst(ast -> ast.getClassByName("Foo") + .ifPresent(clazz -> clazz.addMarkerAnnotation("Deprecated"))); +} +``` + +Configure the generator to use the class: + +```diff + "@azure-tools/typespec-java": + emitter-output-dir: "{output-dir}/{service-dir}/azure-contoso-widgetmanager" + namespace: com.azure.contoso.widgetmanager ++ customization-class: customization/src/main/java/MyCustomization.java +``` + +## Navigate generated code + +Start with `LibraryCustomization`, then navigate to a package or class with `getClass(packageName, className)`. +`LibraryCustomization`, `PackageCustomization`, and `ClassCustomization` provide the primary navigation APIs. +Use `customizeAst` to change the JavaParser AST for the selected source file. Add imports with +`ast.addImport(...)` whenever the new code references a type not already imported. + +## Supported customizations + +The following common changes can be made through the JavaParser AST: + +| Customization | Typical AST operation | +|---|---| +| Change a class modifier | `ClassOrInterfaceDeclaration.setModifiers(...)` | +| Change a method modifier | `MethodDeclaration.setModifiers(...)` | +| Change a method return type or body | `MethodDeclaration.setType(...)` and `setBody(...)` | +| Change a class supertype | Clear and add `ClassOrInterfaceType` extended types | +| Add or remove a class or method annotation | `addMarkerAnnotation(...)` or remove the matching annotation node | +| Add a field default value | `VariableDeclarator.setInitializer(...)` | +| Add getter and setter methods | `ClassOrInterfaceDeclaration.addMethod(...)` | +| Rename an enum member | `EnumConstantDeclaration.setName(...)` | +| Update class or method Javadoc | `setJavadocComment(...)` | +| Add or remove Javadoc tags | Update the `Javadoc` returned by `getJavadoc()` | + +### Change a method modifier + +```java +@Override +public void customize(LibraryCustomization customization, Logger logger) { + customization.getClass("com.azure.myservice.models", "Foo").customizeAst(ast -> ast.getClassByName("Foo") + .ifPresent(clazz -> clazz.getMethodsByName("getBar") + .forEach(method -> method.setModifiers(Modifier.Keyword.PRIVATE)))); +} +``` + +### Change a method return type + +When changing a return type, update both the declared type and method body. Add an import for the replacement type. + +```java +@Override +public void customize(LibraryCustomization customization, Logger logger) { + customization.getClass("com.azure.myservice.models", "Foo").customizeAst(ast -> { + ast.addImport(UUID.class); + ast.getClassByName("Foo").ifPresent(clazz -> clazz.getMethodsByName("getId").forEach(method -> { + method.setType("UUID"); + method.setBody(StaticJavaParser.parseBlock("{ return UUID.fromString(this.id); }")); + })); + }); +} +``` + +For a method originally returning `void`, provide the complete return expression. To change a method to return +`void`, set its body appropriately and do not add a return value. + +### Change a supertype + +```java +@Override +public void customize(LibraryCustomization customization, Logger logger) { + customization.getClass("com.azure.myservice.models", "Foo").customizeAst(ast -> ast.getClassByName("Foo") + .ifPresent(clazz -> { + ast.addImport("com.azure.myservice.models.Bar1"); + clazz.getExtendedTypes().clear(); + clazz.addExtendedType(new ClassOrInterfaceType(null, "Bar1")); + })); +} +``` + +### Add a field default value + +```java +@Override +public void customize(LibraryCustomization customization, Logger logger) { + customization.getClass("com.azure.myservice.models", "Foo").customizeAst(ast -> ast.getClassByName("Foo") + .flatMap(clazz -> clazz.getFieldByName("bar")) + .ifPresent(field -> field.getVariables().forEach(variable -> { + if ("bar".equals(variable.getNameAsString())) { + variable.setInitializer("\"bar\""); + } + }))); +} +``` + +### Generate accessor methods + +```java +@Override +public void customize(LibraryCustomization customization, Logger logger) { + customization.getClass("com.azure.myservice.models", "Foo").customizeAst(ast -> ast.getClassByName("Foo") + .ifPresent(clazz -> { + clazz.addMethod("isActive", Modifier.Keyword.PUBLIC).setType("boolean") + .setBody(StaticJavaParser.parseBlock("{ return this.active; }")); + clazz.addMethod("setActive", Modifier.Keyword.PUBLIC).setType("Foo") + .addParameter("boolean", "active") + .setBody(StaticJavaParser.parseBlock("{ this.active = active; return this; }")); + })); +} +``` + +### Update Javadoc + +Set a complete description with `setJavadocComment`. For parameter, return, and exception tags, get the existing +Javadoc and add a block tag: + +```java +method.getJavadoc().ifPresent(javadoc -> method.setJavadocComment( + javadoc.addBlockTag("param", "active", "whether the Foo is active"))); +``` + +Use `return` for a return-value tag and `throws` with the exception type and description for an exception tag. + +## Troubleshooting + +### TypeSpec Java reports “Failed to format file: ``. File content: ``.” + +Customized Java code likely contains a syntax error. + +Inspect the source shown after `File content:` to locate the malformed code, then fix the customization and regenerate SDK. diff --git a/docs/contributor/typespec-quickstart.md b/docs/contributor/typespec-quickstart.md index c482c82b5fde..c2d0878c0c90 100644 --- a/docs/contributor/typespec-quickstart.md +++ b/docs/contributor/typespec-quickstart.md @@ -101,6 +101,8 @@ Set `partial-update: true` in `tspconfig.yaml` emitter options. TypeSpec-Java wi customization-class: customization/src/main/java/MyCustomization.java ``` +See [TypeSpec Java Customization](https://github.com/Azure/azure-sdk-for-java/blob/main/docs/contributor/typespec-java-customization.md) for guidance on writing AST-based Java customizations. + --- ## 5. Improve Documentation