Skip to content

Commit ece5fc3

Browse files
author
Teamcity Account
committed
Merge pull request #16941 in MA/avito-ios from MBS-9680_ai_boilerplate to develop
Automatically merge pull request #16941 in MA/avito-ios from MBS-9680_ai_boilerplate to develop * commit '0733672bec8b69ad5a1e5db551a57f19bae67172': MBS-9680: fix linter issues MBS-9680: run ci checks MBS-9680: gen Package.swift when invoking ./ai, basic command tree, basic state mutator MBS-9680: add Types module GitOrigin-RevId: 5a4786e45347e8aba943051ee17781beaabfecc3
1 parent c13a431 commit ece5fc3

12 files changed

+699
-0
lines changed

.gitignore

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# System
2+
.DS_Store
3+
4+
# Swift
5+
.build
6+
.cache
7+
.swiftpm
8+
/Packages
9+
/*.xcodeproj
10+
xcuserdata
11+
12+
# Python
13+
*.pyc
14+
.idea
15+
.cache
16+
__pycache__
17+
.pytest_cache
18+
venv
19+
.python-version
20+
.mypy_cache
21+
.coverage
22+
23+
# Utility
24+
*.ignored*

Makefile

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
open:
2+
bash make.sh open
3+
4+
gen:
5+
bash make.sh generate
6+
7+
clean:
8+
bash make.sh clean
9+
10+
build:
11+
bash make.sh build
12+
13+
test:
14+
bash make.sh test
15+
16+
run:
17+
bash make.sh run
18+
19+
package:
20+
bash make.sh package
21+
22+
run_ci_tests:
23+
bash make.sh run_ci_tests

Package.swift

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// swift-tools-version:5.2
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "CommandLineToolkit",
7+
platforms: [
8+
.macOS(.v10_15),
9+
],
10+
products: [
11+
.library(
12+
name: "CommandLineToolkit",
13+
targets: [
14+
"Types",
15+
]
16+
),
17+
],
18+
dependencies: [
19+
],
20+
targets: [
21+
.target(
22+
// MARK: Types
23+
name: "Types",
24+
dependencies: [
25+
],
26+
path: "Sources/Types"
27+
),
28+
.testTarget(
29+
// MARK: TypesTests
30+
name: "TypesTests",
31+
dependencies: [
32+
"Types",
33+
],
34+
path: "Tests/TypesTests"
35+
),
36+
]
37+
)

PackageGenerator.swift

+148
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import Foundation
2+
3+
func log(_ text: String) {
4+
if ProcessInfo.processInfo.environment["DEBUG"] != nil {
5+
print(text)
6+
}
7+
}
8+
9+
let knownImportsToIgnore = [
10+
"Darwin",
11+
"Dispatch",
12+
"Foundation",
13+
"XCTest",
14+
"Network",
15+
]
16+
17+
let packageNameForDependency = [
18+
"ArgumentParser": "swift-argument-parser",
19+
]
20+
21+
let jsonEncoder = JSONEncoder()
22+
jsonEncoder.outputFormatting = [.prettyPrinted, .sortedKeys]
23+
24+
let importStatementExpression = try NSRegularExpression(
25+
pattern: "^(@testable )?import (.*)$",
26+
options: [.anchorsMatchLines]
27+
)
28+
29+
struct ModuleDescription {
30+
let name: String
31+
let deps: [String]
32+
let path: String
33+
let isTest: Bool
34+
}
35+
36+
func generate(at url: URL, isTestTarget: Bool) throws -> [ModuleDescription] {
37+
guard let enumerator = FileManager().enumerator(
38+
at: url,
39+
includingPropertiesForKeys: nil,
40+
options: [.skipsSubdirectoryDescendants, .skipsHiddenFiles]
41+
) else {
42+
print("Failed to create file enumerator for url '\(url)'")
43+
return []
44+
}
45+
46+
var result = [ModuleDescription]()
47+
48+
while let moduleFolderUrl = enumerator.nextObject() as? URL {
49+
let moduleEnumerator = FileManager().enumerator(
50+
at: moduleFolderUrl,
51+
includingPropertiesForKeys: [.isRegularFileKey],
52+
options: [.skipsHiddenFiles]
53+
)
54+
let moduleName = moduleFolderUrl.lastPathComponent
55+
log("Analyzing \(moduleName)")
56+
57+
var importedModuleNames = Set<String>()
58+
59+
while let moduleFile = moduleEnumerator?.nextObject() as? URL {
60+
if ["md"].contains(moduleFile.pathExtension) {
61+
continue
62+
}
63+
log(" Analyzing \(moduleFile.lastPathComponent)")
64+
guard try moduleFile.resourceValues(forKeys: [.isRegularFileKey]).isRegularFile == true else {
65+
log(" Skipping \(moduleFile.lastPathComponent): is not regular file")
66+
continue
67+
}
68+
let fileContents = try String(contentsOf: moduleFile)
69+
.split(separator: "\n")
70+
.filter { !$0.starts(with: "//") }
71+
for line in fileContents {
72+
let matches = importStatementExpression.matches(in: String(line), options: [], range: NSMakeRange(0, line.count))
73+
guard matches.count == 1 else {
74+
continue
75+
}
76+
77+
let importedModuleName = (line as NSString).substring(with: matches[0].range(at: 2))
78+
importedModuleNames.insert(importedModuleName)
79+
}
80+
}
81+
82+
let path = moduleFolderUrl.path.dropFirst(moduleFolderUrl.deletingLastPathComponent().deletingLastPathComponent().path.count + 1)
83+
let dependencies = importedModuleNames.filter { !knownImportsToIgnore.contains($0) }.sorted()
84+
85+
let isTestHelper = moduleFolderUrl.path.hasSuffix("TestHelpers")
86+
87+
result.append(
88+
ModuleDescription(name: moduleName, deps: dependencies, path: String(path), isTest: isTestTarget && !isTestHelper)
89+
)
90+
}
91+
92+
return result
93+
}
94+
95+
func generatePackageSwift(raplacementForTargets: [String]) throws {
96+
log("Loading template")
97+
var templateContents = try String(contentsOf: URL(fileURLWithPath: "PackageTemplate.swift.txt"))
98+
templateContents = templateContents.replacingOccurrences(
99+
of: "<__TARGETS__>",
100+
with: raplacementForTargets.map { " \($0)" }.joined(separator: "\n")
101+
)
102+
103+
let packageSwiftPath = URL(fileURLWithPath: "Package.swift")
104+
105+
if ProcessInfo.processInfo.environment["ON_CI"] != nil {
106+
log("Checking for Package.swift consistency")
107+
let existingContents = try String(contentsOf: packageSwiftPath)
108+
if existingContents != templateContents {
109+
print("\(#file):\(#line): ON_CI is set, and Package.swift differs. Please update and commit Package.swift!")
110+
exit(1)
111+
}
112+
}
113+
114+
log("Saving Package.swift")
115+
try templateContents.write(to: packageSwiftPath, atomically: true, encoding: .utf8)
116+
}
117+
118+
func main() throws {
119+
var moduleDescriptions = [ModuleDescription]()
120+
moduleDescriptions.append(
121+
contentsOf: try generate(at: URL(fileURLWithPath: "Sources"), isTestTarget: false)
122+
)
123+
moduleDescriptions.append(
124+
contentsOf: try generate(at: URL(fileURLWithPath: "Tests"), isTestTarget: true)
125+
)
126+
127+
var generatedTargetStatements = [String]()
128+
let sortedModuleDescriptions: [ModuleDescription] = moduleDescriptions.sorted { $0.name < $1.name }
129+
for moduleDescription in sortedModuleDescriptions {
130+
generatedTargetStatements.append(".\(!moduleDescription.isTest ? "target" : "testTarget")(")
131+
generatedTargetStatements.append(" // MARK: \(moduleDescription.name)")
132+
generatedTargetStatements.append(" name: " + "\"\(moduleDescription.name)\"" + ",")
133+
generatedTargetStatements.append(" dependencies: [")
134+
for dependency in moduleDescription.deps {
135+
if let packageName = packageNameForDependency[dependency] {
136+
generatedTargetStatements.append(" .product(name: \"\(dependency)\", package: \"\(packageName)\"),")
137+
} else {
138+
generatedTargetStatements.append(" \"\(dependency)\",")
139+
}
140+
}
141+
generatedTargetStatements.append(" ],")
142+
generatedTargetStatements.append(" path: " + "\"" + moduleDescription.path + "\"")
143+
generatedTargetStatements.append("),")
144+
}
145+
try generatePackageSwift(raplacementForTargets: generatedTargetStatements)
146+
}
147+
148+
try main()

PackageTemplate.swift.txt

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// swift-tools-version:5.2
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "CommandLineToolkit",
7+
platforms: [
8+
.macOS(.v10_15),
9+
],
10+
products: [
11+
.library(
12+
name: "CommandLineToolkit",
13+
targets: [
14+
"Types",
15+
]
16+
),
17+
],
18+
dependencies: [
19+
],
20+
targets: [
21+
<__TARGETS__>
22+
]
23+
)

README.md

+6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
11
# CommandLineToolkit
22

33
Various things that make it easier to implement command line tools in Swift.
4+
5+
## Development
6+
7+
- To generate and open Xcode project: `make open`
8+
- To generate Package.swift only: `make package`
9+
- To run tests: `make test`

Sources/Types/Either.swift

+128
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import Foundation
2+
3+
public enum Either<Left, Right>: CustomStringConvertible {
4+
case left(Left)
5+
case right(Right)
6+
7+
public init(_ value: Left) {
8+
self = .left(value)
9+
}
10+
11+
public init(_ value: Right) {
12+
self = .right(value)
13+
}
14+
15+
public var description: String {
16+
switch self {
17+
case .left(let value):
18+
return "Result.left(\(value)))"
19+
case .right(let value):
20+
return "Result.right(\(value))"
21+
}
22+
}
23+
24+
public var isLeft: Bool {
25+
switch self {
26+
case .left:
27+
return true
28+
case .right:
29+
return false
30+
}
31+
}
32+
33+
public var isRight: Bool {
34+
return !isLeft
35+
}
36+
37+
public var left: Left? {
38+
switch self {
39+
case .left(let value):
40+
return value
41+
case .right:
42+
return nil
43+
}
44+
}
45+
46+
public var right: Right? {
47+
switch self {
48+
case .right(let value):
49+
return value
50+
case .left:
51+
return nil
52+
}
53+
}
54+
}
55+
56+
public extension Either where Right: Error {
57+
static func success(_ value: Left) -> Either {
58+
return Either.left(value)
59+
}
60+
61+
static func error(_ error: Right) -> Either {
62+
return Either.right(error)
63+
}
64+
65+
init(value: Left) {
66+
self = .left(value)
67+
}
68+
69+
init(error: Right) {
70+
self = .right(error)
71+
}
72+
73+
func dematerialize() throws -> Left {
74+
switch self {
75+
case .left(let value):
76+
return value
77+
case .right(let error):
78+
throw error
79+
}
80+
}
81+
82+
func mapResult<NewResult>(_ transform: (Left) -> NewResult) -> Either<NewResult, Error> {
83+
do {
84+
let result = try dematerialize()
85+
return .success(transform(result))
86+
} catch {
87+
return .error(error)
88+
}
89+
}
90+
91+
var isSuccess: Bool { return isLeft }
92+
var isError: Bool { return isRight }
93+
}
94+
95+
extension Either: Equatable where Left: Equatable, Right: Equatable {}
96+
97+
extension Either: Codable where Left: Codable, Right: Codable {
98+
private enum CodingKeys: String, CodingKey {
99+
case value, caseId
100+
}
101+
102+
private enum CaseId: String, Codable {
103+
case left, right
104+
}
105+
106+
public func encode(to encoder: Encoder) throws {
107+
var container = encoder.container(keyedBy: CodingKeys.self)
108+
switch self {
109+
case .left(let value):
110+
try container.encode(CaseId.left, forKey: CodingKeys.caseId)
111+
try container.encode(value, forKey: CodingKeys.value)
112+
case .right(let value):
113+
try container.encode(CaseId.right, forKey: CodingKeys.caseId)
114+
try container.encode(value, forKey: CodingKeys.value)
115+
}
116+
}
117+
118+
public init(from decoder: Decoder) throws {
119+
let container = try decoder.container(keyedBy: CodingKeys.self)
120+
let caseId = try container.decode(CaseId.self, forKey: CodingKeys.caseId)
121+
switch caseId {
122+
case .left:
123+
self = .left(try container.decode(Left.self, forKey: CodingKeys.value))
124+
case .right:
125+
self = .right(try container.decode(Right.self, forKey: CodingKeys.value))
126+
}
127+
}
128+
}

0 commit comments

Comments
 (0)