Skip to content

Commit

Permalink
✨ codegen
Browse files Browse the repository at this point in the history
  • Loading branch information
maticzav committed Feb 7, 2021
1 parent 1af07f1 commit 996ed85
Show file tree
Hide file tree
Showing 37 changed files with 1,091 additions and 1,545 deletions.
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ let package = Package(
),
.testTarget(
name: "SwiftGraphQLCodegenTests",
dependencies: ["Files", "SwiftGraphQLCodegen"]
dependencies: ["Files", "SwiftGraphQLCodegen", "GraphQLAST"]
),
.testTarget(
name: "GraphQLASTTests",
Expand Down
62 changes: 44 additions & 18 deletions Sources/GraphQLAST/Schema.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,54 +3,80 @@ import Foundation
// MARK: - Schema

public struct Schema: Decodable, Equatable {
public let description: String?

/// Collection of all types in the schema.
public let types: [NamedType]

/**
Internal information about the types of root operations.
*/
private let queryTypeName: String
private let mutationTypeName: String?
private let subscriptionTypeName: String?
private let _query: String
private let _mutation: String?
private let _subscription: String?

// MARK: - Initializer

public init(types: [NamedType], query: String, mutation: String? = nil, subscription: String? = nil) {
self.types = types

self._query = query
self._mutation = mutation
self._subscription = subscription
}

// MARK: - Decoder

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

self.types = try container.decode([NamedType].self, forKey: .types)
self._query = try container.decode(_Operation.self, forKey: .query).name
self._mutation = try container.decode(_Operation?.self, forKey: .mutation)?.name
self._subscription = try container.decode(_Operation?.self, forKey: .subscription)?.name
}

private enum CodingKeys: String, CodingKey {
case types
case query = "queryType"
case mutation = "mutationType"
case subscription = "subscriptionType"
}

private struct _Operation: Codable {
var name: String
}
}

// MARK: - Accessors

public extension Schema {

/// Searches for a type with a given name.
func type(name: String) -> NamedType? {
types.first(where: { $0.name == name })
}

/// Searches for an object with a given name.
func object(name: String) -> ObjectType? {
objects.first(where: { $0.name == name })
}

// MARK: - Operations

/// Query operation type in the schema.
var query: Operation {
.query(object(name: queryTypeName)!)
.query(object(name: _query)!)
}

/// Mutation operation type in the schema.
var mutation: Operation? {
mutationTypeName
_mutation
.flatMap { object(name: $0) }
.flatMap { .mutation($0) }
}

/// Subscription operation type in the schema.
var subscription: Operation? {
subscriptionTypeName
_subscription
.flatMap { object(name: $0) }
.flatMap { .subscription($0) }
}

/// Returns operation types in the schema.
var operations: [Operation] {
[query, mutation, subscription].compactMap { $0 }
Expand Down
4 changes: 2 additions & 2 deletions Sources/GraphQLAST/Type.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ public enum Operation {
case query(ObjectType)
case mutation(ObjectType)
case subscription(ObjectType)

public var type: ObjectType {
switch self {
case .query(let type), .mutation(let type), .subscription(let type):
case let .query(type), let .mutation(type), let .subscription(type):
return type
}
}
Expand Down
13 changes: 13 additions & 0 deletions Sources/GraphQLAST/TypeRef.swift
Original file line number Diff line number Diff line change
Expand Up @@ -309,3 +309,16 @@ public typealias OutputTypeRef = TypeRef<OutputRef>
public typealias InputTypeRef = TypeRef<InputRef>
public typealias ObjectTypeRef = TypeRef<ObjectRef>
public typealias InterfaceTypeRef = TypeRef<InterfaceRef>

public extension ObjectTypeRef {
var name: String {
switch self {
case let .named(ref):
return ref.name
case let .list(ref):
return ref.namedType.name
case let .nonNull(ref):
return ref.namedType.name
}
}
}
3 changes: 3 additions & 0 deletions Sources/GraphQLAST/Value.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Foundation

/// Represents a single GraphQL field.
public struct Field: Decodable, Equatable {
public let name: String
public let description: String?
Expand All @@ -9,12 +10,14 @@ public struct Field: Decodable, Equatable {
public let deprecationReason: String?
}

/// Represents a GraphQL type that may be used as an input value.
public struct InputValue: Decodable, Equatable {
public let name: String
public let description: String?
public let type: InputTypeRef
}

/// Represents a GraphQL enumerator case.
public struct EnumValue: Codable, Equatable {
public let name: String
public let description: String?
Expand Down
1 change: 0 additions & 1 deletion Sources/SwiftGraphQL/Internal/Decode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ public struct HashMap {
}
}


/*
We use DynamicCodingKeys to allow response decoder to look up
for arbitrary key in the response. Otherwise, we would have to
Expand Down
18 changes: 18 additions & 0 deletions Sources/SwiftGraphQLCodegen/Extensions/Collection+UniqueBy.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Foundation

extension Collection {
/// Returns a list of unique items.
///
/// - Note: Item's uniqueness is determined by the hash value of that item.
/// If there are more items with the same hash, we use the last one.
///
func unique<T>(by: (Element) -> T) -> [Element] where T: Hashable {
var dict = [T: Element]()

for item in self {
dict[by(item)] = item
}

return [Element](dict.values)
}
}
5 changes: 4 additions & 1 deletion Sources/SwiftGraphQLCodegen/Extensions/String+Format.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import SwiftFormat
extension String {
/// Formats the given Swift source code.
func format() throws -> String {
let formatted = try SwiftFormat.format(self)
let trimmed = trimmingCharacters(
in: CharacterSet.newlines.union(.whitespaces)
)
let formatted = try SwiftFormat.format(trimmed)
return formatted
}
}
8 changes: 8 additions & 0 deletions Sources/SwiftGraphQLCodegen/Extensions/String+Lines.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import Foundation

extension Collection where Element == String {
/// Returns a collection of strings, each string in new line.
var lines: String {
joined(separator: "\n")
}
}
157 changes: 29 additions & 128 deletions Sources/SwiftGraphQLCodegen/Generator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,52 +7,15 @@ import GraphQLAST
*/

public struct GraphQLCodegen {
let options: Options
private let scalars: ScalarMap

// MARK: - Initializer

public init(options: Options = Options()) {
self.options = options
}

// MARK: - Options

public struct Options {
public typealias ScalarMap = [String: String]

/// Map of scalar GraphQL values and their Swift types.
private let scalarMap: ScalarMap

// MARK: - Initializer

public init(scalarMappings: ScalarMap = [:]) {
let map = builtInScalars.merging(
scalarMappings,
uniquingKeysWith: { _, override in override }
)

scalarMap = map
}

// MARK: - Methods

/// Returns the mapped value of the scalar.
func scalar(_ name: String) throws -> String {
if let mapping = scalarMap[name] {
return mapping
}
throw GraphQLCodegenError.unknownScalar(name)
}

// MARK: - Default values

private let builtInScalars: ScalarMap = [
"ID": "String",
"String": "String",
"Int": "Int",
"Boolean": "Bool",
"Float": "Double",
]
public init(scalars: ScalarMap) {
self.scalars = ScalarMap.builtin.merging(
scalars,
uniquingKeysWith: { _, override in override }
)
}

// MARK: - Methods
Expand All @@ -74,103 +37,41 @@ public struct GraphQLCodegen {
/// Generates the API and returns it to handler.
public func generate(from endpoint: URL) throws -> String {
let schema = try Schema(from: endpoint)
let code = try generate(from: schema)
let code = try generate(schema: schema)
return code
}

/// Generates the code that can be used to define selections.
func generate(from schema: Schema) throws -> String {
/* Data */

// ObjectTypes for operations
let operations: [(type: ObjectType, operation: Operation)] = [
(schema.queryType.name.pascalCase, .query),
(schema.mutationType?.name.pascalCase, .mutation),
(schema.subscriptionType?.name.pascalCase, .subscription),
].compactMap { type, operation in
schema.objects.first(where: { $0.name == type }).map { ($0, operation) }
}

// Object types for all other objects.
let objects: [ObjectType] = schema.objects

// Phantom type references
var types = [NamedType]()
types.append(contentsOf: schema.objects.map { .object($0) })
types.append(contentsOf: schema.inputObjects.map { .inputObject($0) })
func generate(schema: Schema) throws -> String {
let code = """
import SwiftGraphQL
/* Code parts. */
// MARK: - Operations
enum Operations {}
\(schema.operations.map { $0.declaration() }.lines)
let operationsPart = try operations.map {
try generateOperation(type: $0.type, operation: $0.operation)
}
// MARK: - Objects
enum Objects {}
\(try schema.objects.map { try $0.declaration(objects: schema.objects, scalars: scalars) }.lines)
let objectsPart = try objects.map {
try generateObject($0)
}
// MARK: - Interfaces
enum Interfaces {}
\(try schema.interfaces.map { try $0.declaration(objects: schema.objects, scalars: scalars) }.lines)
let interfacesPart = try schema.interfaces.map {
try generateInterface($0, with: objects)
}
// MARK: - Unions
enum Unions {}
\(try schema.unions.map { try $0.declaration(objects: schema.objects, scalars: scalars) }.lines)
let unionsPart = try schema.unions.map {
try generateUnion($0, with: objects)
}
// MARK: - Enums
enum Enums {}
\(schema.enums.map { $0.declaration }.lines)
let enumsPart = schema.enums.map {
generateEnum($0)
}

let inputObjectsPart = try schema.inputObjects.map {
try generateInputObject($0.name.pascalCase, for: $0)
}

/* File. */
let code = [
"import SwiftGraphQL",
"",
"// MARK: - Operations",
"",
"enum Operations {}",
"",
operationsPart,
"",
"// MARK: - Objects",
"",
"enum Objects {}",
"",
objectsPart,
"",
"// MARK: - Interfaces",
"",
"enum Interfaces {}",
"",
interfacesPart,
"",
"// MARK: - Unions",
"",
"enum Unions {}",
"",
unionsPart,
"",
"// MARK: - Enums",
"",
"enum Enums {",
enumsPart,
"}",
"",
"// MARK: - Input Objects",
"",
"enum InputObjects {",
inputObjectsPart,
"}",
].joined(separator: "\n")
// MARK: - Input Objects
enum InputObjects {}
\(try schema.inputObjects.map { try $0.declaration(scalars: scalars) }.lines)
"""

let source = try code.format()
return source
}
}

enum GraphQLCodegenError: Error {
case unknownScalar(String)
}
Loading

0 comments on commit 996ed85

Please sign in to comment.