SwiftPoet

repository·main·Indexed 43 days ago

https://github.com/outfoxx/swiftpoet

A Kotlin and Java API for generating .swift source files, utilizing a builder pattern inspired by JavaPoet. It allows developers to programmatically construct Swift code structures such as classes, properties, and functions, making it ideal for annotation processing or generating code from metadata like database schemas. Version 1.6.11 provides tools for managing FileSpec, TypeSpec, and FunctionSpec, along with a robust CodeBlock system for handling literals, types, and control flow.

Tokens
9.1K
Snippets
32
Records
36
Agent score
55%

What's inside SwiftPoet

  1. Install SwiftPoet via Maven or Gradle

    main

    SwiftPoet is a Kotlin and Java API for generating .swift source files. You can add it to your project using Maven or Gradle. The current version is 1.6.11.

    <!-- Maven -->
    <dependency>
      <groupId>io.outfoxx</groupId>
      <artifactId>swiftpoet</artifactId>
      <version>1.6.11</version>
    </dependency>
    
    <!-- Gradle -->
    implementation 'io.outfoxx:swiftpoet:1.6.11'
  2. How to cut a release

    main

    To release a new version of SwiftPoet, follow these steps to update documentation, set version variables, update project files via sed, tag the release in Git, and prepare the development branch for the next version.

    1. Update Changelog

    Update CHANGELOG.md with the new release details.

    2. Set Version Environment Variables

    Define the current release version and the subsequent snapshot version:

    export RELEASE_VERSION=X.Y.Z
    export NEXT_VERSION=X.Y.Z-SNAPSHOT

    3. Update Project Files

    Use sed to update version strings in README.md and gradle.properties. This ensures that dependency references, documentation URLs, and Gradle properties reflect the new $RELEASE_VERSION.

    4. Tag and Push the Release

    Create a release branch, commit the changes, tag it, and push to GitHub:

    git checkout -b release.$RELEASE_VERSION
    git commit -am "Prepare for release $RELEASE_VERSION."
    git tag -a $RELEASE_VERSION -m "Version $RELEASE_VERSION"
    git push && git push --tags

    5. Merge and Automate

    Merge the release branch once CI passes. The CI pipeline will automatically release the artifacts upon merge.

    6. Prepare for Ongoing Development

    Switch to a new development branch and update gradle.properties to the $NEXT_VERSION to prepare for the next development cycle:

    git checkout -b dev.$NEXT_VERSION
    sed -i "" "s/releaseVersion=.*/releaseVersion=$NEXT_VERSION/g" `find . -name "gradle.properties"`
    git commit -am "Prepare next development version."
    git push

    Once pushed, CI will release the artifact and publish the documentation.

    export RELEASE_VERSION=X.Y.Z
    export NEXT_VERSION=X.Y.Z-SNAPSHOT
    
    git checkout -b release.$RELEASE_VERSION
    git commit -am "Prepare for release $RELEASE_VERSION."
    git tag -a $RELEASE_VERSION -m "Version $RELEASE_VERSION"
    git push && git push --tags
  3. Generate Swift source files with SwiftPoet

    main

    SwiftPoet uses a builder pattern inspired by JavaPoet to construct Swift code structures like classes, properties, and functions. You build a TypeSpec, add it to a FileSpec, and then write the file to a destination (such as a StringWriter or a file system).

    val observableTypeName = DeclaredTypeName.typeName("RxSwift.Observable")
    
    val testClass = TypeSpec.classBuilder("Greeter")
       .addProperty("name", STRING, Modifier.PRIVATE)
       .addFunction(
          FunctionSpec.constructorBuilder()
             .addParameter("name", STRING)
             .addCode("self.name = name\n")
             .build()
       )
       .addFunction(
          FunctionSpec.builder("greet")
             .returns(observableTypeName.parameterizedBy(STRING))
             .addCode("return %T.from(\"Hello \\(name)\")\n", observableTypeName)
             .build()
       )
       .build()
    
    val file = FileSpec.builder("Greeter")
       .addType(testClass)
       .build()
    
    val out = StringWriter()
    file.writeTo(out)
  4. Build a FileSpec using FileSpec.Builder

    main

    Use FileSpec.Builder to programmatically construct a Swift file. The builder provides methods to add various Swift constructs and configure file-level metadata.

    Builder Methods:

    • addComment(format: String, vararg args: Any): Adds a comment to the top of the file.
    • addMember(memberSpec: FileMemberSpec): Adds a generic member.
    • addType(typeSpec: AnyTypeSpec): Adds a type (class, struct, enum, etc.).
    • addFunction(functionSpec: FunctionSpec): Adds a top-level function. Note: Constructors and accessors cannot be added directly to a file; they must be members of a type.
    • addProperty(propertySpec: PropertySpec): Adds a top-level property.
    • addExtension(extensionSpec: ExtensionSpec): Adds a top-level extension.
    • addImport(importSpec: ImportSpec): Adds a specific import.
    • addImport(moduleName: String, vararg attributes: AttributeSpec): Adds an import for a module with optional attributes.
    • indent(indent: String): Sets the indentation string (defaults to `
  5. Create a Swift TypeVariableName

    main

    Use TypeVariableName to represent a generic type variable in generated Swift code. You can create a type variable by name using the invoke operator or the typeVariable factory method. You can optionally attach constraints (bounds) to the variable to specify that it must conform to certain protocols or be of a specific type.

    // Create a simple type variable named 'T'
    val t = TypeVariableName("T")
    
    // Create a type variable 'T' constrained to conform to 'MyProtocol'
    val tWithBound = TypeVariableName("T", TypeVariableName.bound(TypeVariableName.Bound.Constraint.CONFORMS_TO, "MyProtocol"))
  6. Manipulate optionality and implicitness in ParameterizedTypeName

    main

    The ParameterizedTypeName class provides methods to transform the type's optional or implicit status. These methods handle the underlying rawType (like OPTIONAL or IMPLICIT) and manage the type arguments accordingly:

    • makeOptional(): Returns a version of the type that is optional. If already optional, returns this.
    • makeNonOptional(): If the type is optional, it unwraps the optionality by returning the first type argument made non-optional. Otherwise, returns this.
    • unwrapOptional(): If the type is optional, returns the first type argument; otherwise, returns this.
    • makeImplicit(): Returns a version of the type that is implicit (using !).
    • makeNonImplicit(): If the type is implicit, returns the first type argument made non-implicit. Otherwise, returns this.
  7. Define Swift properties using PropertySpec.Builder

    main

    Use PropertySpec.Builder to construct Swift property declarations. You can define simple properties (constants with let or variables with var), computed properties using getters and setters, or subscripts.

    Key Capabilities

    • Simple Properties: Use builder(name, type) for let constants and varBuilder(name, type) for var variables.
    • Computed Properties: Add a getter or setter using FunctionSpec. Note that you cannot combine computed accessors with property observers (willSet/didSet).
    • Property Observers: Add willSet or didSet using FunctionSpec. These cannot be added to computed properties.
    • Subscripts: Use subscriptBuilder(signature) to define a subscript. Subscripts cannot be mutable or have initializers.
    • Modifiers & Documentation: Add visibility modifiers (e.g., PUBLIC, PRIVATE) and documentation blocks using addDoc().
    • Initializers: Set an initial value for simple properties using initializer(format, *args) or initializer(codeBlock).

    Constraints

    • Subscripts require at least one parameter.
    • Computed properties (those with getter or setter) cannot have willSet or didSet observers.
    • Property observers (willSet/didSet) cannot be added to computed properties.
    // Create a simple variable
    val myVar = PropertySpec.varBuilder("count", Int::class.asTypeName())
        .initializer("%L", 0)
        .build()
    
    // Create a computed property with a getter
    val computed = PropertySpec.builder("isReady", Boolean::class.asTypeName())
        .getter(FunctionSpec.getterBuilder().addCode("return true").build())
        .build()
  8. Join multiple CodeBlocks together

    main

    You can join a Collection<CodeBlock> into a single CodeBlock using the joinToCode extension function. This is useful for creating lists or comma-separated code segments.

    Parameters:

    • separator: The string used to separate blocks (default is ", ").
    • prefix: A string to prepend to the joined result.
    • suffix: A string to append to the joined result.
    val blocks = listOf(CodeBlock.of("a"), CodeBlock.of("b"))
    val joined = blocks.joinToCode(separator = " | ", prefix = "(", suffix = ")")
    // Resulting CodeBlock represents: (a | b)
  9. Configure modifiers and type variables for a TypeAliasSpec

    main

    The TypeAliasSpec.Builder provides methods to attach modifiers and generic type variables to the type alias:

    • addModifiers(vararg modifiers: Modifier): Adds one or more modifiers (e.g., PUBLIC, INTERNAL, PRIVATE).
    • addTypeVariable(typeVariable: TypeVariableName): Adds a single generic type variable.
    • addTypeVariables(typeVariables: Iterable<TypeVariableName>): Adds multiple generic type variables.
    TypeAliasSpec.builder("MyAlias", typeName)
        .addModifiers(Modifier.PUBLIC, Modifier.INTERNAL)
        .addTypeVariable(TypeVariableName("T"))
        .build()
  10. Create a CodeBlock using CodeBlock.of()

    main

    Use CodeBlock.of(format, *args) to quickly create a code fragment using positional or relative arguments. The format string uses percent-sign placeholders to inject values into the code block.

    // Example of creating a code block with positional arguments
    val block = CodeBlock.of("fun %N(%S: %T) {
      %W%L
    }", "myFunction", "paramName", String::class.java, "value")
  11. Configure property accessors and observers

    main

    PropertySpec.Builder allows you to define how a property is accessed or how it reacts to changes.

    Accessors (Computed Properties)

    Use these for computed properties. They are incompatible with property observers.

    • getter(getter: FunctionSpec): Sets the getter. The FunctionSpec must have the name GETTER.
    • setter(setter: FunctionSpec): Sets the setter. The FunctionSpec must have the name SETTER.
    • abstractGetter(): A convenience method to create an abstract getter.
    • abstractSetter(): A convenience method to create an abstract setter.

    Observers (Stored Properties)

    Use these for stored properties. They are incompatible with computed accessors.

    • willSet(willSet: FunctionSpec): Sets the willSet observer. The FunctionSpec must have the name WILL_SET.
    • didSet(didSet: FunctionSpec): Sets the didSet observer. The FunctionSpec must have the name DID_SET.
  12. Define a Swift type alias using TypeAliasSpec.Builder

    main

    Use TypeAliasSpec.builder(name, type) to create a new builder for a Swift typealias declaration. The builder allows you to configure documentation, access modifiers, and generic type variables before generating the final spec.

    Supported modifiers are:

    • Modifier.PUBLIC
    • Modifier.INTERNAL
    • Modifier.PRIVATE
    val myAlias = TypeAliasSpec.builder("MyAlias", someTypeName)
        .addDoc("// This is a documentation comment")
        .addModifiers(Modifier.PUBLIC)
        .addTypeVariable(someTypeVariable)
        .build()