Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 29 additions & 17 deletions Sources/XCDocsCLI/SearchCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,26 +54,38 @@ struct SearchCommand: AsyncParsableCommand {
}

@available(macOS 26, *)
private func printDocumentationSearchJSON(_ results: [SearchResult]) throws {
struct DocumentationSearchDocument: Encodable {
let contents: String
let score: Double
let title: String
let uri: String

init(searchResult: SearchResult) {
self.contents = searchResult.entry.content ?? ""
self.score = searchResult.score
self.title = searchResult.entry.title ?? ""
self.uri = searchResult.entry.id
}
struct DocumentationSearchDocument: Encodable {
let contents: String?
let score: Double
let title: String?
let uri: String

init(searchResult: SearchResult) {
self.contents = searchResult.entry.content
self.score = searchResult.score
self.title = searchResult.entry.title
self.uri = searchResult.entry.id
}

struct DocumentationSearchResponse: Encodable {
let documents: [DocumentationSearchDocument]

init(searchResults: [SearchResult]) { self.documents = searchResults.map(DocumentationSearchDocument.init) }
func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(contents, forKey: .contents)
try container.encode(score, forKey: .score)
try container.encode(title, forKey: .title)
try container.encode(uri, forKey: .uri)
}

private enum CodingKeys: String, CodingKey { case contents, score, title, uri }
}

@available(macOS 26, *)
struct DocumentationSearchResponse: Encodable {
let documents: [DocumentationSearchDocument]

init(searchResults: [SearchResult]) { self.documents = searchResults.map(DocumentationSearchDocument.init) }
}

@available(macOS 26, *)
private func printDocumentationSearchJSON(_ results: [SearchResult]) throws {
try printJSON(DocumentationSearchResponse(searchResults: results), prettyPrinted: false)
}
64 changes: 64 additions & 0 deletions Tests/XCDocsCLITests/SearchJSONOutputTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import Foundation
import Testing
import XCDocs

@testable import XCDocsCLI

@Suite("Search JSON Output")
struct SearchJSONOutputTests {
@Test
func searchDocumentEmitsNullForNilFields() throws {
guard #available(macOS 26, *) else { return }
try assertSearchDocumentEmitsNullForNilFields()
}

@Test
func searchDocumentPreservesNonNilValues() throws {
guard #available(macOS 26, *) else { return }
try assertSearchDocumentPreservesNonNilValues()
}
}

@available(macOS 26, *)
private func assertSearchDocumentEmitsNullForNilFields() throws {
let entry = DocumentationEntry(id: "/documentation/Testing", framework: nil, kind: nil, title: nil, content: nil)
let result = SearchResult(score: 0.5, entry: entry)

let response = DocumentationSearchResponse(searchResults: [result])
let data = try JSONEncoder().encode(response)
let string = try #require(String(data: data, encoding: .utf8))
let json = try #require(JSONSerialization.jsonObject(with: data) as? [String: Any])
let documents = try #require(json["documents"] as? [[String: Any]])
let doc = try #require(documents.first)

// nil fields must be encoded as JSON null, not coerced to empty strings or omitted.
#expect(doc["title"] is NSNull, "Expected null for nil title, got \(String(describing: doc["title"]))")
#expect(doc["contents"] is NSNull, "Expected null for nil contents, got \(String(describing: doc["contents"]))")
#expect(!string.contains("\"title\":\"\""), "title should not be an empty string")
#expect(!string.contains("\"contents\":\"\""), "contents should not be an empty string")
#expect(doc["uri"] as? String == "/documentation/Testing")
#expect(doc["score"] as? Double == 0.5)
}

@available(macOS 26, *)
private func assertSearchDocumentPreservesNonNilValues() throws {
let entry = DocumentationEntry(
id: "/documentation/Testing",
framework: "Swift Testing",
kind: .article,
title: "Swift Testing",
content: "Create and run tests."
)
let result = SearchResult(score: 0.75, entry: entry)

let response = DocumentationSearchResponse(searchResults: [result])
let data = try JSONEncoder().encode(response)
let json = try #require(JSONSerialization.jsonObject(with: data) as? [String: Any])
let documents = try #require(json["documents"] as? [[String: Any]])
let doc = try #require(documents.first)

#expect(doc["title"] as? String == "Swift Testing")
#expect(doc["contents"] as? String == "Create and run tests.")
#expect(doc["uri"] as? String == "/documentation/Testing")
#expect(doc["score"] as? Double == 0.75)
}
Loading