diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/General-Package.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/General-Package.xcscheme
new file mode 100644
index 0000000..7bf6c57
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/General-Package.xcscheme
@@ -0,0 +1,104 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/General.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/General.xcscheme
new file mode 100644
index 0000000..b244165
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/General.xcscheme
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/GeneralIOs.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/GeneralIOs.xcscheme
new file mode 100644
index 0000000..77c7f6b
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/GeneralIOs.xcscheme
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/GeneralKit.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/GeneralKit.xcscheme
new file mode 100644
index 0000000..cf20136
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/GeneralKit.xcscheme
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Package.resolved b/Package.resolved
index bdb6027..1115124 100644
--- a/Package.resolved
+++ b/Package.resolved
@@ -10,6 +10,33 @@
"version": "4.5.0"
}
},
+ {
+ "package": "Files",
+ "repositoryURL": "https://github.com/JohnSundell/Files",
+ "state": {
+ "branch": null,
+ "revision": "d273b5b7025d386feef79ef6bad7de762e106eaf",
+ "version": "4.2.0"
+ }
+ },
+ {
+ "package": "PathKit",
+ "repositoryURL": "https://github.com/kylef/PathKit",
+ "state": {
+ "branch": "master",
+ "revision": "2fcd4618d52869b342e208324d455131a48f9e9b",
+ "version": null
+ }
+ },
+ {
+ "package": "SourceKitten",
+ "repositoryURL": "https://github.com/jpsim/SourceKitten.git",
+ "state": {
+ "branch": null,
+ "revision": "558628392eb31d37cb251cfe626c53eafd330df6",
+ "version": "0.31.1"
+ }
+ },
{
"package": "Spectre",
"repositoryURL": "https://github.com/kylef/Spectre.git",
@@ -33,7 +60,7 @@
"repositoryURL": "https://github.com/rosberry/StencilSwiftKit.git",
"state": {
"branch": "stable",
- "revision": "2e289f24626ce6bc75e515fd41a52db0b139097d",
+ "revision": "17d5fb6c67a7a391b36f1154c80c429ac3653ec4",
"version": null
}
},
@@ -46,6 +73,33 @@
"version": "0.5.0"
}
},
+ {
+ "package": "SwiftParsec",
+ "repositoryURL": "https://github.com/rosberry/SwiftParsec",
+ "state": {
+ "branch": "master",
+ "revision": "2725b7e9580988be124d86e507921f88a0e5bb3a",
+ "version": null
+ }
+ },
+ {
+ "package": "SWXMLHash",
+ "repositoryURL": "https://github.com/drmohundro/SWXMLHash.git",
+ "state": {
+ "branch": null,
+ "revision": "9183170d20857753d4f331b0ca63f73c60764bf3",
+ "version": "5.0.2"
+ }
+ },
+ {
+ "package": "umaler",
+ "repositoryURL": "https://github.com/rosberry/umaler.git",
+ "state": {
+ "branch": "master",
+ "revision": "7a713e838978f7cdee6b40eae3a8efd89cf055d0",
+ "version": null
+ }
+ },
{
"package": "XcodeProj",
"repositoryURL": "https://github.com/tuist/XcodeProj.git",
@@ -60,8 +114,8 @@
"repositoryURL": "https://github.com/jpsim/Yams.git",
"state": {
"branch": null,
- "revision": "6652aa7b793d3c8a075db0614acb575fcaecf457",
- "version": "0.7.0"
+ "revision": "9ff1cc9327586db4e0c8f46f064b6a82ec1566fa",
+ "version": "4.0.6"
}
},
{
diff --git a/Package.swift b/Package.swift
index f656344..ceb994f 100644
--- a/Package.swift
+++ b/Package.swift
@@ -8,7 +8,7 @@ import PackageDescription
let package = Package(
name: "General",
- platforms: [.macOS(.v10_12)],
+ platforms: [.macOS(.v10_13)],
products: [
.executable(name: "General", targets: ["General"]),
.executable(name: "GeneralIOs", targets: ["GeneralIOs"]),
@@ -16,11 +16,13 @@ let package = Package(
],
dependencies: [
//with bumped PathKit version
+ .package(url: "https://github.com/kylef/PathKit", .branch("master")),
.package(url: "https://github.com/rosberry/StencilSwiftKit.git", .branch("stable")),
.package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMajor(from: "0.1.0")),
- .package(url: "https://github.com/jpsim/Yams.git", .upToNextMajor(from: "0.0.0")),
+ .package(url: "https://github.com/jpsim/Yams.git", .upToNextMajor(from: "4.0.6")),
.package(url: "https://github.com/weichsel/ZIPFoundation.git", .upToNextMajor(from: "0.9.0")),
- .package(url: "https://github.com/tuist/XcodeProj.git", .upToNextMajor(from: "7.0.0"))
+ .package(url: "https://github.com/tuist/XcodeProj.git", .upToNextMajor(from: "7.0.0")),
+ .package(url: "https://github.com/rosberry/umaler.git", .branch("master")),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
@@ -42,6 +44,7 @@ let package = Package(
name: "GeneralIOs",
dependencies: [
.target(name: "GeneralKit"),
+ .product(name: "UmalerKit", package: "umaler"),
"XcodeProj"
])
]
diff --git a/Sources/GeneralIOs/Commands/Bootstrap.swift b/Sources/GeneralIOs/Commands/Bootstrap.swift
new file mode 100644
index 0000000..09f5b6b
--- /dev/null
+++ b/Sources/GeneralIOs/Commands/Bootstrap.swift
@@ -0,0 +1,245 @@
+//
+// Bootstrap.swift
+// GeneralIOs
+//
+// Created by Nick Tyunin on 15.10.2021.
+//
+
+import Foundation
+import ArgumentParser
+import Files
+import UmalerKit
+import GeneralKit
+import Stencil
+
+final class Bootstrap: ParsableCommand {
+ static let configuration: CommandConfiguration = .init(abstract: "Allows to generate project from plant uml definition",
+ subcommands: [Make.self, Config.self],
+ defaultSubcommand: Make.self)
+
+ enum Error: Swift.Error {
+ case template
+ case bundleId
+ case github
+
+ var description: String {
+ switch self {
+ case .template:
+ return "No template path was specified. Please use `--template` option or specify it in reusable config " +
+ "with `bootsrap config update --template`"
+ case .bundleId:
+ return "No bundle id was specified. Please use --bundle_id option or provide company name using --company. " +
+ "You also can specify company in reusable config using `bootsrap config update --company`"
+ case .github:
+ return "Templates repo was not specified"
+ }
+ }
+ }
+
+ final class Config: ParsableCommand {
+
+ enum Constants {
+ static let template: String = "template"
+ static let company: String = "company"
+ static let firebase: String = "firebase"
+ static let swiftgen: String = "swiftgen"
+ static let licenseplist: String = "licenseplist"
+ }
+
+ static let configuration: CommandConfiguration = .init(abstract: "Allows read or modify reusable bootstrap config",
+ subcommands: [UpdateConfig.self, PrintConfig.self],
+ defaultSubcommand: UpdateConfig.self)
+
+
+ }
+
+ final class UpdateConfig: ParsableCommand {
+
+ public typealias Dependencies = HasBootstrapService
+ static let configuration: CommandConfiguration = .init(commandName: "update", abstract: "Allows to modify reusable bootstrap config")
+
+ @Option(name: .shortAndLong, help: "The path to the project template", completion: .directory)
+ var template: String?
+
+ @Option(name: .shortAndLong, help: "The bundle identifier of new project")
+ var company: String?
+
+ @Option(name: .shortAndLong, help: "Enable or disable firebase")
+ var firebase: Bool?
+
+ @Option(name: .shortAndLong, help: "Enable or disable firebase")
+ var swiftgen: Bool?
+
+ @Option(name: .shortAndLong, help: "Enable or disable licenseplist")
+ var licenseplist: Bool?
+
+ @Option(name: .shortAndLong, help: "Set additional variable value. Format name=value ")
+ var variable: String?
+
+ var dependencies: Dependencies {
+ Services
+ }
+
+ func run() throws {
+ var config = dependencies.bootstrapService.config
+ if let template = self.template {
+ config[Config.Constants.template] = template
+ }
+ if let company = self.company {
+ config[Config.Constants.company] = company
+ }
+ if let firebase = self.firebase {
+ config[Config.Constants.firebase] = firebase
+ }
+ if let swiftgen = self.swiftgen {
+ config[Config.Constants.swiftgen] = swiftgen
+ }
+ if let licenseplist = self.licenseplist {
+ config[Config.Constants.licenseplist] = licenseplist
+ }
+ if let variable = self.variable {
+ let components = variable.split(separator: "=").map { component in
+ String(component.trimmingCharacters(in: .whitespaces))
+ }
+ guard components.count == 2 else {
+ print(yellow("Invalid variable format provided: `\(variable)`. Missing `name=value`"))
+ return
+ }
+ let name = components[0]
+ let value = components[1]
+ config[name] = value
+ }
+ dependencies.bootstrapService.config = config
+ }
+
+ }
+
+ final class PrintConfig: ParsableCommand {
+ public typealias Dependencies = HasBootstrapService
+ static let configuration: CommandConfiguration = .init(commandName: "print", abstract: "Displays reusable bootstrap config")
+
+ var dependencies: Dependencies {
+ Services
+ }
+
+ func run() throws {
+ let config = dependencies.bootstrapService.config
+ guard let data = try? JSONSerialization.data(withJSONObject: config, options: .prettyPrinted),
+ let string = String(data: data, encoding: .utf8) else {
+ print(red("Could not parse stored config"))
+ return
+ }
+ print(green(string))
+ }
+ }
+
+ final class Make: ParsableCommand {
+ public typealias Dependencies = HasBootstrapService
+
+ static let configuration: CommandConfiguration = .init(abstract: "Generate project from plant uml definition")
+
+ @Option(name: .shortAndLong, help: "The name of new project")
+ var name: String
+
+ @Option(name: .shortAndLong, help: "The path to uml diagrams file")
+ var uml: String?
+
+ @Option(name: .shortAndLong, help: "The path to the project template", completion: .directory)
+ var template: String?
+
+ @Option(name: .shortAndLong, help: "The bundle identifier of new project. If not specified will be composed using company and name values: `com.company.name`")
+ var bundleId: String?
+
+ @Option(name: .shortAndLong, help: "The bundle identifier of new project")
+ var company: String?
+
+ @Option(name: .shortAndLong, help: "Enable or disable firebase")
+ var firebase: Bool?
+
+ @Option(name: .shortAndLong, help: "Enable or disable swiftgen")
+ var swiftgen: Bool?
+
+ @Option(name: .shortAndLong, help: "Enable or disable licenseplist")
+ var licenseplist: Bool?
+
+ // @Option(name: .shortAndLong, help: "Path to plant uml file", completion: .directory)
+ // var uml: String
+
+ @Option(name: [.customLong("repo"), .customShort("r")],
+ help: .init(stringLiteral:
+ "Fetch templates from specified github repo." +
+ " Format: \"\\ [branch]\"."),
+ completion: .templatesRepos)
+ var githubPath: String?
+
+ var dependencies: Dependencies {
+ Services
+ }
+
+ func run() throws {
+ let config = uml != nil ? try composeUMLConfig() : try composeProjectConfig()
+ try dependencies.bootstrapService.bootstrap(with: config)
+ }
+
+ private func composeUMLConfig() throws -> BootstrapConfig {
+ var projectConfig = dependencies.bootstrapService.config
+ guard let template = self.template ?? projectConfig[Config.Constants.template] as? String else {
+ throw Error.template
+ }
+ projectConfig.removeValue(forKey: Config.Constants.template)
+ if let company = self.company {
+ projectConfig[Config.Constants.company] = company
+ }
+ if let firebase = self.firebase {
+ projectConfig[Config.Constants.firebase] = firebase
+ }
+ if let swiftgen = self.swiftgen {
+ projectConfig[Config.Constants.swiftgen] = swiftgen
+ }
+ if let licenseplist = self.licenseplist {
+ projectConfig[Config.Constants.licenseplist] = licenseplist
+ }
+ projectConfig["name"] = name
+ projectConfig["year"] = "\(Calendar.current.component(.year, from: Date()))"
+ if let bundleId = self.bundleId {
+ projectConfig["bundle_identifier"] = bundleId
+ } else if let company = self.company ?? projectConfig[Config.Constants.company] as? String {
+ projectConfig["bundle_identifier"] = "com.\(company).\(name)".lowercased()
+ } else {
+ throw Error.bundleId
+ }
+ var context = [String: Any]()
+ context["project"] = projectConfig
+ return .init(name: name, context: context, template: template, diagrams: uml)
+ }
+
+ private func composeProjectConfig() throws -> BootstrapConfig {
+ var projectConfig = dependencies.bootstrapService.config
+ guard let template = self.template ?? projectConfig[Config.Constants.template] as? String else {
+ throw Error.template
+ }
+ projectConfig.removeValue(forKey: Config.Constants.template)
+ if let company = self.company ?? projectConfig[Config.Constants.company] as? String {
+ projectConfig["organization_name"] = company
+ }
+ if let firebase = self.firebase ?? projectConfig[Config.Constants.firebase] as? Bool {
+ projectConfig[Config.Constants.firebase] = firebase ? "Yes" : "No"
+ }
+ if let swiftgen = self.swiftgen ?? projectConfig[Config.Constants.swiftgen] as? Bool {
+ projectConfig[Config.Constants.swiftgen] = swiftgen ? "Yes" : "No"
+ }
+ if let licenseplist = self.licenseplist ?? projectConfig[Config.Constants.licenseplist] as? Bool {
+ projectConfig[Config.Constants.licenseplist] = licenseplist ? "Yes" : "No"
+ }
+ projectConfig["name"] = name
+ if let bundleId = self.bundleId {
+ projectConfig["bundle_identifier"] = bundleId
+ } else if let company = self.company ?? projectConfig[Config.Constants.company] as? String {
+ projectConfig["bundle_identifier"] = "com.\(company).\(name)".lowercased()
+ } else {
+ throw Error.bundleId
+ }
+ return .init(name: name, context: projectConfig, template: template, diagrams: nil)
+ }
+ }
+}
diff --git a/Sources/GeneralIOs/Models/BootstrapConfig.swift b/Sources/GeneralIOs/Models/BootstrapConfig.swift
new file mode 100644
index 0000000..4e9b090
--- /dev/null
+++ b/Sources/GeneralIOs/Models/BootstrapConfig.swift
@@ -0,0 +1,13 @@
+//
+// BootstrapConfig.swift
+// GeneralIOs
+//
+// Created by Nick Tyunin on 11.11.2021.
+//
+
+public struct BootstrapConfig {
+ let name: String
+ let context: [String: Any]
+ let template: String
+ let diagrams: String?
+}
diff --git a/Sources/GeneralIOs/Services/BootstrapService/BootstrapService.swift b/Sources/GeneralIOs/Services/BootstrapService/BootstrapService.swift
new file mode 100644
index 0000000..2d74f4c
--- /dev/null
+++ b/Sources/GeneralIOs/Services/BootstrapService/BootstrapService.swift
@@ -0,0 +1,12 @@
+//
+// Copyright © 2021 Rosberry. All rights reserved.
+//
+
+public protocol HasBootstrapService: AnyObject {
+ var bootstrapService: BootstrapService { get }
+}
+
+public protocol BootstrapService: AnyObject {
+ var config: [String: Any] { get set }
+ func bootstrap(with config: BootstrapConfig) throws
+}
diff --git a/Sources/GeneralIOs/Services/BootstrapService/BootstrapServiceImpl.swift b/Sources/GeneralIOs/Services/BootstrapService/BootstrapServiceImpl.swift
new file mode 100644
index 0000000..1d1a8c8
--- /dev/null
+++ b/Sources/GeneralIOs/Services/BootstrapService/BootstrapServiceImpl.swift
@@ -0,0 +1,87 @@
+//
+// Copyright © 2021 Rosberry. All rights reserved.
+//
+import UmalerKit
+import GeneralKit
+import Stencil
+import Foundation
+
+public final class BootstrapServiceImpl: BootstrapService {
+
+ private lazy var configPath: String = "\(Constants.generalHomePath)/.bootstrap"
+
+ typealias Dependencies = HasFileHelper & HasProjectServiceFactory & HasShell
+
+ private var dependencies: Dependencies
+
+ public var config: [String : Any] {
+ get {
+ guard let data = try? Data(contentsOf: URL(fileURLWithPath: configPath)),
+ let config = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] else {
+ return [:]
+ }
+ return config
+ }
+ set {
+ guard let data = try? JSONSerialization.data(withJSONObject: newValue, options: .fragmentsAllowed) else {
+ return
+ }
+ try? data.write(to: URL(fileURLWithPath: configPath))
+ }
+ }
+
+ init(dependencies: Dependencies) {
+ self.dependencies = dependencies
+ }
+
+
+ public func bootstrap(with config: BootstrapConfig) throws {
+ if config.diagrams != nil {
+ let bootsraper = UMLBootstraper(dependencies: dependencies)
+ try bootsraper.bootstrap(with: config)
+ }
+ else {
+ try bootstrapProject(with: config)
+ }
+
+ swiftgen(with: config)
+ //try depo(with: config)
+ }
+
+ private func bootstrapProject(with config: BootstrapConfig) throws {
+ var arguments = ["--no-input", config.template]
+ arguments.append(contentsOf: config.context.map { key, value in
+ "\(key)=\(value)"
+ })
+ try dependencies.shell(path: "/usr/local/bin/cookiecutter", arguments: arguments)
+ }
+
+ private func depo(with config: BootstrapConfig) throws {
+ guard isInstalled(tool: "depo"),
+ isExists(path: "\(config.name)/Depofile") else {
+ return
+ }
+ let shell = dependencies.shell
+ try shell(loud: "(cd \(config.name) && depo install)")
+ }
+
+ private func swiftgen(with config: BootstrapConfig) {
+ guard isInstalled(tool: "swiftgen") else {
+ return
+ }
+ let shell = dependencies.shell
+ _ = try? shell(loud: "(cd \(config.name) && swiftgen)")
+ }
+
+ private func isExists(path: String) -> Bool{
+ return dependencies.fileHelper.fileManager.fileExists(atPath: path)
+ }
+
+ private func isInstalled(tool: String) -> Bool {
+ guard let path = try? dependencies.shell(silent: "command -v \(tool)"),
+ path.isEmpty == false else {
+ return false
+ }
+ return true
+ }
+}
diff --git a/Sources/GeneralIOs/Services/BootstrapService/UMLBootstraper.swift b/Sources/GeneralIOs/Services/BootstrapService/UMLBootstraper.swift
new file mode 100644
index 0000000..dfccb93
--- /dev/null
+++ b/Sources/GeneralIOs/Services/BootstrapService/UMLBootstraper.swift
@@ -0,0 +1,590 @@
+//
+// Bootstraper.swift
+// GeneralIOs
+//
+// Created by Nick Tyunin on 28.10.2021.
+//
+
+import UmalerKit
+import GeneralKit
+import Stencil
+import StencilSwiftKit
+import Yams
+import PathKit
+import Foundation
+
+final class UMLBootstraper {
+
+ enum Error: LocalizedError {
+ case architecture(String)
+ case noDiagrams
+
+ var errorDescription: String? {
+ switch self {
+ case let .architecture(path):
+ return "Could not parse architecture using specified uml path \(path)"
+ case .noDiagrams:
+ return "No diagrams were specified"
+ }
+ }
+ }
+
+ enum StringResolve {
+ case resolved(String)
+ case unresolved([ArchitectureTemplateItem.MatchToken])
+ case renderFailure
+ }
+
+ enum Resolve {
+ case resolvedDirectory(String)
+ case unresolvedDirectoryName(StringResolve)
+ case resolvedFile(String, String)
+ case unresolvedFile
+ case unresolvedFileName(StringResolve)
+ case renderFailure
+ }
+
+ var context: [String: Any] = [:]
+ private lazy var environment: Environment = {
+ let environment = Environment(loader: nil, extensions: [], templateClass: VariablesTemplate.self)
+ environment.extensions.forEach { ext in
+ ext.registerStencilSwiftExtensions()
+ }
+ return environment
+ }()
+
+ private lazy var architectureParser = ArchitectureUMLParser()
+ private lazy var diagramsParser = PlantUMLParser()
+ private lazy var plantuml = Plantuml()
+ private lazy var projectService: ProjectService = {
+ let service: ProjectService
+ if let projectContext = context["project"] as? [String: Any],
+ let projectName = projectContext["name"] as? String,
+ let path = try? ProjectService.findProject(in: .current + projectName) {
+ service = dependencies.projectServiceFactory.makeProjectService(path: path.parent())
+ try? service.createProject(projectName: path.lastComponent)
+ } else {
+ service = dependencies.projectServiceFactory.makeProjectService(path: .current)
+ }
+ return service
+ }()
+
+ typealias Dependencies = HasFileHelper & HasProjectServiceFactory
+
+ private var dependencies: Dependencies
+
+ init(dependencies: Dependencies) {
+ self.dependencies = dependencies
+ }
+
+ public func bootstrap(with config: BootstrapConfig) throws {
+ initContext(with: config)
+ guard let template = try createProjectFiles(template: config.template, destination: ".") else {
+ return
+ }
+ guard let diagramsPath = config.diagrams else {
+ throw Error.noDiagrams
+ }
+ let diagrams = try parseDiagrams(path: diagramsPath)
+ guard let architecture = try parseArchitecture(
+ diagrams: diagrams,
+ template: template,
+ methodsExcepts: [
+ "success",
+ "failure",
+ "finish"
+ ]) else {
+ throw Error.architecture(diagramsPath)
+ }
+ // First try generate basic implementation and compose specific context
+ try bootstrap(item: architecture, destination: ".")
+ // Second try regenerate specific files implementations using modified context
+ try bootstrap(item: architecture, destination: ".")
+ try dependencies.fileHelper.removeFile(at: .init(fileURLWithPath: "./.boot"))
+ try projectService.write()
+ }
+
+ private func initContext(with config: BootstrapConfig) {
+ context = config.context
+ }
+
+ private func createProjectFiles(template: String, destination: String) throws -> (ArchitectureTemplateItem?) {
+ let fileInfo = try dependencies.fileHelper.fileInfo(with: URL(fileURLWithPath: expandingTildeInPath(template)))
+ let status = resolve(fileInfo)
+
+ switch status {
+ case let .resolvedDirectory(name):
+ return try composeFolder(fileInfo: fileInfo, tokens: [.concrete(name)], name: name, destination: destination)
+ case let .resolvedFile(name, content):
+ try content.write(toFile: "\(destination)/\(name)", atomically: true, encoding: .utf8)
+ case let .unresolvedDirectoryName(resolve):
+ switch resolve {
+ case .resolved:
+ print("Invalid resolved status for template at \(fileInfo.url)")
+ case let .unresolved(tokens):
+ return try composeFolder(fileInfo: fileInfo, tokens: tokens, name: nil, destination: destination)
+ case .renderFailure:
+ print("Could not render file name of template \(fileInfo.url)")
+ }
+ case let .unresolvedFileName(resolve):
+ switch resolve {
+ case .resolved:
+ print("Invalid resolved status for template at \(fileInfo.url)")
+ case let .unresolved(tokens):
+ return try makeTemplateFile(fileInfo: fileInfo, tokens: tokens, destination: destination)
+ case .renderFailure:
+ return try makeTemplateFile(fileInfo: fileInfo, tokens: [], destination: destination)
+ }
+ case .unresolvedFile:
+ return try makeTemplateFile(fileInfo: fileInfo, tokens: [], destination: destination)
+ case .renderFailure:
+ return try makeTemplateFile(fileInfo: fileInfo, tokens: [], destination: destination)
+ }
+ return nil
+ }
+
+ private func composeFolder(fileInfo: FileInfo, tokens: [ArchitectureTemplateItem.MatchToken], name: String?, destination: String) throws -> ArchitectureTemplateItem? {
+ if let name = name {
+ try dependencies.fileHelper.createDirectory(at: "\(destination)/\(name)")
+ }
+ let folder = bootPath(destination: destination, name: name ?? fileInfo.url.lastPathComponent)
+ try dependencies.fileHelper.createDirectory(at: folder)
+ let filesDestination = "\(destination)/\(name ?? fileInfo.url.lastPathComponent)"
+ let items = try dependencies.fileHelper.contentsOfDirectory(at: fileInfo.url).compactMap { fileInfo in
+ try createProjectFiles(template: fileInfo.url.path, destination: filesDestination)
+ }
+ guard items.isEmpty == false else {
+ let url = URL(fileURLWithPath: folder)
+ try dependencies.fileHelper.removeFile(at: url)
+ return nil
+ }
+ return .folder(tokens, items)
+ }
+
+ private func makeTemplateFile(fileInfo: FileInfo, tokens: [ArchitectureTemplateItem.MatchToken], destination: String) throws -> ArchitectureTemplateItem? {
+ guard fileInfo.url.lastPathComponent != ".DS_Store" else {
+ return nil
+ }
+ let floder = ".boot/\(destination)"
+ try dependencies.fileHelper.createDirectory(at: floder)
+ let path = "\(floder)/\(fileInfo.url.lastPathComponent)"
+ guard let data = try? Data(contentsOf: fileInfo.url) else {
+ print("Could not read template file at \(fileInfo.url)")
+ return nil
+ }
+ try data.write(to: URL(fileURLWithPath: path), options: .atomic)
+ let name = fileInfo.url.deletingPathExtension().lastPathComponent
+ switch resolveVariables(string: name) {
+ case let .unresolved(tokens):
+ return .file(tokens)
+ default:
+ return .file([.concrete(name)])
+ }
+ }
+
+ private func resolve(_ fileInfo: FileInfo) -> Resolve {
+ let fileNameResolve = resolveFileName(fileInfo)
+ switch fileNameResolve {
+ case let .resolved(fileName):
+ guard fileInfo.isDirectory == false else {
+ return .resolvedDirectory(fileName)
+ }
+ return resolveFile(with: fileName, fileInfo: fileInfo)
+ case .unresolved:
+ return fileInfo.isDirectory ? .unresolvedDirectoryName(fileNameResolve) : .unresolvedFileName(fileNameResolve)
+ case .renderFailure:
+ return .renderFailure
+ }
+ }
+
+ private func resolveFile(with fileName: String, fileInfo: FileInfo) -> Resolve {
+ guard let data = try? Data(contentsOf: fileInfo.url),
+ let string = String(data: data, encoding: .utf8) else {
+ return .renderFailure
+ }
+ let template = VariablesTemplate(templateString: string, environment: environment)
+ let unresolvedVariables = self.unresolvedVariables(in: template)
+ guard unresolvedVariables.isEmpty else {
+ return .unresolvedFile
+ }
+ guard let content = try? template.render(context) else {
+ return .renderFailure
+ }
+ return .resolvedFile(fileName, content)
+ }
+
+ private func resolveFileName(_ fileInfo: FileInfo) -> StringResolve {
+ let fileName = fileInfo.url.lastPathComponent.trimmingCharacters(in: .whitespaces)
+ return resolveVariables(string: fileName)
+ }
+
+ private func resolveVariables(string: String) -> StringResolve {
+ let template = VariablesTemplate(templateString: string, environment: environment)
+ let unresolvedVariables = self.unresolvedVariables(in: template)
+ guard unresolvedVariables.isEmpty else {
+ return .unresolved(tokenize(string: string, variables: unresolvedVariables, template: template))
+ }
+ guard let value = try? template.render(context) else {
+ return .renderFailure
+ }
+ return .resolved(value)
+ }
+
+ private func tokenize(string: String, variables: [String], template: VariablesTemplate) -> [ArchitectureTemplateItem.MatchToken] {
+ let pattern = "\\{\\{\\s*([a-zA-Z][a-zA-Z0-9.]*)\\s*\\}\\}"
+ let matches = parseAllRegexMatches(pattern: pattern, rangeIndex: 0, string: string)
+ var tokens = [ArchitectureTemplateItem.MatchToken]()
+ var startIndex = string.startIndex
+
+ func unresolved(match: String) -> String? {
+ variables.first { string in
+ match.contains(string)
+ }
+ }
+
+ matches.forEach { match in
+ guard let range = string.range(of: match) else {
+ return
+ }
+ let prefix = String(string[startIndex.. [String] {
+ template.variables.compactMap { variable -> String? in
+ hasValue(for: variable) ? nil : variable
+ }
+ }
+
+ private func hasValue(for variable: String) -> Bool {
+ hasValue(for: variable.split(separator: ".").map({ String($0) }), in: context)
+ }
+
+ private func hasValue(for components: [String], in dictionary: [String: Any]) -> Bool {
+ guard let key = components.first?.trimmingCharacters(in: .whitespacesAndNewlines),
+ let anyValue = dictionary[key] else {
+ return false
+ }
+ guard components.count > 1 else {
+ return true
+ }
+ guard let dictionary = anyValue as? [String: Any] else {
+ return false
+ }
+ return hasValue(for: Array(components.dropFirst()), in: dictionary)
+ }
+
+ private func expandingTildeInPath(_ path: String) -> String {
+ return path.replacingOccurrences(of: "~", with: FileManager.default.homeDirectoryForCurrentUser.path)
+ }
+
+ private func parseDiagrams(path: String) throws -> [Diagram] {
+ let preprocessedPath = try plantuml.preprocessed(path: path, mode: "ios")
+ let diagrams = try diagramsParser.parse(path: preprocessedPath)
+ return diagrams
+ }
+
+ private func parseArchitecture(diagrams: [Diagram],
+ template: ArchitectureTemplateItem,
+ methodsExcepts: [String]) throws -> ArchitectureItem? {
+ try architectureParser.parse(diagrams: diagrams, using: [template] + methodsExcepts.map { name in
+ .except(.method([.concrete(name)]))
+ }).first
+ }
+
+ @discardableResult
+ private func bootstrap(item: ArchitectureItem, destination: String) throws -> Resolve {
+ switch item {
+ case let .folder(folder):
+ return try bootstrap(folder: folder, destination: destination)
+ case let .object(object):
+ return try bootstrap(object: object, destination: destination)
+ }
+ }
+
+ @discardableResult
+ private func bootstrap(folder: ArchitectureItem.Folder, destination: String) throws -> Resolve {
+ let path = "\(destination)/\(folder.name)"
+ try dependencies.fileHelper.createDirectory(at: path)
+ guard let template = self.template(for: destination, name: folder.name),
+ let files = try? dependencies.fileHelper.contentsOfDirectory(at: template.url) else {
+ try folder.items.forEach { item in
+ try bootstrap(item: item, destination: path)
+ }
+ return .resolvedDirectory(folder.name)
+ }
+
+ func item(for file: FileInfo) -> ArchitectureItem? {
+ let fileName = fileNameBase(from: file)
+ return folder.items.first { item in
+ return isMatch(pattern: fileName, using: name(of: item))
+ }
+ }
+
+ func bootstrapMissingFile(_ file: FileInfo, destination: String) throws {
+ guard file.url.lastPathComponent.lowercased() != ".ds_store" else {
+ return
+ }
+ switch resolve(file) {
+ case let .resolvedFile(name, content):
+ try addFile(name: name, destination: destination, content: content)
+ case let .resolvedDirectory(name):
+ let path = "\(destination)/\(name)"
+ try dependencies.fileHelper.createDirectory(at: path)
+ try dependencies.fileHelper.contentsOfDirectory(at: file.url).forEach { file in
+ try bootstrapMissingFile(file, destination: path)
+ }
+ default:
+ break
+ }
+ }
+
+ let fileName = fileNameBase(from: template)
+ let variables = resolveContext(pattern: fileName, using: folder.name)
+
+ func renderFiles() throws {
+ try folder.items.forEach { item in
+ try bootstrap(item: item, destination: path)
+ }
+
+ try files.forEach { file in
+ try bootstrapMissingFile(file, destination: path)
+ }
+ }
+
+ try renderFiles()
+
+ variables.forEach { name in
+ context = clean(context: context, name: name) ?? context
+ }
+ return .resolvedDirectory(folder.name)
+ }
+
+ @discardableResult
+ private func bootstrap(object: ArchitectureItem.Object, destination: String) throws -> Resolve {
+ guard let template = self.template(for: destination, name: object.name) else {
+ return .renderFailure
+ }
+ var methods = [[String: Any]]()
+ object.methods.forEach { method in
+ var methodContext = [String:Any]()
+ methodContext["name"] = method.name
+ methodContext["calls"] = method.calls.map{ call -> [String: Any] in
+ var callContext = [String: Any]()
+ callContext["name"] = call.name
+ callContext["called"] = call.called
+ return callContext
+ }
+ methods.append(methodContext)
+ }
+ context[object.name] = ["methods": methods]
+ let fileName = fileNameBase(from: template)
+ resolveContext(pattern: fileName, using: object.name)
+ let resolve = self.resolve(template)
+ switch resolve {
+ case let .resolvedFile(name, content):
+ try addFile(name: name, destination: destination, content: content)
+ default:
+ break
+ }
+ return resolve
+ }
+
+ private func template(for destination: String, name: String?) -> FileInfo? {
+ let bootUrl = URL(fileURLWithPath: bootPath(destination: destination))
+
+ guard let fileInfo = try? dependencies.fileHelper.fileInfo(with: bootUrl) else {
+ return nil
+ }
+
+ func findMatchFile(folder: FileInfo, name: String?) -> FileInfo? {
+ guard let name = name else {
+ return folder
+ }
+ guard let files = try? dependencies.fileHelper.contentsOfDirectory(at: folder.url) else {
+ return nil
+ }
+ return files.first { file in
+ let fileName = fileNameBase(from: file)
+ guard fileName != name else {
+ return true
+ }
+ return isMatch(pattern: fileName, using: name)
+ }
+ }
+
+ guard fileInfo.isExists == false else {
+ return findMatchFile(folder: fileInfo, name: name)
+ }
+ var parentFolderPath = URL(fileURLWithPath: destination).deletingLastPathComponent().path
+ let rootPath = URL(fileURLWithPath: "./").path
+ parentFolderPath.removeFirst(rootPath.count)
+ parentFolderPath = "." + parentFolderPath
+ guard let parentFolderTemplate = template(for: parentFolderPath, name: nil),
+ let thisFolderTemplate = findMatchFile(folder: parentFolderTemplate, name: fileInfo.url.lastPathComponent) else {
+ return nil
+ }
+ return findMatchFile(folder: thisFolderTemplate, name: name)
+ }
+
+ private func isMatch(pattern: String, using resolved: String) -> Bool {
+ let (isSuccess, _) = parseMatch(pattern: pattern, using: resolved, parseHandler: nil)
+ return isSuccess
+ }
+
+ @discardableResult
+ private func resolveContext(pattern: String, using resolved: String) -> [String] {
+ let (isSuccess, values) = parseMatch(pattern: pattern, using: resolved) { name, value in
+ self.context = self.update(context: self.context, name: name, value: value) ?? self.context
+ }
+ guard isSuccess else {
+ return []
+ }
+ return values
+ }
+
+ private func parseMatch(pattern: String, using resolved: String, parseHandler: ((String, String) -> Void)?) -> (Bool, [String]) {
+ guard pattern != resolved else {
+ return (true, [])
+ }
+ var values = [String]()
+ let variablesResolve = resolveVariables(string: pattern)
+ let tokens: [ArchitectureTemplateItem.MatchToken]
+ switch variablesResolve {
+ case let .resolved(resolvedPattern):
+ return (resolvedPattern == resolved, [])
+ case let .unresolved(missingTokens):
+ tokens = missingTokens
+ case .renderFailure:
+ return (false, [])
+ }
+ let regex = tokens.map { token -> String in
+ switch token {
+ case let .concrete(name):
+ return name
+ default:
+ return "([a-zA-Z][a-zA-Z0-9]*)"
+ }
+ }.joined()
+ var matches = parseAllRegexMatches(pattern: regex, rangeIndex: 1, string: resolved)
+ guard matches.isEmpty == false else {
+ return (false, [])
+ }
+ var string = resolved
+ for token in tokens {
+ switch token {
+ case let .concrete(substing):
+ guard string.starts(with: substing) else {
+ return (false, [])
+ }
+ string.removeFirst(substing.count)
+ case let .variable(name):
+ guard matches.isEmpty == false else {
+ return (false, [])
+ }
+ let value = matches.removeFirst()
+ string.removeFirst(value.count)
+ parseHandler?(name, value)
+ values.append(name)
+ case .any:
+ guard matches.isEmpty == false else {
+ continue
+ }
+ let value = matches.removeFirst()
+ string.removeFirst(value.count)
+ }
+ }
+ return (true, values)
+ }
+
+ private func update(context: [String: Any]?, name: String, value: String) -> [String: Any]? {
+ update(context: context, path: name.split(separator: ".").map{ String($0)}, value: value)
+ }
+
+ private func update(context: [String: Any]?, path: [String], value: String) -> [String: Any]? {
+ guard let key = path.first else {
+ return nil
+ }
+ var path = path
+ path.removeFirst()
+
+ var context = context ?? [:]
+ if path.isEmpty {
+ context[key] = value
+ } else {
+ context[key] = update(context: context[key] as? [String:Any], path: path, value: value)
+ }
+ return context
+ }
+
+ private func clean(context: [String: Any]?, name: String) -> [String: Any]? {
+ guard let rootKey = name.split(separator: ".").first else {
+ return context
+ }
+ var context = context
+ context?.removeValue(forKey: String(rootKey))
+ return context
+ }
+
+ private func fileNameBase(from fileInfo: FileInfo) -> String {
+ if fileInfo.isDirectory {
+ return fileInfo.url.lastPathComponent
+ }
+ else {
+ return fileInfo.url.deletingPathExtension().lastPathComponent
+ }
+ }
+
+ private func bootPath(destination: String) -> String {
+ let path: String
+ if let prefix = [".//", "./", "."].first { destination.contains($0) } {
+ var destination = destination
+ destination.removeFirst(prefix.count)
+ if destination.isEmpty {
+ path = ".boot"
+ } else {
+ path = ".boot/\(destination)"
+ }
+ } else {
+ path = ".boot/\(destination)"
+ }
+ return path
+ }
+
+ private func bootPath(destination: String, name: String) -> String {
+ "\(bootPath(destination: destination))/\(name)"
+ }
+
+ private func name(of item: ArchitectureItem) -> String {
+ switch item {
+ case let .folder(folder):
+ return folder.name
+ case let .object(object):
+ return object.name
+ }
+ }
+
+ private func addFile(name: String, destination: String, content: String) throws {
+ let path = "\(destination)/\(name)"
+ try content.write(toFile: path, atomically: true, encoding: .utf8)
+ try projectService.addFile(targetName: nil, filePath: .current + .init(path))
+ }
+}
diff --git a/Sources/GeneralIOs/Services/FontService/FontService.swift b/Sources/GeneralIOs/Services/FontService/FontService.swift
deleted file mode 100644
index 61533f6..0000000
--- a/Sources/GeneralIOs/Services/FontService/FontService.swift
+++ /dev/null
@@ -1,295 +0,0 @@
-//
-// Created by Evgeny Schwarzkopf on 08.04.2022.
-//
-
-import Foundation
-import ArgumentParser
-import GeneralKit
-import AEXML
-import Stencil
-import PathKit
-
-public final class FontService {
-
- private enum Error: Swift.Error, LocalizedError {
- case notFoundFonts(String)
- case notFoundTarget(String)
- case notFoundInfoPlist(String)
- case invalidData
- case notFoundTemplate
- case somethingGoingWrong(String, String)
-
- public var errorDescription: String? {
- switch self {
- case let .notFoundFonts(path):
- return red("Is not found fonts in path \(path). Please check correctly path.")
- case let .notFoundTarget(targetName):
- return red("Is not found target - \(targetName)")
- case let .notFoundInfoPlist(path):
- return red("Is not found info plist by path \(path)")
- case .invalidData:
- return red("Data is not valid. Please check correctly Info.plist")
- case .notFoundTemplate:
- return red(#"""
- Is not found folder templates in directory.
- Please, call command `general setup -r rosberry/general-templates\ ios`
- """#)
- case .somethingGoingWrong(let title, let subtitle):
- return red("""
- Something going wrong...
- I try it \(title) ...
- But I can't perform because \(subtitle).
- Please check and try again.
- """)
- }
- }
- }
-
- typealias Dependencies = HasFileHelper &
- HasProjectServiceFactory &
- HasSpecFactory
-
- private lazy var fileHelper = dependencies.fileHelper
-
- private lazy var projectService: ProjectService = {
- let service = dependencies.projectServiceFactory.makeProjectService(path: "./")
- try? service.createProject(projectName: "\(unwrappedProjectName).xcodeproj")
- return service
- }()
-
- private lazy var specFactory: SpecFactory = dependencies.specFactory
-
- private lazy var generalSpec: GeneralSpec? = {
- let pathURL = URL(fileURLWithPath: directoryPath, isDirectory: true)
- let specURL = URL(fileURLWithPath: Constants.generalSpecName, relativeTo: pathURL)
- return try? specFactory.makeSpec(url: specURL)
- }()
-
- private lazy var projectName: String? = {
- return (try? fileHelper.contentsOfDirectory(at: "./").first { file in
- file.url.pathExtension == "xcodeproj"
- })?.url.deletingPathExtension().lastPathComponent
- }()
-
- private lazy var unwrappedProjectName: String = {
- return projectName ?? ask("Please enter project name") ?? ""
- }()
-
- private var dependencies: Dependencies {
- Services
- }
-
- private let directoryPath: String
-
- public init(directoryPath: String) {
- self.directoryPath = directoryPath
- }
-
- public func addFontsInProject(_ fontsPath: String, target: String?) throws {
- let fonts = fonts(in: URL(fileURLWithPath: fontsPath))
- let infoPlistURL = URL(fileURLWithPath: "./\(unwrappedProjectName)/Info.plist")
-
- guard fonts.isEmpty == false else {
- throw Error.notFoundFonts(fontsPath)
- }
-
- let targetName = target ?? unwrappedProjectName
-
- guard targetName != "" else {
- throw Error.notFoundTarget(targetName)
- }
-
- guard let fontSpec = generalSpec?.font else {
- throw Error.notFoundTemplate
- }
-
- try createGroupIfNeeded(fontSpec)
-
- guard let infoPlistData = try? Data(contentsOf: infoPlistURL) else {
- throw Error.notFoundInfoPlist(infoPlistURL.path)
- }
-
- let appFonts = try writeFontsInInfoPlistAndFolderFonts(fonts,
- infoPlistData: infoPlistData,
- infoPlistPath: infoPlistURL.path,
- fontSpec: fontSpec,
- target: targetName)
-
- try renderTemplateAndWrite(appFonts: appFonts, fontSpec: fontSpec)
-
- // Same thing
- sleep(1)
-
- try addFileInProject(targetName, filePath: Path(fontSpec.extensionFontPath))
- try write()
- }
-
- private func write() throws {
- do {
- try projectService.write()
- print("✨ \(green("Successfully")) completed added fonts... ✨")
- }
- catch {
- throw Error.somethingGoingWrong("save changes", error.localizedDescription)
- }
- }
-
- private func addFileInProject(_ target: String, filePath: Path) throws {
- do {
- try projectService.addFile(targetName: target, filePath: filePath, sourceTree: .group)
- }
- catch {
- throw Error.somethingGoingWrong("add file in target", error.localizedDescription)
- }
- }
-
- private func renderTemplateAndWrite(appFonts: AEXMLElement, fontSpec: FontSpec) throws {
- guard fileHelper.fileManager.fileExists(atPath: fontSpec.commandTemplatePath) else {
- throw Error.notFoundTemplate
- }
-
- let env = Environment(loader: FileSystemLoader(paths: [
- Path(fontSpec.fontsTemplatePath),
- Path(fontSpec.commonTemplatePath)
- ]))
-
- env.extensions.forEach { ext in
- ext.registerStencilSwiftExtensions()
- ext.registerFilter("fontFunction") { arg in
- guard let name = arg as? String else {
- return arg
- }
- return name.filter { !"-".contains($0) }.capitalizingFirstLetter()
- }
- }
-
- let fontName = appFonts.children.compactMap { font in
- URL(fileURLWithPath: font.value ?? "").deletingPathExtension().lastPathComponent
- }
-
- let company = generalSpec?.xcode.company ?? ask("Please enter license company") ?? ""
- let year = Calendar.current.component(.year, from: .init())
-
- guard let result = try? env.renderTemplate(name: fontSpec.fontTemplateName,
- context: ["fonts": fontName,
- "year": year,
- "company": company]) else {
- throw Error.somethingGoingWrong("generate ", "check font template in ./templates")
- }
-
- do {
- try result.write(toFile: fontSpec.extensionFontPath, atomically: true, encoding: .utf8)
- }
- catch {
- throw Error.somethingGoingWrong("write extension font", error.localizedDescription)
- }
- }
-
- private func writeFontsInInfoPlistAndFolderFonts(_ fonts: [FileInfo],
- infoPlistData: Data,
- infoPlistPath: String,
- fontSpec: FontSpec, target: String) throws -> AEXMLElement {
- guard let xmlDoc = try? AEXMLDocument(xml: infoPlistData) else {
- throw Error.invalidData
- }
-
- let plsit = xmlDoc.root["dict"]
-
- let index = plsit.children.firstIndex { e in
- e.value == fontSpec.infoFontName
- }
-
- let appFonts: AEXMLElement
- if let i = index {
- appFonts = plsit.children[i + 1];
- }
- else {
- plsit.addChild(.init(name: "key", value: fontSpec.infoFontName))
- appFonts = .init(name: "array")
- plsit.addChild(appFonts)
- }
-
- try addFontsInInfoPlistAndFolderFonts(with: fonts,
- fontsFolderPath: fontSpec.fontsFolderPath,
- appFonts: appFonts,
- target: target)
-
- try xmlDoc.xml.write(toFile: infoPlistPath, atomically: true, encoding: .utf8)
- return appFonts
- }
-
- private func addFontsInInfoPlistAndFolderFonts(with fonts: [FileInfo],
- fontsFolderPath: String,
- appFonts: AEXMLElement,
- target: String) throws {
- for newFont in fonts {
- if appFonts.children.contains(where: { $0.value == newFont.url.lastPathComponent }) == false {
- appFonts.addChild(.init(name: "string", value: newFont.url.lastPathComponent))
- let destination = URL(fileURLWithPath: fontsFolderPath + "/" + newFont.url.lastPathComponent)
- if fileHelper.fileManager.fileExists(atPath: destination.path) == false {
- do {
- try fileHelper.fileManager.copyItem(at: newFont.url, to: destination)
- }
- catch {
- throw Error.somethingGoingWrong("copy item", error.localizedDescription)
- }
-
- // Here setup sleep 1 second when copy item in directory after install target for file.
- // if sleep delete when target is not install for file because cannot execute asynchronously.
- sleep(1)
-
- do {
- try projectService.addFile(targetName: target,
- filePath: Path(destination.relativePath),
- sourceTree: .group,
- isResource: true)
- print("🎉 \(green("Added font:")) \(newFont.url.lastPathComponent) ... 🎉")
- }
- catch {
- throw Error.somethingGoingWrong("add target", error.localizedDescription)
- }
- }
- }
- }
- }
-
- private func createGroupIfNeeded(_ fontSpec: FontSpec) throws {
- if fileHelper.fileManager.fileExists(atPath: fontSpec.fontsFolderPath) == false {
- try fileHelper.createDirectory(at: fontSpec.fontsFolderPath)
- }
-
- if fileHelper.fileManager.fileExists(atPath: fontSpec.extensionFolderPath) == false {
- try fileHelper.createDirectory(at: fontSpec.extensionFolderPath)
- }
- }
-
- private func fonts(in folder: URL) -> [FileInfo] {
- guard let filesInfo = try? fileHelper.contentsOfDirectory(at: folder) else {
- return []
- }
- return filesInfo.flatMap { file -> [FileInfo] in
- if file.isDirectory {
- return fonts(in: file.url)
- }
-
- let pathExtenstion = file.url.pathExtension
- switch pathExtenstion {
- case "otf", "ttf":
- return [file]
- default:
- break
- }
- return []
- }
- }
-}
-
-private extension String {
- func capitalizingFirstLetter() -> String {
- return prefix(1).lowercased() + self.dropFirst()
- }
-
- mutating func capitalizeFirstLetter() {
- self = self.capitalizingFirstLetter()
- }
-}
diff --git a/Sources/GeneralIOs/Services/FontService/FontServiceFactory.swift b/Sources/GeneralIOs/Services/FontService/FontServiceFactory.swift
deleted file mode 100644
index af7c4d0..0000000
--- a/Sources/GeneralIOs/Services/FontService/FontServiceFactory.swift
+++ /dev/null
@@ -1,17 +0,0 @@
-//
-// Created by Evgeny Schwarzkopf on 08.04.2022.
-//
-
-import Foundation
-import PathKit
-
-public protocol HasFontServiceFactory {
- var fontServiceFactory: FontServiceFactory { get }
-}
-
-public class FontServiceFactory {
- func makeFontService( directoryPath: String) -> FontService {
- return .init(directoryPath: directoryPath)
- }
-}
-
diff --git a/Sources/GeneralIOs/Services/ProjectService/ProjectService.swift b/Sources/GeneralIOs/Services/ProjectService/ProjectService.swift
index b45c89c..faf97d3 100644
--- a/Sources/GeneralIOs/Services/ProjectService/ProjectService.swift
+++ b/Sources/GeneralIOs/Services/ProjectService/ProjectService.swift
@@ -27,16 +27,17 @@ public final class ProjectService {
try Path.current.children().first { $0.extension == Constants.xcodeProjectPathExtension }
}
+ static func findProject(in path: Path) throws -> Path? {
+ try path.children().first { $0.extension == Constants.xcodeProjectPathExtension }
+ }
+
func createProject(projectName: String) throws {
let xcodeprojPath = path + Path(projectName)
xcodeproj = try XcodeProj(path: xcodeprojPath)
self.xcodeprojPath = xcodeprojPath
}
- func addFile(targetName: String?,
- filePath: Path,
- sourceTree: PBXSourceTree = .sourceRoot,
- isResource: Bool = false) throws {
+ func addFile(targetName: String?, filePath: Path) throws {
guard let project = xcodeproj?.pbxproj.projects.first else {
throw Error.noProject(path: path.string)
}
@@ -44,6 +45,7 @@ public final class ProjectService {
// Folowing code fixes issue with absolute path linkinking.
// Creating of relative urls based on project url becomes useless
// due to XcodeProj internal issues.
+ let fullPath = filePath
var filePath = filePath
if filePath.string.contains(path.string) {
var string = filePath.string.replacingOccurrences(of: path.string, with: "")
@@ -60,12 +62,7 @@ public final class ProjectService {
throw Error.noGroup
}
- let file = try group.addFile(at: filePath, sourceTree: sourceTree, sourceRoot: path)
-
- if isResource {
- let _ = try xcodeproj?.pbxproj.resourcesBuildPhases.first?.add(file: file)
- }
-
+ let file = try group.addFile(at: fullPath, sourceTree: .sourceRoot, sourceRoot: path)
xcodeproj?.pbxproj.add(object: file)
let targets = xcodeproj?.pbxproj.nativeTargets
let target: PBXNativeTarget?
diff --git a/Sources/GeneralIOs/Services/ServiceFactory+GeneralIOs.swift b/Sources/GeneralIOs/Services/ServiceFactory+GeneralIOs.swift
index ed35669..7976479 100644
--- a/Sources/GeneralIOs/Services/ServiceFactory+GeneralIOs.swift
+++ b/Sources/GeneralIOs/Services/ServiceFactory+GeneralIOs.swift
@@ -4,12 +4,12 @@
import GeneralKit
-extension ServiceFactory: HasProjectServiceFactory, HasFontServiceFactory {
+extension ServiceFactory: HasProjectServiceFactory, HasBootstrapService {
public var projectServiceFactory: ProjectServiceFactory {
.init()
}
- public var fontServiceFactory: FontServiceFactory {
- .init()
+ public var bootstrapService: BootstrapService {
+ BootstrapServiceImpl(dependencies: self)
}
}
diff --git a/Sources/GeneralKit/Models/CustomLexer.swift b/Sources/GeneralKit/Models/CustomLexer.swift
new file mode 100644
index 0000000..6aa1444
--- /dev/null
+++ b/Sources/GeneralKit/Models/CustomLexer.swift
@@ -0,0 +1,366 @@
+//
+// CustomLexer.swift
+// GeneralKit
+//
+// Created by Nick Tyunin on 27.12.2021.
+//
+
+import Foundation
+import Stencil
+
+typealias Line = (content: String, number: UInt, range: Range)
+
+public struct CustomSourceMap: Equatable {
+ public let filename: String?
+ public let location: ContentLocation
+
+ init(filename: String? = nil, location: ContentLocation = ("", 0, 0)) {
+ self.filename = filename
+ self.location = location
+ }
+
+ static let unknown = CustomSourceMap()
+
+ public static func == (lhs: CustomSourceMap, rhs: CustomSourceMap) -> Bool {
+ return lhs.filename == rhs.filename && lhs.location == rhs.location
+ }
+}
+
+public class CustomToken: Equatable {
+ public enum Kind: Equatable {
+ /// A token representing a piece of text.
+ case text
+ /// A token representing a variable.
+ case variable
+ /// A token representing a comment.
+ case comment
+ /// A token representing a template block.
+ case block
+ }
+
+ public let contents: String
+ public let kind: Kind
+ public let sourceMap: CustomSourceMap
+
+ /// Returns the underlying value as an array seperated by spaces
+ public private(set) lazy var components: [String] = self.contents.smartSplit()
+
+ init(contents: String, kind: Kind, sourceMap: CustomSourceMap) {
+ self.contents = contents
+ self.kind = kind
+ self.sourceMap = sourceMap
+ }
+
+ /// A token representing a piece of text.
+ public static func text(value: String, at sourceMap: CustomSourceMap) -> CustomToken {
+ return CustomToken(contents: value, kind: .text, sourceMap: sourceMap)
+ }
+
+ /// A token representing a variable.
+ public static func variable(value: String, at sourceMap: CustomSourceMap) -> CustomToken {
+ return CustomToken(contents: value, kind: .variable, sourceMap: sourceMap)
+ }
+
+ /// A token representing a comment.
+ public static func comment(value: String, at sourceMap: CustomSourceMap) -> CustomToken {
+ return CustomToken(contents: value, kind: .comment, sourceMap: sourceMap)
+ }
+
+ /// A token representing a template block.
+ public static func block(value: String, at sourceMap: CustomSourceMap) -> CustomToken {
+ return CustomToken(contents: value, kind: .block, sourceMap: sourceMap)
+ }
+
+ public static func == (lhs: CustomToken, rhs: CustomToken) -> Bool {
+ return lhs.contents == rhs.contents && lhs.kind == rhs.kind && lhs.sourceMap == rhs.sourceMap
+ }
+}
+
+struct CustomLexer {
+ let templateName: String?
+ let templateString: String
+ let lines: [Line]
+
+ /// The potential token start characters. In a template these appear after a
+ /// `{` character, for example `{{`, `{%`, `{#`, ...
+ private static let tokenChars: [Unicode.Scalar] = ["{", "%", "#"]
+
+ /// The token end characters, corresponding to their token start characters.
+ /// For example, a variable token starts with `{{` and ends with `}}`
+ private static let tokenCharMap: [Unicode.Scalar: Unicode.Scalar] = [
+ "{": "}",
+ "%": "%",
+ "#": "#"
+ ]
+
+ init(templateName: String? = nil, templateString: String) {
+ self.templateName = templateName
+ self.templateString = templateString
+
+ self.lines = templateString.components(separatedBy: .newlines).enumerated().compactMap {
+ guard !$0.element.isEmpty,
+ let range = templateString.range(of: $0.element) else { return nil }
+ return (content: $0.element, number: UInt($0.offset + 1), range)
+ }
+ }
+
+ /// Create a token that will be passed on to the parser, with the given
+ /// content and a range. The content will be tested to see if it's a
+ /// `variable`, a `block` or a `comment`, otherwise it'll default to a simple
+ /// `text` token.
+ ///
+ /// - Parameters:
+ /// - string: The content string of the token
+ /// - range: The range within the template content, used for smart
+ /// error reporting
+ func createToken(string: String, at range: Range) -> CustomToken {
+ func strip() -> String {
+ guard string.count > 4 else { return "" }
+ let trimmed = String(string.dropFirst(2).dropLast(2))
+ .components(separatedBy: "\n")
+ .filter { !$0.isEmpty }
+ .map { $0.trim(character: " ") }
+ .joined(separator: " ")
+ return trimmed
+ }
+
+ if string.hasPrefix("{{") || string.hasPrefix("{%") || string.hasPrefix("{#") {
+ let value = strip()
+ let range = templateString.range(of: value, range: range) ?? range
+ let location = rangeLocation(range)
+ let sourceMap = CustomSourceMap(filename: templateName, location: location)
+ if string.hasPrefix("{{") {
+ return .variable(value: value, at: sourceMap)
+ } else if string.hasPrefix("{%") {
+ return .block(value: value, at: sourceMap)
+ } else if string.hasPrefix("{#") {
+ return .comment(value: value, at: sourceMap)
+ }
+ }
+
+ let location = rangeLocation(range)
+ let sourceMap = CustomSourceMap(filename: templateName, location: location)
+ return .text(value: string, at: sourceMap)
+ }
+
+ /// Transforms the template into a list of tokens, that will eventually be
+ /// passed on to the parser.
+ ///
+ /// - Returns: The list of tokens (see `createToken(string: at:)`).
+ func tokenize() -> [CustomToken] {
+ var tokens: [CustomToken] = []
+
+ let scanner = Scanner(templateString)
+ while !scanner.isEmpty {
+ if let (char, text) = scanner.scanForTokenStart(CustomLexer.tokenChars) {
+ if !text.isEmpty {
+ tokens.append(createToken(string: text, at: scanner.range))
+ }
+
+ guard let end = CustomLexer.tokenCharMap[char] else { continue }
+ let result = scanner.scanForTokenEnd(end)
+ tokens.append(createToken(string: result, at: scanner.range))
+ } else {
+ tokens.append(createToken(string: scanner.content, at: scanner.range))
+ scanner.content = ""
+ }
+ }
+
+ return tokens
+ }
+
+ /// Finds the line matching the given range (for a token)
+ ///
+ /// - Parameter range: The range to search for.
+ /// - Returns: The content for that line, the line number and offset within
+ /// the line.
+ func rangeLocation(_ range: Range) -> ContentLocation {
+ guard let line = self.lines.first(where: { $0.range.contains(range.lowerBound) }) else {
+ return ("", 0, 0)
+ }
+ let offset = templateString.distance(from: line.range.lowerBound, to: range.lowerBound)
+ return (line.content, line.number, offset)
+ }
+}
+
+class Scanner {
+ let originalContent: String
+ var content: String
+ var range: Range
+
+ /// The start delimiter for a token.
+ private static let tokenStartDelimiter: Unicode.Scalar = "{"
+ /// And the corresponding end delimiter for a token.
+ private static let tokenEndDelimiter: Unicode.Scalar = "}"
+
+ init(_ content: String) {
+ self.originalContent = content
+ self.content = content
+ range = content.unicodeScalars.startIndex.. String {
+ var foundChar = false
+
+ for (index, char) in content.unicodeScalars.enumerated() {
+ if foundChar && char == Scanner.tokenEndDelimiter {
+ let result = String(content.unicodeScalars.prefix(index + 1))
+ content = String(content.unicodeScalars.dropFirst(index + 1))
+ range = range.upperBound.. (Unicode.Scalar, String)? {
+ var foundBrace = false
+
+ range = range.upperBound.. String.Index? {
+ var index = startIndex
+
+ while index != endIndex {
+ if character != self[index] {
+ return index
+ }
+ index = self.index(after: index)
+ }
+
+ return nil
+ }
+
+ func findLastNot(character: Character) -> String.Index? {
+ var index = self.index(before: endIndex)
+
+ while index != startIndex {
+ if character != self[index] {
+ return self.index(after: index)
+ }
+ index = self.index(before: index)
+ }
+
+ return nil
+ }
+
+ func trim(character: Character) -> String {
+ let first = findFirstNot(character: character) ?? startIndex
+ let last = findLastNot(character: character) ?? endIndex
+ return String(self[first.. [String] {
+ var word = ""
+ var components: [String] = []
+ var separate: Character = separator
+ var singleQuoteCount = 0
+ var doubleQuoteCount = 0
+
+ for character in self {
+ if character == "'" {
+ singleQuoteCount += 1
+ } else if character == "\"" {
+ doubleQuoteCount += 1
+ }
+
+ if character == separate {
+ if separate != separator {
+ word.append(separate)
+ } else if (singleQuoteCount % 2 == 0 || doubleQuoteCount % 2 == 0) && !word.isEmpty {
+ appendWord(word, to: &components)
+ word = ""
+ }
+
+ separate = separator
+ } else {
+ if separate == separator && (character == "'" || character == "\"") {
+ separate = character
+ }
+ word.append(character)
+ }
+ }
+
+ if !word.isEmpty {
+ appendWord(word, to: &components)
+ }
+
+ return components
+ }
+
+ private func appendWord(_ word: String, to components: inout [String]) {
+ let specialCharacters = ",|:"
+
+ if !components.isEmpty {
+ if let precedingChar = components.last?.last, specialCharacters.contains(precedingChar) {
+ components[components.count - 1] += word
+ } else if specialCharacters.contains(word) {
+ components[components.count - 1] += word
+ } else if word != "(" && word.first == "(" || word != ")" && word.first == ")" {
+ components.append(String(word.prefix(1)))
+ appendWord(String(word.dropFirst()), to: &components)
+ } else if word != "(" && word.last == "(" || word != ")" && word.last == ")" {
+ appendWord(String(word.dropLast()), to: &components)
+ components.append(String(word.suffix(1)))
+ } else {
+ components.append(word)
+ }
+ } else {
+ components.append(word)
+ }
+ }
+}
diff --git a/Sources/GeneralKit/Models/VariablesTemplate.swift b/Sources/GeneralKit/Models/VariablesTemplate.swift
new file mode 100644
index 0000000..096bbb7
--- /dev/null
+++ b/Sources/GeneralKit/Models/VariablesTemplate.swift
@@ -0,0 +1,114 @@
+//
+// VariablesTemplate.swift
+// GeneralIOs
+//
+// Created by Nick Tyunin on 11.11.2021.
+//
+
+import Stencil
+import StencilSwiftKit
+
+public class VariablesTemplate: Template {
+
+ public var variables: [String]
+ public required init(templateString: String, environment: Environment? = nil, name: String? = nil) {
+ var variables: [String] = []
+ var dynamicVariables: [String] = []
+ let lexer = CustomLexer(templateString: templateString)
+ let tokens = lexer.tokenize()
+ var ifDepth = 0
+
+ func add(dynamic: String) {
+ guard dynamicVariables.contains(dynamic) == false else {
+ return
+ }
+ dynamicVariables.append(dynamic)
+ }
+
+ func add(variable: String) {
+ guard ifDepth == 0 else {
+ return
+ }
+ var variable = variable
+ if let unfiltered = variable.split(separator: "|").first {
+ variable = String(unfiltered).trimmingCharacters(in: .whitespaces)
+ }
+ guard variables.contains(variable) == false else {
+ return
+ }
+ let components = variable.split(separator: ".")
+ for i in 0..= 4 else {
+ return
+ }
+ add(dynamic: "forloop")
+ add(dynamic: components[1])
+ add(variable: components[3])
+ case "only":
+ guard components.count >= 5 else {
+ return
+ }
+ add(dynamic: components[2])
+ add(variable: components[4])
+ case "using":
+ guard components.count >= 4 else {
+ return
+ }
+ add(dynamic: components[3])
+ case "if":
+ ifDepth += 1
+ case "endif":
+ ifDepth -= 1
+ case "first":
+ add(dynamic: components[1])
+ add(variable: components[3])
+ default:
+ return
+ }
+ default:
+ return
+ }
+ }
+ self.variables = Array(Set(variables))
+ super.init(templateString: templateString, environment: environment, name: name)
+ }
+
+ public override func render(_ dictionary: [String : Any]? = nil) throws -> String {
+ var context = [String : Any]()
+ Export.global.forEach { key, value in
+ context[key] = value
+ }
+ dictionary?.forEach{ key, value in
+ context[key] = value
+ }
+ return try super.render(context)
+ }
+}
diff --git a/Sources/GeneralKit/Services/Shell/ShellImpl.swift b/Sources/GeneralKit/Services/Shell/ShellImpl.swift
index fc4f4de..c0ef22c 100644
--- a/Sources/GeneralKit/Services/Shell/ShellImpl.swift
+++ b/Sources/GeneralKit/Services/Shell/ShellImpl.swift
@@ -43,6 +43,7 @@ public final class ShellImpl: Shell {
} else {
process.launch()
}
+ process.waitUntilExit()
let statusCode = process.terminationStatus
if statusCode == 0 {
return statusCode