Files

171 lines
6.5 KiB
Swift
Raw Permalink Normal View History

// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import SwiftSyntax
/// Generates the `Generable` members of a struct-like type: a `@Generable` struct, or a
/// `Discriminated*` helper struct of an enum with associated values.
struct ObjectCodeGenerator {
/// The access modifier repeated on generated members.
var access: GeneratedAccess
/// The `name:` argument of `@Generable(name:)`, if any.
var name: ExprSyntax?
/// The type description.
var description: ExprSyntax?
/// The `representNilExplicitlyInGeneratedContent:` argument, if any.
var representNilExplicitly: ExprSyntax?
/// The stored properties, in declaration order.
var properties: [GenerableProperty]
/// The `Equatable`/`Hashable` conformances that `PartiallyGenerated` repeats.
var equatableConformances: [String]
/// The properties that appear in the schema and in generated content.
private var schemaProperties: [GenerableProperty] {
properties.filter { !$0.isGenerationID }
}
/// The `id: GenerationID` property, if the type declares one.
private var generationIDProperty: GenerableProperty? {
properties.first { $0.isGenerationID }
}
/// Whether the type declares an `id` property that isn't a `GenerationID`.
///
/// `PartiallyGenerated` then keeps the property and doesn't synthesize `Identifiable`.
private var hasNonGenerationIDProperty: Bool {
properties.contains { $0.schemaName == "id" && !$0.isGenerationID }
}
/// The `generationSchema` static property.
var generationSchemaSource: String {
var arguments: [String] = []
if let name {
arguments.append("__name: \(name)")
} else {
arguments.append("type: Self.self")
}
if let description {
arguments.append("description: \(description)")
}
if let representNilExplicitly {
arguments.append("representNilExplicitlyInGeneratedContent: \(representNilExplicitly)")
}
let schemaProperties = schemaProperties
if schemaProperties.isEmpty {
arguments.append("properties: []")
} else {
let list = schemaProperties.map { " \($0.schemaPropertySource)" }
.joined(separator: ",\n")
arguments.append("properties: [\n\(list)\n]")
}
return """
nonisolated \(access.prefix)static var generationSchema: \(moduleName).GenerationSchema {
\(moduleName).GenerationSchema(
\(arguments.joined(separator: ",\n").indented(by: 8, skippingFirstLine: true))
)
}
"""
}
/// The `generatedContent` property.
var generatedContentSource: String {
let additions = schemaProperties.map {
" addProperty(name: \(stringLiteral($0.schemaName)), value: self.\($0.identifier))\n"
}
let idArgument = generationIDProperty.map { " id: self.\($0.identifier),\n" } ?? ""
let explicitNil = representNilExplicitly?.description ?? "false"
let elementType = "(name: String, value: any \(moduleName).ConvertibleToGeneratedContent)"
return """
nonisolated \(access.prefix)var generatedContent: \(moduleName).GeneratedContent {
let explicitNil = \(explicitNil)
var properties = [\(elementType)]()
\(additions.joined()) return \(moduleName).GeneratedContent(
properties: properties,
\(idArgument) uniquingKeysWith: { _, second in
second
}
)
func addProperty(name: String, value: some \(moduleName).Generable) {
properties.append((name: name, value: value))
}
func addProperty(name: String, value: (some \(moduleName).Generable)?) {
if explicitNil || value != nil {
properties.append((name: name, value: value))
}
}
}
"""
}
/// The `init(_:)` initializer that decodes every stored property.
var initializerSource: String {
let assignments = properties.map { " \($0.initializationSource)\n" }
return """
nonisolated \(access.prefix)init(_ content: \(moduleName).GeneratedContent) throws {
\(assignments.joined())}
"""
}
/// The nested `PartiallyGenerated` struct.
var partiallyGeneratedSource: String {
var conformances: [String] = []
var members: [String] = []
var assignments: [String] = []
if !hasNonGenerationIDProperty {
conformances.append("Identifiable")
members.append("\(access.prefix)var id: \(moduleName).GenerationID")
assignments.append("self.id = content.id ?? \(moduleName).GenerationID()")
}
conformances.append("nonisolated \(moduleName).ConvertibleFromGeneratedContent")
conformances.append(contentsOf: equatableConformances)
for property in schemaProperties {
members.append(
"\(property.modifierPrefix)var \(property.identifier): \(property.type).PartiallyGenerated?"
)
assignments.append(property.initializationSource)
}
let memberLines = members.map { " \($0)\n" }.joined()
let assignmentLines = assignments.map { " \($0)\n" }.joined()
let initializer = "init(_ content: \(moduleName).GeneratedContent) throws"
let inheritance = conformances.joined(separator: ", ")
return """
nonisolated \(access.prefix)struct PartiallyGenerated: \(inheritance) {
\(memberLines) nonisolated \(access.prefix)\(initializer) {
\(assignmentLines) }
}
"""
}
}
extension String {
/// Returns the string with every line indented by `width` spaces.
///
/// - Parameters:
/// - width: The number of spaces to add.
/// - skippingFirstLine: Whether to leave the first line unchanged.
/// - Returns: The indented string. Empty lines stay empty.
func indented(by width: Int, skippingFirstLine: Bool = false) -> String {
let indentation = String(repeating: " ", count: width)
return split(separator: "\n", omittingEmptySubsequences: false).enumerated()
.map { index, line in
line.isEmpty || (skippingFirstLine && index == 0) ? String(line) : indentation + line
}
.joined(separator: "\n")
}
}