Skip to content

Commit

Permalink
Merge pull request #1 from amzn/stronger_modelerrorsdelegate_contract
Browse files Browse the repository at this point in the history
Create a stronger ModelErrorsDelegate protocol contract.
  • Loading branch information
tachyonics authored Mar 29, 2019
2 parents f7a8d1f + 359ae67 commit 0b039a6
Show file tree
Hide file tree
Showing 7 changed files with 347 additions and 128 deletions.
67 changes: 62 additions & 5 deletions Sources/ServiceModelCodeGeneration/ModelErrorsDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,73 @@ public protocol ModelErrorsDelegate {
var canExpectValidationError: Bool { get }

/**
Generator for the error type initializer.
Generator for the error type additional imports.
- Parameters:
- fileBuilder: The FileBuilder to output to.
- errorTypes: The sorted list of error types.
- codingErrorUnknownError: the error that can be thrown for an unknown error.
*/
func errorTypeInitializerGenerator(fileBuilder: FileBuilder,
errorTypes: [String],
codingErrorUnknownError: String)
func errorTypeAdditionalImportsGenerator(fileBuilder: FileBuilder,
errorTypes: [String])

/**
Generator for the error type additional error identities.
- Parameters:
- fileBuilder: The FileBuilder to output to.
- errorTypes: The sorted list of error types.
*/
func errorTypeAdditionalErrorIdentitiesGenerator(fileBuilder: FileBuilder,
errorTypes: [String])

/**
Indicates the number of additional error cases that will be added.
- Parameters:
- fileBuilder: The FileBuilder to output to.
- errorTypes: The sorted list of error types.
*/
func errorTypeWillAddAdditionalCases(fileBuilder: FileBuilder,
errorTypes: [String]) -> Int

/**
Generator for the error type additional error cases.
- Parameters:
- fileBuilder: The FileBuilder to output to.
- errorTypes: The sorted list of error types.
*/
func errorTypeAdditionalErrorCasesGenerator(fileBuilder: FileBuilder,
errorTypes: [String])

/**
Generator for the error type CodingKeys.
- Parameters:
- fileBuilder: The FileBuilder to output to.
- errorTypes: The sorted list of error types.
*/
func errorTypeCodingKeysGenerator(fileBuilder: FileBuilder,
errorTypes: [String])

/**
Generator for the error type identity.
- Parameters:
- fileBuilder: The FileBuilder to output to.
- Returns: the variable name used to store the identity.
*/
func errorTypeIdentityGenerator(fileBuilder: FileBuilder) -> String

/**
Generator for the error type additional decode cases using the error identity.
- Parameters:
- fileBuilder: The FileBuilder to output to.
- errorTypes: The sorted list of error types.
*/
func errorTypeAdditionalErrorDecodeStatementsGenerator(fileBuilder: FileBuilder,
errorTypes: [String])

}

Expand Down
6 changes: 5 additions & 1 deletion Sources/ServiceModelEntities/ModelOverride.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ public struct ModelOverride: Codable {
/// structure attributes whose optionality should be overridden from the model
/// Can be specified as "*.{attributeName}" or "{type}.{attributeName}"
public let requiredOverrides: [String: Bool]?
/// any additional error codes that can be returned
public let additionalErrors: Set<String>?

public init(matchCase: Set<String>? = nil,
enumerations: EnumerationNaming? = nil,
Expand All @@ -49,7 +51,8 @@ public struct ModelOverride: Codable {
operationOutputOverrides: [String: OperationOutputDescription]? = nil,
modelStringPatternsAreAlternativeList: Bool = false,
codingKeyOverrides: [String: String]? = nil,
requiredOverrides: [String: Bool]? = nil) {
requiredOverrides: [String: Bool]? = nil,
additionalErrors: Set<String>? = nil) {
self.matchCase = matchCase
self.enumerations = enumerations
self.fieldRawTypeOverride = fieldRawTypeOverride
Expand All @@ -59,6 +62,7 @@ public struct ModelOverride: Codable {
self.modelStringPatternsAreAlternativeList = modelStringPatternsAreAlternativeList
self.codingKeyOverrides = codingKeyOverrides
self.requiredOverrides = requiredOverrides
self.additionalErrors = additionalErrors
}

public func getCodingKeyOverride(attributeName: String, inType: String?) -> String? {
Expand Down
1 change: 1 addition & 0 deletions Sources/ServiceModelEntities/ServiceModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public protocol ServiceModel {
var fieldDescriptions: [String: Fields] { get }
var errorTypes: Set<String> { get }
var typeMappings: [String: String] { get }
var errorCodeMappings: [String: String] { get }

/**
Initialize an instance of this ServiceModel type from a data instance
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License").
// You may not use this file except in compliance with the License.
// A copy of the License is located at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// or in the "license" file accompanying this file. This file 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.
//
// ServiceModelCodeGenerator+generateErrorDefinition.swift
// ServiceModelGenerate
//

import Foundation
import ServiceModelCodeGeneration
import ServiceModelEntities

public extension ServiceModelCodeGenerator {

internal func generateErrorDefinition(fileBuilder: FileBuilder,
sortedErrors: [String],
delegate: ModelErrorsDelegate) {
let baseName = applicationDescription.baseName
addErrorIdentities(fileBuilder: fileBuilder, sortedErrors: sortedErrors,
delegate: delegate)

let errorPayloadTypeName = generateErrorPayloadType(fileBuilder: fileBuilder,
sortedErrors: sortedErrors,
delegate: delegate)

let entityType = getEntityType(fileBuilder: fileBuilder, sortedErrors: sortedErrors,
delegate: delegate)

addCodingError(fileBuilder: fileBuilder)

fileBuilder.appendLine("""
public \(entityType) \(baseName)Error: Swift.Error, Decodable {
""")
fileBuilder.incIndent()

addErrorCases(fileBuilder: fileBuilder, sortedErrors: sortedErrors,
errorPayloadTypeName: errorPayloadTypeName)

// add any additional error cases from the delegate
delegate.errorTypeAdditionalErrorCasesGenerator(
fileBuilder: fileBuilder,
errorTypes: sortedErrors)

// add the coding keys from the delegate
fileBuilder.appendEmptyLine()
delegate.errorTypeCodingKeysGenerator(fileBuilder: fileBuilder,
errorTypes: sortedErrors)

fileBuilder.appendEmptyLine()
fileBuilder.appendLine("public init(from decoder: Decoder) throws {", postInc: true)

// add code to get the identity variable from the delegate
let identityVariable = delegate.errorTypeIdentityGenerator(fileBuilder: fileBuilder)

fileBuilder.appendEmptyLine()
fileBuilder.appendLine("switch \(identityVariable) {")

addErrorDecodeStatements(fileBuilder: fileBuilder, sortedErrors: sortedErrors,
delegate: delegate, errorPayloadTypeName: errorPayloadTypeName)
fileBuilder.decIndent()

// Otherwise this is a unrecognized error
fileBuilder.appendLine("""
default:
throw \(unrecognizedErrorType).unrecognizedError(errorReason, errorMessage)
}
}
""")

fileBuilder.decIndent()
fileBuilder.appendLine("""
}
""")
}

private func generateErrorPayloadType(fileBuilder: FileBuilder,
sortedErrors: [String],
delegate: ModelErrorsDelegate) -> String {
// if there are additional errors, create a payload type for them
let errorPayloadTypeName = "\(applicationDescription.baseName)ErrorPayload"
if modelOverride?.additionalErrors?.count ?? 0 > 0 {
fileBuilder.appendEmptyLine()
fileBuilder.appendLine("""
public struct \(errorPayloadTypeName): Codable {
public let type: String
public let message: String
""")

// use the coding keys from the delegate for this type.
fileBuilder.appendEmptyLine()
fileBuilder.incIndent()
delegate.errorTypeCodingKeysGenerator(fileBuilder: fileBuilder,
errorTypes: sortedErrors)
fileBuilder.decIndent()
fileBuilder.appendLine("""
}
""")
}

return errorPayloadTypeName
}

private func addErrorCases(fileBuilder: FileBuilder, sortedErrors: [String],
errorPayloadTypeName: String) {
// for each of the errors
for name in sortedErrors {
let enumName = getNormalizedEnumCaseName(
modelTypeName: name.normalizedErrorName,
inStructure: "\(applicationDescription.baseName)Error",
usingUpperCamelCase: true)

let payload: String
// if this is an error from the model
if model.errorTypes.contains(name) {
payload = name
} else {
payload = errorPayloadTypeName
}

fileBuilder.appendLine("case \(enumName)(\(payload))")
}
}

private func addErrorDecodeStatements(fileBuilder: FileBuilder,
sortedErrors: [String],
delegate: ModelErrorsDelegate,
errorPayloadTypeName: String) {
let baseName = applicationDescription.baseName

// for each of the errors
for name in sortedErrors {
let identityName = getNormalizedVariableName(
modelTypeName: name.normalizedErrorName,
inStructure: nil,
reservedWordsAllowed: true)

let parameterName = getNormalizedVariableName(
modelTypeName: name.normalizedErrorName,
inStructure: "\(baseName)Error",
reservedWordsAllowed: true)

let payload: String
if model.errorTypes.contains(name) {
payload = name
} else {
payload = errorPayloadTypeName
}

fileBuilder.appendLine("""
case \(identityName)Identity:
let errorPayload = try \(payload)(from: decoder)
self = \(baseName)Error.\(parameterName)(errorPayload)
""")
}

// If validation errors can be expected
if delegate.canExpectValidationError {
fileBuilder.appendLine("""
case validationErrorIdentityBuiltIn:
let errorMessage = try values.decodeIfPresent(String.self, forKey: .errorMessage) ?? ""
throw \(validationErrorType).validationError(reason: errorMessage)
""")
}

// add any additional error decode statements from the delegate
delegate.errorTypeAdditionalErrorDecodeStatementsGenerator(
fileBuilder: fileBuilder,
errorTypes: sortedErrors)
}

private func addErrorIdentities(fileBuilder: FileBuilder,
sortedErrors: [String],
delegate: ModelErrorsDelegate) {
if delegate.canExpectValidationError {
fileBuilder.appendLine("""
private let validationErrorIdentityBuiltIn = "ValidationError"
""")
}

// for each of the errors
for name in sortedErrors {
let identityName = getNormalizedVariableName(
modelTypeName: name.normalizedErrorName,
inStructure: nil,
reservedWordsAllowed: true)

let identity = model.errorCodeMappings[name] ?? name

fileBuilder.appendLine("""
private let \(identityName)Identity = "\(identity)"
""")
}

delegate.errorTypeAdditionalErrorIdentitiesGenerator(fileBuilder: fileBuilder, errorTypes: sortedErrors)
}

private func getEntityType(fileBuilder: FileBuilder,
sortedErrors: [String],
delegate: ModelErrorsDelegate) -> String {
// add any additional error cases from the delegate
let additionalCases = delegate.errorTypeWillAddAdditionalCases(fileBuilder: fileBuilder, errorTypes: sortedErrors)

// avoid an enum with no cases
let entityType: String
if sortedErrors.count + additionalCases > 0 {
entityType = "enum"
} else {
entityType = "struct"
}

return entityType
}

private func addCodingError(fileBuilder: FileBuilder) {
let baseName = applicationDescription.baseName
fileBuilder.appendLine("""
public enum \(baseName)CodingError: Swift.Error {
case unknownError
""")

// if we are using an internal validation error
if case .internal = customizations.validationErrorDeclaration {
fileBuilder.appendLine("""
case validationError(reason: String)
""")
}

// if we are using an internal unrecognized error
if case .internal = customizations.unrecognizedErrorDeclaration {
fileBuilder.appendLine("""
case unrecognizedError(String, String?)
""")
}

fileBuilder.appendLine("""
}
""")
fileBuilder.appendEmptyLine()
}
}
Loading

0 comments on commit 0b039a6

Please sign in to comment.