Skip to content

Instantly share code, notes, and snippets.

@Jeehut
Last active April 18, 2022 13:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Jeehut/ecc47d40f9c68d3823960bd71a021c7b to your computer and use it in GitHub Desktop.
Save Jeehut/ecc47d40f9c68d3823960bd71a021c7b to your computer and use it in GitHub Desktop.
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)
#!/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