Skip to content

Commit

Permalink
Generate operation calls that replace input shape with parameters (#81)
Browse files Browse the repository at this point in the history
* Add support for generating functions that replace Input shape with list of parameters

* Update soto-smithy version

* waiters

* Move waiter/paginator context generation inside service generation

* swift 6

* Make shape init inlinable
  • Loading branch information
adam-fowler authored Oct 13, 2024
1 parent 53379e4 commit 2a7dc09
Show file tree
Hide file tree
Showing 12 changed files with 295 additions and 71 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ jobs:
strategy:
matrix:
tag:
- swift:5.8
- swift:5.9
- swift:5.10
- swift:6.0
container:
image: ${{ matrix.tag }}
steps:
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ let package = Package(
.plugin(name: "SotoCodeGeneratorPlugin", targets: ["SotoCodeGeneratorPlugin"]),
],
dependencies: [
.package(url: "https://github.com/soto-project/soto-smithy.git", from: "0.4.1"),
.package(url: "https://github.com/soto-project/soto-smithy.git", from: "0.4.2"),
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.0.0"),
.package(url: "https://github.com/hummingbird-project/swift-mustache.git", from: "2.0.0-beta"),
.package(url: "https://github.com/apple/swift-log.git", from: "1.4.0"),
Expand Down
9 changes: 7 additions & 2 deletions Sources/SotoCodeGeneratorLib/AwsService+paginators.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import SotoSmithy

extension AwsService {
/// Generate paginator context
func generatePaginatorContext() throws -> [String: Any] {
func generatePaginatorContext(_ operationContexts: [ShapeId: OperationContext]) throws -> [String: Any] {
let paginatorOperations = self.operations.filter { $0.value.hasTrait(type: PaginatedTrait.self) }
guard paginatorOperations.count > 0 else { return [:] }
var context: [String: Any] = ["name": serviceName]
Expand Down Expand Up @@ -53,9 +53,14 @@ extension AwsService {
inputKeyToken = nil
}

guard var operation = operationContexts[operation.key] else { continue }
if let inputKeyToken {
// remove input key from list of parameters to paginator function
operation.initParameters = operation.initParameters.filter { $0.parameter != inputKeyToken.toSwiftVariableCase() }
}
paginatorContexts.append(
PaginatorContext(
operation: try self.generateOperationContext(operationShape, operationName: operation.key, streaming: false),
operation: operation,
inputKey: inputKeyToken.map { self.toKeyPath(token: $0, structure: inputShape) },
outputKey: self.toKeyPath(token: outputToken, structure: outputShape),
moreResultsKey: paginatedTruncatedTrait.map { self.toKeyPath(token: $0.isTruncated, structure: outputShape) }
Expand Down
63 changes: 51 additions & 12 deletions Sources/SotoCodeGeneratorLib/AwsService+shapes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,7 @@ import SotoSmithyAWS

extension AwsService {
/// Generate context for outputting Shapes
func generateShapesContext() throws -> [String: Any] {
var context: [String: Any] = [:]
context["name"] = serviceName

func generateShapesContext() throws -> ShapesContext {
// generate enums
let traitEnums: [EnumContext] = try model
.select(from: "[trait|enum]")
Expand All @@ -30,13 +27,13 @@ extension AwsService {
.select(type: EnumShape.self)
.compactMap { self.generateEnumContext($0.value, shapeName: $0.key.shapeName) }
let enums = (traitEnums + shapeEnums).sorted { $0.name < $1.name }
var shapeContexts: [[String: Any]] = enums.map { ["enum": $0] }
var shapeContexts: [ShapesContext.ShapeType] = enums.map { .enum($0) }

// generate structures
let structures = model.select(type: StructureShape.self).sorted { $0.key.shapeName < $1.key.shapeName }
for structure in structures {
guard let shapeContext = self.generateStructureContext(structure.value, shapeId: structure.key, typeIsUnion: false) else { continue }
shapeContexts.append(["struct": shapeContext])
shapeContexts.append(.struct(shapeContext))
}

// generate unions
Expand All @@ -46,16 +43,20 @@ extension AwsService {
let typeIsUnion = union.value.members?.count == 1 ? false : true
guard let shapeContext = self.generateStructureContext(union.value, shapeId: union.key, typeIsUnion: typeIsUnion) else { continue }
if typeIsUnion {
shapeContexts.append(["enumWithValues": shapeContext])
shapeContexts.append(.enumWithValues(shapeContext))
} else {
shapeContexts.append(["struct": shapeContext])
shapeContexts.append(.struct(shapeContext))
}
}
let errorContext = try self.generateErrorContext()

if shapeContexts.count > 0 {
context["shapes"] = shapeContexts
}
return context
return .init(
name: self.serviceName,
shapes: shapeContexts,
errors: errorContext.count > 0 ? errorContext : nil,
scope: "public",
extraCode: nil /* self.generateExtraCode() */
)
}

/// Generate the context information for outputting an enum from strings with enum traits
Expand Down Expand Up @@ -622,4 +623,42 @@ extension AwsService {
}
return nil
}

func generateExtraCode() -> String? {
switch self.serviceName {
case "DynamoDB":
"""
extension DynamoDB.AttributeValue: Equatable {
public static func == (lhs: Self, rhs: Self) -> Bool {
switch (lhs, rhs) {
case (.b(let lhs), .b(let rhs)):
return lhs == rhs
case (.bool(let lhs), .bool(let rhs)):
return lhs == rhs
case (.bs(let lhs), .bs(let rhs)):
return lhs == rhs
case (.l(let lhs), .l(let rhs)):
return lhs == rhs
case (.m(let lhs), .m(let rhs)):
return lhs == rhs
case (.n(let lhs), .n(let rhs)):
return lhs == rhs
case (.ns(let lhs), .ns(let rhs)):
return lhs == rhs
case (.null(let lhs), .null(let rhs)):
return lhs == rhs
case (.s(let lhs), .s(let rhs)):
return lhs == rhs
case (.ss(let lhs), .ss(let rhs)):
return lhs == rhs
default:
return false
}
}
}
"""
default:
nil
}
}
}
10 changes: 5 additions & 5 deletions Sources/SotoCodeGeneratorLib/AwsService+waiters.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,18 @@ import SotoSmithy

extension AwsService {
/// Generate list of waiter contexts
func generateWaiterContexts() throws -> [String: Any] {
func generateWaiterContexts(_ operationContexts: [ShapeId: OperationContext]) throws -> [String: Any] {
var context: [String: Any] = [:]
context["name"] = self.serviceName
var waiters: [WaiterContext] = []
for operation in self.operations {
guard let trait = operation.value.trait(type: WaitableTrait.self) else { continue }
guard let operationContext = operationContexts[operation.key] else { continue }
for waiter in trait.value {
let waiterContext = try generateWaiterContext(
waiter.value,
name: waiter.key,
operation: operation.value,
operationContext: operationContext,
operationName: operation.key
)
waiters.append(waiterContext)
Expand All @@ -40,12 +41,11 @@ extension AwsService {
}

/// Generate waiter context from waiter
func generateWaiterContext(_ waiter: WaitableTrait.Waiter, name: String, operation: OperationShape, operationName: ShapeId) throws -> WaiterContext {
func generateWaiterContext(_ waiter: WaitableTrait.Waiter, name: String, operationContext: OperationContext, operationName: ShapeId) throws -> WaiterContext {
var acceptorContexts: [AcceptorContext] = []
for acceptor in waiter.acceptors {
acceptorContexts.append(self.generateAcceptorContext(acceptor))
}
let operationContext = try self.generateOperationContext(operation, operationName: operationName, streaming: false)
return .init(
waiterName: name,
operation: operationContext,
Expand Down Expand Up @@ -88,7 +88,7 @@ extension AwsService {
/// Basically convert all fields into format used for variables - ie lowercase first character
func generatePathArgument(argument: String) -> String {
// a field is any series of letters that doesn't end with a `(`
var output: String = ""
var output = ""
var index = argument.startIndex
var fieldStartIndex: String.Index?
while index != argument.endIndex {
Expand Down
137 changes: 111 additions & 26 deletions Sources/SotoCodeGeneratorLib/AwsService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ struct AwsService {
self.logger = logger

self.markInputOutputShapes(model)
// this is a breaking change (maybe for v8)
// self.removeEmptyInputs(model)
}

/// Return service name from API
Expand Down Expand Up @@ -91,7 +93,8 @@ struct AwsService {
let serviceId = serviceEntry.key
let service = serviceEntry.value
let authSigV4 = service.trait(type: AwsAuthSigV4Trait.self)
let operations = try generateOperationContexts()

let operationContexts = try self.generateOperationContexts()

context["name"] = self.serviceName
context["description"] = self.processDocs(from: service)
Expand Down Expand Up @@ -132,9 +135,18 @@ struct AwsService {
context["variantEndpoints"] = self.getVariantEndpoints()
.map { (variant: $0.key, endpoints: $0.value) }
.sorted { $0.variant < $1.variant }
context["operations"] = operations.operations
context["streamingOperations"] = operations.streamingOperations
context["operations"] = operationContexts.values.sorted { $0.funcName < $1.funcName }
let paginators = try self.generatePaginatorContext(operationContexts)
let waiters = try self.generateWaiterContexts(operationContexts)
if paginators["paginators"] != nil {
context["paginators"] = paginators
}
if waiters["waiters"] != nil {
context["waiters"] = waiters
}

context["logger"] = self.getSymbol(for: "Logger", from: "Logging", model: self.model, namespace: serviceId.namespace ?? "")

return context
}

Expand Down Expand Up @@ -173,34 +185,27 @@ struct AwsService {
return context
}

/// Generate list of operation and streaming operation contexts
func generateOperationContexts() throws -> (operations: [OperationContext], streamingOperations: [OperationContext]) {
var operationContexts: [OperationContext] = []
var streamingOperationContexts: [OperationContext] = []
/// Generate map of operation
func generateOperationContexts() throws -> [ShapeId: OperationContext] {
var operationContexts: [ShapeId: OperationContext] = [:]
let operations = self.operations
for operation in operations {
let operationContext = try generateOperationContext(operation.value, operationName: operation.key, streaming: false)
operationContexts.append(operationContext)

if let output = operation.value.output,
let outputShape = model.shape(for: output.target) as? StructureShape,
let payloadMember = getPayloadMember(from: outputShape),
let payloadShape = model.shape(for: payloadMember.value.target),
payloadShape.trait(type: StreamingTrait.self) != nil,
payloadShape is BlobShape
{
let operationContext = try generateOperationContext(operation.value, operationName: operation.key, streaming: true)
streamingOperationContexts.append(operationContext)
}
let operationContext = try generateOperationContext(
operation.value,
operationName: operation.key,
streaming: false
)
operationContexts[operation.key] = operationContext
}
return (
operations: operationContexts.sorted { $0.funcName < $1.funcName },
streamingOperations: streamingOperationContexts.sorted { $0.funcName < $1.funcName }
)
return operationContexts
}

/// Generate context for rendering a single operation. Used by both `generateServiceContext` and `generatePaginatorContext`
func generateOperationContext(_ operation: OperationShape, operationName: ShapeId, streaming: Bool) throws -> OperationContext {
func generateOperationContext(
_ operation: OperationShape,
operationName: ShapeId,
streaming: Bool
) throws -> OperationContext {
let httpTrait = operation.trait(type: HttpTrait.self)
let deprecatedTrait = operation.trait(type: DeprecatedTrait.self)
let endpointTrait = operation.trait(type: EndpointTrait.self)
Expand All @@ -216,6 +221,11 @@ struct AwsService {
if outputShapeTarget == "smithy.api#Unit" {
outputShapeTarget = nil
}
// get member contexts from shape
var initParamContext: [OperationInitParamContext] = []
if let inputShapeTarget {
initParamContext = self.generateInitParameterContexts(inputShapeTarget)
}
return OperationContext(
comment: self.processDocs(from: operation),
funcName: operationName.shapeName.toSwiftVariableCase(),
Expand All @@ -228,10 +238,44 @@ struct AwsService {
deprecated: deprecatedTrait?.message,
streaming: streaming ? "ByteBuffer" : nil,
documentationUrl: nil, // added to comment
endpointRequired: requireEndpointDiscovery.map { OperationContext.DiscoverableEndpoint(required: $0) }
endpointRequired: requireEndpointDiscovery.map { OperationContext.DiscoverableEndpoint(required: $0) },
initParameters: initParamContext
)
}

func generateInitParameterContexts(_ inputShapeId: ShapeId) -> [OperationInitParamContext] {
guard let shape = self.model.shape(for: inputShapeId) as? StructureShape else { return [] }
guard let members = shape.members else { return [] }
let sortedMembers = members.map { $0 }.sorted { $0.key.lowercased() < $1.key.lowercased() }
var contexts: [MemberContext] = []
for member in sortedMembers {
guard let targetShape = self.model.shape(for: member.value.target) else { continue }
// member context
let memberContext = self.generateMemberContext(
member.value,
targetShape: targetShape,
name: member.key,
shapeName: inputShapeId.shapeName,
typeIsUnion: false,
isOutputShape: false
)
contexts.append(memberContext)
}
return contexts.compactMap {
if !$0.deprecated {
OperationInitParamContext(
variable: $0.variable,
parameter: $0.parameter,
type: $0.type,
default: $0.default,
comment: $0.comment
)
} else {
nil
}
}
}

static func getTrait<T: StaticTrait>(from shape: SotoSmithy.Shape, trait: T.Type, id: ShapeId) throws -> T {
guard let trait = shape.trait(type: T.self) else {
throw Error(reason: "\(id) does not have a \(T.staticName) trait")
Expand Down Expand Up @@ -456,6 +500,24 @@ struct AwsService {
}
}

func removeEmptyInputs(_ model: Model) {
for operation in self.operations {
if let input = operation.value.input {
if let shape = model.shape(for: input.target) {
if let structureShape = shape as? StructureShape {
if let members = structureShape.members {
if members.count == 0 {
operation.value.input = nil
}
} else {
operation.value.input = nil
}
}
}
}
}
}

/// The JSON decoder requires an array to exist, even if it is empty so we have to make
/// all arrays in output shapes optional
func removeRequiredTraitFromOutputCollections(_ model: Model) {
Expand Down Expand Up @@ -666,6 +728,29 @@ extension AwsService {
let streaming: String?
let documentationUrl: String?
let endpointRequired: DiscoverableEndpoint?
var initParameters: [OperationInitParamContext]
}

struct OperationInitParamContext {
let variable: String
let parameter: String
let type: String
let `default`: String?
let comment: [String.SubSequence]
}

struct ShapesContext {
enum ShapeType {
case `enum`(EnumContext)
case `struct`(StructureContext)
case enumWithValues(StructureContext)
}

let name: String
let shapes: [ShapeType]
let errors: [String: Any]?
var scope: String
let extraCode: String?
}

struct PaginatorContext {
Expand Down
Loading

0 comments on commit 2a7dc09

Please sign in to comment.