pointfreeco / swift-composable-architecture

A library for building applications in a consistent and understandable way, with composition, testing, and ergonomics in mind.
https://www.pointfree.co/collections/composable-architecture
MIT License
12.08k stars 1.41k forks source link

Add command line tool to speedup some common actions #326

Closed Jeehut closed 3 years ago

Jeehut commented 3 years ago

I'm just getting started with TCA and I already have the problem that each time I create a new composable, there's lots of boilerplate code to write, files to create etc. – I've actually created a swift-sh script for me to simplify at least the creation of new composables, see here:

generate.swift ```swift #!/usr/local/bin/swift-sh import Foundation import Files // @JohnSundell ~> 4.2 import ShellOut // @JohnSundell ~> 2.3 import HandySwift // @Flinesoft == 246400b6ef9768a7bc9f4e58f9102664e46d4aea // MARK: - Input Handling let usageInstructons: String = """ Run like this: ./generate.swift Replace with one of: composable Replace with the name to use for your generated file(s). For example: ./generate.swift composable 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 composable } 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 viewFileContents(name: String) -> String { """ import ComposableArchitecture import SwiftUI struct \(name)View: View { let store: Store<\(name)State, \(name)Action> var body: some View { WithViewStore(store) { viewStore in Button("Example") { viewStore.send(.exampleButtonClicked) } } .padding() } } struct \(name)View_Previews: PreviewProvider { static var previews: some View { \(name)View( store: Store( initialState: \(name)State(), reducer: \(name.firstLowercased)Reducer, environment: \(name)Environment() ) ) } } """ } func stateFileContents(name: String) -> String { """ import Foundation struct \(name)State: Equatable { #warning("TODO: not yet implemented") } """ } func actionFileContents(name: String) -> String { """ import Foundation enum \(name)Action { #warning("TODO: not yet implemented") case exampleButtonClicked } """ } func environmentFileContents(name: String) -> String { """ import Foundation struct \(name)Environment { #warning("TODO: not yet implemented") } """ } func reducerFileContents(name: String) -> String { """ import ComposableArchitecture import Foundation let \(name.firstLowercased)Reducer = Reducer<\(name)State, \(name)Action, \(name)Environment>() { state, action, environment in #warning("TODO: not yet implemented") switch action { case .exampleButtonClicked: print("example button was clicked") return .none } } """ } // MARK: - Generating Code Files switch kind { case .composable: let sourcesFolder = try Folder(path: "Shared/Sources/Composables") guard !sourcesFolder.containsSubfolder(named: name) else { print("ERROR: There's already a folder named '\(name)' in Shared/Sources, please delete it first and retry.") exit(EXIT_FAILURE) } let folder = try sourcesFolder.createSubfolder(at: name) let viewFile = try folder.createFile(named: "\(name)View.swift") try viewFile.write(viewFileContents(name: name)) let stateFile = try folder.createFile(named: "\(name)State.swift") try stateFile.write(stateFileContents(name: name)) let actionFile = try folder.createFile(named: "\(name)Action.swift") try actionFile.write(actionFileContents(name: name)) let environmentFile = try folder.createFile(named: "\(name)Environment.swift") try environmentFile.write(environmentFileContents(name: name)) let reducerFile = try folder.createFile(named: "\(name)Reducer.swift") try reducerFile.write(reducerFileContents(name: name)) print("Successfully generated files. Drag & drop folder to Xcode to finish.") try shellOut(to: "open", arguments: [folder.path, "--reveal"]) } ```

With the above, I can run ./generate.swift composable Dashboard and it will create the following files:

DashboardView.swift ```swift import ComposableArchitecture import SwiftUI struct DashboardView: View { let store: Store var body: some View { WithViewStore(store) { viewStore in Button("Example") { viewStore.send(.exampleButtonClicked) } } .padding() } } struct DashboardView_Previews: PreviewProvider { static var previews: some View { DashboardView( store: Store( initialState: DashboardState(), reducer: dashboardReducer, environment: DashboardEnvironment() ) ) } } ```
DashboardState.swift ```swift import Foundation struct DashboardState: Equatable { #warning("TODO: not yet implemented") } ```
DashboardEnvironment.swift ```swift import Foundation struct DashboardEnvironment { #warning("TODO: not yet implemented") } ```
DashboardReducer.swift ```swift import ComposableArchitecture import Foundation let dashboardReducer = Reducer() { state, action, environment in #warning("TODO: not yet implemented") switch action { case .exampleButtonClicked: print("example button was clicked") return .none } } ```
DashboardAction.swift ```swift import Foundation enum DashboardAction { #warning("TODO: not yet implemented") case exampleButtonClicked } ```

While this works for me for now, I think 1. it should be part of some official TCA command line tool and 2. there will probably be some more common unnecessary manual work places where a tool could help out pretty nicely.

So, what do you think about adding an official command line tool to TCA?

gkaimakas commented 3 years ago

I have created an Xcode template for this kind workflow.

It’s pretty easy!

On Sat, 21 Nov 2020 at 13:08, Cihat Gündüz notifications@github.com wrote:

I'm just getting started with TCA and I already have the problem that each time I create a new composable, there's lots of boilerplate code to write, files to create etc. – I've actually created a swift-sh https://github.com/mxcl/swift-sh script for me to simplify at least the creation of new composables, see here: generate.swift

!/usr/local/bin/swift-sh

import Foundation import Files // @JohnSundell ~> 4.2 import ShellOut // @JohnSundell ~> 2.3 import HandySwift // @Flinesoft == 246400b6ef9768a7bc9f4e58f9102664e46d4aea

// MARK: - Input Handling

let usageInstructons: String = """

Run like this: ./generate.swift Replace with one of: composable Replace with the name to use for your generated file(s).

For example: ./generate.swift composable 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 composable

}

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 viewFileContents(name: String) -> String {

""" import ComposableArchitecture import SwiftUI

struct (name)View: View { let store: Store<(name)State, (name)Action>

var body: some View {
  WithViewStore(store) { viewStore in
    Button("Example") {
      viewStore.send(.exampleButtonClicked)
    }
  }
  .padding()
}

}

struct (name)View_Previews: PreviewProvider { static var previews: some View { (name)View( store: Store( initialState: (name)State(), reducer: (name.firstLowercased)Reducer, environment: (name)Environment() ) ) } }

"""

}

func stateFileContents(name: String) -> String {

""" import Foundation

struct (name)State: Equatable {

warning("TODO: not yet implemented")

}

"""

}

func actionFileContents(name: String) -> String {

""" import Foundation

enum (name)Action {

warning("TODO: not yet implemented")

case exampleButtonClicked

}

"""

}

func environmentFileContents(name: String) -> String {

""" import Foundation

struct (name)Environment {

warning("TODO: not yet implemented")

}

"""

}

func reducerFileContents(name: String) -> String {

""" import ComposableArchitecture import Foundation

let (name.firstLowercased)Reducer = Reducer<(name)State, (name)Action, (name)Environment>() { state, action, environment in

warning("TODO: not yet implemented")

switch action {
  case .exampleButtonClicked:
    print("example button was clicked")
    return .none
}

}

"""

}

// MARK: - Generating Code Files

switch kind { case .composable:

let sourcesFolder = try Folder(path: "Shared/Sources/Composables")

guard !sourcesFolder.containsSubfolder(named: name) else {

print("ERROR: There's already a folder named '\(name)' in Shared/Sources, please delete it first and retry.")

exit(EXIT_FAILURE)

}

let folder = try sourcesFolder.createSubfolder(at: name)

let viewFile = try folder.createFile(named: "(name)View.swift")

try viewFile.write(viewFileContents(name: name))

let stateFile = try folder.createFile(named: "(name)State.swift")

try stateFile.write(stateFileContents(name: name))

let actionFile = try folder.createFile(named: "(name)Action.swift")

try actionFile.write(actionFileContents(name: name))

let environmentFile = try folder.createFile(named: "(name)Environment.swift")

try environmentFile.write(environmentFileContents(name: name))

let reducerFile = try folder.createFile(named: "(name)Reducer.swift")

try reducerFile.write(reducerFileContents(name: name))

print("Successfully generated files. Drag & drop folder to Xcode to finish.")

try shellOut(to: "open", arguments: [folder.path, "--reveal"])

}

With the above, I can run ./generate.swift composable Dashboard and it will create the following files: DashboardView.swift

import ComposableArchitecture import SwiftUI

struct DashboardView: View {

let store: Store<DashboardState, DashboardAction>

var body: some View {

WithViewStore(store) { viewStore in

  Button("Example") {

    viewStore.send(.exampleButtonClicked)

  }

}

.padding()

}

}

struct DashboardView_Previews: PreviewProvider {

static var previews: some View {

DashboardView(

  store: Store(

    initialState: DashboardState(),

    reducer: dashboardReducer,

    environment: DashboardEnvironment()

  )

)

}

}

DashboardState.swift

import Foundation

struct DashboardState: Equatable {

warning("TODO: not yet implemented")

}

DashboardEnvironment.swift

import Foundation

struct DashboardEnvironment {

warning("TODO: not yet implemented")

}

DashboardReducer.swift

import ComposableArchitecture import Foundation

let dashboardReducer = Reducer<DashboardState, DashboardAction, DashboardEnvironment>() { state, action, environment in

warning("TODO: not yet implemented")

switch action {

case .exampleButtonClicked:

  print("example button was clicked")

  return .none

}

}

DashboardAction.swift

import Foundation

enum DashboardAction {

warning("TODO: not yet implemented")

case exampleButtonClicked

}

While this works for me for now, I think 1. it should be part of some official TCA command line tool and 2. there will probably be some more common unnecessary manual work places where a tool could help out pretty nicely.

So, what do you think about adding an official command line tool to TCA?

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/pointfreeco/swift-composable-architecture/issues/326, or unsubscribe https://github.com/notifications/unsubscribe-auth/AANVHGKF7I5X5UV3NKMLDE3SQ6NS3ANCNFSM4T5W7U5A .

-- Γιώργος

Jeehut commented 3 years ago

@gkaimakas Thanks for pointing out Xcode templates, but I have a few questions related to them:

  1. Are "Xcode templates" even an official feature Apples supports? I can't find any official documentation from Apple ...
  2. Is there anywhere a collection of Xcode templates where we could add TCA-specific ones for the community?
  3. How easy is it to roll out the "Xcode templates" approach to a team? How can everyone set them up easily?
  4. Do Xcode templates flexible enough for us to cover most common actions we need to do in TCA?
gkaimakas commented 3 years ago

I believe they are supported by Apple since it’s the same think Xcode uses to generate files e.g when you create a new UIKit class.

I haven’t found any collection of templates online but there are a lot of tutorials on how to create one yourself.

Rolling out a template to a team is super easy, barely an inconvenience I’d say. What I have done is added them in git and each new member that wants to use them can run a simple script to “install” them (or a build phase to do automatically)

Flexibility is a subjective. The template allows me to generate the scaffolding for a TCA feature fast, like different files for the environment, reducer, state, action and the view / view controller etc. The rest is up to me to do.

I will try and share an example with you later today.

On Sat, 21 Nov 2020 at 13:20, Cihat Gündüz notifications@github.com wrote:

@gkaimakas https://github.com/gkaimakas Thanks for pointing out Xcode templates, but I have a few questions related to them:

  1. Are "Xcode templates" even an official feature Apples supports? I can't find any official documentation from Apple ...
  2. Is there anywhere a collection of Xcode templates where we could add TCA-specific ones for the community?
  3. How easy is it to roll out the "Xcode templates" approach to a team? How can everyone set them up easily?
  4. Do Xcode templates flexible enough for us to cover most common actions we need to do in TCA?

— You are receiving this because you were mentioned.

Reply to this email directly, view it on GitHub https://github.com/pointfreeco/swift-composable-architecture/issues/326#issuecomment-731565211, or unsubscribe https://github.com/notifications/unsubscribe-auth/AANVHGI664TGKPYVGST6UVDSQ6PAJANCNFSM4T5W7U5A .

-- Γιώργος

Jeehut commented 3 years ago

@gkaimakas That'd be great, thank you!

mbrandonw commented 3 years ago

Hi @Jeehut, thanks for bringing this topic up!

We are reluctant to have any official support for this in TCA primarily because we do not want to recommend a particular style for people to structure their features. For example, we personally do not separate state, action, environment and reducers into separate files. More often than not we even have the view in the same file too. We prefer to have everything in one spot so that we do not have to jump around to a bunch of files.

On the other hand we know others may like to put all of those pieces into separate files, and that's great if they prefer to have everything separated into shorter files.

I think your tool could be really great for the community, and if you open sourced it it could be added to the directory of TCA related projects: https://github.com/antranapp/awesome-tca. We are also in the planning stages of building a "TCA community" GitHub organization where such projects could also be hosted.

mbrandonw commented 3 years ago

I'm going to close this out for now. Thanks for the discussion!