Files
swift-openapi-lambda/Sources/Router/OpenAPILambdaRouterNode.swift
T
2023-12-13 18:24:01 -05:00

206 lines
8.9 KiB
Swift

//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift OpenAPI Lambda open source project
//
// Copyright (c) 2023 Amazon.com, Inc. or its affiliates
// and the Swift OpenAPI Lambda project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Swift OpenAPI Lambda project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import HTTPTypes
/// A node in the graph
/// A node has a value and a list of children nodes
/// A node value can be
/// - `root` (only one, at the top of the tree),
/// - an HTTP method (represented as a HTTPRequest.Method)
/// - a path element (represented as a `String`),
/// - a path parameter (represented as a `String`)
/// - a handler, only for leaf nodes (represented as `OpenAPIHandler`)
class Node {
let value: NodeValue
var children: [String: Node] = [:]
/// Default init, create a root node
public init() { value = .root }
/// Creates a node for an HTTP method
public init(httpMethod: HTTPRequest.Method) { value = .httpMethod(httpMethod) }
/// Creates a node for a path element
public init(pathElement: String) { value = .pathElement(pathElement) }
/// Creates a node for a path parameter
public init(parameterName: String) { value = .pathParameter(parameterName) }
/// Creates a node for an OpenAPI handler
public init(handler: @escaping OpenAPIHandler) { value = .handler(handler) }
/// Creates a node for an existing node value
private init(value: NodeValue) { self.value = value }
/// Convenience method to add a child node of type HttpRequest.Method to this node
/// - Parameter:
/// - httpMethod: an HTTP Method
/// - Returns:
/// - the node that has been created
/// - Throws:
/// - URIPathCollectionError.canNotAddChildToHandlerNode when trying to add a child to leaf node of type `.handler`
/// - URIPathCollectionError.canNotHaveMultipleParamChilds when trying to add multiple child node of type `.parameter`
func add(httpMethod: HTTPRequest.Method) throws -> Node { try add(child: NodeValue.httpMethod(httpMethod)) }
/// Convenience method to add a child node of type path element to this node
/// - Parameter:
/// - pathElement: the name of a path element. A path element is a `String` usually found between `/` characters in the URI
/// - Returns:
/// - the node that has been created
/// - Throws:
/// - URIPathCollectionError.canNotAddChildToHandlerNode when trying to add a child to leaf node of type `.handler`
/// - URIPathCollectionError.canNotHaveMultipleParamChilds when trying to add multiple child node of type `.parameter`
func add(pathElement: String) throws -> Node { try add(child: NodeValue.pathElement(pathElement)) }
/// Convenience method to add a child node of type path parameter to this node
/// - Parameter:
/// - pathParameter: the name of a path parameter. A path parameter is a `{name}` usually found between `/` characters in the URI
/// - Returns:
/// - the node that has been created
/// - Throws:
/// - URIPathCollectionError.canNotAddChildToHandlerNode when trying to add a child to leaf node of type `.handler`
/// - URIPathCollectionError.canNotHaveMultipleParamChilds when trying to add multiple child node of type `.parameter`
func add(parameter: String) throws -> Node { try add(child: NodeValue.pathParameter(parameter)) }
/// Convenience method to add a child node of type handler to this node
/// - Parameter:
/// - handler: a function handler. A handler MUST be a leaf node (it has no children) and is of type `OpenAPIHandler`
/// - Returns:
/// - the node that has been created
/// - Throws:
/// - URIPathCollectionError.canNotAddChildToHandlerNode when trying to add a child to leaf node of type `.handler`
/// - URIPathCollectionError.canNotHaveMultipleParamChilds when trying to add multiple child node of type `.parameter`
func add(handler: @escaping OpenAPIHandler) throws -> Node { try add(child: NodeValue.handler(handler)) }
/// Convenience method to add a child node to this node
/// - Parameter:
/// - child: A NodeValue to add as child node to this node
/// - Returns:
/// - the node that has been created
/// - Throws:
/// - URIPathCollectionError.canNotAddChildToHandlerNode when trying to add a child to leaf node of type `.handler`
/// - URIPathCollectionError.canNotHaveMultipleParamChilds when trying to add multiple child node of type `.parameter`
private func add(child: NodeValue) throws -> Node {
let key = key(for: child)
// if the child node already exist, just ignore
guard children[key] == nil else { return children[key]! }
// Reject the add() operation when this node is an handler. Handler are leaf nodes.
if self.value.isHandler { throw URIPathCollectionError.canNotAddChildToHandlerNode }
// Reject the add() operation when there is already a param Node as child
// OpenAI specification -> https://swagger.io/specification/
// The following paths are considered identical and invalid:
// /pets/{petId}
// /pets/{name}
// when the child we try to add is a param
if child.isPathParameter {
// and there is already a param child
if hasParamChild() { throw URIPathCollectionError.canNotHaveMultipleParamChilds }
}
let newNode = Node(value: child)
children[key] = newNode
return newNode
}
/// Returns the child with the given value, nil if no child with such name exist
/// - Parameter:
/// - name: the value of the child node
func child(with name: String) -> Node? { self.children[name] }
/// Returns the child with the given value, nil if no child with such name exist
/// - Parameter:
/// - name: the value of the child node
func child(with name: Substring) -> Node? { self.children[String(name)] }
/// Returns the parameter child for this node if there is one, nil otherwise
func parameterChild() -> Node? {
let paramNodes = self.children.filter { key, value in self.children[key]!.value.isPathParameter }
precondition(paramNodes.count <= 1)
return paramNodes.count == 1 ? paramNodes.first!.value : nil
}
/// Returns true when one of the child is a param node, false otherwise
private func hasParamChild() -> Bool {
if self.children.isEmpty { return false }
return self.children.keys.allSatisfy { key in self.children[key]!.value.isPathParameter }
}
/// Returns the handler child for this node if there is one, nil otherwise
func handlerChild() -> Node? {
let handlerNodes = self.children.filter { key, value in self.children[key]!.value.isHandler }
precondition(handlerNodes.count <= 1)
return handlerNodes.count == 1 ? handlerNodes.first!.value : nil
}
/// Get a `Hashable` key for a node value
/// There can be only one root node and one child handler (as a leaf of the tree), so it is OK to hardcode these keys
private func key(for value: NodeValue) -> String {
switch value {
case .root: "root"
case .httpMethod(let method): method.rawValue
case .pathElement(let element): element
case .pathParameter(let name): name
case .handler: "handler"
}
}
/// A value for a node.
/// Possible values are:
/// - `root` (only one, at the top of the tree),
/// - an HTTP method (represented as a HTTPRequest.Method)
/// - a path element (represented as a `String`),
/// - a path parameter (represented as a `String`)
/// - a handler, only for leaf nodes (represented as `OpenAPIHandler`)
enum NodeValue {
case root
case httpMethod(HTTPRequest.Method)
case pathElement(String)
case pathParameter(String)
case handler(OpenAPIHandler)
var isPathParameter: Bool {
switch self {
case .pathParameter(_): return true
default: return false
}
}
var isHandler: Bool {
switch self {
case .handler(_): return true
default: return false
}
}
var handler: OpenAPIHandler? {
switch self {
case .handler(let handler): return handler
default: return nil
}
}
var asString: String? {
switch self {
case .root: return nil
case .httpMethod(let method): return method.rawValue
case .pathElement(let element): return element
case .pathParameter(let param): return param
case .handler(_): return nil
}
}
}
}