Make sure to run `chmod +x generate.swift` to make this file executable & install https://github.com/mxcl/swift-sh (on Intel Macs, fix path in 1st line)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/opt/homebrew/bin/swift-sh | |
import Files // @JohnSundell ~> 4.2 | |
import Foundation | |
import HandySwift // @FlineDev ~> 3.4 | |
import ShellOut // @JohnSundell ~> 2.3 | |
// MARK: - Input Handling | |
let usageInstructons: String = """ | |
Run like this: ./generate.swift <kind> <name> | |
Replace <kind> with one of: package | |
Replace <name> with the name to use for your generated file(s). | |
For example: ./generate.swift package Login | |
""" | |
guard CommandLine.arguments.count == 3 else { | |
print("ERROR: Wrong number of arguments. Expected 2, got \(CommandLine.arguments.count - 1).") | |
print(usageInstructons) | |
exit(EXIT_FAILURE) | |
} | |
enum Kind: String, CaseIterable { | |
case package | |
} | |
guard let kind = Kind(rawValue: CommandLine.arguments[1]) else { | |
print("ERROR: Unknown kind '\(CommandLine.arguments[1])'. Use one of: \(Kind.allCases)") | |
print(usageInstructons) | |
exit(EXIT_FAILURE) | |
} | |
let name = CommandLine.arguments[2] | |
// MARK: - Defining File Contents | |
func actionFileContents(name: String) -> String { | |
""" | |
import Foundation | |
import ComposableArchitecture | |
import HelpfulError | |
public enum \(name)Action: Equatable, BindableAction { | |
#warning("add Action cases here") | |
case binding(BindingAction<\(name)State>) | |
case errorOccurred(error: \(name)Error) | |
case setHelpfulError(isPresented: Bool) | |
case helpfulError(action: HelpfulErrorAction) | |
} | |
""" | |
} | |
func actionHandlerFileContents(name: String) -> String { | |
""" | |
import Foundation | |
import ComposableArchitecture | |
import Utility | |
struct \(name)ActionHandler { | |
typealias State = \(name)State | |
typealias Action = \(name)Action | |
typealias Next = Effect<Action, Never> | |
let env: AppEnv | |
#warning("add Action handlers here") | |
} | |
// MARK: - HelpfulError Handler | |
extension \(name)ActionHandler { | |
func handleHelpfulErrorAction(state: inout State, action: \(name)Action) -> Next { | |
switch action { | |
case .errorOccurred(let error): | |
return self.errorOccurred(state: &state, error: error) | |
case .setHelpfulError(let isPresented): | |
return self.setHelpfulError(state: &state, isPresented: isPresented) | |
case .helpfulError(.closeButtonPressed): | |
return .init(value: .setHelpfulError(isPresented: false)) | |
case .helpfulError: | |
return .none // handled by child reducer | |
default: | |
assertionFailure("The method 'handleHelpfulErrorAction' should only be called for 'HelpfulError' related actions.") | |
return .none | |
} | |
} | |
func errorOccurred(state: inout State, error: \(name)Error) -> Next { | |
state.helpfulErrorState = .init(helpfulError: error) | |
return .none | |
} | |
func setHelpfulError(state: inout State, isPresented: Bool) -> Next { | |
guard !isPresented else { | |
assertionFailure("SwiftUI should never trigger the presentation of a views, only dismissals.") | |
return .none | |
} | |
state.helpfulErrorState = nil | |
return .none | |
} | |
} | |
""" | |
} | |
func errorFileContents(name: String) -> String { | |
""" | |
import Foundation | |
import HelpfulError | |
public enum \(name)Error: Error { | |
// MARK: - Leaf | |
// None | |
// MARK: - Nested | |
// None | |
// MARK: - Unexpected | |
case unexpectedError(errorDescription: String) | |
} | |
extension \(name)Error: HelpfulError { | |
public static let typeId: HelpfulErrorTypeId = .\(name.firstLowercased)Error | |
public var errorDescription: String { | |
switch self { | |
case .unexpectedError(let errorDescription): | |
return Self.unexpectedErrorDescription(errorDescription: errorDescription) | |
} | |
} | |
public var id: String { | |
switch self { | |
case .unexpectedError: | |
return Self.unexpectedErrorId | |
} | |
} | |
} | |
""" | |
} | |
func reducerFileContents(name: String) -> String { | |
""" | |
import Foundation | |
import ComposableArchitecture | |
import HelpfulError | |
import Utility | |
public let \(name.firstLowercased)Reducer = Reducer.combine( | |
helpfulErrorReducer | |
.optional() | |
.pullback( | |
state: \\\(name)State.helpfulErrorState, | |
action: /\(name)Action.helpfulError(action:), | |
environment: { _ in .init() } | |
), | |
Reducer<\(name)State, \(name)Action, AppEnv> { state, action, env in | |
let actionHandler = \(name)ActionHandler(env: env) | |
#warning("redirect Action cases here to `actionHandler`") | |
switch action { | |
case .binding: | |
return .none // assignment handled by `.binding()` below | |
case .errorOccurred, .setHelpfulError, .helpfulError: | |
return actionHandler.handleHelpfulErrorAction(state: &state, action: action) | |
} | |
} | |
.binding() | |
) | |
""" | |
} | |
func stateFileContents(name: String) -> String { | |
""" | |
import Foundation | |
import ComposableArchitecture | |
import HandySwift | |
import HelpfulError | |
public struct \(name)State: Equatable { | |
#warning("add State properties here") | |
var helpfulErrorState: HelpfulErrorState? | |
public init() {} | |
} | |
#if DEBUG | |
extension \(name)State: Withable {} | |
#endif | |
""" | |
} | |
func viewFileContents(name: String) -> String { | |
""" | |
import SwiftUI | |
import ComposableArchitecture | |
import HelpfulError | |
import SFSafeSymbols | |
import Utility | |
public struct \(name)View: View { | |
public var body: some View { | |
WithViewStore(self.store) { viewStore in | |
#warning("implement proper UI here") | |
VStack { | |
Spacer() | |
HStack { | |
Spacer() | |
Text("\(name)") | |
Spacer() | |
} | |
Spacer() | |
} | |
.padding() | |
.sheet( | |
isPresented: viewStore.binding( | |
valueFromState: { $0.helpfulErrorState != nil }, | |
actionFromValue: \(name)Action.setHelpfulError(isPresented:) | |
) | |
) { | |
IfLetStore( | |
self.store.scope(state: \\.helpfulErrorState, action: \(name)Action.helpfulError(action:)), | |
then: HelpfulErrorView.init(store:) | |
) | |
} | |
} | |
} | |
let store: Store<\(name)State, \(name)Action> | |
public init(store: Store<\(name)State, \(name)Action>) { | |
self.store = store | |
} | |
} | |
#if DEBUG | |
struct \(name)View_Previews: PreviewProvider { | |
static let store = Store( | |
initialState: .init(), | |
reducer: \(name.firstLowercased)Reducer, | |
environment: .mocked | |
) | |
static var previews: some View { | |
\(name)View(store: self.store).previewVariants() | |
} | |
} | |
#endif | |
""" | |
} | |
// MARK: - Generating Code Files | |
switch kind { | |
case .package: | |
let sourcesFolder = try Folder(path: "Sources") | |
guard !sourcesFolder.containsSubfolder(named: name) else { | |
print("ERROR: There's already a folder named '\(name)' in Sources, please delete it first and retry.") | |
exit(EXIT_FAILURE) | |
} | |
let folder = try sourcesFolder.createSubfolder(at: name) | |
let actionFile = try folder.createFile(named: "\(name)Action.swift") | |
try actionFile.write(actionFileContents(name: name)) | |
let actionHandlerFile = try folder.createFile(named: "\(name)ActionHandler.swift") | |
try actionHandlerFile.write(actionHandlerFileContents(name: name)) | |
let errorFile = try folder.createFile(named: "\(name)Error.swift") | |
try errorFile.write(errorFileContents(name: name)) | |
let reducerFile = try folder.createFile(named: "\(name)Reducer.swift") | |
try reducerFile.write(reducerFileContents(name: name)) | |
let stateFile = try folder.createFile(named: "\(name)State.swift") | |
try stateFile.write(stateFileContents(name: name)) | |
let viewFile = try folder.createFile(named: "\(name)View.swift") | |
try viewFile.write(viewFileContents(name: name)) | |
print( | |
""" | |
Successfully generated files. To make all work, copy the following snippets to their respective places: | |
======================================================== | |
===== Package.swift: Insert into `products:` array ===== | |
======================================================== | |
.library(name: "\(name)", targets: ["\(name)"]), | |
======================================================== | |
===== Package.swift: Insert into `targets:` array ====== | |
======================================================== | |
.target( | |
name: "\(name)", | |
dependencies: [ | |
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"), | |
.product(name: "HandySwift", package: "HandySwift"), | |
"HelpfulError", | |
.product(name: "SFSafeSymbols", package: "SFSafeSymbols"), | |
"Utility", | |
] | |
), | |
======================================================== | |
===== HelpfulErrorTypeId.swift: Add new static var ===== | |
======================================================== | |
public static var \(name.firstLowercased)Error: Self { .init(id: <#T##String#>) } | |
======================================================== | |
""" | |
) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment