Skip to content
4 changes: 4 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,10 @@ let package = Package(
],
resources: [ .process("Resources") ]
),
.testTarget(
name: "SmithyTests",
dependencies: ["Smithy"]
),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added a test target to the Smithy target since none previously existed.

.testTarget(
name: "SmithyCBORTests",
dependencies: ["SmithyCBOR", "ClientRuntime", "SmithyTestUtil"]
Expand Down
109 changes: 109 additions & 0 deletions Sources/Smithy/Schema/Node.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

Copy link
Contributor Author

@jbelkins jbelkins Nov 5, 2025

Choose a reason for hiding this comment

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

This type holds the actual content of a trait.

/// Contains the value of a Smithy Node.
///
/// 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
public enum Node: Sendable {
case object([String: Node])
case list([Node])
case string(String)
case number(Double)
case boolean(Bool)
case null
}

public extension Node {

/// Returns the object dictionary if this Node is `.object`, else returns `nil`.
var object: [String: Node]? {
guard case .object(let value) = self else { return nil }
return value
}

/// Returns the array of `Node` if this node is `.list`, else returns `nil`.
var list: [Node]? {
guard case .list(let value) = self else { return nil }
return value
}

/// Returns the string if this node is `.string`, else returns `nil`.
var string: String? {
guard case .string(let value) = self else { return nil }
return value
}

/// Returns the Double if this node is `.number`, else returns `nil`.
var number: Double? {
guard case .number(let value) = self else { return nil }
return value
}

/// Returns the `Bool` value if this node is `.boolean`, else returns `nil`.
var boolean: Bool? {
guard case .boolean(let value) = self else { return nil }
return value
}

/// Returns `true` if this node is `.null`, else returns `false`.
var null: Bool {
guard case .null = self else { return false }
return true
}
}

extension Node: ExpressibleByDictionaryLiteral {

public init(dictionaryLiteral elements: (String, Node)...) {
self = .object(Dictionary(uniqueKeysWithValues: elements))
}
}

extension Node: ExpressibleByArrayLiteral {

public init(arrayLiteral elements: Node...) {
self = .list(elements)
}
}

extension Node: ExpressibleByStringLiteral {

public init(stringLiteral value: String) {
self = .string(value)
}
}

extension Node: ExpressibleByIntegerLiteral {

public init(integerLiteral value: IntegerLiteralType) {
self = .number(Double(value))
}
}

extension Node: ExpressibleByFloatLiteral {

public init(floatLiteral value: FloatLiteralType) {
self = .number(Double(value))
}
}

extension Node: ExpressibleByBooleanLiteral {

public init(booleanLiteral value: BooleanLiteralType) {
self = .boolean(value)
}
}

extension Node: ExpressibleByNilLiteral {

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

// Below are schemas for all model shapes defined in the Smithy 2.0 prelude.
// Schemas for custom Smithy types may use these schemas in their definitions.

public enum Prelude {

public static var unitSchema: Schema {
Schema(id: .init("smithy.api", "Unit"), type: .structure)
}

public static var booleanSchema: Schema {
Schema(id: .init("smithy.api", "Boolean"), type: .boolean)
}

public static var stringSchema: Schema {
Schema(id: .init("smithy.api", "String"), type: .string)
}

public static var integerSchema: Schema {
Schema(id: .init("smithy.api", "Integer"), type: .integer)
}

public static var blobSchema: Schema {
Schema(id: .init("smithy.api", "Blob"), type: .blob)
}

public static var timestampSchema: Schema {
Schema(id: .init("smithy.api", "Timestamp"), type: .timestamp)
}

public static var byteSchema: Schema {
Schema(id: .init("smithy.api", "Byte"), type: .byte)
}

public static var shortSchema: Schema {
Schema(id: .init("smithy.api", "Short"), type: .short)
}

public static var longSchema: Schema {
Schema(id: .init("smithy.api", "Long"), type: .long)
}

public static var floatSchema: Schema {
Schema(id: .init("smithy.api", "Float"), type: .float)
}

public static var doubleSchema: Schema {
Schema(id: .init("smithy.api", "Double"), type: .double)
}

public static var documentSchema: Schema {
Schema(id: .init("smithy.api", "PrimitiveDocument"), type: .document)
}

public static var primitiveBooleanSchema: Schema {
Schema(id: .init("smithy.api", "PrimitiveBoolean"), type: .boolean, traits: [defaultTraitID: false])
}

public static var primitiveIntegerSchema: Schema {
Schema(id: .init("smithy.api", "PrimitiveInteger"), type: .integer, traits: [defaultTraitID: 0])
}

public static var primitiveByteSchema: Schema {
Schema(id: .init("smithy.api", "PrimitiveByte"), type: .byte, traits: [defaultTraitID: 0])
}

public static var primitiveShortSchema: Schema {
Schema(id: .init("smithy.api", "PrimitiveShort"), type: .short, traits: [defaultTraitID: 0])
}

public static var primitiveLongSchema: Schema {
Schema(id: .init("smithy.api", "PrimitiveLong"), type: .long, traits: [defaultTraitID: 0])
}

public static var primitiveFloatSchema: Schema {
Schema(id: .init("smithy.api", "PrimitiveFloat"), type: .float, traits: [defaultTraitID: 0])
}

public static var primitiveDoubleSchema: Schema {
Schema(id: .init("smithy.api", "PrimitiveDouble"), type: .double, traits: [defaultTraitID: 0])
}
}

private let defaultTraitID = ShapeID("smithy.api", "default")
72 changes: 72 additions & 0 deletions Sources/Smithy/Schema/Schema.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

/// A class which describes selected Smithy model information for a Smithy model shape.
///
/// Typically, the Schema contains only modeled info & properties that are relevant to
/// serialization, transport bindings, and other functions performed by the SDK.
public final class Schema: Sendable {

/// The Smithy shape ID for the shape described by this schema.
public let id: ShapeID

/// The type of the shape being described.
public let type: ShapeType

/// A dictionary of the described shape's trait shape IDs to Nodes with trait data.
///
/// Not all traits for a shape will be represented in the schema;
/// typically the Schema contains only the traits relevant to the client-side SDK.
public let traits: [ShapeID: Node]

/// The member schemas for this schema, if any.
///
/// Typically only a schema of type Structure, Union, Enum, IntEnum, List or Map will have members.
public let members: [Schema]

/// The target schema for this schema. Will only be used when this is a member schema.
public let target: Schema?

/// The index of this schema, if it represents a Smithy member.
///
/// For a member schema, index will be set to its index in the members array.
/// For other types of schema, index will be `-1`.
///
/// This index is intended for use as a performance enhancement when looking up member schemas
/// during deserialization.
public let index: Int

/// Creates a new Schema using the passed parameters.
///
/// No validation is performed on the parameters since calls to this initializer
/// are almost always code-generated from a previously validated Smithy model.
public init(
id: ShapeID,
type: ShapeType,
traits: [ShapeID: Node] = [:],
members: [Schema] = [],
target: Schema? = nil,
index: Int = -1
) {
self.id = id
self.type = type
self.traits = traits
self.members = members
self.target = target
self.index = index
}
}

public extension Schema {

/// The member name for this schema, if any.
///
/// Member name is computed from the schema's ID.
var memberName: String? {
id.member
}
}
42 changes: 42 additions & 0 deletions Sources/Smithy/Schema/ShapeID.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

/// Represents a single Smithy shape ID.
///
/// Shape ID is described in the Smithy 2.0 spec [here](https://smithy.io/2.0/spec/model.html#shape-id).
public struct ShapeID: Sendable, Hashable {
public let namespace: String
public let name: String
public let member: String?

/// Creates a Shape ID for a Smithy shape.
///
/// This initializer does no validation of length or of allowed characters in the Shape ID;
/// that is to be ensured by the caller (typically calls to this initializer will be code-generated
/// from previously validated Smithy models.)
/// - Parameters:
/// - namespace: The namespace for this shape, i.e. `smithy.api`.
/// - name: The name for this shape, i.e. `Integer`.
/// - member: The optional member name for this shape.
public init(_ namespace: String, _ name: String, _ member: String? = nil) {
self.namespace = namespace
self.name = name
self.member = member
}
}

extension ShapeID: CustomStringConvertible {

/// Returns the absolute Shape ID in a single, printable string.
public var description: String {
if let member = self.member {
return "\(namespace)#\(name)$\(member)"
} else {
return "\(namespace)#\(name)"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@
// SPDX-License-Identifier: Apache-2.0
//

/// Reproduces the cases in Smithy `ShapeType`.
/// https://github.com/smithy-lang/smithy/blob/main/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeType.java
public enum ShapeType {
/// Reproduces the cases in Smithy [ShapeType](https://github.com/smithy-lang/smithy/blob/main/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeType.java).
public enum ShapeType: Sendable {
case blob
case boolean
case string
Expand Down
22 changes: 22 additions & 0 deletions Tests/SmithyTests/ShapeIDTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import XCTest
import Smithy
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Simple test on ShapeID to ensure it renders to String properly.


class ShapeIDTests: XCTestCase {

func test_description_noMember() {
let subject = ShapeID("smithy.test", "TestShape")
XCTAssertEqual(subject.description, "smithy.test#TestShape")
}

func test_description_withMember() {
let subject = ShapeID("smithy.test", "TestShape", "TestMember")
XCTAssertEqual(subject.description, "smithy.test#TestShape$TestMember")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ class DirectedSwiftCodegen(

LOGGER.info("Generating Swift client for service ${directive.settings().service}")

var shouldGenerateTestTarget = false
Copy link
Contributor Author

Choose a reason for hiding this comment

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

unused property

context.protocolGenerator?.apply {
val ctx = ProtocolGenerator.GenerationContext(settings, model, service, symbolProvider, integrations, this.protocol, writers)
LOGGER.info("[${service.id}] Generating serde for protocol ${this.protocol}")
Expand All @@ -81,12 +80,12 @@ class DirectedSwiftCodegen(
generateMessageMarshallable(ctx)
generateMessageUnmarshallable(ctx)
generateCodableConformanceForNestedTypes(ctx)
generateSchemas(ctx)

initializeMiddleware(ctx)

LOGGER.info("[${service.id}] Generating unit tests for protocol ${this.protocol}")
val numProtocolUnitTestsGenerated = generateProtocolUnitTests(ctx)
shouldGenerateTestTarget = (numProtocolUnitTestsGenerated > 0)

LOGGER.info("[${service.id}] Generated $numProtocolUnitTestsGenerated tests for protocol ${this.protocol}")

Expand Down
Loading
Loading