소스 검색

[swift|vapor] Added Vapor+Swifql+Ikiga (#9146)

* Added Vapor+Swifql+Ikiga

(cherry picked from commit cf1fc17c508024927d35ca5da15aab1fa85d20ad)

* Fixes so everything compiles

* Another attempt at fixes

* Removed unsupported @retroactive

* Added db benchmark

* FoundationEssentials JSON Coders support

* Added everything else

* Remove ikiga

* remove FoundationPreview due to bug https://github.com/apple/swift-foundation/issues/715

* fix db hostname

* fix fortune  template

* last fixes

* fixed resources

* hb2 compiles successfully

* hb2-postgres compiles

---------

Co-authored-by: Shagit Ziganshin <[email protected]>
Co-authored-by: Yakov Shapovalov <[email protected]>
Shagit Ziganshin 1 년 전
부모
커밋
31ca740439

+ 1 - 1
frameworks/Swift/hummingbird2/src-postgres/Sources/server/Controllers/FortunesController.swift

@@ -4,7 +4,7 @@ import PostgresNIO
 
 struct HTML: ResponseGenerator, Sendable {
     let html: String
-    public func response(from request: Request, context: some BaseRequestContext) -> Response {
+    public func response(from request: Request, context: some RequestContext) -> Response {
         let buffer = context.allocator.buffer(string: html)
         return Response(status: .ok, headers: [.contentType: "text/html; charset=utf-8"], body: .init(byteBuffer: buffer))
     }

+ 9 - 2
frameworks/Swift/hummingbird2/src-postgres/Sources/server/main.swift

@@ -1,3 +1,4 @@
+import Foundation
 import Hummingbird
 import PostgresNIO
 
@@ -15,15 +16,21 @@ struct TechFrameworkRequestContext: RequestContext {
     static let jsonEncoder = JSONEncoder()
     static let jsonDecoder = JSONDecoder()
 
-    var coreContext: Hummingbird.CoreRequestContext
+    var coreContext: Hummingbird.CoreRequestContextStorage
 
     // Use a global JSON Encoder
     var responseEncoder: JSONEncoder { Self.jsonEncoder }
     // Use a global JSON Decoder
     var requestDecoder: JSONDecoder { Self.jsonDecoder }
 
+    init(source: ApplicationRequestContextSource) {
+        self.init(channel: source.channel, logger: source.logger)
+    }
+
     init(channel: any Channel, logger: Logger) {
-        self.coreContext = .init(allocator: channel.allocator, logger: logger)
+        self.coreContext = CoreRequestContextStorage(
+            source: ApplicationRequestContextSource(channel: channel, logger: logger)
+        )
     }
 }
 

+ 9 - 2
frameworks/Swift/hummingbird2/src/Sources/server/main.swift

@@ -1,4 +1,6 @@
+import Foundation
 import Hummingbird
+import HummingbirdCore
 import Logging
 import NIOCore
 
@@ -7,18 +9,23 @@ struct Object: ResponseEncodable {
 }
 
 struct TechFrameworkRequestContext: RequestContext {
+
     static let jsonEncoder = JSONEncoder()
     static let jsonDecoder = JSONDecoder()
 
-    var coreContext: Hummingbird.CoreRequestContext
+    var coreContext: Hummingbird.CoreRequestContextStorage
 
     // Use a global JSON Encoder
     var responseEncoder: JSONEncoder { Self.jsonEncoder }
     // Use a global JSON Decoder
     var requestDecoder: JSONDecoder { Self.jsonDecoder }
 
+    init(source: Hummingbird.ApplicationRequestContextSource) {
+        self.coreContext = CoreRequestContextStorage(source: ApplicationRequestContextSource(channel: source.channel, logger: source.logger))
+    }
+
     init(channel: any Channel, logger: Logger) {
-        self.coreContext = .init(allocator: channel.allocator, logger: logger)
+        self.coreContext = CoreRequestContextStorage(source: ApplicationRequestContextSource(channel: channel, logger: logger))
     }
 }
 

+ 23 - 0
frameworks/Swift/vapor/benchmark_config.json

@@ -93,6 +93,29 @@
       "display_name": "Vapor",
       "notes": "",
       "versus": "None"
+    },
+    "swifql": {
+      "plaintext_url": "/plaintext",
+      "json_url": "/json",
+      "db_url": "/db",
+      "query_url": "/queries?queries=",
+      "update_url": "/updates?queries=",
+      "fortune_url": "/fortunes",
+      "database": "Postgres",
+      "orm": "Micro",
+      "port": 8080,
+      "approach": "Realistic",
+      "classification": "Fullstack",
+      "framework": "Vapor",
+      "language": "Swift",
+      "flavor": "None",
+      "platform": "None",
+      "webserver": "None",
+      "os": "Linux",
+      "database_os": "Linux",
+      "display_name": "Vapor",
+      "notes": "",
+      "versus": "None"
     }
   }]
 }

+ 17 - 0
frameworks/Swift/vapor/config.toml

@@ -51,3 +51,20 @@ orm = "Micro"
 platform = "None"
 webserver = "None"
 versus = "None"
+
+[swifql]
+urls.plaintext = "/plaintext"
+urls.json = "/json"
+urls.db = "/db"
+urls.query = "/queries?queries="
+urls.update = "/updates?queries="
+urls.fortune = "/fortunes"
+approach = "Realistic"
+classification = "Fullstack"
+database = "Postgres"
+database_os = "Linux"
+os = "Linux"
+orm = "Micro"
+platform = "None"
+webserver = "None"
+versus = "None"

+ 30 - 0
frameworks/Swift/vapor/vapor-swifql.dockerfile

@@ -0,0 +1,30 @@
+# ================================
+# Build image
+# ================================
+FROM swift:5.10 as build
+WORKDIR /build
+
+# Copy entire repo into container
+COPY ./vapor-swifql .
+
+# Compile with optimizations
+RUN swift build \
+	-c release \
+	-Xswiftc -enforce-exclusivity=unchecked
+
+# ================================
+# Run image
+# ================================
+FROM swift:5.10-slim
+WORKDIR /run
+
+# Copy build artifacts
+COPY --from=build /build/.build/release /run
+COPY ./vapor-swifql/Resources/Views/fortune.leaf /run/Resources/Views/fortune.leaf
+
+# Copy Swift runtime libraries
+COPY --from=build /usr/lib/swift/ /usr/lib/swift/
+
+EXPOSE 8080
+
+ENTRYPOINT ["./app", "serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"]

+ 2 - 0
frameworks/Swift/vapor/vapor-swifql/.dockerignore

@@ -0,0 +1,2 @@
+.build/
+.swiftpm/

+ 14 - 0
frameworks/Swift/vapor/vapor-swifql/.gitignore

@@ -0,0 +1,14 @@
+Packages
+.build
+xcuserdata
+*.xcodeproj
+DerivedData/
+.DS_Store
+db.sqlite
+.swiftpm
+.env
+.env.*
+! .env.example
+.vscode
+docker-compose.yml
+Dockerfile

+ 46 - 0
frameworks/Swift/vapor/vapor-swifql/Package.swift

@@ -0,0 +1,46 @@
+// swift-tools-version:5.10
+
+import PackageDescription
+
+let package = Package(
+    name: "vapor-swifql-ikiga",
+    platforms: [
+       .macOS(.v12)
+    ],
+    products: [
+        .executable(name: "app", targets: ["App"])
+    ],
+    dependencies: [
+        // 💧 A server-side Swift web framework.
+        .package(url: "https://github.com/vapor/vapor.git", from: "4.99.3"),
+        .package(url: "https://github.com/vapor/leaf.git", from: "4.0.0"),
+        // 🔵 Non-blocking, event-driven networking for Swift. Used for custom executors
+        .package(url: "https://github.com/apple/swift-nio.git", from: "2.65.0"),
+        // json encoder/decoder
+        .package(url: "https://github.com/orlandos-nl/IkigaJSON.git", from: "2.0.0"),
+        // sql builder
+        .package(url: "https://github.com/SwifQL/VaporBridges.git", from: "1.0.0-rc"),
+        .package(url: "https://github.com/SwifQL/PostgresBridge.git", from: "1.0.0-rc"),
+    ],
+    targets: [
+        .executableTarget(
+            name: "App",
+            dependencies: [
+                .product(name: "Vapor", package: "vapor"),
+                .product(name: "Leaf", package: "leaf"),
+                .product(name: "NIOCore", package: "swift-nio"),
+                .product(name: "NIOPosix", package: "swift-nio"),
+                .product(name: "VaporBridges", package: "VaporBridges"),
+                .product(name: "PostgresBridge", package: "PostgresBridge"),
+                .product(name: "IkigaJSON", package: "IkigaJSON"),
+            ],
+            swiftSettings: swiftSettings
+        )
+    ]
+)
+
+var swiftSettings: [SwiftSetting] { [
+    .enableUpcomingFeature("DisableOutwardActorInference"),
+    .enableExperimentalFeature("StrictConcurrency"),
+    .unsafeFlags(["-parse-as-library"]),
+] }

+ 10 - 0
frameworks/Swift/vapor/vapor-swifql/Resources/Views/fortune.leaf

@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+<head><title>Fortunes</title></head>
+<body>
+<table>
+<tr><th>id</th><th>message</th></tr>
+#for(fortune in fortunes):<tr><td>#(fortune.id)</td><td>#(fortune.message)</td></tr>
+#endfor</table>
+</body>
+</html>

+ 56 - 0
frameworks/Swift/vapor/vapor-swifql/Sources/Extensions/IkigaJSONCoders+ContentCoders.swift

@@ -0,0 +1,56 @@
+//
+//  IkigaJSONCoders+ContentCoders.swift
+//  
+//
+//  Created by Yakov Shapovalov on 04.07.2024.
+//
+
+import IkigaJSON
+import Vapor
+
+extension IkigaJSONEncoder: ContentEncoder {
+    public func encode<E: Encodable>(
+        _ encodable: E,
+        to body: inout ByteBuffer,
+        headers: inout HTTPHeaders
+    ) throws {
+        headers.contentType = .json
+        try self.encodeAndWrite(encodable, into: &body)
+    }
+
+    public func encode<E>(_ encodable: E, to body: inout ByteBuffer, headers: inout HTTPHeaders, userInfo: [CodingUserInfoKey : Sendable]) throws where E : Encodable {
+        var encoder = self
+        encoder.userInfo = userInfo
+        headers.contentType = .json
+        try encoder.encodeAndWrite(encodable, into: &body)
+    }
+
+    public func encode<E>(_ encodable: E, to body: inout ByteBuffer, headers: inout HTTPHeaders, userInfo: [CodingUserInfoKey : Any]) throws where E : Encodable {
+        var encoder = self
+        encoder.userInfo = userInfo
+        headers.contentType = .json
+        try encoder.encodeAndWrite(encodable, into: &body)
+    }
+}
+
+extension IkigaJSONDecoder: ContentDecoder {
+    public func decode<D: Decodable>(
+        _ decodable: D.Type,
+        from body: ByteBuffer,
+        headers: HTTPHeaders
+    ) throws -> D {
+        return try self.decode(D.self, from: body)
+    }
+
+    public func decode<D>(_ decodable: D.Type, from body: ByteBuffer, headers: HTTPHeaders, userInfo: [CodingUserInfoKey : Sendable]) throws -> D where D : Decodable {
+        let decoder = IkigaJSONDecoder(settings: settings)
+        decoder.settings.userInfo = userInfo
+        return try decoder.decode(D.self, from: body)
+    }
+
+    public func decode<D>(_ decodable: D.Type, from body: ByteBuffer, headers: HTTPHeaders, userInfo: [CodingUserInfoKey : Any]) throws -> D where D : Decodable {
+        let decoder = IkigaJSONDecoder(settings: settings)
+        decoder.settings.userInfo = userInfo
+        return try decoder.decode(D.self, from: body)
+    }
+}

+ 11 - 0
frameworks/Swift/vapor/vapor-swifql/Sources/Extensions/Models+Content.swift

@@ -0,0 +1,11 @@
+//
+//  Models+Content.swift
+//  
+//
+//  Created by Yakov Shapovalov on 04.07.2024.
+//
+
+import Vapor
+
+extension World: Content {}
+extension Fortune: Content {}

+ 18 - 0
frameworks/Swift/vapor/vapor-swifql/Sources/Extensions/Utils.swift

@@ -0,0 +1,18 @@
+extension Int {
+    func bounded(to range: ClosedRange<Int>) -> Int {
+        switch self {
+        case ...range.lowerBound:
+            return range.lowerBound
+        case range.upperBound...:
+            return range.upperBound
+        default:
+            return self
+        }
+    }
+}
+
+extension Int: Sequence {
+    public func makeIterator() -> CountableRange<Int>.Iterator {
+        return (0..<self).makeIterator()
+    }
+}

+ 17 - 0
frameworks/Swift/vapor/vapor-swifql/Sources/Models/Fortune.swift

@@ -0,0 +1,17 @@
+import Bridges
+import SwifQL
+
+final class Fortune: Table, Schemable {
+    @Column("id")
+    var id: Int32?
+    
+    @Column("message")
+    var message: String
+    
+    init() {}
+    init(id: Int32, message: String) {
+        self.id = id
+        self.message = message
+    }
+}
+

+ 11 - 0
frameworks/Swift/vapor/vapor-swifql/Sources/Models/World.swift

@@ -0,0 +1,11 @@
+import Bridges
+
+final class World: Table {
+    @Column("id")
+    var id: Int32?
+    
+    @Column("randomnumber")
+    var randomnumber: Int
+    
+    init() {}
+}

+ 128 - 0
frameworks/Swift/vapor/vapor-swifql/Sources/configure.swift

@@ -0,0 +1,128 @@
+
+import IkigaJSON
+import Leaf
+import PostgresBridge
+import Vapor
+import VaporBridges
+
+
+extension DatabaseHost {
+    public static var DbHost: DatabaseHost {
+        return .init(
+            hostname: "tfb-database",
+            port: 5432,
+            username: "benchmarkdbuser",
+            password: "benchmarkdbpass"
+        )
+    }
+}
+
+extension DatabaseIdentifier {
+    public static var Db: DatabaseIdentifier {
+        .init(name: "hello_world", host: .DbHost, maxConnectionsPerEventLoop: 2000 / (System.coreCount * 2))
+    }
+}
+
+
+public func configure(_ app: Application) throws {
+    let decoder = IkigaJSONDecoder()
+    decoder.settings.dateDecodingStrategy = .iso8601
+    ContentConfiguration.global.use(decoder: decoder as ContentDecoder, for: .json)
+
+    var encoder = IkigaJSONEncoder()
+    encoder.settings.dateEncodingStrategy = .iso8601
+    ContentConfiguration.global.use(encoder: encoder as ContentEncoder, for: .json)
+
+    app.http.server.configuration.serverName = "Vapor"
+    app.logger.logLevel = .notice
+    app.logger.notice("💧 VAPOR")
+    app.logger.notice("System.coreCount: \(System.coreCount)")
+
+    app.views.use(.leaf)
+    
+    try routes(app)
+    
+    try app.run()
+}
+
+public func routes(_ app: Application) throws {
+    app.get { req async in 
+        "It works!"
+    }
+
+    app.get("plaintext") { req async in
+        "Hello, world!"
+    }
+
+    app.get("json") { req async in
+        ["message": "Hello, world!"]
+    }
+
+    app.get("db") { req async throws -> World in
+        guard let world: World = try await req.postgres.connection(to: .Db, { conn in
+            World.select
+                .where(\World.$id == Int.random(in: 1...10_000))
+                .execute(on: conn)
+                .first(decoding: World.self) 
+        }).get() else {
+            throw Abort(.notFound)
+        }
+        return world
+    }
+    
+    app.get("queries") { req async throws -> [World] in
+        let queries: Int = (req.query["queries"] ?? 1).bounded(to: 1...500)
+
+        var worlds: [World] = []
+
+        for _ in queries {
+            guard let world: World = try await req.postgres.connection(to: .Db, { conn in
+                World.select
+                    .where(\World.$id == Int.random(in: 1...10_000))
+                    .execute(on: conn)
+                    .first(decoding: World.self) 
+            }).get() else {
+                throw Abort(.notFound)
+            }
+
+        worlds.append(world)
+        }
+        return worlds
+    }
+
+    app.get("updates") { req async throws -> [World] in
+        let queries = (req.query["queries"] ?? 1).bounded(to: 1...500)
+
+        var worlds: [World] = []
+
+        for _ in queries {
+            let world = try await req.postgres.connection(to: .Db, { conn in
+                    World.select.where(\World.$id == Int.random(in: 1...10_000)).execute(on: conn).first(decoding: World.self).flatMap { world in
+                        world!.randomnumber = .random(in: 1...10_000)
+                        return world!.update(on: \.$id, on: conn)
+                }
+            }).get()
+
+            worlds.append(world)
+        }
+
+        return worlds
+        
+    }
+
+    app.get("fortunes") { req async throws -> View in 
+        var fortunes: [Fortune] = try await req.postgres.connection(to: .Db, {conn in 
+            Fortune.select.execute(on: conn).all(decoding: Fortune.self)
+        })
+        .get()
+
+        fortunes.append(Fortune(id: 0, message: "Additional fortune added at request time."))
+
+        fortunes.sort(by: {
+            $0.message < $1.message
+        })
+
+        return try await req.view.render("fortune", ["fortunes": fortunes])
+    }
+}
+

+ 16 - 0
frameworks/Swift/vapor/vapor-swifql/Sources/main.swift

@@ -0,0 +1,16 @@
+import Vapor
+import PostgresBridge
+import Logging
+
+@main
+enum App {
+    static func main() throws {
+        var env = try Environment.detect()
+        try LoggingSystem.bootstrap(from: &env)
+
+        let app = Application(env)
+        defer { app.shutdown() }
+
+        try configure(app)
+    }
+}