diff --git a/packages/graphql-types-generator/src/swift/appSyncCompatibilityTypes.ts b/packages/graphql-types-generator/src/swift/appSyncCompatibilityTypes.ts new file mode 100644 index 000000000..e6dd4e649 --- /dev/null +++ b/packages/graphql-types-generator/src/swift/appSyncCompatibilityTypes.ts @@ -0,0 +1,426 @@ +// Blank lines at the beginning are intentional +export const appSyncCompatibilityTypesCode = ` +import Foundation + +public protocol GraphQLInputValue { +} + +public struct GraphQLVariable { + let name: String + + public init(_ name: String) { + self.name = name + } +} + +extension GraphQLVariable: GraphQLInputValue { +} + +extension JSONEncodable { + public func evaluate(with variables: [String: JSONEncodable]?) throws -> Any { + return jsonValue + } +} + +public typealias GraphQLMap = [String: JSONEncodable?] + +extension Dictionary where Key == String, Value == JSONEncodable? { + public var withNilValuesRemoved: Dictionary { + var filtered = Dictionary(minimumCapacity: count) + for (key, value) in self { + if value != nil { + filtered[key] = value + } + } + return filtered + } +} + +public protocol GraphQLMapConvertible: JSONEncodable { + var graphQLMap: GraphQLMap { get } +} + +public extension GraphQLMapConvertible { + var jsonValue: Any { + return graphQLMap.withNilValuesRemoved.jsonValue + } +} + +public extension GraphQLMapConvertible { + func evaluate(with variables: [String: JSONEncodable]?) throws -> Any { + return try graphQLMap.evaluate(with: variables) + } +} + +public typealias GraphQLID = String + +public protocol GraphQLOperation: AnyObject { + + static var operationString: String { get } + static var requestString: String { get } + static var operationIdentifier: String? { get } + + var variables: GraphQLMap? { get } + + associatedtype Data: GraphQLSelectionSet +} + +public extension GraphQLOperation { + static var requestString: String { + return operationString + } + + static var operationIdentifier: String? { + return nil + } + + var variables: GraphQLMap? { + return nil + } +} + +public protocol GraphQLQuery: GraphQLOperation {} + +public protocol GraphQLMutation: GraphQLOperation {} + +public protocol GraphQLSubscription: GraphQLOperation {} + +public protocol GraphQLFragment: GraphQLSelectionSet { + static var possibleTypes: [String] { get } +} + +public typealias Snapshot = [String: Any?] + +public protocol GraphQLSelectionSet: Decodable { + static var selections: [GraphQLSelection] { get } + + var snapshot: Snapshot { get } + init(snapshot: Snapshot) +} + +extension GraphQLSelectionSet { + public init(from decoder: Decoder) throws { + if let jsonObject = try? APISwiftJSONValue(from: decoder) { + let encoder = JSONEncoder() + let jsonData = try encoder.encode(jsonObject) + let decodedDictionary = try JSONSerialization.jsonObject(with: jsonData, options: []) as! [String: Any] + let optionalDictionary = decodedDictionary.mapValues { $0 as Any? } + + self.init(snapshot: optionalDictionary) + } else { + self.init(snapshot: [:]) + } + } +} + +enum APISwiftJSONValue: Codable { + case array([APISwiftJSONValue]) + case boolean(Bool) + case number(Double) + case object([String: APISwiftJSONValue]) + case string(String) + case null + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + + if let value = try? container.decode([String: APISwiftJSONValue].self) { + self = .object(value) + } else if let value = try? container.decode([APISwiftJSONValue].self) { + self = .array(value) + } else if let value = try? container.decode(Double.self) { + self = .number(value) + } else if let value = try? container.decode(Bool.self) { + self = .boolean(value) + } else if let value = try? container.decode(String.self) { + self = .string(value) + } else { + self = .null + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + + switch self { + case .array(let value): + try container.encode(value) + case .boolean(let value): + try container.encode(value) + case .number(let value): + try container.encode(value) + case .object(let value): + try container.encode(value) + case .string(let value): + try container.encode(value) + case .null: + try container.encodeNil() + } + } +} + + +public protocol GraphQLSelection { +} + +public struct GraphQLField: GraphQLSelection { + let name: String + let alias: String? + let arguments: [String: GraphQLInputValue]? + + var responseKey: String { + return alias ?? name + } + + let type: GraphQLOutputType + + public init(_ name: String, alias: String? = nil, arguments: [String: GraphQLInputValue]? = nil, type: GraphQLOutputType) { + self.name = name + self.alias = alias + + self.arguments = arguments + + self.type = type + } +} + +public indirect enum GraphQLOutputType { + case scalar(JSONDecodable.Type) + case object([GraphQLSelection]) + case nonNull(GraphQLOutputType) + case list(GraphQLOutputType) + + var namedType: GraphQLOutputType { + switch self { + case .nonNull(let innerType), .list(let innerType): + return innerType.namedType + case .scalar, .object: + return self + } + } +} + +public struct GraphQLBooleanCondition: GraphQLSelection { + let variableName: String + let inverted: Bool + let selections: [GraphQLSelection] + + public init(variableName: String, inverted: Bool, selections: [GraphQLSelection]) { + self.variableName = variableName + self.inverted = inverted; + self.selections = selections; + } +} + +public struct GraphQLTypeCondition: GraphQLSelection { + let possibleTypes: [String] + let selections: [GraphQLSelection] + + public init(possibleTypes: [String], selections: [GraphQLSelection]) { + self.possibleTypes = possibleTypes + self.selections = selections; + } +} + +public struct GraphQLFragmentSpread: GraphQLSelection { + let fragment: GraphQLFragment.Type + + public init(_ fragment: GraphQLFragment.Type) { + self.fragment = fragment + } +} + +public struct GraphQLTypeCase: GraphQLSelection { + let variants: [String: [GraphQLSelection]] + let \`default\`: [GraphQLSelection] + + public init(variants: [String: [GraphQLSelection]], default: [GraphQLSelection]) { + self.variants = variants + self.default = \`default\`; + } +} + +public typealias JSONObject = [String: Any] + +public protocol JSONDecodable { + init(jsonValue value: Any) throws +} + +public protocol JSONEncodable: GraphQLInputValue { + var jsonValue: Any { get } +} + +public enum JSONDecodingError: Error, LocalizedError { + case missingValue + case nullValue + case wrongType + case couldNotConvert(value: Any, to: Any.Type) + + public var errorDescription: String? { + switch self { + case .missingValue: + return "Missing value" + case .nullValue: + return "Unexpected null value" + case .wrongType: + return "Wrong type" + case .couldNotConvert(let value, let expectedType): + return "Could not convert \"\(value)\" to \(expectedType)" + } + } +} + +extension String: JSONDecodable, JSONEncodable { + public init(jsonValue value: Any) throws { + guard let string = value as? String else { + throw JSONDecodingError.couldNotConvert(value: value, to: String.self) + } + self = string + } + + public var jsonValue: Any { + return self + } +} + +extension Int: JSONDecodable, JSONEncodable { + public init(jsonValue value: Any) throws { + guard let number = value as? NSNumber else { + throw JSONDecodingError.couldNotConvert(value: value, to: Int.self) + } + self = number.intValue + } + + public var jsonValue: Any { + return self + } +} + +extension Float: JSONDecodable, JSONEncodable { + public init(jsonValue value: Any) throws { + guard let number = value as? NSNumber else { + throw JSONDecodingError.couldNotConvert(value: value, to: Float.self) + } + self = number.floatValue + } + + public var jsonValue: Any { + return self + } +} + +extension Double: JSONDecodable, JSONEncodable { + public init(jsonValue value: Any) throws { + guard let number = value as? NSNumber else { + throw JSONDecodingError.couldNotConvert(value: value, to: Double.self) + } + self = number.doubleValue + } + + public var jsonValue: Any { + return self + } +} + +extension Bool: JSONDecodable, JSONEncodable { + public init(jsonValue value: Any) throws { + guard let bool = value as? Bool else { + throw JSONDecodingError.couldNotConvert(value: value, to: Bool.self) + } + self = bool + } + + public var jsonValue: Any { + return self + } +} + +extension RawRepresentable where RawValue: JSONDecodable { + public init(jsonValue value: Any) throws { + let rawValue = try RawValue(jsonValue: value) + if let tempSelf = Self(rawValue: rawValue) { + self = tempSelf + } else { + throw JSONDecodingError.couldNotConvert(value: value, to: Self.self) + } + } +} + +extension RawRepresentable where RawValue: JSONEncodable { + public var jsonValue: Any { + return rawValue.jsonValue + } +} + +extension Optional where Wrapped: JSONDecodable { + public init(jsonValue value: Any) throws { + if value is NSNull { + self = .none + } else { + self = .some(try Wrapped(jsonValue: value)) + } + } +} + +extension Optional: JSONEncodable { + public var jsonValue: Any { + switch self { + case .none: + return NSNull() + case .some(let wrapped as JSONEncodable): + return wrapped.jsonValue + default: + fatalError("Optional is only JSONEncodable if Wrapped is") + } + } +} + +extension Dictionary: JSONEncodable { + public var jsonValue: Any { + return jsonObject + } + + public var jsonObject: JSONObject { + var jsonObject = JSONObject(minimumCapacity: count) + for (key, value) in self { + if case let (key as String, value as JSONEncodable) = (key, value) { + jsonObject[key] = value.jsonValue + } else { + fatalError("Dictionary is only JSONEncodable if Value is (and if Key is String)") + } + } + return jsonObject + } +} + +extension Array: JSONEncodable { + public var jsonValue: Any { + return map() { element -> (Any) in + if case let element as JSONEncodable = element { + return element.jsonValue + } else { + fatalError("Array is only JSONEncodable if Element is") + } + } + } +} + +extension URL: JSONDecodable, JSONEncodable { + public init(jsonValue value: Any) throws { + guard let string = value as? String else { + throw JSONDecodingError.couldNotConvert(value: value, to: URL.self) + } + self.init(string: string)! + } + + public var jsonValue: Any { + return self.absoluteString + } +} + +extension Dictionary { + static func += (lhs: inout Dictionary, rhs: Dictionary) { + lhs.merge(rhs) { (_, new) in new } + } +} +`; \ No newline at end of file diff --git a/packages/graphql-types-generator/src/swift/codeGeneration.ts b/packages/graphql-types-generator/src/swift/codeGeneration.ts index e9937552f..1ac26c6b6 100644 --- a/packages/graphql-types-generator/src/swift/codeGeneration.ts +++ b/packages/graphql-types-generator/src/swift/codeGeneration.ts @@ -18,6 +18,7 @@ import { join, wrap } from '../utilities/printing'; import { SwiftGenerator, Property, escapeIdentifierIfNeeded, Struct } from './language'; import { Helpers } from './helpers'; import { s3WrapperCode } from './s3Wrapper'; +import { appSyncCompatibilityTypesCode } from './appSyncCompatibilityTypes'; import { isList } from '../utilities/graphql'; import { typeCaseForSelectionSet, TypeCase, Variant } from '../compiler/visitors/typeCase'; @@ -120,7 +121,11 @@ export class SwiftAPIGenerator extends SwiftGenerator { fileHeader() { this.printOnNewline('// This file was automatically generated and should not be edited.'); this.printNewline(); + this.printOnNewline('#if canImport(AWSAPIPlugin)'); + this.print(appSyncCompatibilityTypesCode); + this.printOnNewline('#elseif canImport(AWSAppSync)'); this.printOnNewline('import AWSAppSync'); + this.printOnNewline('#endif'); } classDeclarationForOperation(operation: Operation) { @@ -818,4 +823,4 @@ export class SwiftAPIGenerator extends SwiftGenerator { } }); } -} +} \ No newline at end of file