Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Creating basic REST resource file(s) #406

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions Sources/VaporToolbox/Resource/Resource.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import ConsoleKit
import Foundation

struct Resource: AnyCommand {
struct Signature: CommandSignature {
@Argument(name: "name", help: "Name of resource.")
var name: String

@Flag(name: "force", help: "Overwrite existing resources.")
var force: Bool
}

let help = """
Generates a new resources.

This command will generate a new resource with the given name.
The resource will be created in the current directory.

example input:
vapor resource User

example output:
./Sources/App/Models/User.swift
./Sources/App/Controllers/UserController.swift
./Sources/App/Migrations/CreateUser.swift
"""

func outputHelp(using context: inout CommandContext) {
Signature().outputHelp(help: self.help, using: &context)
}

func run(using context: inout CommandContext) throws {
let signature = try Signature(from: &context.input)
let name =
signature.name.isEmpty
? "Model"
: signature.name
.prefix(1)
.uppercased()
+ signature.name
.dropFirst()
let force = signature.force

let cwd = FileManager.default.currentDirectoryPath
let package = cwd.appendingPathComponents("Package.swift")

// Checking if the project is a valid Swift Package
guard FileManager.default.fileExists(atPath: package) else {
throw ResourceError.notValidProject.localizedDescription
}

let scaffolder = ResourceScaffolder(console: context.console, modelName: name)

// Generating the resource structures from given model name
scaffolder.generate { model, migration, controller in

let creator = ResourceCreator(
console: context.console, modelName: name, modelFile: model, modelMigrationFile: migration,
modelControllerFile: controller, force: force)

// Executing the resource creation
creator.execute()
}

}
}
105 changes: 105 additions & 0 deletions Sources/VaporToolbox/Resource/ResourceCreator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import ConsoleKit
import Foundation

struct ResourceCreator {
let console: Console
let modelName: String
let modelFile: String
let modelMigrationFile: String
let modelControllerFile: String
let force: Bool
let cwd: String

init(
console: Console, modelName: String, modelFile: String, modelMigrationFile: String,
modelControllerFile: String, force: Bool = false
) {
self.console = console
self.modelName = modelName
self.modelFile = modelFile
self.modelMigrationFile = modelMigrationFile
self.modelControllerFile = modelControllerFile
self.force = force

self.cwd = FileManager.default.currentDirectoryPath
}

func execute() {
console.output("Generating resource...")
generateModelFile()
generateMigrationFile()
generateControllerFile()
}

private func generateModelFile() {
let modelDirectory = cwd.appendingPathComponents("Sources/App/Models")
let modelOutputPath = modelDirectory.appendingPathComponents("\(modelName).swift")

process(at: modelDirectory, to: modelOutputPath, with: modelFile)

}

private func generateMigrationFile() {
let migrationDirectory = cwd.appendingPathComponents("Sources/App/Migrations")
let migrationOutputPath = migrationDirectory.appendingPathComponents("Create\(modelName).swift")

process(at: migrationDirectory, to: migrationOutputPath, with: modelMigrationFile)

}

private func generateControllerFile() {
let controllerDirectory = cwd.appendingPathComponents("Sources/App/Controllers")
let controllerOutputPath = controllerDirectory.appendingPathComponents(
"\(modelName)Controller.swift")

process(at: controllerDirectory, to: controllerOutputPath, with: modelControllerFile)
}

private func process(at directory: String, to file: String, with content: String) {

if !isDirectoryExist(at: directory) {
createDirectory(at: directory)
}

if !isFileExist(at: file) {
createFile(at: file, with: content)
} else if force {
createFile(at: file, with: content)
} else {
console.warning("Resource file already exists at \(file)")
}
}

private func isDirectoryExist(at path: String) -> Bool {
guard FileManager.default.fileExists(atPath: path) else {
return false
}

return true
}

private func createDirectory(at path: String) {
do {
try FileManager.default.createDirectory(atPath: path, withIntermediateDirectories: true)
} catch {
console.error("Error creating directory: \(error.localizedDescription)")
}
}

private func isFileExist(at path: String) -> Bool {
guard FileManager.default.fileExists(atPath: path) else {
return false
}

return true
}

private func createFile(at path: String, with file: String) {
do {
try file.write(toFile: path, atomically: true, encoding: .utf8)
console.output("Resource file created at \(path)")
} catch {
console.error("Error creating resourse file: \(error.localizedDescription)")
}
}
}
12 changes: 12 additions & 0 deletions Sources/VaporToolbox/Resource/ResourceError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Foundation

enum ResourceError: Error {
case notValidProject

var localizedDescription: String {
switch self {
case .notValidProject:
return "No Package.swift found. Are you in a Vapor project?"
}
}
}
138 changes: 138 additions & 0 deletions Sources/VaporToolbox/Resource/ResourceScaffolder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import ConsoleKit
import Foundation

struct ResourceScaffolder {
let console: Console
let modelName: String

init(console: Console, modelName: String) {
self.console = console
self.modelName = modelName
}

func generate(escaping: (String, String, String) -> Void) {
console.output("Generating resource...")
let model = generateModel()
let migration = generateMigration()
let controller = generateController()

escaping(model, migration, controller)
}

private func generateModel() -> String {
console.output("Generating model for \(modelName)...")

let model =
"""
import Fluent
import Vapor

final class \(modelName): Model, Content {
static let schema = "\(modelName.lowercased())_table"

@ID(key: .id)
var id: UUID?

// Add your fields here
// Example:
// @Field(key: "name")
// var name: String

init() {}

init(id: UUID? = nil) {
self.id = id
}
}
"""

return model
}

private func generateController() -> String {
console.output("Generating controller for \(modelName)Controller...")

let endpoint = modelName.lowercased() + "s"
let query = modelName.lowercased() + "ID"

let controller =
"""
import Vapor

struct \(modelName)Controller: RouteCollection {
func boot(routes: RoutesBuilder) throws {
let \(endpoint) = routes.grouped("\(endpoint)")
\(endpoint).get(use: index)
\(endpoint).post(use: create)
\(endpoint).group(":\(query)") { \(modelName) in
\(modelName).get(use: read)
\(modelName).put(use: update)
\(modelName).delete(use: delete)
}
}

func index(req: Request) async throws -> [\(modelName)] {
throw Abort(.notImplemented)
}

func create(req: Request) async throws -> \(modelName) {
throw Abort(.notImplemented)
}

func read(req: Request) async throws -> \(modelName) {
guard let id = req.parameters.get("\(query)", as: UUID.self) else {
throw Abort(.badRequest)
}

throw Abort(.notImplemented)
}

func update(req: Request) async throws -> \(modelName) {
guard let id = req.parameters.get("\(query)", as: UUID.self) else {
throw Abort(.badRequest)
}

throw Abort(.notImplemented)
}

func delete(req: Request) async throws -> HTTPStatus {
guard let id = req.parameters.get("\(query)", as: UUID.self) else {
throw Abort(.badRequest)
}

throw Abort(.notImplemented)
}
}
"""

return controller
}

private func generateMigration() -> String {
console.output("Generating migration for Create\(modelName)...")

let schema = modelName.lowercased() + "_table"

let migration =
"""
import Fluent

struct Create\(modelName): Migration {
func prepare(on database: Database) -> EventLoopFuture<Void> {
database.schema("\(schema)")
.id()
// Add fields here
// Example:
// .field("modelName", .string, .required)
.create()
}

func revert(on database: Database) -> EventLoopFuture<Void> {
database.schema("\(schema)").delete()
}
}
"""

return migration
}
}
1 change: 1 addition & 0 deletions Sources/VaporToolbox/Toolbox.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ final class Toolbox: CommandGroup {
"build": Build(),
"heroku": Heroku(),
"run": Run(),
"resource": Resource(),
"supervisor": Supervisor(),
]

Expand Down