Code generation: Swift Syntax or Mustache?

I'm exploring tools for Swift code generation based on my mentor's (@sebsto) recommendation of Swift-Syntax and Swift-Mustache, However, I'm still a bit unsure about their specific functionalities.

  • Has anyone used these for code generation and can share their experience?
  • Is Swift-Syntax for low-level code manipulation, while Swift-Mustache is for templating with variables?
  • Are there any advantages in terms of convenience or ease of use when choosing between these two?
2 Likes

Can't comment on those, but also should point out GitHub - krzysztofzablocki/Sourcery: Meta-programming for Swift, stop writing boilerplate code. which works pretty nicely for our use cases.

1 Like

They aim for totally different things.

Mustache is a templating languages to describe, for example, html templates on server to render, and isn’t bounded to the language itself, you can find Mustache support for JS, for example.

Swift Syntax on the other hand is a concrete implementation of Swift language syntax, used among others things to power Swift Macros.

So the question what is the use case for generation, if you want to generate boilerplate code — macros or pointed above Sourcery are the ways to go. Macros can be complex to write, so if the need falls into one of the common cases, like unit tests boilerplates, I’d lookup for existing solutions first.

1 Like

Thank you @vns
The context here is to generate code that represents a Swift struct to adhere a given JSON Schema.

The flow would be
Read a JSON Schema -> generate Swift struct that matched the specification of the JSON schema

Have you considered using OpenAPI generator? GitHub - apple/swift-openapi-generator: Generate Swift client and server code from an OpenAPI document.

Given use case, Swift Syntax might be a burden here as generating structs can be easily written by hand compared to complexity of the package, so either a generator from openapi or custom util are the options.

1 Like

OpenAPI is used to define APIs: the data structure and how to use them. While a JSON Schema just define the JSON schema. The purpose is not the same.

As far as I know, OpenAPI spec diverges from JSON Schema spec.

Can OpenAPI generator generate code for generic (complex) JSON Schema ? That's something to try.

@esraa maybe it's worth converting the JSON Schema to an OpenAPI and then use the Swift OpenAPI generator to give it a try.

The advantage of coding the generation ourselves however is to better control how the resulting struct looks like, which will help downstream code that consumes that struct.

1 Like

See GitHub - soto-project/soto-codegenerator: Code generation for Soto for a similar example - this uses mustache

2 Likes

I can't comment on Mustache as I have never used it, but when it comes to source code generation and manipulation, we use swift-syntax extensively at my company in a way that seems to be quite similar to your use case. We have a set of JSON files describing various aspects of our Lambda functions, and then we auto-generate a lot of boilerplate code, including all the request/response DTOs, DTOs for SQS and EB, injection of all the soto clients required to interact with other AWS services, extraction and sanitation of query and path parameters, retrieval of environment variables and SSM parameters and so on. The code is generated by buildTool plugins, making the whole experience quite satisfying.

Swift-syntax is very powerful for these scenarios. It is easily composable and reusable, and, assuming you'll be using SyntaxBuilders (which I strongly suggest), it's also easy to read (most of the time).

Here is a minimal snippet of swift-syntax in action for DTO generation where you can see what I mean by composable.

struct TypeSchema {
    let typeName: String
    let properties: [Property]
    let subTypes: [TypeSchema]
    
    struct Property {
        let name: String
        let type: String
    }
}

func generateDecodableStruct(for schema: TypeSchema) throws -> StructDeclSyntax {
    try StructDeclSyntax("public struct \(raw: schema.typeName)Request: Decodable") {
        for property in schema.properties {
            "public let \(raw: property.name): \(raw: property.type)"
        }
        for subType in schema.subTypes {
            try generateDecodableStruct(for: subType)
        }
    }
}

Another reason why I would advocate for swift-syntax is its integration with swift-parser and swift-format. If you are not going to use buildTool plugins, having the possibility to evaluate if a file has actually changed prior to writing it to disk, and having the possibility of formatting the source code in the same step instead of invoking swift-format and "rewrite it in-place" later on can save you a few minutes every time (obviously we are talking about a few thousands of files here).

Last but not least, with swift-syntax you can manipulate source code in existing files. I'll bring a real example here to explain better what I mean. Our whole system was based on gyb, which means that every time we wanted to change the dependencies of a target that wasn't auto generated, we had to change our Package.swift.gyb template and run gyb.
With swift-syntax we simply parse the whole Package.swift file, we find the declaration of

let autogeneratedTargets: [Target] = [ /*....*/]

and then we just replace that declaration, without altering any other statement of the file. This means we can safely manage the 'non-autogenerated' targets manually directly in our Package.swift instead of using a boring gyb file with no code completion and no syntax highlight support.

4 Likes

The Soto code generator does use Mustache, but it was written at a time when swift-syntax wasn't available to generate swift source code. If I was to rewrite it I would certain consider using swift-syntax now.

2 Likes

Thank you Allessio for having shared such an extended and detailed answer. This is very inspiring.

Thank you @alessioburatti for the detailed explanation and the code snippet
your insights have given me a clearer understanding

SwiftGen should also be able to generate arbitrary code from parsing a JSON file.

Sourcery is to generate Swift code from other Swift code, while SwiftGen is to generate Swift code (typically constants or struct layouts) from resource files (xcassets, custom fonts, .strings files). So if your goal is to take a JSON as input SwiftGen would be more appropriate than Sourcery.

SwiftGen uses Stencil as its tempting language, which is inspired by and similar to Mustache. (Though I created SwiftGen before swift-syntax existed and if I were to rewrite it today I'd likely consider allowing people to use swift-syntax to write their templates too…)

With all that being said, if the JSON you're willing to parse to generate custom code is in fact a JSONSchema file, there might also be other existing tools (like the OpenAPI generator mentioned above) even more fitted to that specific need…

2 Likes

When I started the Teco project by “copying” Soto’s API, I was lucky enough to have SwiftSyntaxBuilder.

In consequence, despite some parts of it being a bit nasty, the code generators for Teco is a valid example for generating a massive codebase (>100kloc) with Swift Syntax. It’s also comparable to the ones for Soto v5, since they’re generating mostly the same kinds of APIs.

1 Like