Skip to content

Commit

Permalink
3.0.0
Browse files Browse the repository at this point in the history
  • Loading branch information
dankinsoid committed Nov 19, 2023
1 parent 2ee6c2a commit 9698d86
Show file tree
Hide file tree
Showing 8 changed files with 267 additions and 13 deletions.
9 changes: 9 additions & 0 deletions Package.resolved
Expand Up @@ -9,6 +9,15 @@
"version" : "0.10.3"
}
},
{
"identity" : "swift-syntax",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-syntax.git",
"state" : {
"revision" : "6ad4ea24b01559dde0773e3d091f1b9e36175036",
"version" : "509.0.2"
}
},
{
"identity" : "xctest-dynamic-overlay",
"kind" : "remoteSourceControl",
Expand Down
13 changes: 12 additions & 1 deletion Package.swift
@@ -1,7 +1,8 @@
// swift-tools-version:5.7
// swift-tools-version:5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription
import CompilerPluginSupport

let package = Package(
name: "SwiftOpenAPI",
Expand All @@ -16,13 +17,23 @@ let package = Package(
],
dependencies: [
.package(url: "https://github.com/pointfreeco/swift-custom-dump.git", from: "0.10.3"),
.package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.2")
],
targets: [
.target(name: "SwiftOpenAPI", dependencies: []),
.macro(
name: "SwiftOpenAPIMacros",
dependencies: [
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
.product(name: "SwiftCompilerPlugin", package: "swift-syntax")
]
),
.testTarget(
name: "SwiftOpenAPITests",
dependencies: [
"SwiftOpenAPI",
"SwiftOpenAPIMacros",
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
.product(name: "CustomDump", package: "swift-custom-dump"),
],
exclude: ["Mocks/"]
Expand Down
40 changes: 28 additions & 12 deletions Sources/SwiftOpenAPI/Encoders/OpenAPIDescription.swift
Expand Up @@ -6,13 +6,7 @@ public protocol OpenAPIDescriptionType {
var schemePropertyDescriptions: [String: String] { get }
}

extension String: OpenAPIDescriptionType {

public var openAPISchemeDescription: String? { self }
public var schemePropertyDescriptions: [String: String] { [:] }
}

public struct OpenAPIDescription<Key: CodingKey>: OpenAPIDescriptionType, ExpressibleByStringInterpolation {
public struct OpenAPIDescription<Key>: OpenAPIDescriptionType, ExpressibleByStringInterpolation, Equatable {

public var openAPISchemeDescription: String?
public var schemePropertyDescriptions: [String: String] = [:]
Expand All @@ -28,12 +22,34 @@ public struct OpenAPIDescription<Key: CodingKey>: OpenAPIDescriptionType, Expres
public init(stringInterpolation: DefaultStringInterpolation) {
openAPISchemeDescription = String(stringInterpolation: stringInterpolation)
}
}

public func add(for key: Key, _ description: String) -> OpenAPIDescription {
var result = self
result.schemePropertyDescriptions[key.stringValue] = description
return result
}
extension OpenAPIDescription where Key: CodingKey {

public func add(for key: Key, _ description: String) -> OpenAPIDescription {
var result = self
result.schemePropertyDescriptions[key.stringValue] = description
return result
}
}

extension OpenAPIDescription where Key == String {

public func add(for key: Key, _ description: String) -> OpenAPIDescription {
var result = self
result.schemePropertyDescriptions[key] = description
return result
}
}

extension OpenAPIDescription where Key: RawRepresentable, Key.RawValue == String {

@_disfavoredOverload
public func add(for key: Key, _ description: String) -> OpenAPIDescription {
var result = self
result.schemePropertyDescriptions[key.rawValue] = description
return result
}
}

extension SchemaObject {
Expand Down
7 changes: 7 additions & 0 deletions Sources/SwiftOpenAPI/OpenAPIType.swift
@@ -1,4 +1,5 @@
import Foundation
import SwiftOpenAPIMacros

public protocol OpenAPIDescriptable {

Expand All @@ -12,6 +13,12 @@ public extension OpenAPIDescriptable {
}
}

@attached(extension, conformances: OpenAPIDescriptable, names: arbitrary)
public macro OpenAPIAutoDescriptable() = #externalMacro(
module: "SwiftOpenAPIMacros",
type: "OpenAPIDescriptionMacro"
)

public protocol OpenAPIType: OpenAPIDescriptable {

static var openAPISchema: SchemaObject { get }
Expand Down
75 changes: 75 additions & 0 deletions Sources/SwiftOpenAPIMacros/OpenAPIDescriptionMacro.swift
@@ -0,0 +1,75 @@
#if canImport(SwiftCompilerPlugin)
import SwiftCompilerPlugin
import SwiftSyntax
import SwiftSyntaxMacros

@main
struct OpenAPIDescriptionPlugin: CompilerPlugin {

let providingMacros: [Macro.Type] = [
OpenAPIDescriptionMacro.self
]
}

public struct OpenAPIDescriptionMacro: ExtensionMacro {

public static func expansion(
of node: AttributeSyntax,
attachedTo declaration: some DeclGroupSyntax,
providingExtensionsOf type: some TypeSyntaxProtocol,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [ExtensionDeclSyntax] {
let typeDoc = declaration
.children(viewMode: .all)
.compactMap(\.documentation)
.first?
.wrapped
let varDocs = declaration.memberBlock.members.compactMap { member -> (String, String)? in
guard
let variable = member.decl.as(VariableDeclSyntax.self),
variable.attributes.isEmpty,
let doc = variable.documentation
else {
return nil
}

var name: String?
for binding in variable.bindings {
if let identifier = binding.pattern.as(IdentifierPatternSyntax.self) {
name = identifier.identifier.text
}
if let closure = binding.accessorBlock {
guard
let list = closure.accessors.as(AccessorDeclListSyntax.self),
list.contains(where: \.accessorSpecifier.isWillSetOrDidSet)
else {
return nil
}
}
}
return name.map { ($0, doc) }
}
let varDocsModifiers = varDocs.map {
"\n .add(for: \"\($0.0)\", \($0.1.wrapped))"
}
.joined()

let sendableExtension: DeclSyntax =
"""
extension \(type.trimmed): OpenAPIDescriptable {
public static var openAPIDescription: OpenAPIDescriptionType? {
OpenAPIDescription<String>(\(raw: typeDoc ?? ""))\(raw: varDocsModifiers)
}
}
"""

guard let extensionDecl = sendableExtension.as(ExtensionDeclSyntax.self) else {
throw StringError("Failed to create extension declaration")
}

return [extensionDecl]
}
}
#endif
10 changes: 10 additions & 0 deletions Sources/SwiftOpenAPIMacros/StringError.swift
@@ -0,0 +1,10 @@
import Foundation

struct StringError: LocalizedError {

var errorDescription: String?

init(_ errorDescription: String? = nil) {
self.errorDescription = errorDescription
}
}
66 changes: 66 additions & 0 deletions Sources/SwiftOpenAPIMacros/SyntaxExt.swift
@@ -0,0 +1,66 @@
#if canImport(SwiftCompilerPlugin)
import SwiftCompilerPlugin
import SwiftSyntax
import SwiftSyntaxMacros

extension TokenSyntax {

var isWillSetOrDidSet: Bool {
self == .keyword(.didSet) || self == .keyword(.willSet)
}
}

extension SyntaxProtocol {

var documentation: String? {
leadingTrivia.documentation
}
}

extension Trivia {

var documentation: String? {
let lines = compactMap { $0.documentation }
guard lines.count > 1 else { return lines.first?.trimmingCharacters(in: .whitespaces) }

let indentation = lines.compactMap { $0.firstIndex(where: { !$0.isWhitespace })?.utf16Offset(in: $0) }
.min() ?? 0

return lines.map {
guard $0.count > indentation else { return String($0) }
return String($0.suffix($0.count - indentation))
}.joined(separator: "\\n")
}
}

extension TriviaPiece {

var documentation: String? {
switch self {
case let .docLineComment(comment):
let startIndex = comment.index(comment.startIndex, offsetBy: 3)
return String(comment.suffix(from: startIndex))
case let .lineComment(comment):
let startIndex = comment.index(comment.startIndex, offsetBy: 2)
return String(comment.suffix(from: startIndex))
case let .docBlockComment(comment):
let startIndex = comment.index(comment.startIndex, offsetBy: 3)
let endIndex = comment.index(comment.endIndex, offsetBy: -2)
return String(comment[startIndex ..< endIndex])
case let .blockComment(comment):
let startIndex = comment.index(comment.startIndex, offsetBy: 2)
let endIndex = comment.index(comment.endIndex, offsetBy: -2)
return String(comment[startIndex ..< endIndex])
default:
return nil
}
}
}

extension String {

var wrapped: String {
"#\"\(self)\"#"
}
}
#endif
60 changes: 60 additions & 0 deletions Tests/SwiftOpenAPITests/SwiftOpenAPIMacrosTests.swift
@@ -0,0 +1,60 @@
import Foundation
import SwiftSyntaxMacros
import SwiftSyntaxMacrosTestSupport
import XCTest
@testable import SwiftOpenAPIMacros
@testable import SwiftOpenAPI

let testMacros: [String: Macro.Type] = [
"OpenAPIAutoDescriptable": OpenAPIDescriptionMacro.self
]

final class OpenAPIDescriptionMacroTests: XCTestCase {

func test_should_create_extension() {
assertMacroExpansion(
"""
/// A person.
@OpenAPIAutoDescriptable
struct Person: Codable {
/// The person's name.
let name: String
}
""",
expandedSource: """
/// A person.
struct Person: Codable {
/// The person's name.
let name: String
}
extension Person: OpenAPIDescriptable {
public static var openAPIDescription: OpenAPIDescriptionType? {
OpenAPIDescription<String>(#"A person."#)
.add(for: "name", #"The person's name."#)
}
}
""",
macros: testMacros
)
}

func test_created_extension() {
XCTAssertEqual(
Person.openAPIDescription as? OpenAPIDescription<String>,
OpenAPIDescription<String>("A person.")
.add(for: "name", "The person's name.")
)
}
}

@OpenAPIAutoDescriptable
/// A person.
struct Person {

/// The person's name.
let name: String
}

0 comments on commit 9698d86

Please sign in to comment.