diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml_ similarity index 100% rename from .github/workflows/ci.yml rename to .github/workflows/ci.yml_ diff --git a/.github/workflows/release.yml_ b/.github/workflows/release.yml_ new file mode 100644 index 0000000..36b96e4 --- /dev/null +++ b/.github/workflows/release.yml_ @@ -0,0 +1,90 @@ +name: Release + +on: + push: + tags: + - 'v*' + +jobs: + release: + runs-on: ubuntu-latest + + name: Release to Maven Central + + permissions: + contents: write + packages: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Java 21 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + cache: 'gradle' + + - name: Extract version from tag + id: version + run: | + VERSION=${GITHUB_REF#refs/tags/v} + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Releasing version: $VERSION" + + - name: Update version in build.gradle.kts + run: | + sed -i "s/version = \".*\"/version = \"${{ steps.version.outputs.version }}\"/" build.gradle.kts + cat build.gradle.kts | grep "version =" + + - name: Make Gradle wrapper executable + run: chmod +x gradlew + + - name: Build + run: ./gradlew clean build + + - name: Import GPG key + uses: crazy-max/ghaction-import-gpg@v6 + with: + gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} + passphrase: ${{ secrets.GPG_PASSPHRASE }} + + - name: Publish to Maven Central + run: ./gradlew publish + env: + MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }} + MAVEN_PASSWORD: ${{ secrets.OSSRH_TOKEN }} + GPG_SIGNING_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + GPG_SIGNING_PASSWORD: ${{ secrets.GPG_PASSPHRASE }} + + - name: Create GitHub Release + uses: softprops/action-gh-release@v1 + with: + name: Release ${{ steps.version.outputs.version }} + draft: false + prerelease: false + files: | + build/libs/smithy-unison-*.jar + body: | + ## smithy-unison v${{ steps.version.outputs.version }} + + ### Installation + + Add to your `smithy-build.json`: + ```json + { + "maven": { + "dependencies": [ + "io.smithy.unison:smithy-unison:${{ steps.version.outputs.version }}" + ] + } + } + ``` + + Or add to your build.gradle.kts: + ```kotlin + dependencies { + implementation("io.smithy.unison:smithy-unison:${{ steps.version.outputs.version }}") + } + ``` diff --git a/examples/aws-demo/compile.sh b/examples/aws-demo/compile.sh index 3334c19..72930c2 100755 --- a/examples/aws-demo/compile.sh +++ b/examples/aws-demo/compile.sh @@ -108,59 +108,6 @@ scratch/main> load generated/aws_s3_client.u scratch/main> add \`\`\` -Create namespace aliases so main.u can use lib.f34nk_aws_0_1_2 imports: - -\`\`\`ucm -scratch/main> alias.type Config lib.f34nk_aws_0_1_2.Config -scratch/main> alias.type Credentials lib.f34nk_aws_0_1_2.Credentials -scratch/main> alias.type ListBucketsRequest lib.f34nk_aws_0_1_2.ListBucketsRequest -scratch/main> alias.type ListBucketsOutput lib.f34nk_aws_0_1_2.ListBucketsOutput -scratch/main> alias.type Bucket lib.f34nk_aws_0_1_2.Bucket -scratch/main> alias.type PutObjectRequest lib.f34nk_aws_0_1_2.PutObjectRequest -scratch/main> alias.type PutObjectOutput lib.f34nk_aws_0_1_2.PutObjectOutput -scratch/main> alias.type ListObjectsV2Request lib.f34nk_aws_0_1_2.ListObjectsV2Request -scratch/main> alias.type ListObjectsV2Output lib.f34nk_aws_0_1_2.ListObjectsV2Output -scratch/main> alias.type Object lib.f34nk_aws_0_1_2.Object -scratch/main> alias.type GetObjectRequest lib.f34nk_aws_0_1_2.GetObjectRequest -scratch/main> alias.type GetObjectOutput lib.f34nk_aws_0_1_2.GetObjectOutput -scratch/main> alias.type DeleteObjectRequest lib.f34nk_aws_0_1_2.DeleteObjectRequest -scratch/main> alias.type DeleteObjectOutput lib.f34nk_aws_0_1_2.DeleteObjectOutput -\`\`\` - -\`\`\`ucm -scratch/main> alias.term objectCannedACLFromText lib.f34nk_aws_0_1_2.objectCannedACLFromText -scratch/main> alias.term listBuckets lib.f34nk_aws_0_1_2.listBuckets -scratch/main> alias.term putObject lib.f34nk_aws_0_1_2.putObject -scratch/main> alias.term listObjectsV2 lib.f34nk_aws_0_1_2.listObjectsV2 -scratch/main> alias.term getObject lib.f34nk_aws_0_1_2.getObject -scratch/main> alias.term deleteObject lib.f34nk_aws_0_1_2.deleteObject -\`\`\` - -Alias constructors: - -\`\`\`ucm -scratch/main> alias.term Credentials.Credentials lib.f34nk_aws_0_1_2.Credentials.Credentials -scratch/main> alias.term Config.Config lib.f34nk_aws_0_1_2.Config.Config -scratch/main> alias.term ListBucketsRequest.ListBucketsRequest lib.f34nk_aws_0_1_2.ListBucketsRequest.ListBucketsRequest -scratch/main> alias.term PutObjectRequest.PutObjectRequest lib.f34nk_aws_0_1_2.PutObjectRequest.PutObjectRequest -scratch/main> alias.term ListObjectsV2Request.ListObjectsV2Request lib.f34nk_aws_0_1_2.ListObjectsV2Request.ListObjectsV2Request -scratch/main> alias.term GetObjectRequest.GetObjectRequest lib.f34nk_aws_0_1_2.GetObjectRequest.GetObjectRequest -scratch/main> alias.term DeleteObjectRequest.DeleteObjectRequest lib.f34nk_aws_0_1_2.DeleteObjectRequest.DeleteObjectRequest -\`\`\` - -Alias record accessors: - -\`\`\`ucm -scratch/main> alias.term Bucket.name lib.f34nk_aws_0_1_2.Bucket.name -scratch/main> alias.term Config.endpoint lib.f34nk_aws_0_1_2.Config.endpoint -scratch/main> alias.term Config.region lib.f34nk_aws_0_1_2.Config.region -scratch/main> alias.term Object.key lib.f34nk_aws_0_1_2.Object.key -scratch/main> alias.term Object.size lib.f34nk_aws_0_1_2.Object.size -scratch/main> alias.term ListBucketsOutput.buckets lib.f34nk_aws_0_1_2.ListBucketsOutput.buckets -scratch/main> alias.term ListObjectsV2Output.contents lib.f34nk_aws_0_1_2.ListObjectsV2Output.contents -scratch/main> alias.term GetObjectOutput.body lib.f34nk_aws_0_1_2.GetObjectOutput.body -\`\`\` - Load the main application: \`\`\`ucm diff --git a/examples/aws-demo/smithy-build.json b/examples/aws-demo/smithy-build.json index 7a6d79b..e55a24c 100644 --- a/examples/aws-demo/smithy-build.json +++ b/examples/aws-demo/smithy-build.json @@ -1,10 +1,14 @@ { "version": "1.0", - "sources": ["model"], + "sources": [ + "model" + ], "maven": { "dependencies": [ - "software.amazon.smithy:smithy-model:1.64.0", "software.amazon.smithy:smithy-aws-traits:1.64.0", + "software.amazon.smithy:smithy-aws-endpoints:1.64.0", + "software.amazon.smithy:smithy-aws-smoke-test-model:1.64.0", + "software.amazon.smithy:smithy-aws-iam-traits:1.64.0", "io.smithy.unison:smithy-unison:0.1.0" ], "repositories": [ @@ -19,6 +23,7 @@ "plugins": { "unison-codegen": { "service": "com.amazonaws.s3#AmazonS3", + "name": "s3", "namespace": "aws.s3", "outputDir": "generated" } diff --git a/examples/aws-demo/src/main.u b/examples/aws-demo/src/main.u index 7e8c4ca..dffcc08 100644 --- a/examples/aws-demo/src/main.u +++ b/examples/aws-demo/src/main.u @@ -8,15 +8,6 @@ -- load aws_http_bridge.u -- add --- Import types from @f34nk/aws library (use statements are file-scoped in Unison) -use lib.f34nk_aws_0_1_2 Config Credentials -use lib.f34nk_aws_0_1_2 ListBucketsRequest ListBucketsOutput Bucket -use lib.f34nk_aws_0_1_2 PutObjectRequest PutObjectOutput objectCannedACLFromText -use lib.f34nk_aws_0_1_2 ListObjectsV2Request ListObjectsV2Output Object -use lib.f34nk_aws_0_1_2 GetObjectRequest GetObjectOutput -use lib.f34nk_aws_0_1_2 DeleteObjectRequest DeleteObjectOutput -use lib.f34nk_aws_0_1_2 listBuckets putObject listObjectsV2 getObject deleteObject - -- ============================================================================= -- Helpers -- ============================================================================= @@ -32,14 +23,14 @@ envOrDefault name default = do -- ============================================================================= -- Create S3 client configuration for LocalStack -defaultConfig : '{IO, Exception} Config +defaultConfig : '{IO, Exception} Aws.S3.Config defaultConfig = do key = !(envOrDefault "AWS_ACCESS_KEY_ID" "dummy") secret = !(envOrDefault "AWS_SECRET_ACCESS_KEY" "dummy") creds = Credentials.Credentials key secret None endpoint = !(envOrDefault "AWS_ENDPOINT" "http://localhost:4566") region = !(envOrDefault "AWS_DEFAULT_REGION" "us-east-1") - Config.Config endpoint region creds true + Aws.S3.Config.Config endpoint region creds true -- Test bucket name - created by terraform provisioner in docker-compose testBucket : Text @@ -134,20 +125,20 @@ runDemo = do List all buckets in the account. The terraform provisioner creates 'us-east-1-nonprod-configs' bucket. }} -listBucketsDemo : Config -> '{IO, Exception, Threads} () +listBucketsDemo : Aws.S3.Config -> '{IO, Exception, Threads} () listBucketsDemo config = do printLine " → Creating ListBucketsRequest..." -- Create request with all optional fields as None - request = ListBucketsRequest.ListBucketsRequest None None None None + request = Aws.S3.ListBucketsRequest.ListBucketsRequest None None None None -- Call the S3 client printLine " → Calling listBuckets..." - result = !(listBuckets config request) + result = !(Aws.S3.listBuckets config request) -- Extract and print buckets printLine " ✓ SUCCESS: ListBuckets returned" - match ListBucketsOutput.buckets result with + match Aws.S3.ListBucketsOutput.buckets result with Some buckets -> count = List.size buckets printLine (" → Found " ++ Nat.toText count ++ " bucket(s):") @@ -158,7 +149,7 @@ listBucketsDemo config = do {{ Upload a test object to the configs bucket. }} -putObjectDemo : Config -> '{IO, Exception, Threads} () +putObjectDemo : Aws.S3.Config -> '{IO, Exception, Threads} () putObjectDemo config = do printLine (" → Creating PutObjectRequest...") printLine (" Bucket: " ++ testBucket) @@ -170,37 +161,37 @@ putObjectDemo config = do -- PutObjectRequest with minimal required fields (41 fields) -- Field order: aCL body bucket bucketKeyEnabled cacheControl checksumAlgorithm ... -- Set ACL to public-read so we can read the object back - acl = objectCannedACLFromText "public-read" + acl = Aws.S3.objectCannedACLFromText "public-read" request = PutObjectRequest.PutObjectRequest acl body testBucket None None None None None None None None None None None None None (Some "text/plain") None None None None None None None None testObjectKey None None None None None None None None None None None None None None None -- Call the S3 client printLine " → Calling putObject..." - result = !(putObject config request) + result = !(Aws.S3.putObject config request) printLine " ✓ SUCCESS: PutObject completed" -- Print ETag if available - match PutObjectOutput.eTag result with + match Aws.S3.PutObjectOutput.eTag result with Some etag -> printLine (" ETag: " ++ etag) None -> () {{ List objects in the configs prefix. }} -listObjectsV2Demo : Config -> '{IO, Exception, Threads} () +listObjectsV2Demo : Aws.S3.Config -> '{IO, Exception, Threads} () listObjectsV2Demo config = do printLine " → Creating ListObjectsV2Request..." printLine (" Bucket: " ++ testBucket) printLine (" Prefix: configs/") -- ListObjectsV2Request (11 fields) - request = ListObjectsV2Request.ListObjectsV2Request testBucket None None None None None (Some +100) None (Some "configs/") None None + request = Aws.S3.ListObjectsV2Request.ListObjectsV2Request testBucket None None None None None (Some +100) None (Some "configs/") None None -- Call the S3 client printLine " → Calling listObjectsV2..." - result = !(listObjectsV2 config request) + result = !(Aws.S3.listObjectsV2 config request) printLine " ✓ SUCCESS: ListObjectsV2 returned" - match ListObjectsV2Output.contents result with + match Aws.S3.ListObjectsV2Output.contents result with Some objects -> count = List.size objects printLine (" → Found " ++ Nat.toText count ++ " object(s):") @@ -211,21 +202,21 @@ listObjectsV2Demo config = do {{ Get the test object we uploaded. }} -getObjectDemo : Config -> '{IO, Exception, Threads} () +getObjectDemo : Aws.S3.Config -> '{IO, Exception, Threads} () getObjectDemo config = do printLine " → Creating GetObjectRequest..." printLine (" Bucket: " ++ testBucket) printLine (" Key: " ++ testObjectKey) -- GetObjectRequest (21 fields) - request = GetObjectRequest.GetObjectRequest testBucket None None None None None None testObjectKey None None None None None None None None None None None None None + request = Aws.S3.GetObjectRequest.GetObjectRequest testBucket None None None None None None testObjectKey None None None None None None None None None None None None None -- Call the S3 client printLine " → Calling getObject..." - result = !(getObject config request) + result = !(Aws.S3.getObject config request) -- Extract and print body - body = GetObjectOutput.body result + body = Aws.S3.GetObjectOutput.body result bodyText = fromUtf8 body printLine " ✓ SUCCESS: GetObject returned" @@ -240,42 +231,42 @@ getObjectDemo config = do {{ Delete the test object (cleanup). }} -deleteObjectDemo : Config -> '{IO, Exception, Threads} () +deleteObjectDemo : Aws.S3.Config -> '{IO, Exception, Threads} () deleteObjectDemo config = do printLine " → Creating DeleteObjectRequest..." printLine (" Bucket: " ++ testBucket) printLine (" Key: " ++ testObjectKey) -- DeleteObjectRequest (10 fields) - request = DeleteObjectRequest.DeleteObjectRequest testBucket None None None None None testObjectKey None None None + request = Aws.S3.DeleteObjectRequest.DeleteObjectRequest testBucket None None None None None testObjectKey None None None -- Call the S3 client printLine " → Calling deleteObject..." - result = !(deleteObject config request) + result = !(Aws.S3.deleteObject config request) printLine " ✓ SUCCESS: DeleteObject completed" -- Print delete marker if versioned - match DeleteObjectOutput.deleteMarker result with + match Aws.S3.DeleteObjectOutput.deleteMarker result with Some true -> printLine " Delete marker created (versioned bucket)" _ -> () {{ Verify the test object was deleted by listing objects. }} -verifyDeletionDemo : Config -> '{IO, Exception, Threads} () +verifyDeletionDemo : Aws.S3.Config -> '{IO, Exception, Threads} () verifyDeletionDemo config = do printLine " → Listing objects after deletion..." printLine (" Bucket: " ++ testBucket) printLine (" Prefix: configs/") -- ListObjectsV2Request (11 fields) - request = ListObjectsV2Request.ListObjectsV2Request testBucket None None None None None (Some +100) None (Some "configs/") None None + request = Aws.S3.ListObjectsV2Request.ListObjectsV2Request testBucket None None None None None (Some +100) None (Some "configs/") None None -- Call the S3 client - result = !(listObjectsV2 config request) + result = !(Aws.S3.listObjectsV2 config request) printLine " ✓ SUCCESS: ListObjectsV2 returned" - match ListObjectsV2Output.contents result with + match Aws.S3.ListObjectsV2Output.contents result with Some objects -> count = List.size objects printLine (" → Found " ++ Nat.toText count ++ " object(s):") diff --git a/generate-aws-sdk/Makefile b/generate-aws-sdk/Makefile index 800bb00..26e3e95 100644 --- a/generate-aws-sdk/Makefile +++ b/generate-aws-sdk/Makefile @@ -19,7 +19,7 @@ build: init # Build AWS SDK models # ./generate.py - tree -h output/*/src + tree -h output/*/generated .PHONY: clean clean: diff --git a/generate-aws-sdk/generate.py b/generate-aws-sdk/generate.py index 17c5a0e..867a866 100755 --- a/generate-aws-sdk/generate.py +++ b/generate-aws-sdk/generate.py @@ -23,7 +23,7 @@ INPUT_PATH = "api-models-aws-main/models" OUTPUT_PATH = "output" MODEL_DIRNAME = "model" -GENERATED_DIRNAME = "src" +GENERATED_DIRNAME = "generated" BUILD_DIRNAME = "build" @@ -74,8 +74,10 @@ def snake_case(x): result["model"] = source_path result["key"] = key - model_name = key.split("#")[1] + # model_name = key.split("#")[1].split("_")[0] + model_name = key.split("#")[0].split(".")[-1] module_name = snake_case(model_name) + namespace = "aws." + module_name build_json = { "version": "1.0", @@ -96,7 +98,8 @@ def snake_case(x): "plugins": { "unison-codegen": { "service": key, - "module": module_name, + "name": model_name, + "namespace": namespace, "outputDir": GENERATED_DIRNAME, } }, diff --git a/src/main/java/io/smithy/unison/codegen/ClientModuleWriter.java b/src/main/java/io/smithy/unison/codegen/ClientModuleWriter.java index 9280bff..87b2be7 100644 --- a/src/main/java/io/smithy/unison/codegen/ClientModuleWriter.java +++ b/src/main/java/io/smithy/unison/codegen/ClientModuleWriter.java @@ -41,6 +41,7 @@ public final class ClientModuleWriter { private final ServiceShape service; private final Model model; private final String namespace; + private final String clientNamespace; private final FileManifest fileManifest; private final String outputDir; private final UnisonContext context; @@ -54,11 +55,41 @@ public ClientModuleWriter(ServiceShape service, Model model, String namespace, this.service = service; this.model = model; this.namespace = namespace; + this.clientNamespace = context.settings().getClientNamespace(); this.fileManifest = fileManifest; this.outputDir = outputDir; this.context = context; } + /** + * Gets the client namespace for prefixing types and functions. + * + * @return The client namespace (e.g., "Aws.S3") + */ + public String getClientNamespace() { + return clientNamespace; + } + + /** + * Converts a type name to a namespaced type name. + * + * @param name The base type name + * @return The namespaced type name (e.g., "Aws.S3.Config") + */ + private String getNamespacedTypeName(String name) { + return UnisonSymbolProvider.toNamespacedTypeName(name, clientNamespace); + } + + /** + * Converts a function name to a namespaced function name. + * + * @param name The base function name + * @return The namespaced function name (e.g., "Aws.S3.createBucket") + */ + private String getNamespacedFunctionName(String name) { + return UnisonSymbolProvider.toNamespacedFunctionName(name, clientNamespace); + } + /** * Creates a writer using UnisonContext. */ @@ -134,7 +165,7 @@ public void generate() throws IOException { } // Generate pagination helpers - PaginationGenerator paginationGenerator = new PaginationGenerator(); + PaginationGenerator paginationGenerator = new PaginationGenerator(clientNamespace); paginationGenerator.generate(service, model, writer); // Write to file @@ -156,18 +187,21 @@ public void generate() throws IOException { *

Used for AWS services that require authentication and S3-style configuration. */ private void generateAwsConfigTypes(UnisonWriter writer) { + String configType = getNamespacedTypeName("Config"); + String credentialsType = getNamespacedTypeName("Credentials"); + writer.writeDocComment("Configuration for the " + service.getId().getName() + " client"); - writer.write("type Config = {"); + writer.write("type $L = {", configType); writer.indent(); writer.write("endpoint : Text,"); writer.write("region : Text,"); - writer.write("credentials : Credentials,"); + writer.write("credentials : $L,", credentialsType); writer.write("usePathStyle : Boolean"); writer.dedent(); writer.write("}"); writer.writeBlankLine(); - writer.write("type Credentials = {"); + writer.write("type $L = {", credentialsType); writer.indent(); writer.write("accessKeyId : Text,"); writer.write("secretAccessKey : Text,"); @@ -183,8 +217,10 @@ private void generateAwsConfigTypes(UnisonWriter writer) { *

Used for services that don't require AWS authentication. */ private void generateGenericConfigType(UnisonWriter writer) { + String configType = getNamespacedTypeName("Config"); + writer.writeDocComment("Configuration for the " + service.getId().getName() + " client"); - writer.write("type Config = {"); + writer.write("type $L = {", configType); writer.indent(); writer.write("endpoint : Text,"); writer.write("headers : [(Text, Text)]"); @@ -232,7 +268,7 @@ private void generateModelTypes(UnisonWriter writer) { for (Shape enumShape : enums) { if (enumShape instanceof EnumShape) { - EnumGenerator generator = new EnumGenerator((EnumShape) enumShape, model); + EnumGenerator generator = new EnumGenerator((EnumShape) enumShape, model, clientNamespace); generator.generate(writer); writer.writeBlankLine(); } else if (enumShape instanceof StringShape && enumShape.hasTrait(software.amazon.smithy.model.traits.EnumTrait.class)) { @@ -241,7 +277,7 @@ private void generateModelTypes(UnisonWriter writer) { writer.writeBlankLine(); } else if (enumShape instanceof UnionShape) { // Generate union types as sum types - UnionGenerator generator = new UnionGenerator((UnionShape) enumShape, model); + UnionGenerator generator = new UnionGenerator((UnionShape) enumShape, model, clientNamespace); generator.generate(writer); writer.writeBlankLine(); } @@ -255,7 +291,7 @@ private void generateModelTypes(UnisonWriter writer) { for (StructureShape structure : structures) { StructureGenerator generator = new StructureGenerator( - structure, model, context.symbolProvider()); + structure, model, context.symbolProvider(), clientNamespace); generator.generate(writer); writer.writeBlankLine(); } @@ -271,7 +307,7 @@ private void generateModelTypes(UnisonWriter writer) { for (StructureShape error : errors) { StructureGenerator generator = new StructureGenerator( - error, model, context.symbolProvider()); + error, model, context.symbolProvider(), clientNamespace); generator.generate(writer); // Generate toFailure function for errors @@ -333,7 +369,7 @@ private void collectReferencedShapes(ShapeId shapeId, Set struct * Generates a toFailure conversion function for an error type. */ private void generateErrorToFailure(StructureShape error, UnisonWriter writer) { - String typeName = UnisonSymbolProvider.toUnisonTypeName(error.getId().getName()); + String typeName = getNamespacedTypeName(error.getId().getName()); String funcName = typeName + ".toFailure"; writer.writeSignature(funcName, typeName + " -> Failure"); @@ -387,8 +423,9 @@ private void generateXmlParsers(Set structures, UnisonWriter wri * Generates an XML parser function for a single structure. */ private void generateXmlParserForStructure(StructureShape structure, UnisonWriter writer) { - String typeName = UnisonSymbolProvider.toUnisonTypeName(structure.getId().getName()); - String funcName = "parse" + typeName + "FromXml"; + String typeName = getNamespacedTypeName(structure.getId().getName()); + String baseTypeName = UnisonSymbolProvider.toUnisonTypeName(structure.getId().getName()); + String funcName = getNamespacedFunctionName("parse" + baseTypeName + "FromXml"); // Write doc comment writer.writeDocComment("Parse " + typeName + " from XML text."); @@ -401,7 +438,8 @@ private void generateXmlParserForStructure(StructureShape structure, UnisonWrite writer.indent(); // Write constructor call with field extractions - writer.write("$L.$L", typeName, typeName); + // Use base type name for constructor (Unison namespacing quirk) + writer.write("$L", baseTypeName); writer.indent(); for (MemberShape member : structure.getAllMembers().values()) { @@ -428,8 +466,8 @@ private String generateFieldExtraction(MemberShape member, Shape targetShape, St if (targetShape instanceof StructureShape) { // Nested structure - use parser function - String nestedTypeName = UnisonSymbolProvider.toUnisonTypeName(targetShape.getId().getName()); - String parserName = "parse" + nestedTypeName + "FromXml"; + String baseTypeName = UnisonSymbolProvider.toUnisonTypeName(targetShape.getId().getName()); + String parserName = getNamespacedFunctionName("parse" + baseTypeName + "FromXml"); if (isOptional) { return "(Aws.Xml.parseNestedFromXml \"" + xmlElementName + "\" " + parserName + " xml)"; } else { @@ -442,8 +480,8 @@ private String generateFieldExtraction(MemberShape member, Shape targetShape, St if (memberTarget instanceof StructureShape) { // List of structures - String itemTypeName = UnisonSymbolProvider.toUnisonTypeName(memberTarget.getId().getName()); - String parserName = "parse" + itemTypeName + "FromXml"; + String baseItemTypeName = UnisonSymbolProvider.toUnisonTypeName(memberTarget.getId().getName()); + String parserName = getNamespacedFunctionName("parse" + baseItemTypeName + "FromXml"); // Get item element name from list member String itemElementName = Character.toUpperCase(listShape.getMember().getMemberName().charAt(0)) + listShape.getMember().getMemberName().substring(1); @@ -457,7 +495,7 @@ private String generateFieldExtraction(MemberShape member, Shape targetShape, St // List of enums - extract text and convert String itemElementName = Character.toUpperCase(listShape.getMember().getMemberName().charAt(0)) + listShape.getMember().getMemberName().substring(1); - String enumFromText = UnisonSymbolProvider.toUnisonFunctionName(memberTarget.getId().getName()) + "FromText"; + String enumFromText = getNamespacedFunctionName(memberTarget.getId().getName() + "FromText"); if (isOptional) { return "(Some (List.filterMap " + enumFromText + " (Aws.Xml.extractAll \"" + itemElementName + "\" xml)))"; } else { @@ -484,7 +522,7 @@ private String generateFieldExtraction(MemberShape member, Shape targetShape, St } else if (targetShape instanceof EnumShape || targetShape.hasTrait(software.amazon.smithy.model.traits.EnumTrait.class)) { // Enum - extract text and convert (check before isStringShape since EnumShape extends StringShape) - String enumFromText = UnisonSymbolProvider.toUnisonFunctionName(targetShape.getId().getName()) + "FromText"; + String enumFromText = getNamespacedFunctionName(targetShape.getId().getName() + "FromText"); if (isOptional) { return "(Optional.flatMap " + enumFromText + " (Aws.Xml.extractElementOpt \"" + xmlElementName + "\" xml))"; } else { @@ -540,21 +578,23 @@ private String generateFieldExtraction(MemberShape member, Shape targetShape, St * */ private void generateOperationStub(OperationShape operation, UnisonWriter writer) { - String opName = UnisonSymbolProvider.toUnisonFunctionName(operation.getId().getName()); + String opName = getNamespacedFunctionName(operation.getId().getName()); - // Get input/output type names + // Get input/output type names (namespaced) String inputType = operation.getInput() - .map(id -> UnisonSymbolProvider.toUnisonTypeName(id.getName())) + .map(id -> getNamespacedTypeName(id.getName())) .orElse("()"); String outputType = operation.getOutput() - .map(id -> UnisonSymbolProvider.toUnisonTypeName(id.getName())) + .map(id -> getNamespacedTypeName(id.getName())) .orElse("()"); + String configType = getNamespacedTypeName("Config"); + writer.writeDocComment(operation.getId().getName() + " operation (NOT IMPLEMENTED)\n\n" + "Raises exception on error, returns output directly on success."); // Exception-based signature: returns output directly, raises on error - String signature = String.format("Config -> %s -> '{IO, Exception, Http} %s", inputType, outputType); + String signature = String.format("%s -> %s -> '{IO, Exception, Http} %s", configType, inputType, outputType); writer.writeSignature(opName, signature); writer.write("$L config input =", opName); diff --git a/src/main/java/io/smithy/unison/codegen/UnisonSettings.java b/src/main/java/io/smithy/unison/codegen/UnisonSettings.java index e4fdda4..81c8f52 100644 --- a/src/main/java/io/smithy/unison/codegen/UnisonSettings.java +++ b/src/main/java/io/smithy/unison/codegen/UnisonSettings.java @@ -105,6 +105,44 @@ public Optional getProtocol() { return Optional.ofNullable(protocol); } + /** + * Gets the Unison namespace for client types and operations. + * + *

Converts the dot-separated namespace to PascalCase segments: + *

+ * + * @return The client namespace prefix, or empty string if no namespace configured + */ + public String getClientNamespace() { + if (namespace == null || namespace.isEmpty()) { + return ""; + } + + // Split by dot and convert each segment to PascalCase + String[] parts = namespace.split("\\."); + StringBuilder result = new StringBuilder(); + + for (int i = 0; i < parts.length; i++) { + String part = parts[i]; + if (part.isEmpty()) { + continue; + } + // Capitalize first letter of each segment + result.append(Character.toUpperCase(part.charAt(0))); + result.append(part.substring(1)); + + if (i < parts.length - 1) { + result.append("."); + } + } + + return result.toString(); + } + public static Builder builder() { return new Builder(); } diff --git a/src/main/java/io/smithy/unison/codegen/UnisonWriter.java b/src/main/java/io/smithy/unison/codegen/UnisonWriter.java index cc46b58..bca5c44 100644 --- a/src/main/java/io/smithy/unison/codegen/UnisonWriter.java +++ b/src/main/java/io/smithy/unison/codegen/UnisonWriter.java @@ -644,9 +644,21 @@ public String apply(Object value, String indent) { /** * Converts a PascalCase string to lowerCamelCase. + * + *

For namespaced names like "Aws.S3.RequestPayer", only the last + * component is converted to lowerCamelCase, resulting in "Aws.S3.requestPayer". */ private String toLowerCamelCase(String s) { if (s == null || s.isEmpty()) return s; + + // Handle namespaced names (e.g., "Aws.S3.RequestPayer" -> "Aws.S3.requestPayer") + int lastDot = s.lastIndexOf('.'); + if (lastDot >= 0) { + String namespace = s.substring(0, lastDot + 1); + String typeName = s.substring(lastDot + 1); + return namespace + Character.toLowerCase(typeName.charAt(0)) + typeName.substring(1); + } + return Character.toLowerCase(s.charAt(0)) + s.substring(1); } diff --git a/src/main/java/io/smithy/unison/codegen/generators/EnumGenerator.java b/src/main/java/io/smithy/unison/codegen/generators/EnumGenerator.java index 1190826..2102150 100644 --- a/src/main/java/io/smithy/unison/codegen/generators/EnumGenerator.java +++ b/src/main/java/io/smithy/unison/codegen/generators/EnumGenerator.java @@ -65,6 +65,8 @@ public final class EnumGenerator { private static final Logger LOGGER = Logger.getLogger(EnumGenerator.class.getName()); private final String typeName; + private final String namespacedTypeName; + private final String clientNamespace; private final List enumValues; private final String documentation; @@ -91,7 +93,7 @@ public EnumValue(String name, String wireValue) { * Creates an enum generator from a StringShape with @enum trait (Smithy 1.0). * * @param enumShape The string shape with @enum trait - * @param context The code generation context (unused but kept for API compatibility) + * @param context The code generation context * @throws IllegalArgumentException if the shape doesn't have EnumTrait */ public EnumGenerator(StringShape enumShape, UnisonContext context) { @@ -102,6 +104,9 @@ public EnumGenerator(StringShape enumShape, UnisonContext context) { } this.typeName = UnisonSymbolProvider.toUnisonTypeName(enumShape.getId().getName()); + this.clientNamespace = context.settings().getClientNamespace(); + this.namespacedTypeName = UnisonSymbolProvider.toNamespacedTypeName( + enumShape.getId().getName(), clientNamespace); this.enumValues = extractEnumTraitValues(enumShape); this.documentation = enumShape.getTrait(DocumentationTrait.class) .map(DocumentationTrait::getValue) @@ -115,10 +120,24 @@ public EnumGenerator(StringShape enumShape, UnisonContext context) { * @param model The Smithy model */ public EnumGenerator(EnumShape enumShape, Model model) { + this(enumShape, model, ""); + } + + /** + * Creates an enum generator from a Smithy 2.0 EnumShape with namespace. + * + * @param enumShape The Smithy 2.0 enum shape + * @param model The Smithy model + * @param clientNamespace The client namespace for prefixing + */ + public EnumGenerator(EnumShape enumShape, Model model, String clientNamespace) { Objects.requireNonNull(enumShape, "enumShape is required"); Objects.requireNonNull(model, "model is required"); this.typeName = UnisonSymbolProvider.toUnisonTypeName(enumShape.getId().getName()); + this.clientNamespace = clientNamespace != null ? clientNamespace : ""; + this.namespacedTypeName = UnisonSymbolProvider.toNamespacedTypeName( + enumShape.getId().getName(), this.clientNamespace); this.enumValues = extractEnumShapeValues(enumShape); this.documentation = enumShape.getTrait(DocumentationTrait.class) .map(DocumentationTrait::getValue) @@ -133,7 +152,21 @@ public EnumGenerator(EnumShape enumShape, Model model) { * @param documentation Optional documentation */ public EnumGenerator(String typeName, List values, String documentation) { + this(typeName, values, documentation, ""); + } + + /** + * Creates an enum generator with explicit values and namespace. + * + * @param typeName The type name + * @param values The enum values + * @param documentation Optional documentation + * @param clientNamespace The client namespace for prefixing + */ + public EnumGenerator(String typeName, List values, String documentation, String clientNamespace) { this.typeName = Objects.requireNonNull(typeName, "typeName is required"); + this.clientNamespace = clientNamespace != null ? clientNamespace : ""; + this.namespacedTypeName = UnisonSymbolProvider.toNamespacedTypeName(typeName, this.clientNamespace); this.enumValues = Objects.requireNonNull(values, "values is required"); this.documentation = documentation; } @@ -157,13 +190,22 @@ public List getValues() { } /** - * Gets the full variant name for an enum value. + * Gets the full variant name for an enum value (namespaced). * * @param valueName The value name - * @return The full variant name (TypeName'ValueName) + * @return The full variant name (Namespace.TypeName'ValueName) */ public String getVariantName(String valueName) { - return UnisonSymbolProvider.toUnisonEnumVariant(typeName, valueName); + return UnisonSymbolProvider.toUnisonEnumVariant(namespacedTypeName, valueName); + } + + /** + * Gets the namespaced type name. + * + * @return The namespaced type name (e.g., "Aws.S3.BucketLocationConstraint") + */ + public String getNamespacedTypeName() { + return namespacedTypeName; } /** @@ -190,49 +232,47 @@ public void generateTypeDefinition(UnisonWriter writer) { writer.writeDocComment(documentation); } - // Build variants list + // Build variants list using namespaced type name List variants = new ArrayList<>(); for (EnumValue value : enumValues) { String variantName = getVariantName(value.name()); variants.add(new UnisonWriter.Variant(variantName, null)); // No payload for enum variants } - // Write union type - writer.writeUnionType(typeName, variants); + // Write union type with namespaced name + writer.writeUnionType(namespacedTypeName, variants); } /** - * Generates the toText conversion function. + * Generates the toText conversion function with namespaced name. * * @param writer The writer to output code to */ public void generateToTextFunction(UnisonWriter writer) { - String funcName = UnisonSymbolProvider.toUnisonFunctionName(typeName) + "ToText"; - // Build match cases List mappings = new ArrayList<>(); for (EnumValue value : enumValues) { mappings.add(new UnisonWriter.EnumMapping(value.name(), value.wireValue())); } - writer.writeEnumToTextFunction(typeName, mappings); + // Use namespaced type name for function generation + writer.writeEnumToTextFunction(namespacedTypeName, mappings); } /** - * Generates the fromText conversion function. + * Generates the fromText conversion function with namespaced name. * * @param writer The writer to output code to */ public void generateFromTextFunction(UnisonWriter writer) { - String funcName = UnisonSymbolProvider.toUnisonFunctionName(typeName) + "FromText"; - // Build match cases List mappings = new ArrayList<>(); for (EnumValue value : enumValues) { mappings.add(new UnisonWriter.EnumMapping(value.name(), value.wireValue())); } - writer.writeEnumFromTextFunction(typeName, mappings); + // Use namespaced type name for function generation + writer.writeEnumFromTextFunction(namespacedTypeName, mappings); } /** diff --git a/src/main/java/io/smithy/unison/codegen/generators/PaginationGenerator.java b/src/main/java/io/smithy/unison/codegen/generators/PaginationGenerator.java index 23f4517..c601651 100644 --- a/src/main/java/io/smithy/unison/codegen/generators/PaginationGenerator.java +++ b/src/main/java/io/smithy/unison/codegen/generators/PaginationGenerator.java @@ -62,10 +62,22 @@ public class PaginationGenerator { private static final Logger LOGGER = Logger.getLogger(PaginationGenerator.class.getName()); + private final String clientNamespace; + /** - * Creates a new PaginationGenerator. + * Creates a new PaginationGenerator without namespace. */ public PaginationGenerator() { + this(""); + } + + /** + * Creates a new PaginationGenerator with namespace. + * + * @param clientNamespace The client namespace for prefixing types + */ + public PaginationGenerator(String clientNamespace) { + this.clientNamespace = clientNamespace != null ? clientNamespace : ""; } /** @@ -127,21 +139,25 @@ public void generatePaginationHelper(OperationShape operation, Model model, Unis } PaginatedTrait pagination = paginatedTrait.get(); - String opName = UnisonSymbolProvider.toUnisonFunctionName(operation.getId().getName()); + String opName = UnisonSymbolProvider.toNamespacedFunctionName( + operation.getId().getName(), clientNamespace); // Get pagination configuration String inputToken = pagination.getInputToken().orElse("continuationToken"); String outputToken = pagination.getOutputToken().orElse("nextContinuationToken"); String items = pagination.getItems().orElse("contents"); - // Get input/output types + // Get input/output types with namespace String inputType = operation.getInput() - .map(id -> UnisonSymbolProvider.toUnisonTypeName(id.getName())) + .map(id -> UnisonSymbolProvider.toNamespacedTypeName(id.getName(), clientNamespace)) .orElse("()"); String outputType = operation.getOutput() - .map(id -> UnisonSymbolProvider.toUnisonTypeName(id.getName())) + .map(id -> UnisonSymbolProvider.toNamespacedTypeName(id.getName(), clientNamespace)) .orElse("()"); + // Config type with namespace + String configType = UnisonSymbolProvider.toNamespacedTypeName("Config", clientNamespace); + // Get the item type from the output structure String itemsField = UnisonSymbolProvider.toUnisonFunctionName(items); String itemType = "a"; // default to polymorphic @@ -163,7 +179,8 @@ public void generatePaginationHelper(OperationShape operation, Model model, Unis if (itemsShape instanceof ListShape) { ListShape listShape = (ListShape) itemsShape; Shape memberShape = model.expectShape(listShape.getMember().getTarget()); - itemType = UnisonSymbolProvider.toUnisonTypeName(memberShape.getId().getName()); + itemType = UnisonSymbolProvider.toNamespacedTypeName( + memberShape.getId().getName(), clientNamespace); } } } @@ -173,10 +190,10 @@ public void generatePaginationHelper(OperationShape operation, Model model, Unis "Automatically fetches all pages and collects all items from the '" + items + "' field.\n" + "Uses '" + inputToken + "' as input token and '" + outputToken + "' as output token."); - // Function signature with concrete item type + // Function signature with concrete item type and namespaced types // Note: HTTP operations use {IO, Exception, Threads} abilities for real HTTP via @unison/http String helperName = opName + "All"; - writer.writeSignature(helperName, "Config -> " + inputType + " -> '{IO, Exception, Threads} [" + itemType + "]"); + writer.writeSignature(helperName, configType + " -> " + inputType + " -> '{IO, Exception, Threads} [" + itemType + "]"); writer.write("$L config input =", helperName); writer.indent(); diff --git a/src/main/java/io/smithy/unison/codegen/generators/StructureGenerator.java b/src/main/java/io/smithy/unison/codegen/generators/StructureGenerator.java index b335c2f..9bb58fd 100644 --- a/src/main/java/io/smithy/unison/codegen/generators/StructureGenerator.java +++ b/src/main/java/io/smithy/unison/codegen/generators/StructureGenerator.java @@ -49,6 +49,7 @@ public final class StructureGenerator { private final Model model; private final StructureShape structure; private final SymbolProvider symbolProvider; + private final String clientNamespace; /** * Creates a new structure generator. @@ -61,6 +62,7 @@ public StructureGenerator(StructureShape structure, UnisonContext context) { Objects.requireNonNull(context, "context is required"); this.model = context.model(); this.symbolProvider = context.symbolProvider(); + this.clientNamespace = context.settings().getClientNamespace(); } /** @@ -71,9 +73,22 @@ public StructureGenerator(StructureShape structure, UnisonContext context) { * @param symbolProvider The symbol provider */ public StructureGenerator(StructureShape structure, Model model, SymbolProvider symbolProvider) { + this(structure, model, symbolProvider, ""); + } + + /** + * Creates a new structure generator with explicit model, symbol provider, and namespace. + * + * @param structure The structure shape to generate + * @param model The Smithy model + * @param symbolProvider The symbol provider + * @param clientNamespace The client namespace for prefixing types + */ + public StructureGenerator(StructureShape structure, Model model, SymbolProvider symbolProvider, String clientNamespace) { this.structure = Objects.requireNonNull(structure, "structure is required"); this.model = Objects.requireNonNull(model, "model is required"); this.symbolProvider = Objects.requireNonNull(symbolProvider, "symbolProvider is required"); + this.clientNamespace = clientNamespace != null ? clientNamespace : ""; } /** @@ -86,12 +101,12 @@ public StructureShape getStructure() { } /** - * Gets the Unison type name for this structure. + * Gets the Unison type name for this structure (namespaced). * - * @return The type name (PascalCase) + * @return The namespaced type name (e.g., "Aws.S3.CreateBucketRequest") */ public String getTypeName() { - return UnisonSymbolProvider.toUnisonTypeName(structure.getId().getName()); + return UnisonSymbolProvider.toNamespacedTypeName(structure.getId().getName(), clientNamespace); } /** @@ -210,12 +225,12 @@ private String wrapIfComplex(String type) { } /** - * Gets the Unison type for a shape. + * Gets the Unison type for a shape (with namespace prefix for complex types). */ private String getUnisonType(Shape shape) { if (shape instanceof StringShape) { if (shape.hasTrait(EnumTrait.class)) { - return UnisonSymbolProvider.toUnisonTypeName(shape.getId().getName()); + return UnisonSymbolProvider.toNamespacedTypeName(shape.getId().getName(), clientNamespace); } return "Text"; } else if (shape instanceof IntegerShape || shape instanceof LongShape || @@ -249,13 +264,13 @@ private String getUnisonType(Shape shape) { String valueType = getUnisonType(valueShape); return "Map " + keyType + " " + valueType; } else if (shape instanceof StructureShape) { - return UnisonSymbolProvider.toUnisonTypeName(shape.getId().getName()); + return UnisonSymbolProvider.toNamespacedTypeName(shape.getId().getName(), clientNamespace); } else if (shape instanceof UnionShape) { - return UnisonSymbolProvider.toUnisonTypeName(shape.getId().getName()); + return UnisonSymbolProvider.toNamespacedTypeName(shape.getId().getName(), clientNamespace); } else if (shape instanceof EnumShape) { - return UnisonSymbolProvider.toUnisonTypeName(shape.getId().getName()); + return UnisonSymbolProvider.toNamespacedTypeName(shape.getId().getName(), clientNamespace); } else if (shape instanceof IntEnumShape) { - return UnisonSymbolProvider.toUnisonTypeName(shape.getId().getName()); + return UnisonSymbolProvider.toNamespacedTypeName(shape.getId().getName(), clientNamespace); } return "a"; // Generic type parameter as fallback } diff --git a/src/main/java/io/smithy/unison/codegen/generators/UnionGenerator.java b/src/main/java/io/smithy/unison/codegen/generators/UnionGenerator.java index ca62ae6..59ec00e 100644 --- a/src/main/java/io/smithy/unison/codegen/generators/UnionGenerator.java +++ b/src/main/java/io/smithy/unison/codegen/generators/UnionGenerator.java @@ -55,6 +55,8 @@ public final class UnionGenerator { private static final Logger LOGGER = Logger.getLogger(UnionGenerator.class.getName()); private final String typeName; + private final String namespacedTypeName; + private final String clientNamespace; private final String documentation; private final List variants; @@ -84,10 +86,24 @@ public UnionVariant(String name, String payloadType) { * @param model The Smithy model */ public UnionGenerator(UnionShape union, Model model) { + this(union, model, ""); + } + + /** + * Creates a union generator from a Smithy UnionShape with namespace. + * + * @param union The union shape to generate + * @param model The Smithy model + * @param clientNamespace The client namespace for prefixing + */ + public UnionGenerator(UnionShape union, Model model, String clientNamespace) { Objects.requireNonNull(union, "union is required"); Objects.requireNonNull(model, "model is required"); this.typeName = UnisonSymbolProvider.toUnisonTypeName(union.getId().getName()); + this.clientNamespace = clientNamespace != null ? clientNamespace : ""; + this.namespacedTypeName = UnisonSymbolProvider.toNamespacedTypeName( + union.getId().getName(), this.clientNamespace); this.documentation = union.getTrait(DocumentationTrait.class) .map(DocumentationTrait::getValue) .orElse(null); @@ -102,18 +118,32 @@ public UnionGenerator(UnionShape union, Model model) { * @param documentation Optional documentation */ public UnionGenerator(String typeName, List variants, String documentation) { + this(typeName, variants, documentation, ""); + } + + /** + * Creates a union generator with explicit values and namespace. + * + * @param typeName The type name + * @param variants The union variants + * @param documentation Optional documentation + * @param clientNamespace The client namespace for prefixing + */ + public UnionGenerator(String typeName, List variants, String documentation, String clientNamespace) { this.typeName = Objects.requireNonNull(typeName, "typeName is required"); + this.clientNamespace = clientNamespace != null ? clientNamespace : ""; + this.namespacedTypeName = UnisonSymbolProvider.toNamespacedTypeName(typeName, this.clientNamespace); this.variants = Objects.requireNonNull(variants, "variants is required"); this.documentation = documentation; } /** - * Gets the Unison type name for this union. + * Gets the Unison type name for this union (namespaced). * - * @return The PascalCase type name + * @return The namespaced type name (e.g., "Aws.S3.StorageType") */ public String getTypeName() { - return typeName; + return namespacedTypeName; } /** @@ -126,13 +156,13 @@ public List getVariants() { } /** - * Gets the full variant name for a member. + * Gets the full variant name for a member (using namespaced type). * * @param memberName The member name - * @return The full variant name (TypeName'MemberName) + * @return The full variant name (Namespace.TypeName'MemberName) */ public String getVariantName(String memberName) { - return UnisonSymbolProvider.toUnisonEnumVariant(typeName, memberName); + return UnisonSymbolProvider.toUnisonEnumVariant(namespacedTypeName, memberName); } /** @@ -147,7 +177,7 @@ public void generate(UnisonWriter writer) { } /** - * Generates the union type definition. + * Generates the union type definition with namespaced type name. * * @param writer The writer to output code to */ @@ -164,8 +194,8 @@ public void generateTypeDefinition(UnisonWriter writer) { writerVariants.add(new UnisonWriter.Variant(variantName, variant.payloadType())); } - // Write union type - writer.writeUnionType(typeName, writerVariants); + // Write union type with namespaced name + writer.writeUnionType(namespacedTypeName, writerVariants); } /** @@ -206,11 +236,11 @@ private String toPascalCase(String name) { } /** - * Gets the Unison type for a Smithy shape. + * Gets the Unison type for a Smithy shape (with namespace prefix for complex types). */ private String getUnisonType(Shape shape) { if (shape.isStructureShape()) { - return UnisonSymbolProvider.toUnisonTypeName(shape.getId().getName()); + return UnisonSymbolProvider.toNamespacedTypeName(shape.getId().getName(), clientNamespace); } else if (shape.isStringShape()) { return "Text"; } else if (shape.isIntegerShape() || shape.isLongShape() || @@ -229,9 +259,9 @@ private String getUnisonType(Shape shape) { } else if (shape.isMapShape()) { return "Map Text a"; // Generic map for now } else if (shape.isUnionShape()) { - return UnisonSymbolProvider.toUnisonTypeName(shape.getId().getName()); + return UnisonSymbolProvider.toNamespacedTypeName(shape.getId().getName(), clientNamespace); } - // Default to the shape's type name - return UnisonSymbolProvider.toUnisonTypeName(shape.getId().getName()); + // Default to the shape's namespaced type name + return UnisonSymbolProvider.toNamespacedTypeName(shape.getId().getName(), clientNamespace); } } diff --git a/src/main/java/io/smithy/unison/codegen/protocols/AwsJsonProtocolGenerator.java b/src/main/java/io/smithy/unison/codegen/protocols/AwsJsonProtocolGenerator.java index 9d06c40..a85a8f3 100644 --- a/src/main/java/io/smithy/unison/codegen/protocols/AwsJsonProtocolGenerator.java +++ b/src/main/java/io/smithy/unison/codegen/protocols/AwsJsonProtocolGenerator.java @@ -49,7 +49,9 @@ public String getContentType(ServiceShape service) { @Override public void generateOperation(OperationShape operation, UnisonWriter writer, UnisonContext context) { // TODO: Implement AWS JSON operation generation - String opName = UnisonSymbolProvider.toUnisonFunctionName(operation.getId().getName()); + String clientNamespace = context.settings().getClientNamespace(); + String opName = UnisonSymbolProvider.toNamespacedFunctionName( + operation.getId().getName(), clientNamespace); writer.writeComment("AWS JSON " + version + " operation: " + opName + " (NOT IMPLEMENTED)"); } diff --git a/src/main/java/io/smithy/unison/codegen/protocols/RestJsonProtocolGenerator.java b/src/main/java/io/smithy/unison/codegen/protocols/RestJsonProtocolGenerator.java index 995b755..b520457 100644 --- a/src/main/java/io/smithy/unison/codegen/protocols/RestJsonProtocolGenerator.java +++ b/src/main/java/io/smithy/unison/codegen/protocols/RestJsonProtocolGenerator.java @@ -46,7 +46,9 @@ public String getContentType(ServiceShape service) { @Override public void generateOperation(OperationShape operation, UnisonWriter writer, UnisonContext context) { // TODO: Implement REST-JSON operation generation - String opName = UnisonSymbolProvider.toUnisonFunctionName(operation.getId().getName()); + String clientNamespace = context.settings().getClientNamespace(); + String opName = UnisonSymbolProvider.toNamespacedFunctionName( + operation.getId().getName(), clientNamespace); writer.writeComment("REST-JSON operation: " + opName + " (NOT IMPLEMENTED)"); } diff --git a/src/main/java/io/smithy/unison/codegen/protocols/RestXmlProtocolGenerator.java b/src/main/java/io/smithy/unison/codegen/protocols/RestXmlProtocolGenerator.java index a7e8be8..42bd136 100644 --- a/src/main/java/io/smithy/unison/codegen/protocols/RestXmlProtocolGenerator.java +++ b/src/main/java/io/smithy/unison/codegen/protocols/RestXmlProtocolGenerator.java @@ -111,16 +111,19 @@ public String getContentType(ServiceShape service) { public void generateOperation(OperationShape operation, UnisonWriter writer, UnisonContext context) { Model model = context.model(); ServiceShape service = context.serviceShape(); + String clientNamespace = context.settings().getClientNamespace(); - String opName = UnisonSymbolProvider.toUnisonFunctionName(operation.getId().getName()); + String opName = UnisonSymbolProvider.toNamespacedFunctionName( + operation.getId().getName(), clientNamespace); - // Determine input and output types + // Determine input and output types (namespaced) String inputType = operation.getInput() - .map(id -> UnisonSymbolProvider.toUnisonTypeName(id.getName())) + .map(id -> UnisonSymbolProvider.toNamespacedTypeName(id.getName(), clientNamespace)) .orElse("()"); String outputType = operation.getOutput() - .map(id -> UnisonSymbolProvider.toUnisonTypeName(id.getName())) + .map(id -> UnisonSymbolProvider.toNamespacedTypeName(id.getName(), clientNamespace)) .orElse("()"); + String configType = UnisonSymbolProvider.toNamespacedTypeName("Config", clientNamespace); // Get HTTP method and URI from @http trait String method = ProtocolUtils.getHttpMethod(operation, "GET"); @@ -153,7 +156,7 @@ public void generateOperation(OperationShape operation, UnisonWriter writer, Uni // Write signature // Note: HTTP operations use {IO, Exception} abilities - there is no separate Http ability in Unison // Use '{IO, Exception, Threads} to support real HTTP via @unison/http bridge - String signature = String.format("Config -> %s -> '{IO, Exception, Threads} %s", inputType, outputType); + String signature = String.format("%s -> %s -> '{IO, Exception, Threads} %s", configType, inputType, outputType); writer.writeSignature(opName, signature); // Write function definition with do block for delayed computation @@ -169,13 +172,13 @@ public void generateOperation(OperationShape operation, UnisonWriter writer, Uni generateUrlBuilding(uri, httpLabelMembers, useS3Url, inputType, writer); // Build query string - generateQueryString(httpQueryMembers, inputType, model, writer); + generateQueryString(httpQueryMembers, inputType, model, clientNamespace, writer); // Build full URL writer.write("fullUrl = url ++ queryString"); // Build headers - generateRequestHeaders(httpHeaderInputMembers, inputType, model, writer); + generateRequestHeaders(httpHeaderInputMembers, inputType, model, clientNamespace, writer); // Build request body generateRequestBodyBinding(operation, model, bodyMembers, payloadMember, inputType, writer); @@ -197,7 +200,7 @@ public void generateOperation(OperationShape operation, UnisonWriter writer, Uni // Handle response - still in scope since we're in the do block writer.write("-- Check for errors and parse response"); writer.write("_ = handleHttpResponse response"); - generateResponseParsing(operation, model, writer); + generateResponseParsing(operation, model, clientNamespace, writer); writer.dedent(); // end function writer.writeBlankLine(); @@ -273,7 +276,7 @@ private void generateUrlBuilding(String uri, List httpLabelMembers, * Handles both required and optional fields, using type-specific toText functions. */ private void generateQueryString(List httpQueryMembers, String inputType, - Model model, UnisonWriter writer) { + Model model, String clientNamespace, UnisonWriter writer) { if (httpQueryMembers.isEmpty()) { writer.write("queryString = \"\""); return; @@ -298,7 +301,7 @@ private void generateQueryString(List httpQueryMembers, String inpu // Get the target shape to determine the correct toText function Shape targetShape = model.expectShape(member.getTarget()); - String toTextFunc = getToTextFunction(targetShape); + String toTextFunc = getToTextFunction(targetShape, clientNamespace); // Check if the member is required (not optional) boolean isRequired = member.isRequired(); @@ -326,19 +329,22 @@ private void generateQueryString(List httpQueryMembers, String inpu * *

Note: Timestamps are generated as Text in Unison (for HTTP serialization), * so they don't need conversion. + * + * @param shape The shape to get toText function for + * @param clientNamespace The client namespace for namespacing enum functions */ - private String getToTextFunction(Shape shape) { + private String getToTextFunction(Shape shape, String clientNamespace) { // Check for Smithy 2.0 enums first if (shape instanceof EnumShape) { - // Enums use a function named like: requestPayerToText (camelCase + ToText) - String enumFuncName = UnisonSymbolProvider.toUnisonFunctionName(shape.getId().getName()); - return enumFuncName + "ToText"; + // Enums use a namespaced function like: Aws.S3.requestPayerToText + return UnisonSymbolProvider.toNamespacedFunctionName( + shape.getId().getName() + "ToText", clientNamespace); } // Check for Smithy 1.0 style enums (strings with @enum trait) if (shape.isStringShape() && shape.hasTrait(software.amazon.smithy.model.traits.EnumTrait.class)) { - // Enums use a function named like: requestPayerToText (camelCase + ToText) - String enumFuncName = UnisonSymbolProvider.toUnisonFunctionName(shape.getId().getName()); - return enumFuncName + "ToText"; + // Enums use a namespaced function like: Aws.S3.requestPayerToText + return UnisonSymbolProvider.toNamespacedFunctionName( + shape.getId().getName() + "ToText", clientNamespace); } if (shape.isStringShape()) { return ""; // No conversion needed for Text @@ -365,7 +371,7 @@ private String getToTextFunction(Shape shape) { *

Converts all header values to Optional Text for homogeneous list types. */ private void generateRequestHeaders(List httpHeaderMembers, String inputType, - Model model, UnisonWriter writer) { + Model model, String clientNamespace, UnisonWriter writer) { writer.write("-- Build headers from @httpHeader members"); if (httpHeaderMembers.isEmpty()) { @@ -400,7 +406,7 @@ private void generateRequestHeaders(List httpHeaderMembers, String if (targetShape.isListShape()) { ListShape listShape = targetShape.asListShape().get(); Shape elementShape = model.expectShape(listShape.getMember().getTarget()); - String elementToText = getToTextFunction(elementShape); + String elementToText = getToTextFunction(elementShape, clientNamespace); // Lists are serialized as comma-separated values // Text.join "," (List.map ElementType.toText list) @@ -425,7 +431,7 @@ private void generateRequestHeaders(List httpHeaderMembers, String } } else { // Non-list types - original logic - String toTextFunc = getToTextFunction(targetShape); + String toTextFunc = getToTextFunction(targetShape, clientNamespace); if (isRequired) { // Required field - wrap in Some and convert to Text @@ -505,7 +511,8 @@ private void generateRequestBodyBinding(OperationShape operation, Model model, *

  • Body members - Decode from XML response body
  • * */ - private void generateResponseParsing(OperationShape operation, Model model, UnisonWriter writer) { + private void generateResponseParsing(OperationShape operation, Model model, + String clientNamespace, UnisonWriter writer) { Optional outputShape = ProtocolUtils.getOutputShape(operation, model); if (!outputShape.isPresent()) { @@ -529,7 +536,7 @@ private void generateResponseParsing(OperationShape operation, Model model, Unis // In do blocks, bindings are scoped to the rest of the block (no need for 'let') // Extract response headers - generateResponseHeaderExtraction(headerMembers, model, writer); + generateResponseHeaderExtraction(headerMembers, model, clientNamespace, writer); // Extract response code if needed if (responseCodeMember.isPresent()) { @@ -550,46 +557,48 @@ private void generateResponseParsing(OperationShape operation, Model model, Unis String varName = memberName + "Val"; String xmlElementName = getXmlElementName(member); Shape targetShape = model.expectShape(member.getTarget()); - generateXmlFieldExtraction(varName, xmlElementName, targetShape, member, model, writer); + generateXmlFieldExtraction(varName, xmlElementName, targetShape, member, model, clientNamespace, writer); } } // Build the result record (final expression of the do block) generateResultRecordConstruction(output, payloadMember, headerMembers, - responseCodeMember, bodyMembers, writer); + responseCodeMember, bodyMembers, clientNamespace, writer); } else if (payloadMember.isPresent()) { // Simple payload extraction - use positional arguments MemberShape payload = payloadMember.get(); Shape targetShape = model.expectShape(payload.getTarget()); - String outputTypeName = UnisonSymbolProvider.toUnisonTypeName(output.getId().getName()); + // Use base type name for constructor (Unison namespacing quirk) + String outputTypeName = UnisonSymbolProvider.toUnisonTypeName( + output.getId().getName()); boolean isOptional = !payload.isRequired(); if (targetShape.isBlobShape()) { if (isOptional) { - writer.write("$L.$L (Some (Response.body response))", outputTypeName, outputTypeName); + writer.write("$L (Some (Response.body response))", outputTypeName); } else { - writer.write("$L.$L (Response.body response)", outputTypeName, outputTypeName); + writer.write("$L (Response.body response)", outputTypeName); } } else if (targetShape.isStringShape()) { if (isOptional) { - writer.write("$L.$L (Some (Aws.Http.bytesToText (Response.body response)))", outputTypeName, outputTypeName); + writer.write("$L (Some (Aws.Http.bytesToText (Response.body response)))", outputTypeName); } else { - writer.write("$L.$L (Aws.Http.bytesToText (Response.body response))", outputTypeName, outputTypeName); + writer.write("$L (Aws.Http.bytesToText (Response.body response))", outputTypeName); } } else { // Structure payload - generate XML parsing - generateXmlResponseParsing(output, bodyMembers, model, writer); + generateXmlResponseParsing(output, bodyMembers, model, clientNamespace, writer); } } else if (bodyMembers.isEmpty()) { // No body content expected - return empty record using constructor - // For empty records like "type X = X", we construct with just "X" + // Use base type name for constructor (Unison namespacing quirk) String outputTypeName = UnisonSymbolProvider.toUnisonTypeName( operation.getOutput().get().getName()); writer.write("-- No body content expected"); - writer.write("$L.$L", outputTypeName, outputTypeName); + writer.write("$L", outputTypeName); } else { // Has body members - generate XML parsing - generateXmlResponseParsing(output, bodyMembers, model, writer); + generateXmlResponseParsing(output, bodyMembers, model, clientNamespace, writer); } } @@ -604,7 +613,8 @@ private void generateResponseParsing(OperationShape operation, Model model, Unis *
  • Enum types - convert Text to enum using fromText function
  • * */ - private void generateResponseHeaderExtraction(List headerMembers, Model model, UnisonWriter writer) { + private void generateResponseHeaderExtraction(List headerMembers, Model model, + String clientNamespace, UnisonWriter writer) { if (headerMembers.isEmpty()) { return; } @@ -623,7 +633,8 @@ private void generateResponseHeaderExtraction(List headerMembers, M if (isEnumType) { // Enum type - need to convert Optional Text to Optional EnumType // Using pattern matching per UNISON_LANGUAGE_SPEC.md - String enumFromText = UnisonSymbolProvider.toUnisonFunctionName(targetShape.getId().getName()) + "FromText"; + String enumFromText = UnisonSymbolProvider.toNamespacedFunctionName( + targetShape.getId().getName() + "FromText", clientNamespace); writer.write("$L = match Response.getHeader \"$L\" response with", memberName, headerName); writer.indent(); @@ -689,8 +700,11 @@ private void generateResultRecordConstruction(StructureShape output, List headerMembers, Optional responseCodeMember, List bodyMembers, + String clientNamespace, UnisonWriter writer) { - String outputTypeName = UnisonSymbolProvider.toUnisonTypeName(output.getId().getName()); + // Use base type name for constructor (Unison namespacing quirk) + String outputTypeName = UnisonSymbolProvider.toUnisonTypeName( + output.getId().getName()); // Build a map of member name to value expression // Use 'Val' suffix on local variables to avoid name clash with accessor functions @@ -736,11 +750,12 @@ private void generateResultRecordConstruction(StructureShape output, } // Write the constructor call with positional arguments + // Record constructor is just the type name (e.g., Aws.S3.GetObjectOutput) if (args.isEmpty()) { - writer.write("$L.$L", outputTypeName, outputTypeName); + writer.write("$L", outputTypeName); } else { StringBuilder sb = new StringBuilder(); - sb.append(outputTypeName).append(".").append(outputTypeName); + sb.append(outputTypeName); for (String arg : args) { sb.append(" ").append(arg); } @@ -790,6 +805,7 @@ public void generateRequestSerializer(OperationShape operation, UnisonWriter wri @Override public void generateResponseDeserializer(OperationShape operation, UnisonWriter writer, UnisonContext context) { Model model = context.model(); + String clientNamespace = context.settings().getClientNamespace(); Optional outputShape = ProtocolUtils.getOutputShape(operation, model); if (!outputShape.isPresent()) { @@ -813,7 +829,7 @@ public void generateResponseDeserializer(OperationShape operation, UnisonWriter } else { // Structure payload - generate inline XML parsing writer.writeComment("Structure payload - parse XML"); - generateXmlResponseParsing(outputShape.get(), ProtocolUtils.getBodyMembers(outputShape.get()), model, writer); + generateXmlResponseParsing(outputShape.get(), ProtocolUtils.getBodyMembers(outputShape.get()), model, clientNamespace, writer); } } else { List bodyMembers = ProtocolUtils.getBodyMembers(outputShape.get()); @@ -823,7 +839,7 @@ public void generateResponseDeserializer(OperationShape operation, UnisonWriter writer.write("{}"); } else { writer.writeComment("Decode XML response body"); - generateXmlResponseParsing(outputShape.get(), bodyMembers, model, writer); + generateXmlResponseParsing(outputShape.get(), bodyMembers, model, clientNamespace, writer); } } } @@ -831,12 +847,14 @@ public void generateResponseDeserializer(OperationShape operation, UnisonWriter @Override public void generateErrorParser(OperationShape operation, UnisonWriter writer, UnisonContext context) { ServiceShape service = context.serviceShape(); + String clientNamespace = context.settings().getClientNamespace(); String serviceName = service.getId().getName(); // Remove "Service" suffix if present to avoid "S3ServiceServiceError" if (serviceName.endsWith("Service")) { serviceName = serviceName.substring(0, serviceName.length() - 7); } - String errorTypeName = UnisonSymbolProvider.toUnisonTypeName(serviceName) + "ServiceError"; + String errorTypeName = UnisonSymbolProvider.toNamespacedTypeName( + serviceName + "ServiceError", clientNamespace); writer.writeDocComment("Parse REST-XML error response"); writer.write("parseError : Response -> $L", errorTypeName); @@ -864,8 +882,10 @@ public void generateErrorParser(OperationShape operation, UnisonWriter writer, U * */ private void generateXmlResponseParsing(StructureShape output, List bodyMembers, - Model model, UnisonWriter writer) { - String outputTypeName = UnisonSymbolProvider.toUnisonTypeName(output.getId().getName()); + Model model, String clientNamespace, UnisonWriter writer) { + // Use base type name for constructor (Unison namespacing quirk) + String outputTypeName = UnisonSymbolProvider.toUnisonTypeName( + output.getId().getName()); // Convert response body to text writer.write("xmlText = fromUtf8 (Response.body response)"); @@ -882,11 +902,12 @@ private void generateXmlResponseParsing(StructureShape output, List // Get the XML element name - use member name by default String xmlElementName = getXmlElementName(member); - generateXmlFieldExtraction(varName, xmlElementName, targetShape, member, model, writer); + generateXmlFieldExtraction(varName, xmlElementName, targetShape, member, model, clientNamespace, writer); } // Construct the output record with extracted values - writer.write("$L.$L", outputTypeName, outputTypeName); + // Record constructor is just the type name (e.g., Aws.S3.ListObjectsV2Output) + writer.write("$L", outputTypeName); writer.indent(); for (int i = 0; i < allMembers.size(); i++) { MemberShape member = allMembers.get(i); @@ -901,14 +922,15 @@ private void generateXmlResponseParsing(StructureShape output, List * Generates code to extract a single field from XML. */ private void generateXmlFieldExtraction(String varName, String xmlElementName, - Shape targetShape, MemberShape member, Model model, UnisonWriter writer) { + Shape targetShape, MemberShape member, Model model, String clientNamespace, UnisonWriter writer) { boolean isOptional = !member.isRequired(); // Check enum types FIRST (before string check, since enums may inherit from string) if (targetShape instanceof EnumShape || (targetShape.isStringShape() && targetShape.hasTrait(software.amazon.smithy.model.traits.EnumTrait.class))) { // Enum type - extract text and convert using enumFromText - String enumFromText = UnisonSymbolProvider.toUnisonFunctionName(targetShape.getId().getName()) + "FromText"; + String enumFromText = UnisonSymbolProvider.toNamespacedFunctionName( + targetShape.getId().getName() + "FromText", clientNamespace); String textVarName = varName + "Text"; writer.write("$L = Aws.Xml.extractElementOpt \"$L\" xmlText", textVarName, xmlElementName); writer.write("$L = Optional.flatMap $L $L", varName, enumFromText, textVarName); @@ -954,8 +976,9 @@ private void generateXmlFieldExtraction(String varName, String xmlElementName, } else if (memberTarget instanceof StructureShape) { // List of structures - use parseListFromXml with inline parser StructureShape structShape = (StructureShape) memberTarget; - String structTypeName = UnisonSymbolProvider.toUnisonTypeName(structShape.getId().getName()); - String parserName = "parse" + structTypeName + "FromXml"; + String baseTypeName = UnisonSymbolProvider.toUnisonTypeName(structShape.getId().getName()); + String parserName = UnisonSymbolProvider.toNamespacedFunctionName( + "parse" + baseTypeName + "FromXml", clientNamespace); // Generate inline parsing that uses the structure parser if (isOptional) { @@ -968,8 +991,8 @@ private void generateXmlFieldExtraction(String varName, String xmlElementName, } else if (memberTarget instanceof EnumShape || (memberTarget.isStringShape() && memberTarget.hasTrait(software.amazon.smithy.model.traits.EnumTrait.class))) { // List of enums - extract text and map using fromText function - String enumTypeName = UnisonSymbolProvider.toUnisonTypeName(memberTarget.getId().getName()); - String enumFromText = UnisonSymbolProvider.toUnisonFunctionName(memberTarget.getId().getName()) + "FromText"; + String enumFromText = UnisonSymbolProvider.toNamespacedFunctionName( + memberTarget.getId().getName() + "FromText", clientNamespace); writer.write("$L = Aws.Xml.extractAll \"$L\" xmlText", itemsVarName, itemElementName); writer.write("$L = List.filterMap $L $L", varName + "Parsed", enumFromText, itemsVarName); if (isOptional) { @@ -1001,8 +1024,9 @@ private void generateXmlFieldExtraction(String varName, String xmlElementName, } else if (targetShape instanceof StructureShape) { // Nested structure - use parseNestedFromXml with structure parser StructureShape structShape = (StructureShape) targetShape; - String structTypeName = UnisonSymbolProvider.toUnisonTypeName(structShape.getId().getName()); - String parserName = "parse" + structTypeName + "FromXml"; + String baseTypeName = UnisonSymbolProvider.toUnisonTypeName(structShape.getId().getName()); + String parserName = UnisonSymbolProvider.toNamespacedFunctionName( + "parse" + baseTypeName + "FromXml", clientNamespace); if (isOptional) { writer.write("$L = Aws.Xml.parseNestedFromXml \"$L\" $L xmlText", varName, xmlElementName, parserName); diff --git a/src/main/java/io/smithy/unison/codegen/symbol/UnisonSymbolProvider.java b/src/main/java/io/smithy/unison/codegen/symbol/UnisonSymbolProvider.java index d98a480..70f1f60 100644 --- a/src/main/java/io/smithy/unison/codegen/symbol/UnisonSymbolProvider.java +++ b/src/main/java/io/smithy/unison/codegen/symbol/UnisonSymbolProvider.java @@ -148,6 +148,48 @@ public static String toUnisonEnumVariant(String typeName, String variantName) { return typeName + "'" + variantName; } + /** + * Converts a name to a namespaced Unison type name. + * + *

    Prepends the namespace prefix to the type name: + *

      + *
    • ("CreateBucketRequest", "Aws.S3") → "Aws.S3.CreateBucketRequest"
    • + *
    • ("Config", "Aws.DynamoDB") → "Aws.DynamoDB.Config"
    • + *
    + * + * @param name The base type name + * @param namespace The namespace prefix (e.g., "Aws.S3") + * @return The fully qualified type name + */ + public static String toNamespacedTypeName(String name, String namespace) { + String typeName = toUnisonTypeName(name); + if (namespace == null || namespace.isEmpty()) { + return typeName; + } + return namespace + "." + typeName; + } + + /** + * Converts a name to a namespaced Unison function name. + * + *

    Prepends the namespace prefix to the function name: + *

      + *
    • ("CreateBucket", "Aws.S3") → "Aws.S3.createBucket"
    • + *
    • ("GetItem", "Aws.DynamoDB") → "Aws.DynamoDB.getItem"
    • + *
    + * + * @param name The base function name (can be PascalCase or camelCase) + * @param namespace The namespace prefix (e.g., "Aws.S3") + * @return The fully qualified function name + */ + public static String toNamespacedFunctionName(String name, String namespace) { + String funcName = toUnisonFunctionName(name); + if (namespace == null || namespace.isEmpty()) { + return funcName; + } + return namespace + "." + funcName; + } + /** * Gets the namespace for symbols. */ diff --git a/src/test/java/io/smithy/unison/codegen/protocols/RestXmlProtocolGeneratorTest.java b/src/test/java/io/smithy/unison/codegen/protocols/RestXmlProtocolGeneratorTest.java index 1cd104d..dfc76bb 100644 --- a/src/test/java/io/smithy/unison/codegen/protocols/RestXmlProtocolGeneratorTest.java +++ b/src/test/java/io/smithy/unison/codegen/protocols/RestXmlProtocolGeneratorTest.java @@ -157,8 +157,9 @@ void testGenerateOperationWithSimpleGetOperation() { String output = writer.toString(); // Check that the function signature is generated (includes Threads for @unison/http) - assertTrue(output.contains("getObject : Config -> GetObjectInput -> '{IO, Exception, Threads} GetObjectOutput"), - "Should generate correct function signature. Got: " + output); + // With namespace "com.example", types are prefixed with "Com.Example." + assertTrue(output.contains("Com.Example.getObject : Com.Example.Config -> Com.Example.GetObjectInput -> '{IO, Exception, Threads} Com.Example.GetObjectOutput"), + "Should generate correct function signature with namespace. Got: " + output); // Check that HTTP method is set assertTrue(output.contains("method = \"GET\""),