Ga naar inhoud

Upgraden naar 4.0

Deze handleiding laat zien hoe u een bestaand Vapor 3.x project kunt opwaarderen naar 4.x. Deze handleiding probeert alle officiële pakketten van Vapor te behandelen, evenals enkele veelgebruikte leveranciers. Als u merkt dat er iets ontbreekt, dan is Vapor's team chat een goede plaats om om hulp te vragen. Issues en pull requests worden ook op prijs gesteld.

Afhankelijkheden

Om Vapor 4 te gebruiken, hebt u Xcode 11.4 en macOS 10.15 of hoger nodig.

De Install sectie van de docs gaat over het installeren van afhankelijkheden.

Package.swift

De eerste stap bij het upgraden naar Vapor 4 is het bijwerken van de afhankelijkheden van uw pakket. Hieronder staat een voorbeeld van een bijgewerkt Package.swift bestand. U kunt ook het bijgewerkte sjabloon Package.swift bekijken.

-// swift-tools-version:4.0
+// swift-tools-version:5.2
 import PackageDescription

 let package = Package(
     name: "api",
+    platforms: [
+        .macOS(.v10_15),
+    ],
     dependencies: [
-        .package(url: "https://github.com/vapor/fluent-postgresql.git", from: "1.0.0"),
+        .package(url: "https://github.com/vapor/fluent.git", from: "4.0.0-rc"),
+        .package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.0.0-rc"),
-        .package(url: "https://github.com/vapor/jwt.git", from: "3.0.0"),
+        .package(url: "https://github.com/vapor/jwt.git", from: "4.0.0-rc"),
-        .package(url: "https://github.com/vapor/vapor.git", from: "3.0.0"),
+        .package(url: "https://github.com/vapor/vapor.git", from: "4.3.0"),
     ],
     targets: [
         .target(name: "App", dependencies: [
-            "FluentPostgreSQL", 
+            .product(name: "Fluent", package: "fluent"),
+            .product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"),
-            "Vapor", 
+            .product(name: "Vapor", package: "vapor"),
-            "JWT", 
+            .product(name: "JWT", package: "jwt"),
         ]),
-        .target(name: "Run", dependencies: ["App"]),
-        .testTarget(name: "AppTests", dependencies: ["App"])
+        .target(name: "Run", dependencies: [
+            .target(name: "App"),
+        ]),
+        .testTarget(name: "AppTests", dependencies: [
+            .target(name: "App"),
+        ])
     ]
 )

Van alle pakketten die opgewaardeerd zijn voor Vapor 4 zal het hoofdversienummer met één verhoogd worden.

Waarschuwing

De -rc pre-release aanduiding wordt gebruikt omdat sommige pakketten van Vapor 4 nog niet officieel zijn uitgebracht.

Oude Pakketten

Sommige pakketten zijn misschien nog niet geüpgraded. Als u er tegenkomt, dien dan een probleem in om de auteur te laten weten.

Sommige Vapor 3 pakketten zijn afgeschreven, zoals:

  • vapor/auth: Nu opgenomen in Vapor.
  • vapor/core: Opgenomen in verschillende modules.
  • vapor/crypto: Vervangen door SwiftCrypto.
  • vapor/multipart: Nu opgenomen in Vapor.
  • vapor/url-encoded-form: Nu opgenomen in Vapor.
  • vapor-community/vapor-ext: Nu opgenomen in Vapor.
  • vapor-community/pagination: Nu onderdeel van Fluent.
  • IBM-Swift/LoggerAPI: Vervangen door SwiftLog.

Fluent Afhankelijkheid

vapor/fluent moet nu als een aparte dependency worden toegevoegd aan uw dependencies lijst en targets. Alle database-specifieke pakketten zijn aangevuld met -driver om de eis voor vapor/fluent duidelijk te maken.

- .package(url: "https://github.com/vapor/fluent-postgresql.git", from: "1.0.0"),
+ .package(url: "https://github.com/vapor/fluent.git", from: "4.0.0-rc"),
+ .package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.0.0-rc"),

Platvormen

Vapor's package manifests ondersteunen nu expliciet macOS 10.15 en hoger. Dit betekent dat uw pakket ook platformondersteuning moet specificeren.

+ platforms: [
+     .macOS(.v10_15),
+ ],

Vapor kan in de toekomst extra ondersteunde platforms toevoegen. Uw pakket mag een subset van deze platformen ondersteunen zolang het versienummer gelijk is aan of hoger is dan de minimum versie-eisen van Vapor.

Xcode

Vapor 4 maakt gebruik van Xcode 11's eigen SPM ondersteuning. Dit betekent dat u niet langer .xcodeproj bestanden hoeft te genereren. Het openen van de map van uw project in Xcode zal automatisch SPM herkennen en de afhankelijkheden binnenhalen.

U kunt uw project openen in Xcode met vapor xcode of open Package.swift.

Nadat u Package.swift hebt bijgewerkt, moet u wellicht Xcode sluiten en de volgende mappen uit de hoofdmap verwijderen:

  • Package.resolved
  • .build
  • .swiftpm
  • *.xcodeproj

Zodra uw bijgewerkte pakketten succesvol opgelost zijn, zou u compilerfouten moeten zien--waarschijnlijk heel wat. Maak u geen zorgen! We zullen u tonen hoe ze te herstellen.

Run

De eerste opdracht is om het bestand main.swift van uw Run module aan te passen aan het nieuwe formaat.

import App
import Vapor

var env = try Environment.detect()
try LoggingSystem.bootstrap(from: &env)
let app = Application(env)
defer { app.shutdown() }
try configure(app)
try app.run()

De inhoud van het bestand main.swift vervangt het bestand app.swift van de App module, dus u kunt dat bestand verwijderen.

App

Laten we eens kijken hoe we de basis App module structuur kunnen bijwerken.

configure.swift

De configure methode moet veranderd worden om een instantie van Application te accepteren.

- public func configure(_ config: inout Config, _ env: inout Environment, _ services: inout Services) throws
+ public func configure(_ app: Application) throws

Hieronder staat een voorbeeld van een bijgewerkte configureermethode.

import Fluent
import FluentSQLiteDriver
import Vapor

// Wordt aangeroepen voordat uw toepassing initialiseert.
public func configure(_ app: Application) throws {
    // Serveert bestanden uit `Public/` directory
    // app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))
    // Configureer SQLite database
    app.databases.use(.sqlite(.file("db.sqlite")), as: .sqlite)

    // Migraties configureren
    app.migrations.add(CreateTodo())

    try routes(app)
}

Syntax veranderingen voor het configureren van zaken als routing, middleware, fluent, en meer staan hieronder vermeld.

boot.swift

De inhoud van boot kan in de configure methode geplaatst worden, omdat deze nu de applicatie instantie accepteert.

routes.swift

De routes methode moet veranderd worden om een instantie van Application te accepteren.

- public func routes(_ router: Router, _ container: Container) throws
+ public func routes(_ app: Application) throws

Meer informatie over wijzigingen in de routingsyntaxis wordt hieronder vermeld.

Services

Vapor 4's services APIs zijn vereenvoudigd om het makkelijker te maken voor u om services te ontdekken en te gebruiken. Services worden nu getoond als methods en properties op Application en Request waardoor de compiler u kan helpen ze te gebruiken.

Laten we, om dit beter te begrijpen, eens naar een paar voorbeelden kijken.

// Verander de standaardpoort van de server in 8281
- services.register { container -> NIOServerConfig in
-     return .default(port: 8281)
- }
+ app.http.server.configuration.port = 8281

In plaats van een NIOServerConfig te registreren bij services, wordt server configuratie nu getoond als eenvoudige eigenschappen op Application die kunnen worden overschreven.

// Registreer cors middleware
let corsConfiguration = CORSMiddleware.Configuration(
    allowedOrigin: .all,
    allowedMethods: [.POST, .GET, .PATCH, .PUT, .DELETE, .OPTIONS]
)
let corsMiddleware = CORSMiddleware(configuration: corsConfiguration)
- var middlewares = MiddlewareConfig() // Create _empty_ middleware config
- middlewares.use(corsMiddleware)
- services.register(middlewares)
+ app.middleware.use(corsMiddleware)

In plaats van een MiddlewareConfig aan te maken en te registreren bij services, worden middleware nu blootgesteld als een eigenschap op Application waaraan toegevoegd kan worden.

// Doe een verzoek in een route handler.
- try req.make(Client.self).get("https://vapor.codes")
+ req.client.get("https://vapor.codes")

Net als Applicatie, stelt Request ook diensten bloot als eenvoudige eigenschappen en methoden. Request-specifieke services moeten altijd worden gebruikt binnen een routeafsluiting.

Dit nieuwe service patroon vervangt de Container, Service, en Config types uit Vapor 3.

Providers

Providers zijn niet langer verplicht om pakketten van derden te configureren. Elk pakket breidt in plaats daarvan Applicatie en Verzoek uit met nieuwe eigenschappen en methoden voor configuratie.

Kijk eens hoe Leaf is geconfigureerd in Vapor 4.

// Gebruik Leaf voor het renderen van views. 
- try services.register(LeafProvider())
- config.prefer(LeafRenderer.self, for: ViewRenderer.self)
+ app.views.use(.leaf)

Om Leaf te configureren, gebruik de app.leaf eigenschap.

// Uitschakelen van de caching van de bladweergave.
- services.register { container -> LeafConfig in
-     return LeafConfig(tags: ..., viewsDir: ..., shouldCache: false)
- }
+ app.leaf.cache.isEnabled = false

Environment

De huidige omgeving (productie, ontwikkeling, etc) kan worden opgevraagd via app.environment.

Aangepaste Services

Aangepaste diensten die voldoen aan het Service protocol en geregistreerd zijn bij de container in Vapor 3 kunnen nu worden uitgedrukt als uitbreidingen op ofwel Applicatie of Verzoek.

struct MyAPI {
    let client: Client
    func foo() { ... }
}
- extension MyAPI: Service { }
- services.register { container -> MyAPI in
-     return try MyAPI(client: container.make())
- }
+ extension Request {
+     var myAPI: MyAPI { 
+         .init(client: self.client)
+     }
+ }

Deze dienst kan dan worden benaderd met de extensie in plaats van make.

- try req.make(MyAPI.self).foo()
+ req.myAPI.foo()

Aangepaste Providers

De meeste aangepaste diensten kunnen worden geïmplementeerd met behulp van extensies zoals getoond in de vorige sectie. Het kan echter nodig zijn om voor sommige geavanceerde diensten in te haken op de levenscyclus van de applicatie of gebruik te maken van opgeslagen eigenschappen.

Applicatie's nieuwe Lifecycle helper kan gebruikt worden om lifecycle handlers te registreren.

struct PrintHello: LifecycleHandler {
    func willBoot(_ app: Application) throws {
        print("Hello!")
    }
}

app.lifecycle.use(PrintHello())

Om waarden op te slaan op Application, kun je de nieuwe Storage helper gebruiken.

struct MyNumber: StorageKey {
    typealias Value = Int
}
app.storage[MyNumber.self] = 5
print(app.storage[MyNumber.self]) // 5

Toegang tot app.storage kan worden verpakt in een instelbare berekende eigenschap om een beknopte API te maken.

extension Application {
    var myNumber: Int? {
        get { self.storage[MyNumber.self] }
        set { self.storage[MyNumber.self] = newValue }
    }
}

app.myNumber = 42
print(app.myNumber) // 42

NIO

Vapor 4 stelt nu SwiftNIO's async APIs direct beschikbaar en probeert niet om methoden als map en flatMap te overloaden of alias types als EventLoopFuture. Vapor 3 voorzag in overloads en aliassen voor achterwaartse compatibiliteit met vroege beta versies die uitgebracht werden voordat SwiftNIO bestond. Deze zijn verwijderd om verwarring met andere SwiftNIO compatibele pakketten te verminderen en beter de best practice aanbevelingen van SwiftNIO te volgen.

Async naamgeving veranderingen

De meest voor de hand liggende verandering is dat de Future typealias voor EventLoopFuture is verwijderd. Dit kan vrij eenvoudig worden opgelost met een find and replace.

Verder ondersteunt NIO niet de to: labels die Vapor 3 toevoegde. Gezien Swift 5.2's verbeterde type-inferentie, is to: nu toch minder nodig.

- futureA.map(to: String.self) { ... }
+ futureA.map { ... }

Methoden die voorafgegaan worden door new, zoals newPromise zijn veranderd in make om beter aan te sluiten bij de Swift stijl.

- let promise = eventLoop.newPromise(String.self)
+ let promise = eventLoop.makePromise(of: String.self)

catchMap is niet langer beschikbaar, maar NIO's methodes zoals mapError en flatMapErrorThrowing zullen in plaats daarvan werken.

Vapor 3's globale flatMap methode voor het combineren van meerdere futures is niet langer beschikbaar. Deze kan worden vervangen door gebruik te maken van NIO's and methode om veel futures samen te voegen.

- flatMap(futureA, futureB) { a, b in 
+ futureA.and(futureB).flatMap { (a, b) in
    // Doe iets met a en b.
}

ByteBuffer

Veel methoden en eigenschappen die voorheen Data gebruikten, gebruiken nu NIO's ByteBuffer. Dit type is een krachtiger en performanter byte opslagtype. U kunt meer lezen over de API in SwiftNIO's ByteBuffer docs.

Om een ByteBuffer terug te converteren naar Data, gebruik je:

Data(buffer.readableBytesView)

Throwing map / flatMap

De moeilijkste verandering is dat map en flatMap niet meer kunnen gooien. map heeft een werpversie met de (enigszins verwarrende) naam flatMapThrowing. flatMap heeft echter geen werpende tegenhanger. Dit kan betekenen dat je wat asynchrone code moet herstructureren.

Maps die niet gooien zouden prima moeten blijven werken.

// Non-throwing map.
futureA.map { a in
    return b
}

Maps die wel gooien moeten worden hernoemd naar flatMapThrowing.

- futureA.map { a in
+ futureA.flatMapThrowing { a in
    if ... {
        throw SomeError()
    } else {
        return b
    }
}

Flat-maps die niet gooien zouden prima moeten blijven werken.

// Non-throwing flatMap.
futureA.flatMap { a in
    return futureB
}

In plaats van het gooien van een fout in een flat-map, retourneer een toekomstige fout. Als de fout afkomstig is van een andere werpmethode, kan de fout worden opgevangen in een do / catch en geretourneerd als een future.

// Een gevangen fout als een toekomst teruggeven.
futureA.flatMap { a in
    do {
        try doSomething()
        return futureB
    } catch {
        return eventLoop.makeFailedFuture(error)
    }
}

Het aanroepen van methodes kan ook worden omgezet in een flatMapThrowing en geketend worden met behulp van tuples.

// Methode voor gooien omgebouwd tot flatMapThrowing met tuple-ketting.
futureA.flatMapThrowing { a in
    try (a, doSomeThing())
}.flatMap { (a, result) in
    // resultaat is de waarde van doSomething.
    return futureB
}

Routing

Routes worden nu rechtstreeks in de applicatie geregistreerd.

app.get("hello") { req in
    return "Hello, world"
}

Dit betekent dat je niet langer een router hoeft te registreren bij services. Geef gewoon de applicatie door aan je routes methode en begin met het toevoegen van routes. Alle methodes die beschikbaar zijn op RoutesBuilder zijn beschikbaar op Application.

Synchrone Content

Het decoderen van de inhoud van verzoeken verloopt nu synchroon.

let payload = try req.content.decode(MyPayload.self)
print(payload) // MyPayload

Dit gedrag kan worden opgeheven door register routes die gebruik maken van de .stream body collectie strategie.

app.on(.POST, "streaming", body: .stream) { req in
    // Het verzoek is nu asynchroon.
    req.body.collect().map { buffer in
        HTTPStatus.ok
    }
}

Comma-separated paths

Paden moeten nu gescheiden zijn door komma's en mogen geen / bevatten voor consistentie.

- router.get("v1/users/", "posts", "/comments") { req in 
+ app.get("v1", "users", "posts", "comments") { req in
    // Handle request.
}

Route parameters

Het Parameter protocol is verwijderd ten gunste van expliciet benoemde parameters. Dit voorkomt problemen met dubbele parameters en ongeordende fetching van parameters in middleware en route handlers.

- router.get("planets", String.parameter) { req in 
-     let id = req.parameters.next(String.self)
+ app.get("planets", ":id") { req in
+     let id = req.parameters.get("id")
      return "Planet id: \(id)"
  }

Het gebruik van routeparameters met modellen wordt vermeld in het Fluent-gedeelte.

Middleware

MiddlewareConfig is hernoemd naar MiddlewareConfiguration en is nu een property op Application. U kunt middleware aan uw app toevoegen met app.middleware.

let corsMiddleware = CORSMiddleware(configuration: ...)
- var middleware = MiddlewareConfig()
- middleware.use(corsMiddleware)
+ app.middleware.use(corsMiddleware)
- services.register(middlewares)

Middleware kan niet langer geregistreerd worden op type-naam. Initialiseer de middleware eerst alvorens te registreren.

- middleware.use(ErrorMiddleware.self)
+ app.middleware.use(ErrorMiddleware.default(environment: app.environment))

Om alle standaard middleware te verwijderen, stel app.middleware in op een lege config met:

app.middleware = .init()

Fluent

Fluent's API is nu database agnostisch. U kunt alleen Fluent importeren.

- import FluentMySQL
+ import Fluent

Models

Alle modellen gebruiken nu het Model protocol en moeten klassen zijn.

- struct Planet: MySQLModel {
+ final class Planet: Model {

Alle velden worden gedeclareerd met @Field of @OptionalField property wrappers.

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

+ @OptionalField(key: "age")
var age: Int?

Het ID van een model moet worden gedefinieerd met de @ID property wrapper.

+ @ID(key: .id)
var id: UUID?

Modellen die een identifier gebruiken met een aangepaste sleutel of type moeten @ID(custom:) gebruiken.

Van alle modellen moet de naam van hun tabel of verzameling statisch gedefinieerd zijn.

final class Planet: Model {
+   static let schema = "Planet"    
}

Alle modellen moeten nu een lege initializer hebben. Aangezien alle eigenschappen property wrappers gebruiken, kan deze leeg zijn.

final class Planet: Model {
+   init() { }
}

Model's save, update, en create retourneren niet langer de model instantie.

- model.save(on: ...)
+ model.save(on: ...).map { model }

Modellen kunnen niet langer worden gebruikt als route path componenten. Gebruik in plaats daarvan find en req.parameters.get.

- try req.parameters.next(ServerSize.self)
+ ServerSize.find(req.parameters.get("size"), on: req.db)
+     .unwrap(or: Abort(.notFound))

Model.ID is hernoemd naar Model.IDValue.

Model tijdstempels worden nu aangegeven met de @Timestamp property wrapper.

- static var createdAtKey: TimestampKey? = \.createdAt
+ @Timestamp(key: "createdAt", on: .create)
var createdAt: Date?

Relaties

Relaties worden nu gedefinieerd met property wrappers.

Parent relaties gebruiken de @Parent property wrapper en bevatten intern de field property. De sleutel die aan @Parent wordt doorgegeven moet de naam zijn van het veld dat de identifier in de database opslaat.

- var serverID: Int
- var server: Parent<App, Server> { 
-    parent(\.serverID) 
- }
+ @Parent(key: "serverID") 
+ var server: Server

Children relaties gebruiken de @Children eigenschap wrapper met een sleutelpad naar de gerelateerde @Parent.

- var apps: Children<Server, App> { 
-     children(\.serverID) 
- }
+ @Children(for: \.$server) 
+ var apps: [App]

Siblings relaties gebruiken de @Siblings property wrapper met sleutelpaden naar het pivot model.

- var users: Siblings<Company, User, Permission> {
-     siblings()
- }
+ @Siblings(through: Permission.self, from: \.$user, to: \.$company) 
+ var companies: [Company]

Pivots zijn nu normale modellen die voldoen aan Model met twee @Parent relaties en nul of meer extra velden.

Query

De database context wordt nu benaderd via req.db in route handlers.

- Planet.query(on: req)
+ Planet.query(on: req.db)

DatabaseConnectable is hernoemd naar Database.

Sleutelpaden naar velden worden nu voorafgegaan door $ om de eigenschap wrapper te specificeren in plaats van de veldwaarde.

- filter(\.foo == ...) 
+ filter(\.$foo == ...)

Migraties

Modellen ondersteunen niet langer op reflectie gebaseerde automatische migraties. Alle migraties moeten handmatig worden geschreven.

- extension Planet: Migration { }
+ struct CreatePlanet: Migration {
+     ...
+}

Migraties zijn nu stringly typed en losgekoppeld van modellen en gebruiken het Migration protocol.

- struct CreateGalaxy: <#Database#>Migration {
+ struct CreateGalaxy: Migration {

De prepare en revert methodes zijn niet langer statisch.

- static func prepare(on conn: <#Database#>Connection) -> Future<Void> {
+ func prepare(on database: Database) -> EventLoopFuture<Void> 

Het aanmaken van een schema builder gebeurt via een instance methode op Database.

- <#Database#>Database.create(Galaxy.self, on: conn) { builder in
-    // Use builder.
- }
+ var builder = database.schema("Galaxy")
+ // Use builder.

De create, update, en delete methods worden nu aangeroepen op de schema builder, vergelijkbaar met hoe query builder werkt.

Velddefinities zijn nu stringent getypeert en volgen het patroon:

field(<name>, <type>, <constraints>)

Zie onderstaand voorbeeld.

- builder.field(for: \.name)
+ builder.field("name", .string, .required)

Schema bouw kan nu aaneengeschakeld worden zoals query bouwer.

database.schema("Galaxy")
    .id()
    .field("name", .string, .required)
    .create()

Fluent Configuratie

DatabasesConfig is vervangen door app.databases.

try app.databases.use(.postgres(url: "postgres://..."), as: .psql)

MigrationsConfig is vervangen door app.migrations.

app.migrations.use(CreatePlanet(), on: .psql)

Repositories

Omdat de manier waarop services werken in Vapor 4 is veranderd, betekent dat ook dat de manier om database repositories te maken is veranderd. Je hebt nog steeds een protocol nodig zoals UserRepository, maar in plaats van een final class te maken die voldoet aan dat protocol, moet je in plaats daarvan een struct maken.

- final class DatabaseUserRepository: UserRepository {
+ struct DatabaseUserRepository: UserRepository {
      let database: Database
      func all() -> EventLoopFuture<[User]> {
          return User.query(on: database).all()
      }
  }

U moet ook de conformiteit van ServiceType verwijderen, omdat deze niet meer bestaat in Vapor 4.

- extension DatabaseUserRepository {
-     static let serviceSupports: [Any.Type] = [Athlete.self]
-     static func makeService(for worker: Container) throws -> Self {
-         return .init()
-     }
- }

In plaats daarvan moet je een UserRepositoryFactory maken:

struct UserRepositoryFactory {
    var make: ((Request) -> UserRepository)?
    mutating func use(_ make: @escaping ((Request) -> UserRepository)) {
        self.make = make
    }
}

Deze fabriek is verantwoordelijk voor het retourneren van een UserRepository voor een Request.

De volgende stap is het toevoegen van een extensie aan Application om uw fabriek te specificeren:

extension Application {
    private struct UserRepositoryKey: StorageKey { 
        typealias Value = UserRepositoryFactory 
    }

    var users: UserRepositoryFactory {
        get {
            self.storage[UserRepositoryKey.self] ?? .init()
        }
        set {
            self.storage[UserRepositoryKey.self] = newValue
        }
    }
}

Om het eigenlijke repository te gebruiken in een Request voeg je deze extensie toe aan Request:

extension Request {
    var users: UserRepository {
        self.application.users.make!(self)
    }
}

De laatste stap is het specificeren van de fabriek in configure.swift.

app.users.use { req in
    DatabaseUserRepository(database: req.db)
}

Je kunt nu je repository benaderen in je route handlers met: req.users.all() en eenvoudig de factory binnen tests vervangen. Als je een mocked repository in tests wilt gebruiken, maak dan eerst een TestUserRepository

final class TestUserRepository: UserRepository {
    var users: [User]
    let eventLoop: EventLoop

    init(users: [User] = [], eventLoop: EventLoop) {
        self.users = users
        self.eventLoop = eventLoop
    }

    func all() -> EventLoopFuture<[User]> {
        eventLoop.makeSuccededFuture(self.users)
    }
}

Je kunt nu deze mocked repository als volgt gebruiken in je tests:

final class MyTests: XCTestCase {
    func test() throws {
        let users: [User] = []
        app.users.use { TestUserRepository(users: users, eventLoop: $0.eventLoop) }
        ...
    }
}