Initial commit

This commit is contained in:
Simon Støvring 2022-12-05 13:25:51 +01:00
commit 030323ce7f
No known key found for this signature in database
GPG Key ID: 30FC5A50FD993021
36 changed files with 1992 additions and 0 deletions

9
.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
DerivedData/
.swiftpm/config/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc

76
.swiftlint.yml Normal file
View File

@ -0,0 +1,76 @@
excluded:
- .build
disabled_rules:
- nesting
opt_in_rules:
- anonymous_argument_in_multiline_closure
- array_init
- capture_variable
- collection_alignment
- conditional_returns_on_newline
- contains_over_filter_count
- contains_over_filter_is_empty
- contains_over_first_not_nil
- contains_over_range_nil_comparison
- convenience_type
- discarded_notification_center_observer
- discouraged_assert
- discouraged_none_name
- discouraged_object_literal
- discouraged_optional_boolean
- empty_collection_literal
- empty_count
- empty_string
- empty_xctest_method
- explicit_init
- fallthrough
- fatal_error_message
- file_name_no_space
- first_where
- flatmap_over_map_reduce
- identical_operands
- implicitly_unwrapped_optional
- joined_default_parameter
- last_where
- literal_expression_end_indentation
- lower_acl_than_parent
- modifier_order
- number_separator
- operator_usage_whitespace
- overridden_super_call
- pattern_matching_keywords
- prefer_self_in_static_references
- prefer_self_type_over_type_of_self
- prefer_zero_over_explicit_init
- prohibited_interface_builder
- prohibited_super_call
- reduce_into
- redundant_nil_coalescing
- redundant_type_annotation
- single_test_class
- sorted_first_last
- sorted_imports
- static_operator
- switch_case_on_newline
- test_case_accessibility
- toggle_bool
- trailing_closure
- unneeded_parentheses_in_closure_argument
- untyped_error_in_catch
- vertical_parameter_alignment_on_call
- yoda_condition
line_length:
warning: 150
error: 175
ignores_comments: true
type_name:
max_length:
warning: 60
error: 60
identifier_name:
allowed_symbols: "_"
max_length:
warning: 60
error: 60
min_length:
warning: 1

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,333 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1410"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "graph-deps"
BuildableName = "graph-deps"
BlueprintName = "graph-deps"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "GraphDepsTests"
BuildableName = "GraphDepsTests"
BlueprintName = "GraphDepsTests"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "GraphDeps"
BuildableName = "GraphDeps"
BlueprintName = "GraphDeps"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "Graph"
BuildableName = "Graph"
BlueprintName = "Graph"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "XcodeProjectReader"
BuildableName = "XcodeProjectReader"
BlueprintName = "XcodeProjectReader"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "XcodeProjectParser"
BuildableName = "XcodeProjectParser"
BlueprintName = "XcodeProjectParser"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "PackageSwiftParser"
BuildableName = "PackageSwiftParser"
BlueprintName = "PackageSwiftParser"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "ShellCommandRunner"
BuildableName = "ShellCommandRunner"
BlueprintName = "ShellCommandRunner"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "PackageSwiftFileParser"
BuildableName = "PackageSwiftFileParser"
BlueprintName = "PackageSwiftFileParser"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "PackageSwiftFileParserLive"
BuildableName = "PackageSwiftFileParserLive"
BlueprintName = "PackageSwiftFileParserLive"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "XcodeProjectParserLive"
BuildableName = "XcodeProjectParserLive"
BlueprintName = "XcodeProjectParserLive"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "ShellCommandRunnerLive"
BuildableName = "ShellCommandRunnerLive"
BlueprintName = "ShellCommandRunnerLive"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "GraphDeps_PackageSwiftFileParserLiveTests"
BuildableName = "GraphDeps_PackageSwiftFileParserLiveTests"
BlueprintName = "GraphDeps_PackageSwiftFileParserLiveTests"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "PackageSwiftFileReader"
BuildableName = "PackageSwiftFileReader"
BlueprintName = "PackageSwiftFileReader"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "PackageSwiftFileReaderLive"
BuildableName = "PackageSwiftFileReaderLive"
BlueprintName = "PackageSwiftFileReaderLive"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DumpPackageService"
BuildableName = "DumpPackageService"
BlueprintName = "DumpPackageService"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DumpPackageServiceLive"
BuildableName = "DumpPackageServiceLive"
BlueprintName = "DumpPackageServiceLive"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "GraphDepsTests"
BuildableName = "GraphDepsTests"
BlueprintName = "GraphDepsTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "PackageSwiftFileParserLiveTests"
BuildableName = "PackageSwiftFileParserLiveTests"
BlueprintName = "PackageSwiftFileParserLiveTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DumpPackageServiceLiveTests"
BuildableName = "DumpPackageServiceLiveTests"
BlueprintName = "DumpPackageServiceLiveTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES"
viewDebuggingEnabled = "No">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "graph-deps"
BuildableName = "graph-deps"
BlueprintName = "graph-deps"
ReferencedContainer = "container:">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "graph-deps"
BuildableName = "graph-deps"
BlueprintName = "graph-deps"
ReferencedContainer = "container:">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

50
Package.resolved Normal file
View File

@ -0,0 +1,50 @@
{
"pins" : [
{
"identity" : "aexml",
"kind" : "remoteSourceControl",
"location" : "https://github.com/tadija/AEXML.git",
"state" : {
"revision" : "38f7d00b23ecd891e1ee656fa6aeebd6ba04ecc3",
"version" : "4.6.1"
}
},
{
"identity" : "pathkit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/kylef/PathKit.git",
"state" : {
"revision" : "3bfd2737b700b9a36565a8c94f4ad2b050a5e574",
"version" : "1.0.1"
}
},
{
"identity" : "spectre",
"kind" : "remoteSourceControl",
"location" : "https://github.com/kylef/Spectre.git",
"state" : {
"revision" : "26cc5e9ae0947092c7139ef7ba612e34646086c7",
"version" : "0.10.1"
}
},
{
"identity" : "swift-argument-parser",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-argument-parser",
"state" : {
"revision" : "fddd1c00396eed152c45a46bea9f47b98e59301d",
"version" : "1.2.0"
}
},
{
"identity" : "xcodeproj",
"kind" : "remoteSourceControl",
"location" : "https://github.com/tuist/XcodeProj.git",
"state" : {
"revision" : "b6de1bfe021b861c94e7c83821b595083f74b997",
"version" : "8.8.0"
}
}
],
"version" : 2
}

59
Package.swift Normal file
View File

@ -0,0 +1,59 @@
// swift-tools-version: 5.7
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "GraphDeps",
platforms: [.macOS(.v13)],
products: [
.executable(name: "graph-deps", targets: ["GraphDeps"])
],
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.2.0"),
.package(url: "https://github.com/tuist/XcodeProj.git", .upToNextMajor(from: "8.8.0"))
],
targets: [
.executableTarget(name: "GraphDeps", dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
"DumpPackageService",
"DumpPackageServiceLive",
"Graph",
"PackageSwiftFileParser",
"PackageSwiftFileParserLive",
"ShellCommandRunner",
"ShellCommandRunnerLive",
"XcodeProjectParser",
"XcodeProjectParserLive"
]),
.target(name: "Graph"),
.target(name: "PackageSwiftFileParser"),
.target(name: "PackageSwiftFileParserLive", dependencies: [
"DumpPackageService",
"PackageSwiftFileParser"
]),
.target(name: "DumpPackageService"),
.target(name: "DumpPackageServiceLive", dependencies: [
"DumpPackageService",
"ShellCommandRunner"
]),
.target(name: "ShellCommandRunner"),
.target(name: "ShellCommandRunnerLive", dependencies: [
"ShellCommandRunner"
]),
.target(name: "XcodeProjectParser"),
.target(name: "XcodeProjectParserLive", dependencies: [
.product(name: "XcodeProj", package: "XcodeProj"),
"XcodeProjectParser"
]),
.testTarget(name: "DumpPackageServiceLiveTests", dependencies: [
"DumpPackageServiceLive",
"ShellCommandRunner"
]),
.testTarget(name: "PackageSwiftFileParserLiveTests", dependencies: [
"DumpPackageService",
"PackageSwiftFileParser",
"PackageSwiftFileParserLive"
], resources: [.copy("MockData")])
]
)

View File

@ -0,0 +1,5 @@
import Foundation
public protocol DumpPackageService {
func dumpPackageForSwiftPackageFile(at fileURL: URL) throws -> Data
}

View File

@ -0,0 +1,49 @@
import DumpPackageService
import Foundation
import ShellCommandRunner
public enum DumpPackageServiceLiveError: LocalizedError {
case invalidPackageSwiftFileURL
case unexpectedTerminationStatus(Int32)
case failedConvertingStringToData
public var errorDescription: String? {
switch self {
case .invalidPackageSwiftFileURL:
return "Expected URL to a Package.swift file"
case .unexpectedTerminationStatus(let status):
return "Unexpected termination status: \(status)"
case .failedConvertingStringToData:
return "Failed converting string to data"
}
}
}
public struct DumpPackageServiceLive: DumpPackageService {
private let shellCommandRunner: ShellCommandRunner
public init(shellCommandRunner: ShellCommandRunner) {
self.shellCommandRunner = shellCommandRunner
}
public func dumpPackageForSwiftPackageFile(at fileURL: URL) throws -> Data {
guard fileURL.lastPathComponent == "Package.swift" else {
throw DumpPackageServiceLiveError.invalidPackageSwiftFileURL
}
let directoryURL = fileURL.deletingLastPathComponent()
return try dumpPackage(at: directoryURL)
}
}
private extension DumpPackageServiceLive {
private func dumpPackage(at directoryURL: URL) throws -> Data {
let output = shellCommandRunner.run(withArguments: ["swift", "package", "dump-package"], fromDirectoryURL: directoryURL)
guard output.status == 0 else {
throw DumpPackageServiceLiveError.unexpectedTerminationStatus(output.status)
}
guard let data = output.message.data(using: .utf8) else {
throw DumpPackageServiceLiveError.failedConvertingStringToData
}
return data
}
}

13
Sources/Graph/Node.swift Normal file
View File

@ -0,0 +1,13 @@
public final class Node {
public let name: String
public private(set) var children: [Node]
public init(name: String, children: [Node] = []) {
self.name = name
self.children = children
}
public func addChild(_ node: Node) {
children.append(node)
}
}

View File

@ -0,0 +1,28 @@
import DumpPackageService
import DumpPackageServiceLive
import PackageSwiftFileParser
import PackageSwiftFileParserLive
import ShellCommandRunner
import ShellCommandRunnerLive
import XcodeProjectParser
import XcodeProjectParserLive
public enum CompositionRoot {
static var xcodeProjectParser: XcodeProjectParser {
return XcodeProjectParserLive(packageSwiftFileParser: packageSwiftFileParser)
}
}
private extension CompositionRoot {
private static var packageSwiftFileParser: PackageSwiftFileParser {
return PackageSwiftFileParserLive(dumpPackageService: dumpPackageService)
}
private static var dumpPackageService: DumpPackageService {
return DumpPackageServiceLive(shellCommandRunner: shellCommandRunner)
}
private static var shellCommandRunner: ShellCommandRunner {
return ShellCommandRunnerLive()
}
}

View File

@ -0,0 +1,14 @@
import ArgumentParser
import Foundation
@main
struct GraphDeps: ParsableCommand {
@Argument(help: "The Xcode project to parse.")
var input: String
static let configuration = CommandConfiguration(
abstract: "A Swift command-line tool to graph package dependencies"
)
func run() throws {}
}

View File

@ -0,0 +1,36 @@
extension PackageSwiftFile {
public enum Dependency: Equatable {
public struct SourceControlParameters: Equatable {
public let identity: String
public init(identity: String) {
self.identity = identity
}
}
public struct FileSystemParameters: Equatable {
public let identity: String
public let path: String
public let packageSwiftFile: PackageSwiftFile
public init(identity: String, path: String, packageSwiftFile: PackageSwiftFile) {
self.identity = identity
self.path = path
self.packageSwiftFile = packageSwiftFile
}
}
case sourceControl(SourceControlParameters)
case fileSystem(FileSystemParameters)
public static func sourceControl(identity: String) -> Self {
let parameters = SourceControlParameters(identity: identity)
return .sourceControl(parameters)
}
public static func fileSystem(identity: String, path: String, packageSwiftFile: PackageSwiftFile) -> Self {
let parameters = FileSystemParameters(identity: identity, path: path, packageSwiftFile: packageSwiftFile)
return .fileSystem(parameters)
}
}
}

View File

@ -0,0 +1,32 @@
public extension PackageSwiftFile.Target {
enum Dependency: Equatable {
public struct NameParameters: Equatable {
public let name: String
public init(name: String) {
self.name = name
}
}
public struct ProductInPackageParameters: Equatable {
public let name: String
public let packageName: String
public init(name: String, packageName: String) {
self.name = name
self.packageName = packageName
}
}
case name(NameParameters)
case productInPackage(ProductInPackageParameters)
public static func name(_ name: String) -> Self {
return .name(.init(name: name))
}
public static func product(_ name: String, inPackage package: String) -> Self {
return .productInPackage(.init(name: name, packageName: package))
}
}
}

View File

@ -0,0 +1,11 @@
public extension PackageSwiftFile {
struct Target: Equatable {
public let name: String
public let dependencies: [Dependency]
public init(name: String, dependencies: [Dependency] = []) {
self.name = name
self.dependencies = dependencies
}
}
}

View File

@ -0,0 +1,11 @@
public struct PackageSwiftFile: Equatable {
public let name: String
public let targets: [Target]
public let dependencies: [Dependency]
public init(name: String, targets: [Target] = [], dependencies: [Dependency] = []) {
self.name = name
self.targets = targets
self.dependencies = dependencies
}
}

View File

@ -0,0 +1,5 @@
import Foundation
public protocol PackageSwiftFileParser {
func parseFile(at fileURL: URL) throws -> PackageSwiftFile
}

View File

@ -0,0 +1,53 @@
extension IntermediatePackageSwiftFile {
enum Dependency: Decodable {
private enum CodingKeys: String, CodingKey {
case sourceControl
case fileSystem
}
struct SourceControlParameters: Decodable {
let identity: String
}
struct FileSystemParameters: Decodable {
let identity: String
let path: String
}
case sourceControl(SourceControlParameters)
case fileSystem(FileSystemParameters)
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if container.allKeys.contains(CodingKeys.sourceControl) {
self = try .decodeSourceControl(using: container)
} else if container.allKeys.contains(CodingKeys.fileSystem) {
self = try .decodeFileSystem(using: container)
} else {
let keysString = container.allKeys.map(\.stringValue).joined(separator: ", ")
let debugDescription = "Unsupported keys: " + keysString
throw DecodingError.dataCorrupted(.init(codingPath: container.codingPath, debugDescription: debugDescription))
}
}
}
}
private extension IntermediatePackageSwiftFile.Dependency {
private static func decodeSourceControl(using container: KeyedDecodingContainer<Self.CodingKeys>) throws -> Self {
let parametersContainer = try container.decode([SourceControlParameters].self, forKey: .sourceControl)
guard parametersContainer.count == 1 else {
let debugDescription = "Expected to decode exactly 1 parameter object but found \(parametersContainer.count)"
throw DecodingError.dataCorrupted(.init(codingPath: container.codingPath, debugDescription: debugDescription))
}
return .sourceControl(parametersContainer[0])
}
private static func decodeFileSystem(using container: KeyedDecodingContainer<Self.CodingKeys>) throws -> Self {
let parametersContainer = try container.decode([FileSystemParameters].self, forKey: .fileSystem)
guard parametersContainer.count == 1 else {
let debugDescription = "Expected to decode exactly 1 parameter object but found \(parametersContainer.count)"
throw DecodingError.dataCorrupted(.init(codingPath: container.codingPath, debugDescription: debugDescription))
}
return .fileSystem(parametersContainer[0])
}
}

View File

@ -0,0 +1,67 @@
extension IntermediatePackageSwiftFile.Target {
enum Dependency: Decodable {
private enum CodingKeys: CodingKey {
case byName
case product
}
struct ByNameParameters {
let name: String
}
struct ProductParameters {
let name: String
let package: String
}
case byName(ByNameParameters)
case product(ProductParameters)
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if container.allKeys.contains(CodingKeys.byName) {
self = try .decodeByName(using: container)
} else if container.allKeys.contains(CodingKeys.product) {
self = try .decodeProduct(using: container)
} else {
let keysString = container.allKeys.map(\.stringValue).joined(separator: ", ")
let debugDescription = "Unsupported keys: " + keysString
throw DecodingError.dataCorrupted(.init(codingPath: container.codingPath, debugDescription: debugDescription))
}
}
}
}
private extension IntermediatePackageSwiftFile.Target.Dependency {
private static func decodeByName(using container: KeyedDecodingContainer<Self.CodingKeys>) throws -> Self {
let values = try container.decode([String?].self, forKey: .byName)
guard values.count >= 1 else {
let debugDescription = "Expected to decode at least 1 string but found \(values.count)"
throw DecodingError.dataCorrupted(.init(codingPath: container.codingPath, debugDescription: debugDescription))
}
guard case let .some(name) = values[0] else {
let debugDescription = "Expected library name to be non-null"
throw DecodingError.dataCorrupted(.init(codingPath: container.codingPath, debugDescription: debugDescription))
}
let parameters = ByNameParameters(name: name)
return .byName(parameters)
}
private static func decodeProduct(using container: KeyedDecodingContainer<Self.CodingKeys>) throws -> Self {
let values = try container.decode([String?].self, forKey: .product)
guard values.count >= 2 else {
let debugDescription = "Expected to decode at least 2 strings but found \(values.count)"
throw DecodingError.dataCorrupted(.init(codingPath: container.codingPath, debugDescription: debugDescription))
}
guard case let .some(name) = values[0] else {
let debugDescription = "Expected library name to be non-null"
throw DecodingError.dataCorrupted(.init(codingPath: container.codingPath, debugDescription: debugDescription))
}
guard case let .some(package) = values[1] else {
let debugDescription = "Expected package name to be non-null"
throw DecodingError.dataCorrupted(.init(codingPath: container.codingPath, debugDescription: debugDescription))
}
let parameters = ProductParameters(name: name, package: package)
return .product(parameters)
}
}

View File

@ -0,0 +1,6 @@
extension IntermediatePackageSwiftFile {
struct Target: Decodable {
let name: String
let dependencies: [Dependency]
}
}

View File

@ -0,0 +1,5 @@
struct IntermediatePackageSwiftFile: Decodable {
let name: String
let targets: [Target]
let dependencies: [Dependency]
}

View File

@ -0,0 +1,47 @@
import DumpPackageService
import Foundation
import PackageSwiftFileParser
struct PackageSwiftFileMapper {
private let dumpPackageService: DumpPackageService
private let packageSwiftFileParser: PackageSwiftFileParser
init(dumpPackageService: DumpPackageService, packageSwiftFileParser: PackageSwiftFileParser) {
self.dumpPackageService = dumpPackageService
self.packageSwiftFileParser = packageSwiftFileParser
}
func map(_ intermediate: IntermediatePackageSwiftFile) throws -> PackageSwiftFile {
let targets = intermediate.targets.map(map)
let dependencies = try intermediate.dependencies.map(map)
return PackageSwiftFile(name: intermediate.name, targets: targets, dependencies: dependencies)
}
}
private extension PackageSwiftFileMapper {
private func map(_ intermediate: IntermediatePackageSwiftFile.Target) -> PackageSwiftFile.Target {
let dependencies = intermediate.dependencies.map(map)
return PackageSwiftFile.Target(name: intermediate.name, dependencies: dependencies)
}
private func map(_ intermediate: IntermediatePackageSwiftFile.Target.Dependency) -> PackageSwiftFile.Target.Dependency {
switch intermediate {
case .byName(let parameters):
return .name(parameters.name)
case .product(let parameters):
return .product(parameters.name, inPackage: parameters.package)
}
}
private func map(_ intermediate: IntermediatePackageSwiftFile.Dependency) throws -> PackageSwiftFile.Dependency {
switch intermediate {
case .sourceControl(let parameters):
return .sourceControl(identity: parameters.identity)
case .fileSystem(let parameters):
let fileURL = URL(filePath: parameters.path).appending(path: "Package.swift")
let contents = try dumpPackageService.dumpPackageForSwiftPackageFile(at: fileURL)
let packageSwiftFile = try packageSwiftFileParser.parseFile(at: fileURL)
return .fileSystem(identity: parameters.identity, path: parameters.path, packageSwiftFile: packageSwiftFile)
}
}
}

View File

@ -0,0 +1,19 @@
import DumpPackageService
import Foundation
import PackageSwiftFileParser
public struct PackageSwiftFileParserLive: PackageSwiftFileParser {
private let dumpPackageService: DumpPackageService
public init(dumpPackageService: DumpPackageService) {
self.dumpPackageService = dumpPackageService
}
public func parseFile(at fileURL: URL) throws -> PackageSwiftFile {
let contents = try dumpPackageService.dumpPackageForSwiftPackageFile(at: fileURL)
let decoder = JSONDecoder()
let intermediate = try decoder.decode(IntermediatePackageSwiftFile.self, from: contents)
let mapper = PackageSwiftFileMapper(dumpPackageService: dumpPackageService, packageSwiftFileParser: self)
return try mapper.map(intermediate)
}
}

View File

@ -0,0 +1,9 @@
public struct ShellCommandOutput {
public let status: Int32
public let message: String
public init(status: Int32, message: String) {
self.status = status
self.message = message
}
}

View File

@ -0,0 +1,5 @@
import Foundation
public protocol ShellCommandRunner {
func run(withArguments arguments: [String], fromDirectoryURL directoryURL: URL) -> ShellCommandOutput
}

View File

@ -0,0 +1,23 @@
import Foundation
import ShellCommandRunner
public struct ShellCommandRunnerLive: ShellCommandRunner {
public init() {}
public func run(withArguments arguments: [String], fromDirectoryURL directoryURL: URL) -> ShellCommandOutput {
let task = Process()
let pipe = Pipe()
task.currentDirectoryURL = directoryURL
task.standardOutput = pipe
task.standardError = pipe
task.arguments = ["-c", arguments.joined(separator: " ")]
task.launchPath = "/bin/zsh"
task.standardInput = nil
task.launch()
task.waitUntilExit()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let message = String(data: data, encoding: .utf8)!
let status = task.terminationStatus
return ShellCommandOutput(status: status, message: message)
}
}

View File

@ -0,0 +1,28 @@
import Foundation
public extension XcodeProject {
enum SwiftPackage {
public struct LocalParameters {
public let name: String
public let fileURL: URL
public init(name: String, fileURL: URL) {
self.name = name
self.fileURL = fileURL
}
}
public struct RemoteParameters {
public let name: String
public let repositoryURL: URL
public init(name: String, repositoryURL: URL) {
self.name = name
self.repositoryURL = repositoryURL
}
}
case local(LocalParameters)
case remote(RemoteParameters)
}
}

View File

@ -0,0 +1,13 @@
import Foundation
extension XcodeProject {
public struct Target {
public let name: String
public let packageProductDependencies: [String]
public init(name: String, packageProductDependencies: [String]) {
self.name = name
self.packageProductDependencies = packageProductDependencies
}
}
}

View File

@ -0,0 +1,13 @@
import Foundation
public struct XcodeProject {
public let name: String
public let targets: [Target]
public let swiftPackages: [SwiftPackage]
public init(name: String, targets: [Target] = [], swiftPackages: [SwiftPackage] = []) {
self.name = name
self.targets = targets
self.swiftPackages = swiftPackages
}
}

View File

@ -0,0 +1,5 @@
import Foundation
public protocol XcodeProjectParser {
func parseProject(at fileURL: URL) throws -> XcodeProject
}

View File

@ -0,0 +1,53 @@
import Foundation
import PackageSwiftFileParser
import PathKit
import XcodeProj
import XcodeProjectParser
public struct XcodeProjectParserLive: XcodeProjectParser {
private let packageSwiftFileParser: PackageSwiftFileParser
public init(packageSwiftFileParser: PackageSwiftFileParser) {
self.packageSwiftFileParser = packageSwiftFileParser
}
public func parseProject(at fileURL: URL) throws -> XcodeProject {
let path = Path(fileURL.relativePath)
let project = try XcodeProj(path: path)
let sourceRoot = fileURL.deletingLastPathComponent()
let remoteSwiftPackages = remoteSwiftPackages(in: project)
let localSwiftPackages = try localSwiftPackages(in: project, atSourceRoot: sourceRoot)
return XcodeProject(
name: fileURL.lastPathComponent,
targets: targets(in: project),
swiftPackages: remoteSwiftPackages + localSwiftPackages
)
}
}
private extension XcodeProjectParserLive {
func targets(in project: XcodeProj) -> [XcodeProject.Target] {
return project.pbxproj.nativeTargets.map { target in
let packageProductDependencies = target.packageProductDependencies.map(\.productName)
return .init(name: target.name, packageProductDependencies: packageProductDependencies)
}
}
func remoteSwiftPackages(in project: XcodeProj) -> [XcodeProject.SwiftPackage] {
return project.pbxproj.nativeTargets.reduce(into: []) { targetResult, target in
targetResult += target.dependencies.reduce(into: []) { dependencyResult, dependency in
guard let package = dependency.product?.package, let packageName = package.name else {
return
}
guard let rawRepositoryURL = package.repositoryURL, let repositoryURL = URL(string: rawRepositoryURL) else {
return
}
dependencyResult.append(.remote(.init(name: packageName, repositoryURL: repositoryURL)))
}
}
}
func localSwiftPackages(in project: XcodeProj, atSourceRoot sourceRoot: URL) throws -> [XcodeProject.SwiftPackage] {
return []
}
}

View File

@ -0,0 +1,27 @@
@testable import DumpPackageServiceLive
import XCTest
final class DumpPackageServiceLiveTests: XCTestCase {
func testThrowsWhenReceivingInvalidPackageSwiftFileURL() throws {
let shellCommandRunner = ShellCommandRunnerMock()
let service = DumpPackageServiceLive(shellCommandRunner: shellCommandRunner)
let fileURL = URL(fileURLWithPath: "/Users/john/Mock/NotAPackageSwiftFile")
XCTAssertThrowsError(try service.dumpPackageForSwiftPackageFile(at: fileURL))
}
func testInvokesDumpPackageCommandOnSwiftCLI() throws {
let shellCommandRunner = ShellCommandRunnerMock()
let service = DumpPackageServiceLive(shellCommandRunner: shellCommandRunner)
let fileURL = URL(fileURLWithPath: "/Users/john/Mock/Package.swift")
_ = try service.dumpPackageForSwiftPackageFile(at: fileURL)
XCTAssertEqual(shellCommandRunner.latestArguments, ["swift", "package", "dump-package"])
}
func testSetsCurrentDirectoryToSwiftPackage() throws {
let shellCommandRunner = ShellCommandRunnerMock()
let service = DumpPackageServiceLive(shellCommandRunner: shellCommandRunner)
let fileURL = URL(fileURLWithPath: "/Users/john/Mock/Package.swift")
_ = try service.dumpPackageForSwiftPackageFile(at: fileURL)
XCTAssertEqual(shellCommandRunner.latestDirectoryURL, URL(filePath: "/Users/john/Mock/"))
}
}

View File

@ -0,0 +1,13 @@
import Foundation
import ShellCommandRunner
final class ShellCommandRunnerMock: ShellCommandRunner {
private(set) var latestArguments: [String]?
private(set) var latestDirectoryURL: URL?
func run(withArguments arguments: [String], fromDirectoryURL directoryURL: URL) -> ShellCommandOutput {
latestArguments = arguments
latestDirectoryURL = directoryURL
return ShellCommandOutput(status: 0, message: "Success")
}
}

View File

@ -0,0 +1,15 @@
import DumpPackageService
import Foundation
struct DumpPackageServiceMock: DumpPackageService {
private let fileURLMap: [URL: URL]
init(fileURLMap: [URL: URL] = [:]) {
self.fileURLMap = fileURLMap
}
func dumpPackageForSwiftPackageFile(at fileURL: URL) throws -> Data {
let mappedFileURL = fileURLMap[fileURL] ?? fileURL
return try Data(contentsOf: mappedFileURL)
}
}

View File

@ -0,0 +1,90 @@
{
"cLanguageStandard" : null,
"cxxLanguageStandard" : null,
"dependencies" : [
],
"name" : "HexColor",
"packageKind" : {
"root" : [
"/Users/simon/Developer/Foo/HexColor"
]
},
"pkgConfig" : null,
"platforms" : [
{
"options" : [
],
"platformName" : "ios",
"version" : "16.0"
},
{
"options" : [
],
"platformName" : "maccatalyst",
"version" : "16.0"
}
],
"products" : [
{
"name" : "ColorCategories",
"settings" : [
],
"targets" : [
"ColorCategories"
],
"type" : {
"library" : [
"automatic"
]
}
}
],
"providers" : null,
"swiftLanguageVersions" : null,
"targets" : [
{
"dependencies" : [
],
"exclude" : [
],
"name" : "ColorCategories",
"resources" : [
],
"settings" : [
],
"type" : "regular"
},
{
"dependencies" : [
{
"byName" : [
"ColorCategories",
null
]
}
],
"exclude" : [
],
"name" : "ColorCategoriesTests",
"resources" : [
],
"settings" : [
],
"type" : "test"
}
],
"toolsVersion" : {
"_version" : "5.7.0"
}
}

View File

@ -0,0 +1,622 @@
{
"cLanguageStandard" : null,
"cxxLanguageStandard" : null,
"dependencies" : [
{
"sourceControl" : [
{
"identity" : "runestone",
"location" : {
"remote" : [
"git@github.com:simonbs/Runestone.git"
]
},
"productFilter" : null,
"requirement" : {
"branch" : [
"main"
]
}
}
]
},
{
"fileSystem" : [
{
"identity" : "hexcolor",
"path" : "/Users/simon/Developer/Foo/HexColor",
"productFilter" : null
}
]
}
],
"name" : "ScriptBrowserFeature",
"packageKind" : {
"root" : [
"/Users/simon/Developer/Foo/ScriptBrowserFeature"
]
},
"pkgConfig" : null,
"platforms" : [
{
"options" : [
],
"platformName" : "ios",
"version" : "16.0"
},
{
"options" : [
],
"platformName" : "maccatalyst",
"version" : "16.0"
}
],
"products" : [
{
"name" : "ScriptBrowserEntities",
"settings" : [
],
"targets" : [
"ScriptBrowserEntities"
],
"type" : {
"library" : [
"automatic"
]
}
},
{
"name" : "ScriptBrowserGroupsService",
"settings" : [
],
"targets" : [
"ScriptBrowserGroupsService"
],
"type" : {
"library" : [
"automatic"
]
}
},
{
"name" : "ScriptBrowserGroupsServiceMock",
"settings" : [
],
"targets" : [
"ScriptBrowserGroupsServiceMock"
],
"type" : {
"library" : [
"automatic"
]
}
},
{
"name" : "ScriptBrowserListService",
"settings" : [
],
"targets" : [
"ScriptBrowserListService"
],
"type" : {
"library" : [
"automatic"
]
}
},
{
"name" : "ScriptBrowserListServiceLive",
"settings" : [
],
"targets" : [
"ScriptBrowserListServiceLive"
],
"type" : {
"library" : [
"automatic"
]
}
},
{
"name" : "ScriptBrowserListServiceFactory",
"settings" : [
],
"targets" : [
"ScriptBrowserListServiceFactory"
],
"type" : {
"library" : [
"automatic"
]
}
},
{
"name" : "ScriptBrowserListServiceFactoryLive",
"settings" : [
],
"targets" : [
"ScriptBrowserListServiceFactoryLive"
],
"type" : {
"library" : [
"automatic"
]
}
},
{
"name" : "ScriptBrowserSearchService",
"settings" : [
],
"targets" : [
"ScriptBrowserSearchService"
],
"type" : {
"library" : [
"automatic"
]
}
},
{
"name" : "ScriptBrowserSidebarService",
"settings" : [
],
"targets" : [
"ScriptBrowserSidebarService"
],
"type" : {
"library" : [
"automatic"
]
}
},
{
"name" : "ScriptBrowserSidebarServiceLive",
"settings" : [
],
"targets" : [
"ScriptBrowserSidebarServiceLive"
],
"type" : {
"library" : [
"automatic"
]
}
},
{
"name" : "ScriptBrowserUI",
"settings" : [
],
"targets" : [
"ScriptBrowserUI"
],
"type" : {
"library" : [
"automatic"
]
}
}
],
"providers" : null,
"swiftLanguageVersions" : null,
"targets" : [
{
"dependencies" : [
],
"exclude" : [
],
"name" : "ScriptBrowserEntities",
"resources" : [
],
"settings" : [
],
"type" : "regular"
},
{
"dependencies" : [
{
"byName" : [
"ScriptBrowserEntities",
null
]
}
],
"exclude" : [
],
"name" : "ScriptBrowserGroupsService",
"resources" : [
],
"settings" : [
],
"type" : "regular"
},
{
"dependencies" : [
{
"byName" : [
"ScriptBrowserEntities",
null
]
},
{
"byName" : [
"ScriptBrowserGroupsService",
null
]
}
],
"exclude" : [
],
"name" : "ScriptBrowserGroupsServiceMock",
"resources" : [
],
"settings" : [
],
"type" : "regular"
},
{
"dependencies" : [
{
"byName" : [
"ScriptBrowserEntities",
null
]
}
],
"exclude" : [
],
"name" : "ScriptBrowserListService",
"resources" : [
],
"settings" : [
],
"type" : "regular"
},
{
"dependencies" : [
{
"byName" : [
"ScriptBrowserEntities",
null
]
},
{
"byName" : [
"ScriptBrowserGroupsService",
null
]
},
{
"byName" : [
"ScriptBrowserListService",
null
]
},
{
"byName" : [
"ScriptBrowserSearchService",
null
]
}
],
"exclude" : [
],
"name" : "ScriptBrowserListServiceLive",
"resources" : [
],
"settings" : [
],
"type" : "regular"
},
{
"dependencies" : [
{
"byName" : [
"ScriptBrowserEntities",
null
]
},
{
"byName" : [
"ScriptBrowserListService",
null
]
}
],
"exclude" : [
],
"name" : "ScriptBrowserListServiceFactory",
"resources" : [
],
"settings" : [
],
"type" : "regular"
},
{
"dependencies" : [
{
"byName" : [
"ScriptBrowserEntities",
null
]
},
{
"byName" : [
"ScriptBrowserGroupsService",
null
]
},
{
"byName" : [
"ScriptBrowserListServiceFactory",
null
]
},
{
"byName" : [
"ScriptBrowserListService",
null
]
},
{
"byName" : [
"ScriptBrowserListServiceLive",
null
]
},
{
"byName" : [
"ScriptBrowserSearchService",
null
]
}
],
"exclude" : [
],
"name" : "ScriptBrowserListServiceFactoryLive",
"resources" : [
],
"settings" : [
],
"type" : "regular"
},
{
"dependencies" : [
],
"exclude" : [
],
"name" : "ScriptBrowserSearchService",
"resources" : [
],
"settings" : [
],
"type" : "regular"
},
{
"dependencies" : [
{
"byName" : [
"ScriptBrowserEntities",
null
]
}
],
"exclude" : [
],
"name" : "ScriptBrowserSidebarService",
"resources" : [
{
"path" : "Resources/Localizable.strings",
"rule" : {
"process" : {
}
}
}
],
"settings" : [
],
"type" : "regular"
},
{
"dependencies" : [
{
"byName" : [
"ScriptBrowserGroupsService",
null
]
},
{
"byName" : [
"ScriptBrowserSidebarService",
null
]
}
],
"exclude" : [
],
"name" : "ScriptBrowserSidebarServiceLive",
"resources" : [
{
"path" : "Resources/Localizable.strings",
"rule" : {
"process" : {
}
}
}
],
"settings" : [
],
"type" : "regular"
},
{
"dependencies" : [
{
"byName" : [
"ScriptBrowserEntities",
null
]
},
{
"byName" : [
"ScriptBrowserListService",
null
]
},
{
"byName" : [
"ScriptBrowserListServiceFactory",
null
]
},
{
"byName" : [
"ScriptBrowserSidebarService",
null
]
},
{
"product" : [
"ColorCategories",
"HexColor",
null,
null
]
}
],
"exclude" : [
],
"name" : "ScriptBrowserUI",
"resources" : [
{
"path" : "Resources/Localizable.strings",
"rule" : {
"process" : {
}
}
}
],
"settings" : [
],
"type" : "regular"
},
{
"dependencies" : [
{
"byName" : [
"ScriptBrowserEntities",
null
]
},
{
"byName" : [
"ScriptBrowserGroupsService",
null
]
},
{
"byName" : [
"ScriptBrowserGroupsServiceMock",
null
]
},
{
"byName" : [
"ScriptBrowserListService",
null
]
},
{
"byName" : [
"ScriptBrowserListServiceFactory",
null
]
},
{
"byName" : [
"ScriptBrowserListServiceLive",
null
]
},
{
"byName" : [
"ScriptBrowserSearchService",
null
]
},
{
"byName" : [
"ScriptBrowserSidebarServiceLive",
null
]
},
{
"byName" : [
"ScriptBrowserUI",
null
]
}
],
"exclude" : [
],
"name" : "ScriptBrowserUITests",
"resources" : [
],
"settings" : [
],
"type" : "test"
}
],
"toolsVersion" : {
"_version" : "5.7.0"
}
}

View File

@ -0,0 +1,140 @@
import PackageSwiftFileParser
@testable import PackageSwiftFileParserLive
import XCTest
final class PackageSwiftFileParserLiveTests: XCTestCase {
private var dumpPackageService: DumpPackageServiceMock {
return DumpPackageServiceMock(fileURLMap: [
URL.Mock.root: Bundle.module.url(forMockDumpPackageNamed: "root"),
URL.Mock.hexColor: Bundle.module.url(forMockDumpPackageNamed: "hexcolor")
])
}
func testParsesName() throws {
let parser = PackageSwiftFileParserLive(dumpPackageService: dumpPackageService)
let swiftPackageFile = try parser.parseFile(at: URL.Mock.root)
XCTAssertEqual(swiftPackageFile.name, "ScriptBrowserFeature")
}
func testParsesTargetNames() throws {
let parser = PackageSwiftFileParserLive(dumpPackageService: dumpPackageService)
let swiftPackageFile = try parser.parseFile(at: URL.Mock.root)
let targetNames = swiftPackageFile.targets.map(\.name)
XCTAssertEqual(targetNames, [
"ScriptBrowserEntities",
"ScriptBrowserGroupsService",
"ScriptBrowserGroupsServiceMock",
"ScriptBrowserListService",
"ScriptBrowserListServiceLive",
"ScriptBrowserListServiceFactory",
"ScriptBrowserListServiceFactoryLive",
"ScriptBrowserSearchService",
"ScriptBrowserSidebarService",
"ScriptBrowserSidebarServiceLive",
"ScriptBrowserUI",
"ScriptBrowserUITests"
])
}
// swiftlint:disable:next function_body_length
func testParsesTargets() throws {
let parser = PackageSwiftFileParserLive(dumpPackageService: dumpPackageService)
let swiftPackageFile = try parser.parseFile(at: URL.Mock.root)
XCTAssertEqual(swiftPackageFile.targets, [
.init(name: "ScriptBrowserEntities", dependencies: []),
.init(name: "ScriptBrowserGroupsService", dependencies: [
.name("ScriptBrowserEntities")
]),
.init(name: "ScriptBrowserGroupsServiceMock", dependencies: [
.name("ScriptBrowserEntities"),
.name("ScriptBrowserGroupsService")
]),
.init(name: "ScriptBrowserListService", dependencies: [
.name("ScriptBrowserEntities")
]),
.init(name: "ScriptBrowserListServiceLive", dependencies: [
.name("ScriptBrowserEntities"),
.name("ScriptBrowserGroupsService"),
.name("ScriptBrowserListService"),
.name("ScriptBrowserSearchService")
]),
.init(name: "ScriptBrowserListServiceFactory", dependencies: [
.name("ScriptBrowserEntities"),
.name("ScriptBrowserListService")
]),
.init(name: "ScriptBrowserListServiceFactoryLive", dependencies: [
.name("ScriptBrowserEntities"),
.name("ScriptBrowserGroupsService"),
.name("ScriptBrowserListServiceFactory"),
.name("ScriptBrowserListService"),
.name("ScriptBrowserListServiceLive"),
.name("ScriptBrowserSearchService")
]),
.init(name: "ScriptBrowserSearchService", dependencies: []),
.init(name: "ScriptBrowserSidebarService", dependencies: [
.name("ScriptBrowserEntities")
]),
.init(name: "ScriptBrowserSidebarServiceLive", dependencies: [
.name("ScriptBrowserGroupsService"),
.name("ScriptBrowserSidebarService")
]),
.init(name: "ScriptBrowserUI", dependencies: [
.name("ScriptBrowserEntities"),
.name("ScriptBrowserListService"),
.name("ScriptBrowserListServiceFactory"),
.name("ScriptBrowserSidebarService"),
.product("ColorCategories", inPackage: "HexColor")
]),
.init(name: "ScriptBrowserUITests", dependencies: [
.name("ScriptBrowserEntities"),
.name("ScriptBrowserGroupsService"),
.name("ScriptBrowserGroupsServiceMock"),
.name("ScriptBrowserListService"),
.name("ScriptBrowserListServiceFactory"),
.name("ScriptBrowserListServiceLive"),
.name("ScriptBrowserSearchService"),
.name("ScriptBrowserSidebarServiceLive"),
.name("ScriptBrowserUI")
])
])
}
func testParsesDependencies() throws {
let parser = PackageSwiftFileParserLive(dumpPackageService: dumpPackageService)
let swiftPackageFile = try parser.parseFile(at: URL.Mock.root)
XCTAssertEqual(swiftPackageFile.dependencies, [
.sourceControl(identity: "runestone"),
.fileSystem(
identity: "hexcolor",
path: "/Users/simon/Developer/Foo/HexColor",
packageSwiftFile: PackageSwiftFile(
name: "HexColor",
targets: [
.init(name: "ColorCategories"),
.init(name: "ColorCategoriesTests", dependencies: [
.name("ColorCategories")
])
]
)
)
])
}
}
private extension URL {
enum Mock {
static var root: URL {
return URL(filePath: "/Users/simon/Developer/Foo/root/Package.swift")
}
static var hexColor: URL {
return URL(filePath: "/Users/simon/Developer/Foo/HexColor/Package.swift")
}
}
}
private extension Bundle {
func url(forMockDumpPackageNamed filename: String) -> URL {
return url(forResource: "MockData/" + filename, withExtension: "json")!
}
}