From 24a4462a76442256ec88db2bf424fa39776b66ba Mon Sep 17 00:00:00 2001 From: Yonas Kolb Date: Tue, 7 May 2024 05:59:43 +1000 Subject: [PATCH] Improve code generation (#204) * single objects lookup * generate seperate files * config to disable static field generation * add server setup docs * make config.generateStaticFields optional for backwards compatability * support single file generation with .swift output * make standard out just be the single file again --------- Co-authored-by: Matic Zavadlal --- .gitignore | 1 - Sources/SwiftGraphQLCLI/main.swift | 34 ++++- Sources/SwiftGraphQLCodegen/Generator.swift | 119 +++++++++++------- examples/thesocialnetwork/README.md | 16 ++- examples/thesocialnetwork/server/.env.example | 6 + 5 files changed, 121 insertions(+), 55 deletions(-) create mode 100644 examples/thesocialnetwork/server/.env.example diff --git a/.gitignore b/.gitignore index 151a6c0b..e38e936f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ .DS_Store *.log* -.env* # Swift diff --git a/Sources/SwiftGraphQLCLI/main.swift b/Sources/SwiftGraphQLCLI/main.swift index 23d25412..58f611b6 100755 --- a/Sources/SwiftGraphQLCLI/main.swift +++ b/Sources/SwiftGraphQLCLI/main.swift @@ -112,10 +112,18 @@ struct SwiftGraphQLCLI: ParsableCommand { let scalars = ScalarMap(scalars: config.scalars) let generator = GraphQLCodegen(scalars: scalars) - let code: String - + let files: [GeneratedFile] + + // If the output is a Swift file generate a single file, otherwise multiple files in that directory + // If there's no output generate a single file as well as it will be printed to standard out + let singleFileOutput = output?.hasSuffix(".swift") ?? true + do { - code = try generator.generate(schema: schema) + files = try generator.generate( + schema: schema, + generateStaticFields: config.generateStaticFields != false, + singleFile: singleFileOutput + ) generateCodeSpinner.success("API generated successfully!") } catch CodegenError.formatting(let err) { generateCodeSpinner.error(err.localizedDescription) @@ -131,9 +139,21 @@ struct SwiftGraphQLCLI: ParsableCommand { // Write to target file or stdout. if let outputPath = output { - try Folder.current.createFile(at: outputPath).write(code) + if singleFileOutput, let file = files.first { + // The generator returns a single file if asked to + try Folder.current.createFile(at: outputPath).write(file.contents) + } else { + // Clear the directory, in case some files were removed + try? Folder.current.subfolder(at: outputPath).delete() + for file in files { + try Folder.current.createFile(at: "\(outputPath)/\(file.name).swift").write(file.contents) + } + } } else { - FileHandle.standardOutput.write(code.data(using: .utf8)!) + for file in files { + // this should always be one file anyway + FileHandle.standardOutput.write(file.contents.data(using: .utf8)!) + } } let analyzeSchemaSpinner = Spinner(.dots, "Analyzing Schema") @@ -174,11 +194,15 @@ struct Config: Codable, Equatable { /// Key-Value dictionary of scalar mappings. let scalars: [String: String] + /// Whether to generate static lookups for object fields + var generateStaticFields: Bool? + // MARK: - Initializers /// Creates an empty configuration instance. init() { self.scalars = [:] + self.generateStaticFields = true } /// Tries to decode the configuration from a string. diff --git a/Sources/SwiftGraphQLCodegen/Generator.swift b/Sources/SwiftGraphQLCodegen/Generator.swift index e15062cf..237b0122 100644 --- a/Sources/SwiftGraphQLCodegen/Generator.swift +++ b/Sources/SwiftGraphQLCodegen/Generator.swift @@ -14,79 +14,102 @@ public struct GraphQLCodegen { } // MARK: - Methods - - /// Generates a SwiftGraphQL Selection File (i.e. the code that tells how to define selections). - public func generate(schema: Schema) throws -> String { + + /// Generates Swift files for the graph selections + /// - Parameters: + /// - schema: The GraphQL schema + /// - generateStaticFields: Whether to generate static selections for fields on objects + /// - singleFile: Whether to return all the swift code in a single file + /// - Returns: A list of generated files + public func generate(schema: Schema, generateStaticFields: Bool, singleFile: Bool = false) throws -> [GeneratedFile] { let context = Context(schema: schema, scalars: self.scalars) let subscription = schema.operations.first { $0.isSubscription }?.type.name - - // Code Parts + let objects = schema.objects let operations = schema.operations.map { $0.declaration() } - let objectDefinitions = try schema.objects.map { object in - try object.declaration( - objects: schema.objects, - context: context, - alias: object.name != subscription - ) - } - - let staticFieldSelection = try schema.objects.map { object in - try object.statics(context: context) - } - - let interfaceDefinitions = try schema.interfaces.map { - try $0.declaration(objects: schema.objects, context: context) - } - - let unionDefinitions = try schema.unions.map { - try $0.declaration(objects: schema.objects, context: context) - } - - let enumDefinitions = schema.enums.map { $0.declaration } - - let inputObjectDefinitions = try schema.inputObjects.map { - try $0.declaration(context: context) - } - - // API - let code = """ + + var files: [GeneratedFile] = [] + + let header = """ // This file was auto-generated using maticzav/swift-graphql. DO NOT EDIT MANUALLY! import Foundation import GraphQL import SwiftGraphQL + """ - // MARK: - Operations + let graphContents = """ public enum Operations {} \(operations.lines) - // MARK: - Objects public enum Objects {} - \(objectDefinitions.lines) - \(staticFieldSelection.lines) - // MARK: - Interfaces public enum Interfaces {} - \(interfaceDefinitions.lines) - // MARK: - Unions public enum Unions {} - \(unionDefinitions.lines) - // MARK: - Enums public enum Enums {} - \(enumDefinitions.lines) - // MARK: - Input Objects - /// Utility pointer to InputObjects. public typealias Inputs = InputObjects public enum InputObjects {} - \(inputObjectDefinitions.lines) """ - let formatted = try code.format() - return formatted + func addFile(name: String, contents: String) throws { + let fileContents: String + if singleFile { + fileContents = "\n// MARK: \(name)\n\(contents)" + } else { + fileContents = "\(header)\n\n\(contents)" + } + let file = GeneratedFile(name: name, contents: try fileContents.format()) + files.append(file) + } + + try addFile(name: "Graph", contents: graphContents) + for object in objects { + var contents = try object.declaration( + objects: objects, + context: context, + alias: object.name != subscription + ) + + if generateStaticFields { + let staticFieldSelection = try object.statics(context: context) + contents += "\n\n\(staticFieldSelection)" + } + try addFile(name: "Objects/\(object.name)", contents: contents) + } + + for object in schema.inputObjects { + let contents = try object.declaration(context: context) + try addFile(name: "InputObjects/\(object.name)", contents: contents) + } + + for enumSchema in schema.enums { + try addFile(name: "Enums/\(enumSchema.name)", contents: enumSchema.declaration) + } + + for interface in schema.interfaces { + let contents = try interface.declaration(objects: objects, context: context) + try addFile(name: "Interfaces/\(interface.name)", contents: contents) + } + + for union in schema.unions { + let contents = try union.declaration(objects: objects, context: context) + try addFile(name: "Unions/\(union.name)", contents: contents) + } + + if singleFile { + let fileContent = "\(header)\n\n\(files.map(\.contents).joined(separator: "\n\n"))" + files = [GeneratedFile(name: "Graph", contents: fileContent)] + } + + return files } } + +public struct GeneratedFile { + public let name: String + public let contents: String +} diff --git a/examples/thesocialnetwork/README.md b/examples/thesocialnetwork/README.md index b8c3d4a6..4d2c3858 100644 --- a/examples/thesocialnetwork/README.md +++ b/examples/thesocialnetwork/README.md @@ -7,12 +7,26 @@ A sample server for a social network that - uses subscriptions, - lets users write to a shared feed. +```bash +# Start Server +yarn start + +# Generate Prisma Client +yarn prisma generate + +# Generate TypeGen +yarn generate +``` + + ### Development Setup Start local Postgres database using Docker Compose. ```bash +# Start DB in the background docker-compose up -d -export DATABASE_URL="postgresql://prisma:prisma@localhost:5432/prisma" +# Setup Environment variables +cp .env.example .env ``` diff --git a/examples/thesocialnetwork/server/.env.example b/examples/thesocialnetwork/server/.env.example new file mode 100644 index 00000000..c722cb74 --- /dev/null +++ b/examples/thesocialnetwork/server/.env.example @@ -0,0 +1,6 @@ +AWS_S3_BUCKET="thesocialnetwork-images" +AWS_ACCESS_KEY_ID="AKIA3JJIBRRXWTIC7" +AWS_SECRET_ACCESS_KEY="puU+kTKu288s+K9HpcbPGIrX79TItKly" + +DATABASE_URL="postgresql://prisma:prisma@localhost:5432/prisma" +