Skip to content
Draft
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
10 changes: 10 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -270,9 +270,15 @@ let package = Package(
.executableTarget(
name: "SmithyCodegenCLI",
dependencies: [
"SmithyCodegenCore",
.product(name: "ArgumentParser", package: "swift-argument-parser"),
]
),
.target(
name: "SmithyCodegenCore",
dependencies: ["Smithy"],
resources: [ .process("Resources") ]
),
.testTarget(
name: "ClientRuntimeTests",
dependencies: [
Expand Down Expand Up @@ -353,5 +359,9 @@ let package = Package(
name: "SmithyStreamsTests",
dependencies: ["SmithyStreams", "Smithy"]
),
.testTarget(
name: "SmithyCodegenCoreTests",
dependencies: ["SmithyCodegenCore"]
),
].compactMap { $0 }
)
21 changes: 21 additions & 0 deletions Sources/Smithy/Schema/SchemaTraits.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

/// The trait IDs that should be copied into schemas. Other traits are omitted for brevity.
///
/// This list can be expanded as features are added to Smithy/SDK that use them.
public let permittedTraitIDs: Set<String> = [
"smithy.api#sparse",
"smithy.api#input",
"smithy.api#output",
"smithy.api#error",
"smithy.api#enumValue",
"smithy.api#jsonName",
"smithy.api#required",
"smithy.api#default",
"smithy.api#timestampFormat",
]
9 changes: 3 additions & 6 deletions Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import ArgumentParser
import Foundation
import struct SmithyCodegenCore.CodeGenerator

@main
struct SmithyCodegenCLI: AsyncParsableCommand {
Expand All @@ -31,12 +32,8 @@ struct SmithyCodegenCLI: AsyncParsableCommand {
// If --schemas-path was supplied, create the schema file URL
let schemasFileURL = resolve(paramName: "--schemas-path", path: schemasPath)

// All file URLs needed for code generation have now been resolved.
// Implement code generation here.
if let schemasFileURL {
print("Schemas file path: \(schemasFileURL)")
FileManager.default.createFile(atPath: schemasFileURL.path, contents: Data())
}
// Use resolved file URLs to run code generator
try CodeGenerator(modelFileURL: modelFileURL, schemasFileURL: schemasFileURL).run()
}

private func currentWorkingDirectoryFileURL() -> URL {
Expand Down
14 changes: 14 additions & 0 deletions Sources/SmithyCodegenCore/AST/ASTError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

public struct ASTError: Error {
public let localizedDescription: String

init(_ localizedDescription: String) {
self.localizedDescription = localizedDescription
}
}
11 changes: 11 additions & 0 deletions Sources/SmithyCodegenCore/AST/ASTMember.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

struct ASTMember: Decodable {
let target: String
let traits: [String: ASTNode]?
}
12 changes: 12 additions & 0 deletions Sources/SmithyCodegenCore/AST/ASTModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

struct ASTModel: Decodable {
let smithy: String
let metadata: ASTNode?
let shapes: [String: ASTShape]
}
45 changes: 45 additions & 0 deletions Sources/SmithyCodegenCore/AST/ASTNode.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

/// Contains the value of a Smithy Node, as used in a JSON AST.
///
/// Smithy node data is basically the same as the data that can be stored in JSON.
/// The root of a Smithy node may be of any type, i.e. unlike JSON, the root element is not limited to object or list.
///
/// See the definition of node value in the Smithy spec: https://smithy.io/2.0/spec/model.html#node-values
enum ASTNode: Sendable {
case object([String: ASTNode])
case list([ASTNode])
case string(String)
case number(Double)
case boolean(Bool)
case null
}

extension ASTNode: Decodable {

init(from decoder: any Decoder) throws {
let container = try decoder.singleValueContainer()
if container.decodeNil() {
self = .null
} else if let bool = try? container.decode(Bool.self) {
self = .boolean(bool)
} else if let int = try? container.decode(Int.self) {
self = .number(Double(int))
} else if let double = try? container.decode(Double.self) {
self = .number(double)
} else if let string = try? container.decode(String.self) {
self = .string(string)
} else if let array = try? container.decode([ASTNode].self) {
self = .list(array)
} else if let dictionary = try? container.decode([String: ASTNode].self) {
self = .object(dictionary)
} else {
throw ASTError("Undecodable value in AST node")
}
}
}
10 changes: 10 additions & 0 deletions Sources/SmithyCodegenCore/AST/ASTReference.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

struct ASTReference: Decodable {
let target: String
}
31 changes: 31 additions & 0 deletions Sources/SmithyCodegenCore/AST/ASTShape.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
//

struct ASTShape: Decodable {
let type: ASTType
let traits: [String: ASTNode]?
let member: ASTMember?
let key: ASTMember?
let value: ASTMember?
let members: [String: ASTMember]?
let version: String?
let operations: [ASTReference]?
let resources: [ASTReference]?
let errors: [ASTReference]?
let rename: [String: String]?
let identifiers: [String: ASTReference]?
let properties: [String: ASTReference]?
let create: ASTReference?
let put: ASTReference?
let read: ASTReference?
let update: ASTReference?
let delete: ASTReference?
let list: ASTReference?
let collectionOperations: [ASTReference]?
let input: ASTReference?
let output: ASTReference?
}
37 changes: 37 additions & 0 deletions Sources/SmithyCodegenCore/AST/ASTType.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

enum ASTType: String, Decodable {
// These cases are all the Smithy shape types
case blob
case boolean
case string
case timestamp
case byte
case short
case integer
case long
case float
case document
case double
case bigDecimal
case bigInteger
case `enum`
case intEnum
case list
case set
case map
case structure
case union
case member
case service
case resource
case operation

// Special for AST, added 'apply' case
case apply
}
30 changes: 30 additions & 0 deletions Sources/SmithyCodegenCore/CodeGenerator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import struct Foundation.Data
import struct Foundation.URL

public struct CodeGenerator {
let modelFileURL: URL
let schemasFileURL: URL?

public init(modelFileURL: URL, schemasFileURL: URL?) {
self.modelFileURL = modelFileURL
self.schemasFileURL = schemasFileURL
}

public func run() throws {
// Load the model from the model file
let model = try Model(modelFileURL: modelFileURL)

// If a schema file URL was provided, generate it
if let schemasFileURL {
let schemaContents = try SmithySchemaCodegen().generate(model: model)
try Data(schemaContents.utf8).write(to: schemasFileURL)
}
}
}
14 changes: 14 additions & 0 deletions Sources/SmithyCodegenCore/CodegenError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

struct CodegenError: Error {
let localizedDescription: String

init(_ localizedDescription: String) {
self.localizedDescription = localizedDescription
}
}
87 changes: 87 additions & 0 deletions Sources/SmithyCodegenCore/Model/Model.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import struct Foundation.Data
import class Foundation.JSONDecoder
import struct Foundation.URL

public class Model {
public let version: String
public let metadata: Node?
public let shapes: [ShapeID: Shape]

public convenience init(modelFileURL: URL) throws {
let modelData = try Data(contentsOf: modelFileURL)
let astModel = try JSONDecoder().decode(ASTModel.self, from: modelData)
try self.init(astModel: astModel)
}

init(astModel: ASTModel) throws {
self.version = astModel.smithy
self.metadata = astModel.metadata?.modelNode
let idToShapePairs = try astModel.shapes.map { try Self.shapePair(id: $0.key, astShape: $0.value) }
let idToMemberShapePairs = try astModel.shapes.flatMap { astShape in
try Self.memberShapePairs(id: astShape.key, astShape: astShape.value)
}
self.shapes = Dictionary(uniqueKeysWithValues: idToShapePairs + idToMemberShapePairs)

// self is now initialized, perform post-initialization wireup

// set the Shapes with references back to this model
self.shapes.values.forEach { $0.model = self }

// set the memberIDs for each Shape
self.shapes.values.filter { $0.type != .member }.forEach { shape in
let namespace = shape.id.namespace
let name = shape.id.name
let memberIDs: [ShapeID] = Array(self.shapes.keys)
let filteredMemberIDs = memberIDs.filter {
$0.namespace == namespace && $0.name == name && $0.member != nil
}
shape.memberIDs = filteredMemberIDs.sorted()
}
}

private static func shapePair(id: String, astShape: ASTShape) throws -> (ShapeID, Shape) {
let shapeID = try ShapeID(id)
let idToTraitPairs = try astShape.traits?.map { (try ShapeID($0.key), $0.value.modelNode) } ?? []
let shape = Shape(
id: shapeID,
type: astShape.type.modelType,
traits: Dictionary(uniqueKeysWithValues: idToTraitPairs),
targetID: nil
)
return (shapeID, shape)
}

private static func memberShapePairs(id: String, astShape: ASTShape) throws -> [(ShapeID, Shape)] {
var baseMembers = (astShape.members ?? [:])
if let member = astShape.member {
baseMembers["member"] = member
}
if let key = astShape.key {
baseMembers["key"] = key
}
if let value = astShape.value {
baseMembers["value"] = value
}
return try baseMembers.map { astMember in
let memberID = ShapeID(id: try ShapeID(id), member: astMember.key)
let traitPairs = try astMember.value.traits?.map { (try ShapeID($0.key), $0.value.modelNode) }
let traits = Dictionary(uniqueKeysWithValues: traitPairs ?? [])
let targetID = try ShapeID(astMember.value.target)
return (memberID, Shape(id: memberID, type: .member, traits: traits, targetID: targetID))
}
}

func expectShape(id: ShapeID) throws -> Shape {
guard let shape = shapes[id] else {
throw ModelError("ShapeID \(id) was expected in model but not found")
}
return shape
}
}
14 changes: 14 additions & 0 deletions Sources/SmithyCodegenCore/Model/ModelError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

public struct ModelError: Error {
public let localizedDescription: String

init(_ localizedDescription: String) {
self.localizedDescription = localizedDescription
}
}
Loading
Loading