ConnectableKit

1.0.2

ConnectableKit is a Swift package for the Vapor framework that simplifies the response DTOs and JSON structures for API projects.
tugcanonbas/connectable-kit

What's New

Custom Error Middleware - Custom Error Handling

2023-12-07T11:56:06Z

What's Changed

  • Custom Error Middleware - Custom Error Handling by @tugcanonbas in #6

Full Changelog: 1.0.1...1.0.2

ConnectableKit

ConnectableKit is a Swift package for the Vapor framework that simplifies the response DTOs and JSON structures for API projects.

Features

  • Generic JSON structure: The Connectable protocol allows you to define a wrapped Vapor Content structs.
  • Custom HTTPStatus for every responses.
  • ErrorMiddleware configurations for handling Vapor's error as ConnectableKit JSON output.
  • CORSMiddleware configurations for handling Vapor's CORSMiddleware with ease.

Structure

TypeDescriptionType
statusFive possible cases: information, success, redirection, failure, and error.ResponseStatus: String
messageOptional custom message from server.String? = nil
dataGeneric associated type that represents the data that is being sent as a response. It can be any type that conforms to Vapor's Content protocol, which includes types such as String, Int, and custom structs or classes.Connectable? = nil
{
  "status": "success",
  "message": "Profile fetched successfully.",
  "data": {
    "id": "EBAD7AA7-A0AF-45F7-9D40-439C62FB26DD",
    "name": "Tuğcan",
    "surname": "ÖNBAŞ",
    "profileImage": "http://localhost:8080/default_profile_image.png",
    "profileCoverImage": "http://localhost:8080/default_profile_cover_image.png"
  }
}

Installation

ConnectableKit can be installed using Swift Package Manager. Simply add the following line to your Package.swift file:

dependencies: [
    .package(url: "https://github.com/tugcanonbas/connectable-kit.git", from: "1.0.0")
]
dependencies: [
    .product(name: "ConnectableKit", package: "connectable-kit"),
],

Usage

To use the ConnectableKit Framework,

- For Connectable

In Struct, simply conform that Profile is Connectable

import ConnectableKit

struct Profile: Model, Connectable {
    @ID(key: .id)
    var id: UUID

    @Field(key: "name")
    var name: String

    @Field(key: "surname")
    var surname: String

    @Field(key: "profileImage")
    var profileImage: String

    @Field(key: "profileCoverImage")
    var profileCoverImage: String
}

In Response call .DTO for responding wrapped generic response.

return .toDTO(_ httpStatus: Vapor.HTTPStatus = .ok, status: ResponserStatus = .success, message: String? = nil) -> Responser<Self>

app.get("/profiles", ":id") { req -> Profile.DTO in
    let id = try req.parameters.require("id", as: UUID.self)
    let profile = try await Profile.query(on: req.db).filter(\.$id == id).first()!

    return profile.toDTO(message: "Profile fetched successfully.")
}
app.post("/profiles") { req -> Profile.DTO in
    let profile = Profile(
        id: UUID(),
        name: "Tuğcan",
        surname: "ÖNBAŞ",
        profileImage: "http://localhost:8080/default_profile_image.png",
        profileCoverImage: "http://localhost:8080/default_profile_cover_image.png"
    )
    try await profile.save(on: req.db)

    return profile.toDTO(.created, message: "Profile fetched successfully.")
}

- For Empty Response

app.put("/profiles", ":id") { req -> Connector.DTO in
    let id = try req.parameters.require("id", as: UUID.self)
    let update = try req.content.decode(Profile.Update.self)

    let profile = try await Profile.query(on: req.db).filter(\.$id == id).first()!
    profile.name = update.name
    try await profile.save(on: req.db)

    return Connector.toDTO(.accepted, message: "Profile updated successfully.")
}
{
  "status": "success",
  "message": "Profile updated successfully."
}

Connectable protocol

public protocol Connectable: Content {
    associatedtype DTO = Responser<Self>
    func toDTO(_ httpStatus: Vapor.HTTPStatus, status: ResponserStatus, message: String?) -> Responser<Self>
}

public extension Connectable {
    func toDTO(_ httpStatus: Vapor.HTTPStatus = .ok, status: ResponserStatus = .success, message: String? = nil) -> Responser<Self> {
        let response = Responser(httpStatus, status: status, message: message, data: self)
        return response
    }
}

- For ErrorMiddleware

import ConnectableKit

Simply call ConnectableKit.configureErrorMiddleware(app) for default error handling.

ConnectableKit.configureErrorMiddleware(app)

Or, you can use custom error middleware error handling with ConnectableErrorMiddleware.ErrorClosure.

let errorClosure: ConnectableErrorMiddleware.ErrorClosure = { error in
            let status: Vapor.HTTPResponseStatus
            let reason: String
            let headers: Vapor.HTTPHeaders

            switch error {
            case let abort as Vapor.AbortError:
                reason = abort.reason
                status = abort.status
                headers = abort.headers
            case let customError as CustomError:
                reason = customError.localizedDescription
                status = customError.httpResponseStatus
                headers = [:]
            default:
                reason = app.environment.isRelease
                    ? "Something went wrong."
                    : String(describing: error)
                status = .internalServerError
                headers = [:]
            }

            return (status, reason, headers)
        }

ConnectableKit.configureErrorMiddleware(app, errorClosure: errorClosure)

Error Response Example

Database Error:

{
  "status": "error",
  "message": "server: duplicate key value violates unique constraint \"uq:users.username\" (_bt_check_unique)"
}

AbortError:

guard let profile = try await Profile.query(on: req.db).filter(\.$id == id).first() else {
    throw Abort(.notFound, reason: "User not found in our Database")
}

Response

{
  "status": "failure",
  "message": "User not found in our Database"
}

If you use ConnectableKitErrorMiddleware, don't forget to use before all middleware configureations.

See in Vapor's Documentation Error Middleware

- For CORSMiddleware

import ConnectableKit
ConnectableKit.configureCors(app)

func configureCORS

public static func configureCORS(_ app: Vapor.Application, configuration: Vapor.CORSMiddleware.Configuration? = nil)

Inspiration

The JSend specification, developed by Omniti Labs, outlines a consistent JSON response format for API endpoints. I found the specification to be very helpful in ensuring that API consumers can easily understand and parse the responses returned by the API.

As a result, I have heavily borrowed from the JSend specification for the response format used in this project. Specifically, I have adopted the status field to indicate the overall success or failure of the request, as well as the use of a message field to provide additional context for the response.

While I have made some modifications to the response format to suit the specific needs of this project, I am grateful for the clear and thoughtful guidance provided by the JSend specification.


License

ConnectableKit is available under the MIT license. See the LICENSE file for more info.


Description

  • Swift Tools 5.5.0
View More Packages from this Author

Dependencies

Last updated: Sun Apr 14 2024 18:01:27 GMT-0900 (Hawaii-Aleutian Daylight Time)