From 67cd637a4eda0fc18dd1ce439ae93bc13b4c28bf Mon Sep 17 00:00:00 2001 From: Jipeng Zhang Date: Thu, 23 Jul 2026 18:45:14 +0700 Subject: [PATCH] fix: parse multiline GraphQL documentation --- CHANGELOG.md | 7 +++ .../BackendAPIReference/AppNetworkClient.html | 14 +++++- .../NetworkingKitDemo/DemoViewModel.swift | 16 ++++++- README.md | 2 +- README.zh-Hans.md | 2 +- Sources/BackendReferenceGenerator/main.swift | 44 ++++++++++++++++++- .../NetworkingKitTests.swift | 14 +++++- 7 files changed, 92 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d0aa94..a406776 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to NetworkingKit are documented in this file. +## 2.4.7 - 2026-07-23 + +### Fixed + +- Recognize and format multi-line triple-quoted GraphQL queries and multi-line variables in generated backend HTML documentation. +- Add Demo and request-body test coverage for multi-line GraphQL queries. + ## 2.4.6 - 2026-07-23 ### Changed diff --git a/Examples/NetworkingKitDemo/BackendAPIReference/AppNetworkClient.html b/Examples/NetworkingKitDemo/BackendAPIReference/AppNetworkClient.html index 56b125d..e35f9e8 100644 --- a/Examples/NetworkingKitDemo/BackendAPIReference/AppNetworkClient.html +++ b/Examples/NetworkingKitDemo/BackendAPIReference/AppNetworkClient.html @@ -1,6 +1,18 @@ AppNetworkClient
← 所有服务器

BACKEND SERVER

AppNetworkClient

https://rickandmortyapi.com · 2 个端点

CLIENT CONFIGURATION

配置

配置项
timeoutInterval15
retryPolicyRetryPolicy(maxAttempts: 2)
errorLocalizerAppNetworkErrorLocalizer()

FEATURE

GraphQL

1 个端点
MethodEndpointKindParametersRequestSource
POST/graphqlGraphQLid: String
query
query Character($id: ID!) {
+
← 所有服务器

BACKEND SERVER

AppNetworkClient

https://rickandmortyapi.com · 2 个端点

CLIENT CONFIGURATION

配置

配置项
timeoutInterval15
retryPolicyRetryPolicy(maxAttempts: 2)
errorLocalizerAppNetworkErrorLocalizer()

FEATURE

GraphQL

1 个端点
MethodEndpointKindParametersRequestSource
POST/graphqlGraphQLid: String
query
query Character($id: ID!) {
   character(id: $id) {
     name
     species
diff --git a/Examples/NetworkingKitDemo/DemoViewModel.swift b/Examples/NetworkingKitDemo/DemoViewModel.swift
index 19d10c3..1aa7995 100644
--- a/Examples/NetworkingKitDemo/DemoViewModel.swift
+++ b/Examples/NetworkingKitDemo/DemoViewModel.swift
@@ -245,6 +245,18 @@ struct FetchCharacterProfileRequest: AppNetworkRequest, GraphQLRequest {
     private let id: String
 
     init(id: String) { self.id = id }
-    var query: String { "query Character($id: ID!) { character(id: $id) { name species status } }" }
-    var variables: [String: AnyEncodable]? { ["id": AnyEncodable(id)] }
+    var query: String {
+        """
+        query Character($id: ID!) {
+          character(id: $id) {
+            name
+            species
+            status
+          }
+        }
+        """
+    }
+    var variables: [String: AnyEncodable]? {
+        ["id": AnyEncodable(id)]
+    }
 }
diff --git a/README.md b/README.md
index 61020fa..ccadf3e 100644
--- a/README.md
+++ b/README.md
@@ -33,7 +33,7 @@ Or add the package manifest dependency:
 
 ```swift
 dependencies: [
-    .package(url: "https://github.com/relaxfinger/NetworkingKit.git", from: "2.4.6")
+    .package(url: "https://github.com/relaxfinger/NetworkingKit.git", from: "2.4.7")
 ]
 ```
 
diff --git a/README.zh-Hans.md b/README.zh-Hans.md
index 8d00868..f6467ad 100644
--- a/README.zh-Hans.md
+++ b/README.zh-Hans.md
@@ -33,7 +33,7 @@ https://github.com/relaxfinger/NetworkingKit.git
 
 ```swift
 dependencies: [
-    .package(url: "https://github.com/relaxfinger/NetworkingKit.git", from: "2.4.6")
+    .package(url: "https://github.com/relaxfinger/NetworkingKit.git", from: "2.4.7")
 ]
 ```
 
diff --git a/Sources/BackendReferenceGenerator/main.swift b/Sources/BackendReferenceGenerator/main.swift
index 41a4bd1..5332bd9 100644
--- a/Sources/BackendReferenceGenerator/main.swift
+++ b/Sources/BackendReferenceGenerator/main.swift
@@ -160,7 +160,7 @@ enum BackendReferenceGenerator {
                 else if inherited.contains("GraphQLRequest") { kind = "GraphQL" }
                 else { continue }
                 guard let protocolName = protocolClients.keys.first(where: { inherited.contains($0) }), let server = protocolClients[protocolName] else { continue }
-                let body = capture(3, from: source, match: match)
+                let body = completeTypeBody(in: source, declaration: match) ?? capture(3, from: source, match: match)
                 let name = capture(1, from: source, match: match)
                 var params = property.matches(in: body, range: NSRange(body.startIndex..., in: body)).map { "\(capture(1, from: body, match: $0)): \(capture(2, from: body, match: $0).trimmingCharacters(in: .whitespaces))" }
                 let feature = feature(before: match.range.location, in: source)
@@ -188,6 +188,10 @@ enum BackendReferenceGenerator {
         let member = try! NSRegularExpression(pattern: #"(?m)^\s*(?:private\s+|public\s+|internal\s+)?(?:let|var)\s+(query|variables|operationName)\b(?:\s*:\s*[^=\{\n]+)?\s*(?:=\s*([^\n]+)|\{\s*(.*)\}\s*$)"#)
         var names = Set(existing.compactMap { $0.split(separator: ":", maxSplits: 1).first.map(String.init) })
         var parameters: [String] = []
+        let multilineQuery = try! NSRegularExpression(pattern: #"(?s)\bvar\s+query\s*:\s*String\s*\{\s*\"\"\"(.*?)\"\"\"\s*\}"#)
+        if let match = multilineQuery.firstMatch(in: body, range: NSRange(body.startIndex..., in: body)), names.insert("query").inserted {
+            parameters.append("query: \(capture(1, from: body, match: match).trimmingCharacters(in: .whitespacesAndNewlines))")
+        }
         for match in member.matches(in: body, range: NSRange(body.startIndex..., in: body)) {
             let name = capture(1, from: body, match: match)
             guard names.insert(name).inserted else { continue }
@@ -196,12 +200,50 @@ enum BackendReferenceGenerator {
                 ?? ""
             parameters.append("\(name): \(expression.trimmingCharacters(in: .whitespaces))")
         }
+        let multilineVariables = try! NSRegularExpression(pattern: #"(?s)\bvar\s+variables\s*:\s*[^\{\n]+\{\s*(\[.*?\])\s*\}"#)
+        if let match = multilineVariables.firstMatch(in: body, range: NSRange(body.startIndex..., in: body)), names.insert("variables").inserted {
+            parameters.append("variables: \(capture(1, from: body, match: match).trimmingCharacters(in: .whitespacesAndNewlines))")
+        }
         if !names.contains("query"), body.range(of: #"\b(?:let|var)\s+query\b"#, options: .regularExpression) != nil {
             parameters.append("query: ")
         }
         return parameters
     }
 
+    static func completeTypeBody(in source: String, declaration: NSTextCheckingResult) -> String? {
+        guard let declarationRange = Range(declaration.range, in: source), let openingBrace = source[declarationRange].firstIndex(of: "{") else { return nil }
+        var index = openingBrace
+        var depth = 0
+        var inString = false
+        var inMultilineString = false
+        var escaped = false
+
+        while index < source.endIndex {
+            if source[index...].hasPrefix("\"\"\"") {
+                inMultilineString.toggle()
+                index = source.index(index, offsetBy: 3)
+                continue
+            }
+            let character = source[index]
+            if inMultilineString {
+                index = source.index(after: index)
+                continue
+            }
+            if character == "\"" && !escaped { inString.toggle() }
+            if !inString {
+                if character == "{" { depth += 1 }
+                if character == "}" {
+                    depth -= 1
+                    if depth == 0 { return String(source[source.index(after: openingBrace).. String {
         if parameter.hasPrefix("query: ") {
             let source = String(parameter.dropFirst("query: ".count))
diff --git a/Tests/NetworkingKitTests/NetworkingKitTests.swift b/Tests/NetworkingKitTests/NetworkingKitTests.swift
index a9be2a3..6876741 100644
--- a/Tests/NetworkingKitTests/NetworkingKitTests.swift
+++ b/Tests/NetworkingKitTests/NetworkingKitTests.swift
@@ -308,6 +308,11 @@ final class NetworkingKitTests: XCTestCase {
     func testGraphQLResponseKeepsDataAndErrors() async throws {
         let client = makeClient { request in
             XCTAssertEqual(request.value(forHTTPHeaderField: "Content-Type"), "application/json")
+            XCTAssertNotNil(request.httpBody)
+            let body = request.httpBody!
+            let payload = try! JSONSerialization.jsonObject(with: body) as! [String: Any]
+            let query = payload["query"] as! String
+            XCTAssertTrue(query.contains("query User {\n  user {\n    id\n    name\n  }\n}"))
             let json = #"{"data":{"id":"42","name":"Ada"},"errors":[{"message":"partial result","path":["user",0]}]}"#
             return (.init(url: try! XCTUnwrap(request.url), statusCode: 200, httpVersion: nil, headerFields: nil)!, json.data(using: .utf8)!)
         }
@@ -486,7 +491,14 @@ private struct DeleteUserRequest: RestfulRequest {
 private struct UserGraphQLRequest: GraphQLRequest {
     typealias Response = GraphQLResponse
     let client: TestClient
-    let query = "query User { user { id name } }"
+    let query = """
+    query User {
+      user {
+        id
+        name
+      }
+    }
+    """
 }
 
 private final class TestClient: NetworkClient, @unchecked Sendable {