Skip to content

Commit f2c24cc

Browse files
committed
Adding all existing files
1 parent e295c06 commit f2c24cc

File tree

79 files changed

+156338
-1
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

79 files changed

+156338
-1
lines changed

.DS_Store

6 KB
Binary file not shown.

Package.swift

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// swift-tools-version: 5.9
2+
// The swift-tools-version declares the minimum version of Swift required to build this package.
3+
4+
import PackageDescription
5+
6+
let package = Package(
7+
name: "public-api-diff",
8+
platforms: [
9+
.macOS(.v13)
10+
],
11+
dependencies: [
12+
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.0")
13+
],
14+
targets: [
15+
// Targets are the basic building blocks of a package, defining a module or a test suite.
16+
// Targets can depend on other targets in this package and products from dependencies.
17+
.executableTarget(
18+
name: "public-api-diff",
19+
dependencies: [
20+
.product(name: "ArgumentParser", package: "swift-argument-parser")
21+
],
22+
path: "Sources"
23+
),
24+
.testTarget(
25+
name: "PublicApiDiffTests",
26+
dependencies: ["public-api-diff"],
27+
resources: [
28+
// Copy Tests/ExampleTests/Resources directories as-is.
29+
// Use to retain directory structure.
30+
// Will be at top level in bundle.
31+
.copy("Resources/dummy.abi.json"),
32+
.copy("Resources/dummi-abi-flat-definition.md")
33+
]
34+
)
35+
]
36+
)

README.md

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,43 @@
1-
Readme
1+
# Swift Public API diff
2+
3+
This tool allows comparing 2 versions of a swift package project and lists all changes in a human readable way.
4+
5+
It makes use of `xcrun swift-api-digester -dump-sdk` to create a dump of the public api of your swift package and then runs it through a custom parser to process them.
6+
7+
Alternatively you could use `xcrun swift-api-digester -diagnose-sdk` and pass the abi dumps into it.
8+
9+
## How it works
10+
11+
![image](https://github.com/user-attachments/assets/6f8b8927-d08b-487d-9d80-e5ee1b8d8302)
12+
13+
### Project Builder
14+
15+
Builds the swift package project which is required for the next step to run the `xcrun swift-api-digester -dump-sdk`
16+
17+
### ABIGenerator
18+
19+
Makes use of `xcrun swift-api-digester -dump-sdk` to "dump" the public interface into an abi.json file.
20+
21+
### SDKDumpGenerator
22+
23+
Parses the abi.json files into an `SDKDump` object
24+
25+
### SDKDumpAnalyzer
26+
27+
Analyzes 2 `SDKDump` objects and detects `addition`s & `removal`s.
28+
29+
### ChangeConsolidator
30+
31+
The `ChangeConsolidator` takes 2 independent changes (`addition` & `removal`) and tries to match them based on the name, declKind and parent.
32+
33+
| Match |
34+
| --- |
35+
| ![image](https://github.com/user-attachments/assets/f057c160-f85d-45af-b08f-203b89e43b41) |
36+
37+
| No Match | Potentially false positive |
38+
| --- | --- |
39+
| ![image](https://github.com/user-attachments/assets/5ae3b624-b32a-41cc-9026-8ba0117cec57) | ![image](https://github.com/user-attachments/assets/a7e60605-fc1c-49ef-a203-d6a5466a6fda) |
40+
41+
### OutputGenerator
42+
43+
Receives a list of `Change`s and processes them into a human readable format.

Sources/.DS_Store

6 KB
Binary file not shown.

Sources/Helpers/.DS_Store

6 KB
Binary file not shown.
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
//
2+
// Copyright (c) 2024 Adyen N.V.
3+
//
4+
// This file is open source and available under the MIT license. See the LICENSE file for more info.
5+
//
6+
7+
import Foundation
8+
9+
enum FileHandlerError: LocalizedError, Equatable {
10+
/// Could not encode the output string into data
11+
case couldNotEncodeOutput
12+
/// Could not persist output at the specified `outputFilePath`
13+
case couldNotCreateFile(outputFilePath: String)
14+
/// Could not load file at the specified `filePath`
15+
case couldNotLoadFile(filePath: String)
16+
/// File/Directory does not exist at `path`
17+
case pathDoesNotExist(path: String)
18+
19+
var errorDescription: String? {
20+
switch self {
21+
case .couldNotEncodeOutput:
22+
"Could not encode the output string into data"
23+
case let .couldNotCreateFile(outputFilePath):
24+
"Could not persist output at `\(outputFilePath)`"
25+
case let .couldNotLoadFile(filePath):
26+
"Could not load file from `\(filePath)`"
27+
case let .pathDoesNotExist(path):
28+
"File/Directory does not exist at `\(path)`"
29+
}
30+
}
31+
}
32+
33+
// MARK: - Convenience
34+
35+
extension FileHandling {
36+
37+
/// Creates a directory at the specified path and deletes any old directory if existing
38+
///
39+
/// - Parameters:
40+
/// - cleanDirectoryPath: The Path where to create the clean directory
41+
func createCleanDirectory(
42+
atPath cleanDirectoryPath: String
43+
) throws {
44+
// Remove existing directory if it exists
45+
try? removeItem(atPath: cleanDirectoryPath)
46+
try createDirectory(atPath: cleanDirectoryPath)
47+
}
48+
49+
/// Persists an output string to a file
50+
///
51+
/// - Parameters:
52+
/// - output: The output string to persist
53+
/// - outputFilePath: The file path to persist the output to
54+
func write(_ output: String, to outputFilePath: String) throws {
55+
56+
guard let data = output.data(using: String.Encoding.utf8) else {
57+
throw FileHandlerError.couldNotEncodeOutput
58+
}
59+
60+
// Remove existing directory if it exists
61+
try? removeItem(atPath: outputFilePath)
62+
if !createFile(atPath: outputFilePath, contents: data) {
63+
throw FileHandlerError.couldNotCreateFile(outputFilePath: outputFilePath)
64+
}
65+
}
66+
67+
/// Get the string contents from a file at a specified path
68+
///
69+
/// - Parameters:
70+
/// - filePath: The path to load the string content from
71+
///
72+
/// - Returns: The string content of the file
73+
func loadString(from filePath: String) throws -> String {
74+
let data = try loadData(from: filePath)
75+
return String(decoding: data, as: UTF8.self)
76+
}
77+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
//
2+
// Copyright (c) 2024 Adyen N.V.
3+
//
4+
// This file is open source and available under the MIT license. See the LICENSE file for more info.
5+
//
6+
7+
import Foundation
8+
9+
protocol FileHandling {
10+
11+
var currentDirectoryPath: String { get }
12+
13+
func loadData(from path: String) throws -> Data
14+
15+
func removeItem(atPath path: String) throws
16+
17+
func contentsOfDirectory(atPath path: String) throws -> [String]
18+
19+
func createDirectory(atPath path: String) throws
20+
21+
func createFile(atPath path: String, contents data: Data) -> Bool
22+
23+
func fileExists(atPath path: String) -> Bool
24+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
//
2+
// Copyright (c) 2024 Adyen N.V.
3+
//
4+
// This file is open source and available under the MIT license. See the LICENSE file for more info.
5+
//
6+
7+
import Foundation
8+
9+
extension FileManager: FileHandling {
10+
11+
/// Creates a directory at the specified path
12+
func createDirectory(atPath path: String) throws {
13+
try createDirectory(atPath: path, withIntermediateDirectories: true)
14+
}
15+
16+
/// Creates a file at the specified path with the provided content
17+
func createFile(atPath path: String, contents data: Data) -> Bool {
18+
createFile(atPath: path, contents: data, attributes: nil)
19+
}
20+
21+
func loadData(from filePath: String) throws -> Data {
22+
guard let data = self.contents(atPath: filePath) else {
23+
throw FileHandlerError.couldNotLoadFile(filePath: filePath)
24+
}
25+
26+
return data
27+
}
28+
}

Sources/Helpers/Git.swift

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
//
2+
// Copyright (c) 2024 Adyen N.V.
3+
//
4+
// This file is open source and available under the MIT license. See the LICENSE file for more info.
5+
//
6+
7+
import Foundation
8+
9+
enum GitError: LocalizedError, Equatable {
10+
case couldNotClone(branchOrTag: String, repository: String)
11+
12+
var errorDescription: String? {
13+
switch self {
14+
case let .couldNotClone(branchOrTag, repository):
15+
"Could not clone \(repository) @ \(branchOrTag) - Please check the provided information"
16+
}
17+
}
18+
}
19+
20+
struct Git {
21+
22+
private let shell: ShellHandling
23+
private let fileHandler: FileHandling
24+
private let logger: Logging?
25+
26+
init(
27+
shell: ShellHandling,
28+
fileHandler: FileHandling,
29+
logger: Logging?
30+
) {
31+
self.shell = shell
32+
self.fileHandler = fileHandler
33+
self.logger = logger
34+
}
35+
36+
/// Clones a repository at a specific branch or tag into the current directory
37+
///
38+
/// - Parameters:
39+
/// - repository: The repository to clone
40+
/// - branchOrTag: The branch or tag to clone
41+
/// - targetDirectoryPath: The directory to clone into
42+
///
43+
/// - Returns: The local directory path where to find the cloned repository
44+
func clone(_ repository: String, at branchOrTag: String, targetDirectoryPath: String) throws {
45+
logger?.log("🐱 Cloning \(repository) @ \(branchOrTag) into \(targetDirectoryPath)", from: String(describing: Self.self))
46+
let command = "git clone -b \(branchOrTag) \(repository) \(targetDirectoryPath)"
47+
shell.execute(command)
48+
49+
guard fileHandler.fileExists(atPath: targetDirectoryPath) else {
50+
throw GitError.couldNotClone(branchOrTag: branchOrTag, repository: repository)
51+
}
52+
}
53+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
//
2+
// Copyright (c) 2024 Adyen N.V.
3+
//
4+
// This file is open source and available under the MIT license. See the LICENSE file for more info.
5+
//
6+
7+
import Foundation
8+
9+
/// A change indicating an `addition`, `removal` or genuine `change` of an element
10+
struct Change: Equatable {
11+
enum ChangeType: Equatable {
12+
case addition(description: String)
13+
case removal(description: String)
14+
case change(oldDescription: String, newDescription: String)
15+
}
16+
17+
var changeType: ChangeType
18+
var parentName: String
19+
20+
var listOfChanges: [String] = []
21+
}
22+
23+
extension Change.ChangeType {
24+
25+
var isAddition: Bool {
26+
switch self {
27+
case .addition:
28+
return true
29+
case .removal:
30+
return false
31+
case .change:
32+
return false
33+
}
34+
}
35+
36+
var isRemoval: Bool {
37+
switch self {
38+
case .addition:
39+
return false
40+
case .removal:
41+
return true
42+
case .change:
43+
return false
44+
}
45+
}
46+
47+
var isChange: Bool {
48+
switch self {
49+
case .addition:
50+
return false
51+
case .removal:
52+
return false
53+
case .change:
54+
return true
55+
}
56+
}
57+
}
58+
59+
extension [String: [Change]] {
60+
61+
var totalChangeCount: Int {
62+
var totalChangeCount = 0
63+
keys.forEach { targetName in
64+
totalChangeCount += self[targetName]?.count ?? 0
65+
}
66+
return totalChangeCount
67+
}
68+
}

0 commit comments

Comments
 (0)