Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/continuous-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ jobs:
- name: Build & Run smithy-swift Kotlin Unit Tests
run: ./gradlew build
- name: Build & Run smithy-swift Swift Unit Tests
timeout-minutes: 15
run: |
set -o pipefail && \
NSUnbufferedIO=YES xcodebuild \
Expand Down
64 changes: 50 additions & 14 deletions Plugins/SmithyCodeGeneratorPlugin/SmithyCodeGeneratorPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,19 +38,22 @@ struct SmithyCodeGeneratorPlugin: BuildToolPlugin {
in outputDirectoryPath: Path,
with generatorToolPath: Path
) throws -> Command? {
// Skip any file that isn't the smithy-model-info.json for this service.
guard inputPath.lastComponent == "smithy-model-info.json" else { return nil }
// Skip any file that isn't the swift-settings.json for this service.
guard inputPath.lastComponent == "swift-settings.json" else { return nil }

let currentWorkingDirectoryFileURL = URL(fileURLWithPath: FileManager.default.currentDirectoryPath)

// Get the smithy-model-info.json file contents.
let modelInfoData = try Data(contentsOf: URL(fileURLWithPath: inputPath.string))
let smithyModelInfo = try JSONDecoder().decode(SmithyModelInfo.self, from: modelInfoData)

// Get the service ID & model path & settings sdkId from smithy-model-info.
// Get the fields from smithy-model-info.
let service = smithyModelInfo.service
let modelPathURL = currentWorkingDirectoryFileURL.appendingPathComponent(smithyModelInfo.path)
let sdkId = smithyModelInfo.sdkId
let modelPathURL = currentWorkingDirectoryFileURL.appendingPathComponent(smithyModelInfo.modelPath)
let modelPath = Path(modelPathURL.path)
let internalClient = smithyModelInfo.internalClient
let operations = smithyModelInfo.operations.joined(separator: ",")

// Construct the Schemas.swift path.
let schemasSwiftPath = outputDirectoryPath.appending("\(name)Schemas.swift")
Expand All @@ -61,32 +64,65 @@ struct SmithyCodeGeneratorPlugin: BuildToolPlugin {
// Construct the Deserialize.swift path.
let deserializeSwiftPath = outputDirectoryPath.appending("\(name)Deserialize.swift")

// Construct the Deserialize.swift path.
let typeRegistrySwiftPath = outputDirectoryPath.appending("\(name)TypeRegistry.swift")

// Construct the Operations.swift path.
let operationsSwiftPath = outputDirectoryPath.appending("\(name)Operations.swift")

var arguments: [any CustomStringConvertible] = [
service,
modelPath,
"--internal", "\(internalClient)",
"--sdk-id", sdkId,
"--schemas-path", schemasSwiftPath,
"--serialize-path", serializeSwiftPath,
"--deserialize-path", deserializeSwiftPath,
"--type-registry-path", typeRegistrySwiftPath,
"--operations-path", operationsSwiftPath,
"--schemas-path", schemasSwiftPath,
"--serialize-path", serializeSwiftPath,
"--deserialize-path", deserializeSwiftPath
]

if !operations.isEmpty {
arguments.append(contentsOf: ["--operations", operations])
}

// Construct the build command that invokes SmithyCodegenCLI.
return .buildCommand(
displayName: "Generating Swift source files from model file \(smithyModelInfo.path)",
displayName: "Generating Swift source files from model file \(smithyModelInfo.modelPath)",
executable: generatorToolPath,
arguments: [
service,
modelPath,
"--schemas-path", schemasSwiftPath,
"--serialize-path", serializeSwiftPath,
"--deserialize-path", deserializeSwiftPath
],
arguments: arguments,
inputFiles: [inputPath, modelPath],
outputFiles: [
schemasSwiftPath,
serializeSwiftPath,
deserializeSwiftPath,
typeRegistrySwiftPath,
operationsSwiftPath,
]
)
}
}

/// Codable structure for reading the contents of `smithy-model-info.json`
/// Decodable structure for reading the contents of `smithy-model-info.json`
private struct SmithyModelInfo: Decodable {
/// The shape ID of the service being generated. Must exist in the model.
let service: String

/// The name to be used for the enclosing module.
let module: String

/// The `sdkId` used by the Smithy-based code generator.
let sdkId: String

/// Set to `true` if the client should be rendered for internal use.
let internalClient: Bool

/// A list of operations to be included in the client. If omitted or empty, all operations are included.
let operations: [String]

/// The path to the model, from the root of the target's project. Required.
let path: String
let modelPath: String
}
26 changes: 25 additions & 1 deletion Sources/Smithy/Schema/Prelude.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
public enum Prelude {

public static var unitSchema: Schema {
Schema(id: .init("smithy.api", "Unit"), type: .structure)
Schema(id: .init("smithy.api", "Unit"), type: .structure, traits: [UnitTypeTrait()])
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add the UnitTypeTrait to the smithy.api#Unit schema.

}

public static var booleanSchema: Schema {
Expand Down Expand Up @@ -54,6 +54,14 @@ public enum Prelude {
Schema(id: .init("smithy.api", "Double"), type: .double)
}

public static var bigIntegerSchema: Schema {
Schema(id: .init("smithy.api", "BigInteger"), type: .bigInteger)
}

public static var bigDecimalSchema: Schema {
Schema(id: .init("smithy.api", "BigDecimal"), type: .bigDecimal)
}

public static var documentSchema: Schema {
Schema(id: .init("smithy.api", "Document"), type: .document)
}
Expand Down Expand Up @@ -85,4 +93,20 @@ public enum Prelude {
public static var primitiveDoubleSchema: Schema {
Schema(id: .init("smithy.api", "PrimitiveDouble"), type: .double, traits: [DefaultTrait(0.0)])
}

// The following schemas aren't strictly part of Smithy's prelude, but are used when deserializing a
// list or map contained in a Smithy document.

public static var listDocumentSchema: Schema {
Schema(id: .init("swift.synthetic", "ListDocument"), type: .list, members: [
.init(id: .init("swift.synthetic", "ListDocument", "member"), type: .document),
])
}

public static var mapDocumentSchema: Schema {
Schema(id: .init("swift.synthetic", "MapDocument"), type: .list, members: [
.init(id: .init("swift.synthetic", "MapDocument", "key"), type: .string),
.init(id: .init("swift.synthetic", "MapDocument", "value"), type: .document),
])
}
}
3 changes: 3 additions & 0 deletions Sources/Smithy/TraitLibrary/AllSupportedTraits.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@ public let allSupportedTraits = Set([
ErrorTrait.id,
InputTrait.id,
OutputTrait.id,
RequiredTrait.id,
SensitiveTrait.id,
SparseTrait.id,
TimestampFormatTrait.id,
UnitTypeTrait.id, // UnitTypeTrait will only ever appear in Prelude.unitSchema
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Above, new trait types are added to the runtime "allow list".


// Synthetic traits
TargetsUnitTrait.id,
Expand Down
16 changes: 16 additions & 0 deletions Sources/Smithy/TraitLibrary/RequiredTrait.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

public struct RequiredTrait: Trait {
public static var id: ShapeID { .init("smithy.api", "required") }

public var node: Node { [:] }

public init(node: Node) throws {}

public init() {}
}
31 changes: 31 additions & 0 deletions Sources/Smithy/TraitLibrary/TimestampFormatTrait.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

public struct TimestampFormatTrait: Trait {

public enum Format: String {
case dateTime = "date-time"
case httpDate = "http-date"
case epochSeconds = "epoch-seconds"
}

public static var id: ShapeID { .init("smithy.api", "timestampFormat") }

public let format: Format

public init(node: Node) throws {
guard let formatString = node.string else {
throw TraitError("TimestampFormatTrait does not have string value")
}
guard let format = Format(rawValue: formatString) else {
throw TraitError("TimestampFormatTrait string value is not valid")
}
self.format = format
}

public var node: Node { .string(format.rawValue) }
}
16 changes: 16 additions & 0 deletions Sources/Smithy/TraitLibrary/UnitTypeTrait.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

public struct UnitTypeTrait: Trait {
public static var id: ShapeID { .init("smithy.api", "Unit") }

public var node: Node { [:] }

public init(node: Node) throws {}

public init() {}
}
36 changes: 34 additions & 2 deletions Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import ArgumentParser
import Foundation
import struct SmithyCodegenCore.CodeGenerator
import struct SmithyCodegenCore.SwiftSettings
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file adds new params to support various new code generator features, plus the operations & type registry Swift files.


@main
struct SmithyCodegenCLI: AsyncParsableCommand {
Expand All @@ -18,6 +19,15 @@ struct SmithyCodegenCLI: AsyncParsableCommand {
@Argument(help: "The full or relative path to read the JSON AST model input file.")
var modelPath: String

@Option(help: "The sdkId used by the Smithy-based code generator")
var sdkId: String?

@Option(help: "Set this to true if the client to be generated should be internal-scoped.")
var `internal` = false

@Option(help: "The comma-separated list of operation IDs to be included in the client.")
var operations: String?

@Option(help: "The full or relative path to write the Schemas output file.")
var schemasPath: String?

Expand All @@ -27,12 +37,26 @@ struct SmithyCodegenCLI: AsyncParsableCommand {
@Option(help: "The full or relative path to write the Deserialize output file.")
var deserializePath: String?

@Option(help: "The full or relative path to write the TypeRegistry output file.")
var typeRegistryPath: String?

@Option(help: "The full or relative path to write the Operations output file.")
var operationsPath: String?

func run() async throws {

let start = Date()

let currentWorkingDirectoryFileURL = currentWorkingDirectoryFileURL()

let operations = (operations ?? "").split(separator: ",").map(String.init)
let settings = try SwiftSettings(
service: service,
sdkId: sdkId,
internal: `internal`,
operations: operations
)

// Create the model file URL
let modelFileURL = URL(fileURLWithPath: modelPath, relativeTo: currentWorkingDirectoryFileURL)
guard FileManager.default.fileExists(atPath: modelFileURL.path) else {
Expand All @@ -48,13 +72,21 @@ struct SmithyCodegenCLI: AsyncParsableCommand {
// If --deserialize-path was supplied, create the Deserialize file URL
let deserializeFileURL = resolve(path: deserializePath)

// If --type-registry-path was supplied, create the TypeRegistry file URL
let typeRegistryFileURL = resolve(path: typeRegistryPath)

// If --operations-path was supplied, create the Operations file URL
let operationsFileURL = resolve(path: operationsPath)

// Use resolved file URLs to run code generator
try CodeGenerator(
service: service,
settings: settings,
modelFileURL: modelFileURL,
schemasFileURL: schemasFileURL,
serializeFileURL: serializeFileURL,
deserializeFileURL: deserializeFileURL
deserializeFileURL: deserializeFileURL,
typeRegistryFileURL: typeRegistryFileURL,
operationsFileURL: operationsFileURL
).run()

let duration = Date().timeIntervalSince(start)
Expand Down
31 changes: 23 additions & 8 deletions Sources/SmithyCodegenCore/CodeGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,35 @@ import struct Smithy.ShapeID

/// The wrapper for Swift-native code generation.
public struct CodeGenerator {
let service: String
let settings: SwiftSettings
let modelFileURL: URL
let schemasFileURL: URL?
let serializeFileURL: URL?
let deserializeFileURL: URL?
let typeRegistryFileURL: URL?
let operationsFileURL: URL?

/// Creates a code generator.
/// - Parameters:
/// - service: The absolute shape ID of the service to be generated. A service with this ID must exist in the model.
/// - modelFileURL: The file URL where the JSON AST model file can be accessed.
/// - schemasFileURL: The file URL to which the `Schemas.swift` source file should be written.
public init(
service: String,
settings: SwiftSettings,
modelFileURL: URL,
schemasFileURL: URL?,
serializeFileURL: URL?,
deserializeFileURL: URL?
deserializeFileURL: URL?,
typeRegistryFileURL: URL?,
operationsFileURL: URL?
) throws {
self.service = service
self.settings = settings
self.modelFileURL = modelFileURL
self.schemasFileURL = schemasFileURL
self.serializeFileURL = serializeFileURL
self.deserializeFileURL = deserializeFileURL
self.typeRegistryFileURL = typeRegistryFileURL
self.operationsFileURL = operationsFileURL
}

/// Executes the code generator.
Expand All @@ -45,14 +51,11 @@ public struct CodeGenerator {
let modelData = try Data(contentsOf: modelFileURL)
let astModel = try JSONDecoder().decode(ASTModel.self, from: modelData)

// Create the service's ShapeID
let serviceID = try ShapeID(service)

// Create the model from the AST
let model = try Model(astModel: astModel)

// Create a generation context from the model
let ctx = try GenerationContext(serviceID: serviceID, model: model)
let ctx = try GenerationContext(settings: settings, model: model)

// If a schemas file URL was provided, generate it
if let schemasFileURL {
Expand All @@ -71,5 +74,17 @@ public struct CodeGenerator {
let deserializeContents = try DeserializeCodegen().generate(ctx: ctx)
try Data(deserializeContents.utf8).write(to: deserializeFileURL)
}

// If a TypeRegistry file URL was provided, generate it
if let typeRegistryFileURL {
let typeRegistryContents = try TypeRegistryCodegen().generate(ctx: ctx)
try Data(typeRegistryContents.utf8).write(to: typeRegistryFileURL)
}

// If an Operations file URL was provided, generate it
if let operationsFileURL {
let operationsContents = try OperationsCodegen().generate(ctx: ctx)
try Data(operationsContents.utf8).write(to: operationsFileURL)
}
}
}
Loading
Loading