diff --git a/Apollo.xcodeproj/project.pbxproj b/Apollo.xcodeproj/project.pbxproj index fa42a8d20a..ffa39d149f 100644 --- a/Apollo.xcodeproj/project.pbxproj +++ b/Apollo.xcodeproj/project.pbxproj @@ -192,7 +192,7 @@ DE181A3626C5DE4F000C0B9C /* WebSocketStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE181A3526C5DE4F000C0B9C /* WebSocketStream.swift */; }; DE223C1C271F3288004A0148 /* AnimalKingdomIRCreationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE223C1B271F3288004A0148 /* AnimalKingdomIRCreationTests.swift */; }; DE223C1D271F332D004A0148 /* AllAnimals.graphql in Resources */ = {isa = PBXBuildFile; fileRef = DE2FCF2E26E8092B0057EA67 /* AllAnimals.graphql */; }; - DE223C1E271F332D004A0148 /* AnimalSchema.graphqls in Resources */ = {isa = PBXBuildFile; fileRef = DE2FCF2D26E8092B0057EA67 /* AnimalSchema.graphqls */; }; + DE223C1E271F332D004A0148 /* Schema.graphqls in Resources */ = {isa = PBXBuildFile; fileRef = DE2FCF2D26E8092B0057EA67 /* Schema.graphqls */; }; DE223C1F271F332D004A0148 /* ClassroomPets.graphql in Resources */ = {isa = PBXBuildFile; fileRef = DE2FCF2F26E8092B0057EA67 /* ClassroomPets.graphql */; }; DE223C20271F332D004A0148 /* HeightInMeters.graphql in Resources */ = {isa = PBXBuildFile; fileRef = DE2FCF3226E8092B0057EA67 /* HeightInMeters.graphql */; }; DE223C21271F332D004A0148 /* PetDetails.graphql in Resources */ = {isa = PBXBuildFile; fileRef = DE2FCF3126E8092B0057EA67 /* PetDetails.graphql */; }; @@ -205,6 +205,8 @@ DE296539279B3B8200BF9B49 /* SelectionSetTemplateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE296536279B3B8200BF9B49 /* SelectionSetTemplateTests.swift */; }; DE29653A279B3B8200BF9B49 /* SelectionSetTemplate_RenderFragment_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE296537279B3B8200BF9B49 /* SelectionSetTemplate_RenderFragment_Tests.swift */; }; DE29653B279B3B8200BF9B49 /* SelectionSetTemplate_RenderOperation_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE296538279B3B8200BF9B49 /* SelectionSetTemplate_RenderOperation_Tests.swift */; }; + DE296BA527A07C37004F571F /* MockMergedSelections.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE296BA427A07C37004F571F /* MockMergedSelections.swift */; }; + DE296BA727A09A11004F571F /* IsEverTrue.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE296BA627A09A11004F571F /* IsEverTrue.swift */; }; DE2FCF1D26E806710057EA67 /* SchemaConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE2FCF1C26E806710057EA67 /* SchemaConfiguration.swift */; }; DE2FCF1F26E807CC0057EA67 /* CacheTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE2FCF1E26E807CC0057EA67 /* CacheTransaction.swift */; }; DE2FCF2126E807EF0057EA67 /* Cacheable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE2FCF2026E807EF0057EA67 /* Cacheable.swift */; }; @@ -226,6 +228,8 @@ DE4766E826F92F30004622E0 /* MockSchemaConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE4766E726F92F30004622E0 /* MockSchemaConfiguration.swift */; }; DE4841A92745BBF10001E594 /* LinkedList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE4841A82745BBF10001E594 /* LinkedList.swift */; }; DE4841AA2745EF530001E594 /* ResponsePath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7BA89822927A3700999B3B /* ResponsePath.swift */; }; + DE4D54E727A3504B00D26B68 /* OperationFileGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE4D54E627A3504B00D26B68 /* OperationFileGenerator.swift */; }; + DE4D54E927A3518100D26B68 /* FragmentFileGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE4D54E827A3518100D26B68 /* FragmentFileGenerator.swift */; }; DE56DC232683B2020090D6E4 /* DefaultInterceptorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE56DC222683B2020090D6E4 /* DefaultInterceptorProvider.swift */; }; DE5EB9C026EFCB010004176A /* TestObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE5EB9BF26EFCB010004176A /* TestObserver.swift */; }; DE5EB9C226EFCBD40004176A /* MockApolloStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE5EB9C126EFCBD40004176A /* MockApolloStore.swift */; }; @@ -262,6 +266,24 @@ DEAFB781270647D400BE02F3 /* MockCompilationResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEAFB780270647D400BE02F3 /* MockCompilationResult.swift */; }; DEAFB78327064F6900BE02F3 /* MockGraphQLType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEAFB78227064F6900BE02F3 /* MockGraphQLType.swift */; }; DEAFB787270652D100BE02F3 /* OrderedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = DEAFB786270652D100BE02F3 /* OrderedCollections */; }; + DEC11F3327A359C800E07121 /* ClassroomPets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC11F2127A359C800E07121 /* ClassroomPets.swift */; }; + DEC11F3427A359C800E07121 /* PetDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC11F2227A359C800E07121 /* PetDetails.swift */; }; + DEC11F3527A359C800E07121 /* HeightInMeters.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC11F2327A359C800E07121 /* HeightInMeters.swift */; }; + DEC11F3627A359C800E07121 /* Bird.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC11F2427A359C800E07121 /* Bird.swift */; }; + DEC11F3727A359C800E07121 /* ClassroomPetDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC11F2527A359C800E07121 /* ClassroomPetDetails.swift */; }; + DEC11F3827A359C800E07121 /* Human.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC11F2627A359C800E07121 /* Human.swift */; }; + DEC11F3927A359C800E07121 /* AllAnimalsQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC11F2727A359C800E07121 /* AllAnimalsQuery.swift */; }; + DEC11F3A27A359C800E07121 /* WarmBloodedDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC11F2827A359C800E07121 /* WarmBloodedDetails.swift */; }; + DEC11F3B27A359C800E07121 /* Cat.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC11F2927A359C800E07121 /* Cat.swift */; }; + DEC11F3C27A359C800E07121 /* Animal.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC11F2A27A359C800E07121 /* Animal.swift */; }; + DEC11F3D27A359C800E07121 /* Pet.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC11F2B27A359C800E07121 /* Pet.swift */; }; + DEC11F3E27A359C800E07121 /* Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC11F2C27A359C800E07121 /* Schema.swift */; }; + DEC11F3F27A359C800E07121 /* ClassroomPet.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC11F2D27A359C800E07121 /* ClassroomPet.swift */; }; + DEC11F4027A359C800E07121 /* Height.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC11F2E27A359C800E07121 /* Height.swift */; }; + DEC11F4127A359C800E07121 /* WarmBlooded.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC11F2F27A359C800E07121 /* WarmBlooded.swift */; }; + DEC11F4227A359C800E07121 /* SkinCovering.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC11F3027A359C800E07121 /* SkinCovering.swift */; }; + DEC11F4327A359C800E07121 /* RelativeSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC11F3127A359C800E07121 /* RelativeSize.swift */; }; + DEC11F4427A359C800E07121 /* PetRock.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC11F3227A359C800E07121 /* PetRock.swift */; }; DECD46D0262F64D000924527 /* StarWarsApolloSchemaDownloaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DECD46CF262F64D000924527 /* StarWarsApolloSchemaDownloaderTests.swift */; }; DECD46FB262F659500924527 /* ApolloCodegenLib.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9B7B6F47233C26D100F32205 /* ApolloCodegenLib.framework */; }; DECD4736262F668500924527 /* UploadAPI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9B2DFBB624E1FA0D00ED3AE6 /* UploadAPI.framework */; }; @@ -467,13 +489,6 @@ remoteGlobalIDString = DE058606266978A100265760; remoteInfo = ApolloModels; }; - DE223C25271F337E004A0148 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 9FC7503B1D2A532C00458D91 /* Project object */; - proxyType = 1; - remoteGlobalIDString = DE3C7A00260A6B9800D2F4FF; - remoteInfo = AnimalKingdomAPI; - }; DE3C7A96260A6C1000D2F4FF /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 9FC7503B1D2A532C00458D91 /* Project object */; @@ -855,6 +870,8 @@ DE296536279B3B8200BF9B49 /* SelectionSetTemplateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectionSetTemplateTests.swift; sourceTree = ""; }; DE296537279B3B8200BF9B49 /* SelectionSetTemplate_RenderFragment_Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectionSetTemplate_RenderFragment_Tests.swift; sourceTree = ""; }; DE296538279B3B8200BF9B49 /* SelectionSetTemplate_RenderOperation_Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectionSetTemplate_RenderOperation_Tests.swift; sourceTree = ""; }; + DE296BA427A07C37004F571F /* MockMergedSelections.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockMergedSelections.swift; sourceTree = ""; }; + DE296BA627A09A11004F571F /* IsEverTrue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IsEverTrue.swift; sourceTree = ""; }; DE2FCF1C26E806710057EA67 /* SchemaConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SchemaConfiguration.swift; sourceTree = ""; }; DE2FCF1E26E807CC0057EA67 /* CacheTransaction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CacheTransaction.swift; sourceTree = ""; }; DE2FCF2026E807EF0057EA67 /* Cacheable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cacheable.swift; sourceTree = ""; }; @@ -863,7 +880,7 @@ DE2FCF2526E8083A0057EA67 /* Object.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Object.swift; sourceTree = ""; }; DE2FCF2626E8083A0057EA67 /* Field.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Field.swift; sourceTree = ""; }; DE2FCF2B26E808560057EA67 /* ObjectType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjectType.swift; sourceTree = ""; }; - DE2FCF2D26E8092B0057EA67 /* AnimalSchema.graphqls */ = {isa = PBXFileReference; lastKnownFileType = text; path = AnimalSchema.graphqls; sourceTree = ""; }; + DE2FCF2D26E8092B0057EA67 /* Schema.graphqls */ = {isa = PBXFileReference; lastKnownFileType = text; path = Schema.graphqls; sourceTree = ""; }; DE2FCF2E26E8092B0057EA67 /* AllAnimals.graphql */ = {isa = PBXFileReference; lastKnownFileType = text; path = AllAnimals.graphql; sourceTree = ""; }; DE2FCF2F26E8092B0057EA67 /* ClassroomPets.graphql */ = {isa = PBXFileReference; lastKnownFileType = text; path = ClassroomPets.graphql; sourceTree = ""; }; DE2FCF3026E8092B0057EA67 /* WarmBloodedDetails.graphql */ = {isa = PBXFileReference; lastKnownFileType = text; path = WarmBloodedDetails.graphql; sourceTree = ""; }; @@ -892,6 +909,8 @@ DE46A55726F13AD000357C52 /* ResponseCodeInterceptorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseCodeInterceptorTests.swift; sourceTree = ""; }; DE4766E726F92F30004622E0 /* MockSchemaConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSchemaConfiguration.swift; sourceTree = ""; }; DE4841A82745BBF10001E594 /* LinkedList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkedList.swift; sourceTree = ""; }; + DE4D54E627A3504B00D26B68 /* OperationFileGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationFileGenerator.swift; sourceTree = ""; }; + DE4D54E827A3518100D26B68 /* FragmentFileGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FragmentFileGenerator.swift; sourceTree = ""; }; DE56DC222683B2020090D6E4 /* DefaultInterceptorProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultInterceptorProvider.swift; sourceTree = ""; }; DE5EB9BF26EFCB010004176A /* TestObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestObserver.swift; sourceTree = ""; }; DE5EB9C126EFCBD40004176A /* MockApolloStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockApolloStore.swift; sourceTree = ""; }; @@ -945,6 +964,24 @@ DEAFB77A2706444B00BE02F3 /* IRRootEntityFieldBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IRRootEntityFieldBuilderTests.swift; sourceTree = ""; }; DEAFB780270647D400BE02F3 /* MockCompilationResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCompilationResult.swift; sourceTree = ""; }; DEAFB78227064F6900BE02F3 /* MockGraphQLType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockGraphQLType.swift; sourceTree = ""; }; + DEC11F2127A359C800E07121 /* ClassroomPets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClassroomPets.swift; sourceTree = ""; }; + DEC11F2227A359C800E07121 /* PetDetails.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PetDetails.swift; sourceTree = ""; }; + DEC11F2327A359C800E07121 /* HeightInMeters.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HeightInMeters.swift; sourceTree = ""; }; + DEC11F2427A359C800E07121 /* Bird.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Bird.swift; sourceTree = ""; }; + DEC11F2527A359C800E07121 /* ClassroomPetDetails.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClassroomPetDetails.swift; sourceTree = ""; }; + DEC11F2627A359C800E07121 /* Human.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Human.swift; sourceTree = ""; }; + DEC11F2727A359C800E07121 /* AllAnimalsQuery.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AllAnimalsQuery.swift; sourceTree = ""; }; + DEC11F2827A359C800E07121 /* WarmBloodedDetails.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WarmBloodedDetails.swift; sourceTree = ""; }; + DEC11F2927A359C800E07121 /* Cat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Cat.swift; sourceTree = ""; }; + DEC11F2A27A359C800E07121 /* Animal.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Animal.swift; sourceTree = ""; }; + DEC11F2B27A359C800E07121 /* Pet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Pet.swift; sourceTree = ""; }; + DEC11F2C27A359C800E07121 /* Schema.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Schema.swift; sourceTree = ""; }; + DEC11F2D27A359C800E07121 /* ClassroomPet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClassroomPet.swift; sourceTree = ""; }; + DEC11F2E27A359C800E07121 /* Height.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Height.swift; sourceTree = ""; }; + DEC11F2F27A359C800E07121 /* WarmBlooded.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WarmBlooded.swift; sourceTree = ""; }; + DEC11F3027A359C800E07121 /* SkinCovering.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SkinCovering.swift; sourceTree = ""; }; + DEC11F3127A359C800E07121 /* RelativeSize.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RelativeSize.swift; sourceTree = ""; }; + DEC11F3227A359C800E07121 /* PetRock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PetRock.swift; sourceTree = ""; }; DECD46CF262F64D000924527 /* StarWarsApolloSchemaDownloaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarWarsApolloSchemaDownloaderTests.swift; sourceTree = ""; }; DECD490B262F81BF00924527 /* ApolloCodegenTestSupport.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ApolloCodegenTestSupport.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DECD490D262F81BF00924527 /* ApolloCodegenTestSupport.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ApolloCodegenTestSupport.h; sourceTree = ""; }; @@ -1367,6 +1404,7 @@ 9B455CE82492D0A7002255A9 /* Extensions */, DE4841A82745BBF10001E594 /* LinkedList.swift */, 9F7BA89822927A3700999B3B /* ResponsePath.swift */, + DE296BA627A09A11004F571F /* IsEverTrue.swift */, ); name = ApolloUtils; path = Sources/ApolloUtils; @@ -1969,8 +2007,9 @@ DE3C7AB7260A6D3E00D2F4FF /* graphql */ = { isa = PBXGroup; children = ( + DEC11F2027A359C800E07121 /* API */, DE2FCF2E26E8092B0057EA67 /* AllAnimals.graphql */, - DE2FCF2D26E8092B0057EA67 /* AnimalSchema.graphqls */, + DE2FCF2D26E8092B0057EA67 /* Schema.graphqls */, DE2FCF2F26E8092B0057EA67 /* ClassroomPets.graphql */, DE2FCF3226E8092B0057EA67 /* HeightInMeters.graphql */, DE2FCF3126E8092B0057EA67 /* PetDetails.graphql */, @@ -2088,6 +2127,31 @@ path = IR; sourceTree = ""; }; + DEC11F2027A359C800E07121 /* API */ = { + isa = PBXGroup; + children = ( + DEC11F2127A359C800E07121 /* ClassroomPets.swift */, + DEC11F2227A359C800E07121 /* PetDetails.swift */, + DEC11F2327A359C800E07121 /* HeightInMeters.swift */, + DEC11F2427A359C800E07121 /* Bird.swift */, + DEC11F2527A359C800E07121 /* ClassroomPetDetails.swift */, + DEC11F2627A359C800E07121 /* Human.swift */, + DEC11F2727A359C800E07121 /* AllAnimalsQuery.swift */, + DEC11F2827A359C800E07121 /* WarmBloodedDetails.swift */, + DEC11F2927A359C800E07121 /* Cat.swift */, + DEC11F2A27A359C800E07121 /* Animal.swift */, + DEC11F2B27A359C800E07121 /* Pet.swift */, + DEC11F2C27A359C800E07121 /* Schema.swift */, + DEC11F2D27A359C800E07121 /* ClassroomPet.swift */, + DEC11F2E27A359C800E07121 /* Height.swift */, + DEC11F2F27A359C800E07121 /* WarmBlooded.swift */, + DEC11F3027A359C800E07121 /* SkinCovering.swift */, + DEC11F3127A359C800E07121 /* RelativeSize.swift */, + DEC11F3227A359C800E07121 /* PetRock.swift */, + ); + path = API; + sourceTree = ""; + }; DECD490C262F81BF00924527 /* ApolloCodegenTestSupport */ = { isa = PBXGroup; children = ( @@ -2097,6 +2161,7 @@ E6D79AB926EC05290094434A /* MockNetworkSession.swift */, DE09F9C0270269E800795949 /* MockJavaScriptObject.swift */, DEAFB780270647D400BE02F3 /* MockCompilationResult.swift */, + DE296BA427A07C37004F571F /* MockMergedSelections.swift */, DE12B2D8273C4338003371CC /* MockIRSubscripts.swift */, DE5FD602276926EF0033EE23 /* IR+Mocking.swift */, DEAFB78227064F6900BE02F3 /* MockGraphQLType.swift */, @@ -2203,6 +2268,8 @@ isa = PBXGroup; children = ( E610D8D6278EA2390023E495 /* EnumFileGenerator.swift */, + DE4D54E627A3504B00D26B68 /* OperationFileGenerator.swift */, + DE4D54E827A3518100D26B68 /* FragmentFileGenerator.swift */, E6E3BBDD276A8D6200E5218B /* FileGenerator.swift */, E6D90D06278FA595009CAC5D /* InputObjectFileGenerator.swift */, E610D8DA278EB0900023E495 /* InterfaceFileGenerator.swift */, @@ -2359,7 +2426,6 @@ buildRules = ( ); dependencies = ( - DE223C26271F337E004A0148 /* PBXTargetDependency */, E6E4209426A7DF5800B82624 /* PBXTargetDependency */, 9B683549246348CB00337AE6 /* PBXTargetDependency */, ); @@ -2849,7 +2915,7 @@ buildActionMask = 2147483647; files = ( DE223C1D271F332D004A0148 /* AllAnimals.graphql in Resources */, - DE223C1E271F332D004A0148 /* AnimalSchema.graphqls in Resources */, + DE223C1E271F332D004A0148 /* Schema.graphqls in Resources */, DE223C1F271F332D004A0148 /* ClassroomPets.graphql in Resources */, DE223C20271F332D004A0148 /* HeightInMeters.graphql in Resources */, DE223C21271F332D004A0148 /* PetDetails.graphql in Resources */, @@ -2918,6 +2984,7 @@ buildActionMask = 2147483647; files = ( DE4841A92745BBF10001E594 /* LinkedList.swift in Sources */, + DE296BA727A09A11004F571F /* IsEverTrue.swift in Sources */, DE4841AA2745EF530001E594 /* ResponsePath.swift in Sources */, 9B455CE52492D0A3002255A9 /* ApolloExtension.swift in Sources */, DE223C292720B897004A0148 /* StringInterpolation+NestedIndentation.swift in Sources */, @@ -2941,6 +3008,7 @@ 9F1A966F258F34BB00A06EEB /* JavaScriptBridge.swift in Sources */, 9BAEEBF72346F0A000808306 /* StaticString+Apollo.swift in Sources */, E623FD2A2797A6F4008B4CED /* InterfaceTemplate.swift in Sources */, + DE4D54E727A3504B00D26B68 /* OperationFileGenerator.swift in Sources */, E6D90D07278FA595009CAC5D /* InputObjectFileGenerator.swift in Sources */, 9BCA8C0926618226004FF2F6 /* UntypedGraphQLRequestBodyCreator.swift in Sources */, DE5FD60527694FA70033EE23 /* SchemaTemplate.swift in Sources */, @@ -2963,6 +3031,7 @@ 9BFE8DA9265D5D8F000BBF81 /* URLDownloader.swift in Sources */, E6C9849327929EBE009481BE /* EnumTemplate.swift in Sources */, 9F1A966D258F34BB00A06EEB /* CompilationResult.swift in Sources */, + DE4D54E927A3518100D26B68 /* FragmentFileGenerator.swift in Sources */, E610D8DB278EB0900023E495 /* InterfaceFileGenerator.swift in Sources */, 9B7B6F69233C2C0C00F32205 /* FileManager+Apollo.swift in Sources */, E674DB41274C0A9B009BB90E /* Glob.swift in Sources */, @@ -3252,7 +3321,25 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + DEC11F3C27A359C800E07121 /* Animal.swift in Sources */, DE223C24271F335D004A0148 /* Resources.swift in Sources */, + DEC11F3827A359C800E07121 /* Human.swift in Sources */, + DEC11F4227A359C800E07121 /* SkinCovering.swift in Sources */, + DEC11F3F27A359C800E07121 /* ClassroomPet.swift in Sources */, + DEC11F3B27A359C800E07121 /* Cat.swift in Sources */, + DEC11F3627A359C800E07121 /* Bird.swift in Sources */, + DEC11F3727A359C800E07121 /* ClassroomPetDetails.swift in Sources */, + DEC11F4327A359C800E07121 /* RelativeSize.swift in Sources */, + DEC11F3927A359C800E07121 /* AllAnimalsQuery.swift in Sources */, + DEC11F3E27A359C800E07121 /* Schema.swift in Sources */, + DEC11F3427A359C800E07121 /* PetDetails.swift in Sources */, + DEC11F4427A359C800E07121 /* PetRock.swift in Sources */, + DEC11F3327A359C800E07121 /* ClassroomPets.swift in Sources */, + DEC11F4027A359C800E07121 /* Height.swift in Sources */, + DEC11F4127A359C800E07121 /* WarmBlooded.swift in Sources */, + DEC11F3D27A359C800E07121 /* Pet.swift in Sources */, + DEC11F3A27A359C800E07121 /* WarmBloodedDetails.swift in Sources */, + DEC11F3527A359C800E07121 /* HeightInMeters.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3279,6 +3366,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + DE296BA527A07C37004F571F /* MockMergedSelections.swift in Sources */, DE5FD603276926EF0033EE23 /* IR+Mocking.swift in Sources */, DE5EEC83279796EA00AF5913 /* MockApolloCodegenConfiguration.swift in Sources */, DE363CB327640FBA001EC05B /* GraphQLJSFrontend+TestHelpers.swift in Sources */, @@ -3389,11 +3477,6 @@ target = DE058606266978A100265760 /* ApolloAPI */; targetProxy = DE05862726697B1D00265760 /* PBXContainerItemProxy */; }; - DE223C26271F337E004A0148 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = DE3C7A00260A6B9800D2F4FF /* AnimalKingdomAPI */; - targetProxy = DE223C25271F337E004A0148 /* PBXContainerItemProxy */; - }; DE3C7A97260A6C1000D2F4FF /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 9B68353D2463481A00337AE6 /* ApolloUtils */; diff --git a/Sources/AnimalKingdomAPI/graphql/API/AllAnimalsQuery.swift b/Sources/AnimalKingdomAPI/graphql/API/AllAnimalsQuery.swift new file mode 100644 index 0000000000..3815591d59 --- /dev/null +++ b/Sources/AnimalKingdomAPI/graphql/API/AllAnimalsQuery.swift @@ -0,0 +1,63 @@ +public struct Data: API.SelectionSet { + public let data: ResponseDict + public init(data: ResponseDict) { self.data = data } + + public static var __parentType: ParentType { .Object(API.Query.self) } + public static var selections: [Selection] { [ + .field("allAnimals", [Animal].self), + ] } + + public var allAnimals: [AllAnimal] { data["allAnimals"] } + + public struct AllAnimal: API.SelectionSet { + public let data: ResponseDict + public init(data: ResponseDict) { self.data = data } + + public static var __parentType: ParentType { .Interface(API.Animal.self) } + public static var selections: [Selection] { [ + .field("height", Height.self), + .field("species", String.self), + .field("skinCovering", GraphQLEnum?.self), + .field("predators", [Animal].self), + .typeCase(AsWarmBlooded.self), + .typeCase(AsPet.self), + .typeCase(AsCat.self), + .typeCase(AsClassroomPet.self), + .fragment(HeightInMeters.self), + ] } + + public var height: Height { data["height"] } + public var species: String { data["species"] } + public var skinCovering: GraphQLEnum? { data["skinCovering"] } + public var predators: [Predator] { data["predators"] } + + public struct Height: API.SelectionSet { + public let data: ResponseDict + public init(data: ResponseDict) { self.data = data } + + public static var __parentType: ParentType { .Object(API.Height.self) } + public static var selections: [Selection] { [ + .field("feet", Int.self), + .field("inches", Int.self), + ] } + + public var feet: Int { data["feet"] } + public var inches: Int { data["inches"] } + public var meters: Int { data["meters"] } + + } + public struct Predator: API.SelectionSet { + public let data: ResponseDict + public init(data: ResponseDict) { self.data = data } + + public static var __parentType: ParentType { .Interface(API.Animal.self) } + public static var selections: [Selection] { [ + .field("species", String.self), + .typeCase(AsWarmBlooded.self), + ] } + + public var species: String { data["species"] } + + } + } +} \ No newline at end of file diff --git a/Sources/AnimalKingdomAPI/graphql/API/Animal.swift b/Sources/AnimalKingdomAPI/graphql/API/Animal.swift new file mode 100644 index 0000000000..811af956c4 --- /dev/null +++ b/Sources/AnimalKingdomAPI/graphql/API/Animal.swift @@ -0,0 +1 @@ +public final class Animal: Interface { } \ No newline at end of file diff --git a/Sources/AnimalKingdomAPI/graphql/API/Bird.swift b/Sources/AnimalKingdomAPI/graphql/API/Bird.swift new file mode 100644 index 0000000000..d4be6410db --- /dev/null +++ b/Sources/AnimalKingdomAPI/graphql/API/Bird.swift @@ -0,0 +1 @@ +public class Bird {} \ No newline at end of file diff --git a/Sources/AnimalKingdomAPI/graphql/API/Cat.swift b/Sources/AnimalKingdomAPI/graphql/API/Cat.swift new file mode 100644 index 0000000000..7275ee816a --- /dev/null +++ b/Sources/AnimalKingdomAPI/graphql/API/Cat.swift @@ -0,0 +1 @@ +public class Cat {} \ No newline at end of file diff --git a/Sources/AnimalKingdomAPI/graphql/API/ClassroomPet.swift b/Sources/AnimalKingdomAPI/graphql/API/ClassroomPet.swift new file mode 100644 index 0000000000..54e4998f8a --- /dev/null +++ b/Sources/AnimalKingdomAPI/graphql/API/ClassroomPet.swift @@ -0,0 +1 @@ +public enum ClassroomPet {} \ No newline at end of file diff --git a/Sources/AnimalKingdomAPI/graphql/API/ClassroomPetDetails.swift b/Sources/AnimalKingdomAPI/graphql/API/ClassroomPetDetails.swift new file mode 100644 index 0000000000..58341f1b4d --- /dev/null +++ b/Sources/AnimalKingdomAPI/graphql/API/ClassroomPetDetails.swift @@ -0,0 +1,16 @@ +public struct ClassroomPetDetails: API.SelectionSet, Fragment { + public let data: ResponseDict + public init(data: ResponseDict) { self.data = data } + + public static var __parentType: ParentType { .Union(API.ClassroomPet.self) } + public static var selections: [Selection] { [ + .typeCase(AsAnimal.self), + .typeCase(AsPet.self), + .typeCase(AsWarmBlooded.self), + .typeCase(AsCat.self), + .typeCase(AsBird.self), + .typeCase(AsPetRock.self), + ] } + + +} \ No newline at end of file diff --git a/Sources/AnimalKingdomAPI/graphql/API/ClassroomPets.swift b/Sources/AnimalKingdomAPI/graphql/API/ClassroomPets.swift new file mode 100644 index 0000000000..4fe06b76c7 --- /dev/null +++ b/Sources/AnimalKingdomAPI/graphql/API/ClassroomPets.swift @@ -0,0 +1,22 @@ +public struct Data: API.SelectionSet { + public let data: ResponseDict + public init(data: ResponseDict) { self.data = data } + + public static var __parentType: ParentType { .Object(API.Query.self) } + public static var selections: [Selection] { [ + .field("classroomPets", [ClassroomPet].self), + ] } + + public var classroomPets: [ClassroomPet] { data["classroomPets"] } + + public struct ClassroomPet: API.SelectionSet { + public let data: ResponseDict + public init(data: ResponseDict) { self.data = data } + + public static var __parentType: ParentType { .Union(API.ClassroomPet.self) } + public static var selections: [Selection] { [ + .fragment(ClassroomPetDetails.self), + ] } + + } +} diff --git a/Sources/AnimalKingdomAPI/graphql/API/Height.swift b/Sources/AnimalKingdomAPI/graphql/API/Height.swift new file mode 100644 index 0000000000..aeda968569 --- /dev/null +++ b/Sources/AnimalKingdomAPI/graphql/API/Height.swift @@ -0,0 +1 @@ +public class Height {} \ No newline at end of file diff --git a/Sources/AnimalKingdomAPI/graphql/API/HeightInMeters.swift b/Sources/AnimalKingdomAPI/graphql/API/HeightInMeters.swift new file mode 100644 index 0000000000..9831ba9325 --- /dev/null +++ b/Sources/AnimalKingdomAPI/graphql/API/HeightInMeters.swift @@ -0,0 +1,24 @@ +public struct HeightInMeters: API.SelectionSet, Fragment { + public let data: ResponseDict + public init(data: ResponseDict) { self.data = data } + + public static var __parentType: ParentType { .Interface(API.Animal.self) } + public static var selections: [Selection] { [ + .field("height", Height.self), + ] } + + public var height: Height { data["height"] } + + public struct Height: API.SelectionSet { + public let data: ResponseDict + public init(data: ResponseDict) { self.data = data } + + public static var __parentType: ParentType { .Object(API.Height.self) } + public static var selections: [Selection] { [ + .field("meters", Int.self), + ] } + + public var meters: Int { data["meters"] } + + } +} \ No newline at end of file diff --git a/Sources/AnimalKingdomAPI/graphql/API/Human.swift b/Sources/AnimalKingdomAPI/graphql/API/Human.swift new file mode 100644 index 0000000000..96b204f455 --- /dev/null +++ b/Sources/AnimalKingdomAPI/graphql/API/Human.swift @@ -0,0 +1 @@ +public class Human {} \ No newline at end of file diff --git a/Sources/AnimalKingdomAPI/graphql/API/Pet.swift b/Sources/AnimalKingdomAPI/graphql/API/Pet.swift new file mode 100644 index 0000000000..05d9752b69 --- /dev/null +++ b/Sources/AnimalKingdomAPI/graphql/API/Pet.swift @@ -0,0 +1 @@ +public final class Pet: Interface { } \ No newline at end of file diff --git a/Sources/AnimalKingdomAPI/graphql/API/PetDetails.swift b/Sources/AnimalKingdomAPI/graphql/API/PetDetails.swift new file mode 100644 index 0000000000..8a531d1f6b --- /dev/null +++ b/Sources/AnimalKingdomAPI/graphql/API/PetDetails.swift @@ -0,0 +1,28 @@ +public struct PetDetails: API.SelectionSet, Fragment { + public let data: ResponseDict + public init(data: ResponseDict) { self.data = data } + + public static var __parentType: ParentType { .Interface(API.Pet.self) } + public static var selections: [Selection] { [ + .field("humanName", String?.self), + .field("favoriteToy", String.self), + .field("owner", Human?.self), + ] } + + public var humanName: String? { data["humanName"] } + public var favoriteToy: String { data["favoriteToy"] } + public var owner: Owner? { data["owner"] } + + public struct Owner: API.SelectionSet { + public let data: ResponseDict + public init(data: ResponseDict) { self.data = data } + + public static var __parentType: ParentType { .Object(API.Human.self) } + public static var selections: [Selection] { [ + .field("firstName", String.self), + ] } + + public var firstName: String { data["firstName"] } + + } +} \ No newline at end of file diff --git a/Sources/AnimalKingdomAPI/graphql/API/PetRock.swift b/Sources/AnimalKingdomAPI/graphql/API/PetRock.swift new file mode 100644 index 0000000000..2de93df447 --- /dev/null +++ b/Sources/AnimalKingdomAPI/graphql/API/PetRock.swift @@ -0,0 +1 @@ +public class PetRock {} \ No newline at end of file diff --git a/Sources/AnimalKingdomAPI/graphql/API/RelativeSize.swift b/Sources/AnimalKingdomAPI/graphql/API/RelativeSize.swift new file mode 100644 index 0000000000..d5f148617d --- /dev/null +++ b/Sources/AnimalKingdomAPI/graphql/API/RelativeSize.swift @@ -0,0 +1,5 @@ +public enum RelativeSize: String, EnumType { + case LARGE + case AVERAGE + case SMALL +} \ No newline at end of file diff --git a/Sources/AnimalKingdomAPI/graphql/API/Schema.swift b/Sources/AnimalKingdomAPI/graphql/API/Schema.swift new file mode 100644 index 0000000000..d8dd42a3ba --- /dev/null +++ b/Sources/AnimalKingdomAPI/graphql/API/Schema.swift @@ -0,0 +1,22 @@ +import ApolloAPI + +public typealias ID = String + +public protocol SelectionSet: ApolloAPI.SelectionSet & ApolloAPI.RootSelectionSet +where Schema == API.Schema {} + +public protocol TypeCase: ApolloAPI.SelectionSet & ApolloAPI.TypeCase +where Schema == API.Schema {} + +public enum Schema: SchemaConfiguration { + public static func objectType(forTypename __typename: String) -> Object.Type? { + switch __typename { + case "Height": return API.Height.self + case "Human": return API.Human.self + case "Cat": return API.Cat.self + case "Bird": return API.Bird.self + case "PetRock": return API.PetRock.self + default: return nil + } + } +} \ No newline at end of file diff --git a/Sources/AnimalKingdomAPI/graphql/API/SkinCovering.swift b/Sources/AnimalKingdomAPI/graphql/API/SkinCovering.swift new file mode 100644 index 0000000000..5abfba8744 --- /dev/null +++ b/Sources/AnimalKingdomAPI/graphql/API/SkinCovering.swift @@ -0,0 +1,6 @@ +public enum SkinCovering: String, EnumType { + case FUR + case HAIR + case FEATHERS + case SCALES +} \ No newline at end of file diff --git a/Sources/AnimalKingdomAPI/graphql/API/WarmBlooded.swift b/Sources/AnimalKingdomAPI/graphql/API/WarmBlooded.swift new file mode 100644 index 0000000000..7422691dfe --- /dev/null +++ b/Sources/AnimalKingdomAPI/graphql/API/WarmBlooded.swift @@ -0,0 +1 @@ +public final class WarmBlooded: Interface { } \ No newline at end of file diff --git a/Sources/AnimalKingdomAPI/graphql/API/WarmBloodedDetails.swift b/Sources/AnimalKingdomAPI/graphql/API/WarmBloodedDetails.swift new file mode 100644 index 0000000000..2ffcdb2361 --- /dev/null +++ b/Sources/AnimalKingdomAPI/graphql/API/WarmBloodedDetails.swift @@ -0,0 +1,28 @@ +public struct WarmBloodedDetails: API.SelectionSet, Fragment { + public let data: ResponseDict + public init(data: ResponseDict) { self.data = data } + + public static var __parentType: ParentType { .Interface(API.WarmBlooded.self) } + public static var selections: [Selection] { [ + .field("bodyTemperature", Int.self), + .field("height", Height.self), + ] } + + public var bodyTemperature: Int { data["bodyTemperature"] } + public var height: Height { data["height"] } + + public struct Height: API.SelectionSet { + public let data: ResponseDict + public init(data: ResponseDict) { self.data = data } + + public static var __parentType: ParentType { .Object(API.Height.self) } + public static var selections: [Selection] { [ + .field("meters", Int.self), + .field("yards", Int.self), + ] } + + public var meters: Int { data["meters"] } + public var yards: Int { data["yards"] } + + } +} \ No newline at end of file diff --git a/Sources/AnimalKingdomAPI/graphql/AnimalSchema.graphqls b/Sources/AnimalKingdomAPI/graphql/Schema.graphqls similarity index 100% rename from Sources/AnimalKingdomAPI/graphql/AnimalSchema.graphqls rename to Sources/AnimalKingdomAPI/graphql/Schema.graphqls diff --git a/Sources/ApolloCodegenLib/ApolloCodegen.swift b/Sources/ApolloCodegenLib/ApolloCodegen.swift index 958089fe45..ad7f076a6e 100644 --- a/Sources/ApolloCodegenLib/ApolloCodegen.swift +++ b/Sources/ApolloCodegenLib/ApolloCodegen.swift @@ -51,7 +51,18 @@ public class ApolloCodegen { directoryPath: modulePath ).generateFile() - #warning("TODO - generate operation/fragment files") + for fragment in compilationResult.fragments { + let irFragment = ir.build(fragment: fragment) + try FragmentFileGenerator(fragment: irFragment, schema: ir.schema, directoryPath: modulePath) + .generateFile() + } + + for operation in compilationResult.operations { + let irOperation = ir.build(operation: operation) + try OperationFileGenerator(operation: irOperation, schema: ir.schema, directoryPath: modulePath) + .generateFile() + } + #warning("TODO - generate package manager manifest") } @@ -125,6 +136,7 @@ public class ApolloCodegen { InputObjectFileGenerator(inputObjectType: graphqlInputObjectType, directoryPath: path) }) } + } #endif diff --git a/Sources/ApolloCodegenLib/FileGenerators/FragmentFileGenerator.swift b/Sources/ApolloCodegenLib/FileGenerators/FragmentFileGenerator.swift new file mode 100644 index 0000000000..813e301a40 --- /dev/null +++ b/Sources/ApolloCodegenLib/FileGenerators/FragmentFileGenerator.swift @@ -0,0 +1,29 @@ +import Foundation + +/// Generates a file containing the Swift representation of a GraphQL Fragment. +struct FragmentFileGenerator: FileGenerator { + /// The `IR.NamedFragment` object used to build the file content. + let fragment: IR.NamedFragment + let schema: IR.Schema + let path: String + + var data: Data? { + SelectionSetTemplate(schema: schema) + .render(for: fragment) + .data(using: .utf8) + } + + /// Designated initializer. + /// + /// - Parameters: + /// - fragment: The `IR.NamedFragment` object used to build the file content. + /// - schema: The `IR.Schema` the fragment is belongs to. + /// - directoryPath: The **directory** path that the file should be written to, used to build the `path` property value. + init(fragment: IR.NamedFragment, schema: IR.Schema, directoryPath: String) { + self.fragment = fragment + self.schema = schema + + self.path = URL(fileURLWithPath: directoryPath) + .appendingPathComponent("\(fragment.definition.name).swift").path + } +} diff --git a/Sources/ApolloCodegenLib/FileGenerators/OperationFileGenerator.swift b/Sources/ApolloCodegenLib/FileGenerators/OperationFileGenerator.swift new file mode 100644 index 0000000000..661660c3b3 --- /dev/null +++ b/Sources/ApolloCodegenLib/FileGenerators/OperationFileGenerator.swift @@ -0,0 +1,29 @@ +import Foundation + +/// Generates a file containing the Swift representation of a GraphQL Operation. +struct OperationFileGenerator: FileGenerator { + /// The `IR.Operation` object used to build the file content. + let operation: IR.Operation + let schema: IR.Schema + let path: String + + var data: Data? { + SelectionSetTemplate(schema: schema) + .render(for: operation) + .data(using: .utf8) + } + + /// Designated initializer. + /// + /// - Parameters: + /// - operation: The `IR.Operation` object used to build the file content. + /// - schema: The `IR.Schema` the operation is belongs to. + /// - directoryPath: The **directory** path that the file should be written to, used to build the `path` property value. + init(operation: IR.Operation, schema: IR.Schema, directoryPath: String) { + self.operation = operation + self.schema = schema + + self.path = URL(fileURLWithPath: directoryPath) + .appendingPathComponent("\(operation.definition.name).swift").path + } +} diff --git a/Sources/ApolloCodegenLib/FileManager+Apollo.swift b/Sources/ApolloCodegenLib/FileManager+Apollo.swift index b5e4f1481f..c59df50635 100644 --- a/Sources/ApolloCodegenLib/FileManager+Apollo.swift +++ b/Sources/ApolloCodegenLib/FileManager+Apollo.swift @@ -6,7 +6,7 @@ import ApolloUtils public typealias FileAttributes = [FileAttributeKey : Any] -/// Enables the `.apollo` etension namespace. +/// Enables the `.apollo` extension namespace. extension FileManager: ApolloCompatible {} extension ApolloExtension where Base: FileManager { diff --git a/Sources/ApolloCodegenLib/IR/IR+EntitySelectionTree.swift b/Sources/ApolloCodegenLib/IR/IR+EntitySelectionTree.swift index 5274829631..5393edea60 100644 --- a/Sources/ApolloCodegenLib/IR/IR+EntitySelectionTree.swift +++ b/Sources/ApolloCodegenLib/IR/IR+EntitySelectionTree.swift @@ -30,10 +30,26 @@ extension IR { // MARK: - Merge Selection Sets Into Tree - func mergeIn(selectionSet: SelectionSet) { - guard let directSelections = selectionSet.selections.direct else { return } + func mergeIn( + selectionSet: SelectionSet, + inFragmentSpread fragmentSpread: FragmentSpread? = nil + ) { + let source = MergedSelections.MergedSource( + typeInfo: selectionSet.typeInfo, + fragment: fragmentSpread + ) + mergeIn(selectionSet: selectionSet, from: source) + } + + private func mergeIn(selectionSet: SelectionSet, from source: MergedSelections.MergedSource) { + guard let directSelections = selectionSet.selections.direct, + (!directSelections.fields.isEmpty || !directSelections.fragments.isEmpty) else { + return + } + mergeIn( selections: directSelections, + from: source, atEnclosingEntityScope: selectionSet.typeInfo.typePath.head, withEntityTypePath: selectionSet.typeInfo.typePath.head.value.typePath.head, to: rootNode, @@ -43,7 +59,8 @@ extension IR { } private func mergeIn( - selections: IR.SortedSelections, + selections: DirectSelections, + from source: MergedSelections.MergedSource, atEnclosingEntityScope currentEntityScope: LinkedList.Node, withEntityTypePath currentEntityTypePath: LinkedList.Node, to node: EnclosingEntityNode, @@ -55,6 +72,7 @@ extension IR { let fieldNode = node.childAsFieldScopeNode() mergeIn( selections: selections, + from: source, withTypeScope: currentEntityScope.value.typePath.head, toFieldNode: fieldNode, ofType: currentNodeRootTypePath.value @@ -69,6 +87,7 @@ extension IR { mergeIn( selections: selections, + from: source, atEnclosingEntityScope: nextEntityScope, withEntityTypePath: nextEntityScope.value.typePath.head, to: nextEntityNode, @@ -85,6 +104,7 @@ extension IR { mergeIn( selections: selections, + from: source, atEnclosingEntityScope: currentEntityScope, withEntityTypePath: nextTypePathForCurrentEntity, to: nextNodeForCurrentEntity, @@ -94,7 +114,8 @@ extension IR { } private func mergeIn( - selections: IR.SortedSelections, + selections: DirectSelections, + from source: IR.MergedSelections.MergedSource, withTypeScope currentSelectionScopeTypeCase: LinkedList.Node, toFieldNode node: FieldScopeNode, ofType fieldNodeType: GraphQLCompositeType @@ -103,12 +124,12 @@ extension IR { let typeForSelections = currentSelectionScopeTypeCase.value if fieldNodeType == typeForSelections { - node.mergeIn(selections) + node.mergeIn(selections, from: source) return } else { let fieldTypeCaseNode = node.typeCaseNode(forType: typeForSelections) - fieldTypeCaseNode.mergeIn(selections) + fieldTypeCaseNode.mergeIn(selections, from: source) return } } @@ -118,6 +139,7 @@ extension IR { mergeIn( selections: selections, + from: source, withTypeScope: nextTypeCaseInScope, toFieldNode: nextNodeForField, ofType: nextTypeCaseInScope.value @@ -221,21 +243,24 @@ extension IR { } class FieldScopeNode: EntitySelectionTreeNode { - var selections: ShallowSelections? + var selections: OrderedDictionary = [:] var typeCases: OrderedDictionary? - fileprivate func mergeIn(_ selections: SortedSelections) { - var fieldSelections = self.selections ?? ShallowSelections() - fieldSelections.mergeIn(selections) - self.selections = fieldSelections + fileprivate func mergeIn( + _ selections: DirectSelections, + from source: IR.MergedSelections.MergedSource + ) { + var selectionsFromSource = self.selections[source] ?? EntityTreeScopeSelections() + selectionsFromSource.mergeIn(selections) + self.selections[source] = selectionsFromSource } func mergeSelections( matchingTypePath typePath: LinkedList.Node, into selections: IR.MergedSelections ) { - if let scopeSelections = self.selections { - selections.mergeIn(scopeSelections) + for (source, scopeSelections) in self.selections { + selections.mergeIn(scopeSelections, from: source) } if let typeCases = typeCases { @@ -268,6 +293,46 @@ extension IR { } } } + + struct EntityTreeScopeSelections: Equatable, CustomDebugStringConvertible + { + fileprivate(set) var fields: OrderedDictionary = [:] + fileprivate(set) var fragments: OrderedDictionary = [:] + + init() {} + + var isEmpty: Bool { + fields.isEmpty && fragments.isEmpty + } + + private mutating func mergeIn(_ field: Field) { + fields[field.hashForSelectionSetScope] = field + } + + private mutating func mergeIn(_ fields: T) where T.Element == Field { + fields.forEach { mergeIn($0) } + } + + private mutating func mergeIn(_ fragment: FragmentSpread) { + fragments[fragment.hashForSelectionSetScope] = fragment + } + + private mutating func mergeIn(_ fragments: T) where T.Element == FragmentSpread { + fragments.forEach { mergeIn($0) } + } + + mutating func mergeIn(_ selections: DirectSelections) { + mergeIn(selections.fields.values) + mergeIn(selections.fragments.values) + } + + var debugDescription: String { + """ + Fields: \(fields.values.elements) + Fragments: \(fragments.values.elements.map(\.definition.name)) + """ + } + } } extension IR.EntitySelectionTree: CustomDebugStringConvertible { @@ -297,7 +362,7 @@ extension IR.EntitySelectionTree.FieldScopeNode: CustomDebugStringConvertible { """ { selections: - \(indented: selections?.debugDescription ?? "[]") + \(indented: selections.debugDescription) typeCases: \(indented: typeCases?.debugDescription ?? "[]") } diff --git a/Sources/ApolloCodegenLib/IR/IR+RootFieldBuilder.swift b/Sources/ApolloCodegenLib/IR/IR+RootFieldBuilder.swift index fa435544d6..525b3677c7 100644 --- a/Sources/ApolloCodegenLib/IR/IR+RootFieldBuilder.swift +++ b/Sources/ApolloCodegenLib/IR/IR+RootFieldBuilder.swift @@ -47,6 +47,7 @@ extension IR { buildSortedSelections( forSelectionSet: rootIrSelectionSet, + inFragmentSpread: nil, from: rootSelectionSet ) @@ -58,24 +59,48 @@ extension IR { private func buildSortedSelections( forSelectionSet selectionSet: SelectionSet, + inFragmentSpread fragmentSpread: FragmentSpread?, + from selections: [CompilationResult.Selection] + ) { + buildDirectSelections( + forSelectionSet: selectionSet, + inFragmentSpread: fragmentSpread, + from: selections + ) + + selectionSet.typeInfo.entity.selectionTree.mergeIn( + selectionSet: selectionSet, + inFragmentSpread: fragmentSpread + ) + } + + private func buildDirectSelections( + forSelectionSet selectionSet: SelectionSet, + inFragmentSpread fragmentSpread: FragmentSpread?, from selections: [CompilationResult.Selection] ) { for selection in selections { switch selection { case let .field(field): - let irField = buildField(from: field, on: selectionSet) + let irField = buildField( + from: field, + on: selectionSet, + inFragmentSpread: fragmentSpread + ) selectionSet.selections.direct!.mergeIn(irField) case let .inlineFragment(typeCaseSelectionSet): if selectionSet.typeInfo.typeScope.matches(typeCaseSelectionSet.parentType) { buildSortedSelections( forSelectionSet: selectionSet, + inFragmentSpread: fragmentSpread, from: typeCaseSelectionSet.selections ) } else { let irTypeCase = buildTypeCaseSelectionSet( fromSelectionSet: typeCaseSelectionSet, + inFragmentSpread: fragmentSpread, onParent: selectionSet ) selectionSet.selections.direct!.mergeIn(irTypeCase) @@ -83,11 +108,10 @@ extension IR { case let .fragmentSpread(fragment): if selectionSet.typeInfo.typeScope.matches(fragment.type) { -#warning("TODO: Might be missing referenced fragments for type case nested fragments?") referencedFragments.append(fragment) let irFragmentSpread = buildFragmentSpread( fromFragment: fragment, - onParent: selectionSet + spreadIntoParent: selectionSet ) selectionSet.selections.direct!.mergeIn(irFragmentSpread) @@ -98,6 +122,7 @@ extension IR { parentType: fragment.type, selections: [selection] ), + inFragmentSpread: fragmentSpread, onParent: selectionSet ) @@ -105,16 +130,20 @@ extension IR { } } } - - selectionSet.typeInfo.entity.selectionTree.mergeIn(selectionSet: selectionSet) } private func buildField( from field: CompilationResult.Field, - on selectionSet: SelectionSet + on selectionSet: SelectionSet, + inFragmentSpread fragmentSpread: FragmentSpread? ) -> Field { if field.type.namedType is GraphQLCompositeType { - let irSelectionSet = buildSelectionSet(forField: field, on: selectionSet) + let irSelectionSet = buildSelectionSet( + forField: field, + on: selectionSet, + inFragmentSpread: fragmentSpread + ) + return EntityField(field, selectionSet: irSelectionSet) } else { @@ -124,7 +153,8 @@ extension IR { private func buildSelectionSet( forField field: CompilationResult.Field, - on enclosingSelectionSet: SelectionSet + on enclosingSelectionSet: SelectionSet, + inFragmentSpread fragmentSpread: FragmentSpread? ) -> SelectionSet { guard let fieldSelectionSet = field.selectionSet else { fatalError("SelectionSet cannot be created for non-entity type field \(field).") @@ -143,7 +173,11 @@ extension IR { parentType: fieldSelectionSet.parentType, typePath: typePath ) - buildSortedSelections(forSelectionSet: irSelectionSet, from: fieldSelectionSet.selections) + buildSortedSelections( + forSelectionSet: irSelectionSet, + inFragmentSpread: fragmentSpread, + from: fieldSelectionSet.selections + ) return irSelectionSet } @@ -173,6 +207,7 @@ extension IR { private func buildTypeCaseSelectionSet( fromSelectionSet selectionSet: CompilationResult.SelectionSet, + inFragmentSpread fragmentSpread: FragmentSpread?, onParent parentSelectionSet: SelectionSet ) -> SelectionSet { let typePath = parentSelectionSet.typeInfo.typePath.mutatingLast { @@ -184,24 +219,35 @@ extension IR { parentType: selectionSet.parentType, typePath: typePath ) - buildSortedSelections(forSelectionSet: irSelectionSet, from: selectionSet.selections) + buildSortedSelections( + forSelectionSet: irSelectionSet, + inFragmentSpread: fragmentSpread, + from: selectionSet.selections + ) return irSelectionSet } private func buildFragmentSpread( fromFragment fragment: CompilationResult.FragmentDefinition, - onParent parentSelectionSet: SelectionSet + spreadIntoParent parentSelectionSet: SelectionSet ) -> FragmentSpread { - #warning("TODO! Why are we wrapping in a type case here??") - let irSelectionSet = buildTypeCaseSelectionSet( - fromSelectionSet: fragment.selectionSet, - onParent: parentSelectionSet + let irSelectionSet = SelectionSet( + entity: parentSelectionSet.typeInfo.entity, + parentType: fragment.selectionSet.parentType, + typePath: parentSelectionSet.typeInfo.typePath ) - return FragmentSpread( + let fragmentSpread = FragmentSpread( definition: fragment, selectionSet: irSelectionSet ) + buildSortedSelections( + forSelectionSet: fragmentSpread.selectionSet, + inFragmentSpread: fragmentSpread, + from: fragment.selectionSet.selections + ) + + return fragmentSpread } } } diff --git a/Sources/ApolloCodegenLib/IR/IR+SelectionSet.swift b/Sources/ApolloCodegenLib/IR/IR+SelectionSet.swift index 9962c9c12d..88b3581b7f 100644 --- a/Sources/ApolloCodegenLib/IR/IR+SelectionSet.swift +++ b/Sources/ApolloCodegenLib/IR/IR+SelectionSet.swift @@ -3,7 +3,7 @@ import ApolloUtils extension IR { @dynamicMemberLookup class SelectionSet: Equatable, CustomDebugStringConvertible { - class TypeInfo { + class TypeInfo: Hashable, CustomDebugStringConvertible { /// The entity that the `selections` are being selected on. /// /// Multiple `SelectionSet`s may reference the same `Entity` @@ -34,6 +34,22 @@ extension IR { self.parentType = parentType self.typePath = typePath } + + static func == (lhs: TypeInfo, rhs: TypeInfo) -> Bool { + lhs.entity === rhs.entity && + lhs.parentType == rhs.parentType && + lhs.typePath == rhs.typePath + } + + func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(entity)) + hasher.combine(parentType) + hasher.combine(typePath) + } + + var debugDescription: String { + typePath.debugDescription + } } class Selections: CustomDebugStringConvertible { diff --git a/Sources/ApolloCodegenLib/IR/IR.swift b/Sources/ApolloCodegenLib/IR/IR.swift index 5de22c9f18..b5d5bbac93 100644 --- a/Sources/ApolloCodegenLib/IR/IR.swift +++ b/Sources/ApolloCodegenLib/IR/IR.swift @@ -86,7 +86,7 @@ class IR { /// /// While a `NamedFragment` can be shared between operations, a `FragmentSpread` represents a /// `NamedFragment` included in a specific operation. - class FragmentSpread: Equatable { + class FragmentSpread: Hashable { let definition: CompilationResult.FragmentDefinition /// The selection set for the fragment in the operation it has been "spread into". /// It's `typePath` and `entity` reference are scoped to the operation it belongs to. @@ -104,6 +104,11 @@ class IR { lhs.definition == rhs.definition && lhs.selectionSet == rhs.selectionSet } + + func hash(into hasher: inout Hasher) { + hasher.combine(definition) + hasher.combine(ObjectIdentifier(selectionSet)) + } } } diff --git a/Sources/ApolloCodegenLib/IR/SortedSelections.swift b/Sources/ApolloCodegenLib/IR/SortedSelections.swift index bca99054e9..d78beb9b6f 100644 --- a/Sources/ApolloCodegenLib/IR/SortedSelections.swift +++ b/Sources/ApolloCodegenLib/IR/SortedSelections.swift @@ -3,22 +3,19 @@ import OrderedCollections import ApolloUtils extension IR { - class SortedSelections: Equatable, CustomDebugStringConvertible { - - typealias Field = IR.Field + class DirectSelections: Equatable, CustomDebugStringConvertible { typealias TypeCase = IR.SelectionSet - typealias Fragment = IR.FragmentSpread fileprivate(set) var fields: OrderedDictionary = [:] fileprivate(set) var typeCases: OrderedDictionary = [:] - fileprivate(set) var fragments: OrderedDictionary = [:] + fileprivate(set) var fragments: OrderedDictionary = [:] init() {} init( fields: [Field] = [], typeCases: [TypeCase] = [], - fragments: [Fragment] = [] + fragments: [FragmentSpread] = [] ) { mergeIn(fields) mergeIn(typeCases) @@ -28,29 +25,47 @@ extension IR { init( fields: OrderedDictionary = [:], typeCases: OrderedDictionary = [:], - fragments: OrderedDictionary = [:] + fragments: OrderedDictionary = [:] ) { mergeIn(fields.values) mergeIn(typeCases.values) mergeIn(fragments.values) } - var isEmpty: Bool { - fields.isEmpty && typeCases.isEmpty && fragments.isEmpty + func mergeIn(_ selections: DirectSelections) { + mergeIn(selections.fields.values) + mergeIn(selections.typeCases.values) + mergeIn(selections.fragments.values) } - // MARK: Merge In - func mergeIn(_ field: Field) { - fatalError("Must be overridden by subclasses!") + let keyInScope = field.hashForSelectionSetScope + + if let existingField = fields[keyInScope] as? EntityField { + if let field = field as? EntityField { + existingField.selectionSet.selections.direct! + .mergeIn(field.selectionSet.selections.direct!) + } + + } else { + fields[keyInScope] = field + } } func mergeIn(_ typeCase: TypeCase) { - fatalError("Must be overridden by subclasses!") + let keyInScope = typeCase.hashForSelectionSetScope + + if let existingTypeCase = typeCases[keyInScope] { + existingTypeCase.selections.direct! + .mergeIn(typeCase.selections.direct!) + + } else { + typeCases[keyInScope] = typeCase + } } - func mergeIn(_ fragment: Fragment) { - fatalError("Must be overridden by subclasses!") + func mergeIn(_ fragment: FragmentSpread) { + fragments[fragment.hashForSelectionSetScope] = fragment } func mergeIn(_ fields: T) where T.Element == Field { @@ -61,18 +76,15 @@ extension IR { typeCases.forEach { mergeIn($0) } } - - func mergeIn(_ fragments: T) where T.Element == Fragment { + func mergeIn(_ fragments: T) where T.Element == FragmentSpread { fragments.forEach { mergeIn($0) } } - func mergeIn(_ selections: SortedSelections) { - mergeIn(selections.fields.values) - mergeIn(selections.typeCases.values) - mergeIn(selections.fragments.values) + var isEmpty: Bool { + fields.isEmpty && typeCases.isEmpty && fragments.isEmpty } - static func == (lhs: IR.SortedSelections, rhs: IR.SortedSelections) -> Bool { + static func == (lhs: DirectSelections, rhs: DirectSelections) -> Bool { lhs.fields == rhs.fields && lhs.typeCases == rhs.typeCases && lhs.fragments == rhs.fragments @@ -91,53 +103,32 @@ extension IR { } struct ReadOnly { - fileprivate let value: SortedSelections + fileprivate let value: DirectSelections var fields: OrderedDictionary { value.fields } var typeCases: OrderedDictionary { value.typeCases } - var fragments: OrderedDictionary { value.fragments } - } - } - - - class DirectSelections: SortedSelections { - - override func mergeIn(_ field: Field) { - let keyInScope = field.hashForSelectionSetScope - - if let existingField = fields[keyInScope] as? EntityField { - if let field = field as? EntityField { - existingField.selectionSet.selections.direct! - .mergeIn(field.selectionSet.selections.direct!) - } - - } else { - fields[keyInScope] = field - } + var fragments: OrderedDictionary { value.fragments } } - override func mergeIn(_ typeCase: TypeCase) { - let keyInScope = typeCase.hashForSelectionSetScope + } - if let existingTypeCase = typeCases[keyInScope] { - existingTypeCase.selections.direct! - .mergeIn(typeCase.selections.direct!) + class MergedSelections: Equatable, CustomDebugStringConvertible { - } else { - typeCases[keyInScope] = typeCase - } - } + typealias TypeCase = IR.SelectionSet - override func mergeIn(_ fragment: Fragment) { - fragments[fragment.hashForSelectionSetScope] = fragment + struct MergedSource: Hashable { + let typeInfo: SelectionSet.TypeInfo + unowned let fragment: FragmentSpread? } - } - - class MergedSelections: SortedSelections { + typealias MergedSources = Set let directSelections: DirectSelections.ReadOnly? let typeInfo: SelectionSet.TypeInfo + fileprivate(set) var mergedSources: MergedSources = [] + fileprivate(set) var fields: OrderedDictionary = [:] + fileprivate(set) var typeCases: OrderedDictionary = [:] + fileprivate(set) var fragments: OrderedDictionary = [:] init( directSelections: DirectSelections.ReadOnly?, @@ -145,19 +136,24 @@ extension IR { ) { self.directSelections = directSelections self.typeInfo = typeInfo - super.init() } - func mergeIn(_ selections: IR.ShallowSelections) { - selections.fields.values.forEach { self.mergeIn($0) } - selections.fragments.values.forEach { self.mergeIn($0) } + func mergeIn(_ selections: EntityTreeScopeSelections, from source: MergedSource) { + @IsEverTrue var didMergeAnySelections: Bool + + selections.fields.values.forEach { didMergeAnySelections = self.mergeIn($0) } + selections.fragments.values.forEach { didMergeAnySelections = self.mergeIn($0) } + + if didMergeAnySelections { + mergedSources.insert(source) + } } - override func mergeIn(_ field: IR.Field) { + private func mergeIn(_ field: IR.Field) -> Bool { let keyInScope = field.hashForSelectionSetScope if let directSelections = directSelections, directSelections.fields.keys.contains(keyInScope) { - return + return false } let fieldToMerge: IR.Field @@ -169,6 +165,7 @@ extension IR { } fields[keyInScope] = fieldToMerge + return true } private func createShallowlyMergedNestedEntityField(from field: IR.EntityField) -> IR.EntityField { @@ -181,14 +178,16 @@ extension IR { return IR.EntityField(field.underlyingField, selectionSet: newSelectionSet) } - override func mergeIn(_ fragment: IR.FragmentSpread) { + private func mergeIn(_ fragment: IR.FragmentSpread) -> Bool { let keyInScope = fragment.hashForSelectionSetScope if let directSelections = directSelections, directSelections.fragments.keys.contains(keyInScope) { - return + return false } fragments[keyInScope] = fragment + + return true } func addMergedTypeCase(withType type: GraphQLCompositeType) { @@ -214,52 +213,25 @@ extension IR { ) } - } - -} - -extension IR { - struct ShallowSelections: Equatable, CustomDebugStringConvertible - { - typealias Field = IR.Field - typealias Fragment = IR.FragmentSpread - - fileprivate(set) var fields: OrderedDictionary = [:] - fileprivate(set) var fragments: OrderedDictionary = [:] - - init() {} - var isEmpty: Bool { - fields.isEmpty && fragments.isEmpty - } - - mutating func mergeIn(_ field: Field) { - fields[field.hashForSelectionSetScope] = field - } - - mutating func mergeIn(_ fields: T) where T.Element == Field { - fields.forEach { mergeIn($0) } - } - - mutating func mergeIn(_ fragment: Fragment) { - fragments[fragment.hashForSelectionSetScope] = fragment - } - - mutating func mergeIn(_ fragments: T) where T.Element == Fragment { - fragments.forEach { mergeIn($0) } + fields.isEmpty && typeCases.isEmpty && fragments.isEmpty } - mutating func mergeIn(_ selections: SortedSelections) { - mergeIn(selections.fields.values) - mergeIn(selections.fragments.values) + static func == (lhs: MergedSelections, rhs: MergedSelections) -> Bool { + lhs.mergedSources == rhs.mergedSources && + lhs.fields == rhs.fields && + lhs.typeCases == rhs.typeCases && + lhs.fragments == rhs.fragments } var debugDescription: String { """ + Merged Sources: \(mergedSources) Fields: \(fields.values.elements) Fragments: \(fragments.values.elements.map(\.definition.name)) """ } + } } diff --git a/Sources/ApolloCodegenLib/IR/TypeScopeDescriptor.swift b/Sources/ApolloCodegenLib/IR/TypeScopeDescriptor.swift index 607dcaa62c..3a296c5760 100644 --- a/Sources/ApolloCodegenLib/IR/TypeScopeDescriptor.swift +++ b/Sources/ApolloCodegenLib/IR/TypeScopeDescriptor.swift @@ -5,7 +5,7 @@ typealias TypeScope = Set /// Defines the scope for an `IR.SelectionSet`. The "scope" indicates where in the operation the /// selection set is located and what types the `SelectionSet` implements. -struct TypeScopeDescriptor: Equatable { +struct TypeScopeDescriptor: Hashable { /// A list of the parent types for the selection set and it's parents on the same entity. /// /// The last element in the list is equal to the parent type for the `SelectionSet` @@ -120,6 +120,11 @@ struct TypeScopeDescriptor: Equatable { lhs.matchingTypes == rhs.matchingTypes } + func hash(into hasher: inout Hasher) { + hasher.combine(typePath) + hasher.combine(matchingTypes) + } + } extension TypeScopeDescriptor: CustomDebugStringConvertible { diff --git a/Sources/ApolloCodegenLib/Templates/SelectionSetTemplate.swift b/Sources/ApolloCodegenLib/Templates/SelectionSetTemplate.swift index 479053e84a..90fc3148b1 100644 --- a/Sources/ApolloCodegenLib/Templates/SelectionSetTemplate.swift +++ b/Sources/ApolloCodegenLib/Templates/SelectionSetTemplate.swift @@ -1,3 +1,6 @@ +import ApolloUtils +import InflectorKit + struct SelectionSetTemplate { let schema: IR.Schema @@ -6,7 +9,7 @@ struct SelectionSetTemplate { TemplateString( """ public struct Data: \(schema.name).SelectionSet { - \(BodyTemplate(operation.rootField)) + \(BodyTemplate(operation.rootField.selectionSet)) } """ ).description @@ -16,34 +19,52 @@ struct SelectionSetTemplate { TemplateString( """ public struct \(fragment.name): \(schema.name).SelectionSet, Fragment { - \(BodyTemplate(fragment.rootField)) + \(BodyTemplate(fragment.rootField.selectionSet)) } """ ).description } func render(field: IR.EntityField) -> String { + TemplateString( + """ + public struct \(field.formattedFieldName): \(schema.name).SelectionSet { + \(BodyTemplate(field.selectionSet)) + } + """ + ).description + } + + func render(typeCase: IR.SelectionSet) -> String { TemplateString( """ public struct TODO: \(schema.name).SelectionSet { - \(BodyTemplate(field)) + \(BodyTemplate(typeCase)) } """ ).description } - private func BodyTemplate(_ field: IR.EntityField) -> TemplateString { + private func BodyTemplate(_ selectionSet: IR.SelectionSet) -> TemplateString { """ \(Self.DataFieldAndInitializerTemplate) - \(ParentTypeTemplate(field.selectionSet.parentType)) - \(ifLet: field.selectionSet.selections.direct, { SelectionsTemplate($0) }, else: "\n") + \(ParentTypeTemplate(selectionSet.parentType)) + \(ifLet: selectionSet.selections.direct, { SelectionsTemplate($0) }, else: "\n") - \(ifLet: field.selectionSet.selections.direct?.fields.values, + \(ifLet: selectionSet.selections.direct?.fields.values, where: { !$0.isEmpty }, { "\($0.map { FieldAccessorTemplate($0) }, separator: "\n")" - }, - else: "\n") + }) + \(if: !selectionSet.selections.merged.fields.values.isEmpty, """ + \(selectionSet.selections.merged.fields.values.map { FieldAccessorTemplate($0) }, + separator: "\n") + """) + + \(ifLet: selectionSet.selections.direct?.fields.values.compactMap { $0 as? IR.EntityField }, + where: { !$0.isEmpty }, { + "\($0.map { render(field: $0) }, separator: "\n")" + }) """ } @@ -91,9 +112,24 @@ struct SelectionSetTemplate { } private func FieldAccessorTemplate(_ field: IR.Field) -> TemplateString { - """ - public var \(field.responseKey): \(field.type.rendered) { data["\(field.responseKey)"] } - """ + func template(withType type: String) -> TemplateString { + """ + public var \(field.responseKey): \(type) { data["\(field.responseKey)"] } + """ + } + + let type: String + switch field { + case let scalarField as IR.ScalarField: + type = scalarField.type.rendered + + case let entityField as IR.EntityField: + type = entityField.generatedSelectionSetType + + default: + fatalError() + } + return template(withType: type) } } @@ -114,26 +150,115 @@ fileprivate extension GraphQLType { rendered(containedInNonNull: false) } - private func rendered(containedInNonNull: Bool) -> String { + func rendered(replacingNamedTypeWith newTypeName: String) -> String { + rendered(containedInNonNull: false, replacingNamedTypeWith: newTypeName) + } + + private func rendered( + containedInNonNull: Bool, + replacingNamedTypeWith newTypeName: String? = nil + ) -> String { switch self { case let .entity(type as GraphQLNamedType), let .scalar(type as GraphQLNamedType), let .inputObject(type as GraphQLNamedType): - return containedInNonNull ? type.name : "\(type.name)?" + let typeName = newTypeName ?? type.name + + return containedInNonNull ? typeName : "\(typeName)?" case let .enum(type as GraphQLNamedType): - let enumType = "GraphQLEnum<\(type.name)>" + let typeName = newTypeName ?? type.name + let enumType = "GraphQLEnum<\(typeName)>" return containedInNonNull ? enumType : "\(enumType)?" case let .nonNull(ofType): - return ofType.rendered(containedInNonNull: true) + return ofType.rendered(containedInNonNull: true, replacingNamedTypeWith: newTypeName) case let .list(ofType): - let inner = "[\(ofType.rendered(containedInNonNull: false))]" + let inner = "[\(ofType.rendered(containedInNonNull: false, replacingNamedTypeWith: newTypeName))]" return containedInNonNull ? inner : "\(inner)?" } } } + +fileprivate extension IR.EntityField { + + var formattedFieldName: String { + return StringInflector.default.singularize(responseKey.firstUppercased) + } + + var generatedSelectionSetName: String { + if selectionSet.selections.direct != nil { + return formattedFieldName + } + + if selectionSet.selections.merged.mergedSources.count == 1 { + return selectionSet.selections.merged.mergedSources + .first.unsafelyUnwrapped + .generatedSelectionSetName + } + + return formattedFieldName + } + + var generatedSelectionSetType: String { + return self.type.rendered(replacingNamedTypeWith: generatedSelectionSetName) + } + +} + +fileprivate extension IR.MergedSelections.MergedSource { + var generatedSelectionSetName: String { + guard let fragmentSource = fragment else { + return typeInfo.debugDescription + } + + var fragmentTypePathCurrentNode = fragmentSource.selectionSet.typeInfo.typePath.head + var sourceTypePathCurrentNode = typeInfo.typePath.head + var nodesToFragment = 1 + + while let nextNode = fragmentTypePathCurrentNode.next { + fragmentTypePathCurrentNode = nextNode + sourceTypePathCurrentNode = sourceTypePathCurrentNode.next! + nodesToFragment += 1 + } + + let fieldPath = Array(typeInfo.entity.fieldPath.toArray().suffix(from: nodesToFragment)) + let selectionSetName = generatedSelectionSetName( + from: sourceTypePathCurrentNode.next!, + withFieldPath: fieldPath + ) + + return "\(fragmentSource.definition.name).\(selectionSetName)" + } + + private func generatedSelectionSetName( + from typePathNode: LinkedList.Node, + withFieldPath fieldPath: [String] + ) -> String { + var currentNode: LinkedList.Node? = typePathNode + var fieldPathIndex = 0 + + var components: [String] = [] + + repeat { + let fieldName = fieldPath[fieldPathIndex] + components.append(StringInflector.default.singularize(fieldName.firstUppercased)) + + var currentTypeScopeNode = currentNode.unsafelyUnwrapped.value.typePath.head + while let typeCaseNode = currentTypeScopeNode.next { + components.append("As\(typeCaseNode.value.name.firstUppercased)") + currentTypeScopeNode = typeCaseNode + } + + fieldPathIndex += 1 + currentNode = currentNode.unsafelyUnwrapped.next + } while currentNode !== nil + + return components.joined(separator: ".") + } + +} diff --git a/Sources/ApolloCodegenTestSupport/MockIRSubscripts.swift b/Sources/ApolloCodegenTestSupport/MockIRSubscripts.swift index 6c690cfd59..2dc4c836ec 100644 --- a/Sources/ApolloCodegenTestSupport/MockIRSubscripts.swift +++ b/Sources/ApolloCodegenTestSupport/MockIRSubscripts.swift @@ -1,6 +1,6 @@ @testable import ApolloCodegenLib -extension IR.SortedSelections { +extension IR.DirectSelections { public subscript(field field: String) -> IR.Field? { fields[field] } @@ -14,7 +14,22 @@ extension IR.SortedSelections { } } -extension IR.ShallowSelections { +extension IR.MergedSelections { + public subscript(field field: String) -> IR.Field? { + fields[field] + } + + public subscript(as typeCase: String) -> IR.SelectionSet? { + typeCases[typeCase] + } + + public subscript(fragment fragment: String) -> IR.FragmentSpread? { + fragments[fragment] + } +} + + +extension IR.EntityTreeScopeSelections { public subscript(field field: String) -> IR.Field? { fields[field] } diff --git a/Sources/ApolloCodegenTestSupport/MockMergedSelections.swift b/Sources/ApolloCodegenTestSupport/MockMergedSelections.swift new file mode 100644 index 0000000000..3d1a47452f --- /dev/null +++ b/Sources/ApolloCodegenTestSupport/MockMergedSelections.swift @@ -0,0 +1,29 @@ +import Foundation +@testable import ApolloCodegenLib +import XCTest + +extension IR.MergedSelections.MergedSource { + + public static func mock(_ field: IR.Field?) throws -> Self { + self.init(typeInfo: try XCTUnwrap(field?.selectionSet?.typeInfo), fragment: nil) + } + + public static func mock(_ typeCase: IR.SelectionSet?) throws -> Self { + self.init(typeInfo: try XCTUnwrap(typeCase?.typeInfo), fragment: nil) + } + + public static func mock(_ fragment: IR.FragmentSpread?) throws -> Self { + let fragment = try XCTUnwrap(fragment) + return self.init( + typeInfo: fragment.selectionSet.typeInfo, + fragment: fragment + ) + } + + public static func mock(for field: IR.Field?, from fragment: IR.FragmentSpread?) throws -> Self { + self.init( + typeInfo: try XCTUnwrap(field?.selectionSet?.typeInfo), + fragment: try XCTUnwrap(fragment) + ) + } +} diff --git a/Sources/ApolloUtils/IsEverTrue.swift b/Sources/ApolloUtils/IsEverTrue.swift new file mode 100644 index 0000000000..968cdb2490 --- /dev/null +++ b/Sources/ApolloUtils/IsEverTrue.swift @@ -0,0 +1,13 @@ +/// A property wrapper that indicates if a `Bool` value was ever set to `true`. +/// Defaults to `false`, if ever set to `true`, it will always be `true`. +@propertyWrapper +public struct IsEverTrue { + private var _wrappedValue: Bool = false + + public var wrappedValue: Bool { + get { _wrappedValue } + set { if newValue { _wrappedValue = true } } + } + + public init() {} +} diff --git a/Sources/ApolloUtils/ResponsePath.swift b/Sources/ApolloUtils/ResponsePath.swift index d3e356b21c..dc870c586d 100644 --- a/Sources/ApolloUtils/ResponsePath.swift +++ b/Sources/ApolloUtils/ResponsePath.swift @@ -25,6 +25,16 @@ public struct ResponsePath: ExpressibleByArrayLiteral { return key } }() + + lazy var components: [String] = { + if let previous = previous { + var components = previous.components + components.append(key) + return components + } else { + return [key] + } + }() } private var head: Node? @@ -32,6 +42,10 @@ public struct ResponsePath: ExpressibleByArrayLiteral { return head?.joined ?? "" } + public func toArray() -> [String] { + return head?.components ?? [] + } + public init(arrayLiteral segments: Key...) { for segment in segments { append(segment) diff --git a/SwiftScripts/.swiftpm/xcode/xcshareddata/xcschemes/Codegen-Package.xcscheme b/SwiftScripts/.swiftpm/xcode/xcshareddata/xcschemes/Codegen-Package.xcscheme new file mode 100644 index 0000000000..ff4e82c43c --- /dev/null +++ b/SwiftScripts/.swiftpm/xcode/xcshareddata/xcschemes/Codegen-Package.xcscheme @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SwiftScripts/.swiftpm/xcode/xcshareddata/xcschemes/Codegen.xcscheme b/SwiftScripts/.swiftpm/xcode/xcshareddata/xcschemes/Codegen.xcscheme new file mode 100644 index 0000000000..699e31688a --- /dev/null +++ b/SwiftScripts/.swiftpm/xcode/xcshareddata/xcschemes/Codegen.xcscheme @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SwiftScripts/.swiftpm/xcode/xcshareddata/xcschemes/SchemaDownload.xcscheme b/SwiftScripts/.swiftpm/xcode/xcshareddata/xcschemes/SchemaDownload.xcscheme new file mode 100644 index 0000000000..a5a89e75c5 --- /dev/null +++ b/SwiftScripts/.swiftpm/xcode/xcshareddata/xcschemes/SchemaDownload.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SwiftScripts/Package.resolved b/SwiftScripts/Package.resolved index 86d54b0237..becdbea359 100644 --- a/SwiftScripts/Package.resolved +++ b/SwiftScripts/Package.resolved @@ -1,15 +1,6 @@ { "object": { "pins": [ - { - "package": "Commandant", - "repositoryURL": "https://github.com/Carthage/Commandant.git", - "state": { - "branch": null, - "revision": "ab68611013dec67413628ac87c1f29e8427bc8e4", - "version": "0.17.0" - } - }, { "package": "InflectorKit", "repositoryURL": "https://github.com/mattt/InflectorKit", @@ -28,15 +19,6 @@ "version": "0.5.0" } }, - { - "package": "Nimble", - "repositoryURL": "https://github.com/Quick/Nimble.git", - "state": { - "branch": null, - "revision": "72f5a90d573f7f7d70aa6b8ad84b3e1e02eabb4d", - "version": "8.0.9" - } - }, { "package": "ProcessRunner", "repositoryURL": "https://github.com/eneko/ProcessRunner.git", @@ -46,22 +28,13 @@ "version": "1.1.0" } }, - { - "package": "Quick", - "repositoryURL": "https://github.com/Quick/Quick.git", - "state": { - "branch": null, - "revision": "09b3becb37cb2163919a3842a4c5fa6ec7130792", - "version": "2.2.1" - } - }, { "package": "Rainbow", "repositoryURL": "https://github.com/onevcat/Rainbow", "state": { "branch": null, - "revision": "9c52c1952e9b2305d4507cf473392ac2d7c9b155", - "version": "3.1.5" + "revision": "626c3d4b6b55354b4af3aa309f998fae9b31a3d9", + "version": "3.2.0" } }, { @@ -69,8 +42,8 @@ "repositoryURL": "https://github.com/eneko/SourceDocs.git", "state": { "branch": null, - "revision": "6c6443434ef04328dc18ac71c73e7a2ec0d531d1", - "version": "1.2.1" + "revision": "3a38adfe18ca73dfc2136b4b8c49407d1a812d50", + "version": "2.0.0" } }, { @@ -78,8 +51,8 @@ "repositoryURL": "https://github.com/jpsim/SourceKitten.git", "state": { "branch": null, - "revision": "77a4dbbb477a8110eb8765e3c44c70fb4929098f", - "version": "0.29.0" + "revision": "558628392eb31d37cb251cfe626c53eafd330df6", + "version": "0.31.1" } }, { @@ -87,8 +60,8 @@ "repositoryURL": "https://github.com/stephencelis/SQLite.swift.git", "state": { "branch": null, - "revision": "60a65015f6402b7c34b9a924f755ca0a73afeeaa", - "version": "0.13.1" + "revision": "5f5ad81ac0d0a0f3e56e39e646e8423c617df523", + "version": "0.13.2" } }, { @@ -96,8 +69,8 @@ "repositoryURL": "https://github.com/apple/swift-argument-parser.git", "state": { "branch": null, - "revision": "92646c0cdbaca076c8d3d0207891785b3379cbff", - "version": "0.3.1" + "revision": "e1465042f195f374b94f915ba8ca49de24300a0d", + "version": "1.0.2" } }, { @@ -105,26 +78,8 @@ "repositoryURL": "https://github.com/apple/swift-collections", "state": { "branch": null, - "revision": "2d33a0ea89c961dcb2b3da2157963d9c0370347e", - "version": "1.0.1" - } - }, - { - "package": "llbuild", - "repositoryURL": "https://github.com/apple/swift-llbuild.git", - "state": { - "branch": null, - "revision": "f1c9ad9a253cdf1aa89a7f5c99c30b4513b06ddb", - "version": "0.1.1" - } - }, - { - "package": "SwiftPM", - "repositoryURL": "https://github.com/apple/swift-package-manager", - "state": { - "branch": null, - "revision": "8656a25cb906c1896339f950ac960ee1b4fe8034", - "version": "0.4.0" + "revision": "48254824bb4248676bf7ce56014ff57b142b77eb", + "version": "1.0.2" } }, { @@ -132,8 +87,8 @@ "repositoryURL": "https://github.com/drmohundro/SWXMLHash.git", "state": { "branch": null, - "revision": "a4931e5c3bafbedeb1601d3bb76bbe835c6d475a", - "version": "5.0.1" + "revision": "9183170d20857753d4f331b0ca63f73c60764bf3", + "version": "5.0.2" } }, { @@ -141,8 +96,8 @@ "repositoryURL": "https://github.com/jpsim/Yams.git", "state": { "branch": null, - "revision": "c947a306d2e80ecb2c0859047b35c73b8e1ca27f", - "version": "2.0.0" + "revision": "9ff1cc9327586db4e0c8f46f064b6a82ec1566fa", + "version": "4.0.6" } } ] diff --git a/SwiftScripts/Package.swift b/SwiftScripts/Package.swift index 1adc36751d..586c89fc3c 100644 --- a/SwiftScripts/Package.swift +++ b/SwiftScripts/Package.swift @@ -6,12 +6,12 @@ import PackageDescription let package = Package( name: "Codegen", platforms: [ - .macOS(.v10_14) + .macOS(.v10_15) ], dependencies: [ .package(name: "Apollo", path: ".."), - .package(url: "https://github.com/apple/swift-argument-parser.git", from: "0.3.0"), - .package(url: "https://github.com/eneko/SourceDocs.git", .upToNextMinor(from: "1.2.0")) + .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.0.2"), + .package(url: "https://github.com/eneko/SourceDocs.git", .upToNextMinor(from: "2.0.0")) ], targets: [ .target(name: "Codegen", diff --git a/SwiftScripts/Sources/Codegen/Target.swift b/SwiftScripts/Sources/Codegen/Target.swift index 4f07747398..bc1472ce63 100644 --- a/SwiftScripts/Sources/Codegen/Target.swift +++ b/SwiftScripts/Sources/Codegen/Target.swift @@ -5,89 +5,89 @@ import ArgumentParser #warning("TODO: Ensure this script keeps up with the changes to codegen!") enum Target { - case starWars - case gitHub - case upload - case animalKingdom - - init?(name: String) { - switch name { - case "StarWars": - self = .starWars - case "GitHub": - self = .gitHub - case "Upload": - self = .upload - case "AnimalKingdom": - self = .animalKingdom - default: - return nil - } - } - - func targetRootURL(fromSourceRoot sourceRootURL: Foundation.URL) -> Foundation.URL { - switch self { - case .gitHub: - return sourceRootURL - .apollo.childFolderURL(folderName: "Sources") - .apollo.childFolderURL(folderName: "GitHubAPI") - case .starWars: - return sourceRootURL - .apollo.childFolderURL(folderName: "Sources") - .apollo.childFolderURL(folderName: "StarWarsAPI") - case .upload: - return sourceRootURL - .apollo.childFolderURL(folderName: "Sources") - .apollo.childFolderURL(folderName: "UploadAPI") - case .animalKingdom: - return sourceRootURL - .apollo.childFolderURL(folderName: "Sources") - .apollo.childFolderURL(folderName: "AnimalKingdomAPI") - } - } - - func options(fromSourceRoot sourceRootURL: Foundation.URL) -> ApolloCodegenOptions { - let targetRootURL = self.targetRootURL(fromSourceRoot: sourceRootURL) - switch self { - case .upload: - let outputFileURL = try! targetRootURL.apollo.childFileURL(fileName: "API.swift") + case starWars + case gitHub + case upload + case animalKingdom - let graphQLFolderURL = targetRootURL.apollo.childFolderURL(folderName: "graphql") - let operationIDsURL = try! graphQLFolderURL.apollo.childFileURL(fileName: "operationIDs.json") - let schema = try! graphQLFolderURL.apollo.childFileURL(fileName: "schema.json") - return ApolloCodegenOptions(operationIDsURL: operationIDsURL, - outputFormat: .singleFile(atFileURL: outputFileURL), - urlToSchemaFile: schema) - case .starWars: - let outputFileURL = try! targetRootURL.apollo.childFileURL(fileName: "API.swift") + init?(name: String) { + switch name { + case "StarWars": + self = .starWars + case "GitHub": + self = .gitHub + case "Upload": + self = .upload + case "AnimalKingdom": + self = .animalKingdom + default: + return nil + } + } - let graphQLFolderURL = targetRootURL.apollo.childFolderURL(folderName: "graphql") - let operationIDsURL = try! graphQLFolderURL.apollo.childFileURL(fileName: "operationIDs.json") - let schema = try! graphQLFolderURL.apollo.childFileURL(fileName: "schema.json") - - return ApolloCodegenOptions(operationIDsURL: operationIDsURL, - outputFormat: .singleFile(atFileURL: outputFileURL), - urlToSchemaFile: schema) - case .gitHub: - let outputFileURL = try! targetRootURL.apollo.childFileURL(fileName: "API.swift") + func targetRootURL(fromSourceRoot sourceRootURL: Foundation.URL) -> Foundation.URL { + switch self { + case .gitHub: + return sourceRootURL + .apollo.childFolderURL(folderName: "Sources") + .apollo.childFolderURL(folderName: "GitHubAPI") + case .starWars: + return sourceRootURL + .apollo.childFolderURL(folderName: "Sources") + .apollo.childFolderURL(folderName: "StarWarsAPI") + case .upload: + return sourceRootURL + .apollo.childFolderURL(folderName: "Sources") + .apollo.childFolderURL(folderName: "UploadAPI") + case .animalKingdom: + return sourceRootURL + .apollo.childFolderURL(folderName: "Sources") + .apollo.childFolderURL(folderName: "AnimalKingdomAPI") + } + } - let graphQLFolderURL = targetRootURL.apollo.childFolderURL(folderName: "graphql") - let schema = try! graphQLFolderURL.apollo.childFileURL(fileName: "schema.docs.graphql") - let operationIDsURL = try! graphQLFolderURL.apollo.childFileURL(fileName: "operationIDs.json") - return ApolloCodegenOptions(includes: "graphql/Queries/**/*.graphql", - mergeInFieldsFromFragmentSpreads: true, - operationIDsURL: operationIDsURL, - outputFormat: .singleFile(atFileURL: outputFileURL), - suppressSwiftMultilineStringLiterals: true, - urlToSchemaFile: schema) - case .animalKingdom: - let graphQLFolderURL = targetRootURL.apollo.childFolderURL(folderName: "graphql") - let outputFolderURL = graphQLFolderURL.apollo.childFolderURL(folderName: "Generated") - let schema = try! graphQLFolderURL.apollo.childFileURL(fileName: "AnimalSchema.graphqls") + func config(fromSourceRoot sourceRootURL: Foundation.URL) -> ApolloCodegenConfiguration { + let targetRootURL = self.targetRootURL(fromSourceRoot: sourceRootURL) + switch self { + case .upload: + fatalError() +// let outputFileURL = try! targetRootURL.apollo.childFileURL(fileName: "API.swift") +// +// let graphQLFolderURL = targetRootURL.apollo.childFolderURL(folderName: "graphql") +// let operationIDsURL = try! graphQLFolderURL.apollo.childFileURL(fileName: "operationIDs.json") +// let schema = try! graphQLFolderURL.apollo.childFileURL(fileName: "schema.json") +// +// return ApolloCodegenOptions(operationIDsURL: operationIDsURL, +// outputFormat: .singleFile(atFileURL: outputFileURL), +// urlToSchemaFile: schema) + case .starWars: + fatalError() +// let outputFileURL = try! targetRootURL.apollo.childFileURL(fileName: "API.swift") +// +// let graphQLFolderURL = targetRootURL.apollo.childFolderURL(folderName: "graphql") +// let operationIDsURL = try! graphQLFolderURL.apollo.childFileURL(fileName: "operationIDs.json") +// let schema = try! graphQLFolderURL.apollo.childFileURL(fileName: "schema.json") +// +// return ApolloCodegenOptions(operationIDsURL: operationIDsURL, +// outputFormat: .singleFile(atFileURL: outputFileURL), +// urlToSchemaFile: schema) + case .gitHub: + fatalError() +// let outputFileURL = try! targetRootURL.apollo.childFileURL(fileName: "API.swift") +// +// let graphQLFolderURL = targetRootURL.apollo.childFolderURL(folderName: "graphql") +// let schema = try! graphQLFolderURL.apollo.childFileURL(fileName: "schema.docs.graphql") +// let operationIDsURL = try! graphQLFolderURL.apollo.childFileURL(fileName: "operationIDs.json") +// return ApolloCodegenOptions(includes: "graphql/Queries/**/*.graphql", +// mergeInFieldsFromFragmentSpreads: true, +// operationIDsURL: operationIDsURL, +// outputFormat: .singleFile(atFileURL: outputFileURL), +// suppressSwiftMultilineStringLiterals: true, +// urlToSchemaFile: schema) + case .animalKingdom: + let graphQLFolderURL = targetRootURL.apollo.childFolderURL(folderName: "graphql") - return ApolloCodegenOptions(codegenEngine: .swift, - outputFormat: .multipleFiles(inFolderAtURL: outputFolderURL), - urlToSchemaFile: schema) - } + return ApolloCodegenConfiguration(basePath: graphQLFolderURL.path) } + } } diff --git a/SwiftScripts/Sources/Codegen/main.swift b/SwiftScripts/Sources/Codegen/main.swift index fa82ac4778..c304bef5b5 100644 --- a/SwiftScripts/Sources/Codegen/main.swift +++ b/SwiftScripts/Sources/Codegen/main.swift @@ -7,52 +7,45 @@ import ArgumentParser // target, so we're using an arg parser to figure out which one to build, // and an enum to hold related options. struct Codegen: ParsableCommand { - - enum ArgumentError: Error, LocalizedError { - case invalidTargetName(name: String) - - var errorDescription: String? { - switch self { - case .invalidTargetName(let name): - return "The target \"\(name)\" was invalid. Please try again." - } - } + + enum ArgumentError: Error, LocalizedError { + case invalidTargetName(name: String) + + var errorDescription: String? { + switch self { + case .invalidTargetName(let name): + return "The target \"\(name)\" was invalid. Please try again." + } } - - @Option(name: [.customLong("target"), .customShort("t")], help: "The target to generate code for. Required.") - var targetName: String - - mutating func run() throws { - guard let target = Target(name: targetName) else { - throw ArgumentError.invalidTargetName(name: targetName) - } - - // Grab the parent folder of this file on the filesystem - let parentFolderOfScriptFile = FileFinder.findParentFolder() - - // Use that to calculate the source root - let sourceRootURL = parentFolderOfScriptFile - .apollo.parentFolderURL() // Sources - .apollo.parentFolderURL() // SwiftScripts - .apollo.parentFolderURL() // apollo-ios - - let targetURL = target.targetRootURL(fromSourceRoot: sourceRootURL) - let options = target.options(fromSourceRoot: sourceRootURL) - - // This more necessary if you're using a sub-folder, but make sure - // there's actually a place to write out what you're doing. - try FileManager.default.apollo.createFolderIfNeeded(at: targetURL) - - // Calculate where you want to download the CLI folder. - let cliFolderURL = sourceRootURL - .apollo.childFolderURL(folderName: "SwiftScripts") - .apollo.childFolderURL(folderName: "ApolloCLI") - - // Actually attempt to generate code. - try ApolloCodegen.run(from: targetURL, - with: cliFolderURL, - options: options) + } + + @Option(name: [.customLong("target"), .customShort("t")], help: "The target to generate code for. Required.") + var targetName: String + + mutating func run() throws { + guard let target = Target(name: targetName) else { + throw ArgumentError.invalidTargetName(name: targetName) } + + // Grab the parent folder of this file on the filesystem + let parentFolderOfScriptFile = FileFinder.findParentFolder() + + // Use that to calculate the source root + let sourceRootURL = parentFolderOfScriptFile + .apollo.parentFolderURL() // Sources + .apollo.parentFolderURL() // SwiftScripts + .apollo.parentFolderURL() // apollo-ios + + let targetURL = target.targetRootURL(fromSourceRoot: sourceRootURL) + let config = target.config(fromSourceRoot: sourceRootURL) + + // This more necessary if you're using a sub-folder, but make sure + // there's actually a place to write out what you're doing. + try FileManager.default.apollo.createDirectoryIfNeeded(atPath: targetURL.path) + + // Actually attempt to generate code. + try ApolloCodegen.build(with: config) + } } Codegen.main() diff --git a/Tests/ApolloCodegenTests/CodeGenIR/IRRootEntityFieldBuilderTests.swift b/Tests/ApolloCodegenTests/CodeGenIR/IRRootEntityFieldBuilderTests.swift index 57d8faa7f4..3bd8977b40 100644 --- a/Tests/ApolloCodegenTests/CodeGenIR/IRRootEntityFieldBuilderTests.swift +++ b/Tests/ApolloCodegenTests/CodeGenIR/IRRootEntityFieldBuilderTests.swift @@ -1589,22 +1589,25 @@ class IRRootEntityFieldBuilderTests: XCTestCase { } """ - let expected_direct: [CompilationResult.Selection] = [ - .field(.mock("b", type: .scalar(.integer()))), - ] - - let expected_merged: [CompilationResult.Selection] = [ - .field(.mock("a", type: .scalar(.integer()))), - ] - // when try buildSubjectRootField() + let expected = SelectionMatcher( + direct: [ + .field(.mock("b", type: .scalar(.integer()))), + ], + merged: [ + .field(.mock("a", type: .scalar(.integer()))), + ], + mergedSources: [ + try .mock(subject[field: "aField"]) + ] + ) + let actual = subject[field: "aField"]?[as: "B"] // then - expect(actual?.selections.direct).to(shallowlyMatch(expected_direct)) - expect(actual?.selections.merged).to(shallowlyMatch(expected_merged)) + expect(actual).to(shallowlyMatch(expected)) } // MARK: - Merged Selections - Siblings @@ -1645,6 +1648,9 @@ class IRRootEntityFieldBuilderTests: XCTestCase { } """ + // when + try buildSubjectRootField() + let asBirdExpected: [CompilationResult.Selection] = [ .field(.mock("wingspan", type: .scalar(.integer()))), ] @@ -1652,9 +1658,6 @@ class IRRootEntityFieldBuilderTests: XCTestCase { .field(.mock("species", type: .scalar(.string()))), ] - // when - try buildSubjectRootField() - let allAnimals = subject[field: "allAnimals"] let asBird = allAnimals?[as: "Bird"] let asCat = allAnimals?[as: "Cat"] @@ -1702,16 +1705,26 @@ class IRRootEntityFieldBuilderTests: XCTestCase { } """ - let asBirdExpected: SelectionMatcher = ( + // when + try buildSubjectRootField() + + let allAnimals = subject[field: "allAnimals"] + let asBird = allAnimals?[as: "Bird"] + let asPet = allAnimals?[as: "Pet"] + + let asBirdExpected = SelectionMatcher( direct: [ .field(.mock("wingspan", type: .scalar(.integer()))), ], merged: [ .field(.mock("species", type: .scalar(.string()))), + ], + mergedSources: [ + try .mock(asPet) ] ) - let asPetExpected: SelectionMatcher = ( + let asPetExpected = SelectionMatcher( direct: [ .field(.mock("species", type: .scalar(.string()))), ], @@ -1719,12 +1732,6 @@ class IRRootEntityFieldBuilderTests: XCTestCase { ] ) - // when - try buildSubjectRootField() - - let allAnimals = subject[field: "allAnimals"] - let asBird = allAnimals?[as: "Bird"] - let asPet = allAnimals?[as: "Pet"] // then expect(asBird).to(shallowlyMatch(asBirdExpected)) @@ -1765,7 +1772,7 @@ class IRRootEntityFieldBuilderTests: XCTestCase { } """ - let asBirdExpected: SelectionMatcher = ( + let asBirdExpected = SelectionMatcher( direct: [ .field(.mock("wingspan", type: .scalar(.integer()))), ], @@ -1773,7 +1780,7 @@ class IRRootEntityFieldBuilderTests: XCTestCase { ] ) - let asPetExpected: SelectionMatcher = ( + let asPetExpected = SelectionMatcher( direct: [ .field(.mock("species", type: .scalar(.string()))), ], @@ -1829,16 +1836,26 @@ class IRRootEntityFieldBuilderTests: XCTestCase { } """ - let asHousePetExpected: SelectionMatcher = ( + // when + try buildSubjectRootField() + + let allAnimals = subject[field: "allAnimals"] + let asHousePet = allAnimals?[as: "HousePet"] + let asPet = allAnimals?[as: "Pet"] + + let asHousePetExpected = SelectionMatcher( direct: [ .field(.mock("humanName", type: .scalar(.string()))), ], merged: [ .field(.mock("species", type: .scalar(.string()))), + ], + mergedSources: [ + try .mock(asPet) ] ) - let asPetExpected: SelectionMatcher = ( + let asPetExpected = SelectionMatcher( direct: [ .field(.mock("species", type: .scalar(.string()))), ], @@ -1846,13 +1863,6 @@ class IRRootEntityFieldBuilderTests: XCTestCase { ] ) - // when - try buildSubjectRootField() - - let allAnimals = subject[field: "allAnimals"] - let asHousePet = allAnimals?[as: "HousePet"] - let asPet = allAnimals?[as: "Pet"] - // then expect(asHousePet).to(shallowlyMatch(asHousePetExpected)) expect(asPet).to(shallowlyMatch(asPetExpected)) @@ -1891,7 +1901,7 @@ class IRRootEntityFieldBuilderTests: XCTestCase { } """ - let asHousePetExpected: SelectionMatcher = ( + let asHousePetExpected = SelectionMatcher( direct: [ .field(.mock("humanName", type: .scalar(.string()))), ], @@ -1899,7 +1909,7 @@ class IRRootEntityFieldBuilderTests: XCTestCase { ] ) - let asPetExpected: SelectionMatcher = ( + let asPetExpected = SelectionMatcher( direct: [ .field(.mock("species", type: .scalar(.string()))), ], @@ -1957,16 +1967,26 @@ class IRRootEntityFieldBuilderTests: XCTestCase { } """ - let onWarmBlooded_onPet_expected: SelectionMatcher = ( + // when + try buildSubjectRootField() + + let allAnimals = subject[field: "allAnimals"] + let asWarmBlooded_asPet_actual = allAnimals?[as:"WarmBlooded"]?[as: "Pet"] + let asPet_actual = allAnimals?[as: "Pet"] + + let onWarmBlooded_onPet_expected = SelectionMatcher( direct: [ .field(.mock("humanName", type: .string())), ], merged: [ .field(.mock("species", type: .scalar(.string()))), + ], + mergedSources: [ + try .mock(asPet_actual) ] ) - let onPet_expected: SelectionMatcher = ( + let onPet_expected = SelectionMatcher( direct: [ .field(.mock("species", type: .scalar(.string()))), ], @@ -1974,18 +1994,9 @@ class IRRootEntityFieldBuilderTests: XCTestCase { ] ) - // when - try buildSubjectRootField() - - let allAnimals = subject[field: "allAnimals"] - - let onWarmBlooded_onPet_actual = allAnimals?[as:"WarmBlooded"]?[as: "Pet"] - - let onPet_actual = allAnimals?[as: "Pet"] - // then - expect(onWarmBlooded_onPet_actual).to(shallowlyMatch(onWarmBlooded_onPet_expected)) - expect(onPet_actual).to(shallowlyMatch(onPet_expected)) + expect(asWarmBlooded_asPet_actual).to(shallowlyMatch(onWarmBlooded_onPet_expected)) + expect(asPet_actual).to(shallowlyMatch(onPet_expected)) } func test__mergedSelections__givenIsObjectInInterfaceType_uncleSelectionSetIsMatchingInterfaceType_mergesUncleSelections() throws { @@ -2030,16 +2041,26 @@ class IRRootEntityFieldBuilderTests: XCTestCase { } """ - let onWarmBlooded_onBird_expected: SelectionMatcher = ( + // when + try buildSubjectRootField() + + let allAnimals = subject[field: "allAnimals"] + let asWarmBlooded_asBird_actual = allAnimals?[as: "WarmBlooded"]?[as: "Bird"] + let asPet_actual = allAnimals?[as: "Pet"] + + let onWarmBlooded_onBird_expected = SelectionMatcher( direct: [ .field(.mock("wingspan", type: .integer())), ], merged: [ .field(.mock("species", type: .scalar(.string()))), + ], + mergedSources: [ + try .mock(asPet_actual) ] ) - let onPet_expected: SelectionMatcher = ( + let onPet_expected = SelectionMatcher( direct: [ .field(.mock("species", type: .scalar(.string()))), ], @@ -2047,15 +2068,6 @@ class IRRootEntityFieldBuilderTests: XCTestCase { ] ) - // when - try buildSubjectRootField() - - let allAnimals = subject[field: "allAnimals"] - - let asWarmBlooded_asBird_actual = allAnimals?[as: "WarmBlooded"]?[as: "Bird"] - - let asPet_actual = allAnimals?[as: "Pet"] - // then expect(asWarmBlooded_asBird_actual).to(shallowlyMatch(onWarmBlooded_onBird_expected)) expect(asPet_actual).to(shallowlyMatch(onPet_expected)) @@ -2101,7 +2113,14 @@ class IRRootEntityFieldBuilderTests: XCTestCase { } """ - let asWarmBlooded_asBird_expected: SelectionMatcher = ( + // when + try buildSubjectRootField() + + let allAnimals = subject[field: "allAnimals"] + let asWarmBlooded_asBird_actual = allAnimals?[as: "WarmBlooded"]?[as: "Bird"] + let asPet_actual = allAnimals?[as: "Pet"] + + let asWarmBlooded_asBird_expected = SelectionMatcher( direct: [ .field(.mock("wingspan", type: .integer())), ], @@ -2109,7 +2128,7 @@ class IRRootEntityFieldBuilderTests: XCTestCase { ] ) - let asPet_expected: SelectionMatcher = ( + let asPet_expected = SelectionMatcher( direct: [ .field(.mock("species", type: .scalar(.string()))), ], @@ -2117,15 +2136,6 @@ class IRRootEntityFieldBuilderTests: XCTestCase { ] ) - // when - try buildSubjectRootField() - - let allAnimals = subject[field: "allAnimals"] - - let asWarmBlooded_asBird_actual = allAnimals?[as: "WarmBlooded"]?[as: "Bird"] - - let asPet_actual = allAnimals?[as: "Pet"] - // then expect(asWarmBlooded_asBird_actual).to(shallowlyMatch(asWarmBlooded_asBird_expected)) expect(asPet_actual).to(shallowlyMatch(asPet_expected)) @@ -2167,32 +2177,37 @@ class IRRootEntityFieldBuilderTests: XCTestCase { } """ - let asBirdExpected: SelectionMatcher = ( + // when + try buildSubjectRootField() + + let allAnimals = subject[field: "allAnimals"] + let asBirdActual = allAnimals?[as: "Bird"] + let asClassroomPet_asBirdActual = allAnimals?[as: "ClassroomPet"]?[as: "Bird"] + + let asBirdExpected = SelectionMatcher( direct: [ .field(.mock("wingspan", type: .integer())) ], merged: [ .field(.mock("species", type: .scalar(.string()))), + ], + mergedSources: [ + try .mock(asClassroomPet_asBirdActual) ] ) - let asClassroomPet_asBirdExpected: SelectionMatcher = ( + let asClassroomPet_asBirdExpected = SelectionMatcher( direct: [ .field(.mock("species", type: .scalar(.string()))), ], merged: [ .field(.mock("wingspan", type: .integer())) + ], + mergedSources: [ + try .mock(asBirdActual) ] ) - // when - try buildSubjectRootField() - - let allAnimals = subject[field: "allAnimals"] - - let asBirdActual = allAnimals?[as: "Bird"] - let asClassroomPet_asBirdActual = allAnimals?[as: "ClassroomPet"]?[as: "Bird"] - // then expect(asBirdActual).to(shallowlyMatch(asBirdExpected)) expect(asClassroomPet_asBirdActual).to(shallowlyMatch(asClassroomPet_asBirdExpected)) @@ -2236,7 +2251,7 @@ class IRRootEntityFieldBuilderTests: XCTestCase { } """ - let asBirdExpected: SelectionMatcher = ( + let asBirdExpected = SelectionMatcher( direct: [ .field(.mock("wingspan", type: .integer())) ], @@ -2244,7 +2259,7 @@ class IRRootEntityFieldBuilderTests: XCTestCase { ] ) - let asClassroomPet_asCatExpected: SelectionMatcher = ( + let asClassroomPet_asCatExpected = SelectionMatcher( direct: [ .field(.mock("species", type: .scalar(.string()))), ], @@ -2306,7 +2321,14 @@ class IRRootEntityFieldBuilderTests: XCTestCase { } """ - let asWarmBlooded_expected: SelectionMatcher = ( + // when + try buildSubjectRootField() + + let allAnimals = subject[field: "allAnimals"] + let asWarmBlooded_actual = allAnimals?[as: "WarmBlooded"] + let asClassroomPet_asWarmBlooded_actual = allAnimals?[as: "ClassroomPet"]?[as: "WarmBlooded"] + + let asWarmBlooded_expected = SelectionMatcher( direct: [ .field(.mock("bodyTemperature", type: .integer())), ], @@ -2314,24 +2336,18 @@ class IRRootEntityFieldBuilderTests: XCTestCase { ] ) - let asClassroomPet_asWarmBlooded_expected: SelectionMatcher = ( + let asClassroomPet_asWarmBlooded_expected = SelectionMatcher( direct: [ .field(.mock("species", type: .scalar(.string()))), ], merged: [ .field(.mock("bodyTemperature", type: .integer())), + ], + mergedSources: [ + try .mock(asWarmBlooded_actual) ] ) - // when - try buildSubjectRootField() - - let allAnimals = subject[field: "allAnimals"] - - let asWarmBlooded_actual = allAnimals?[as: "WarmBlooded"] - - let asClassroomPet_asWarmBlooded_actual = allAnimals?[as: "ClassroomPet"]?[as: "WarmBlooded"] - // then expect(asWarmBlooded_actual).to(shallowlyMatch(asWarmBlooded_expected)) expect(asClassroomPet_asWarmBlooded_actual).to(shallowlyMatch(asClassroomPet_asWarmBlooded_expected)) @@ -2383,7 +2399,14 @@ class IRRootEntityFieldBuilderTests: XCTestCase { } """ - let asPet_expected: SelectionMatcher = ( + // when + try buildSubjectRootField() + + let allAnimals = subject[field: "allAnimals"] + let asPet_actual = allAnimals?[as: "Pet"] + let asClassroomPet_asWarmBloodedPet_actual = allAnimals?[as: "ClassroomPet"]?[as: "WarmBloodedPet"] + + let asPet_expected = SelectionMatcher( direct: [ .field(.mock("humanName", type: .string())), ], @@ -2391,24 +2414,18 @@ class IRRootEntityFieldBuilderTests: XCTestCase { ] ) - let asClassroomPet_asWarmBloodedPet_expected: SelectionMatcher = ( + let asClassroomPet_asWarmBloodedPet_expected = SelectionMatcher( direct: [ .field(.mock("species", type: .scalar(.string()))), ], merged: [ .field(.mock("humanName", type: .string())), + ], + mergedSources: [ + try .mock(asPet_actual) ] ) - // when - try buildSubjectRootField() - - let allAnimals = subject[field: "allAnimals"] - - let asPet_actual = allAnimals?[as: "Pet"] - - let asClassroomPet_asWarmBloodedPet_actual = allAnimals?[as: "ClassroomPet"]?[as: "WarmBloodedPet"] - // then expect(asPet_actual).to(shallowlyMatch(asPet_expected)) expect(asClassroomPet_asWarmBloodedPet_actual).to(shallowlyMatch(asClassroomPet_asWarmBloodedPet_expected)) @@ -2460,7 +2477,16 @@ class IRRootEntityFieldBuilderTests: XCTestCase { } """ - let asWarmBlooded_expected: SelectionMatcher = ( + // when + try buildSubjectRootField() + + let allAnimals = subject[field: "allAnimals"] + + let asWarmBlooded_actual = allAnimals?[as: "WarmBlooded"] + + let asClassroomPet_asPet_actual = allAnimals?[as: "ClassroomPet"]?[as: "Pet"] + + let asWarmBlooded_expected = SelectionMatcher( direct: [ .field(.mock("bodyTemperature", type: .integer())), ], @@ -2468,7 +2494,7 @@ class IRRootEntityFieldBuilderTests: XCTestCase { ] ) - let asClassroomPet_asPet_expected: SelectionMatcher = ( + let asClassroomPet_asPet_expected = SelectionMatcher( direct: [ .field(.mock("species", type: .scalar(.string()))), ], @@ -2476,14 +2502,6 @@ class IRRootEntityFieldBuilderTests: XCTestCase { ] ) - // when - try buildSubjectRootField() - - let allAnimals = subject[field: "allAnimals"] - - let asWarmBlooded_actual = allAnimals?[as: "WarmBlooded"] - - let asClassroomPet_asPet_actual = allAnimals?[as: "ClassroomPet"]?[as: "Pet"] // then expect(asWarmBlooded_actual).to(shallowlyMatch(asWarmBlooded_expected)) expect(asClassroomPet_asPet_actual).to(shallowlyMatch(asClassroomPet_asPet_expected)) @@ -2518,24 +2536,73 @@ class IRRootEntityFieldBuilderTests: XCTestCase { // when try buildSubjectRootField() - let Fragment_AnimalDetails = try XCTUnwrap(ir.compilationResult[fragment: "AnimalDetails"]) + let allAnimals = subject[field: "allAnimals"] + let Fragment_AnimalDetails = try XCTUnwrap(allAnimals?[fragment: "AnimalDetails"]) + let actual = allAnimals?.selectionSet - let expected: SelectionMatcher = ( + let expected = SelectionMatcher( direct: [ - .fragmentSpread(Fragment_AnimalDetails) + .fragmentSpread(Fragment_AnimalDetails.definition) ], merged: [ .field(.mock("species", type: .scalar(.string()))), + ], + mergedSources: [ + try .mock(Fragment_AnimalDetails) ] ) - let allAnimals = subject[field: "allAnimals"] - let actual = allAnimals?.selectionSet - // then expect(actual).to(shallowlyMatch(expected)) } + func test__mergedSelections__givenChildIsNamedFragmentOnSameType_fragmentSpreadTypePathIsCorrect() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + species: String + } + """ + + document = """ + query Test { + allAnimals { + ...AnimalDetails + } + } + + fragment AnimalDetails on Animal { + species + } + """ + + // when + try buildSubjectRootField() + + let actual = subject[field: "allAnimals"]?[fragment: "AnimalDetails"] + + let query_TypeScope = TypeScopeDescriptor.descriptor( + forType: operation.rootType, + givenAllTypesInSchema: schema.referencedTypes) + + let allAnimals_TypeScope = TypeScopeDescriptor.descriptor( + forType: schema[interface: "Animal"]!, + givenAllTypesInSchema: schema.referencedTypes + ) + + let expectedTypePath = LinkedList(array: [ + query_TypeScope, + allAnimals_TypeScope, + ]) + + // then + expect(actual?.selectionSet.typeInfo.typePath).to(equal(expectedTypePath)) + } + func test__mergedSelections__givenChildIsNamedFragmentOnMoreSpecificType_doesNotMergeFragmentFields_hasTypeCaseForNamedFragmentType() throws { // given schemaSDL = """ @@ -2570,7 +2637,7 @@ class IRRootEntityFieldBuilderTests: XCTestCase { let Object_Bird = try XCTUnwrap(schema[object: "Bird"]) let Fragment_BirdDetails = try XCTUnwrap(ir.compilationResult[fragment: "BirdDetails"]) - let expected: SelectionMatcher = ( + let expected = SelectionMatcher( direct: [ .inlineFragment(.init(parentType: Object_Bird, selections: [.fragmentSpread(Fragment_BirdDetails)])) @@ -2617,21 +2684,25 @@ class IRRootEntityFieldBuilderTests: XCTestCase { // when try buildSubjectRootField() + + let birds = subject[field: "birds"] + let actual = birds?.selectionSet + let Field_Species = CompilationResult.Field.mock("species", type: .string()) - let Fragment_AnimalDetails = try XCTUnwrap(ir.compilationResult[fragment: "AnimalDetails"]) + let Fragment_AnimalDetails = try XCTUnwrap(birds?[fragment: "AnimalDetails"]) - let expected: SelectionMatcher = ( + let expected = SelectionMatcher( direct: [ - .fragmentSpread(Fragment_AnimalDetails) + .fragmentSpread(Fragment_AnimalDetails.definition) ], merged: [ .field(Field_Species), + ], + mergedSources: [ + try .mock(Fragment_AnimalDetails) ] ) - let birds = subject[field: "birds"] - let actual = birds?.selectionSet - // then expect(actual).to(shallowlyMatch(expected)) } @@ -2667,21 +2738,25 @@ class IRRootEntityFieldBuilderTests: XCTestCase { // when try buildSubjectRootField() + + let flyingAnimals = subject[field: "flyingAnimals"] + let actual = flyingAnimals?.selectionSet + let Field_Species = CompilationResult.Field.mock("species", type: .string()) - let Fragment_AnimalDetails = try XCTUnwrap(ir.compilationResult[fragment: "AnimalDetails"]) + let Fragment_AnimalDetails = try XCTUnwrap(flyingAnimals?[fragment: "AnimalDetails"]) - let expected: SelectionMatcher = ( + let expected = SelectionMatcher( direct: [ - .fragmentSpread(Fragment_AnimalDetails) + .fragmentSpread(Fragment_AnimalDetails.definition) ], merged: [ .field(Field_Species), + ], + mergedSources: [ + try .mock(Fragment_AnimalDetails) ] ) - let flyingAnimals = subject[field: "flyingAnimals"] - let actual = flyingAnimals?.selectionSet - // then expect(actual).to(shallowlyMatch(expected)) } @@ -2725,7 +2800,7 @@ class IRRootEntityFieldBuilderTests: XCTestCase { let Fragment_BirdDetails = try XCTUnwrap(ir.compilationResult[fragment: "BirdDetails"]) - let expected: SelectionMatcher = ( + let expected = SelectionMatcher( direct: [ .inlineFragment(.init(parentType: Object_Bird, selections: [.fragmentSpread(Fragment_BirdDetails)])) @@ -2783,8 +2858,10 @@ class IRRootEntityFieldBuilderTests: XCTestCase { try buildSubjectRootField() let allAnimals = subject[field: "allAnimals"] + let allAnimals_height_actual = allAnimals?[field: "height"]?.selectionSet + let allAnimals_asPet_height_actual = allAnimals?[as: "Pet"]?[field: "height"]?.selectionSet - let allAnimals_expected: SelectionMatcher = ( + let allAnimals_height_expected = SelectionMatcher( direct: [ .field(.mock("feet", type: .integer())) ], @@ -2792,21 +2869,21 @@ class IRRootEntityFieldBuilderTests: XCTestCase { ] ) - let allAnimals_asPet_expected: SelectionMatcher = ( + let allAnimals_asPet_height_expected = SelectionMatcher( direct: [ .field(.mock("meters", type: .integer())), ], merged: [ .field(.mock("feet", type: .integer())), + ], + mergedSources: [ + try .mock(allAnimals_height_actual) ] ) - let allAnimals_height_actual = allAnimals?[field: "height"]?.selectionSet - let allAnimals_asPet_height_actual = allAnimals?[as: "Pet"]?[field: "height"]?.selectionSet - // then - expect(allAnimals_height_actual).to(shallowlyMatch(allAnimals_expected)) - expect(allAnimals_asPet_height_actual).to(shallowlyMatch(allAnimals_asPet_expected)) + expect(allAnimals_height_actual).to(shallowlyMatch(allAnimals_height_expected)) + expect(allAnimals_asPet_height_actual).to(shallowlyMatch(allAnimals_asPet_height_expected)) } func test__mergedSelections__givenEntityFieldOnObjectWithSelectionSetIncludingSameFieldNameAndDifferentSelections_doesNotMergeFieldIntoNestedFieldsSelections() throws { @@ -2847,7 +2924,7 @@ class IRRootEntityFieldBuilderTests: XCTestCase { let allAnimals = subject[field: "allAnimals"] - let allAnimals_expected: SelectionMatcher = ( + let allAnimals_expected = SelectionMatcher( direct: [ .field(.mock("feet", type: .integer())) ], @@ -2855,7 +2932,7 @@ class IRRootEntityFieldBuilderTests: XCTestCase { ] ) - let predators_expected: SelectionMatcher = ( + let predators_expected = SelectionMatcher( direct: [ .field(.mock("meters", type: .integer())), ], @@ -2921,17 +2998,20 @@ class IRRootEntityFieldBuilderTests: XCTestCase { try buildSubjectRootField() let allAnimals = subject[field: "allAnimals"] + let allAnimals_asCat_height_actual = allAnimals?[as: "Cat"]?[field: "height"]?.selectionSet - let allAnimals_asCat_height_expected: SelectionMatcher = ( + let allAnimals_asCat_height_expected = SelectionMatcher( direct: nil, merged: [ .field(.mock("feet", type: .integer())), .field(.mock("meters", type: .integer())), + ], + mergedSources: [ + try .mock(allAnimals?[field: "height"]), + try .mock(allAnimals?[as: "Pet"]?[field: "height"]), ] ) - let allAnimals_asCat_height_actual = allAnimals?[as: "Cat"]?[field: "height"]?.selectionSet - // then expect(allAnimals_asCat_height_actual).to(shallowlyMatch(allAnimals_asCat_height_expected)) } @@ -2987,10 +3067,13 @@ class IRRootEntityFieldBuilderTests: XCTestCase { let allAnimals = subject[field: "allAnimals"] - let allAnimals_asElephant_height_expected: SelectionMatcher = ( + let allAnimals_asElephant_height_expected = SelectionMatcher( direct: nil, merged: [ .field(.mock("feet", type: .integer())) + ], + mergedSources: [ + try .mock(allAnimals?[field: "height"]) ] ) @@ -3058,8 +3141,11 @@ class IRRootEntityFieldBuilderTests: XCTestCase { try buildSubjectRootField() let allAnimals = subject[field: "allAnimals"] + let allAnimals_height = allAnimals?[field: "height"] + let allAnimals_asPet_height = allAnimals?[as: "Pet"]?[field: "height"] + let allAnimals_asWarmBlooded_height = allAnimals?[as: "WarmBlooded"]?[field: "height"] - let allAnimals_height_expected: SelectionMatcher = ( + let allAnimals_height_expected = SelectionMatcher( direct: [ .field(.mock("feet", type: .integer())) ], @@ -3067,16 +3153,19 @@ class IRRootEntityFieldBuilderTests: XCTestCase { ] ) - let allAnimals_asPet_height_expected: SelectionMatcher = ( + let allAnimals_asPet_height_expected = SelectionMatcher( direct: [ .field(.mock("meters", type: .integer())), ], merged: [ .field(.mock("feet", type: .integer())), + ], + mergedSources: [ + try .mock(allAnimals_height) ] ) - let allAnimals_asPet_asWarmBlooded_height_expected: SelectionMatcher = ( + let allAnimals_asPet_asWarmBlooded_height_expected = SelectionMatcher( direct: [ .field(.mock("inches", type: .integer())), ], @@ -3084,15 +3173,23 @@ class IRRootEntityFieldBuilderTests: XCTestCase { .field(.mock("feet", type: .integer())), .field(.mock("meters", type: .integer())), .field(.mock("yards", type: .integer())), + ], + mergedSources: [ + try .mock(allAnimals_height), + try .mock(allAnimals_asPet_height), + try .mock(allAnimals_asWarmBlooded_height), ] ) - let allAnimals_asWarmBlooded_height_expected: SelectionMatcher = ( + let allAnimals_asWarmBlooded_height_expected = SelectionMatcher( direct: [ .field(.mock("yards", type: .integer())), ], merged: [ .field(.mock("feet", type: .integer())), + ], + mergedSources: [ + try .mock(allAnimals_height) ] ) @@ -3508,6 +3605,61 @@ class IRRootEntityFieldBuilderTests: XCTestCase { .to(equal(allAnimals_asCat_predator_height_expectedTypePath)) } + // MARK: - Nested Entity In Fragments - Merged Sources + + func test__mergedSources__givenEntityField_DirectSelectonsAndMergedFromNestedEntityInFragment_nestedEntityFieldHasFragmentMergedSources() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + species: String! + predator: Animal! + height: Height! + } + + type Height { + feet: Int! + } + """ + + document = """ + query TestOperation { + allAnimals { + predator { + species + } + ...PredatorDetails + } + } + + fragment PredatorDetails on Animal { + predator { + height { + feet + } + } + } + """ + + // when + try buildSubjectRootField() + + let allAnimals_predator = try XCTUnwrap( + subject?[field: "allAnimals"]?[field: "predator"] as? IR.EntityField + ) + let Fragment_PredatorDetails = subject?[field: "allAnimals"]?[fragment: "PredatorDetails"] + + let expected: IR.MergedSelections.MergedSources = [ + try .mock(for: allAnimals_predator, from: Fragment_PredatorDetails) + ] + + // then + expect(allAnimals_predator.selectionSet.selections.merged.mergedSources).to(equal(expected)) + } + // MARK: - Referenced Fragments func test__referencedFragments__givenUsesNoFragments_isEmpty() throws { diff --git a/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplateTests.swift b/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplateTests.swift index ee446e5bbc..0dcf249f76 100644 --- a/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplateTests.swift +++ b/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplateTests.swift @@ -187,35 +187,51 @@ class SelectionSetTemplateTests: XCTestCase { func test__render_selections__givenNilDirectSelections_doesNotRenderSelections() throws { // given - let type = GraphQLObjectType.mock("Animal") - - let field = IR.EntityField( - .mock(), - selectionSet: .init( - entity: .init( - rootTypePath: [type], - fieldPath: ["query", "allAnimals"] - ), - parentType: type, - typePath: [.descriptor(forType: type, givenAllTypesInSchema: .init([type]))], - mergedSelectionsOnly: true - ) - ) + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + type Dog { + species: String! + nested: Nested! + } + + type Nested { + a: Int! + b: Int! + } - let result = CompilationResult.emptyMockObject() - result.referencedTypes = [type] + interface Animal { + nested: Nested! + } + """ - let schema = IR.mock(compilationResult: result).schema + document = """ + query TestOperation { + allAnimals { + nested { + a + } + ... on Dog { + species + } + } + } + """ let expected = """ - public static var __parentType: ParentType { .Object(TestSchema.Animal.self) } + public static var __parentType: ParentType { .Object(TestSchema.Nested.self) } """ // when - subject = SelectionSetTemplate(schema: schema) + try buildSubjectAndOperation() + let asDog_Nested = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"]?[field: "nested"] as? IR.EntityField + ) - let actual = subject.render(field: field) + let actual = subject.render(field: asDog_Nested) // then expect(actual).to(equalLineByLine(expected, atLine: 5, ignoringExtraLines: true)) @@ -683,4 +699,519 @@ class SelectionSetTemplateTests: XCTestCase { expect(actual).to(equalLineByLine(expected, atLine: 10, ignoringExtraLines: true)) } + func test__render_fieldAccessors__givenMergedScalarField_rendersFieldAccessor() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + a: String! + } + + type Dog { + b: String! + } + """ + + document = """ + query TestOperation { + allAnimals { + a + ... on Dog { + b + } + } + } + """ + + let expected = """ + public var b: String { data["b"] } + public var a: String { data["a"] } + """ + + // when + try buildSubjectAndOperation() + let dog = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"] + ) + + let actual = subject.render(typeCase: dog) + + // then + expect(actual).to(equalLineByLine(expected, atLine: 10, ignoringExtraLines: true)) + } + + // MARK: - Field Accessors - Entity + + func test__render_fieldAccessors__givenDirectEntityField_rendersFieldAccessor() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + species: String! + predator: Animal! + } + """ + + document = """ + query TestOperation { + allAnimals { + predator { + species + } + } + } + """ + + let expected = """ + public var predator: Predator { data["predator"] } + """ + + // when + try buildSubjectAndOperation() + let allAnimals = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField + ) + + let actual = subject.render(field: allAnimals) + + // then + expect(actual).to(equalLineByLine(expected, atLine: 10, ignoringExtraLines: true)) + } + + func test__render_fieldAccessors__givenDirectEntityFieldWithAlias_rendersFieldAccessor() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + species: String! + predator: Animal! + } + """ + + document = """ + query TestOperation { + allAnimals { + aliasedPredator: predator { + species + } + } + } + """ + + let expected = """ + public var aliasedPredator: AliasedPredator { data["aliasedPredator"] } + """ + + // when + try buildSubjectAndOperation() + let allAnimals = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField + ) + + let actual = subject.render(field: allAnimals) + + // then + expect(actual).to(equalLineByLine(expected, atLine: 10, ignoringExtraLines: true)) + } + + func test__render_fieldAccessors__givenDirectEntityFieldAsOptional_rendersFieldAccessor() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + species: String! + predator: Animal + } + """ + + document = """ + query TestOperation { + allAnimals { + predator { + species + } + } + } + """ + + let expected = """ + public var predator: Predator? { data["predator"] } + """ + + // when + try buildSubjectAndOperation() + let allAnimals = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField + ) + + let actual = subject.render(field: allAnimals) + + // then + expect(actual).to(equalLineByLine(expected, atLine: 10, ignoringExtraLines: true)) + } + + func test__render_fieldAccessors__givenDirectEntityFieldAsList_rendersFieldAccessor() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + species: String! + predators: [Animal!] + } + """ + + document = """ + query TestOperation { + allAnimals { + predators { + species + } + } + } + """ + + let expected = """ + public var predators: [Predator]? { data["predators"] } + """ + + // when + try buildSubjectAndOperation() + let allAnimals = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField + ) + + let actual = subject.render(field: allAnimals) + + // then + expect(actual).to(equalLineByLine(expected, atLine: 10, ignoringExtraLines: true)) + } + + func test__render_fieldAccessors__givenEntityFieldWithDirectSelectionsAndMergedFromFragment_rendersFieldAccessor() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + species: String! + name: String! + predator: Animal! + } + """ + + document = """ + query TestOperation { + allAnimals { + ...PredatorDetails + predator { + name + } + } + } + + fragment PredatorDetails on Animal { + predator { + species + } + } + """ + + let expected = """ + public var predator: Predator { data["predator"] } + """ + + // when + try buildSubjectAndOperation() + let allAnimals = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField + ) + + let actual = subject.render(field: allAnimals) + + // then + expect(actual).to(equalLineByLine(expected, atLine: 11, ignoringExtraLines: true)) + } + + func test__render_fieldAccessors__givenEntityFieldMergedFromFragment_rendersFieldAccessor() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + species: String! + predator: Animal! + } + """ + + document = """ + query TestOperation { + allAnimals { + ...PredatorDetails + } + } + + fragment PredatorDetails on Animal { + predator { + species + } + } + """ + + let expected = """ + public var predator: PredatorDetails.Predator { data["predator"] } + """ + + // when + try buildSubjectAndOperation() + let allAnimals = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField + ) + + let actual = subject.render(field: allAnimals) + + // then + expect(actual).to(equalLineByLine(expected, atLine: 10, ignoringExtraLines: true)) + } + + func test__render_fieldAccessors__givenEntityFieldMergedFromFragmentEntityNestedInEntity_rendersFieldAccessor() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + species: String! + predator: Animal! + height: Height! + } + + type Height { + feet: Int! + } + """ + + document = """ + query TestOperation { + allAnimals { + predator { + species + } + ...PredatorDetails + } + } + + fragment PredatorDetails on Animal { + predator { + height { + feet + } + } + } + """ + + let expected = """ + public var species: String { data["species"] } + public var height: PredatorDetails.Predator.Height { data["height"] } + """ + + // when + try buildSubjectAndOperation() + let allAnimals_predator = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[field: "predator"] as? IR.EntityField + ) + + let actual = subject.render(field: allAnimals_predator) + + // then + expect(actual).to(equalLineByLine(expected, atLine: 10, ignoringExtraLines: true)) + } + + func test__render_fieldAccessors__givenEntityFieldMergedFromFragmentInTypeCaseWithEntityNestedInEntity_rendersFieldAccessor() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + species: String! + predator: Animal! + height: Height! + } + + interface Pet { + predator: Animal! + } + + type Height { + feet: Int! + } + """ + + document = """ + query TestOperation { + allAnimals { + predator { + species + } + ...PredatorDetails + } + } + + fragment PredatorDetails on Pet { + predator { + height { + feet + } + } + } + """ + + let expected = """ + public var height: PredatorDetails.Predator.Height { data["height"] } + """ + + // when + try buildSubjectAndOperation() + let allAnimals_asPet_predator = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[as: "Pet"]?[field: "predator"] as? IR.EntityField + ) + + let actual = subject.render(field: allAnimals_asPet_predator) + + // then + expect(actual).to(equalLineByLine(expected, atLine: 10, ignoringExtraLines: true)) + } + + func test__render_fieldAccessors__givenEntityFieldMergedFromFragmentWithEntityNestedInEntityTypeCase_rendersFieldAccessor() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + species: String! + predator: Animal! + height: Height! + } + + interface Pet { + height: Height! + } + + type Height { + feet: Int! + } + """ + + document = """ + query TestOperation { + allAnimals { + predator { + species + } + ...PredatorDetails + } + } + + fragment PredatorDetails on Animal { + predator { + ... on Pet { + height { + feet + } + } + } + } + """ + + let predator_expected = """ + public var species: String { data["species"] } + + """ + + let predator_asPet_expected = """ + public var species: String { data["species"] } + public var height: PredatorDetails.Predator.AsPet.Height { data["height"] } + """ + + // when + try buildSubjectAndOperation() + let allAnimals_predator = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[field: "predator"] as? IR.EntityField + ) + + let allAnimals_predator_asPet = try XCTUnwrap(allAnimals_predator[as: "Pet"]) + + let allAnimals_predator_actual = subject.render(field: allAnimals_predator) + let allAnimals_predator_asPet_actual = subject.render(typeCase: allAnimals_predator_asPet) + + // then + expect(allAnimals_predator_actual).to(equalLineByLine(predator_expected, atLine: 10, ignoringExtraLines: true)) + expect(allAnimals_predator_asPet_actual).to(equalLineByLine(predator_asPet_expected, atLine: 9, ignoringExtraLines: true)) + } + + // MARK: Nested Selection Sets + + func test__render_nestedSelectionSets__givenDirectEntityFieldAsList_rendersNestedSelectionSet() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + species: String! + predators: [Animal!] + } + """ + + document = """ + query TestOperation { + allAnimals { + predators { + species + } + } + } + """ + + let expected = """ + public var predators: [Predator]? { data["predators"] } + + public struct Predator: TestSchema.SelectionSet { + """ + + // when + try buildSubjectAndOperation() + let allAnimals = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField + ) + + let actual = subject.render(field: allAnimals) + + // then + expect(actual).to(equalLineByLine(expected, atLine: 10, ignoringExtraLines: true)) + } + } diff --git a/Tests/ApolloCodegenTests/TestHelpers/IRMatchers.swift b/Tests/ApolloCodegenTests/TestHelpers/IRMatchers.swift index 23bd6f0595..b8bf125ad7 100644 --- a/Tests/ApolloCodegenTests/TestHelpers/IRMatchers.swift +++ b/Tests/ApolloCodegenTests/TestHelpers/IRMatchers.swift @@ -16,8 +16,9 @@ protocol SelectionShallowMatchable { var isEmpty: Bool { get } } -extension IR.SortedSelections: SelectionShallowMatchable { } -extension IR.ShallowSelections: SelectionShallowMatchable { +extension IR.DirectSelections: SelectionShallowMatchable { } +extension IR.MergedSelections: SelectionShallowMatchable { } +extension IR.EntityTreeScopeSelections: SelectionShallowMatchable { var typeCases: OrderedDictionary { [:] } } @@ -56,21 +57,33 @@ func shallowlyMatch( return shallowlyMatch((expectedFields, expectedTypeCases, expectedFragments)) } -typealias SelectionMatcher = ( - direct: [CompilationResult.Selection]?, - merged:[CompilationResult.Selection] -) +struct SelectionMatcher { + let direct: [CompilationResult.Selection]? + let merged: [CompilationResult.Selection] + let mergedSources: Set + + public init( + direct: [CompilationResult.Selection]?, + merged: [CompilationResult.Selection], + mergedSources: Set = [] + ) { + self.direct = direct + self.merged = merged + self.mergedSources = mergedSources + } +} func shallowlyMatch( _ expectedValue: SelectionMatcher ) -> Predicate { - let directPredicate: Predicate = expectedValue.direct == nil + let directPredicate: Predicate = expectedValue.direct == nil ? beNil() : shallowlyMatch(expectedValue.direct!) return satisfyAllOf([ directPredicate.mappingActualTo { $0?.selections.direct }, - shallowlyMatch(expectedValue.merged).mappingActualTo { $0?.selections.merged } + shallowlyMatch(expectedValue.merged).mappingActualTo { $0?.selections.merged }, + equal(expectedValue.mergedSources).mappingActualTo { $0?.selections.merged.mergedSources } ]) }