From f227d322347162ddbdda86f5c9627dd74df3ed54 Mon Sep 17 00:00:00 2001 From: Tibor Bodecs Date: Mon, 13 Jan 2025 22:45:06 +0100 Subject: [PATCH 1/4] update snippets location, fix grpc snippets --- .gitignore | 1 + Package.resolved | 14 +- Package.swift | 6 +- Snippets/{ => 2024}/HummingbirdApp.swift | 0 .../HummingbirdRequestContext.swift | 0 .../{ => 2024}/advanced-async-sequences.swift | 0 Snippets/{ => 2024}/ahc_download.swift | 0 Snippets/{ => 2024}/ahc_json.swift | 0 Snippets/{ => 2024}/ahc_request.swift | 0 Snippets/{ => 2024}/ahc_setup.swift | 0 .../.DS_Store | Bin 0 -> 6148 bytes ...protocol-buffers-and-grpc-with-swift.swift | 1344 +++++++++++++++++ .../todo_messages.proto | 0 .../todo_service.proto | 0 .../building-swiftnio-clients-01.swift | 0 Snippets/{ => 2024}/environment.swift | 0 ...etting-started-concurrency-buy-books.swift | 0 ...getting-started-concurrency-dispatch.swift | 0 ...started-concurrency-imagecache-locks.swift | 0 Snippets/{ => 2024}/hummingbird-2-proxy.swift | 2 +- .../hummingbird-2-routerbuilder.swift | 0 Snippets/{ => 2024}/hummingbird-2.swift | 0 Snippets/{ => 2024}/mongokitten-basics.swift | 0 .../{ => 2024}/realtime-mongodb-app.swift | 0 Snippets/{ => 2024}/shared-state.swift | 0 .../swiftpm-snippets/SnippetsExample_I.swift | 0 .../swiftpm-snippets/SnippetsExample_II.swift | 0 .../SnippetsExample_III.swift | 0 .../swiftpm-snippets/SnippetsExample_IV.swift | 0 .../swiftpm-snippets/SnippetsExample_V.swift | 0 .../{ => 2024}/using-swiftnio-channels.swift | 0 Snippets/{ => 2024}/websockets-app.swift | 0 ...bsockets-connection-manager-add-user.swift | 0 .../websockets-connection-manager-types.swift | 0 Snippets/{ => 2024}/websockets.swift | 0 .../working-with-udp-in-swiftnio.swift | 0 .../jwt-kit.swift | 0 .../TodoService.swift | 840 ----------- ...to-protocol-buffers-and-grpc-with-swift.md | 157 +- .../introduction-to-jwts-in-swift.md} | 0 40 files changed, 1391 insertions(+), 973 deletions(-) rename Snippets/{ => 2024}/HummingbirdApp.swift (100%) rename Snippets/{ => 2024}/HummingbirdRequestContext.swift (100%) rename Snippets/{ => 2024}/advanced-async-sequences.swift (100%) rename Snippets/{ => 2024}/ahc_download.swift (100%) rename Snippets/{ => 2024}/ahc_json.swift (100%) rename Snippets/{ => 2024}/ahc_request.swift (100%) rename Snippets/{ => 2024}/ahc_setup.swift (100%) create mode 100644 Snippets/2024/beginners-guide-to-protocol-buffers-and-grpc-with-swift/.DS_Store create mode 100644 Snippets/2024/beginners-guide-to-protocol-buffers-and-grpc-with-swift/beginners-guide-to-protocol-buffers-and-grpc-with-swift.swift rename Snippets/{ => 2024}/beginners-guide-to-protocol-buffers-and-grpc-with-swift/todo_messages.proto (100%) rename Snippets/{ => 2024}/beginners-guide-to-protocol-buffers-and-grpc-with-swift/todo_service.proto (100%) rename Snippets/{ => 2024}/building-swiftnio-clients-01.swift (100%) rename Snippets/{ => 2024}/environment.swift (100%) rename Snippets/{ => 2024}/getting-started-concurrency-buy-books.swift (100%) rename Snippets/{ => 2024}/getting-started-concurrency-dispatch.swift (100%) rename Snippets/{ => 2024}/getting-started-concurrency-imagecache-locks.swift (100%) rename Snippets/{ => 2024}/hummingbird-2-proxy.swift (98%) rename Snippets/{ => 2024}/hummingbird-2-routerbuilder.swift (100%) rename Snippets/{ => 2024}/hummingbird-2.swift (100%) rename Snippets/{ => 2024}/mongokitten-basics.swift (100%) rename Snippets/{ => 2024}/realtime-mongodb-app.swift (100%) rename Snippets/{ => 2024}/shared-state.swift (100%) rename Snippets/{ => 2024}/swiftpm-snippets/SnippetsExample_I.swift (100%) rename Snippets/{ => 2024}/swiftpm-snippets/SnippetsExample_II.swift (100%) rename Snippets/{ => 2024}/swiftpm-snippets/SnippetsExample_III.swift (100%) rename Snippets/{ => 2024}/swiftpm-snippets/SnippetsExample_IV.swift (100%) rename Snippets/{ => 2024}/swiftpm-snippets/SnippetsExample_V.swift (100%) rename Snippets/{ => 2024}/using-swiftnio-channels.swift (100%) rename Snippets/{ => 2024}/websockets-app.swift (100%) rename Snippets/{ => 2024}/websockets-connection-manager-add-user.swift (100%) rename Snippets/{ => 2024}/websockets-connection-manager-types.swift (100%) rename Snippets/{ => 2024}/websockets.swift (100%) rename Snippets/{ => 2024}/working-with-udp-in-swiftnio.swift (100%) rename Snippets/{ => 2025/introduction-to-jwts-in-swift}/jwt-kit.swift (100%) delete mode 100644 Snippets/beginners-guide-to-protocol-buffers-and-grpc-with-swift/TodoService.swift rename Sources/Articles/Documentation.docc/2025/{jwt-kit/jwt-kit.md => introduction-to-jwts-in-swift/introduction-to-jwts-in-swift.md} (100%) diff --git a/.gitignore b/.gitignore index ba7cf03..c6e8b31 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.DS_Store /.DS_Store /.swiftpm /.vscode diff --git a/Package.resolved b/Package.resolved index 0fb3d2c..b6f149e 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "39ee378a5452b7ecbf42ffb8e850cc3bb68bdddb2394e39ac26615ba789f3c5c", + "originHash" : "1e7038e38f57c124171d09107eef07a450c94d04865a9ad003a9d06b87a76bff", "pins" : [ { "identity" : "async-http-client", @@ -42,8 +42,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/grpc/grpc-swift.git", "state" : { - "revision" : "432c955d9781bfb4c20c813de042bc098d2277cf", - "version" : "2.0.0-alpha.1" + "revision" : "0696e0adf7e081ba7a757a4c87242a1aab524078", + "version" : "2.0.0-beta.2" } }, { @@ -51,8 +51,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/grpc/grpc-swift-nio-transport", "state" : { - "revision" : "0d1c61a501683a2acf5810b1ce1230e993c87b3f", - "version" : "1.0.0-alpha.1" + "revision" : "e97f97e399f62dbedadfa4e55e962577c313907d", + "version" : "1.0.0-beta.2" } }, { @@ -60,8 +60,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/grpc/grpc-swift-protobuf", "state" : { - "revision" : "424a4d8bd6f0c97500517b827a9d0e2c6a1df924", - "version" : "1.0.0-alpha.1" + "revision" : "3ed26dad8d43d7a11f0c6784a93db900e3cd4281", + "version" : "1.0.0-beta.2" } }, { diff --git a/Package.swift b/Package.swift index 6e233da..5d241d9 100644 --- a/Package.swift +++ b/Package.swift @@ -13,7 +13,7 @@ let package = Package( .package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.0.0"), .package(url: "https://github.com/hummingbird-project/hummingbird-auth.git", from: "2.0.2"), .package(url: "https://github.com/hummingbird-project/hummingbird-websocket.git", from: "2.0.0"), - .package(url: "https://github.com/hummingbird-project/swift-mustache.git", from: "2.0.0-beta.3"), + .package(url: "https://github.com/hummingbird-project/swift-mustache.git", from: "2.0.0"), .package(url: "https://github.com/hummingbird-project/swift-jobs.git", from: "1.0.0-beta.6"), .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.21.1"), .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"), @@ -22,8 +22,8 @@ let package = Package( .package(url: "https://github.com/apple/swift-log.git", from: "1.5.4"), .package(url: "https://github.com/apple/swift-http-types.git", from: "1.0.0"), .package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.5.0"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf", exact: "1.0.0-alpha.1"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport", exact: "1.0.0-alpha.1"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", exact: "1.0.0-beta.2"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", exact: "1.0.0-beta.2"), .package(url: "https://github.com/orlandos-nl/MongoKitten.git", from: "7.0.0"), .package(url: "https://github.com/vapor/jwt-kit.git", from: "5.0.0"), ], diff --git a/Snippets/HummingbirdApp.swift b/Snippets/2024/HummingbirdApp.swift similarity index 100% rename from Snippets/HummingbirdApp.swift rename to Snippets/2024/HummingbirdApp.swift diff --git a/Snippets/HummingbirdRequestContext.swift b/Snippets/2024/HummingbirdRequestContext.swift similarity index 100% rename from Snippets/HummingbirdRequestContext.swift rename to Snippets/2024/HummingbirdRequestContext.swift diff --git a/Snippets/advanced-async-sequences.swift b/Snippets/2024/advanced-async-sequences.swift similarity index 100% rename from Snippets/advanced-async-sequences.swift rename to Snippets/2024/advanced-async-sequences.swift diff --git a/Snippets/ahc_download.swift b/Snippets/2024/ahc_download.swift similarity index 100% rename from Snippets/ahc_download.swift rename to Snippets/2024/ahc_download.swift diff --git a/Snippets/ahc_json.swift b/Snippets/2024/ahc_json.swift similarity index 100% rename from Snippets/ahc_json.swift rename to Snippets/2024/ahc_json.swift diff --git a/Snippets/ahc_request.swift b/Snippets/2024/ahc_request.swift similarity index 100% rename from Snippets/ahc_request.swift rename to Snippets/2024/ahc_request.swift diff --git a/Snippets/ahc_setup.swift b/Snippets/2024/ahc_setup.swift similarity index 100% rename from Snippets/ahc_setup.swift rename to Snippets/2024/ahc_setup.swift diff --git a/Snippets/2024/beginners-guide-to-protocol-buffers-and-grpc-with-swift/.DS_Store b/Snippets/2024/beginners-guide-to-protocol-buffers-and-grpc-with-swift/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..68e18877b6ee41a9575017cbe92cf2c1657f37bd GIT binary patch literal 6148 zcmeHK%}c{D6i?i=Eko!*L2m)C1NXuFz)PtU?^g7nGFw`(IGeF{^e_g!>p$c_;@{(Y zNj5g@Sw!B0>pT%9qT*jCU4UwZ#BWSL5ZJA&W2o2_#vXB)lVR2N} zO!OB`xORtK!{{fs8#G%q@xFTT}iZtqx@W3{ZO;6;`}5f-!D52p_} zx|MPqE%h+EOOtZs?p?~P2-7T?s)RI7Ams5r&0<;la+bxp$_=!`acpPg9?s{ze!nex zXM;ss%)6cQL0g>kPZtZv-ak6NxEZ~U-!l2G`Q*3-lwA$0{$r()LJSZC!~iis46HF= zE)Ki3W+RmtAO?s5WB|_xVbC*JX;eoCbgBdZY=T({=vYf&t}y5stTciLgzHp5oytv# z!F4*=g^BYFRvL9Wh| zhyh~YpE1Cj!(iycrp(>?V|#el3TV&JP%y4S1qAesM*s})963-(decoder: inout D) throws { + // Load everything into unknown fields + while try decoder.nextFieldNumber() != nil {} + } + + func traverse(visitor: inout V) throws { + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Todos_Empty, rhs: Todos_Empty) -> Bool { + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Todos_TodoID: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".TodoID" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "todoID"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.todoID) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.todoID.isEmpty { + try visitor.visitSingularStringField(value: self.todoID, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Todos_TodoID, rhs: Todos_TodoID) -> Bool { + if lhs.todoID != rhs.todoID {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Todos_Todo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".Todo" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "todoID"), + 2: .same(proto: "title"), + 3: .same(proto: "completed"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self._todoID) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.title) }() + case 3: try { try decoder.decodeSingularBoolField(value: &self.completed) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._todoID { + try visitor.visitSingularStringField(value: v, fieldNumber: 1) + } }() + if !self.title.isEmpty { + try visitor.visitSingularStringField(value: self.title, fieldNumber: 2) + } + if self.completed != false { + try visitor.visitSingularBoolField(value: self.completed, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Todos_Todo, rhs: Todos_Todo) -> Bool { + if lhs._todoID != rhs._todoID {return false} + if lhs.title != rhs.title {return false} + if lhs.completed != rhs.completed {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Todos_TodoList: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".TodoList" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "todos"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeRepeatedMessageField(value: &self.todos) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.todos.isEmpty { + try visitor.visitRepeatedMessageField(value: self.todos, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Todos_TodoList, rhs: Todos_TodoList) -> Bool { + if lhs.todos != rhs.todos {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the gRPC Swift generator plugin for the protocol buffer compiler. +// Source: todo_service.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/grpc/grpc-swift + +import GRPCCore +import GRPCProtobuf + +// MARK: - todos.TodoService + +/// Namespace containing generated types for the "todos.TodoService" service. +internal enum Todos_TodoService { + /// Service descriptor for the "todos.TodoService" service. + internal static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: "todos.TodoService") + /// Namespace for method metadata. + internal enum Method { + /// Namespace for "FetchTodos" metadata. + internal enum FetchTodos { + /// Request type for "FetchTodos". + internal typealias Input = Todos_Empty + /// Response type for "FetchTodos". + internal typealias Output = Todos_TodoList + /// Descriptor for "FetchTodos". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "todos.TodoService"), + method: "FetchTodos" + ) + } + /// Namespace for "CreateTodo" metadata. + internal enum CreateTodo { + /// Request type for "CreateTodo". + internal typealias Input = Todos_Todo + /// Response type for "CreateTodo". + internal typealias Output = Todos_Todo + /// Descriptor for "CreateTodo". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "todos.TodoService"), + method: "CreateTodo" + ) + } + /// Namespace for "DeleteTodo" metadata. + internal enum DeleteTodo { + /// Request type for "DeleteTodo". + internal typealias Input = Todos_TodoID + /// Response type for "DeleteTodo". + internal typealias Output = Todos_Empty + /// Descriptor for "DeleteTodo". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "todos.TodoService"), + method: "DeleteTodo" + ) + } + /// Namespace for "CompleteTodo" metadata. + internal enum CompleteTodo { + /// Request type for "CompleteTodo". + internal typealias Input = Todos_TodoID + /// Response type for "CompleteTodo". + internal typealias Output = Todos_Todo + /// Descriptor for "CompleteTodo". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "todos.TodoService"), + method: "CompleteTodo" + ) + } + /// Descriptors for all methods in the "todos.TodoService" service. + internal static let descriptors: [GRPCCore.MethodDescriptor] = [ + FetchTodos.descriptor, + CreateTodo.descriptor, + DeleteTodo.descriptor, + CompleteTodo.descriptor + ] + } +} + +extension GRPCCore.ServiceDescriptor { + /// Service descriptor for the "todos.TodoService" service. + internal static let todos_TodoService = GRPCCore.ServiceDescriptor(fullyQualifiedService: "todos.TodoService") +} + +// MARK: todos.TodoService (server) + +extension Todos_TodoService { + /// Streaming variant of the service protocol for the "todos.TodoService" service. + /// + /// This protocol is the lowest-level of the service protocols generated for this service + /// giving you the most flexibility over the implementation of your service. This comes at + /// the cost of more verbose and less strict APIs. Each RPC requires you to implement it in + /// terms of a request stream and response stream. Where only a single request or response + /// message is expected, you are responsible for enforcing this invariant is maintained. + /// + /// Where possible, prefer using the stricter, less-verbose ``ServiceProtocol`` + /// or ``SimpleServiceProtocol`` instead. + /// + /// > Source IDL Documentation: + /// > + /// > 2. + internal protocol StreamingServiceProtocol: GRPCCore.RegistrableRPCService { + /// Handle the "FetchTodos" method. + /// + /// > Source IDL Documentation: + /// > + /// > 3. + /// + /// - Parameters: + /// - request: A streaming request of `Todos_Empty` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Todos_TodoList` messages. + func fetchTodos( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "CreateTodo" method. + /// + /// > Source IDL Documentation: + /// > + /// > 4. + /// + /// - Parameters: + /// - request: A streaming request of `Todos_Todo` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Todos_Todo` messages. + func createTodo( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "DeleteTodo" method. + /// + /// > Source IDL Documentation: + /// > + /// > 5. + /// + /// - Parameters: + /// - request: A streaming request of `Todos_TodoID` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Todos_Empty` messages. + func deleteTodo( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "CompleteTodo" method. + /// + /// > Source IDL Documentation: + /// > + /// > 6. + /// + /// - Parameters: + /// - request: A streaming request of `Todos_TodoID` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Todos_Todo` messages. + func completeTodo( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + } + + /// Service protocol for the "todos.TodoService" service. + /// + /// This protocol is higher level than ``StreamingServiceProtocol`` but lower level than + /// the ``SimpleServiceProtocol``, it provides access to request and response metadata and + /// trailing response metadata. If you don't need these then consider using + /// the ``SimpleServiceProtocol``. If you need fine grained control over your RPCs then + /// use ``StreamingServiceProtocol``. + /// + /// > Source IDL Documentation: + /// > + /// > 2. + internal protocol ServiceProtocol: Todos_TodoService.StreamingServiceProtocol { + /// Handle the "FetchTodos" method. + /// + /// > Source IDL Documentation: + /// > + /// > 3. + /// + /// - Parameters: + /// - request: A request containing a single `Todos_Empty` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `Todos_TodoList` message. + func fetchTodos( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "CreateTodo" method. + /// + /// > Source IDL Documentation: + /// > + /// > 4. + /// + /// - Parameters: + /// - request: A request containing a single `Todos_Todo` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `Todos_Todo` message. + func createTodo( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "DeleteTodo" method. + /// + /// > Source IDL Documentation: + /// > + /// > 5. + /// + /// - Parameters: + /// - request: A request containing a single `Todos_TodoID` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `Todos_Empty` message. + func deleteTodo( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "CompleteTodo" method. + /// + /// > Source IDL Documentation: + /// > + /// > 6. + /// + /// - Parameters: + /// - request: A request containing a single `Todos_TodoID` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `Todos_Todo` message. + func completeTodo( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + } + + /// Simple service protocol for the "todos.TodoService" service. + /// + /// This is the highest level protocol for the service. The API is the easiest to use but + /// doesn't provide access to request or response metadata. If you need access to these + /// then use ``ServiceProtocol`` instead. + /// + /// > Source IDL Documentation: + /// > + /// > 2. + internal protocol SimpleServiceProtocol: Todos_TodoService.ServiceProtocol { + /// Handle the "FetchTodos" method. + /// + /// > Source IDL Documentation: + /// > + /// > 3. + /// + /// - Parameters: + /// - request: A `Todos_Empty` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `Todos_TodoList` to respond with. + func fetchTodos( + request: Todos_Empty, + context: GRPCCore.ServerContext + ) async throws -> Todos_TodoList + + /// Handle the "CreateTodo" method. + /// + /// > Source IDL Documentation: + /// > + /// > 4. + /// + /// - Parameters: + /// - request: A `Todos_Todo` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `Todos_Todo` to respond with. + func createTodo( + request: Todos_Todo, + context: GRPCCore.ServerContext + ) async throws -> Todos_Todo + + /// Handle the "DeleteTodo" method. + /// + /// > Source IDL Documentation: + /// > + /// > 5. + /// + /// - Parameters: + /// - request: A `Todos_TodoID` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `Todos_Empty` to respond with. + func deleteTodo( + request: Todos_TodoID, + context: GRPCCore.ServerContext + ) async throws -> Todos_Empty + + /// Handle the "CompleteTodo" method. + /// + /// > Source IDL Documentation: + /// > + /// > 6. + /// + /// - Parameters: + /// - request: A `Todos_TodoID` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `Todos_Todo` to respond with. + func completeTodo( + request: Todos_TodoID, + context: GRPCCore.ServerContext + ) async throws -> Todos_Todo + } +} + +// Default implementation of 'registerMethods(with:)'. +extension Todos_TodoService.StreamingServiceProtocol { + internal func registerMethods(with router: inout GRPCCore.RPCRouter) { + router.registerHandler( + forMethod: Todos_TodoService.Method.FetchTodos.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.fetchTodos( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Todos_TodoService.Method.CreateTodo.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.createTodo( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Todos_TodoService.Method.DeleteTodo.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.deleteTodo( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Todos_TodoService.Method.CompleteTodo.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.completeTodo( + request: request, + context: context + ) + } + ) + } +} + +// Default implementation of streaming methods from 'StreamingServiceProtocol'. +extension Todos_TodoService.ServiceProtocol { + internal func fetchTodos( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.fetchTodos( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } + + internal func createTodo( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.createTodo( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } + + internal func deleteTodo( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.deleteTodo( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } + + internal func completeTodo( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.completeTodo( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } +} + +// Default implementation of methods from 'ServiceProtocol'. +extension Todos_TodoService.SimpleServiceProtocol { + internal func fetchTodos( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.fetchTodos( + request: request.message, + context: context + ), + metadata: [:] + ) + } + + internal func createTodo( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.createTodo( + request: request.message, + context: context + ), + metadata: [:] + ) + } + + internal func deleteTodo( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.deleteTodo( + request: request.message, + context: context + ), + metadata: [:] + ) + } + + internal func completeTodo( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.completeTodo( + request: request.message, + context: context + ), + metadata: [:] + ) + } +} + +// MARK: todos.TodoService (client) + +extension Todos_TodoService { + /// Generated client protocol for the "todos.TodoService" service. + /// + /// You don't need to implement this protocol directly, use the generated + /// implementation, ``Client``. + /// + /// > Source IDL Documentation: + /// > + /// > 2. + internal protocol ClientProtocol: Sendable { + /// Call the "FetchTodos" method. + /// + /// > Source IDL Documentation: + /// > + /// > 3. + /// + /// - Parameters: + /// - request: A request containing a single `Todos_Empty` message. + /// - serializer: A serializer for `Todos_Empty` messages. + /// - deserializer: A deserializer for `Todos_TodoList` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func fetchTodos( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "CreateTodo" method. + /// + /// > Source IDL Documentation: + /// > + /// > 4. + /// + /// - Parameters: + /// - request: A request containing a single `Todos_Todo` message. + /// - serializer: A serializer for `Todos_Todo` messages. + /// - deserializer: A deserializer for `Todos_Todo` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func createTodo( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "DeleteTodo" method. + /// + /// > Source IDL Documentation: + /// > + /// > 5. + /// + /// - Parameters: + /// - request: A request containing a single `Todos_TodoID` message. + /// - serializer: A serializer for `Todos_TodoID` messages. + /// - deserializer: A deserializer for `Todos_Empty` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func deleteTodo( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "CompleteTodo" method. + /// + /// > Source IDL Documentation: + /// > + /// > 6. + /// + /// - Parameters: + /// - request: A request containing a single `Todos_TodoID` message. + /// - serializer: A serializer for `Todos_TodoID` messages. + /// - deserializer: A deserializer for `Todos_Todo` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func completeTodo( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + } + + /// Generated client for the "todos.TodoService" service. + /// + /// The ``Client`` provides an implementation of ``ClientProtocol`` which wraps + /// a `GRPCCore.GRPCCClient`. The underlying `GRPCClient` provides the long-lived + /// means of communication with the remote peer. + /// + /// > Source IDL Documentation: + /// > + /// > 2. + internal struct Client: ClientProtocol { + private let client: GRPCCore.GRPCClient + + /// Creates a new client wrapping the provided `GRPCCore.GRPCClient`. + /// + /// - Parameters: + /// - client: A `GRPCCore.GRPCClient` providing a communication channel to the service. + internal init(wrapping client: GRPCCore.GRPCClient) { + self.client = client + } + + /// Call the "FetchTodos" method. + /// + /// > Source IDL Documentation: + /// > + /// > 3. + /// + /// - Parameters: + /// - request: A request containing a single `Todos_Empty` message. + /// - serializer: A serializer for `Todos_Empty` messages. + /// - deserializer: A deserializer for `Todos_TodoList` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func fetchTodos( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Todos_TodoService.Method.FetchTodos.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "CreateTodo" method. + /// + /// > Source IDL Documentation: + /// > + /// > 4. + /// + /// - Parameters: + /// - request: A request containing a single `Todos_Todo` message. + /// - serializer: A serializer for `Todos_Todo` messages. + /// - deserializer: A deserializer for `Todos_Todo` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func createTodo( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Todos_TodoService.Method.CreateTodo.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "DeleteTodo" method. + /// + /// > Source IDL Documentation: + /// > + /// > 5. + /// + /// - Parameters: + /// - request: A request containing a single `Todos_TodoID` message. + /// - serializer: A serializer for `Todos_TodoID` messages. + /// - deserializer: A deserializer for `Todos_Empty` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func deleteTodo( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Todos_TodoService.Method.DeleteTodo.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "CompleteTodo" method. + /// + /// > Source IDL Documentation: + /// > + /// > 6. + /// + /// - Parameters: + /// - request: A request containing a single `Todos_TodoID` message. + /// - serializer: A serializer for `Todos_TodoID` messages. + /// - deserializer: A deserializer for `Todos_Todo` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func completeTodo( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Todos_TodoService.Method.CompleteTodo.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + } +} + +// Helpers providing default arguments to 'ClientProtocol' methods. +extension Todos_TodoService.ClientProtocol { + /// Call the "FetchTodos" method. + /// + /// > Source IDL Documentation: + /// > + /// > 3. + /// + /// - Parameters: + /// - request: A request containing a single `Todos_Empty` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func fetchTodos( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.fetchTodos( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "CreateTodo" method. + /// + /// > Source IDL Documentation: + /// > + /// > 4. + /// + /// - Parameters: + /// - request: A request containing a single `Todos_Todo` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func createTodo( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.createTodo( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "DeleteTodo" method. + /// + /// > Source IDL Documentation: + /// > + /// > 5. + /// + /// - Parameters: + /// - request: A request containing a single `Todos_TodoID` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func deleteTodo( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.deleteTodo( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "CompleteTodo" method. + /// + /// > Source IDL Documentation: + /// > + /// > 6. + /// + /// - Parameters: + /// - request: A request containing a single `Todos_TodoID` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func completeTodo( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.completeTodo( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } +} + +// Helpers providing sugared APIs for 'ClientProtocol' methods. +extension Todos_TodoService.ClientProtocol { + /// Call the "FetchTodos" method. + /// + /// > Source IDL Documentation: + /// > + /// > 3. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func fetchTodos( + _ message: Todos_Empty, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.fetchTodos( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "CreateTodo" method. + /// + /// > Source IDL Documentation: + /// > + /// > 4. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func createTodo( + _ message: Todos_Todo, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.createTodo( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "DeleteTodo" method. + /// + /// > Source IDL Documentation: + /// > + /// > 5. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func deleteTodo( + _ message: Todos_TodoID, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.deleteTodo( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "CompleteTodo" method. + /// + /// > Source IDL Documentation: + /// > + /// > 6. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func completeTodo( + _ message: Todos_TodoID, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.completeTodo( + request: request, + options: options, + onResponse: handleResponse + ) + } +} + +// snippet.todo_service_protocol +actor TodoService: Todos_TodoService.ServiceProtocol { + + var todos: [Todos_Todo] + + init( + todos: [Todos_Todo] = [] + ) { + self.todos = todos + } + + func createTodo( + request: ServerRequest, + context: ServerContext + ) async throws -> ServerResponse { + todos.append(request.message) + return .init(message: request.message) + } + + func fetchTodos( + request: ServerRequest, + context: ServerContext + ) async throws -> ServerResponse { + var result = Todos_TodoList() + result.todos = todos + return .init(message: result) + } + + func completeTodo( + request: ServerRequest, + context: ServerContext + ) async throws -> ServerResponse { + guard + var todo = todos.first(where: { $0.todoID == request.message.todoID }) + else { + return .init( + error: RPCError.init( + code: .notFound, + message: "Todo not found." + ) + ) + } + todo.completed = true + todos = todos.filter { $0.todoID != request.message.todoID } + todos.append(todo) + return .init(message: todo) + + } + + func deleteTodo( + request: ServerRequest, + context: ServerContext + ) async throws -> ServerResponse { + guard + let todo = todos.first(where: { $0.todoID == request.message.todoID }) + else { + return .init( + error: RPCError.init( + code: .notFound, + message: "Todo not found." + ) + ) + } + todos = todos.filter { $0.todoID != todo.todoID } + return .init(message: .init()) + } +} +// snippet.end + +// snippet.app_entrypoint +import ArgumentParser +import GRPCNIOTransportHTTP2 +import GRPCProtobuf + +@main +struct Entrypoint: AsyncParsableCommand { + + @Option(name: .shortAndLong) + var hostname: String = "127.0.0.1" + + @Option(name: .shortAndLong) + var port: Int = 1234 + + func run() async throws { + // 1. + let server = GRPCServer( + transport: .http2NIOPosix( + address: .ipv4(host: hostname, port: port), + transportSecurity: .plaintext, + config: .defaults + ), + services: [ + // 2. + TodoService() + ] + ) + + // 3. + try await withThrowingDiscardingTaskGroup { group in + group.addTask { try await server.serve() } + + // 4. + if let address = try await server.listeningAddress { + print("gRPC server listening on \(address)") + } + } + } +} +// snippet.end diff --git a/Snippets/beginners-guide-to-protocol-buffers-and-grpc-with-swift/todo_messages.proto b/Snippets/2024/beginners-guide-to-protocol-buffers-and-grpc-with-swift/todo_messages.proto similarity index 100% rename from Snippets/beginners-guide-to-protocol-buffers-and-grpc-with-swift/todo_messages.proto rename to Snippets/2024/beginners-guide-to-protocol-buffers-and-grpc-with-swift/todo_messages.proto diff --git a/Snippets/beginners-guide-to-protocol-buffers-and-grpc-with-swift/todo_service.proto b/Snippets/2024/beginners-guide-to-protocol-buffers-and-grpc-with-swift/todo_service.proto similarity index 100% rename from Snippets/beginners-guide-to-protocol-buffers-and-grpc-with-swift/todo_service.proto rename to Snippets/2024/beginners-guide-to-protocol-buffers-and-grpc-with-swift/todo_service.proto diff --git a/Snippets/building-swiftnio-clients-01.swift b/Snippets/2024/building-swiftnio-clients-01.swift similarity index 100% rename from Snippets/building-swiftnio-clients-01.swift rename to Snippets/2024/building-swiftnio-clients-01.swift diff --git a/Snippets/environment.swift b/Snippets/2024/environment.swift similarity index 100% rename from Snippets/environment.swift rename to Snippets/2024/environment.swift diff --git a/Snippets/getting-started-concurrency-buy-books.swift b/Snippets/2024/getting-started-concurrency-buy-books.swift similarity index 100% rename from Snippets/getting-started-concurrency-buy-books.swift rename to Snippets/2024/getting-started-concurrency-buy-books.swift diff --git a/Snippets/getting-started-concurrency-dispatch.swift b/Snippets/2024/getting-started-concurrency-dispatch.swift similarity index 100% rename from Snippets/getting-started-concurrency-dispatch.swift rename to Snippets/2024/getting-started-concurrency-dispatch.swift diff --git a/Snippets/getting-started-concurrency-imagecache-locks.swift b/Snippets/2024/getting-started-concurrency-imagecache-locks.swift similarity index 100% rename from Snippets/getting-started-concurrency-imagecache-locks.swift rename to Snippets/2024/getting-started-concurrency-imagecache-locks.swift diff --git a/Snippets/hummingbird-2-proxy.swift b/Snippets/2024/hummingbird-2-proxy.swift similarity index 98% rename from Snippets/hummingbird-2-proxy.swift rename to Snippets/2024/hummingbird-2-proxy.swift index a4d1369..d7c28cc 100644 --- a/Snippets/hummingbird-2-proxy.swift +++ b/Snippets/2024/hummingbird-2-proxy.swift @@ -21,7 +21,7 @@ func forward( clientRequest.headers = .init(request.headers) // 2. - let contentLength = if let header = request.headers[.contentLength], let value = Int(header) { + let contentLength = if let header = request.headers[.contentLength], let value = Int64(header) { HTTPClientRequest.Body.Length.known(value) } else { HTTPClientRequest.Body.Length.unknown diff --git a/Snippets/hummingbird-2-routerbuilder.swift b/Snippets/2024/hummingbird-2-routerbuilder.swift similarity index 100% rename from Snippets/hummingbird-2-routerbuilder.swift rename to Snippets/2024/hummingbird-2-routerbuilder.swift diff --git a/Snippets/hummingbird-2.swift b/Snippets/2024/hummingbird-2.swift similarity index 100% rename from Snippets/hummingbird-2.swift rename to Snippets/2024/hummingbird-2.swift diff --git a/Snippets/mongokitten-basics.swift b/Snippets/2024/mongokitten-basics.swift similarity index 100% rename from Snippets/mongokitten-basics.swift rename to Snippets/2024/mongokitten-basics.swift diff --git a/Snippets/realtime-mongodb-app.swift b/Snippets/2024/realtime-mongodb-app.swift similarity index 100% rename from Snippets/realtime-mongodb-app.swift rename to Snippets/2024/realtime-mongodb-app.swift diff --git a/Snippets/shared-state.swift b/Snippets/2024/shared-state.swift similarity index 100% rename from Snippets/shared-state.swift rename to Snippets/2024/shared-state.swift diff --git a/Snippets/swiftpm-snippets/SnippetsExample_I.swift b/Snippets/2024/swiftpm-snippets/SnippetsExample_I.swift similarity index 100% rename from Snippets/swiftpm-snippets/SnippetsExample_I.swift rename to Snippets/2024/swiftpm-snippets/SnippetsExample_I.swift diff --git a/Snippets/swiftpm-snippets/SnippetsExample_II.swift b/Snippets/2024/swiftpm-snippets/SnippetsExample_II.swift similarity index 100% rename from Snippets/swiftpm-snippets/SnippetsExample_II.swift rename to Snippets/2024/swiftpm-snippets/SnippetsExample_II.swift diff --git a/Snippets/swiftpm-snippets/SnippetsExample_III.swift b/Snippets/2024/swiftpm-snippets/SnippetsExample_III.swift similarity index 100% rename from Snippets/swiftpm-snippets/SnippetsExample_III.swift rename to Snippets/2024/swiftpm-snippets/SnippetsExample_III.swift diff --git a/Snippets/swiftpm-snippets/SnippetsExample_IV.swift b/Snippets/2024/swiftpm-snippets/SnippetsExample_IV.swift similarity index 100% rename from Snippets/swiftpm-snippets/SnippetsExample_IV.swift rename to Snippets/2024/swiftpm-snippets/SnippetsExample_IV.swift diff --git a/Snippets/swiftpm-snippets/SnippetsExample_V.swift b/Snippets/2024/swiftpm-snippets/SnippetsExample_V.swift similarity index 100% rename from Snippets/swiftpm-snippets/SnippetsExample_V.swift rename to Snippets/2024/swiftpm-snippets/SnippetsExample_V.swift diff --git a/Snippets/using-swiftnio-channels.swift b/Snippets/2024/using-swiftnio-channels.swift similarity index 100% rename from Snippets/using-swiftnio-channels.swift rename to Snippets/2024/using-swiftnio-channels.swift diff --git a/Snippets/websockets-app.swift b/Snippets/2024/websockets-app.swift similarity index 100% rename from Snippets/websockets-app.swift rename to Snippets/2024/websockets-app.swift diff --git a/Snippets/websockets-connection-manager-add-user.swift b/Snippets/2024/websockets-connection-manager-add-user.swift similarity index 100% rename from Snippets/websockets-connection-manager-add-user.swift rename to Snippets/2024/websockets-connection-manager-add-user.swift diff --git a/Snippets/websockets-connection-manager-types.swift b/Snippets/2024/websockets-connection-manager-types.swift similarity index 100% rename from Snippets/websockets-connection-manager-types.swift rename to Snippets/2024/websockets-connection-manager-types.swift diff --git a/Snippets/websockets.swift b/Snippets/2024/websockets.swift similarity index 100% rename from Snippets/websockets.swift rename to Snippets/2024/websockets.swift diff --git a/Snippets/working-with-udp-in-swiftnio.swift b/Snippets/2024/working-with-udp-in-swiftnio.swift similarity index 100% rename from Snippets/working-with-udp-in-swiftnio.swift rename to Snippets/2024/working-with-udp-in-swiftnio.swift diff --git a/Snippets/jwt-kit.swift b/Snippets/2025/introduction-to-jwts-in-swift/jwt-kit.swift similarity index 100% rename from Snippets/jwt-kit.swift rename to Snippets/2025/introduction-to-jwts-in-swift/jwt-kit.swift diff --git a/Snippets/beginners-guide-to-protocol-buffers-and-grpc-with-swift/TodoService.swift b/Snippets/beginners-guide-to-protocol-buffers-and-grpc-with-swift/TodoService.swift deleted file mode 100644 index 7f44d3e..0000000 --- a/Snippets/beginners-guide-to-protocol-buffers-and-grpc-with-swift/TodoService.swift +++ /dev/null @@ -1,840 +0,0 @@ -import GRPCNIOTransportHTTP2 -import GRPCProtobuf - -// snippet.hide -import GRPCCore -import SwiftProtobuf -// DO NOT EDIT. -// swift-format-ignore-file -// swiftlint:disable all -// -// Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: todo_messages.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/apple/swift-protobuf/ - -/// todo_messages.proto - -import SwiftProtobuf - -// If the compiler emits an error on this type, it is because this file -// was generated by a version of the `protoc` Swift plug-in that is -// incompatible with the version of SwiftProtobuf to which you are linking. -// Please ensure that you are building against the same version of the API -// that was used to generate this file. -fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { - struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} - typealias Version = _2 -} - -/// 2. -struct Todos_Empty: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -/// 3. -struct Todos_TodoID: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var todoID: String = String() - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -/// 4. -struct Todos_Todo: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var todoID: String { - get {return _todoID ?? String()} - set {_todoID = newValue} - } - /// Returns true if `todoID` has been explicitly set. - var hasTodoID: Bool {return self._todoID != nil} - /// Clears the value of `todoID`. Subsequent reads from it will return its default value. - mutating func clearTodoID() {self._todoID = nil} - - var title: String = String() - - var completed: Bool = false - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - - fileprivate var _todoID: String? = nil -} - -/// 5. -struct Todos_TodoList: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var todos: [Todos_Todo] = [] - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -// MARK: - Code below here is support for the SwiftProtobuf runtime. - -fileprivate let _protobuf_package = "todos" - -extension Todos_Empty: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".Empty" - static let _protobuf_nameMap = SwiftProtobuf._NameMap() - - mutating func decodeMessage(decoder: inout D) throws { - // Load everything into unknown fields - while try decoder.nextFieldNumber() != nil {} - } - - func traverse(visitor: inout V) throws { - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Todos_Empty, rhs: Todos_Empty) -> Bool { - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Todos_TodoID: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".TodoID" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "todoID"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.todoID) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if !self.todoID.isEmpty { - try visitor.visitSingularStringField(value: self.todoID, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Todos_TodoID, rhs: Todos_TodoID) -> Bool { - if lhs.todoID != rhs.todoID {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Todos_Todo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".Todo" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "todoID"), - 2: .same(proto: "title"), - 3: .same(proto: "completed"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self._todoID) }() - case 2: try { try decoder.decodeSingularStringField(value: &self.title) }() - case 3: try { try decoder.decodeSingularBoolField(value: &self.completed) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - try { if let v = self._todoID { - try visitor.visitSingularStringField(value: v, fieldNumber: 1) - } }() - if !self.title.isEmpty { - try visitor.visitSingularStringField(value: self.title, fieldNumber: 2) - } - if self.completed != false { - try visitor.visitSingularBoolField(value: self.completed, fieldNumber: 3) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Todos_Todo, rhs: Todos_Todo) -> Bool { - if lhs._todoID != rhs._todoID {return false} - if lhs.title != rhs.title {return false} - if lhs.completed != rhs.completed {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Todos_TodoList: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".TodoList" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "todos"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeRepeatedMessageField(value: &self.todos) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if !self.todos.isEmpty { - try visitor.visitRepeatedMessageField(value: self.todos, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Todos_TodoList, rhs: Todos_TodoList) -> Bool { - if lhs.todos != rhs.todos {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} -/// todo.proto - -// DO NOT EDIT. -// swift-format-ignore-file -// -// Generated by the gRPC Swift generator plugin for the protocol buffer compiler. -// Source: todo_service.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/grpc/grpc-swift - -import GRPCCore -import GRPCProtobuf - -internal enum Todos_TodoService { - internal static let descriptor = GRPCCore.ServiceDescriptor.todos_TodoService - internal enum Method { - internal enum FetchTodos { - internal typealias Input = Todos_Empty - internal typealias Output = Todos_TodoList - internal static let descriptor = GRPCCore.MethodDescriptor( - service: Todos_TodoService.descriptor.fullyQualifiedService, - method: "FetchTodos" - ) - } - internal enum CreateTodo { - internal typealias Input = Todos_Todo - internal typealias Output = Todos_Todo - internal static let descriptor = GRPCCore.MethodDescriptor( - service: Todos_TodoService.descriptor.fullyQualifiedService, - method: "CreateTodo" - ) - } - internal enum DeleteTodo { - internal typealias Input = Todos_TodoID - internal typealias Output = Todos_Empty - internal static let descriptor = GRPCCore.MethodDescriptor( - service: Todos_TodoService.descriptor.fullyQualifiedService, - method: "DeleteTodo" - ) - } - internal enum CompleteTodo { - internal typealias Input = Todos_TodoID - internal typealias Output = Todos_Todo - internal static let descriptor = GRPCCore.MethodDescriptor( - service: Todos_TodoService.descriptor.fullyQualifiedService, - method: "CompleteTodo" - ) - } - internal static let descriptors: [GRPCCore.MethodDescriptor] = [ - FetchTodos.descriptor, - CreateTodo.descriptor, - DeleteTodo.descriptor, - CompleteTodo.descriptor - ] - } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias StreamingServiceProtocol = Todos_TodoService_StreamingServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias ServiceProtocol = Todos_TodoService_ServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias ClientProtocol = Todos_TodoService_ClientProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias Client = Todos_TodoService_Client -} - -extension GRPCCore.ServiceDescriptor { - internal static let todos_TodoService = Self( - package: "todos", - service: "TodoService" - ) -} - -/// 2. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -internal protocol Todos_TodoService_StreamingServiceProtocol: GRPCCore.RegistrableRPCService { - /// 3. - func fetchTodos( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse - - /// 4. - func createTodo( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse - - /// 5. - func deleteTodo( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse - - /// 6. - func completeTodo( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse -} - -/// Conformance to `GRPCCore.RegistrableRPCService`. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension Todos_TodoService.StreamingServiceProtocol { - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal func registerMethods(with router: inout GRPCCore.RPCRouter) { - router.registerHandler( - forMethod: Todos_TodoService.Method.FetchTodos.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.fetchTodos( - request: request, - context: context - ) - } - ) - router.registerHandler( - forMethod: Todos_TodoService.Method.CreateTodo.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.createTodo( - request: request, - context: context - ) - } - ) - router.registerHandler( - forMethod: Todos_TodoService.Method.DeleteTodo.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.deleteTodo( - request: request, - context: context - ) - } - ) - router.registerHandler( - forMethod: Todos_TodoService.Method.CompleteTodo.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.completeTodo( - request: request, - context: context - ) - } - ) - } -} - -/// 2. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -internal protocol Todos_TodoService_ServiceProtocol: Todos_TodoService.StreamingServiceProtocol { - /// 3. - func fetchTodos( - request: GRPCCore.ServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse - - /// 4. - func createTodo( - request: GRPCCore.ServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse - - /// 5. - func deleteTodo( - request: GRPCCore.ServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse - - /// 6. - func completeTodo( - request: GRPCCore.ServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse -} - -/// Partial conformance to `Todos_TodoService_StreamingServiceProtocol`. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension Todos_TodoService.ServiceProtocol { - internal func fetchTodos( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse { - let response = try await self.fetchTodos( - request: GRPCCore.ServerRequest(stream: request), - context: context - ) - return GRPCCore.StreamingServerResponse(single: response) - } - - internal func createTodo( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse { - let response = try await self.createTodo( - request: GRPCCore.ServerRequest(stream: request), - context: context - ) - return GRPCCore.StreamingServerResponse(single: response) - } - - internal func deleteTodo( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse { - let response = try await self.deleteTodo( - request: GRPCCore.ServerRequest(stream: request), - context: context - ) - return GRPCCore.StreamingServerResponse(single: response) - } - - internal func completeTodo( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse { - let response = try await self.completeTodo( - request: GRPCCore.ServerRequest(stream: request), - context: context - ) - return GRPCCore.StreamingServerResponse(single: response) - } -} - -/// 2. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -internal protocol Todos_TodoService_ClientProtocol: Sendable { - /// 3. - func fetchTodos( - request: GRPCCore.ClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R - ) async throws -> R where R: Sendable - - /// 4. - func createTodo( - request: GRPCCore.ClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R - ) async throws -> R where R: Sendable - - /// 5. - func deleteTodo( - request: GRPCCore.ClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R - ) async throws -> R where R: Sendable - - /// 6. - func completeTodo( - request: GRPCCore.ClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R - ) async throws -> R where R: Sendable -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension Todos_TodoService.ClientProtocol { - internal func fetchTodos( - request: GRPCCore.ClientRequest, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R = { - try $0.message - } - ) async throws -> R where R: Sendable { - try await self.fetchTodos( - request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - body - ) - } - - internal func createTodo( - request: GRPCCore.ClientRequest, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R = { - try $0.message - } - ) async throws -> R where R: Sendable { - try await self.createTodo( - request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - body - ) - } - - internal func deleteTodo( - request: GRPCCore.ClientRequest, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R = { - try $0.message - } - ) async throws -> R where R: Sendable { - try await self.deleteTodo( - request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - body - ) - } - - internal func completeTodo( - request: GRPCCore.ClientRequest, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R = { - try $0.message - } - ) async throws -> R where R: Sendable { - try await self.completeTodo( - request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - body - ) - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension Todos_TodoService.ClientProtocol { - /// 3. - internal func fetchTodos( - _ message: Todos_Empty, - metadata: GRPCCore.Metadata = [:], - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { - try $0.message - } - ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest( - message: message, - metadata: metadata - ) - return try await self.fetchTodos( - request: request, - options: options, - handleResponse - ) - } - - /// 4. - internal func createTodo( - _ message: Todos_Todo, - metadata: GRPCCore.Metadata = [:], - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { - try $0.message - } - ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest( - message: message, - metadata: metadata - ) - return try await self.createTodo( - request: request, - options: options, - handleResponse - ) - } - - /// 5. - internal func deleteTodo( - _ message: Todos_TodoID, - metadata: GRPCCore.Metadata = [:], - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { - try $0.message - } - ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest( - message: message, - metadata: metadata - ) - return try await self.deleteTodo( - request: request, - options: options, - handleResponse - ) - } - - /// 6. - internal func completeTodo( - _ message: Todos_TodoID, - metadata: GRPCCore.Metadata = [:], - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { - try $0.message - } - ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest( - message: message, - metadata: metadata - ) - return try await self.completeTodo( - request: request, - options: options, - handleResponse - ) - } -} - -/// 2. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -internal struct Todos_TodoService_Client: Todos_TodoService.ClientProtocol { - private let client: GRPCCore.GRPCClient - - internal init(wrapping client: GRPCCore.GRPCClient) { - self.client = client - } - - /// 3. - internal func fetchTodos( - request: GRPCCore.ClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R = { - try $0.message - } - ) async throws -> R where R: Sendable { - try await self.client.unary( - request: request, - descriptor: Todos_TodoService.Method.FetchTodos.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - handler: body - ) - } - - /// 4. - internal func createTodo( - request: GRPCCore.ClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R = { - try $0.message - } - ) async throws -> R where R: Sendable { - try await self.client.unary( - request: request, - descriptor: Todos_TodoService.Method.CreateTodo.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - handler: body - ) - } - - /// 5. - internal func deleteTodo( - request: GRPCCore.ClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R = { - try $0.message - } - ) async throws -> R where R: Sendable { - try await self.client.unary( - request: request, - descriptor: Todos_TodoService.Method.DeleteTodo.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - handler: body - ) - } - - /// 6. - internal func completeTodo( - request: GRPCCore.ClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R = { - try $0.message - } - ) async throws -> R where R: Sendable { - try await self.client.unary( - request: request, - descriptor: Todos_TodoService.Method.CompleteTodo.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - handler: body - ) - } -} -// snippet.show -actor TodoService: Todos_TodoService_ServiceProtocol { - - var todos: [Todos_Todo] - - init( - todos: [Todos_Todo] = [] - ) { - self.todos = todos - } - - func createTodo( - request: ServerRequest, - context: ServerContext - ) async throws -> ServerResponse { - todos.append(request.message) - return .init(message: request.message) - } - - func fetchTodos( - request: ServerRequest, - context: ServerContext - ) async throws -> ServerResponse { - var result = Todos_TodoList() - result.todos = todos - return .init(message: result) - } - - func completeTodo( - request: ServerRequest, - context: ServerContext - ) async throws -> ServerResponse { - guard - var todo = todos.first(where: { $0.todoID == request.message.todoID }) - else { - return .init( - error: RPCError.init( - code: .notFound, - message: "Todo not found." - ) - ) - } - todo.completed = true - todos = todos.filter { $0.todoID != request.message.todoID } - todos.append(todo) - return .init(message: todo) - - } - - func deleteTodo( - request: ServerRequest, - context: ServerContext - ) async throws -> ServerResponse { - guard - let todo = todos.first(where: { $0.todoID == request.message.todoID }) - else { - return .init( - error: RPCError.init( - code: .notFound, - message: "Todo not found." - ) - ) - } - todos = todos.filter { $0.todoID != todo.todoID } - return .init(message: .init()) - } -} - - -import ArgumentParser -import GRPCNIOTransportHTTP2 -import GRPCProtobuf - -@main -struct Entrypoint: AsyncParsableCommand { - - @Option(name: .shortAndLong) - var hostname: String = "127.0.0.1" - - @Option(name: .shortAndLong) - var port: Int = 1234 - - func run() async throws { - // 1. - let server = GRPCServer( - transport: .http2NIOPosix( - address: .ipv4(host: hostname, port: port), - config: .defaults(transportSecurity: .plaintext) - ), - services: [ - // 2. - TodoService() - ] - ) - - // 3. - try await withThrowingDiscardingTaskGroup { group in - group.addTask { try await server.serve() } - // 4. - if let address = try await server.listeningAddress { - print("gRPC server listening on \(address)") - } - } - } -} \ No newline at end of file diff --git a/Sources/Articles/Documentation.docc/2024/beginners-guide-to-protocol-buffers-and-grpc-with-swift/beginners-guide-to-protocol-buffers-and-grpc-with-swift.md b/Sources/Articles/Documentation.docc/2024/beginners-guide-to-protocol-buffers-and-grpc-with-swift/beginners-guide-to-protocol-buffers-and-grpc-with-swift.md index 33c7685..1c188f6 100644 --- a/Sources/Articles/Documentation.docc/2024/beginners-guide-to-protocol-buffers-and-grpc-with-swift/beginners-guide-to-protocol-buffers-and-grpc-with-swift.md +++ b/Sources/Articles/Documentation.docc/2024/beginners-guide-to-protocol-buffers-and-grpc-with-swift/beginners-guide-to-protocol-buffers-and-grpc-with-swift.md @@ -9,7 +9,7 @@ If you're interested in building your own gRPC services, this tutorial explores [Protocol Buffers](https://protobuf.dev/), also called "protobuf", is an efficient data serialization method developed by Google. Using protobuf consists of three main components: - The Protobuf language -- The Swift-Protobuf Compiler +- The Protobuf Compiler - Swift-Protobuf Library 1. The [protobuf language](https://protobuf.dev/programming-guides/proto3/) is used to define your protocol's data structures. @@ -32,9 +32,9 @@ A [VSCode plugin](https://marketplace.visualstudio.com/items?itemName=zxh404.vsc ### The Protocol Buffer Compiler -The protocol buffer compiler (protoc) is a tool that generates source code for data serialization and gRPC communication from `.proto` schema files. +The protocol buffer compiler (`protoc`) is a tool that generates source code for data serialization and gRPC communication from `.proto` schema files. -The protocol buffers compiler supports many [languages](https://protobuf.dev/getting-started/), including Swift. To [install](https://grpc.io/docs/protoc-installation/) protoc on your machine, follow the instructions based on your operating system: +The protocol buffers compiler supports many [languages](https://protobuf.dev/getting-started/), including Swift. To [install](https://grpc.io/docs/protoc-installation/) `protoc` on your machine, follow the instructions based on your operating system: ```sh # install on macOS @@ -50,15 +50,14 @@ unzip protoc-25.1-linux-x86_64.zip -d $HOME/.local export PATH="$PATH:$HOME/.local/bin" ``` -The `protoc` command is now ready for use. This extensible tool allows for the generation of language-specific code through plugins. The second major version of the [gRPC Swift protobuf](https://github.com/grpc/grpc-swift-protobuf/) library includes a protoc plugin that generates Swift data structures and a gRPC interface, specifically tailored for this version. +The `protoc` command is now ready for use. This extensible tool allows for the generation of language-specific code through plugins. -You can install the plugin using the following snippet, which will place the `protoc-gen-grpc-swift` binary in your home folder: +### The Swift-Protobuf Library + +To convert `proto` files into Swift, you will also need the [SwiftProtobuf](https://github.com/apple/swift-protobuf) code generator plugin. The following command will install the necessary tools: ```sh -git clone https://github.com/grpc/grpc-swift-protobuf.git -cd grpc-swift-protobuf -swift build -c release -cp "$(swift build --show-bin-path -c release)/protoc-gen-grpc-swift" /Users/{me}/ +brew install swift-protobuf ``` The next step involves defining the data structure for a sample todo application by creating a `todo_messages.proto` file. This file will specify the models for the application. Below is the complete protobuf definition: @@ -102,13 +101,10 @@ Protocol buffer field numbers provide efficient, compact encoding and ensure com This approach optimizes both data transmission and parsing speed, making protocol buffers highly reliable for evolving data models. -With the basics of the proto file covered, the next step is to generate a Swift data structure using the protoc command. To generate the Swift source code, run the following command in the same directory as your proto file: +With the basics of the proto file covered, the next step is to generate a Swift data structure using the `protoc` command. To generate the Swift source code, run the following command in the same directory as your proto file: ```sh -protoc \ - --plugin=/Users/{me}/protoc-gen-grpc-swift \ - --swift_out=./ \ - todo_messages.proto +protoc --swift_out=./ todo_messages.proto ``` Running this command will generate a `todo_messages.pb.swift` file, containing the Swift models that represent the protocol buffer description. @@ -124,7 +120,6 @@ It's used similarly to the OpenAPI standard, but has two key differences: - gRPC uses protobuf, which is more compact and performant than JSON or Multipart. - gRPC supports bi-directional streaming as opposed to OpenAPI's request/response model. - Below is the protobuf definition for the todo gRPC service: ```proto @@ -155,9 +150,30 @@ service TodoService { 5. The `DeleteTodo` method removes a todo, taking a `TodoID` as input and returning an empty response. 6. The `CompleteTodo` method toggles the completion status of a todo, taking a `TodoID` as input and returning the updated `Todo`. +### The gRPC Swift protobuf plugin + +The [gRPC Swift protobuf](https://github.com/grpc/grpc-swift-protobuf/) repository comes with a `protoc` plugin that can generate the Swift code for the gRPC interface using a proto file. + +You can build the plugin using the following snippet, which will place the `protoc-gen-grpc-swift` binary in your home folder: + +```sh +git clone https://github.com/grpc/grpc-swift-protobuf.git +cd grpc-swift-protobuf +swift build -c release +cp "$(swift build --show-bin-path -c release)/protoc-gen-grpc-swift" ~/ + +# alternatively place it next to the protoc command +cp "$(swift build --show-bin-path -c release)/protoc-gen-grpc-swift" "$(dirname $(which protoc))" +# if you don't have permission run the command with sudo +# sudo !! +# verify installation +protoc-gen-grpc-swift --help +``` + It's possible to generate the complete gRPC service protocol directly from the `todo_service.proto` file using the `protoc` command. To do so, simply run the following command in the same directory as your proto file: ```sh +# if you placed the plugin next to protoc command, omit the --plugin argument protoc \ --plugin=/Users/tib/protoc-gen-grpc-swift \ --grpc-swift_out=./ \ @@ -188,8 +204,8 @@ let package = Package( .macOS(.v15), ], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift-protobuf", exact: "1.0.0-alpha.1"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport", exact: "1.0.0-alpha.1"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf", exact: "1.0.0-beta.2"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport", exact: "1.0.0-beta.2"), .package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.0"), ], targets: [ @@ -212,73 +228,7 @@ It's also a good practice to retain the original `.proto` files for reference an With the generated data types and interfaces in place, the server-side interface can now be implemented using the gRPC library. Below is an example of a simple actor-based implementation that utilizes in-memory storage and fulfills the `TodoService` protocol requirements: -```swift -actor TodoService: Todos_TodoService_ServiceProtocol { - - var todos: [Todos_Todo] - - init( - todos: [Todos_Todo] = [] - ) { - self.todos = todos - } - - func createTodo( - request: ServerRequest, - context: ServerContext - ) async throws -> ServerResponse { - todos.append(request.message) - return .init(message: request.message) - } - - func fetchTodos( - request: ServerRequest, - context: ServerContext - ) async throws -> ServerResponse { - var result = Todos_TodoList() - result.todos = todos - return .init(message: result) - } - - func completeTodo( - request: ServerRequest, - context: ServerContext - ) async throws -> ServerResponse { - guard - var todo = todos.first(where: { $0.todoID == request.message.todoID }) - else { - return .init( - error: RPCError.init( - code: .notFound, - message: "Todo not found." - ) - ) - } - todo.completed = true - todos = todos.filter { $0.todoID != request.message.todoID } - todos.append(todo) - return .init(message: todo) - } - - func deleteTodo( - request: ServerRequest, - context: ServerContext - ) async throws -> ServerResponse { - guard - let todo = todos.first(where: { $0.todoID == request.message.todoID }) - else { - return .init( - error: RPCError.init( - code: .notFound, - message: "Todo not found." - ) - ) - } - todos = todos.filter { $0.todoID != todo.todoID } - return .init(message: .init()) - } -} -``` +@Snippet(path: "site/Snippets/beginners-guide-to-protocol-buffers-and-grpc-with-swift", slice: todo_service_protocol) The snippet above relies on several types from the gRPC library, such as `ServerRequest` and `ServerContext`, which are passed as arguments to each function call. The functions also use Swift data types generated from the `todo_messages.proto` file, ensuring that the required input and output data is provided correctly. @@ -286,44 +236,7 @@ The final step involves configuring the gRPC server. This can be achieved by cre This setup is built on the v2 gRPC library, which introduces support for modern concurrency features, including task groups and the Service Lifecycle library. Below is an example demonstrating how to configure the server using the upcoming gRPC v2 release: -```swift -import ArgumentParser -import GRPCNIOTransportHTTP2 -import GRPCProtobuf - -@main -struct Entrypoint: AsyncParsableCommand { - - @Option(name: .shortAndLong) - var hostname: String = "127.0.0.1" - - @Option(name: .shortAndLong) - var port: Int = 1234 - - func run() async throws { - // 1. - let server = GRPCServer( - transport: .http2NIOPosix( - address: .ipv4(host: hostname, port: port), - config: .defaults(transportSecurity: .plaintext) - ), - services: [ - // 2. - TodoService() - ] - ) - - // 3. - try await withThrowingDiscardingTaskGroup { group in - group.addTask { try await server.serve() } - // 4. - if let address = try await server.listeningAddress { - print("gRPC server listening on \(address)") - } - } - } -} -``` +@Snippet(path: "site/Snippets/beginners-guide-to-protocol-buffers-and-grpc-with-swift", slice: app_entrypoint) 1. Creates a gRPC server using the specified `http2NIOPosix` transport layer with the provided configuration. 2. Add the `TodoService` as a service, which contains the logic for handling gRPC requests. diff --git a/Sources/Articles/Documentation.docc/2025/jwt-kit/jwt-kit.md b/Sources/Articles/Documentation.docc/2025/introduction-to-jwts-in-swift/introduction-to-jwts-in-swift.md similarity index 100% rename from Sources/Articles/Documentation.docc/2025/jwt-kit/jwt-kit.md rename to Sources/Articles/Documentation.docc/2025/introduction-to-jwts-in-swift/introduction-to-jwts-in-swift.md From 3fe02c2c45809f6195b2463ab48fa271800b0ff2 Mon Sep 17 00:00:00 2001 From: Tibor Bodecs Date: Mon, 13 Jan 2025 22:49:40 +0100 Subject: [PATCH 2/4] jwt name fix --- .../.DS_Store | Bin 6148 -> 0 bytes .../introduction-to-jwts-in-swift.md | 16 ++++++++-------- 2 files changed, 8 insertions(+), 8 deletions(-) delete mode 100644 Snippets/2024/beginners-guide-to-protocol-buffers-and-grpc-with-swift/.DS_Store diff --git a/Snippets/2024/beginners-guide-to-protocol-buffers-and-grpc-with-swift/.DS_Store b/Snippets/2024/beginners-guide-to-protocol-buffers-and-grpc-with-swift/.DS_Store deleted file mode 100644 index 68e18877b6ee41a9575017cbe92cf2c1657f37bd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK%}c{D6i?i=Eko!*L2m)C1NXuFz)PtU?^g7nGFw`(IGeF{^e_g!>p$c_;@{(Y zNj5g@Sw!B0>pT%9qT*jCU4UwZ#BWSL5ZJA&W2o2_#vXB)lVR2N} zO!OB`xORtK!{{fs8#G%q@xFTT}iZtqx@W3{ZO;6;`}5f-!D52p_} zx|MPqE%h+EOOtZs?p?~P2-7T?s)RI7Ams5r&0<;la+bxp$_=!`acpPg9?s{ze!nex zXM;ss%)6cQL0g>kPZtZv-ak6NxEZ~U-!l2G`Q*3-lwA$0{$r()LJSZC!~iis46HF= zE)Ki3W+RmtAO?s5WB|_xVbC*JX;eoCbgBdZY=T({=vYf&t}y5stTciLgzHp5oytv# z!F4*=g^BYFRvL9Wh| zhyh~YpE1Cj!(iycrp(>?V|#el3TV&JP%y4S1qAesM*s})963- Date: Sun, 19 Jan 2025 10:22:29 +0100 Subject: [PATCH 3/4] fix mustache --- Package.resolved | 28 +++++++++---------- .../templating-with-swift-mustache.md | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Package.resolved b/Package.resolved index b6f149e..a1b7809 100644 --- a/Package.resolved +++ b/Package.resolved @@ -69,8 +69,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/hummingbird-project/hummingbird.git", "state" : { - "revision" : "7a41c20c25866064f22b2bfa2c8194083e7e1595", - "version" : "2.6.1" + "revision" : "480f29f074878cd258b615225acf9049f94f52c3", + "version" : "2.6.2" } }, { @@ -132,8 +132,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-asn1.git", "state" : { - "revision" : "7faebca1ea4f9aaf0cda1cef7c43aecd2311ddf6", - "version" : "1.3.0" + "revision" : "ae33e5941bb88d88538d0a6b19ca0b01e6c76dcf", + "version" : "1.3.1" } }, { @@ -159,8 +159,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-certificates.git", "state" : { - "revision" : "1fbb6ef21f1525ed5faf4c95207b9c11bea27e94", - "version" : "1.6.1" + "revision" : "274f8668d3ec5d2892904d8465635c5ea659f767", + "version" : "1.7.0" } }, { @@ -186,8 +186,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-distributed-tracing.git", "state" : { - "revision" : "6483d340853a944c96dbcc28b27dd10b6c581703", - "version" : "1.1.2" + "revision" : "a64a0abc2530f767af15dd88dda7f64d5f1ff9de", + "version" : "1.2.0" } }, { @@ -213,8 +213,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/hummingbird-project/swift-jobs.git", "state" : { - "revision" : "6aeb0a179567b944ecf81e1bbfdd474c1ee171f1", - "version" : "1.0.0-beta.6" + "revision" : "1fb1585ef88b3b74f190e6f8d390ad47987651e1", + "version" : "1.0.0-beta.7" } }, { @@ -231,8 +231,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-metrics.git", "state" : { - "revision" : "e0165b53d49b413dd987526b641e05e246782685", - "version" : "2.5.0" + "revision" : "5e63558d12e0267782019f5dadfcae83a7d06e09", + "version" : "2.5.1" } }, { @@ -249,8 +249,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio.git", "state" : { - "revision" : "dca6594f65308c761a9c409e09fbf35f48d50d34", - "version" : "2.77.0" + "revision" : "ba72f31e11275fc5bf060c966cf6c1f36842a291", + "version" : "2.79.0" } }, { diff --git a/Sources/Articles/Documentation.docc/2024/templating-with-swift-mustache/templating-with-swift-mustache.md b/Sources/Articles/Documentation.docc/2024/templating-with-swift-mustache/templating-with-swift-mustache.md index e7ee71a..341ae30 100644 --- a/Sources/Articles/Documentation.docc/2024/templating-with-swift-mustache/templating-with-swift-mustache.md +++ b/Sources/Articles/Documentation.docc/2024/templating-with-swift-mustache/templating-with-swift-mustache.md @@ -17,7 +17,7 @@ Unlike Leaf, which is Swift-specific, Mustache templates can be used in any lang First, you need to add the Swift-Mustache package to your project. You can do this by adding the following line to your `Package.swift` file: ```swift -.package(url: "https://github.com/hummingbird-project/swift-mustache", from: "2.0.0-beta.1"), +.package(url: "https://github.com/hummingbird-project/swift-mustache", from: "2.0.0"), ``` Then, add the `Mustache` library to your target dependencies: From 4285b96d36fe22a2c07100eadde27a3b2172f8fa Mon Sep 17 00:00:00 2001 From: Tibor Bodecs Date: Sun, 19 Jan 2025 12:32:31 +0100 Subject: [PATCH 4/4] update snippets --- Makefile | 10 + Package.swift | 2 +- README.md | 15 +- .../snippets.swift} | 0 .../download.swift} | 0 .../json.swift} | 0 .../request.swift} | 0 .../setup.swift} | 0 ...d-grpc-with-swift.swift => snippets.swift} | 0 .../todo_messages.proto | 24 - .../todo_service.proto | 18 - .../snippets.swift} | 0 .../snippets.swift} | 0 .../snippets.swift} | 0 .../books.swift} | 0 .../dispatch.swift} | 0 .../locks.swift} | 0 .../example-01.swift} | 0 .../example-02.swift} | 0 .../example-03.swift} | 0 .../example-04.swift} | 0 .../example-05.swift} | 0 .../snippets.swift} | 0 .../snippets.swift} | 0 .../snippets.swift} | 0 .../snippets.swift} | 0 .../snippets.swift} | 0 .../snippets.swift} | 0 .../app.swift} | 0 .../connection-manager-add-user.swift} | 0 .../connection-manager-types.swift} | 0 .../snippets.swift} | 0 .../router-builder.swift} | 0 .../snippets.swift} | 0 .../snippets.swift} | 0 .../advanced-async-sequences.md | 60 +- .../async-http-client-by-example.md | 16 +- ...to-protocol-buffers-and-grpc-with-swift.md | 10 +- .../building-swiftnio-clients.md | 40 +- .../getting-started-with-hummingbird.md | 25 +- .../getting-started-with-mongokitten.md | 35 +- ...ed-with-structured-concurrency-in-swift.md | 34 +- .../getting-started-with-swiftpm-snippets.md | 84 ++- ...o-build-a-proxy-server-with-hummingbird.md | 35 +- ...dates-with-changestreams-and-websockets.md | 20 +- ...d-concurrency-and-shared-state-in-swift.md | 60 +- .../using-environment-variables-in-swift.md | 10 +- .../using-hummingbird-request-context.md | 25 +- .../using-swiftnio-channels.md | 15 +- ...ts-tutorial-using-swift-and-hummingbird.md | 27 +- .../whats-new-in-hummingbird-2.md | 29 +- .../working-with-udp-in-swiftnio.md | 10 +- .../introduction-to-jwts-in-swift.md | 32 +- .../.build.ssgc/ssgc/articles.package.json | 600 ++++++++++++++++++ 54 files changed, 1068 insertions(+), 168 deletions(-) rename Snippets/2024/{advanced-async-sequences.swift => advanced-async-sequences/snippets.swift} (100%) rename Snippets/2024/{ahc_download.swift => async-http-client-by-example/download.swift} (100%) rename Snippets/2024/{ahc_json.swift => async-http-client-by-example/json.swift} (100%) rename Snippets/2024/{ahc_request.swift => async-http-client-by-example/request.swift} (100%) rename Snippets/2024/{ahc_setup.swift => async-http-client-by-example/setup.swift} (100%) rename Snippets/2024/beginners-guide-to-protocol-buffers-and-grpc-with-swift/{beginners-guide-to-protocol-buffers-and-grpc-with-swift.swift => snippets.swift} (100%) delete mode 100644 Snippets/2024/beginners-guide-to-protocol-buffers-and-grpc-with-swift/todo_messages.proto delete mode 100644 Snippets/2024/beginners-guide-to-protocol-buffers-and-grpc-with-swift/todo_service.proto rename Snippets/2024/{building-swiftnio-clients-01.swift => building-swiftnio-clients/snippets.swift} (100%) rename Snippets/2024/{HummingbirdApp.swift => getting-started-with-hummingbird/snippets.swift} (100%) rename Snippets/2024/{mongokitten-basics.swift => getting-started-with-mongokitten/snippets.swift} (100%) rename Snippets/2024/{getting-started-concurrency-buy-books.swift => getting-started-with-structured-concurrency-in-swift/books.swift} (100%) rename Snippets/2024/{getting-started-concurrency-dispatch.swift => getting-started-with-structured-concurrency-in-swift/dispatch.swift} (100%) rename Snippets/2024/{getting-started-concurrency-imagecache-locks.swift => getting-started-with-structured-concurrency-in-swift/locks.swift} (100%) rename Snippets/2024/{swiftpm-snippets/SnippetsExample_I.swift => getting-started-with-swiftpm-snippets/example-01.swift} (100%) rename Snippets/2024/{swiftpm-snippets/SnippetsExample_II.swift => getting-started-with-swiftpm-snippets/example-02.swift} (100%) rename Snippets/2024/{swiftpm-snippets/SnippetsExample_III.swift => getting-started-with-swiftpm-snippets/example-03.swift} (100%) rename Snippets/2024/{swiftpm-snippets/SnippetsExample_IV.swift => getting-started-with-swiftpm-snippets/example-04.swift} (100%) rename Snippets/2024/{swiftpm-snippets/SnippetsExample_V.swift => getting-started-with-swiftpm-snippets/example-05.swift} (100%) rename Snippets/2024/{hummingbird-2-proxy.swift => how-to-build-a-proxy-server-with-hummingbird/snippets.swift} (100%) rename Snippets/2024/{realtime-mongodb-app.swift => realtime-mongodb-updates-with-changestreams-and-websockets/snippets.swift} (100%) rename Snippets/2024/{shared-state.swift => structured-concurrency-and-shared-state-in-swift/snippets.swift} (100%) rename Snippets/2024/{environment.swift => using-environment-variables-in-swift/snippets.swift} (100%) rename Snippets/2024/{HummingbirdRequestContext.swift => using-hummingbird-request-context/snippets.swift} (100%) rename Snippets/2024/{using-swiftnio-channels.swift => using-swiftnio-channels/snippets.swift} (100%) rename Snippets/2024/{websockets-app.swift => websockets-tutorial-using-swift-and-hummingbird/app.swift} (100%) rename Snippets/2024/{websockets-connection-manager-add-user.swift => websockets-tutorial-using-swift-and-hummingbird/connection-manager-add-user.swift} (100%) rename Snippets/2024/{websockets-connection-manager-types.swift => websockets-tutorial-using-swift-and-hummingbird/connection-manager-types.swift} (100%) rename Snippets/2024/{websockets.swift => websockets-tutorial-using-swift-and-hummingbird/snippets.swift} (100%) rename Snippets/2024/{hummingbird-2-routerbuilder.swift => whats-new-in-hummingbird-2/router-builder.swift} (100%) rename Snippets/2024/{hummingbird-2.swift => whats-new-in-hummingbird-2/snippets.swift} (100%) rename Snippets/2024/{working-with-udp-in-swiftnio.swift => working-with-udp-in-swiftnio/snippets.swift} (100%) create mode 100644 articles/.build.ssgc/ssgc/articles.package.json diff --git a/Makefile b/Makefile index 5d4d5bf..f8032e1 100644 --- a/Makefile +++ b/Makefile @@ -20,3 +20,13 @@ check: format: ./scripts/run-swift-format.sh --fix + +unidoc: + unidoc compile -I .. \ +--ci fail-on-errors \ +--package-name articles \ +--define DARWIN + +unidoc-local: + unidoc local -i . + diff --git a/Package.swift b/Package.swift index 5d241d9..9c90056 100644 --- a/Package.swift +++ b/Package.swift @@ -2,7 +2,7 @@ import PackageDescription let package = Package( - name: "swiftonserver-articles", + name: "articles", platforms: [ .macOS(.v15), ], diff --git a/README.md b/README.md index 0bebb30..fbefe58 100644 --- a/README.md +++ b/README.md @@ -49,13 +49,14 @@ If a tutorial uses multiple snippets, you can create a folder with your tutorial DocC uses the following format to locate a snippet: ``` -@Snippet(path: "site/Snippets/hummingbird-2") +@Snippet( + path: "articles/Snippets/2025/introduction-to-jwts-in-swift/jwt-kit" +) ``` -- `site/` is mandatory, and refers to the current Swift package by package name. -- `Snippets/` is the folder where we store our snippets -- Optionally, and subfolders of `Snippets/` can be added in between -- `hummingbird-2` refers to the snippet file. Do not put a `.swift` extensionh ere. +- `articles` is mandatory, and refers to the current Swift package by package name. +- The next part is the location where we store our snippet. It is mandatory to start the location with `Snippets`. +- `jwt-kit` refers to the actual snippet file. Do not use the `.swift` extension here. If you _don't_ want to render certain example code in the tutorial, you can mark the start of hidden lines of code as such: @@ -88,7 +89,9 @@ And you can end that labeled block as such: You can refer to a labeled block (called a "slice") in your tutorial's code: ``` -@Snippet(path: "site/Snippets/building-swiftnio-clients-01", slice: "imports") +@Snippet( + path: "articles/Snippets/2025/introduction-to-jwts-in-swift/jwt-kit", slice: "key_collection_add_hmac" +) ``` ### Planning an Article (recommended) diff --git a/Snippets/2024/advanced-async-sequences.swift b/Snippets/2024/advanced-async-sequences/snippets.swift similarity index 100% rename from Snippets/2024/advanced-async-sequences.swift rename to Snippets/2024/advanced-async-sequences/snippets.swift diff --git a/Snippets/2024/ahc_download.swift b/Snippets/2024/async-http-client-by-example/download.swift similarity index 100% rename from Snippets/2024/ahc_download.swift rename to Snippets/2024/async-http-client-by-example/download.swift diff --git a/Snippets/2024/ahc_json.swift b/Snippets/2024/async-http-client-by-example/json.swift similarity index 100% rename from Snippets/2024/ahc_json.swift rename to Snippets/2024/async-http-client-by-example/json.swift diff --git a/Snippets/2024/ahc_request.swift b/Snippets/2024/async-http-client-by-example/request.swift similarity index 100% rename from Snippets/2024/ahc_request.swift rename to Snippets/2024/async-http-client-by-example/request.swift diff --git a/Snippets/2024/ahc_setup.swift b/Snippets/2024/async-http-client-by-example/setup.swift similarity index 100% rename from Snippets/2024/ahc_setup.swift rename to Snippets/2024/async-http-client-by-example/setup.swift diff --git a/Snippets/2024/beginners-guide-to-protocol-buffers-and-grpc-with-swift/beginners-guide-to-protocol-buffers-and-grpc-with-swift.swift b/Snippets/2024/beginners-guide-to-protocol-buffers-and-grpc-with-swift/snippets.swift similarity index 100% rename from Snippets/2024/beginners-guide-to-protocol-buffers-and-grpc-with-swift/beginners-guide-to-protocol-buffers-and-grpc-with-swift.swift rename to Snippets/2024/beginners-guide-to-protocol-buffers-and-grpc-with-swift/snippets.swift diff --git a/Snippets/2024/beginners-guide-to-protocol-buffers-and-grpc-with-swift/todo_messages.proto b/Snippets/2024/beginners-guide-to-protocol-buffers-and-grpc-with-swift/todo_messages.proto deleted file mode 100644 index 56f6952..0000000 --- a/Snippets/2024/beginners-guide-to-protocol-buffers-and-grpc-with-swift/todo_messages.proto +++ /dev/null @@ -1,24 +0,0 @@ -syntax = "proto3"; - -// 1. -package todos; - -// 2. -message Empty {} - -// 3. -message TodoID { - string todoID = 1; -} - -// 4. -message Todo { - optional string todoID = 1; - string title = 2; - bool completed = 3; -} - -// 5. -message TodoList { - repeated Todo todos = 1; -} diff --git a/Snippets/2024/beginners-guide-to-protocol-buffers-and-grpc-with-swift/todo_service.proto b/Snippets/2024/beginners-guide-to-protocol-buffers-and-grpc-with-swift/todo_service.proto deleted file mode 100644 index 2e693f5..0000000 --- a/Snippets/2024/beginners-guide-to-protocol-buffers-and-grpc-with-swift/todo_service.proto +++ /dev/null @@ -1,18 +0,0 @@ -syntax = "proto3"; - -package todos; - -// 1. -import "todo_messages.proto"; - -// 2. -service TodoService { - // 3. - rpc FetchTodos (Empty) returns (TodoList) {} - // 4. - rpc CreateTodo (Todo) returns (Todo) {} - // 5. - rpc DeleteTodo (TodoID) returns (Empty) {} - // 6. - rpc CompleteTodo (TodoID) returns (Todo) {} -} diff --git a/Snippets/2024/building-swiftnio-clients-01.swift b/Snippets/2024/building-swiftnio-clients/snippets.swift similarity index 100% rename from Snippets/2024/building-swiftnio-clients-01.swift rename to Snippets/2024/building-swiftnio-clients/snippets.swift diff --git a/Snippets/2024/HummingbirdApp.swift b/Snippets/2024/getting-started-with-hummingbird/snippets.swift similarity index 100% rename from Snippets/2024/HummingbirdApp.swift rename to Snippets/2024/getting-started-with-hummingbird/snippets.swift diff --git a/Snippets/2024/mongokitten-basics.swift b/Snippets/2024/getting-started-with-mongokitten/snippets.swift similarity index 100% rename from Snippets/2024/mongokitten-basics.swift rename to Snippets/2024/getting-started-with-mongokitten/snippets.swift diff --git a/Snippets/2024/getting-started-concurrency-buy-books.swift b/Snippets/2024/getting-started-with-structured-concurrency-in-swift/books.swift similarity index 100% rename from Snippets/2024/getting-started-concurrency-buy-books.swift rename to Snippets/2024/getting-started-with-structured-concurrency-in-swift/books.swift diff --git a/Snippets/2024/getting-started-concurrency-dispatch.swift b/Snippets/2024/getting-started-with-structured-concurrency-in-swift/dispatch.swift similarity index 100% rename from Snippets/2024/getting-started-concurrency-dispatch.swift rename to Snippets/2024/getting-started-with-structured-concurrency-in-swift/dispatch.swift diff --git a/Snippets/2024/getting-started-concurrency-imagecache-locks.swift b/Snippets/2024/getting-started-with-structured-concurrency-in-swift/locks.swift similarity index 100% rename from Snippets/2024/getting-started-concurrency-imagecache-locks.swift rename to Snippets/2024/getting-started-with-structured-concurrency-in-swift/locks.swift diff --git a/Snippets/2024/swiftpm-snippets/SnippetsExample_I.swift b/Snippets/2024/getting-started-with-swiftpm-snippets/example-01.swift similarity index 100% rename from Snippets/2024/swiftpm-snippets/SnippetsExample_I.swift rename to Snippets/2024/getting-started-with-swiftpm-snippets/example-01.swift diff --git a/Snippets/2024/swiftpm-snippets/SnippetsExample_II.swift b/Snippets/2024/getting-started-with-swiftpm-snippets/example-02.swift similarity index 100% rename from Snippets/2024/swiftpm-snippets/SnippetsExample_II.swift rename to Snippets/2024/getting-started-with-swiftpm-snippets/example-02.swift diff --git a/Snippets/2024/swiftpm-snippets/SnippetsExample_III.swift b/Snippets/2024/getting-started-with-swiftpm-snippets/example-03.swift similarity index 100% rename from Snippets/2024/swiftpm-snippets/SnippetsExample_III.swift rename to Snippets/2024/getting-started-with-swiftpm-snippets/example-03.swift diff --git a/Snippets/2024/swiftpm-snippets/SnippetsExample_IV.swift b/Snippets/2024/getting-started-with-swiftpm-snippets/example-04.swift similarity index 100% rename from Snippets/2024/swiftpm-snippets/SnippetsExample_IV.swift rename to Snippets/2024/getting-started-with-swiftpm-snippets/example-04.swift diff --git a/Snippets/2024/swiftpm-snippets/SnippetsExample_V.swift b/Snippets/2024/getting-started-with-swiftpm-snippets/example-05.swift similarity index 100% rename from Snippets/2024/swiftpm-snippets/SnippetsExample_V.swift rename to Snippets/2024/getting-started-with-swiftpm-snippets/example-05.swift diff --git a/Snippets/2024/hummingbird-2-proxy.swift b/Snippets/2024/how-to-build-a-proxy-server-with-hummingbird/snippets.swift similarity index 100% rename from Snippets/2024/hummingbird-2-proxy.swift rename to Snippets/2024/how-to-build-a-proxy-server-with-hummingbird/snippets.swift diff --git a/Snippets/2024/realtime-mongodb-app.swift b/Snippets/2024/realtime-mongodb-updates-with-changestreams-and-websockets/snippets.swift similarity index 100% rename from Snippets/2024/realtime-mongodb-app.swift rename to Snippets/2024/realtime-mongodb-updates-with-changestreams-and-websockets/snippets.swift diff --git a/Snippets/2024/shared-state.swift b/Snippets/2024/structured-concurrency-and-shared-state-in-swift/snippets.swift similarity index 100% rename from Snippets/2024/shared-state.swift rename to Snippets/2024/structured-concurrency-and-shared-state-in-swift/snippets.swift diff --git a/Snippets/2024/environment.swift b/Snippets/2024/using-environment-variables-in-swift/snippets.swift similarity index 100% rename from Snippets/2024/environment.swift rename to Snippets/2024/using-environment-variables-in-swift/snippets.swift diff --git a/Snippets/2024/HummingbirdRequestContext.swift b/Snippets/2024/using-hummingbird-request-context/snippets.swift similarity index 100% rename from Snippets/2024/HummingbirdRequestContext.swift rename to Snippets/2024/using-hummingbird-request-context/snippets.swift diff --git a/Snippets/2024/using-swiftnio-channels.swift b/Snippets/2024/using-swiftnio-channels/snippets.swift similarity index 100% rename from Snippets/2024/using-swiftnio-channels.swift rename to Snippets/2024/using-swiftnio-channels/snippets.swift diff --git a/Snippets/2024/websockets-app.swift b/Snippets/2024/websockets-tutorial-using-swift-and-hummingbird/app.swift similarity index 100% rename from Snippets/2024/websockets-app.swift rename to Snippets/2024/websockets-tutorial-using-swift-and-hummingbird/app.swift diff --git a/Snippets/2024/websockets-connection-manager-add-user.swift b/Snippets/2024/websockets-tutorial-using-swift-and-hummingbird/connection-manager-add-user.swift similarity index 100% rename from Snippets/2024/websockets-connection-manager-add-user.swift rename to Snippets/2024/websockets-tutorial-using-swift-and-hummingbird/connection-manager-add-user.swift diff --git a/Snippets/2024/websockets-connection-manager-types.swift b/Snippets/2024/websockets-tutorial-using-swift-and-hummingbird/connection-manager-types.swift similarity index 100% rename from Snippets/2024/websockets-connection-manager-types.swift rename to Snippets/2024/websockets-tutorial-using-swift-and-hummingbird/connection-manager-types.swift diff --git a/Snippets/2024/websockets.swift b/Snippets/2024/websockets-tutorial-using-swift-and-hummingbird/snippets.swift similarity index 100% rename from Snippets/2024/websockets.swift rename to Snippets/2024/websockets-tutorial-using-swift-and-hummingbird/snippets.swift diff --git a/Snippets/2024/hummingbird-2-routerbuilder.swift b/Snippets/2024/whats-new-in-hummingbird-2/router-builder.swift similarity index 100% rename from Snippets/2024/hummingbird-2-routerbuilder.swift rename to Snippets/2024/whats-new-in-hummingbird-2/router-builder.swift diff --git a/Snippets/2024/hummingbird-2.swift b/Snippets/2024/whats-new-in-hummingbird-2/snippets.swift similarity index 100% rename from Snippets/2024/hummingbird-2.swift rename to Snippets/2024/whats-new-in-hummingbird-2/snippets.swift diff --git a/Snippets/2024/working-with-udp-in-swiftnio.swift b/Snippets/2024/working-with-udp-in-swiftnio/snippets.swift similarity index 100% rename from Snippets/2024/working-with-udp-in-swiftnio.swift rename to Snippets/2024/working-with-udp-in-swiftnio/snippets.swift diff --git a/Sources/Articles/Documentation.docc/2024/advanced-async-sequences/advanced-async-sequences.md b/Sources/Articles/Documentation.docc/2024/advanced-async-sequences/advanced-async-sequences.md index 0e91d89..41bb1ba 100644 --- a/Sources/Articles/Documentation.docc/2024/advanced-async-sequences/advanced-async-sequences.md +++ b/Sources/Articles/Documentation.docc/2024/advanced-async-sequences/advanced-async-sequences.md @@ -27,11 +27,17 @@ _IteratorProtocol_ should be implemented as a `struct`, and has a single functio A common way to consume sequences and iterators is the `for .. in` syntax: -@Snippet(path: "site/Snippets/advanced-async-sequences", slice: "iterateSequence") +@Snippet( + path: "articles/Snippets/2024/advanced-async-sequences/snippets", + slice: "iterateSequence" +) Swift uses some syntax sugar in the above example, and unwinds that code behind the scenes: -@Snippet(path: "site/Snippets/advanced-async-sequences", slice: "unwrappedSequenceIterator") +@Snippet( + path: "articles/Snippets/2024/advanced-async-sequences/snippets", + slice: "unwrappedSequenceIterator" +) The ``IteratorProtocol/next()`` function returns the Element as an ``Optional``. The sequence ends when the 'next' element is `nil`. @@ -43,11 +49,17 @@ While Sequences commonly represent a collection of elements, there's no hard req This enables the following syntax: -@Snippet(path: "site/Snippets/advanced-async-sequences", slice: "iterateAsyncSequence") +@Snippet( + path: "articles/Snippets/2024/advanced-async-sequences/snippets", + slice: "iterateAsyncSequence" +) This unwinds to the following code: -@Snippet(path: "site/Snippets/advanced-async-sequences", slice: "unwrappedAsyncSequenceIterator") +@Snippet( + path: "articles/Snippets/2024/advanced-async-sequences/snippets", + slice: "unwrappedAsyncSequenceIterator" +) The simplest AsyncSequence you can create is ``AsyncStream``, which we'll cover later. @@ -61,7 +73,10 @@ This is extremely helpful for using an AsyncSequence in networking operations - The throwing counterpart to ``AsyncStream`` is ``AsyncThrowingStream``, which we'll also cover later. You can iterate over throwing async sequences in a similar way to non-throwing async sequences: -@Snippet(path: "site/Snippets/advanced-async-sequences", slice: "iterateAsyncThrowingSequence") +@Snippet( + path: "articles/Snippets/2024/advanced-async-sequences/snippets", + slice: "iterateAsyncThrowingSequence" +) Some libraries such as [MongoKitten](https://github.com/orlandos-nl/MongoKitten) use a throwing ``AsyncSequence`` to provide a stream of documents from a MongoDB collection. Since network errors can occur at any point, these errors are thrown from the iterator. @@ -69,7 +84,10 @@ Some libraries such as [MongoKitten](https://github.com/orlandos-nl/MongoKitten) The simplest way to create an ``AsyncSequence`` is to use ``AsyncStream``. This is a stream of elements that you can append to, and iterate over. The main way to create an ``AsyncSequence`` is to use the static ``AsyncStream/makeStream(of:bufferingPolicy:)`` function. -@Snippet(path: "site/Snippets/advanced-async-sequences", slice: "makeAsyncStream") +@Snippet( + path: "articles/Snippets/2024/advanced-async-sequences/snippets", + slice: "makeAsyncStream" +) The two arguments of the function have a default value. `of:` specifies the type of element that this stream carries. This is currently being inferred to ``Int`` through generics. @@ -81,7 +99,10 @@ You can add elements to the stream using the ``AsyncStream.Continuation/yield(_: To create an AsyncStream, first you need to define the type of elements that the stream will carry. In this case, a custom Event is being defined. -@Snippet(path: "site/Snippets/advanced-async-sequences", slice: "uievent") +@Snippet( + path: "articles/Snippets/2024/advanced-async-sequences/snippets", + slice: "uievent" +) After creating the stream, you can yield events to the stream. This is done by calling the `yield` function on the continuation. The follwing example shows how to yield an event when a button is tapped in SwiftUI. Note that these practices can be used in any Swift codebase, including on Linux and Windows. @@ -99,7 +120,10 @@ struct StartDownloadView: View { By leveraging the structured nature of Swift's concurrency model, we can predict the behavior of our code more easily. For example, when a user taps the button twice, we can be sure that the stream will receive two events in order. -@Snippet(path: "site/Snippets/advanced-async-sequences", slice: "appstate") +@Snippet( + path: "articles/Snippets/2024/advanced-async-sequences/snippets", + slice: "appstate" +) ### Parallising Work @@ -107,7 +131,10 @@ The main issue in the above logic is that the `startDownload` function is preven We can rewrite the loop using a ``DiscardingTaskGroup`` to start the download in parallel with handling other events. -@Snippet(path: "site/Snippets/advanced-async-sequences", slice: "parallel") +@Snippet( + path: "articles/Snippets/2024/advanced-async-sequences/snippets", + slice: "parallel" +) The `downloadState` property is set to `downloading` before parallelising work. This ensures that quickly tapping the button twice can never result in two downloads happening at the same time. @@ -117,17 +144,26 @@ You can implement your own ``AsyncSequence`` by implementing the ``AsyncSequence First we'll define our custom `DelayedElementEmitter` struct. This struct will emit elements from an array with a delay between each element. -@Snippet(path: "site/Snippets/advanced-async-sequences", slice: "delayedelementemitter") +@Snippet( + path: "articles/Snippets/2024/advanced-async-sequences/snippets", + slice: "delayedelementemitter" +) This struct needs an iterator and a function to construct that iterator. The iterator is not expected to be shared between multiple consumers, so it can be a struct. Unlike a regular ``Sequence``, the ``AsyncSequence/makeAsyncIterator()`` is expected to be called only once. While structured concurrency allows calling this function multiple times, it's not expected that a sequence supports this in practice. -@Snippet(path: "site/Snippets/advanced-async-sequences", slice: "delayedelementiterator") +@Snippet( + path: "articles/Snippets/2024/advanced-async-sequences/snippets", + slice: "delayedelementiterator" +) You can now use this custom ``AsyncSequence`` in your code. The following example shows how to create a sequence that emits numbers from 1 to 5 with a delay of 1 second between each number. -@Snippet(path: "site/Snippets/advanced-async-sequences", slice: "delayedprint") +@Snippet( + path: "articles/Snippets/2024/advanced-async-sequences/snippets", + slice: "delayedprint" +) Note that our custom `DelayedElementEmitter` cannot be iterated upon without a `try` keyword to handle errors. This is because the ``AsyncIteratorProtocol/next()`` function can throw errors due to being marked as `throws`. We can also handle the ``CancellationError``s that ``Task.sleep(for:tolerance:clock:)`` throws in the `next()` function. diff --git a/Sources/Articles/Documentation.docc/2024/async-http-client-by-example/async-http-client-by-example.md b/Sources/Articles/Documentation.docc/2024/async-http-client-by-example/async-http-client-by-example.md index f5848ed..bb60792 100644 --- a/Sources/Articles/Documentation.docc/2024/async-http-client-by-example/async-http-client-by-example.md +++ b/Sources/Articles/Documentation.docc/2024/async-http-client-by-example/async-http-client-by-example.md @@ -39,7 +39,9 @@ let package = Package( In the `main.swift` file, import the AsyncHTTPClient library and initialize an HTTPClient instance for future use: -@Snippet(path: "site/Snippets/ahc_setup") +@Snippet( + path: "articles/Snippets/2024/async-http-client-by-example/setup" +) 1. Specify the event loop group provider as ``HTTPClient/EventLoopGroupProvider.singleton``, which manages the underlying ``EventLoopGroup`` for asynchronous operations. 2. The ``HTTPClient/Configuration`` parameter is set, defining various aspects of the ``HTTPClient``'s behavior. @@ -56,7 +58,9 @@ An HTTP request includes the method, a URL, headers providing supplementary deta Below is an illustration of how to employ the HTTP request and response objects using the AsyncHTTPClient library in Swift: -@Snippet(path: "site/Snippets/ahc_request") +@Snippet( + path: "articles/Snippets/2024/async-http-client-by-example/request" +) 1. A new ``HTTPClientRequest`` object is created targeting the specified URL. 2. The HTTP request method is set to POST. @@ -78,7 +82,9 @@ JSON requests involve sending and receiving data formatted in JSON to a server. The following code snippet demonstrates how to encode request bodies and decode response bodies using JSON objects: -@Snippet(path: "site/Snippets/ahc_json") +@Snippet( + path: "articles/Snippets/2024/async-http-client-by-example/json" +) 1. Two ``Codable`` structures are defined: `Input` for the data to be sent and `Output` for receiving the JSON response. 2. An HTTP request is created using a POST method and a `content-type: application/json` header. @@ -93,7 +99,9 @@ The code snippet above demonstrates how to use Swift's Codable protocol to handl The AsyncHTTPClient library provides support for file downloads using the ``FileDownloadDelegate``. This feature enables asynchronous streaming of downloaded data while simultaneously reporting the download progress, as demonstrated in the following example: -@Snippet(path: "site/Snippets/ahc_download") +@Snippet( + path: "articles/Snippets/2024/async-http-client-by-example/download" +) 1. A ``FileDownloadDelegate`` is created to manage file downloads. 2. Specify the download destination path. diff --git a/Sources/Articles/Documentation.docc/2024/beginners-guide-to-protocol-buffers-and-grpc-with-swift/beginners-guide-to-protocol-buffers-and-grpc-with-swift.md b/Sources/Articles/Documentation.docc/2024/beginners-guide-to-protocol-buffers-and-grpc-with-swift/beginners-guide-to-protocol-buffers-and-grpc-with-swift.md index 1c188f6..73c224a 100644 --- a/Sources/Articles/Documentation.docc/2024/beginners-guide-to-protocol-buffers-and-grpc-with-swift/beginners-guide-to-protocol-buffers-and-grpc-with-swift.md +++ b/Sources/Articles/Documentation.docc/2024/beginners-guide-to-protocol-buffers-and-grpc-with-swift/beginners-guide-to-protocol-buffers-and-grpc-with-swift.md @@ -228,7 +228,10 @@ It's also a good practice to retain the original `.proto` files for reference an With the generated data types and interfaces in place, the server-side interface can now be implemented using the gRPC library. Below is an example of a simple actor-based implementation that utilizes in-memory storage and fulfills the `TodoService` protocol requirements: -@Snippet(path: "site/Snippets/beginners-guide-to-protocol-buffers-and-grpc-with-swift", slice: todo_service_protocol) +@Snippet( + path: "articles/Snippets/2024/beginners-guide-to-protocol-buffers-and-grpc-with-swift/snippets", + slice: "todo_service_protocol" +) The snippet above relies on several types from the gRPC library, such as `ServerRequest` and `ServerContext`, which are passed as arguments to each function call. The functions also use Swift data types generated from the `todo_messages.proto` file, ensuring that the required input and output data is provided correctly. @@ -236,7 +239,10 @@ The final step involves configuring the gRPC server. This can be achieved by cre This setup is built on the v2 gRPC library, which introduces support for modern concurrency features, including task groups and the Service Lifecycle library. Below is an example demonstrating how to configure the server using the upcoming gRPC v2 release: -@Snippet(path: "site/Snippets/beginners-guide-to-protocol-buffers-and-grpc-with-swift", slice: app_entrypoint) +@Snippet( + path: "articles/Snippets/2024/beginners-guide-to-protocol-buffers-and-grpc-with-swift/snippets", + slice: "app_entrypoint" +) 1. Creates a gRPC server using the specified `http2NIOPosix` transport layer with the provided configuration. 2. Add the `TodoService` as a service, which contains the logic for handling gRPC requests. diff --git a/Sources/Articles/Documentation.docc/2024/building-swiftnio-clients/building-swiftnio-clients.md b/Sources/Articles/Documentation.docc/2024/building-swiftnio-clients/building-swiftnio-clients.md index 2fb347d..f4bdde2 100644 --- a/Sources/Articles/Documentation.docc/2024/building-swiftnio-clients/building-swiftnio-clients.md +++ b/Sources/Articles/Documentation.docc/2024/building-swiftnio-clients/building-swiftnio-clients.md @@ -30,11 +30,17 @@ Add these dependencies to your executable target in your `Package.swift` file: Now, let's create a ``ClientBootstrap`` and configure it to use the ``/NIOHTTP1`` module's handlers. First, import the necessary modules: -@Snippet(path: "site/Snippets/building-swiftnio-clients-01", slice: "imports") +@Snippet( + path: "articles/Snippets/2024/building-swiftnio-clients/snippets", + slice: "imports" +) Then, create a ``ClientBootstrap``: -@Snippet(path: "site/Snippets/building-swiftnio-clients-01", slice: "bootstrap") +@Snippet( + path: "articles/Snippets/2024/building-swiftnio-clients/snippets", + slice: "bootstrap" +) This code prepares a template for creating a client channel. Let's break it down: @@ -49,11 +55,17 @@ Before creating the HTTP client, it's necessary to add a few types that are need When a `connect` fails, NIO already throws an error. There is no need to catch or represent those. However, the HTTP Client might encounter errors when processing the response. Create an enum to represent these errors: -@Snippet(path: "site/Snippets/building-swiftnio-clients-01", slice: "error") +@Snippet( + path: "articles/Snippets/2024/building-swiftnio-clients/snippets", + slice: "error" +) Finally, add an enum to represent the state of processing the response: -@Snippet(path: "site/Snippets/building-swiftnio-clients-01", slice: "partial") +@Snippet( + path: "articles/Snippets/2024/building-swiftnio-clients/snippets", + slice: "partial" +) The enum if pretty simple, and is not representative of a _mature_ HTTP client implementation such as [AsyncHTTPClient](https://github.com/swift-server/async-http-client). However, it's enough to get started with building a (TCP) client. @@ -61,7 +73,10 @@ The enum if pretty simple, and is not representative of a _mature_ HTTP client i Now that the necessary types have been created, create the `HTTPClient` type with a simple function that sends a request and returns the response. -@Snippet(path: "site/Snippets/building-swiftnio-clients-01", slice: "client") +@Snippet( + path: "articles/Snippets/2024/building-swiftnio-clients/snippets", + slice: "client" +) Let's break it down: @@ -75,7 +90,10 @@ Let's break it down: In place of the TODO comment, add the code to send a request and process the response. First, create a ``HTTPRequestHead``. Note that this function does not currently support sending a body with the request. Do so by adding the following code: -@Snippet(path: "site/Snippets/building-swiftnio-clients-01", slice: "executeThenClose") +@Snippet( + path: "articles/Snippets/2024/building-swiftnio-clients/snippets", + slice: "executeThenClose" +) This is a structured concurrency block that sends the request: @@ -84,7 +102,10 @@ This is a structured concurrency block that sends the request: Below that, receive and process the response parts as such: -@Snippet(path: "site/Snippets/building-swiftnio-clients-01", slice: "httpLogic") +@Snippet( + path: "articles/Snippets/2024/building-swiftnio-clients/snippets", + slice: "httpLogic" +) This sets up a state variable to keep track of the response parts received. It then processes the response parts as they come in: @@ -97,7 +118,10 @@ processes the response parts as they come in: Now that the HTTP client is complete, it's time to use it. Add the following code to the `main.swift` file: -@Snippet(path: "site/Snippets/building-swiftnio-clients-01", slice: "usage") +@Snippet( + path: "articles/Snippets/2024/building-swiftnio-clients/snippets", + slice: "usage" +) This creates a client and sends a GET request to `example.com`. The response is then printed to the console. diff --git a/Sources/Articles/Documentation.docc/2024/getting-started-with-hummingbird/getting-started-with-hummingbird.md b/Sources/Articles/Documentation.docc/2024/getting-started-with-hummingbird/getting-started-with-hummingbird.md index 4e070ee..b0079e1 100644 --- a/Sources/Articles/Documentation.docc/2024/getting-started-with-hummingbird/getting-started-with-hummingbird.md +++ b/Sources/Articles/Documentation.docc/2024/getting-started-with-hummingbird/getting-started-with-hummingbird.md @@ -75,7 +75,10 @@ You can read up on the result builder routers [here](https://docs.hummingbird.co Before adding routes, a Router is created: -@Snippet(path: "site/Snippets/HummingbirdApp", slice: router) +@Snippet( + path: "articles/Snippets/2024/getting-started-with-hummingbird/snippets", + slice: "router" +) Notice that a "context" is provided here. An instance of this context is created for each request that passes through your Hummingbird server. The default one is ``BasicRequestContext``, but you can customise this to your needs. A context must be a concrete type, and can conform to many protocols. Through this system, you can integrate with various different libraries that need to inject or read properties. @@ -83,7 +86,10 @@ Notice that a "context" is provided here. An instance of this context is created In the template, the first route has already been created. This is a `GET /health` route, as indicated in the function signature: -@Snippet(path: "site/Snippets/HummingbirdApp", slice: health) +@Snippet( + path: "articles/Snippets/2024/getting-started-with-hummingbird/snippets", + slice: "health" +) This route is a simple health check that returns a 200 OK status code. You can test this route by navigating to `http://localhost:8080/health` in your browser. Although it returns a status code, the body is empty, meaning you'll see an empty page. @@ -91,7 +97,10 @@ A route handler has two input parameters: a ``Request`` first, and the Context s Let's add a new GET route at the `/` path. This means that visiting your server at `http://localhost:8080` you'll see the response. -@Snippet(path: "site/Snippets/HummingbirdApp", slice: basic_route) +@Snippet( + path: "articles/Snippets/2024/getting-started-with-hummingbird/snippets", + slice: "basic_route" +) Rebuild and re-run your app, using Xcode, `swift run` or your other preferred method. Note that we've changed the return type to ``String`` to return a body. @@ -105,13 +114,19 @@ While returning these simple types is a nice way to get started, you'll quickly Create a type that conforms to ``ResponseCodable``, and return that from your route handler. This type will be encoded to the response body. -@Snippet(path: "site/Snippets/HummingbirdApp", slice: codable_route) +@Snippet( + path: "articles/Snippets/2024/getting-started-with-hummingbird/snippets", + slice: "codable_route" +) This route will return a JSON response with the message "Hello, world!" encoded within! From here, just run the app! -@Snippet(path: "site/Snippets/HummingbirdApp", slice: run) +@Snippet( + path: "articles/Snippets/2024/getting-started-with-hummingbird/snippets", + slice: "run" +) ## Conclusion diff --git a/Sources/Articles/Documentation.docc/2024/getting-started-with-mongokitten/getting-started-with-mongokitten.md b/Sources/Articles/Documentation.docc/2024/getting-started-with-mongokitten/getting-started-with-mongokitten.md index 62945fe..106d60a 100644 --- a/Sources/Articles/Documentation.docc/2024/getting-started-with-mongokitten/getting-started-with-mongokitten.md +++ b/Sources/Articles/Documentation.docc/2024/getting-started-with-mongokitten/getting-started-with-mongokitten.md @@ -40,13 +40,19 @@ let package = Package( Let's create our first connection: -@Snippet(path: "site/Snippets/mongokitten-basics", slice: "connection") +@Snippet( + path: "articles/Snippets/2024/getting-started-with-mongokitten/snippets", + slice: "connection" +) ### Defining Our Models For our social network, we'll create a Post model using the ``Codable`` protocol: -@Snippet(path: "site/Snippets/mongokitten-basics", slice: "models") +@Snippet( + path: "articles/Snippets/2024/getting-started-with-mongokitten/snippets", + slice: "models" +) The ``ObjectId`` type is MongoDB's native unique identifier, similar to a ``UUID``. Every document in MongoDB has an `_id` field, which must be unique within a collection. @@ -54,7 +60,10 @@ The ``ObjectId`` type is MongoDB's native unique identifier, similar to a ``UUID Let's create a function to save posts: -@Snippet(path: "site/Snippets/mongokitten-basics", slice: "insert") +@Snippet( + path: "articles/Snippets/2024/getting-started-with-mongokitten/snippets", + slice: "insert" +) 1. Create the post as a regular Swift struct 2. Access the posts ``MongoCollection``, this is similar to a table in a relational database @@ -66,7 +75,10 @@ In BSON, all entities are stored as '**documents**'. To read posts, we can use MongoKitten's query API: -@Snippet(path: "site/Snippets/mongokitten-basics", slice: "find-all") +@Snippet( + path: "articles/Snippets/2024/getting-started-with-mongokitten/snippets", + slice: "find-all" +) All the methods are chainable, and modify the query. MongoKitten will only execute the query when you call ``MongoCursor.drain``, or iterate over the results of any query. @@ -80,13 +92,19 @@ The drain function will execute the query and return the results as an array of MongoKitten supports filtering and sorting on most queries. -@Snippet(path: "site/Snippets/mongokitten-basics", slice: "find-by-author") +@Snippet( + path: "articles/Snippets/2024/getting-started-with-mongokitten/snippets", + slice: "find-by-author" +) The ``MongoCollection.find(_:) (Query)`` method returns a ``FindQueryBuilder``, a type of ``MongoCursor`` that allows you to chain more methods. ### Sorting and Limiting -@Snippet(path: "site/Snippets/mongokitten-basics", slice: "find-recent") +@Snippet( + path: "articles/Snippets/2024/getting-started-with-mongokitten/snippets", + slice: "find-recent" +) The **find** method accepts one argument, a filter. By providing the find filter, MongoDB will only return documents that match the filter. @@ -99,7 +117,10 @@ Then, chain the following methods: MongoDB, and by extension MongoKitten, uses ``BSON`` (Binary JSON) as its native data format. While MongoKitten handles most BSON conversions automatically through Codable, you can also work with BSON directly: -@Snippet(path: "site/Snippets/mongokitten-basics", slice: "bson") +@Snippet( + path: "articles/Snippets/2024/getting-started-with-mongokitten/snippets", + slice: "bson" +) ## Next Steps diff --git a/Sources/Articles/Documentation.docc/2024/getting-started-with-structured-concurrency-in-swift/getting-started-with-structured-concurrency-in-swift.md b/Sources/Articles/Documentation.docc/2024/getting-started-with-structured-concurrency-in-swift/getting-started-with-structured-concurrency-in-swift.md index 0d49479..0140c75 100644 --- a/Sources/Articles/Documentation.docc/2024/getting-started-with-structured-concurrency-in-swift/getting-started-with-structured-concurrency-in-swift.md +++ b/Sources/Articles/Documentation.docc/2024/getting-started-with-structured-concurrency-in-swift/getting-started-with-structured-concurrency-in-swift.md @@ -14,7 +14,10 @@ Imagine that you're shopping for groceries with a friend. You both have a list o Concurrency has been a part of Swift for a long time, for example, through the use of ``DispatchQueue`` and ``OperationQueue``. In these models, you can submit work to a queue, and the queue will execute the work in the background. Often times, you'll have to wait for the work to finish, either successfully or with an error. -@Snippet(path: "site/Snippets/getting-started-concurrency-dispatch", slice: "dispatchGlobal") +@Snippet( + path: "articles/Snippets/2024/getting-started-with-structured-concurrency-in-swift/dispatch", + slice: "dispatchGlobal" +) In these models, you're responsible for managing the lifecycle of the work. You'll need to ensure that work is properly cancelled when it's no longer needed. @@ -22,7 +25,10 @@ When implementing a function that has callbacks, you're responsible for calling Take the following example: -@Snippet(path: "site/Snippets/getting-started-concurrency-dispatch", slice: "fetchImageCallback") +@Snippet( + path: "articles/Snippets/2024/getting-started-with-structured-concurrency-in-swift/dispatch", + slice: "fetchImageCallback" +) In this example, various bugs can arise. For example, in `if let error`, omitting the `return` statement will cause the completion handler to be called twice. @@ -32,7 +38,9 @@ When accessing shared state from concurrently running code, it's critical to ens Race conditions need to be carefully and correctly solved. When using a mutex/lock to protect shared state, you need to ensure that this lock starts and ends at the right time. And when working with locks in long running calls such as network calls, you need to be careful to avoid performance bottlenecks. Finally, you can cause deadlocks when multiple functions that call each other access the same lock. Take the following example: -@Snippet(path: "site/Snippets/getting-started-concurrency-imagecache-locks") +@Snippet( + path: "articles/Snippets/2024/getting-started-with-structured-concurrency-in-swift/locks" +) The above example is non-trivial. It's not always obvious that you need to lock access to `image` twice. There are not one, but four traps here. @@ -98,7 +106,10 @@ func watchTelevision() async throws { The `Task` object is not the only way to run concurrent work. The simplest way of running an `async` function in parallel is using the `async let` construct. This is a _structured_ way to start a task and let it run until you need the result: -@Snippet(path: "site/Snippets/getting-started-concurrency-buy-books", slice: "buyBooks") +@Snippet( + path: "articles/Snippets/2024/getting-started-with-structured-concurrency-in-swift/books", + slice: "buyBooks" +) When this `async let` is not `await`ed for, it will continue to run in the background until the end of the function. If the function returns without awaiting the `async let`, the task will be cancelled. @@ -110,13 +121,19 @@ Like how a for-loop iterates over a sequence of items, a for-await-in loop itera You can create your own sequences, using ``AsyncStream`` or your own implementation. -@Snippet(path: "site/Snippets/getting-started-concurrency-buy-books", slice: "asyncSequence") +@Snippet( + path: "articles/Snippets/2024/getting-started-with-structured-concurrency-in-swift/books", + slice: "asyncSequence" +) These can be used to represent an asynchronous set of results, such as incoming messages on a websocket. Since an ``AsyncIteratorProtocol`` implementation can throw, it can even exit the for-loop on network errors. -@Snippet(path: "site/Snippets/getting-started-concurrency-buy-books", slice: "browseBooks") +@Snippet( + path: "articles/Snippets/2024/getting-started-with-structured-concurrency-in-swift/books", + slice: "browseBooks" +) This is a powerful feature, as it allows you to easily reason about _streams_ of data. On iOS, this can be a stream of keyboard events, StoreKit purchases, notifications or sensor data. For backend developers, this can be a WebSocket, a database query or the incoming connections on a TCP server. If you're interested in that, please check out our tutorial on . @@ -136,7 +153,10 @@ A task is a concurrent unit of work. In concurrency, many tasks can run in paral The _easiest_ way to create a task is using the **unstructured** `Task` type. It's used to run a piece of code concurrently in the background, similar to `DispatchQueue.global().async {}`. In addition, you can manage it's lifecycle by `cancel()`ing it. Finally, you can also `await` its `value` for it to finish. -@Snippet(path: "site/Snippets/getting-started-concurrency-buy-books", slice: "buyBooksInBackground") +@Snippet( + path: "articles/Snippets/2024/getting-started-with-structured-concurrency-in-swift/books", + slice: "buyBooksInBackground" +) This looks great in theory, but the `Task` object is _not_ structured. It's not clear when the task starts, when it ends, and what happens when it's cancelled. You're required to manage the lifecycle of the task yourself, and don't even need to await the result or handle errors. This is inherently unsafe and re-introduces the problems that structured concurrency is designed to solve. diff --git a/Sources/Articles/Documentation.docc/2024/getting-started-with-swiftpm-snippets/getting-started-with-swiftpm-snippets.md b/Sources/Articles/Documentation.docc/2024/getting-started-with-swiftpm-snippets/getting-started-with-swiftpm-snippets.md index 240c3ec..3ce5917 100644 --- a/Sources/Articles/Documentation.docc/2024/getting-started-with-swiftpm-snippets/getting-started-with-swiftpm-snippets.md +++ b/Sources/Articles/Documentation.docc/2024/getting-started-with-swiftpm-snippets/getting-started-with-swiftpm-snippets.md @@ -51,11 +51,17 @@ $ swift package init --name 'Swift Snippets' This should initialize a new Swift package with a `Package.swift` resembling the following. -@Code(name: "Package.swift", file: Manifest.1.swift) +@Code( + name: "Package.swift", + file: "Manifest.1.swift" +) Rename the library target to `SnippetsExample`. -@Code(name: "Package.swift", file: Manifest.2.swift) +@Code( + name: "Package.swift", + file: "Manifest.2.swift" +) Make sure the `Sources/SnippetsExample` directory contains at least one Swift file. For this tutorial, we will add an empty file named `anchor.swift`. @@ -69,7 +75,9 @@ $ mkdir Snippets Inside the `Snippets` directory, create a new Swift file named `SnippetsExample_I.swift`. -@Snippet(id: "SnippetsExample_I") +@Snippet( + path: "articles/Snippets/2024/getting-started-with-swiftpm-snippets/example-01", +) The directory structure should now look like this: @@ -142,7 +150,10 @@ $ mkdir -p Sources/SnippetsExample/docs.docc Create a markdown article named `My article.md` in the `docs.docc` directory. -@Code(name: "My article.md", file: "My article (1).md.txt") +@Code( + name: "My article.md", + file: "My article (1).md.txt" +) In the example above, we have specified the Snippet to include by `path` identity. @@ -152,7 +163,10 @@ If the Snippet ID contains special characters, you should pass the ID as-is, wit Some documentation engines such as [Unidoc](https://github.com/tayloraswift/swift-unidoc) support referencing Snippets by `id`. -@Code(name: "My article.md", file: "My article (2).md.txt") +@Code( + name: "My article.md", + file: "My article (2).md.txt" +) > Important: > DocC does not currently support referencing Snippets by ID. Instead, you must use the fully-qualified `path` syntax to reference a Snippet. @@ -177,7 +191,10 @@ The project layout should now look like this: Many developers find [DocC](https://github.com/apple/swift-docc) helpful for previewing documentation locally. To use DocC, add the [swift-docc-plugin](https://github.com/apple/swift-docc-plugin) to the package manifest. -@Code(name: Package.swift, file: Manifest.3.swift) +@Code( + name: "Package.swift", + file: "Manifest.3.swift" +) Please note that while it is possible to build DocC documentation using Xcode, Snippets will not render, due to [FB13482049](http://ww.openradar.appspot.com/FB13482049). @@ -197,13 +214,18 @@ You can find the rendered article at [`http://localhost:8080/documentation/snipp If a Snippet begins with contiguous line comments, those comments will be parsed as Markdown and treated as a Snippet caption. Try adding the following code to a Snippet named `SnippetsExample_II.swift`. -@Code(name: "SnippetsExample_II.swift", file: SnippetsExample_II.swift) +@Code( + name: "SnippetsExample_II.swift", + file: SnippetsExample_II.swift +) When embedded, it should look like this: --- -@Snippet(id: SnippetsExample_II) +@Snippet( + path: "articles/Snippets/2024/getting-started-with-swiftpm-snippets/example-02", +) > Note: > Snippet captions cannot embed other Snippets. @@ -222,14 +244,18 @@ You can redact portions of a Snippet using **slice directives**. A slice directi Below is an example of a Snippet that uses redactions to hide the `import` statements. -@Code(name: SnippetsExample_III.swift, file: SnippetsExample_III.swift) +@Code( + name: "SnippetsExample_III.swift", + file: "SnippetsExample_III.swift" +) When embedded, it should look like this: --- -@Snippet(id: SnippetsExample_III) - +@Snippet( + path: "articles/Snippets/2024/getting-started-with-swiftpm-snippets/example-03", +) > Warning: > At the time of writing, there is a [known bug](https://github.com/apple/swift-docc/issues/946) preventing DocC from slicing Snippets that contain multi-line string literals correctly. @@ -239,14 +265,18 @@ When embedded, it should look like this: The indentation of the first `snippet.show` determines specifies the maximum amount of indentation to remove from the Snippet. In the example below, four spaces of indentation will be removed from the rendered Snippet. Note that the `snippet.end` token is required in order to prevent the Snippet from including the final brace, which would have prevented the indentation from being removed. -@Code(name: SnippetsExample_IV.swift, file: SnippetsExample_IV.swift) +@Code( + name: "SnippetsExample_IV.swift", + file: "SnippetsExample_IV.swift" +) When embedded, it should look like this: --- -@Snippet(id: SnippetsExample_IV) - +@Snippet( + path: "articles/Snippets/2024/getting-started-with-swiftpm-snippets/example-04", +) ## Using named slices @@ -254,20 +284,36 @@ You can also use **named slices** to create Snippets with multiple embeddable se Below is an example of a Snippet with a caption and three named slices. -@Code(name: SnippetsExample_V.swift, file: SnippetsExample_V.swift) +@Code( + name: "SnippetsExample_V.swift", + file: "SnippetsExample_V.swift" +) Here’s how you might embed the slices in a Markdown article. -@Code(name: "My article.md", file: "My article (3).md.txt") +@Code( + name: "My article.md", + file: "My article (3).md.txt" +) And here’s how the embedded slices should look. --- -@Snippet(id: SnippetsExample_V, slice: DECLARATION) -@Snippet(id: SnippetsExample_V, slice: BODY) -@Snippet(id: SnippetsExample_V, slice: EXIT) +@Snippet( + path: "articles/Snippets/2024/getting-started-with-swiftpm-snippets/example-05", + slice: "DECLARATION" +) + +@Snippet( + path: "articles/Snippets/2024/getting-started-with-swiftpm-snippets/example-05", + slice: "BODY" +) +@Snippet( + path: "articles/Snippets/2024/getting-started-with-swiftpm-snippets/example-05", + slice: "EXIT" +) ## Where to go from here diff --git a/Sources/Articles/Documentation.docc/2024/how-to-build-a-proxy-server-with-hummingbird/how-to-build-a-proxy-server-with-hummingbird.md b/Sources/Articles/Documentation.docc/2024/how-to-build-a-proxy-server-with-hummingbird/how-to-build-a-proxy-server-with-hummingbird.md index 5cc6e37..f82fe85 100644 --- a/Sources/Articles/Documentation.docc/2024/how-to-build-a-proxy-server-with-hummingbird/how-to-build-a-proxy-server-with-hummingbird.md +++ b/Sources/Articles/Documentation.docc/2024/how-to-build-a-proxy-server-with-hummingbird/how-to-build-a-proxy-server-with-hummingbird.md @@ -16,7 +16,10 @@ Finally, you can forward every request by intercepting them all in a custom ``HT Each of these solutions will need to import the necessary modules: -@Snippet(path: "site/Snippets/hummingbird-2-proxy", slice: "imports") +@Snippet( + path: "articles/Snippets/2024/how-to-build-a-proxy-server-with-hummingbird/snippets", + slice: "imports" +) ## Forwarding Requests @@ -26,7 +29,10 @@ To forward a request, you need to create a new ``HTTPClientRequest`` as defined The HTTP Client is swappable in this function, but the rest of this tutorial will use the ``HTTPClient.shared``. -@Snippet(path: "site/Snippets/hummingbird-2-proxy", slice: "forwarding") +@Snippet( + path: "articles/Snippets/2024/how-to-build-a-proxy-server-with-hummingbird/snippets", + slice: "forwarding" +) As you see, there are four steps to forward a request: @@ -45,27 +51,42 @@ The first option is to create a route on the ``Router`` that will intercept the Since the code is the same as the previous example, we will only need to invoke the `forward` function from within this middleware. -@Snippet(path: "site/Snippets/hummingbird-2-proxy", slice: "route") +@Snippet( + path: "articles/Snippets/2024/how-to-build-a-proxy-server-with-hummingbird/snippets", + slice: "route" +) ### Middleware For the second option you need to create a new ``RouterMiddleware``. This middleware will intercept the request and forward it to the target server. -@Snippet(path: "site/Snippets/hummingbird-2-proxy", slice: "middleware") +@Snippet( + path: "articles/Snippets/2024/how-to-build-a-proxy-server-with-hummingbird/snippets", + slice: "middleware" +) Then, once the middleware is set up, you can ``Router.add(middleware:)`` to the router. -@Snippet(path: "site/Snippets/hummingbird-2-proxy", slice: "middleware-router") +@Snippet( + path: "articles/Snippets/2024/how-to-build-a-proxy-server-with-hummingbird/snippets", + slice: "middleware-router" +) ### Custom Responder The final option will implement an ``HTTPResponder``. -@Snippet(path: "site/Snippets/hummingbird-2-proxy", slice: "responder") +@Snippet( + path: "articles/Snippets/2024/how-to-build-a-proxy-server-with-hummingbird/snippets", + slice: "responder" +) Then, you can set up the responder in the ``Application``. -@Snippet(path: "site/Snippets/hummingbird-2-proxy", slice: "setup-responder") +@Snippet( + path: "articles/Snippets/2024/how-to-build-a-proxy-server-with-hummingbird/snippets", + slice: "setup-responder" +) ## Conclusion diff --git a/Sources/Articles/Documentation.docc/2024/realtime-mongodb-updates-with-changestreams-and-websockets/realtime-mongodb-updates-with-changestreams-and-websockets.md b/Sources/Articles/Documentation.docc/2024/realtime-mongodb-updates-with-changestreams-and-websockets/realtime-mongodb-updates-with-changestreams-and-websockets.md index 3e0a597..458d78c 100644 --- a/Sources/Articles/Documentation.docc/2024/realtime-mongodb-updates-with-changestreams-and-websockets/realtime-mongodb-updates-with-changestreams-and-websockets.md +++ b/Sources/Articles/Documentation.docc/2024/realtime-mongodb-updates-with-changestreams-and-websockets/realtime-mongodb-updates-with-changestreams-and-websockets.md @@ -23,7 +23,10 @@ Make sure you have MongoDB running locally before starting. The `ConnectionManager` handles WebSocket connections and MongoDB change notifications: -@Snippet(path: "site/Snippets/realtime-mongodb-app", slice: "connection-manager") +@Snippet( + path: "articles/Snippets/2024/realtime-mongodb-updates-with-changestreams-and-websockets/snippets", + slice: "connection-manager" +) 1. The manager is an actor to ensure thread-safe access to connections 2. It maintains a dictionary of active WebSocket connections @@ -38,7 +41,10 @@ The use of `withRegisteredClient` ensures that the WebSocket connection is prope Now that the `ConnectionManager` is implemented, we can watch for changes in the MongoDB database. For this, we'll tie the `ConnectionManager` to the application lifecycle using the ``Service`` protocol. -@Snippet(path: "site/Snippets/realtime-mongodb-app", slice: "watch-changes") +@Snippet( + path: "articles/Snippets/2024/realtime-mongodb-updates-with-changestreams-and-websockets/snippets", + slice: "watch-changes" +) 1. Get a reference to the posts collection 2. Create a change stream watching for post changes @@ -53,7 +59,10 @@ This flow is very scalable, as only one ChangeStream is created and maintained p Let's create the main application entry point: -@Snippet(path: "site/Snippets/realtime-mongodb-app", slice: "main") +@Snippet( + path: "articles/Snippets/2024/realtime-mongodb-updates-with-changestreams-and-websockets/snippets", + slice: "main" +) 1. Connect to MongoDB 2. Create the connection manager @@ -64,7 +73,10 @@ Let's create the main application entry point: ### Adding Routes -@Snippet(path: "site/Snippets/realtime-mongodb-app", slice: "routes") +@Snippet( + path: "articles/Snippets/2024/realtime-mongodb-updates-with-changestreams-and-websockets/snippets", + slice: "routes" +) This snippet adds a POST route to the application that creates a new post in the database. That process then triggers the change streams, which broadcast to all connected clients. diff --git a/Sources/Articles/Documentation.docc/2024/structured-concurrency-and-shared-state-in-swift/structured-concurrency-and-shared-state-in-swift.md b/Sources/Articles/Documentation.docc/2024/structured-concurrency-and-shared-state-in-swift/structured-concurrency-and-shared-state-in-swift.md index 48123c9..5df5da8 100644 --- a/Sources/Articles/Documentation.docc/2024/structured-concurrency-and-shared-state-in-swift/structured-concurrency-and-shared-state-in-swift.md +++ b/Sources/Articles/Documentation.docc/2024/structured-concurrency-and-shared-state-in-swift/structured-concurrency-and-shared-state-in-swift.md @@ -14,7 +14,10 @@ Classes are not automatically Sendable. Since reference types are explicitly not If you're working with a class that is not a set of constants, you can still mark it as Sendable by using the `@unchecked Sendable` conformance. When you use this conformance, you're telling the compiler that you're sure that the class is Sendable, and that you're taking responsibility of isolating the state. In this case, you can adopt your own isolation such as Locks. -@Snippet(path: "site/Snippets/shared-state", slice: "sharedState") +@Snippet( + path: "articles/Snippets/2024/structured-concurrency-and-shared-state-in-swift/snippets", + slice: "sharedState" +) ### Actors and Isolation @@ -28,13 +31,19 @@ When accessing an actor's state or calling its functions, you can prefix your ca You can define an actor like so: -@Snippet(path: "site/Snippets/shared-state", slice: "bankAccount") +@Snippet( + path: "articles/Snippets/2024/structured-concurrency-and-shared-state-in-swift/snippets", + slice: "bankAccount" +) Just like any type, you can make an `extension` on an actor. Actors can also conform to protocols, assuming that the protocol's signature can be feasibly implemented with isolation. A common obstacle is that you can't easily conform to a protocol that has properties or methods that are not isolated. An actor's isolation is inherited by its properties and methods. Actor Isolation is compile-time checked to ensures that only one task can access the actor's state at a time. This is achieved through the ``Actor/unownedExecutor`` of an actor. This is a ``SerialExecutor`` that the Swift runtime submits tasks to, which provides the isolation in this actor. The SerialExecutor may be a single thread, or multiple. But needs to guarantee that only one task is running on this at a time. Akin to `DispatchQueue.main.async { }` in GCD. -@Snippet(path: "site/Snippets/shared-state", slice: "unownedExecutor") +@Snippet( + path: "articles/Snippets/2024/structured-concurrency-and-shared-state-in-swift/snippets", + slice: "unownedExecutor" +) You can create your own ``SerialExecutor`` for use with your actors. SwiftNIO's EventLoop already has a ``EventLoop/executor [requirement: false]`` property that you can use. ``/Dispatch``'s ``DispatchQueue`` can be adapted easily as well. @@ -44,7 +53,10 @@ Since ``Actor/unownedExecutor`` is not a static member of an actor, an actor's s You can use the `nonisolated` keyword to mark a function as lacking isolation. This allows you to access these functions without the `await` keyword, and conform to protocols that have non-isolated methods. -@Snippet(path: "site/Snippets/shared-state", slice: "bookStore") +@Snippet( + path: "articles/Snippets/2024/structured-concurrency-and-shared-state-in-swift/snippets", + slice: "bookStore" +) Starting with Swift 5.10, `nonisolated(unsafe)` can be used to opt-out of actor isolation checking for stored properties. This is useful to expose a property or method to the outside world, but you're sure that it's safe to do so. In this case, you're taking responsibility of isolating the state. @@ -52,7 +64,10 @@ Starting with Swift 5.10, `nonisolated(unsafe)` can be used to opt-out of actor The alternative way to conform to protocols, is for the _protocol_ to be aware of the actor's isolation. This is done by using `async` computed properties. -@Snippet(path: "site/Snippets/shared-state", slice: "bankAccountProtocol") +@Snippet( + path: "articles/Snippets/2024/structured-concurrency-and-shared-state-in-swift/snippets", + slice: "bankAccountProtocol" +) Because actor isolation makes these functions and properties `async`, this actor can now to the defined protocol. @@ -72,13 +87,19 @@ Because of re-entrancy, multiple tasks can call functions on the same actor at t Let's take the image cache example as an actor: -@Snippet(path: "site/Snippets/shared-state", slice: "imageCache") +@Snippet( + path: "articles/Snippets/2024/structured-concurrency-and-shared-state-in-swift/snippets", + slice: "imageCache" +) The above function is an implementation of the image cache. It's a simple actor that allows storing and retrieving images by URL. Since actors are re-entrant, `loadImage` can be ran multiple times concurrently. This can lead to multiple fetches of the same image, and multiple writes to the cache. Your code can still be correct and crash-free, but can be inefficient. -@Snippet(path: "site/Snippets/shared-state", slice: "loadingAwareImageCache") +@Snippet( + path: "articles/Snippets/2024/structured-concurrency-and-shared-state-in-swift/snippets", + slice: "loadingAwareImageCache" +) The above function is an improved implementation of the image cache. By tracking the URLs that are currently being loaded, you can avoid fetching the same image multiple times. @@ -113,7 +134,10 @@ final class NeedsImage { By explicitly creating a capture group, you'll only retain the values needed. See the following example: -@Snippet(path: "site/Snippets/shared-state", slice: "captureGroups") +@Snippet( + path: "articles/Snippets/2024/structured-concurrency-and-shared-state-in-swift/snippets", + slice: "captureGroups" +) ### @Sendable Functions @@ -121,7 +145,10 @@ When marking functions as `@Sendable`, you're telling Swift that the function is Callback function arguments can be marked `@Sendable` as such: -@Snippet(path: "site/Snippets/shared-state", slice: "findImages") +@Snippet( + path: "articles/Snippets/2024/structured-concurrency-and-shared-state-in-swift/snippets", + slice: "findImages" +) Finally, regular functions can be marked `@Sendable` as well: @@ -138,7 +165,10 @@ So far, we've been using `await` to wait for a value to be available. But not al A continuation is a way to capture the current state of a task, and to resume the task at a later point. Let's implement a simple continuation that fetches an image: -@Snippet(path: "site/Snippets/shared-state", slice: "continuations") +@Snippet( + path: "articles/Snippets/2024/structured-concurrency-and-shared-state-in-swift/snippets", + slice: "continuations" +) There are two variations of continuations. @@ -160,7 +190,10 @@ Let's go back to the ImageCache example. In that example, the `loadImage` functi We can restructure the `loadImage` function to use a continuation: -@Snippet(path: "site/Snippets/shared-state", slice: "efficientImageCache") +@Snippet( + path: "articles/Snippets/2024/structured-concurrency-and-shared-state-in-swift/snippets", + slice: "efficientImageCache" +) **Note:** When creating a continuation, you're starting a new workload that does not (yet) adopt structured concurrency. When this happens, this code is also responsible for ensuring that Task Cancellation is handled propertly. For that, please refer back to ``withTaskCancellationHandler(operation:onCancel:isolation:)`` earlier in this article. @@ -215,7 +248,10 @@ This frees up the actor to continue processing other tasks, and prevents the act Custom global actors can be created through the `@globalActor` attribute: -@Snippet(path: "site/Snippets/shared-state", slice: "globalActor") +@Snippet( + path: "articles/Snippets/2024/structured-concurrency-and-shared-state-in-swift/snippets", + slice: "globalActor" +) With this addition, you can isolate properties, functions _and types_ to the `SensorActor`: diff --git a/Sources/Articles/Documentation.docc/2024/using-environment-variables-in-swift/using-environment-variables-in-swift.md b/Sources/Articles/Documentation.docc/2024/using-environment-variables-in-swift/using-environment-variables-in-swift.md index f29b631..8e98382 100644 --- a/Sources/Articles/Documentation.docc/2024/using-environment-variables-in-swift/using-environment-variables-in-swift.md +++ b/Sources/Articles/Documentation.docc/2024/using-environment-variables-in-swift/using-environment-variables-in-swift.md @@ -13,7 +13,10 @@ In Swift, it is possible to access environment variables using the [ProcessInfo] Here's a quick example how to get the value of the `LOG_LEVEL` variable: -@Snippet(path: "site/Snippets/environment", slice: "processInfo") +@Snippet( + path: "articles/Snippets/2024/using-environment-variables-in-swift/snippets", + slice: "processInfo" +) The process info's environment is represented a `[String: String]` dictionary. When requesting a specific key, the value is going to be an optional `String` type. @@ -153,7 +156,10 @@ Vapor will look for dotenv files in the current working directory. If you're usi In Hummingbird, it is possible to use the shared environment or load dotenv files using the static `dotEnv()` method on the [HBEnvironment](https://hummingbird-project.github.io/hummingbird-docs/1.0/documentation/hummingbirdauth/hbenvironment) struct: -@Snippet(path: "site/Snippets/environment", slice: "hummingbird") +@Snippet( + path: "articles/Snippets/2024/using-environment-variables-in-swift/snippets", + slice: "hummingbird" +) If you run the project from Xcode, make sure you set a custom working directory, otherwise the framework won't be able to locate your dotenv file. diff --git a/Sources/Articles/Documentation.docc/2024/using-hummingbird-request-context/using-hummingbird-request-context.md b/Sources/Articles/Documentation.docc/2024/using-hummingbird-request-context/using-hummingbird-request-context.md index 65547f7..330df98 100644 --- a/Sources/Articles/Documentation.docc/2024/using-hummingbird-request-context/using-hummingbird-request-context.md +++ b/Sources/Articles/Documentation.docc/2024/using-hummingbird-request-context/using-hummingbird-request-context.md @@ -24,13 +24,19 @@ Hummingbird provides a very simple context called ``BasicRequestContext``, which To create a custom context, create a struct that conforms to the ``RequestContext`` protocol. This struct stores any properties related to the request. -@Snippet(path: "site/Snippets/HummingbirdRequestContext", slice: custom_request_context) +@Snippet( + path: "articles/Snippets/2024/using-hummingbird-request-context/snippets", + slice: "custom_request_context" +) The context is not to be used for dependency injection, such as a database connection. If a property is shared between requests, inject that type in the controller instead. From here, instantiate a ``Router`` instance using the new context as a basis. -@Snippet(path: "site/Snippets/HummingbirdRequestContext", slice: router) +@Snippet( + path: "articles/Snippets/2024/using-hummingbird-request-context/snippets", + slice: "router" +) ### Authentication @@ -52,7 +58,10 @@ First, create a middleware type that conforms to the ``RouterMiddleware`` protoc While middleware can specify a `typealias` to constrain to a specific context, it's also possible make a middleware generic. -@Snippet(path: "site/Snippets/HummingbirdRequestContext", slice: simple_middleware) +@Snippet( + path: "articles/Snippets/2024/using-hummingbird-request-context/snippets", + slice: "simple_middleware" +) ### Context Protocols @@ -60,11 +69,17 @@ In the example above, the middleware specifies the type of Context it needs. How First, specify a protocol that the context must conform to. This protocol can be as simple as a marker protocol, or it can specify properties that the middleware needs. -@Snippet(path: "site/Snippets/HummingbirdRequestContext", slice: context_protocol) +@Snippet( + path: "articles/Snippets/2024/using-hummingbird-request-context/snippets", + slice: "context_protocol" +) Then, remove the `typealias` and replace it with a generic parameter. This parameter is constrained to the new protocol. -@Snippet(path: "site/Snippets/HummingbirdRequestContext", slice: context_protocol_middleware) +@Snippet( + path: "articles/Snippets/2024/using-hummingbird-request-context/snippets", + slice: "context_protocol_middleware" +) That's all you need to do! Now, the middleware can be used with any context that conforms to the protocol. diff --git a/Sources/Articles/Documentation.docc/2024/using-swiftnio-channels/using-swiftnio-channels.md b/Sources/Articles/Documentation.docc/2024/using-swiftnio-channels/using-swiftnio-channels.md index b02efbf..6c70dd6 100644 --- a/Sources/Articles/Documentation.docc/2024/using-swiftnio-channels/using-swiftnio-channels.md +++ b/Sources/Articles/Documentation.docc/2024/using-swiftnio-channels/using-swiftnio-channels.md @@ -56,7 +56,10 @@ In order to create a TCP server, you'll first need to create a ``ServerBootstrap ServerBootstrap requires an ``EventLoopGroup`` to run on. This is a group of EventLoops that the server will use to run on. Each client will be handled by a single specific ``EventLoop``, that is randomly assigned. This helps your server scale to many threads (and cores) without having to worry about thread-safety. -@Snippet(path: "site/Snippets/using-swiftnio-channels", slice: "bootstrap") +@Snippet( + path: "articles/Snippets/2024/using-swiftnio-channels/snippets", + slice: "bootstrap" +) The above code can create a TCP server, without any logic to accept or communicate with clients. Let's go over the code step-by-step: @@ -72,7 +75,10 @@ The above code can create a TCP server, without any logic to accept or communica With this newly created server, this code can start accepting clients. Let's implement that: -@Snippet(path: "site/Snippets/using-swiftnio-channels", slice: "acceptClients") +@Snippet( + path: "articles/Snippets/2024/using-swiftnio-channels/snippets", + slice: "acceptClients" +) This code is an implementation of the server bootstrap that was created in the previous snippet. Let's go over the code step-by-step: @@ -86,7 +92,10 @@ This code is an implementation of the server bootstrap that was created in the p The server is not able to accept client, but can not yet communicate with them. Let's implement that: -@Snippet(path: "site/Snippets/using-swiftnio-channels", slice: "handleClient") +@Snippet( + path: "articles/Snippets/2024/using-swiftnio-channels/snippets", + slice: "handleClient" +) This code receives messages from a client, and echoes it back. It's functional, efficient and easy to understand. Let's go over the code step-by-step: diff --git a/Sources/Articles/Documentation.docc/2024/websockets-tutorial-using-swift-and-hummingbird/websockets-tutorial-using-swift-and-hummingbird.md b/Sources/Articles/Documentation.docc/2024/websockets-tutorial-using-swift-and-hummingbird/websockets-tutorial-using-swift-and-hummingbird.md index a83a076..a07b988 100644 --- a/Sources/Articles/Documentation.docc/2024/websockets-tutorial-using-swift-and-hummingbird/websockets-tutorial-using-swift-and-hummingbird.md +++ b/Sources/Articles/Documentation.docc/2024/websockets-tutorial-using-swift-and-hummingbird/websockets-tutorial-using-swift-and-hummingbird.md @@ -93,7 +93,10 @@ let package = Package( The `App.swift` file is the main entry point for a Hummingbird application using the ``ArgumentParser`` library. -@Snippet(path: "site/Snippets/websockets", slice: "entrypoint") +@Snippet( + path: "articles/Snippets/2024/websockets-tutorial-using-swift-and-hummingbird/snippets", + slice: "entrypoint" +) 1. The `AppArguments` protocol defines hostname and port properties. 2. The `HummingbirdArguments` structure is the main entry point, using ``AsyncParsableCommand``, and sets command-line options. @@ -101,7 +104,10 @@ The `App.swift` file is the main entry point for a Hummingbird application using The code inside the `Application+build.swift` file sets up a Hummingbird application configured for WebSocket communication. It defines a function buildApplication that takes command-line arguments for hostname and port, initializes a logger, and sets up routers with middlewares for logging and file handling. It creates a `ConnectionManager` for managing WebSocket connections and configures the WebSocket router to handle chat connections, upgrading the connection if a username is provided: -@Snippet(path: "site/Snippets/websockets", slice: "buildApplication") +@Snippet( + path: "articles/Snippets/2024/websockets-tutorial-using-swift-and-hummingbird/snippets", + slice: "buildApplication" +) 1. A ``Router`` instance is created, and middlewares for logging requests and serving files are added to it. 2. A `ConnectionManager` instance is created with a logger for managing WebSocket connections. @@ -111,14 +117,18 @@ The code inside the `Application+build.swift` file sets up a Hummingbird applica The application is configured to use HTTP with WebSocket upgrades and includes WebSocket compression. Finally, the application is returned with the necessary services added: -@Snippet(path: "site/Snippets/websockets-app") +@Snippet( + path: "articles/Snippets/2024/websockets-tutorial-using-swift-and-hummingbird/app" +) 1. An `Application` instance is created with the previously configured routers for both HTTP and WebSocket requests. 2. The `ConnectionManager` is added as a service to the application before returning it. The `ConnectionManager` struct manages WebSocket connections, allowing users to join, send messages, and leave the chat, using an `AsyncStream` for connection handling and Actor for managing outbound connections: -@Snippet(path: "site/Snippets/websockets-connection-manager-types") +@Snippet( + path: "articles/Snippets/2024/websockets-tutorial-using-swift-and-hummingbird/connection-manager-types" +) 1. The `ConnectionManager` implements the ``Service`` protocol to manage WebSocket connections and ensure graceful shutdown. 2. ``OutputStream`` is defined as an ``AsyncChannel`` for sending WebSocket outbound frames. @@ -127,11 +137,16 @@ The `ConnectionManager` struct manages WebSocket connections, allowing users to The `addUser` function creates a `Connection` object with a given name and WebSocket streams, yields this connection, and returns a new ``OutputStream``: -@Snippet(path: "site/Snippets/websockets-connection-manager-add-user") +@Snippet( + path: "articles/Snippets/2024/websockets-tutorial-using-swift-and-hummingbird/connection-manager-add-user" +) The `init(logger:)` method creates an asynchronous stream for `Connection` objects along with a logger, and the `run` function asynchronously handles connections by logging their addition, processing inbound messages, sending outbound messages, and logging their removal, with graceful shutdown support: -@Snippet(path: "site/Snippets/websockets", slice: "ConnectionManager") +@Snippet( + path: "articles/Snippets/2024/websockets-tutorial-using-swift-and-hummingbird/snippets", + slice: "ConnectionManager" +) 1. The `run` function iterates through the `connectionStream` asynchronously to handle incoming connections and messages. 2. For each connection, a task is added to the group to manage the connection and broadcast the "joined" message. diff --git a/Sources/Articles/Documentation.docc/2024/whats-new-in-hummingbird-2/whats-new-in-hummingbird-2.md b/Sources/Articles/Documentation.docc/2024/whats-new-in-hummingbird-2/whats-new-in-hummingbird-2.md index 863e216..e7ed9d2 100644 --- a/Sources/Articles/Documentation.docc/2024/whats-new-in-hummingbird-2/whats-new-in-hummingbird-2.md +++ b/Sources/Articles/Documentation.docc/2024/whats-new-in-hummingbird-2/whats-new-in-hummingbird-2.md @@ -36,7 +36,9 @@ Hummingbird 2 features a brand new routing library, based on Swift result builde Here's a little peak into the usage of the new ``RouterBuilder`` object: -@Snippet(path: "site/Snippets/hummingbird-2-routerbuilder") +@Snippet( + path: "articles/Snippets/2024/whats-new-in-hummingbird-2/router-builder" +) ## Generic request context @@ -87,7 +89,10 @@ let package = Package( Here's how to build a custom decoder to handle different media types on your backend server: -@Snippet(path: "site/Snippets/hummingbird-2", slice: "requestDecoder") +@Snippet( + path: "articles/Snippets/2024/whats-new-in-hummingbird-2/snippets", + slice: "requestDecoder" +) 1. Define the custom decoder by implementing the ``RequestDecoder`` protocol. 2. Make sure that the incoming request has a `Content-Type` HTTP header field. @@ -97,7 +102,10 @@ Here's how to build a custom decoder to handle different media types on your bac To use the custom decoder, let's define a custom request context. A request context is a container for the Hummingbird framework to store information needed by the framework. The following snippet demonstrates how to build one using the _RequestContext_ protocol: -@Snippet(path: "site/Snippets/hummingbird-2", slice: "requestContext") +@Snippet( + path: "articles/Snippets/2024/whats-new-in-hummingbird-2/snippets", + slice: "requestContext" +) 1. Define a custom `MyRequestContext` protocol using the _RequestContext_ protocol. 2. Implement the `MyRequestContext` protocol using a `MyBaseRequestContext` struct. @@ -110,7 +118,10 @@ It is possible to compose multiple protocols such as _AuthRequestContext_ by con Create the application instance using the `buildApplication` function. -@Snippet(path: "site/Snippets/hummingbird-2", slice: "buildApp") +@Snippet( + path: "articles/Snippets/2024/whats-new-in-hummingbird-2/snippets", + slice: "buildApp" +) 1. Setup the router using the `MyBaseRequestContext` type as a custom context. 2. Add middlewares to the router, HB2 has middlewares on the router instead of the app @@ -120,11 +131,17 @@ Create the application instance using the `buildApplication` function. Inside the main entrypoint you can start the server by calling the ``Application.runService(gracefulShutdownSignals:)`` method: -@Snippet(path: "site/Snippets/hummingbird-2", slice: "run") +@Snippet( + path: "articles/Snippets/2024/whats-new-in-hummingbird-2/snippets", + slice: "run" +) The route handlers in the `MyController` struct can access of the custom context type. -@Snippet(path: "site/Snippets/hummingbird-2", slice: "controller") +@Snippet( + path: "articles/Snippets/2024/whats-new-in-hummingbird-2/snippets", + slice: "controller" +) 1. Register route handlers using the router group 2. Hummingbird is thread-safe, so every route handler should be marked with `@Sendable` to propagate these thread-safety checks. diff --git a/Sources/Articles/Documentation.docc/2024/working-with-udp-in-swiftnio/working-with-udp-in-swiftnio.md b/Sources/Articles/Documentation.docc/2024/working-with-udp-in-swiftnio/working-with-udp-in-swiftnio.md index 3e5abec..88871db 100644 --- a/Sources/Articles/Documentation.docc/2024/working-with-udp-in-swiftnio/working-with-udp-in-swiftnio.md +++ b/Sources/Articles/Documentation.docc/2024/working-with-udp-in-swiftnio/working-with-udp-in-swiftnio.md @@ -16,7 +16,10 @@ Unlike TCP, UDP sockets don't distinguish between client and server. Any client In order to start accepting packets, bind a UDP socket to a port: -@Snippet(path: "site/Snippets/working-with-udp-in-swiftnio", slice: "bootstrap") +@Snippet( + path: "articles/Snippets/2024/working-with-udp-in-swiftnio/snippets", + slice: "bootstrap" +) 1. First, create a ``DatagramBootstrap`` to open a UDP socket. 2. Next, bind the socket to a port using the `bind` method. @@ -31,7 +34,10 @@ First, start observing the socket using ``NIOAsyncChannel/executeThenClose(_:) ( Inbound is a stream of incoming packets, whereas outbound is a writer that you can write packets to. -@Snippet(path: "site/Snippets/working-with-udp-in-swiftnio", slice: "packets") +@Snippet( + path: "articles/Snippets/2024/working-with-udp-in-swiftnio/snippets", + slice: "packets" +) 1. Each packet received in `inbound` is read into a String 2. The string is reversed, and packet back into a ByteBuffer. This is not very optimised, nor a real use case, but serves a as simple example. diff --git a/Sources/Articles/Documentation.docc/2025/introduction-to-jwts-in-swift/introduction-to-jwts-in-swift.md b/Sources/Articles/Documentation.docc/2025/introduction-to-jwts-in-swift/introduction-to-jwts-in-swift.md index 485cf6b..e935bfc 100644 --- a/Sources/Articles/Documentation.docc/2025/introduction-to-jwts-in-swift/introduction-to-jwts-in-swift.md +++ b/Sources/Articles/Documentation.docc/2025/introduction-to-jwts-in-swift/introduction-to-jwts-in-swift.md @@ -47,11 +47,15 @@ targets: [ In JWTKit everything revolves around the ``JWTKeyCollection`` object: a collection of keys that can be used to sign and verify JWTs. The declaration is simple, you can add this in your app's configuration code: -@Snippet(path: "site/Snippets/introduction-to-jwts-in-swift", slice: key_collection_init) +@Snippet( + path: "articles/Snippets/2025/introduction-to-jwts-in-swift/jwt-kit", slice: "key_collection_init" +) Adding keys to the collection is also straightforward: -@Snippet(path: "site/Snippets/introduction-to-jwts-in-swift", slice: key_collection_add_hmac) +@Snippet( + path: "articles/Snippets/2025/introduction-to-jwts-in-swift/jwt-kit", slice: "key_collection_add_hmac" +) This snippet adds an HMAC (Hash-based Message Authentication Code) key to the collection. HMAC is one of the most common algorithms used to sign JWTs. You can read about it [here](https://blog.boot.dev/cryptography/hmac-and-macs-in-jwts/), but in short, it works like this: 1. First, the JWT's content (header and payload) is hashed using SHA-256 @@ -68,7 +72,9 @@ Other than HMAC, JWTKit also supports **ECDSA**, **EdDSA** and **RSA** keys. The Once you have a ``JWTKeyCollection`` object, you can use it to "create" a JWT. Creating a JWT means signing the payload with a key, in this case one from the collection. The payload is the data we want to transmit securely: -@Snippet(path: "site/Snippets/introduction-to-jwts-in-swift", slice: payload_struct) +@Snippet( + path: "articles/Snippets/2025/introduction-to-jwts-in-swift/jwt-kit", slice: "payload_struct" +) In this example, we define a `TestPayload` struct that conforms to the ``JWTPayload`` protocol. This protocol requires us to implement the ``JWTPayload/verify(using:)`` method, which includes optional additional validation logic that can be performed when creating the JWT. In this case, we're verifying that the token has not expired. The properties of the struct are the claims we want to include in the JWT. JWTKit provides a number of built-in claims, such as ``ExpirationClaim`` and ``IssuerClaim``, which are commonly used in JWTs. JWTKit supports the [seven registered claims](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1) defined in the JWT specification, but you can also define custom claims if needed. @@ -77,7 +83,9 @@ The properties of the struct are the claims we want to include in the JWT. JWTKi To create a JWT with this payload, you can create a new instance of the payload and use the key collection to sign it: -@Snippet(path: "site/Snippets/introduction-to-jwts-in-swift", slice: jwt_sign) +@Snippet( + path: "articles/Snippets/2025/introduction-to-jwts-in-swift/jwt-kit", slice: "jwt_sign" +) This will create a token that looks like the one we showed earlier. The token is now ready to be transmitted to the other party. @@ -85,7 +93,9 @@ This will create a token that looks like the one we showed earlier. The token is Once you receive a JWT, you can use the key collection to verify it. Verification involves verifying the signature of the token and then checking the claims in the payload: -@Snippet(path: "site/Snippets/introduction-to-jwts-in-swift", slice: jwt_verify) +@Snippet( + path: "articles/Snippets/2025/introduction-to-jwts-in-swift/jwt-kit", slice: "jwt_verify" +) If the token is invalid, an error will be thrown. Otherwise, the payload will be returned and should look like the original payload you signed. @@ -103,18 +113,24 @@ This flow allows you to securely transmit information between the client and ser Let's put this into practice with a simple example. The following snippets use Swift pseudo-code to demonstrate the flow, without using a specific web framework. You can adapt this code to work with your preferred web framework. First, we'll create a payload struct that contains the user's information: -@Snippet(path: "site/Snippets/introduction-to-jwts-in-swift", slice: auth_user_payload) +@Snippet( + path: "articles/Snippets/2025/introduction-to-jwts-in-swift/jwt-kit", slice: "auth_user_payload" +) The `UserPayload` struct represents the claims we want to include in the JWT. In this snippet, we include the user's ID, an expiration claim, and a list of roles. The ``JWTPayload/verify(using:)`` method checks that the token has not expired and that the user is an admin. The `init` method creates a new payload from a `User` object, which could be retrieved from a database, for example. The roles claim is a custom claim: -@Snippet(path: "site/Snippets/introduction-to-jwts-in-swift", slice: auth_user_role_claim) +@Snippet( + path: "articles/Snippets/2025/introduction-to-jwts-in-swift/jwt-kit", slice: "auth_user_role_claim" +) This is a simple struct that conforms to the ``JWTClaim`` protocol. The ``JWTClaim/value`` property is the value of the claim, which in this case is a list of roles. Next, we'll create a route that handles user logins and returns a JWT: -@Snippet(path: "site/Snippets/introduction-to-jwts-in-swift", slice: auth_user_payload) +@Snippet( + path: "articles/Snippets/2025/introduction-to-jwts-in-swift/jwt-kit", slice: "auth_user_payload" +) The `UserPayload` struct represents the claims we want to include in the JWT. In this example, we include the user's ID, an expiration claim, and a list of roles. The ``JWTPayload/verify(using:)`` method checks that the token has not expired and that the user has the "admin" role. The `init` method creates a new payload from a `User` object, which could be retrieved from a database, for example. diff --git a/articles/.build.ssgc/ssgc/articles.package.json b/articles/.build.ssgc/ssgc/articles.package.json new file mode 100644 index 0000000..a0dc6d0 --- /dev/null +++ b/articles/.build.ssgc/ssgc/articles.package.json @@ -0,0 +1,600 @@ +{ + "cLanguageStandard" : null, + "cxxLanguageStandard" : null, + "dependencies" : [ + { + "sourceControl" : [ + { + "identity" : "hummingbird", + "location" : { + "remote" : [ + { + "urlString" : "https://github.com/hummingbird-project/hummingbird.git" + } + ] + }, + "productFilter" : null, + "requirement" : { + "range" : [ + { + "lowerBound" : "2.0.0", + "upperBound" : "3.0.0" + } + ] + } + } + ] + }, + { + "sourceControl" : [ + { + "identity" : "hummingbird-auth", + "location" : { + "remote" : [ + { + "urlString" : "https://github.com/hummingbird-project/hummingbird-auth.git" + } + ] + }, + "productFilter" : null, + "requirement" : { + "range" : [ + { + "lowerBound" : "2.0.2", + "upperBound" : "3.0.0" + } + ] + } + } + ] + }, + { + "sourceControl" : [ + { + "identity" : "hummingbird-websocket", + "location" : { + "remote" : [ + { + "urlString" : "https://github.com/hummingbird-project/hummingbird-websocket.git" + } + ] + }, + "productFilter" : null, + "requirement" : { + "range" : [ + { + "lowerBound" : "2.0.0", + "upperBound" : "3.0.0" + } + ] + } + } + ] + }, + { + "sourceControl" : [ + { + "identity" : "swift-mustache", + "location" : { + "remote" : [ + { + "urlString" : "https://github.com/hummingbird-project/swift-mustache.git" + } + ] + }, + "productFilter" : null, + "requirement" : { + "range" : [ + { + "lowerBound" : "2.0.0", + "upperBound" : "3.0.0" + } + ] + } + } + ] + }, + { + "sourceControl" : [ + { + "identity" : "swift-jobs", + "location" : { + "remote" : [ + { + "urlString" : "https://github.com/hummingbird-project/swift-jobs.git" + } + ] + }, + "productFilter" : null, + "requirement" : { + "range" : [ + { + "lowerBound" : "1.0.0-beta.6", + "upperBound" : "2.0.0" + } + ] + } + } + ] + }, + { + "sourceControl" : [ + { + "identity" : "async-http-client", + "location" : { + "remote" : [ + { + "urlString" : "https://github.com/swift-server/async-http-client.git" + } + ] + }, + "productFilter" : null, + "requirement" : { + "range" : [ + { + "lowerBound" : "1.21.1", + "upperBound" : "2.0.0" + } + ] + } + } + ] + }, + { + "sourceControl" : [ + { + "identity" : "swift-argument-parser", + "location" : { + "remote" : [ + { + "urlString" : "https://github.com/apple/swift-argument-parser.git" + } + ] + }, + "productFilter" : null, + "requirement" : { + "range" : [ + { + "lowerBound" : "1.5.0", + "upperBound" : "2.0.0" + } + ] + } + } + ] + }, + { + "sourceControl" : [ + { + "identity" : "swift-nio", + "location" : { + "remote" : [ + { + "urlString" : "https://github.com/apple/swift-nio.git" + } + ] + }, + "productFilter" : null, + "requirement" : { + "range" : [ + { + "lowerBound" : "2.65.0", + "upperBound" : "3.0.0" + } + ] + } + } + ] + }, + { + "sourceControl" : [ + { + "identity" : "swift-nio-extras", + "location" : { + "remote" : [ + { + "urlString" : "https://github.com/apple/swift-nio-extras.git" + } + ] + }, + "productFilter" : null, + "requirement" : { + "range" : [ + { + "lowerBound" : "1.20.0", + "upperBound" : "2.0.0" + } + ] + } + } + ] + }, + { + "sourceControl" : [ + { + "identity" : "swift-log", + "location" : { + "remote" : [ + { + "urlString" : "https://github.com/apple/swift-log.git" + } + ] + }, + "productFilter" : null, + "requirement" : { + "range" : [ + { + "lowerBound" : "1.5.4", + "upperBound" : "2.0.0" + } + ] + } + } + ] + }, + { + "sourceControl" : [ + { + "identity" : "swift-http-types", + "location" : { + "remote" : [ + { + "urlString" : "https://github.com/apple/swift-http-types.git" + } + ] + }, + "productFilter" : null, + "requirement" : { + "range" : [ + { + "lowerBound" : "1.0.0", + "upperBound" : "2.0.0" + } + ] + } + } + ] + }, + { + "sourceControl" : [ + { + "identity" : "swift-service-lifecycle", + "location" : { + "remote" : [ + { + "urlString" : "https://github.com/swift-server/swift-service-lifecycle.git" + } + ] + }, + "productFilter" : null, + "requirement" : { + "range" : [ + { + "lowerBound" : "2.5.0", + "upperBound" : "3.0.0" + } + ] + } + } + ] + }, + { + "sourceControl" : [ + { + "identity" : "grpc-swift-protobuf", + "location" : { + "remote" : [ + { + "urlString" : "https://github.com/grpc/grpc-swift-protobuf.git" + } + ] + }, + "productFilter" : null, + "requirement" : { + "exact" : [ + "1.0.0-beta.2" + ] + } + } + ] + }, + { + "sourceControl" : [ + { + "identity" : "grpc-swift-nio-transport", + "location" : { + "remote" : [ + { + "urlString" : "https://github.com/grpc/grpc-swift-nio-transport.git" + } + ] + }, + "productFilter" : null, + "requirement" : { + "exact" : [ + "1.0.0-beta.2" + ] + } + } + ] + }, + { + "sourceControl" : [ + { + "identity" : "mongokitten", + "location" : { + "remote" : [ + { + "urlString" : "https://github.com/orlandos-nl/MongoKitten.git" + } + ] + }, + "productFilter" : null, + "requirement" : { + "range" : [ + { + "lowerBound" : "7.0.0", + "upperBound" : "8.0.0" + } + ] + } + } + ] + }, + { + "sourceControl" : [ + { + "identity" : "jwt-kit", + "location" : { + "remote" : [ + { + "urlString" : "https://github.com/vapor/jwt-kit.git" + } + ] + }, + "productFilter" : null, + "requirement" : { + "range" : [ + { + "lowerBound" : "5.0.0", + "upperBound" : "6.0.0" + } + ] + } + } + ] + } + ], + "name" : "swiftonserver-articles", + "packageKind" : { + "root" : [ + "/Users/tib/Developer/sos/articles" + ] + }, + "pkgConfig" : null, + "platforms" : [ + { + "options" : [ + + ], + "platformName" : "macos", + "version" : "15.0" + } + ], + "products" : [ + { + "name" : "Articles", + "settings" : [ + + ], + "targets" : [ + "Articles" + ], + "type" : { + "library" : [ + "automatic" + ] + } + } + ], + "providers" : null, + "swiftLanguageVersions" : null, + "targets" : [ + { + "dependencies" : [ + { + "product" : [ + "Hummingbird", + "hummingbird", + null, + null + ] + }, + { + "product" : [ + "HummingbirdRouter", + "hummingbird", + null, + null + ] + }, + { + "product" : [ + "AsyncHTTPClient", + "async-http-client", + null, + null + ] + }, + { + "product" : [ + "ArgumentParser", + "swift-argument-parser", + null, + null + ] + }, + { + "product" : [ + "Jobs", + "swift-jobs", + null, + null + ] + }, + { + "product" : [ + "NIOCore", + "swift-nio", + null, + null + ] + }, + { + "product" : [ + "NIOPosix", + "swift-nio", + null, + null + ] + }, + { + "product" : [ + "Logging", + "swift-log", + null, + null + ] + }, + { + "product" : [ + "ServiceLifecycle", + "swift-service-lifecycle", + null, + null + ] + }, + { + "product" : [ + "HummingbirdAuth", + "hummingbird-auth", + null, + null + ] + }, + { + "product" : [ + "HummingbirdWebSocket", + "hummingbird-websocket", + null, + null + ] + }, + { + "product" : [ + "HummingbirdWSCompression", + "hummingbird-websocket", + null, + null + ] + }, + { + "product" : [ + "Mustache", + "swift-mustache", + null, + null + ] + }, + { + "product" : [ + "GRPCNIOTransportHTTP2", + "grpc-swift-nio-transport", + null, + null + ] + }, + { + "product" : [ + "GRPCProtobuf", + "grpc-swift-protobuf", + null, + null + ] + }, + { + "product" : [ + "NIOHTTPTypes", + "swift-nio-extras", + null, + null + ] + }, + { + "product" : [ + "NIOHTTPTypesHTTP1", + "swift-nio-extras", + null, + null + ] + }, + { + "product" : [ + "NIOExtras", + "swift-nio-extras", + null, + null + ] + }, + { + "product" : [ + "MongoKitten", + "MongoKitten", + null, + null + ] + }, + { + "product" : [ + "JWTKit", + "jwt-kit", + null, + null + ] + } + ], + "exclude" : [ + + ], + "name" : "Articles", + "packageAccess" : true, + "resources" : [ + + ], + "settings" : [ + + ], + "type" : "regular" + }, + { + "dependencies" : [ + + ], + "exclude" : [ + + ], + "name" : "SnippetsExample", + "packageAccess" : true, + "resources" : [ + + ], + "settings" : [ + + ], + "type" : "regular" + } + ], + "toolsVersion" : { + "_version" : "6.0.0" + } +}