diff --git a/Sources/Graphaello/Errors/GraphQLFragmentResolverError.swift b/Sources/Graphaello/Errors/GraphQLFragmentResolverError.swift index 88a2edb..4a43ee8 100644 --- a/Sources/Graphaello/Errors/GraphQLFragmentResolverError.swift +++ b/Sources/Graphaello/Errors/GraphQLFragmentResolverError.swift @@ -3,7 +3,7 @@ import Foundation enum GraphQLFragmentResolverError: Error, CustomStringConvertible { case cannotInferFragmentType(Schema.GraphQLType) case invalidTypeNameForFragment(String) - case failedToDecodeAnyOfTheStructsDueToPossibleRecursion([Struct]) + case failedToDecodeImportedFragments([Struct], resolved: [Struct]) case cannotResolveFragmentOrQueryWithEmptyPath(Stage.Resolved.Path) case cannotIncludeFragmentsInsideAQuery(GraphQLFragment) case cannotDowncastQueryToType(Schema.GraphQLType) @@ -15,13 +15,9 @@ enum GraphQLFragmentResolverError: Error, CustomStringConvertible { return "Usage of GraphQL object type \(type.name) must have a selection of subfields. Please use a value from this object or map it to a Fragment." case .invalidTypeNameForFragment(let name): return "Failed to parse \(name) as a Fragment" - case .failedToDecodeAnyOfTheStructsDueToPossibleRecursion(let structs): - let recursion = structs.recursion() - return recursion.map { property in - return "\(property.code.location.locationDescription): error: Property \(property.name) is referencing a Fragment that cannot be used. This is either due to a complexer syntax, or a recursive cycle between fragments" - } - .joined(separator: "\n") - + case .failedToDecodeImportedFragments(let structs, let resolved): + let errors = structs.resolutionErrors(resolved: resolved) + return errors.map(\.description).joined(separator: "\n") case .cannotResolveFragmentOrQueryWithEmptyPath: return "Query with Empty Paths are invalid" case .cannotIncludeFragmentsInsideAQuery(let fragment): @@ -34,11 +30,31 @@ enum GraphQLFragmentResolverError: Error, CustomStringConvertible { } } +private enum FragmentResolutionError { + case recursion(Property) + case fragmentNotFound(Property, referencing: StructResolution.ReferencedFragment) + + var description: String { + switch self { + case .recursion(let property): + return "\(property.code.location.locationDescription): error: Property \(property.name) is referencing a Fragment that cannot be used. This is either due to a syntax that is too complex for Graphaello, or a recursive cycle between fragments" + + case .fragmentNotFound(let property, .name(.fullName(let fullname))), + .fragmentNotFound(let property, .paging(.fullName(let fullname))): + return "\(property.code.location.locationDescription): error: Property \(property.name) is referencing a Fragment (\(fullname)) that could not be found. This is either due to a syntax that is too complex for Graphaello, or most likely because the Fragment no longer exists" + + case .fragmentNotFound(let property, .name(.typealiasOnStruct(let structName, let typeName))), + .fragmentNotFound(let property, .paging(.typealiasOnStruct(let structName, let typeName))): + return "\(property.code.location.locationDescription): error: Property \(property.name) is referencing a Fragment of \(typeName) from \(structName) that could not be found. This is either due to a syntax that is too complex for Graphaello, or most likely because the Fragment no longer exists" + } + } +} + extension Sequence where Element == Struct { - fileprivate func recursion() -> [Property] { + fileprivate func resolutionErrors(resolved: [Struct]) -> [FragmentResolutionError] { let structNames = Set(map { $0.name.lowercased() }) - let fragmentNames = Set( + let fragmentsInSequence = Set( flatMap { validated -> [String] in let simpleDefinitionName = validated.name.replacingOccurrences(of: #"[\[\]\.\?]"#, with: "", options: .regularExpression) return validated.properties.compactMap { property -> String? in @@ -49,28 +65,63 @@ extension Sequence where Element == Struct { } ) - return flatMap { validated -> [Property] in - return validated.properties.filter { property in - guard case .concrete(let type) = property.type, property.graphqlPath?.returnType.isFragment == true else { return false } + let resolvedFragments = Set(resolved.flatMap(\.fragments).map { $0.name.lowercased() }) + let structsToFragments = Dictionary(resolved.map { ($0.name, Set($0.fragments.map { $0.name.lowercased() })) }) { $1 } + + return flatMap { validated -> [FragmentResolutionError] in + return validated.properties.compactMap { property in + guard case .concrete(let type) = property.type, property.graphqlPath?.returnType.isFragment == true else { return nil } do { let fragment = try StructResolution.ReferencedFragment(typeName: type) switch fragment { case .name(.fullName(let name)): - return fragmentNames.contains(name.lowercased()) + if fragmentsInSequence.contains(name.lowercased()) { + return .recursion(property) + } + + if resolvedFragments.contains(name.lowercased()) { + return nil + } + + return .fragmentNotFound(property, referencing: fragment) case .paging(.fullName(let name)): - return fragmentNames.contains(name.lowercased()) + if fragmentsInSequence.contains(name.lowercased()) { + return .recursion(property) + } + + if resolvedFragments.contains(name.lowercased()) { + return nil + } + + return .fragmentNotFound(property, referencing: fragment) + + case .name(.typealiasOnStruct(let structName, let targetName)): + if structNames.contains(structName.lowercased()) { + return .recursion(property) + } + + if let fragmentsForStruct = structsToFragments[structName], fragmentsForStruct.contains(targetName.lowercased()) { + return nil + } + + return .fragmentNotFound(property, referencing: fragment) + + case .paging(.typealiasOnStruct(let structName, let targetName)): + if structNames.contains(structName.lowercased()) { + return .recursion(property) + } - case .name(.typealiasOnStruct(let structName, _)): - return structNames.contains(structName.lowercased()) + if let fragmentsForStruct = structsToFragments[structName], fragmentsForStruct.contains(targetName.lowercased()) { + return nil + } - case .paging(.typealiasOnStruct(let structName, _)): - return structNames.contains(structName.lowercased()) + return .fragmentNotFound(property, referencing: fragment) } } catch { - return true + return nil } } } diff --git a/Sources/Graphaello/Processing/Pipeline/Component/4. Resolution/BasicResolver.swift b/Sources/Graphaello/Processing/Pipeline/Component/4. Resolution/BasicResolver.swift index dd11ebc..0a83ec1 100644 --- a/Sources/Graphaello/Processing/Pipeline/Component/4. Resolution/BasicResolver.swift +++ b/Sources/Graphaello/Processing/Pipeline/Component/4. Resolution/BasicResolver.swift @@ -16,7 +16,9 @@ struct BasicResolver: Resolver where SubResolver.Re let missing = finalContext.failedDueToMissingFragment - guard missing.count < validated.count else { throw GraphQLFragmentResolverError.failedToDecodeAnyOfTheStructsDueToPossibleRecursion(missing) } + guard missing.count < validated.count else { + throw GraphQLFragmentResolverError.failedToDecodeImportedFragments(missing, resolved: finalContext.resolved) + } return try resolve(validated: missing, using: finalContext.cleared()) }