diff --git a/Sources/AnimalKingdomAPI/AnimalKingdomAPI/Sources/Schema/SchemaConfiguration.swift b/Sources/AnimalKingdomAPI/AnimalKingdomAPI/Sources/Schema/SchemaConfiguration.swift index 7976ecf82..b446d8f9a 100644 --- a/Sources/AnimalKingdomAPI/AnimalKingdomAPI/Sources/Schema/SchemaConfiguration.swift +++ b/Sources/AnimalKingdomAPI/AnimalKingdomAPI/Sources/Schema/SchemaConfiguration.swift @@ -8,7 +8,10 @@ import ApolloAPI public enum SchemaConfiguration: ApolloAPI.SchemaConfiguration { - public static func cacheKeyInfo(for type: Object, object: ObjectData) -> CacheKeyInfo? { + public static func cacheKeyInfo( + for type: ApolloAPI.Object, + object: ApolloAPI.ObjectData + ) -> ApolloAPI.CacheKeyInfo? { // Implement this function to configure cache key resolution for your schema types. return nil } diff --git a/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift b/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift index a0542f7b0..88bdf3d0a 100644 --- a/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift +++ b/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift @@ -4327,6 +4327,60 @@ class IRRootFieldBuilderTests: XCTestCase { expect(self.computedReferencedFragments).to(equal(expected)) } + func test__referencedFragments__givenMultipleFragments_hasFragmentsInAlphbeticalOrder() async throws { + // given + schemaSDL = """ + type Query { + name: String! + } + """ + + document = + """ + query NameQuery { + ...Fragment4 + ...Fragment1 + } + + fragment Fragment4 on Query { + name + ...Fragment3 + } + + fragment Fragment3 on Query { + name + ...Fragment2 + } + + fragment Fragment2 on Query { + name + } + + fragment Fragment1 on Query { + name + ...Fragment5 + } + + fragment Fragment5 on Query { + name + } + """ + + // when + try await buildSubjectRootField() + + let expected: OrderedSet = await [ + try ir.builtFragmentStorage.getFragmentIfBuilt(named: "Fragment1").xctUnwrapped(), + try ir.builtFragmentStorage.getFragmentIfBuilt(named: "Fragment2").xctUnwrapped(), + try ir.builtFragmentStorage.getFragmentIfBuilt(named: "Fragment3").xctUnwrapped(), + try ir.builtFragmentStorage.getFragmentIfBuilt(named: "Fragment4").xctUnwrapped(), + try ir.builtFragmentStorage.getFragmentIfBuilt(named: "Fragment5").xctUnwrapped(), + ] + + // then + expect(self.computedReferencedFragments).to(equal(expected)) + } + // MARK: - Deferred Fragments - hasDeferredFragments property func test__deferredFragments__givenNoDeferredFragment_hasDeferredFragmentsFalse() async throws { diff --git a/Tests/ApolloCodegenTests/OperationDescriptorTests.swift b/Tests/ApolloCodegenTests/OperationDescriptorTests.swift new file mode 100644 index 000000000..e6d6db95b --- /dev/null +++ b/Tests/ApolloCodegenTests/OperationDescriptorTests.swift @@ -0,0 +1,104 @@ +import Foundation +import XCTest +import Nimble +import OrderedCollections +import GraphQLCompiler +@testable import IR +import Utilities +@testable import ApolloCodegenLib +import ApolloCodegenInternalTestHelpers + +class OperationDescriptorTests: XCTestCase { + var schemaSDL: String! + var document: String! + var ir: IRBuilder! + var subject: OperationDescriptor! + + override func setUp() { + super.setUp() + } + + override func tearDown() { + subject = nil + schemaSDL = nil + document = nil + ir = nil + super.tearDown() + } + + // MARK: - Helpers + + func getOperation( + named operationName: String? = nil, + fromJSONSchema json: Bool = false + ) async throws { + ir = json ? + try await .mock(schemaJSON: schemaSDL, document: document) : + try await .mock(schema: schemaSDL, document: document) + + var operation: CompilationResult.OperationDefinition + if let operationName = operationName { + operation = try XCTUnwrap(ir.compilationResult.operations.first {$0.name == operationName}) + } else { + operation = try XCTUnwrap(ir.compilationResult.operations.first) + } + + subject = OperationDescriptor(operation) + } + + // MARK: - Tests + + @available(macOS 13.0, *) + func test__rawSourceText__givenOperationWithDeeplyNestedFragmentsNotInAlphabeticalOrder__hasReferencedFragmentsInSameOrderAsBuiltOperation() async throws { + // given + schemaSDL = """ + type Query { + name: String! + } + """ + + document = + """ + query NameQuery { + ...Fragment4 + ...Fragment1 + } + + fragment Fragment4 on Query { + name + ...Fragment3 + } + + fragment Fragment3 on Query { + name + ...Fragment2 + } + + fragment Fragment2 on Query { + name + } + + fragment Fragment1 on Query { + name + ...Fragment5 + } + + fragment Fragment5 on Query { + name + } + """ + + try await getOperation(named: "NameQuery") + + // when + let operationDescriptorReferencedFragments = subject.rawSourceText + .matches(of: /fragment (\S*)\s/) + .map(\.output.1.description) + + let operation = await ir.build(operation: subject.underlyingDefinition) + let builtOperationReferencedFragments = operation.referencedFragments.map(\.name) + + // then + expect(operationDescriptorReferencedFragments).to(equal(builtOperationReferencedFragments)) + } +} diff --git a/Tests/ApolloCodegenTests/OperationIdentifierFactoryTests.swift b/Tests/ApolloCodegenTests/OperationIdentifierFactoryTests.swift index dda4bf4ec..1c3278373 100644 --- a/Tests/ApolloCodegenTests/OperationIdentifierFactoryTests.swift +++ b/Tests/ApolloCodegenTests/OperationIdentifierFactoryTests.swift @@ -25,10 +25,11 @@ class OperationIdentifierFactoryTests: XCTestCase { schemaSDL = nil document = nil operation = nil + ir = nil super.tearDown() } - // MARK: = Helpers + // MARK: - Helpers func getOperation( named operationName: String? = nil, @@ -47,43 +48,44 @@ class OperationIdentifierFactoryTests: XCTestCase { // MARK: - Default Operation Identifier Computation Tests - func test__buildOperation__givenOperationWithNoFragments__hasCorrectOperationIdentifier() async throws { - // given - document = try String( - contentsOf: ApolloCodegenInternalTestHelpers.Resources.StarWars.GraphQLOperation(named: "HeroAndFriendsNames") - ) + func test__identifierForOperation__givenOperationWithNoFragments__hasCorrectOperationIdentifier() async throws { + // given + document = try String( + contentsOf: ApolloCodegenInternalTestHelpers.Resources.StarWars.GraphQLOperation(named: "HeroAndFriendsNames") + ) - schemaSDL = try String( - contentsOf: ApolloCodegenInternalTestHelpers.Resources.StarWars.JSONSchema) + schemaSDL = try String( + contentsOf: ApolloCodegenInternalTestHelpers.Resources.StarWars.JSONSchema) - let expected = "1e36c3331171b74c012b86caa04fbb01062f37c61227655d9c0729a62c6f7285" - try await getOperation(named: "HeroAndFriendsNames", fromJSONSchema: true) + let expected = "1e36c3331171b74c012b86caa04fbb01062f37c61227655d9c0729a62c6f7285" + try await getOperation(named: "HeroAndFriendsNames", fromJSONSchema: true) - // when - let actual = try await subject.identifier(for: operation) + // when + let actual = try await subject.identifier(for: operation) - // then - expect(actual).to(equal(expected)) - } + // then + expect(actual).to(equal(expected)) + } - func test__buildOperation__givenOperationWithFragment__hasCorrectOperationIdentifier() async throws { - // given - document = try String( - contentsOf: ApolloCodegenInternalTestHelpers.Resources.StarWars.GraphQLOperation(named: "HeroAndFriendsNamesWithFragment") - ) + "\n" + String( - contentsOf: ApolloCodegenInternalTestHelpers.Resources.StarWars.GraphQLOperation(named: "HeroName") - ) + func test__identifierForOperation__givenOperationWithFragment__hasCorrectOperationIdentifier() async throws { + // given + document = try String( + contentsOf: ApolloCodegenInternalTestHelpers.Resources.StarWars.GraphQLOperation(named: "HeroAndFriendsNamesWithFragment") + ) + "\n" + String( + contentsOf: ApolloCodegenInternalTestHelpers.Resources.StarWars.GraphQLOperation(named: "HeroName") + ) - schemaSDL = try String( - contentsOf: ApolloCodegenInternalTestHelpers.Resources.StarWars.JSONSchema) + schemaSDL = try String( + contentsOf: ApolloCodegenInternalTestHelpers.Resources.StarWars.JSONSchema) - let expected = "599cd7d91ede7a5508cdb26b424e3b8e99e6c2c5575b799f6090695289ff8e99" - try await getOperation(named: "HeroAndFriendsNamesWithFragment", fromJSONSchema: true) + let expected = "599cd7d91ede7a5508cdb26b424e3b8e99e6c2c5575b799f6090695289ff8e99" + try await getOperation(named: "HeroAndFriendsNamesWithFragment", fromJSONSchema: true) - // when - let actual = try await subject.identifier(for: operation) + // when + let actual = try await subject.identifier(for: operation) + + // then + expect(actual).to(equal(expected)) + } - // then - expect(actual).to(equal(expected)) - } } diff --git a/apollo-ios-codegen/Sources/ApolloCodegenLib/OperationDescriptor.swift b/apollo-ios-codegen/Sources/ApolloCodegenLib/OperationDescriptor.swift index 1b39743f4..8e14b211a 100644 --- a/apollo-ios-codegen/Sources/ApolloCodegenLib/OperationDescriptor.swift +++ b/apollo-ios-codegen/Sources/ApolloCodegenLib/OperationDescriptor.swift @@ -62,22 +62,38 @@ public struct OperationDescriptor: Sendable { } func sourceText(withFormat format: SourceFormat) -> String { - format.formatted(underlyingDefinition) + format.formatted(self) } - + + fileprivate var allReferencedFragments: [CompilationResult.FragmentDefinition] { + func insertAllFragments( + from fragment: CompilationResult.FragmentDefinition, + into set: inout Set + ) { + set.insert(fragment) + for referencedFragment in fragment.referencedFragments { + insertAllFragments(from: referencedFragment, into: &set) + } + } + + var fragmentSet = Set() + for fragment in underlyingDefinition.referencedFragments { + insertAllFragments(from: fragment, into: &fragmentSet) + } + return fragmentSet.sorted { $0.name < $1.name } + } + } // MARK: - Formatting fileprivate extension OperationDescriptor.SourceFormat { - func formatted(_ operation: CompilationResult.OperationDefinition) -> String { - var source = operation.source.convertedToSingleLine() - var set = Set() - append( - to: &source, - set: &set, - fragments: operation.referencedFragments - ) + func formatted(_ operation: OperationDescriptor) -> String { + var source = operation.underlyingDefinition.source.convertedToSingleLine() + for fragment in operation.allReferencedFragments { + source += formatted(fragment) + } + switch self { case .rawSource: return source @@ -86,20 +102,6 @@ fileprivate extension OperationDescriptor.SourceFormat { } } - private func append( - to source: inout String, - set: inout Set, - fragments: [CompilationResult.FragmentDefinition] - ) { - for fragment in fragments { - if !set.contains(fragment.name) { - set.insert(fragment.name) - source += formatted(fragment) - append(to: &source, set: &set, fragments: fragment.referencedFragments) - } - } - } - private func formatted(_ fragment: CompilationResult.FragmentDefinition) -> String { switch self { case .rawSource: diff --git a/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift b/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift index 2b7d5c9ea..932d154e6 100644 --- a/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift +++ b/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift @@ -121,6 +121,10 @@ class RootFieldBuilder { from: rootSelectionSet ) + referencedFragments.sort(by: { + $0.name < $1.name + }) + return Result( rootField: EntityField(rootField, selectionSet: rootIrSelectionSet), referencedFragments: referencedFragments,