diff --git a/Apollo.xcodeproj/project.pbxproj b/Apollo.xcodeproj/project.pbxproj index fa18860a9a..b4175974a0 100644 --- a/Apollo.xcodeproj/project.pbxproj +++ b/Apollo.xcodeproj/project.pbxproj @@ -19,6 +19,20 @@ 9B21FD772422C8CC00998B5C /* TestFileHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B21FD762422C8CC00998B5C /* TestFileHelper.swift */; }; 9B21FD782424305700998B5C /* ExpectedEnumWithDifferentCases.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B68F05F2416F80C00E97318 /* ExpectedEnumWithDifferentCases.swift */; }; 9B21FD792424305E00998B5C /* ExpectedEnumWithSanitizedCases.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B68F063241703B200E97318 /* ExpectedEnumWithSanitizedCases.swift */; }; + 9B260BEB245A020300562176 /* ApolloInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BEA245A020300562176 /* ApolloInterceptor.swift */; }; + 9B260BED245A021300562176 /* Parseable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BEC245A021300562176 /* Parseable.swift */; }; + 9B260BEF245A022E00562176 /* FlexibleDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BEE245A022E00562176 /* FlexibleDecoder.swift */; }; + 9B260BF1245A025400562176 /* HTTPRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BF0245A025400562176 /* HTTPRequest.swift */; }; + 9B260BF3245A026F00562176 /* RequestChain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BF2245A026F00562176 /* RequestChain.swift */; }; + 9B260BF5245A028D00562176 /* HTTPResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BF4245A028D00562176 /* HTTPResponse.swift */; }; + 9B260BF9245A030100562176 /* ResponseCodeInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BF8245A030100562176 /* ResponseCodeInterceptor.swift */; }; + 9B260BFB245A031900562176 /* NetworkFetchInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BFA245A031900562176 /* NetworkFetchInterceptor.swift */; }; + 9B260BFF245A054700562176 /* JSONRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BFE245A054700562176 /* JSONRequest.swift */; }; + 9B260C01245A059700562176 /* CodableParsingInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260C00245A059700562176 /* CodableParsingInterceptor.swift */; }; + 9B260C04245A090600562176 /* RequestChainNetworkTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260C03245A090600562176 /* RequestChainNetworkTransport.swift */; }; + 9B260C08245A437400562176 /* InterceptorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260C07245A437400562176 /* InterceptorProvider.swift */; }; + 9B260C0A245A532500562176 /* LegacyParsingInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260C09245A532500562176 /* LegacyParsingInterceptor.swift */; }; + 9B2B66F42513FAFE00B53ABF /* CancellationHandlingInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B2B66F32513FAFE00B53ABF /* CancellationHandlingInterceptor.swift */; }; 9B2DFBBF24E1FA1A00ED3AE6 /* Apollo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9FC750441D2A532C00458D91 /* Apollo.framework */; }; 9B2DFBC024E1FA1A00ED3AE6 /* Apollo.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9FC750441D2A532C00458D91 /* Apollo.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 9B2DFBC724E1FA4800ED3AE6 /* UploadAPI.h in Headers */ = {isa = PBXBuildFile; fileRef = 9B2DFBC524E1FA3E00ED3AE6 /* UploadAPI.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -110,6 +124,10 @@ 9B8C3FB3248DA2FE00707B13 /* URL+Apollo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B8C3FB1248DA2EA00707B13 /* URL+Apollo.swift */; }; 9B8C3FB5248DA3E000707B13 /* URLExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B8C3FB4248DA3E000707B13 /* URLExtensionsTests.swift */; }; 9B95EDC022CAA0B000702BB2 /* GETTransformerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B95EDBF22CAA0AF00702BB2 /* GETTransformerTests.swift */; }; + 9B96500A24BE62B7003C29C0 /* RequestChainTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B96500824BE6201003C29C0 /* RequestChainTests.swift */; }; + 9B96500C24BE7239003C29C0 /* LegacyCacheReadInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B96500B24BE7239003C29C0 /* LegacyCacheReadInterceptor.swift */; }; + 9B9BBAF324DB39D70021C30F /* UploadRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B9BBAF224DB39D70021C30F /* UploadRequest.swift */; }; + 9B9BBAF524DB4F890021C30F /* AutomaticPersistedQueryInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B9BBAF424DB4F890021C30F /* AutomaticPersistedQueryInterceptor.swift */; }; 9B9BBB1C24DB760B0021C30F /* UploadTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B9BBB1A24DB75E60021C30F /* UploadTests.swift */; }; 9BA1244A22D8A8EA00BF1D24 /* JSONSerialization+Sorting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BA1244922D8A8EA00BF1D24 /* JSONSerialization+Sorting.swift */; }; 9BA1245E22DE116B00BF1D24 /* Result+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BA1245D22DE116B00BF1D24 /* Result+Helpers.swift */; }; @@ -127,7 +145,12 @@ 9BAEEC15234C132600808306 /* CLIExtractorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BAEEC14234C132600808306 /* CLIExtractorTests.swift */; }; 9BAEEC17234C275600808306 /* ApolloSchemaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BAEEC16234C275600808306 /* ApolloSchemaTests.swift */; }; 9BAEEC19234C297800808306 /* ApolloCodegenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BAEEC18234C297800808306 /* ApolloCodegenTests.swift */; }; + 9BC139A424EDCA6C00876D29 /* InterceptorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BC139A224EDCA4400876D29 /* InterceptorTests.swift */; }; + 9BC139A624EDCAD900876D29 /* BlindRetryingTestInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BC139A524EDCAD900876D29 /* BlindRetryingTestInterceptor.swift */; }; + 9BC139A824EDCE4F00876D29 /* RetryToCountThenSucceedInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BC139A724EDCE4F00876D29 /* RetryToCountThenSucceedInterceptor.swift */; }; 9BC2D9D3233C6EF0007BD083 /* Basher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BC2D9D1233C6DC0007BD083 /* Basher.swift */; }; + 9BC742AC24CFB2FF0029282C /* ApolloErrorInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BC742AB24CFB2FF0029282C /* ApolloErrorInterceptor.swift */; }; + 9BC742AE24CFB6450029282C /* LegacyCacheWriteInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BC742AD24CFB6450029282C /* LegacyCacheWriteInterceptor.swift */; }; 9BCF0CE023FC9CA50031D2A2 /* TestCacheProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCF0CD923FC9CA50031D2A2 /* TestCacheProvider.swift */; }; 9BCF0CE323FC9CA50031D2A2 /* XCTAssertHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCF0CDC23FC9CA50031D2A2 /* XCTAssertHelpers.swift */; }; 9BCF0CE423FC9CA50031D2A2 /* MockURLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCF0CDD23FC9CA50031D2A2 /* MockURLSession.swift */; }; @@ -154,9 +177,13 @@ 9BE071AF2368D34D00FA5952 /* Matchable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BE071AE2368D34D00FA5952 /* Matchable.swift */; }; 9BE071B12368D3F500FA5952 /* Dictionary+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BE071B02368D3F500FA5952 /* Dictionary+Helpers.swift */; }; 9BE74D3D23FB4A8E006D354F /* FileFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BE74D3C23FB4A8E006D354F /* FileFinder.swift */; }; - 9BEDC79E22E5D2CF00549BF6 /* RequestCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BEDC79D22E5D2CF00549BF6 /* RequestCreator.swift */; }; - 9BF1A94F22CA5784005292C2 /* HTTPTransportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BF1A94C22CA54F9005292C2 /* HTTPTransportTests.swift */; }; + 9BEDC79E22E5D2CF00549BF6 /* RequestBodyCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BEDC79D22E5D2CF00549BF6 /* RequestBodyCreator.swift */; }; + 9BEEDC2824E351E5001D1294 /* MaxRetryInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BEEDC2724E351E5001D1294 /* MaxRetryInterceptor.swift */; }; + 9BEEDC2B24E61995001D1294 /* TestURLs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BEEDC2A24E61995001D1294 /* TestURLs.swift */; }; 9BF1A95122CA6E71005292C2 /* GraphQLGETTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BF1A95022CA6E71005292C2 /* GraphQLGETTransformer.swift */; }; + 9BF6C94325194DE2000D5B93 /* MultipartFormData+Testing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BF6C91725194D7B000D5B93 /* MultipartFormData+Testing.swift */; }; + 9BF6C97025194ED7000D5B93 /* MultipartFormDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BF6C95225194EA5000D5B93 /* MultipartFormDataTests.swift */; }; + 9BF6C99C25195019000D5B93 /* String+IncludesForTesting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BF6C99B25195019000D5B93 /* String+IncludesForTesting.swift */; }; 9F19D8441EED568200C57247 /* ResultOrPromise.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F19D8431EED568200C57247 /* ResultOrPromise.swift */; }; 9F19D8461EED8D3B00C57247 /* ResultOrPromiseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F19D8451EED8D3B00C57247 /* ResultOrPromiseTests.swift */; }; 9F27D4641D40379500715680 /* JSONStandardTypeConversions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F27D4631D40379500715680 /* JSONStandardTypeConversions.swift */; }; @@ -202,7 +229,6 @@ 9FC9A9C81E2EFE6E0023C4D5 /* CacheKeyForFieldTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC9A9C71E2EFE6E0023C4D5 /* CacheKeyForFieldTests.swift */; }; 9FC9A9CC1E2FD0760023C4D5 /* Record.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC9A9CB1E2FD0760023C4D5 /* Record.swift */; }; 9FC9A9D31E2FD48B0023C4D5 /* GraphQLError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC9A9D21E2FD48B0023C4D5 /* GraphQLError.swift */; }; - 9FCDFD231E33A0D8007519DC /* AsynchronousOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FCDFD221E33A0D8007519DC /* AsynchronousOperation.swift */; }; 9FCDFD291E33D0CE007519DC /* GraphQLQueryWatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FCDFD281E33D0CE007519DC /* GraphQLQueryWatcher.swift */; }; 9FCE2CEE1E6BE2D900E34457 /* NormalizedCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FCE2CED1E6BE2D800E34457 /* NormalizedCache.swift */; }; 9FCE2D091E6C254700E34457 /* StarWarsAPI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9FCE2CFA1E6C213D00E34457 /* StarWarsAPI.framework */; }; @@ -210,14 +236,13 @@ 9FE941D01E62C771007CDD89 /* Promise.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FE941CF1E62C771007CDD89 /* Promise.swift */; }; 9FEB050D1DB5732300DA3B44 /* JSONSerializationFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEB050C1DB5732300DA3B44 /* JSONSerializationFormat.swift */; }; 9FEC15B41E681DAD00D461B4 /* GroupedSequence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEC15B31E681DAD00D461B4 /* GroupedSequence.swift */; }; - 9FF33D811E48B98200F608A4 /* HTTPNetworkTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F4DAF2D1E48B84B00EBFF0B /* HTTPNetworkTransport.swift */; }; 9FF90A611DDDEB100034C3B6 /* GraphQLResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF90A5B1DDDEB100034C3B6 /* GraphQLResponse.swift */; }; 9FF90A651DDDEB100034C3B6 /* GraphQLExecutor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF90A5C1DDDEB100034C3B6 /* GraphQLExecutor.swift */; }; 9FF90A6F1DDDEB420034C3B6 /* InputValueEncodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF90A6A1DDDEB420034C3B6 /* InputValueEncodingTests.swift */; }; 9FF90A711DDDEB420034C3B6 /* ReadFieldValueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF90A6B1DDDEB420034C3B6 /* ReadFieldValueTests.swift */; }; 9FF90A731DDDEB420034C3B6 /* ParseQueryResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF90A6C1DDDEB420034C3B6 /* ParseQueryResponseTests.swift */; }; - C3279FC72345234D00224790 /* TestCustomRequestCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3279FC52345233000224790 /* TestCustomRequestCreator.swift */; }; - C338DF1722DD9DE9006AF33E /* RequestCreatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C338DF1622DD9DE9006AF33E /* RequestCreatorTests.swift */; }; + C3279FC72345234D00224790 /* TestCustomRequestBodyCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3279FC52345233000224790 /* TestCustomRequestBodyCreator.swift */; }; + C338DF1722DD9DE9006AF33E /* RequestBodyCreatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C338DF1622DD9DE9006AF33E /* RequestBodyCreatorTests.swift */; }; C35D43C222DDD4AC00BCBABE /* b.txt in Resources */ = {isa = PBXBuildFile; fileRef = C35D43BE22DDD3C100BCBABE /* b.txt */; }; C35D43C322DDD4AF00BCBABE /* c.txt in Resources */ = {isa = PBXBuildFile; fileRef = C35D43BF22DDD3C100BCBABE /* c.txt */; }; C35D43C622DDE28D00BCBABE /* a.txt in Resources */ = {isa = PBXBuildFile; fileRef = C304EBD322DDC7B200748F72 /* a.txt */; }; @@ -349,6 +374,13 @@ remoteGlobalIDString = 9B7B6F46233C26D100F32205; remoteInfo = ApolloCodegenLib; }; + 9BEEDC2C24EB6419001D1294 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 9FC7503B1D2A532C00458D91 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 9F8A95771EC0FC1200304A2D; + remoteInfo = ApolloTestSupport; + }; 9F65B11F1EC106E80090B25F /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 9FC7503B1D2A532C00458D91 /* Project object */; @@ -450,6 +482,20 @@ 9B1CCDD82360F02C007C9032 /* Bundle+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+Helpers.swift"; sourceTree = ""; }; 9B21FD742422C29D00998B5C /* GraphQLFileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphQLFileTests.swift; sourceTree = ""; }; 9B21FD762422C8CC00998B5C /* TestFileHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestFileHelper.swift; sourceTree = ""; }; + 9B260BEA245A020300562176 /* ApolloInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApolloInterceptor.swift; sourceTree = ""; }; + 9B260BEC245A021300562176 /* Parseable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Parseable.swift; sourceTree = ""; }; + 9B260BEE245A022E00562176 /* FlexibleDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlexibleDecoder.swift; sourceTree = ""; }; + 9B260BF0245A025400562176 /* HTTPRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPRequest.swift; sourceTree = ""; }; + 9B260BF2245A026F00562176 /* RequestChain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestChain.swift; sourceTree = ""; }; + 9B260BF4245A028D00562176 /* HTTPResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPResponse.swift; sourceTree = ""; }; + 9B260BF8245A030100562176 /* ResponseCodeInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseCodeInterceptor.swift; sourceTree = ""; }; + 9B260BFA245A031900562176 /* NetworkFetchInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkFetchInterceptor.swift; sourceTree = ""; }; + 9B260BFE245A054700562176 /* JSONRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONRequest.swift; sourceTree = ""; }; + 9B260C00245A059700562176 /* CodableParsingInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodableParsingInterceptor.swift; sourceTree = ""; }; + 9B260C03245A090600562176 /* RequestChainNetworkTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestChainNetworkTransport.swift; sourceTree = ""; }; + 9B260C07245A437400562176 /* InterceptorProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterceptorProvider.swift; sourceTree = ""; }; + 9B260C09245A532500562176 /* LegacyParsingInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyParsingInterceptor.swift; sourceTree = ""; }; + 9B2B66F32513FAFE00B53ABF /* CancellationHandlingInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancellationHandlingInterceptor.swift; sourceTree = ""; }; 9B2DFBB624E1FA0D00ED3AE6 /* UploadAPI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = UploadAPI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 9B2DFBC524E1FA3E00ED3AE6 /* UploadAPI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UploadAPI.h; sourceTree = ""; }; 9B2DFBC624E1FA3E00ED3AE6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -563,6 +609,10 @@ 9B8C3FB1248DA2EA00707B13 /* URL+Apollo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Apollo.swift"; sourceTree = ""; }; 9B8C3FB4248DA3E000707B13 /* URLExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLExtensionsTests.swift; sourceTree = ""; }; 9B95EDBF22CAA0AF00702BB2 /* GETTransformerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GETTransformerTests.swift; sourceTree = ""; }; + 9B96500824BE6201003C29C0 /* RequestChainTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestChainTests.swift; sourceTree = ""; }; + 9B96500B24BE7239003C29C0 /* LegacyCacheReadInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyCacheReadInterceptor.swift; sourceTree = ""; }; + 9B9BBAF224DB39D70021C30F /* UploadRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadRequest.swift; sourceTree = ""; }; + 9B9BBAF424DB4F890021C30F /* AutomaticPersistedQueryInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutomaticPersistedQueryInterceptor.swift; sourceTree = ""; }; 9B9BBB1624DB74720021C30F /* Apollo-Target-UploadAPI.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Apollo-Target-UploadAPI.xcconfig"; sourceTree = ""; }; 9B9BBB1A24DB75E60021C30F /* UploadTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadTests.swift; sourceTree = ""; }; 9BA1244922D8A8EA00BF1D24 /* JSONSerialization+Sorting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSONSerialization+Sorting.swift"; sourceTree = ""; }; @@ -583,8 +633,13 @@ 9BAEEC16234C275600808306 /* ApolloSchemaTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApolloSchemaTests.swift; sourceTree = ""; }; 9BAEEC18234C297800808306 /* ApolloCodegenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApolloCodegenTests.swift; sourceTree = ""; }; 9BB1DAC624A66B2500396235 /* ApolloMacPlayground.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; name = ApolloMacPlayground.playground; path = Playgrounds/ApolloMacPlayground.playground; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + 9BC139A224EDCA4400876D29 /* InterceptorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterceptorTests.swift; sourceTree = ""; }; + 9BC139A524EDCAD900876D29 /* BlindRetryingTestInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlindRetryingTestInterceptor.swift; sourceTree = ""; }; + 9BC139A724EDCE4F00876D29 /* RetryToCountThenSucceedInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetryToCountThenSucceedInterceptor.swift; sourceTree = ""; }; 9BC2D9CE233C3531007BD083 /* Apollo-Target-ApolloCodegen.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Apollo-Target-ApolloCodegen.xcconfig"; sourceTree = ""; }; 9BC2D9D1233C6DC0007BD083 /* Basher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Basher.swift; sourceTree = ""; }; + 9BC742AB24CFB2FF0029282C /* ApolloErrorInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApolloErrorInterceptor.swift; sourceTree = ""; }; + 9BC742AD24CFB6450029282C /* LegacyCacheWriteInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyCacheWriteInterceptor.swift; sourceTree = ""; }; 9BCF0CD923FC9CA50031D2A2 /* TestCacheProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestCacheProvider.swift; sourceTree = ""; }; 9BCF0CDA23FC9CA50031D2A2 /* ApolloTestSupport.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ApolloTestSupport.h; sourceTree = ""; }; 9BCF0CDC23FC9CA50031D2A2 /* XCTAssertHelpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XCTAssertHelpers.swift; sourceTree = ""; }; @@ -637,16 +692,19 @@ 9BE071AE2368D34D00FA5952 /* Matchable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Matchable.swift; sourceTree = ""; }; 9BE071B02368D3F500FA5952 /* Dictionary+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Dictionary+Helpers.swift"; sourceTree = ""; }; 9BE74D3C23FB4A8E006D354F /* FileFinder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileFinder.swift; sourceTree = ""; }; - 9BEDC79D22E5D2CF00549BF6 /* RequestCreator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestCreator.swift; sourceTree = ""; }; - 9BF1A94C22CA54F9005292C2 /* HTTPTransportTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPTransportTests.swift; sourceTree = ""; }; + 9BEDC79D22E5D2CF00549BF6 /* RequestBodyCreator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestBodyCreator.swift; sourceTree = ""; }; + 9BEEDC2724E351E5001D1294 /* MaxRetryInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaxRetryInterceptor.swift; sourceTree = ""; }; + 9BEEDC2A24E61995001D1294 /* TestURLs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestURLs.swift; sourceTree = ""; }; 9BF1A95022CA6E71005292C2 /* GraphQLGETTransformer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphQLGETTransformer.swift; sourceTree = ""; }; + 9BF6C91725194D7B000D5B93 /* MultipartFormData+Testing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MultipartFormData+Testing.swift"; sourceTree = ""; }; + 9BF6C95225194EA5000D5B93 /* MultipartFormDataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultipartFormDataTests.swift; sourceTree = ""; }; + 9BF6C99B25195019000D5B93 /* String+IncludesForTesting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+IncludesForTesting.swift"; sourceTree = ""; }; 9F19D8431EED568200C57247 /* ResultOrPromise.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResultOrPromise.swift; sourceTree = ""; }; 9F19D8451EED8D3B00C57247 /* ResultOrPromiseTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResultOrPromiseTests.swift; sourceTree = ""; }; 9F27D4631D40379500715680 /* JSONStandardTypeConversions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONStandardTypeConversions.swift; sourceTree = ""; }; 9F295E301E27534800A24949 /* NormalizeQueryResults.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NormalizeQueryResults.swift; sourceTree = ""; }; 9F295E371E277B2A00A24949 /* GraphQLResultNormalizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GraphQLResultNormalizer.swift; sourceTree = ""; }; 9F438D0B1E6C494C007BDC1A /* BatchedLoadTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BatchedLoadTests.swift; sourceTree = ""; }; - 9F4DAF2D1E48B84B00EBFF0B /* HTTPNetworkTransport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPNetworkTransport.swift; sourceTree = ""; }; 9F55347A1DE1DB2100E54264 /* ApolloStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApolloStore.swift; sourceTree = ""; }; 9F578D8F1D8D2CB300C0EA36 /* HTTPURLResponse+Helpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "HTTPURLResponse+Helpers.swift"; sourceTree = ""; }; 9F69FFA81D42855900E000B1 /* NetworkTransport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkTransport.swift; sourceTree = ""; }; @@ -686,7 +744,6 @@ 9FC9A9C71E2EFE6E0023C4D5 /* CacheKeyForFieldTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CacheKeyForFieldTests.swift; sourceTree = ""; }; 9FC9A9CB1E2FD0760023C4D5 /* Record.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Record.swift; sourceTree = ""; }; 9FC9A9D21E2FD48B0023C4D5 /* GraphQLError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GraphQLError.swift; sourceTree = ""; }; - 9FCDFD221E33A0D8007519DC /* AsynchronousOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AsynchronousOperation.swift; sourceTree = ""; }; 9FCDFD281E33D0CE007519DC /* GraphQLQueryWatcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GraphQLQueryWatcher.swift; sourceTree = ""; }; 9FCE2CED1E6BE2D800E34457 /* NormalizedCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NormalizedCache.swift; sourceTree = ""; }; 9FCE2CFA1E6C213D00E34457 /* StarWarsAPI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = StarWarsAPI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -700,8 +757,8 @@ 9FF90A6B1DDDEB420034C3B6 /* ReadFieldValueTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadFieldValueTests.swift; sourceTree = ""; }; 9FF90A6C1DDDEB420034C3B6 /* ParseQueryResponseTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParseQueryResponseTests.swift; sourceTree = ""; }; C304EBD322DDC7B200748F72 /* a.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = a.txt; sourceTree = ""; }; - C3279FC52345233000224790 /* TestCustomRequestCreator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestCustomRequestCreator.swift; sourceTree = ""; }; - C338DF1622DD9DE9006AF33E /* RequestCreatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestCreatorTests.swift; sourceTree = ""; }; + C3279FC52345233000224790 /* TestCustomRequestBodyCreator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestCustomRequestBodyCreator.swift; sourceTree = ""; }; + C338DF1622DD9DE9006AF33E /* RequestBodyCreatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestBodyCreatorTests.swift; sourceTree = ""; }; C35D43BE22DDD3C100BCBABE /* b.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = b.txt; sourceTree = ""; }; C35D43BF22DDD3C100BCBABE /* c.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = c.txt; sourceTree = ""; }; C377CCA822D798BD00572E03 /* GraphQLFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphQLFile.swift; sourceTree = ""; }; @@ -877,14 +934,48 @@ 9B0417812390320A00C9EC4E /* TestHelpers */ = { isa = PBXGroup; children = ( - C3279FC52345233000224790 /* TestCustomRequestCreator.swift */, - 9B64F6752354D219002D1BB5 /* URL+QueryDict.swift */, - 9B21FD762422C8CC00998B5C /* TestFileHelper.swift */, + 9BC139A524EDCAD900876D29 /* BlindRetryingTestInterceptor.swift */, + 9B2B66F32513FAFE00B53ABF /* CancellationHandlingInterceptor.swift */, 9B4F4540244A2A9200C2CF7D /* HTTPBinAPI.swift */, + 9BF6C91725194D7B000D5B93 /* MultipartFormData+Testing.swift */, + 9BC139A724EDCE4F00876D29 /* RetryToCountThenSucceedInterceptor.swift */, + 9BF6C99B25195019000D5B93 /* String+IncludesForTesting.swift */, + C3279FC52345233000224790 /* TestCustomRequestBodyCreator.swift */, + 9B21FD762422C8CC00998B5C /* TestFileHelper.swift */, + 9B64F6752354D219002D1BB5 /* URL+QueryDict.swift */, ); name = TestHelpers; sourceTree = ""; }; + 9B260BE9245A01B900562176 /* Interceptor */ = { + isa = PBXGroup; + children = ( + 9BC742B024D09F9E0029282C /* Codable */, + 9BC742AF24D09F880029282C /* Legacy */, + 9B260BEA245A020300562176 /* ApolloInterceptor.swift */, + 9BC742AB24CFB2FF0029282C /* ApolloErrorInterceptor.swift */, + 9B260C07245A437400562176 /* InterceptorProvider.swift */, + 9B9BBAF424DB4F890021C30F /* AutomaticPersistedQueryInterceptor.swift */, + 9BEEDC2724E351E5001D1294 /* MaxRetryInterceptor.swift */, + 9B260BFA245A031900562176 /* NetworkFetchInterceptor.swift */, + 9B260BF8245A030100562176 /* ResponseCodeInterceptor.swift */, + ); + name = Interceptor; + sourceTree = ""; + }; + 9B260C02245A07C200562176 /* RequestChain */ = { + isa = PBXGroup; + children = ( + 9B260BF0245A025400562176 /* HTTPRequest.swift */, + 9B260BFE245A054700562176 /* JSONRequest.swift */, + 9B9BBAF224DB39D70021C30F /* UploadRequest.swift */, + 9B260BF4245A028D00562176 /* HTTPResponse.swift */, + 9B260BF2245A026F00562176 /* RequestChain.swift */, + 9B260C03245A090600562176 /* RequestChainNetworkTransport.swift */, + ); + name = RequestChain; + sourceTree = ""; + }; 9B2DFBC424E1FA3E00ED3AE6 /* UploadAPI */ = { isa = PBXGroup; children = ( @@ -1120,6 +1211,24 @@ path = ApolloCodegenTests; sourceTree = ""; }; + 9BC742AF24D09F880029282C /* Legacy */ = { + isa = PBXGroup; + children = ( + 9B96500B24BE7239003C29C0 /* LegacyCacheReadInterceptor.swift */, + 9BC742AD24CFB6450029282C /* LegacyCacheWriteInterceptor.swift */, + 9B260C09245A532500562176 /* LegacyParsingInterceptor.swift */, + ); + name = Legacy; + sourceTree = ""; + }; + 9BC742B024D09F9E0029282C /* Codable */ = { + isa = PBXGroup; + children = ( + 9B260C00245A059700562176 /* CodableParsingInterceptor.swift */, + ); + name = Codable; + sourceTree = ""; + }; 9BCB585D240758B2002F766E /* Extensions */ = { isa = PBXGroup; children = ( @@ -1135,12 +1244,13 @@ 9BCF0CD823FC9CA50031D2A2 /* ApolloTestSupport */ = { isa = PBXGroup; children = ( - 9BCF0CD923FC9CA50031D2A2 /* TestCacheProvider.swift */, 9BCF0CDA23FC9CA50031D2A2 /* ApolloTestSupport.h */, - 9BCF0CDC23FC9CA50031D2A2 /* XCTAssertHelpers.swift */, - 9BCF0CDD23FC9CA50031D2A2 /* MockURLSession.swift */, 9BCF0CDE23FC9CA50031D2A2 /* Info.plist */, + 9BCF0CDD23FC9CA50031D2A2 /* MockURLSession.swift */, 9BCF0CDF23FC9CA50031D2A2 /* MockNetworkTransport.swift */, + 9BCF0CD923FC9CA50031D2A2 /* TestCacheProvider.swift */, + 9BEEDC2A24E61995001D1294 /* TestURLs.swift */, + 9BCF0CDC23FC9CA50031D2A2 /* XCTAssertHelpers.swift */, ); name = ApolloTestSupport; path = Sources/ApolloTestSupport; @@ -1238,6 +1348,8 @@ children = ( 9BDE43D022C6655200FD7C7F /* Cancellable.swift */, 9BE071AE2368D34D00FA5952 /* Matchable.swift */, + 9B260BEC245A021300562176 /* Parseable.swift */, + 9B260BEE245A022E00562176 /* FlexibleDecoder.swift */, ); name = Protocols; sourceTree = ""; @@ -1410,16 +1522,18 @@ 9F8622F91EC2117C00C38162 /* FragmentConstructionAndConversionTests.swift */, 9B95EDBF22CAA0AF00702BB2 /* GETTransformerTests.swift */, 9B21FD742422C29D00998B5C /* GraphQLFileTests.swift */, - 9BF1A94C22CA54F9005292C2 /* HTTPTransportTests.swift */, + 9BC139A224EDCA4400876D29 /* InterceptorTests.swift */, 9FF90A6A1DDDEB420034C3B6 /* InputValueEncodingTests.swift */, E86D8E03214B32DA0028EFE1 /* JSONTests.swift */, 9F91CF8E1F6C0DB2008DD0BE /* MutatingResultsTests.swift */, + 9BF6C95225194EA5000D5B93 /* MultipartFormDataTests.swift */, 9F295E301E27534800A24949 /* NormalizeQueryResults.swift */, 9FF90A6C1DDDEB420034C3B6 /* ParseQueryResponseTests.swift */, 9FE1C6E61E634C8D00C02284 /* PromiseTests.swift */, F16D083B21EF6F7300C458B8 /* QueryFromJSONBuildingTests.swift */, 9FF90A6B1DDDEB420034C3B6 /* ReadFieldValueTests.swift */, - C338DF1622DD9DE9006AF33E /* RequestCreatorTests.swift */, + 9B96500824BE6201003C29C0 /* RequestChainTests.swift */, + C338DF1622DD9DE9006AF33E /* RequestBodyCreatorTests.swift */, 9F19D8451EED8D3B00C57247 /* ResultOrPromiseTests.swift */, 9B4F4542244A2AD300C2CF7D /* URLSessionClientTests.swift */, 9B9BBB1A24DB75E60021C30F /* UploadTests.swift */, @@ -1447,16 +1561,17 @@ 9FC9A9CE1E2FD0CC0023C4D5 /* Network */ = { isa = PBXGroup; children = ( + 9B260C02245A07C200562176 /* RequestChain */, + 9B260BE9245A01B900562176 /* Interceptor */, C377CCA822D798BD00572E03 /* GraphQLFile.swift */, 9BF1A95022CA6E71005292C2 /* GraphQLGETTransformer.swift */, 5AC6CA4222AAF7B200B7C94D /* GraphQLHTTPMethod.swift */, 9BDE43DE22C6708600FD7C7F /* GraphQLHTTPRequestError.swift */, 9BDE43DC22C6705300FD7C7F /* GraphQLHTTPResponseError.swift */, 9FF90A5B1DDDEB100034C3B6 /* GraphQLResponse.swift */, - 9F4DAF2D1E48B84B00EBFF0B /* HTTPNetworkTransport.swift */, C377CCAA22D7992E00572E03 /* MultipartFormData.swift */, 9F69FFA81D42855900E000B1 /* NetworkTransport.swift */, - 9BEDC79D22E5D2CF00549BF6 /* RequestCreator.swift */, + 9BEDC79D22E5D2CF00549BF6 /* RequestBodyCreator.swift */, 9B4F453E244A27B900C2CF7D /* URLSessionClient.swift */, 9B554CC3247DC29A002F452A /* TaskData.swift */, ); @@ -1466,7 +1581,6 @@ 9FCDFD211E33A09F007519DC /* Utilities */ = { isa = PBXGroup; children = ( - 9FCDFD221E33A0D8007519DC /* AsynchronousOperation.swift */, 9B1CCDD82360F02C007C9032 /* Bundle+Helpers.swift */, 9BE071AC2368D08700FA5952 /* Collection+Helpers.swift */, 9BE071B02368D3F500FA5952 /* Dictionary+Helpers.swift */, @@ -1767,6 +1881,7 @@ buildRules = ( ); dependencies = ( + 9BEEDC2D24EB6419001D1294 /* PBXTargetDependency */, 9B68354D24634A2000337AE6 /* PBXTargetDependency */, 9BAEEC03234BB8FD00808306 /* PBXTargetDependency */, ); @@ -2298,6 +2413,7 @@ buildActionMask = 2147483647; files = ( 9BCF0CE423FC9CA50031D2A2 /* MockURLSession.swift in Sources */, + 9BEEDC2B24E61995001D1294 /* TestURLs.swift in Sources */, 9BCF0CE023FC9CA50031D2A2 /* TestCacheProvider.swift in Sources */, 9BCF0CE323FC9CA50031D2A2 /* XCTAssertHelpers.swift in Sources */, 9BCF0CE523FC9CA50031D2A2 /* MockNetworkTransport.swift in Sources */, @@ -2330,9 +2446,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 9FF33D811E48B98200F608A4 /* HTTPNetworkTransport.swift in Sources */, C377CCAB22D7992E00572E03 /* MultipartFormData.swift in Sources */, + 9B260C08245A437400562176 /* InterceptorProvider.swift in Sources */, + 9B9BBAF324DB39D70021C30F /* UploadRequest.swift in Sources */, 9FCE2CEE1E6BE2D900E34457 /* NormalizedCache.swift in Sources */, + 9B260C0A245A532500562176 /* LegacyParsingInterceptor.swift in Sources */, + 9B96500C24BE7239003C29C0 /* LegacyCacheReadInterceptor.swift in Sources */, 9F8F334C229044A200C0E83B /* Decoding.swift in Sources */, 9FADC84A1E6B0B2300C677E6 /* Locking.swift in Sources */, 9F295E381E277B2A00A24949 /* GraphQLResultNormalizer.swift in Sources */, @@ -2348,35 +2467,49 @@ 9FC9A9BF1E2C27FB0023C4D5 /* GraphQLResult.swift in Sources */, 9FC9A9D31E2FD48B0023C4D5 /* GraphQLError.swift in Sources */, 9FEB050D1DB5732300DA3B44 /* JSONSerializationFormat.swift in Sources */, + 9B260BEB245A020300562176 /* ApolloInterceptor.swift in Sources */, 54DDB0921EA045870009DD99 /* InMemoryNormalizedCache.swift in Sources */, 9FC9A9C51E2D6CE70023C4D5 /* GraphQLSelectionSet.swift in Sources */, + 9B260C01245A059700562176 /* CodableParsingInterceptor.swift in Sources */, 9BDE43DD22C6705300FD7C7F /* GraphQLHTTPResponseError.swift in Sources */, 9B554CC4247DC29A002F452A /* TaskData.swift in Sources */, - 9FCDFD231E33A0D8007519DC /* AsynchronousOperation.swift in Sources */, + 9B9BBAF524DB4F890021C30F /* AutomaticPersistedQueryInterceptor.swift in Sources */, 9BA1244A22D8A8EA00BF1D24 /* JSONSerialization+Sorting.swift in Sources */, + 9B260BF1245A025400562176 /* HTTPRequest.swift in Sources */, 9B708AAD2305884500604A11 /* ApolloClientProtocol.swift in Sources */, C377CCA922D798BD00572E03 /* GraphQLFile.swift in Sources */, + 9BEEDC2824E351E5001D1294 /* MaxRetryInterceptor.swift in Sources */, 9FC9A9CC1E2FD0760023C4D5 /* Record.swift in Sources */, 9FC4B9201D2A6F8D0046A641 /* JSON.swift in Sources */, + 9B260BFB245A031900562176 /* NetworkFetchInterceptor.swift in Sources */, + 9B260BED245A021300562176 /* Parseable.swift in Sources */, 9FEC15B41E681DAD00D461B4 /* GroupedSequence.swift in Sources */, 9F578D901D8D2CB300C0EA36 /* HTTPURLResponse+Helpers.swift in Sources */, + 9B260C04245A090600562176 /* RequestChainNetworkTransport.swift in Sources */, 9F7BA89922927A3700999B3B /* ResponsePath.swift in Sources */, 9FC9A9BD1E2C271C0023C4D5 /* RecordSet.swift in Sources */, 9BF1A95122CA6E71005292C2 /* GraphQLGETTransformer.swift in Sources */, + 9B260BFF245A054700562176 /* JSONRequest.swift in Sources */, + 9B260BF9245A030100562176 /* ResponseCodeInterceptor.swift in Sources */, 9FADC84F1E6B865E00C677E6 /* DataLoader.swift in Sources */, + 9B260BF3245A026F00562176 /* RequestChain.swift in Sources */, 9FF90A611DDDEB100034C3B6 /* GraphQLResponse.swift in Sources */, 9F27D4641D40379500715680 /* JSONStandardTypeConversions.swift in Sources */, - 9BEDC79E22E5D2CF00549BF6 /* RequestCreator.swift in Sources */, + 9BEDC79E22E5D2CF00549BF6 /* RequestBodyCreator.swift in Sources */, 9BE071AD2368D08700FA5952 /* Collection+Helpers.swift in Sources */, 9FA6F3681E65DF4700BF8D73 /* GraphQLResultAccumulator.swift in Sources */, 9FF90A651DDDEB100034C3B6 /* GraphQLExecutor.swift in Sources */, 9FC750611D2A59C300458D91 /* GraphQLOperation.swift in Sources */, 9BDE43DF22C6708600FD7C7F /* GraphQLHTTPRequestError.swift in Sources */, 9B1CCDD92360F02C007C9032 /* Bundle+Helpers.swift in Sources */, + 9B260BF5245A028D00562176 /* HTTPResponse.swift in Sources */, 5AC6CA4322AAF7B200B7C94D /* GraphQLHTTPMethod.swift in Sources */, + 9B260BEF245A022E00562176 /* FlexibleDecoder.swift in Sources */, 9FE941D01E62C771007CDD89 /* Promise.swift in Sources */, + 9BC742AE24CFB6450029282C /* LegacyCacheWriteInterceptor.swift in Sources */, 9BA1245E22DE116B00BF1D24 /* Result+Helpers.swift in Sources */, 9B4F453F244A27B900C2CF7D /* URLSessionClient.swift in Sources */, + 9BC742AC24CFB2FF0029282C /* ApolloErrorInterceptor.swift in Sources */, 9FC750631D2A59F600458D91 /* ApolloClient.swift in Sources */, 9BA3130E2302BEA5007B7FC5 /* DispatchQueue+Optional.swift in Sources */, 9F86B6901E65533D00B885FF /* GraphQLResponseGenerator.swift in Sources */, @@ -2391,29 +2524,36 @@ 5BB2C0232380836100774170 /* VersionNumberTests.swift in Sources */, 9B78C71E2326E86E000C8C32 /* ErrorGenerationTests.swift in Sources */, 9FC9A9C81E2EFE6E0023C4D5 /* CacheKeyForFieldTests.swift in Sources */, + 9BF6C99C25195019000D5B93 /* String+IncludesForTesting.swift in Sources */, 9F91CF8F1F6C0DB2008DD0BE /* MutatingResultsTests.swift in Sources */, 9B9BBB1C24DB760B0021C30F /* UploadTests.swift in Sources */, + 9BC139A424EDCA6C00876D29 /* InterceptorTests.swift in Sources */, F82E62E122BCD223000C311B /* AutomaticPersistedQueriesTests.swift in Sources */, + 9BC139A824EDCE4F00876D29 /* RetryToCountThenSucceedInterceptor.swift in Sources */, 9B4F4543244A2AD300C2CF7D /* URLSessionClientTests.swift in Sources */, 9F19D8461EED8D3B00C57247 /* ResultOrPromiseTests.swift in Sources */, 9F533AB31E6C4A4200CBE097 /* BatchedLoadTests.swift in Sources */, - C3279FC72345234D00224790 /* TestCustomRequestCreator.swift in Sources */, + C3279FC72345234D00224790 /* TestCustomRequestBodyCreator.swift in Sources */, 9B95EDC022CAA0B000702BB2 /* GETTransformerTests.swift in Sources */, 9FF90A6F1DDDEB420034C3B6 /* InputValueEncodingTests.swift in Sources */, 9FE1C6E71E634C8D00C02284 /* PromiseTests.swift in Sources */, 9B64F6762354D219002D1BB5 /* URL+QueryDict.swift in Sources */, 9FADC8541E6B86D900C677E6 /* DataLoaderTests.swift in Sources */, + 9B2B66F42513FAFE00B53ABF /* CancellationHandlingInterceptor.swift in Sources */, 9B21FD772422C8CC00998B5C /* TestFileHelper.swift in Sources */, + 9BC139A624EDCAD900876D29 /* BlindRetryingTestInterceptor.swift in Sources */, + 9B96500A24BE62B7003C29C0 /* RequestChainTests.swift in Sources */, 9B21FD752422C29D00998B5C /* GraphQLFileTests.swift in Sources */, E86D8E05214B32FD0028EFE1 /* JSONTests.swift in Sources */, 9F8622FA1EC2117C00C38162 /* FragmentConstructionAndConversionTests.swift in Sources */, - C338DF1722DD9DE9006AF33E /* RequestCreatorTests.swift in Sources */, + C338DF1722DD9DE9006AF33E /* RequestBodyCreatorTests.swift in Sources */, F16D083C21EF6F7300C458B8 /* QueryFromJSONBuildingTests.swift in Sources */, + 9BF6C97025194ED7000D5B93 /* MultipartFormDataTests.swift in Sources */, 9FF90A711DDDEB420034C3B6 /* ReadFieldValueTests.swift in Sources */, 9F295E311E27534800A24949 /* NormalizeQueryResults.swift in Sources */, 9FF90A731DDDEB420034C3B6 /* ParseQueryResponseTests.swift in Sources */, + 9BF6C94325194DE2000D5B93 /* MultipartFormData+Testing.swift in Sources */, 9B4F4541244A2A9200C2CF7D /* HTTPBinAPI.swift in Sources */, - 9BF1A94F22CA5784005292C2 /* HTTPTransportTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2521,6 +2661,11 @@ target = 9B7B6F46233C26D100F32205 /* ApolloCodegenLib */; targetProxy = 9BAEEC02234BB8FD00808306 /* PBXContainerItemProxy */; }; + 9BEEDC2D24EB6419001D1294 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 9F8A95771EC0FC1200304A2D /* ApolloTestSupport */; + targetProxy = 9BEEDC2C24EB6419001D1294 /* PBXContainerItemProxy */; + }; 9F65B1201EC106E80090B25F /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 9FC750431D2A532C00458D91 /* Apollo */; diff --git a/CHANGELOG.md b/CHANGELOG.md index 922e21d015..2860126438 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,41 @@ # Change log +## v0.34.0-rc.2 + +Networking Stack, Release Candidate + +- Made `RequestChainNetworkTransport` subclassable and changed two methods to be `open` so they can be subclassed in order to facilitate using subclasses of `HTTPRequest` when needed. ([#1405](https://github.com/apollographql/apollo-ios/pull/1405)) +- Made numerous improvements to creating upload requests - all upload request setup is now happening through the `UploadRequest` class, which is now `open` for your subclassing funtimes. ([#1405](https://github.com/apollographql/apollo-ios/pull/1405)) +- Renamed `RequestCreator` to `RequestBodyCreator` to more accurately reflect what it's doing (particularly in light of the fact that we didn't have a `Request` in the old networking stack, and now we do), and renamed associated properties and parameters. ([#1405](https://github.com/apollographql/apollo-ios/pull/1405)) + +## v0.34.0-rc.1 + +Networking Stack, Release Candidate + +- Added some final tweaks: + - Updated `ApolloStore` to take a default cache of the `InMemoryNormalizedCache`. + - Updated LegacyInterceptorProvider to take a default store of the `ApolloStore` with that default cache. + - Added a method to `InterceptorProvider` to provide an error interceptor, along with a default implementation that returns `nil`. + - Updated `JSONRequest` to be open so it can be subclassed. + + This is now at the point where if there are no further major bugs, I'd like to release this - get your bugs in ASAP! ([#1399](https://github.com/apollographql/apollo-ios/pull/1399) + +## v0.34.0-beta2 + +Networking Stack, Beta 2 + +- Merges `0.33.0` changes into the networking stack for Swift 5.3 and Xcode 12. + ## v0.33.0 - Adds support for Xcode 12 and Swift 5.3. ([#1280](https://github.com/apollographql/apollo-ios/pull/1280)) - Adds workaround script for Carthage support in Xcode 12. Please see [Carthage-3019](https://github.com/Carthage/Carthage/issues/3019) for details. TL;DR: cd into `[YourProject]/Carthage/Checkouts/apollo-ios/scripts` and then run `./carthage-build-workaround.sh` to actually get Carthage builds that work. (#yolo committed to `main`) +### 0.33.0-beta1 + +Networking Stack, Beta 1 + +- **SPECTACULARLY BREAKING**: The networking stack for HTTP requests has been completely rewritten. This is described in great detail in the [RFC for the networking changes](https://github.com/apollographql/apollo-ios/issues/1340), as well as the [updated documentation for Advanced Client Creation](https://deploy-preview-1386--apollo-ios-docs.netlify.app/docs/ios/initialization/#advanced-client-creation). Please, please, please file bugs or requests for clarification of the docs as soon as possible. Note that all changes until the networking stack comes out of beta will live on the `betas/networking-stack` branch. ([#1341](https://github.com/apollographql/apollo-ios/issues/1341)) + ## v0.32.1 - Improves invalidation of a `URLSesionClient` to include cancellation of in-flight operations. ([#1376](https://github.com/apollographql/apollo-ios/issues/1376)) diff --git a/Configuration/Shared/Project-Version.xcconfig b/Configuration/Shared/Project-Version.xcconfig index 4ac83829ef..22e9e40147 100644 --- a/Configuration/Shared/Project-Version.xcconfig +++ b/Configuration/Shared/Project-Version.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 0.33.0 +CURRENT_PROJECT_VERSION = 0.34.0 diff --git a/Package.swift b/Package.swift index d01610c76d..e8c1d20234 100644 --- a/Package.swift +++ b/Package.swift @@ -103,6 +103,7 @@ let package = Package( .testTarget( name: "ApolloCodegenTests", dependencies: [ + "ApolloTestSupport", "ApolloCodegenLib" ]), .testTarget( diff --git a/Playgrounds/ApolloMacPlayground.playground/Pages/SQLiteCache.xcplaygroundpage/Contents.swift b/Playgrounds/ApolloMacPlayground.playground/Pages/SQLiteCache.xcplaygroundpage/Contents.swift index e7f7600311..c9c08c10f6 100644 --- a/Playgrounds/ApolloMacPlayground.playground/Pages/SQLiteCache.xcplaygroundpage/Contents.swift +++ b/Playgrounds/ApolloMacPlayground.playground/Pages/SQLiteCache.xcplaygroundpage/Contents.swift @@ -6,10 +6,6 @@ import PlaygroundSupport //: # Setting up a client with a SQLite cache -//: First, you'll need to set up a network transport, since you will also need that to set up the client: -let serverURL = URL(string: "http://localhost:8080/graphql")! -let networkTransport = HTTPNetworkTransport(url: serverURL) - //: You'll need to make sure you import the ApolloSQLite library IF you are not using CocoaPods (CocoaPods will automatically flatten everything down to a single Apollo import): import ApolloSQLite @@ -26,12 +22,16 @@ let sqliteCache = try SQLiteNormalizedCache(fileURL: sqliteFileURL) //: And then instantiate an instance of `ApolloStore` with the cache you've just created: let store = ApolloStore(cache: sqliteCache) +//: Next, you'll need to set up a network transport, since you will also need that to set up the client: +let serverURL = URL(string: "http://localhost:8080/graphql")! +let networkTransport = RequestChainNetworkTransport(interceptorProvider: LegacyInterceptorProvider(store: store), endpointURL: serverURL) + //: Finally, pass that into your `ApolloClient` initializer, and you're now set up to use the SQLite cache for persistent storage: let apolloClient = ApolloClient(networkTransport: networkTransport, store: store) - -//: Now, let's test +//: Now, let's test it out against the Star Wars API! import StarWarsAPI + let query = HeroDetailsQuery(episode: .newhope) apolloClient.fetch(query: query) { result in // This is the outer Result, which has either a `GraphQLResult` or an `Error` diff --git a/Playgrounds/ApolloMacPlayground.playground/Pages/Subscriptions.xcplaygroundpage/Contents.swift b/Playgrounds/ApolloMacPlayground.playground/Pages/Subscriptions.xcplaygroundpage/Contents.swift index a188659b7b..4e274452c7 100644 --- a/Playgrounds/ApolloMacPlayground.playground/Pages/Subscriptions.xcplaygroundpage/Contents.swift +++ b/Playgrounds/ApolloMacPlayground.playground/Pages/Subscriptions.xcplaygroundpage/Contents.swift @@ -17,15 +17,15 @@ Your web backend must declare support for subscriptions in the Schema just like To use subscriptions, you need to have a `NetworkTransport` implementation which supports them. Fortunately, with the `ApolloWebSocket` package, there are two! -The first is the `WebSocketTransport`, which works with the web socket, and the second is the `SplitNetworkTransport`, which uses a web socket for subscriptions but a normal `HTTPNetworkTransport` for everything else. +The first is the `WebSocketTransport`, which works with the web socket, and the second is the `SplitNetworkTransport`, which uses a web socket for subscriptions but a normal `RequestChainNetworkTransport` for everything else. In this instance, we'll use a `SplitNetworkTransport` since we want to demonstrate subscribing to changes, but we need to also be able to send changes for that subscription to come through. */ -//:First, setup the `HTTPNetworkTransport`: +//:First, setup the `RequestChainNetworkTransport` that will handle your HTTP requests: let url = URL(string: "http://localhost:8080/graphql")! -let normalTransport = HTTPNetworkTransport(url: url) +let normalTransport = RequestChainNetworkTransport(interceptorProvider: LegacyInterceptorProvider(), endpointURL: url) //: Next, set up the `WebSocketTransport` to talk to the websocket endpoint. Note that this may take a different URL, sometimes with a `ws` prefix, than your normal http endpoint: @@ -34,7 +34,7 @@ let webSocketTransport = WebSocketTransport(request: URLRequest(url: webSocketUR //: Then, set up the split transport with the two transports you've just created: -let splitTransport = SplitNetworkTransport(httpNetworkTransport: normalTransport, webSocketNetworkTransport: webSocketTransport) +let splitTransport = SplitNetworkTransport(uploadingNetworkTransport: normalTransport, webSocketNetworkTransport: webSocketTransport) //: Finally, instantiate your client with the split transport: diff --git a/Sources/Apollo/ApolloClient.swift b/Sources/Apollo/ApolloClient.swift index 2c8bb3915f..1ffff9f3e8 100644 --- a/Sources/Apollo/ApolloClient.swift +++ b/Sources/Apollo/ApolloClient.swift @@ -13,6 +13,11 @@ public enum CachePolicy { case returnCacheDataDontFetch /// Return data from the cache if available, and always fetch results from the server. case returnCacheDataAndFetch + + /// The current default cache policy. + public static var `default`: CachePolicy { + .returnCacheDataElseFetch + } } /// A handler for operation results. @@ -28,9 +33,6 @@ public class ApolloClient { public let store: ApolloStore // <- conformance to ApolloClientProtocol - private let queue: DispatchQueue - private let operationQueue: OperationQueue - public enum ApolloClientError: Error, LocalizedError { case noUploadTransport @@ -50,69 +52,18 @@ public class ApolloClient { public init(networkTransport: NetworkTransport, store: ApolloStore = ApolloStore(cache: InMemoryNormalizedCache())) { self.networkTransport = networkTransport self.store = store - - queue = DispatchQueue(label: "com.apollographql.ApolloClient") - operationQueue = OperationQueue() - operationQueue.underlyingQueue = queue } /// Creates a client with an HTTP network transport connecting to the specified URL. /// /// - Parameter url: The URL of a GraphQL server to connect to. public convenience init(url: URL) { - self.init(networkTransport: HTTPNetworkTransport(url: url)) - } - - fileprivate func send(operation: Operation, - shouldPublishResultToStore: Bool, - context: UnsafeMutableRawPointer?, - resultHandler: @escaping GraphQLResultHandler) -> Cancellable { - return networkTransport.send(operation: operation) { [weak self] result in - guard let self = self else { - return - } - self.handleOperationResult(shouldPublishResultToStore: shouldPublishResultToStore, - context: context, - result, - resultHandler: resultHandler) - } - } - - private func handleOperationResult(shouldPublishResultToStore: Bool, - context: UnsafeMutableRawPointer?, - _ result: Result, Error>, - resultHandler: @escaping GraphQLResultHandler) { - switch result { - case .failure(let error): - resultHandler(.failure(error)) - case .success(let response): - // If there is no need to publish the result to the store, we can use a fast path. - if !shouldPublishResultToStore { - do { - let result = try response.parseResultFast() - resultHandler(.success(result)) - } catch { - resultHandler(.failure(error)) - } - return - } - - firstly { - try response.parseResult(cacheKeyForObject: self.cacheKeyForObject) - }.andThen { [weak self] (result, records) in - guard let self = self else { - return - } - if let records = records { - self.store.publish(records: records, context: context).catch { error in - preconditionFailure(String(describing: error)) - } - } - resultHandler(.success(result)) - }.catch { error in - resultHandler(.failure(error)) - } - } + let store = ApolloStore(cache: InMemoryNormalizedCache()) + let provider = LegacyInterceptorProvider(store: store) + let transport = RequestChainNetworkTransport(interceptorProvider: provider, + endpointURL: url) + + self.init(networkTransport: transport, store: store) } } @@ -124,180 +75,81 @@ extension ApolloClient: ApolloClientProtocol { get { return self.store.cacheKeyForObject } - set { self.store.cacheKeyForObject = newValue } } - public func clearCache(callbackQueue: DispatchQueue = .main, completion: ((Result) -> Void)? = nil) { + public func clearCache(callbackQueue: DispatchQueue = .main, + completion: ((Result) -> Void)? = nil) { self.store.clearCache(completion: completion) } - + @discardableResult public func fetch(query: Query, cachePolicy: CachePolicy = .returnCacheDataElseFetch, - context: UnsafeMutableRawPointer? = nil, + contextIdentifier: UUID? = nil, queue: DispatchQueue = DispatchQueue.main, resultHandler: GraphQLResultHandler? = nil) -> Cancellable { - let resultHandler = wrapResultHandler(resultHandler, queue: queue) - - // If we don't have to go through the cache, there is no need to create an operation - // and we can return a network task directly - if cachePolicy == .fetchIgnoringCacheData || cachePolicy == .fetchIgnoringCacheCompletely { - return self.send(operation: query, shouldPublishResultToStore: cachePolicy != .fetchIgnoringCacheCompletely, context: context, resultHandler: resultHandler) - } else { - let operation = FetchQueryOperation(client: self, query: query, cachePolicy: cachePolicy, context: context, resultHandler: resultHandler) - self.operationQueue.addOperation(operation) - return operation + return self.networkTransport.send(operation: query, + cachePolicy: cachePolicy, + contextIdentifier: contextIdentifier, + callbackQueue: queue) { result in + resultHandler?(result) } } public func watch(query: Query, cachePolicy: CachePolicy = .returnCacheDataElseFetch, - queue: DispatchQueue = .main, resultHandler: @escaping GraphQLResultHandler) -> GraphQLQueryWatcher { let watcher = GraphQLQueryWatcher(client: self, query: query, - resultHandler: wrapResultHandler(resultHandler, queue: queue)) + resultHandler: resultHandler) watcher.fetch(cachePolicy: cachePolicy) return watcher } @discardableResult public func perform(mutation: Mutation, - context: UnsafeMutableRawPointer? = nil, - queue: DispatchQueue = DispatchQueue.main, + queue: DispatchQueue = .main, resultHandler: GraphQLResultHandler? = nil) -> Cancellable { - return self.send(operation: mutation, - shouldPublishResultToStore: true, - context: context, - resultHandler: wrapResultHandler(resultHandler, queue: queue)) + return self.networkTransport.send(operation: mutation, + cachePolicy: .default, + contextIdentifier: nil, + callbackQueue: queue) { result in + resultHandler?(result) + } } @discardableResult public func upload(operation: Operation, - context: UnsafeMutableRawPointer? = nil, files: [GraphQLFile], queue: DispatchQueue = .main, resultHandler: GraphQLResultHandler? = nil) -> Cancellable { - let wrappedHandler = wrapResultHandler(resultHandler, queue: queue) guard let uploadingTransport = self.networkTransport as? UploadingNetworkTransport else { assertionFailure("Trying to upload without an uploading transport. Please make sure your network transport conforms to `UploadingNetworkTransport`.") - wrappedHandler(.failure(ApolloClientError.noUploadTransport)) + queue.async { + resultHandler?(.failure(ApolloClientError.noUploadTransport)) + } return EmptyCancellable() } - return uploadingTransport.upload(operation: operation, files: files) { [weak self] result in - guard let self = self else { - return - } - self.handleOperationResult(shouldPublishResultToStore: true, - context: context, result, - resultHandler: wrappedHandler) + return uploadingTransport.upload(operation: operation, + files: files, + callbackQueue: queue) { result in + resultHandler?(result) } } - + @discardableResult public func subscribe(subscription: Subscription, queue: DispatchQueue = .main, resultHandler: @escaping GraphQLResultHandler) -> Cancellable { - return self.send(operation: subscription, - shouldPublishResultToStore: true, - context: nil, - resultHandler: wrapResultHandler(resultHandler, queue: queue)) + return self.networkTransport.send(operation: subscription, + cachePolicy: .default, + contextIdentifier: nil, + callbackQueue: queue, + completionHandler: resultHandler) } } -private func wrapResultHandler(_ resultHandler: GraphQLResultHandler?, queue handlerQueue: DispatchQueue) -> GraphQLResultHandler { - guard let resultHandler = resultHandler else { - return { _ in } - } - - return { result in - handlerQueue.async { - resultHandler(result) - } - } -} -private final class FetchQueryOperation: AsynchronousOperation, Cancellable { - weak var client: ApolloClient? - let query: Query - let cachePolicy: CachePolicy - let context: UnsafeMutableRawPointer? - let resultHandler: GraphQLResultHandler - - private var networkTask: Cancellable? - - init(client: ApolloClient, - query: Query, - cachePolicy: CachePolicy, - context: UnsafeMutableRawPointer?, - resultHandler: @escaping GraphQLResultHandler) { - self.client = client - self.query = query - self.cachePolicy = cachePolicy - self.context = context - self.resultHandler = resultHandler - } - - override public func start() { - if isCancelled { - state = .finished - return - } - - state = .executing - - if cachePolicy == .fetchIgnoringCacheData { - fetchFromNetwork() - return - } - - client?.store.load(query: query) { [weak self] result in - guard let self = self else { - return - } - if self.isCancelled { - self.state = .finished - return - } - - switch result { - case .success: - self.resultHandler(result) - - if self.cachePolicy != .returnCacheDataAndFetch { - self.state = .finished - return - } - case .failure: - if self.cachePolicy == .returnCacheDataDontFetch { - self.resultHandler(result) - self.state = .finished - return - } - } - - self.fetchFromNetwork() - } - } - - func fetchFromNetwork() { - networkTask = client?.send(operation: query, - shouldPublishResultToStore: true, - context: context) { [weak self] result in - guard let self = self else { - return - } - self.resultHandler(result) - self.state = .finished - return - } - } - - override public func cancel() { - super.cancel() - networkTask?.cancel() - } -} diff --git a/Sources/Apollo/ApolloClientProtocol.swift b/Sources/Apollo/ApolloClientProtocol.swift index b73b3fa1e8..db4961436b 100644 --- a/Sources/Apollo/ApolloClientProtocol.swift +++ b/Sources/Apollo/ApolloClientProtocol.swift @@ -22,13 +22,13 @@ public protocol ApolloClientProtocol: class { /// - Parameters: /// - query: The query to fetch. /// - cachePolicy: A cache policy that specifies when results should be fetched from the server and when data should be loaded from the local cache. - /// - context: [optional] A context to use for the cache to work with results. Should default to nil. /// - queue: A dispatch queue on which the result handler will be called. Defaults to the main queue. + /// - contextIdentifier: [optional] A unique identifier for this request, to help with deduping cache hits for watchers. Should default to `nil`. /// - resultHandler: [optional] A closure that is called when query results are available or when an error occurs. /// - Returns: An object that can be used to cancel an in progress fetch. func fetch(query: Query, cachePolicy: CachePolicy, - context: UnsafeMutableRawPointer?, + contextIdentifier: UUID?, queue: DispatchQueue, resultHandler: GraphQLResultHandler?) -> Cancellable @@ -36,26 +36,21 @@ public protocol ApolloClientProtocol: class { /// /// - Parameters: /// - query: The query to fetch. - /// - fetchHTTPMethod: The HTTP Method to be used. /// - cachePolicy: A cache policy that specifies when results should be fetched from the server or from the local cache. - /// - queue: A dispatch queue on which the result handler will be called. Should default to the main queue. /// - resultHandler: [optional] A closure that is called when query results are available or when an error occurs. /// - Returns: A query watcher object that can be used to control the watching behavior. func watch(query: Query, cachePolicy: CachePolicy, - queue: DispatchQueue, resultHandler: @escaping GraphQLResultHandler) -> GraphQLQueryWatcher /// Performs a mutation by sending it to the server. /// /// - Parameters: /// - mutation: The mutation to perform. - /// - context: [optional] A context to use for the cache to work with results. Should default to nil. /// - queue: A dispatch queue on which the result handler will be called. Defaults to the main queue. /// - resultHandler: An optional closure that is called when mutation results are available or when an error occurs. /// - Returns: An object that can be used to cancel an in progress mutation. func perform(mutation: Mutation, - context: UnsafeMutableRawPointer?, queue: DispatchQueue, resultHandler: GraphQLResultHandler?) -> Cancellable @@ -63,14 +58,11 @@ public protocol ApolloClientProtocol: class { /// /// - Parameters: /// - operation: The operation to send - /// - context: [optional] A context to use for the cache to work with results. Should default to nil. /// - files: An array of `GraphQLFile` objects to send. /// - queue: A dispatch queue on which the result handler will be called. Should default to the main queue. - /// - completionHandler: The completion handler to execute when the request completes or errors + /// - completionHandler: The completion handler to execute when the request completes or errors. Note that an error will be returned If your `networkTransport` does not also conform to `UploadingNetworkTransport`. /// - Returns: An object that can be used to cancel an in progress request. - /// - Throws: If your `networkTransport` does not also conform to `UploadingNetworkTransport`. func upload(operation: Operation, - context: UnsafeMutableRawPointer?, files: [GraphQLFile], queue: DispatchQueue, resultHandler: GraphQLResultHandler?) -> Cancellable diff --git a/Sources/Apollo/ApolloErrorInterceptor.swift b/Sources/Apollo/ApolloErrorInterceptor.swift new file mode 100644 index 0000000000..eee485b2a2 --- /dev/null +++ b/Sources/Apollo/ApolloErrorInterceptor.swift @@ -0,0 +1,20 @@ +import Foundation + +/// An error interceptor called to allow further examination of error data when an error occurs in the chain. +public protocol ApolloErrorInterceptor { + + /// Asynchronously handles the receipt of an error at any point in the chain. + /// + /// - Parameters: + /// - error: The received error + /// - chain: The chain the error was received on + /// - request: The request, as far as it was constructed + /// - response: [optional] The response, if one was received + /// - completion: The completion closure to fire when the operation has completed. Note that if you call `retry` on the chain, you will not want to call the completion block in this method. + func handleErrorAsync( + error: Error, + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) +} diff --git a/Sources/Apollo/ApolloInterceptor.swift b/Sources/Apollo/ApolloInterceptor.swift new file mode 100644 index 0000000000..00819d3fec --- /dev/null +++ b/Sources/Apollo/ApolloInterceptor.swift @@ -0,0 +1,16 @@ +/// A protocol to set up a chainable unit of networking work. +public protocol ApolloInterceptor: class { + + /// Called when this interceptor should do its work. + /// + /// - Parameters: + /// - chain: The chain the interceptor is a part of. + /// - request: The request, as far as it has been constructed + /// - response: [optional] The response, if received + /// - completion: The completion block to fire when data needs to be returned to the UI. + func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) +} diff --git a/Sources/Apollo/ApolloStore.swift b/Sources/Apollo/ApolloStore.swift index 4a42462769..93f1fd7382 100644 --- a/Sources/Apollo/ApolloStore.swift +++ b/Sources/Apollo/ApolloStore.swift @@ -1,8 +1,8 @@ -import Dispatch +import Foundation /// A function that returns a cache key for a particular result object. If it returns `nil`, a default cache key based on the field path will be used. public typealias CacheKeyForObject = (_ object: JSONObject) -> JSONValue? -public typealias DidChangeKeysFunc = (Set, UnsafeMutableRawPointer?) -> Void +public typealias DidChangeKeysFunc = (Set, UUID?) -> Void func rootCacheKey(for operation: Operation) -> String { switch operation.operationType { @@ -16,9 +16,16 @@ func rootCacheKey(for operation: Operation) -> Stri } protocol ApolloStoreSubscriber: class { + + /// A callback that can be received by subcribers when keys are changed within the database + /// + /// - Parameters: + /// - store: The store which made the changes + /// - changedKeys: The list of changed keys + /// - contextIdentifier: [optional] A unique identifier for the request that kicked off this change, to assist in de-duping cache hits for watchers. func store(_ store: ApolloStore, didChangeKeys changedKeys: Set, - context: UnsafeMutableRawPointer?) + contextIdentifier: UUID?) } /// The `ApolloStore` class acts as a local cache for normalized GraphQL results. @@ -37,15 +44,15 @@ public final class ApolloStore { /// Designated initializer /// - /// - Parameter cache: An instance of `normalizedCache` to use to cache results. - public init(cache: NormalizedCache) { + /// - Parameter cache: An instance of `normalizedCache` to use to cache results. Defaults to an `InMemoryNormalizedCache`. + public init(cache: NormalizedCache = InMemoryNormalizedCache()) { self.cache = cache queue = DispatchQueue(label: "com.apollographql.ApolloStore", attributes: .concurrent) } - fileprivate func didChangeKeys(_ changedKeys: Set, context: UnsafeMutableRawPointer?) { + fileprivate func didChangeKeys(_ changedKeys: Set, identifier: UUID?) { for subscriber in self.subscribers { - subscriber.store(self, didChangeKeys: changedKeys, context: context) + subscriber.store(self, didChangeKeys: changedKeys, contextIdentifier: identifier) } } @@ -64,13 +71,13 @@ public final class ApolloStore { } } - func publish(records: RecordSet, context: UnsafeMutableRawPointer? = nil) -> Promise { + func publish(records: RecordSet, identifier: UUID? = nil) -> Promise { return Promise { fulfill, reject in queue.async(flags: .barrier) { self.cacheLock.withWriteLock { self.cache.mergePromise(records: records) }.andThen { changedKeys in - self.didChangeKeys(changedKeys, context: context) + self.didChangeKeys(changedKeys, identifier: identifier) fulfill(()) }.wait() } @@ -164,16 +171,16 @@ public final class ApolloStore { } } - func load(query: Query) -> Promise> { + func load(query: Operation) -> Promise> { return withinReadTransactionPromise { transaction in - let mapper = GraphQLSelectionSetMapper() + let mapper = GraphQLSelectionSetMapper() let dependencyTracker = GraphQLDependencyTracker() - return try transaction.execute(selections: Query.Data.selections, + return try transaction.execute(selections: Operation.Data.selections, onObjectWithKey: rootCacheKey(for: query), variables: query.variables, accumulator: zip(mapper, dependencyTracker)) - }.map { (data: Query.Data, dependentKeys: Set) in + }.map { (data: Operation.Data, dependentKeys: Set) in GraphQLResult(data: data, extensions: nil, errors: nil, @@ -187,7 +194,7 @@ public final class ApolloStore { /// - Parameters: /// - query: The query to load results for /// - resultHandler: The completion handler to execute on success or error - public func load(query: Query, resultHandler: @escaping GraphQLResultHandler) { + public func load(query: Operation, resultHandler: @escaping GraphQLResultHandler) { load(query: query).andThen { result in resultHandler(.success(result)) }.catch { error in diff --git a/Sources/Apollo/AsynchronousOperation.swift b/Sources/Apollo/AsynchronousOperation.swift deleted file mode 100644 index 1fabbb2881..0000000000 --- a/Sources/Apollo/AsynchronousOperation.swift +++ /dev/null @@ -1,47 +0,0 @@ -import Foundation - -class AsynchronousOperation: Operation { - @objc class func keyPathsForValuesAffectingIsExecuting() -> Set { - return ["state"] - } - - @objc class func keyPathsForValuesAffectingIsFinished() -> Set { - return ["state"] - } - - enum State { - case initialized - case ready - case executing - case finished - } - - var state: State = .initialized { - willSet { - willChangeValue(forKey: "state") - } - didSet { - didChangeValue(forKey: "state") - } - } - - override var isAsynchronous: Bool { - return true - } - - override var isReady: Bool { - let ready = super.isReady - if ready { - state = .ready - } - return ready - } - - override var isExecuting: Bool { - return state == .executing - } - - override var isFinished: Bool { - return state == .finished - } -} diff --git a/Sources/Apollo/AutomaticPersistedQueryInterceptor.swift b/Sources/Apollo/AutomaticPersistedQueryInterceptor.swift new file mode 100644 index 0000000000..886e855ab7 --- /dev/null +++ b/Sources/Apollo/AutomaticPersistedQueryInterceptor.swift @@ -0,0 +1,78 @@ +import Foundation + +public class AutomaticPersistedQueryInterceptor: ApolloInterceptor { + + public enum APQError: LocalizedError { + case noParsedResponse + case persistedQueryRetryFailed(operationName: String) + + public var errorDescription: String? { + switch self { + case .noParsedResponse: + return "The Automatic Persisted Query Interceptor was called before a response was received. Double-check the order of your interceptors." + case .persistedQueryRetryFailed(let operationName): + return "Persisted query retry failed for operation \"\(operationName)\"." + } + } + } + + /// Designated initializer + public init() {} + + public func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) { + + guard + let jsonRequest = request as? JSONRequest, + jsonRequest.autoPersistQueries else { + // Not a request that handles APQs, continue along + chain.proceedAsync(request: request, + response: response, + completion: completion) + return + } + + guard let result = response?.parsedResponse else { + // This is in the wrong order - this needs to be parsed before we can check it. + chain.handleErrorAsync(APQError.noParsedResponse, + request: request, + response: response, + completion: completion) + return + } + + guard let errors = result.errors else { + // No errors were returned so no retry is necessary, continue along. + chain.proceedAsync(request: request, + response: response, + completion: completion) + return + } + + let errorMessages = errors.compactMap { $0.message } + guard errorMessages.contains("PersistedQueryNotFound") else { + // The errors were not APQ errors, continue along. + chain.proceedAsync(request: request, + response: response, + completion: completion) + return + } + + guard !jsonRequest.isPersistedQueryRetry else { + // We already retried this and it didn't work. + chain.handleErrorAsync(APQError.persistedQueryRetryFailed(operationName: jsonRequest.operation.operationName), + request: jsonRequest, + response: response, + completion: completion) + return + } + + // We need to retry this query with the full body. + jsonRequest.isPersistedQueryRetry = true + chain.retry(request: jsonRequest, + completion: completion) + } +} diff --git a/Sources/Apollo/CodableParsingInterceptor.swift b/Sources/Apollo/CodableParsingInterceptor.swift new file mode 100644 index 0000000000..cff53482c2 --- /dev/null +++ b/Sources/Apollo/CodableParsingInterceptor.swift @@ -0,0 +1,54 @@ +import Foundation + +public class CodableParsingInterceptor: ApolloInterceptor { + + public enum CodableParsingError: Error, LocalizedError { + case noResponseToParse + + public var errorDescription: String? { + switch self { + case .noResponseToParse: + return "The Codable Parsing Interceptor was called before a response was received to be parsed. Double-check the order of your interceptors." + } + } + } + + let decoder: FlexDecoder + + var isCancelled: Bool = false + + public init(decoder: FlexDecoder) { + self.decoder = decoder + } + + public func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) { + guard !self.isCancelled else { + return + } + + guard let createdResponse = response else { + chain.handleErrorAsync(CodableParsingError.noResponseToParse, + request: request, + response: response, + completion: completion) + return + } + + do { + let parsedData = try GraphQLResult(from: createdResponse.rawData, decoder: self.decoder) + createdResponse.parsedResponse = parsedData + chain.proceedAsync(request: request, + response: response, + completion: completion) + } catch { + chain.handleErrorAsync(error, + request: request, + response: createdResponse, + completion: completion) + } + } +} diff --git a/Sources/Apollo/FlexibleDecoder.swift b/Sources/Apollo/FlexibleDecoder.swift new file mode 100644 index 0000000000..ff8f1ab135 --- /dev/null +++ b/Sources/Apollo/FlexibleDecoder.swift @@ -0,0 +1,17 @@ +import Foundation + +// Adapted from Combine's `TopLevelDecoder` protocol to allow easy swapping of +// decoders which decode in similar fashions. +public protocol FlexibleDecoder { + associatedtype Input + + func decode(_ type: T.Type, from data: Data) throws -> T where T : Decodable +} + +extension JSONDecoder: FlexibleDecoder { + public typealias Input = Data +} + +extension PropertyListDecoder: FlexibleDecoder { + public typealias Input = Data +} diff --git a/Sources/Apollo/GraphQLHTTPRequestError.swift b/Sources/Apollo/GraphQLHTTPRequestError.swift index dca673e0a6..23200a4d99 100644 --- a/Sources/Apollo/GraphQLHTTPRequestError.swift +++ b/Sources/Apollo/GraphQLHTTPRequestError.swift @@ -2,14 +2,11 @@ import Foundation /// An error which has occurred during the serialization of a request. public enum GraphQLHTTPRequestError: Error, LocalizedError { - case cancelledByDelegate case serializedBodyMessageError case serializedQueryParamsMessageError public var errorDescription: String? { switch self { - case .cancelledByDelegate: - return "The request was cancelled by the HTTPNetworkTransportPreflightDelegate." case .serializedBodyMessageError: return "JSONSerialization error: Error while serializing request's body" case .serializedQueryParamsMessageError: diff --git a/Sources/Apollo/GraphQLQueryWatcher.swift b/Sources/Apollo/GraphQLQueryWatcher.swift index 5d73cb01a2..5135bf9414 100644 --- a/Sources/Apollo/GraphQLQueryWatcher.swift +++ b/Sources/Apollo/GraphQLQueryWatcher.swift @@ -1,4 +1,4 @@ -import Dispatch +import Foundation /// A `GraphQLQueryWatcher` is responsible for watching the store, and calling the result handler with a new result whenever any of the data the previous result depends on changes. /// @@ -8,7 +8,7 @@ public final class GraphQLQueryWatcher: Cancellable, Apollo public let query: Query let resultHandler: GraphQLResultHandler - private var context = 0 + private var contextIdentifier = UUID() private weak var fetching: Cancellable? @@ -41,7 +41,7 @@ public final class GraphQLQueryWatcher: Cancellable, Apollo func fetch(cachePolicy: CachePolicy) { // Cancel anything already in flight before starting a new fetch fetching?.cancel() - fetching = client?.fetch(query: query, cachePolicy: cachePolicy, context: &context, queue: callbackQueue) { [weak self] result in + fetching = client?.fetch(query: query, cachePolicy: cachePolicy, contextIdentifier: self.contextIdentifier, queue: callbackQueue) { [weak self] result in guard let self = self else { return } switch result { @@ -63,10 +63,20 @@ public final class GraphQLQueryWatcher: Cancellable, Apollo func store(_ store: ApolloStore, didChangeKeys changedKeys: Set, - context: UnsafeMutableRawPointer?) { - if context == &self.context { return } + contextIdentifier: UUID?) { + if + let incomingIdentifier = contextIdentifier, + incomingIdentifier == self.contextIdentifier { + // This is from changes to the keys made from the `fetch` method above, + // changes will be returned through that and do not need to be returned + // here as well. + return + } - guard let dependentKeys = dependentKeys else { return } + guard let dependentKeys = dependentKeys else { + // This query has nil dependent keys, so nothing that changed will affect it. + return + } if !dependentKeys.isDisjoint(with: changedKeys) { // First, attempt to reload the query from the cache directly, in order not to interrupt any in-flight server-side fetch. diff --git a/Sources/Apollo/GraphQLResponse.swift b/Sources/Apollo/GraphQLResponse.swift index 32383c263c..a22f1a5bcf 100644 --- a/Sources/Apollo/GraphQLResponse.swift +++ b/Sources/Apollo/GraphQLResponse.swift @@ -1,15 +1,38 @@ +import Foundation + /// Represents a GraphQL response received from a server. -public final class GraphQLResponse { +public final class GraphQLResponse: Parseable { + + public init(from data: Foundation.Data, decoder: T) throws where T : FlexibleDecoder { + // Giant hack to make all this conform to Parseable. + throw ParseableError.unsupportedInitializer + } + public let body: JSONObject - private let rootKey: String - private let variables: GraphQLMap? + private var rootKey: String + private var variables: GraphQLMap? public init(operation: Operation, body: JSONObject) where Operation.Data == Data { self.body = body rootKey = rootCacheKey(for: operation) variables = operation.variables } + + func setupOperation (_ operation: Operation) { + self.rootKey = rootCacheKey(for: operation) + self.variables = operation.variables + } + + public func parseResultWithCompletion(cacheKeyForObject: CacheKeyForObject? = nil, + completion: (Result<(GraphQLResult, RecordSet?), Error>) -> Void) { + do { + let result = try parseResult(cacheKeyForObject: cacheKeyForObject).await() + completion(.success(result)) + } catch { + completion(.failure(error)) + } + } func parseResult(cacheKeyForObject: CacheKeyForObject? = nil) throws -> Promise<(GraphQLResult, RecordSet?)> { let errors: [GraphQLError]? diff --git a/Sources/Apollo/GraphQLResult.swift b/Sources/Apollo/GraphQLResult.swift index 0dd7d31afe..2e2d094bd0 100644 --- a/Sources/Apollo/GraphQLResult.swift +++ b/Sources/Apollo/GraphQLResult.swift @@ -1,5 +1,12 @@ +import Foundation + /// Represents the result of a GraphQL operation. -public struct GraphQLResult { +public struct GraphQLResult: Parseable { + + public init(from data: Foundation.Data, decoder: T) throws { + throw ParseableError.unsupportedInitializer + } + /// The typed result data, or `nil` if an error was encountered that prevented a valid response. public let data: Data? /// A list of errors, or `nil` if the operation completed without encountering any errors. @@ -29,3 +36,16 @@ public struct GraphQLResult { self.dependentKeys = dependentKeys } } + +extension GraphQLResult where Data: Decodable { + + public init(from data: Foundation.Data, decoder: T) throws { + // SWIFT CODEGEN: fix this to handle codable better + let data = try decoder.decode(Data.self, from: data) + self.init(data: data, + extensions: nil, + errors: [], + source: .server, + dependentKeys: nil) + } +} diff --git a/Sources/Apollo/HTTPNetworkTransport.swift b/Sources/Apollo/HTTPNetworkTransport.swift deleted file mode 100644 index a6c5396e2e..0000000000 --- a/Sources/Apollo/HTTPNetworkTransport.swift +++ /dev/null @@ -1,475 +0,0 @@ -import Foundation - -/// Empty base protocol to allow multiple sub-protocols to just use a single parameter. -public protocol HTTPNetworkTransportDelegate: class {} - -/// Methods which will be called prior to a request being sent to the server. -public protocol HTTPNetworkTransportPreflightDelegate: HTTPNetworkTransportDelegate { - - /// Called when a request is about to send, to validate that it should be sent. - /// Good for early-exiting if your user is not logged in, for example. - /// - /// - Parameters: - /// - networkTransport: The network transport which wants to send a request - /// - request: The request, BEFORE it has been modified by `willSend` - /// - Returns: True if the request should proceed, false if not. - func networkTransport(_ networkTransport: HTTPNetworkTransport, shouldSend request: URLRequest) -> Bool - - /// Called when a request is about to send. Allows last minute modification of any properties on the request, - /// - /// - /// - Parameters: - /// - networkTransport: The network transport which is about to send a request - /// - request: The request, as an `inout` variable for modification - func networkTransport(_ networkTransport: HTTPNetworkTransport, willSend request: inout URLRequest) -} - -// MARK: - - -/// Methods which will be called after some kind of response has been received to a `URLSessionTask`. -public protocol HTTPNetworkTransportTaskCompletedDelegate: HTTPNetworkTransportDelegate { - - /// A callback to allow hooking in URL session responses for things like logging and examining headers. - /// NOTE: This will call back on whatever thread the URL session calls back on, which is never the main thread. Call `DispatchQueue.main.async` before touching your UI! - /// - /// - Parameters: - /// - networkTransport: The network transport that completed a task - /// - request: The request which was completed by the task - /// - data: [optional] Any data received. Passed through from `URLSession`. - /// - response: [optional] Any response received. Passed through from `URLSession`. - /// - error: [optional] Any error received. Passed through from `URLSession`. - func networkTransport(_ networkTransport: HTTPNetworkTransport, - didCompleteRawTaskForRequest request: URLRequest, - withData data: Data?, - response: URLResponse?, - error: Error?) -} - -// MARK: - - -/// Methods which will be called if an error is receieved at the network level. -public protocol HTTPNetworkTransportRetryDelegate: HTTPNetworkTransportDelegate { - - /// Called when an error has been received after a request has been sent to the server to see if an operation should be retried or not. - /// NOTE: Don't just call the `continueHandler` with `.retry` all the time, or you can potentially wind up in an infinite loop of errors - /// - /// - Parameters: - /// - networkTransport: The network transport which received the error - /// - error: The received error - /// - request: The URLRequest which generated the error - /// - response: [Optional] Any response received when the error was generated - /// - continueHandler: A closure indicating whether the operation should be retried. Asyncrhonous to allow for re-authentication or other async operations to complete. - func networkTransport(_ networkTransport: HTTPNetworkTransport, - receivedError error: Error, - for request: URLRequest, - response: URLResponse?, - continueHandler: @escaping (_ action: HTTPNetworkTransport.ContinueAction) -> Void) -} - -// MARK: - - -/// Methods which will be called after some kind of response has been received and it contains GraphQLErrors. -public protocol HTTPNetworkTransportGraphQLErrorDelegate: HTTPNetworkTransportDelegate { - - /// Called when response contains one or more GraphQL errors. - /// - /// NOTE: The mere presence of a GraphQL error does not necessarily mean a request failed! - /// GraphQL is design to allow partial success/failures to return, so make sure - /// you're validating the *type* of error you're getting in this before deciding whether to retry or not. - /// - /// ALSO NOTE: Don't just call the `retryHandler` with `true` all the time, or you can - /// potentially wind up in an infinite loop of errors - /// - /// - Parameters: - /// - networkTransport: The network transport which received the error - /// - errors: The received GraphQL errors - /// - retryHandler: A closure indicating whether the operation should be retried. Asyncrhonous to allow for re-authentication or other async operations to complete. - func networkTransport(_ networkTransport: HTTPNetworkTransport, - receivedGraphQLErrors errors: [GraphQLError], - retryHandler: @escaping (_ shouldRetry: Bool) -> Void) -} - -// MARK: - - -/// A network transport that uses HTTP POST requests to send GraphQL operations to a server, and that uses `URLSession` as the networking implementation. -public class HTTPNetworkTransport { - - /// The action to take when retrying - public enum ContinueAction { - /// Directly retry the action - case retry - /// Fail with the specified error. - case fail(_ error: Error) - } - - let url: URL - let client: URLSessionClient - let serializationFormat = JSONSerializationFormat.self - let useGETForQueries: Bool - let enableAutoPersistedQueries: Bool - let useGETForPersistedQueryRetry: Bool - private let requestCreator: RequestCreator - private let sendOperationIdentifiers: Bool - - /// A delegate which can conform to any or all of `HTTPNetworkTransportPreflightDelegate`, `HTTPNetworkTransportTaskCompletedDelegate`, and `HTTPNetworkTransportRetryDelegate`. - public weak var delegate: HTTPNetworkTransportDelegate? - - public lazy var clientName = HTTPNetworkTransport.defaultClientName - public lazy var clientVersion = HTTPNetworkTransport.defaultClientVersion - - /// Creates a network transport with the specified server URL and session configuration. - /// - /// - Parameters: - /// - url: The URL of a GraphQL server to connect to. - /// - client: The client to handle URL Session calls. - /// - sendOperationIdentifiers: Whether to send operation identifiers rather than full operation text, for use with servers that support query persistence. Defaults to false. - /// - useGETForQueries: If query operation should be sent using GET instead of POST. Defaults to false. - /// - enableAutoPersistedQueries: Whether to send persistedQuery extension. QueryDocument will be absent at 1st request, retry with QueryDocument if server respond PersistedQueryNotFound or PersistedQueryNotSupport. Defaults to false. - /// - useGETForPersistedQueryRetry: Whether to retry persistedQuery request with HttpGetMethod. Defaults to false. - public init(url: URL, - client: URLSessionClient = URLSessionClient(), - sendOperationIdentifiers: Bool = false, - useGETForQueries: Bool = false, - enableAutoPersistedQueries: Bool = false, - useGETForPersistedQueryRetry: Bool = false, - requestCreator: RequestCreator = ApolloRequestCreator()) { - self.url = url - self.client = client - self.sendOperationIdentifiers = sendOperationIdentifiers - self.useGETForQueries = useGETForQueries - self.enableAutoPersistedQueries = enableAutoPersistedQueries - self.useGETForPersistedQueryRetry = useGETForPersistedQueryRetry - self.requestCreator = requestCreator - } - - deinit { - self.client.invalidate() - } - - private func send(operation: Operation, - isPersistedQueryRetry: Bool, - files: [GraphQLFile]?, - completionHandler: @escaping (_ results: Result, Error>) -> Void) -> Cancellable { - let request: URLRequest - do { - request = try self.createRequest(for: operation, - isPersistedQueryRetry: isPersistedQueryRetry, - files: files) - } catch { - completionHandler(.failure(error)) - return EmptyCancellable() - } - - let task = self.client.sendRequest(request, rawTaskCompletionHandler: { [weak self] data, response, error in - self?.rawTaskCompleted(request: request, data: data, response: response, error: error) - }, completion: { [weak self] result in - guard let self = self else { - // None of the rest of this really matters - return - } - - switch result { - case .failure(let error): - self.handleErrorOrRetry(operation: operation, - files: files, - error: error, - for: request, - response: nil, - completionHandler: completionHandler) - case .success(let (data, httpResponse)): - guard httpResponse.apollo.isSuccessful == true else { - let unsuccessfulError = GraphQLHTTPResponseError(body: data, - response: httpResponse, - kind: .errorResponse) - self.handleErrorOrRetry(operation: operation, - files: files, - error: unsuccessfulError, - for: request, - response: httpResponse, - completionHandler: completionHandler) - return - } - - do { - guard let body = try self.serializationFormat.deserialize(data: data) as? JSONObject else { - throw GraphQLHTTPResponseError(body: data, response: httpResponse, kind: .invalidResponse) - } - - let graphQLResponse = GraphQLResponse(operation: operation, body: body) - - if let errors = graphQLResponse.parseErrorsOnlyFast() { - // Handle specific errors from response - self.handleGraphQLErrorsIfNeeded(operation: operation, - files: files, - for: request, - body: body, - errors: errors, - completionHandler: completionHandler) - } else { - completionHandler(.success(graphQLResponse)) - } - } catch let parsingError { - self.handleErrorOrRetry(operation: operation, - files: files, - error: parsingError, - for: request, - response: httpResponse, - completionHandler: completionHandler) - } - } - }) - - // Task is resumed by underlying framework - return task - } - - private func handleGraphQLErrorsOrComplete(operation: Operation, - files: [GraphQLFile]?, - response: GraphQLResponse, - completionHandler: @escaping (_ result: Result, Error>) -> Void) { - guard - let delegate = self.delegate as? HTTPNetworkTransportGraphQLErrorDelegate, - let graphQLErrors = response.parseErrorsOnlyFast(), - graphQLErrors.apollo.isNotEmpty else { - completionHandler(.success(response)) - return - } - - delegate.networkTransport(self, receivedGraphQLErrors: graphQLErrors, retryHandler: { [weak self] shouldRetry in - guard let self = self else { - // None of the rest of this really matters - return - } - - guard shouldRetry else { - completionHandler(.success(response)) - return - } - - _ = self.send(operation: operation, - isPersistedQueryRetry: self.enableAutoPersistedQueries, - files: files, - completionHandler: completionHandler) - }) - } - - private func handleGraphQLErrorsIfNeeded(operation: Operation, - files: [GraphQLFile]?, - for request: URLRequest, - body: JSONObject, - errors: [GraphQLError], - completionHandler: @escaping (_ results: Result, Error>) -> Void) { - - let errorMessages = errors.compactMap { $0.message } - if self.enableAutoPersistedQueries, - errorMessages.contains("PersistedQueryNotFound") { - // We need to retry this with the full body. - _ = self.send(operation: operation, - isPersistedQueryRetry: true, - files: nil, - completionHandler: completionHandler) - } else { - // Pass the response on to the rest of the chain - let response = GraphQLResponse(operation: operation, body: body) - handleGraphQLErrorsOrComplete(operation: operation, files: files, response: response, completionHandler: completionHandler) - } - } - - private func handleErrorOrRetry(operation: Operation, - files: [GraphQLFile]?, - error: Error, - for request: URLRequest, - response: URLResponse?, - completionHandler: @escaping (_ result: Result, Error>) -> Void) { - guard - let delegate = self.delegate, - let retrier = delegate as? HTTPNetworkTransportRetryDelegate else { - completionHandler(.failure(error)) - return - } - - retrier.networkTransport( - self, - receivedError: error, - for: request, - response: response, - continueHandler: { [weak self] (action: HTTPNetworkTransport.ContinueAction) in - guard let self = self else { - // None of the rest of this really matters - return - } - - switch action { - case .retry: - _ = self.send(operation: operation, - isPersistedQueryRetry: self.enableAutoPersistedQueries, - files: files, - completionHandler: completionHandler) - case .fail(let error): - completionHandler(.failure(error)) - } - }) - } - - private func rawTaskCompleted(request: URLRequest, - data: Data?, - response: URLResponse?, - error: Error?) { - guard - let delegate = self.delegate, - let taskDelegate = delegate as? HTTPNetworkTransportTaskCompletedDelegate else { - return - } - - taskDelegate.networkTransport(self, - didCompleteRawTaskForRequest: request, - withData: data, - response: response, - error: error) - } - - private func createRequest(for operation: Operation, - isPersistedQueryRetry: Bool, - files: [GraphQLFile]?) throws -> URLRequest { - let useGetMethod: Bool - let sendQueryDocument: Bool - let autoPersistQueries: Bool - switch operation.operationType { - case .query: - if isPersistedQueryRetry { - useGetMethod = self.useGETForPersistedQueryRetry - sendQueryDocument = true - autoPersistQueries = true - } else { - useGetMethod = self.useGETForQueries || (self.enableAutoPersistedQueries && self.useGETForPersistedQueryRetry) - sendQueryDocument = !self.enableAutoPersistedQueries - autoPersistQueries = self.enableAutoPersistedQueries - } - case .mutation: - useGetMethod = false - if isPersistedQueryRetry { - sendQueryDocument = true - autoPersistQueries = true - } else { - sendQueryDocument = !self.enableAutoPersistedQueries - autoPersistQueries = self.enableAutoPersistedQueries - } - default: - useGetMethod = false - sendQueryDocument = true - autoPersistQueries = false - } - - return try self.createRequest(for: operation, - files: files, - httpMethod: useGetMethod ? .GET : .POST, - sendQueryDocument: sendQueryDocument, - autoPersistQueries: autoPersistQueries) - } - - private func createRequest(for operation: Operation, - files: [GraphQLFile]?, - httpMethod: GraphQLHTTPMethod, - sendQueryDocument: Bool, - autoPersistQueries: Bool) throws -> URLRequest { - let body = self.requestCreator.requestBody(for: operation, - sendOperationIdentifiers: self.sendOperationIdentifiers, - sendQueryDocument: sendQueryDocument, - autoPersistQuery: autoPersistQueries) - var request = URLRequest(url: self.url) - self.addApolloClientHeaders(to: &request) - - // We default to json, but this can be changed below if needed. - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - - switch httpMethod { - case .GET: - let transformer = GraphQLGETTransformer(body: body, url: self.url) - if let urlForGet = transformer.createGetURL() { - request = URLRequest(url: urlForGet) - request.httpMethod = GraphQLHTTPMethod.GET.rawValue - } else { - throw GraphQLHTTPRequestError.serializedQueryParamsMessageError - } - case .POST: - do { - if - let files = files, - files.apollo.isNotEmpty { - let formData = try requestCreator.requestMultipartFormData( - for: operation, - files: files, - sendOperationIdentifiers: self.sendOperationIdentifiers, - serializationFormat: self.serializationFormat, - manualBoundary: nil) - - request.setValue("multipart/form-data; boundary=\(formData.boundary)", forHTTPHeaderField: "Content-Type") - request.httpBody = try formData.encode() - } else { - request.httpBody = try serializationFormat.serialize(value: body) - } - - request.httpMethod = GraphQLHTTPMethod.POST.rawValue - } catch { - throw GraphQLHTTPRequestError.serializedBodyMessageError - } - } - - request.setValue(operation.operationName, forHTTPHeaderField: "X-APOLLO-OPERATION-NAME") - - if let operationID = operation.operationIdentifier { - request.setValue(operationID, forHTTPHeaderField: "X-APOLLO-OPERATION-ID") - } - - // If there's a delegate, do a pre-flight check and allow modifications to the request. - if - let delegate = self.delegate, - let preflightDelegate = delegate as? HTTPNetworkTransportPreflightDelegate { - guard preflightDelegate.networkTransport(self, shouldSend: request) else { - throw GraphQLHTTPRequestError.cancelledByDelegate - } - - preflightDelegate.networkTransport(self, willSend: &request) - } - - return request - } -} - -// MARK: - NetworkTransport conformance - -extension HTTPNetworkTransport: NetworkTransport { - - public func send(operation: Operation, completionHandler: @escaping (_ result: Result, Error>) -> Void) -> Cancellable { - return send(operation: operation, - isPersistedQueryRetry: false, - files: nil, - completionHandler: completionHandler) - } -} - -// MARK: - UploadingNetworkTransport conformance - -extension HTTPNetworkTransport: UploadingNetworkTransport { - - public func upload(operation: Operation, - files: [GraphQLFile], - completionHandler: @escaping (_ result: Result, Error>) -> Void) -> Cancellable { - return send(operation: operation, - isPersistedQueryRetry: false, - files: files, - completionHandler: completionHandler) - } -} - -// MARK: - Equatable conformance - -extension HTTPNetworkTransport: Equatable { - - public static func ==(lhs: HTTPNetworkTransport, rhs: HTTPNetworkTransport) -> Bool { - return lhs.url == rhs.url - && lhs.client == rhs.client - && lhs.sendOperationIdentifiers == rhs.sendOperationIdentifiers - && lhs.useGETForQueries == rhs.useGETForQueries - } -} diff --git a/Sources/Apollo/HTTPRequest.swift b/Sources/Apollo/HTTPRequest.swift new file mode 100644 index 0000000000..b031a51951 --- /dev/null +++ b/Sources/Apollo/HTTPRequest.swift @@ -0,0 +1,106 @@ +import Foundation + +/// Encapsulation of all information about a request before it hits the network +open class HTTPRequest { + + /// The endpoint to make a GraphQL request to + open var graphQLEndpoint: URL + + /// The GraphQL Operation to execute + open var operation: Operation + + /// Any additional headers you wish to add by default to this request + open var additionalHeaders: [String: String] + + /// The `CachePolicy` to use for this request. + public let cachePolicy: CachePolicy + + /// [optional] A unique identifier for this request, to help with deduping cache hits for watchers. + public let contextIdentifier: UUID? + + /// Designated Initializer + /// + /// - Parameters: + /// - graphQLEndpoint: The endpoint to make a GraphQL request to + /// - operation: The GraphQL Operation to execute + /// - contextIdentifier: [optional] A unique identifier for this request, to help with deduping cache hits for watchers. Defaults to `nil`. + /// - contentType: The `Content-Type` header's value. Should usually be set for you by a subclass. + /// - clientName: The name of the client to send with the `"apollographql-client-name"` header + /// - clientVersion: The version of the client to send with the `"apollographql-client-version"` header + /// - additionalHeaders: Any additional headers you wish to add by default to this request. + /// - cachePolicy: The `CachePolicy` to use for this request. Defaults to the `.default` policy + public init(graphQLEndpoint: URL, + operation: Operation, + contextIdentifier: UUID? = nil, + contentType: String, + clientName: String, + clientVersion: String, + additionalHeaders: [String: String], + cachePolicy: CachePolicy = .default) { + self.graphQLEndpoint = graphQLEndpoint + self.operation = operation + self.contextIdentifier = contextIdentifier + self.additionalHeaders = additionalHeaders + self.cachePolicy = cachePolicy + + self.addHeader(name: "Content-Type", value: contentType) + self.addHeader(name: "X-APOLLO-OPERATION-NAME", value: self.operation.operationName) + self.addHeader(name: "X-APOLLO-OPERATION-TYPE", value: String(describing: operation.operationType)) + if let operationID = self.operation.operationIdentifier { + self.addHeader(name: "X-APOLLO-OPERATION-ID", value: operationID) + } + + self.addHeader(name: "apollographql-client-version", value: clientVersion) + self.addHeader(name: "apollographql-client-name", value: clientName) + } + + open func addHeader(name: String, value: String) { + self.additionalHeaders[name] = value + } + + open func updateContentType(to contentType: String) { + self.addHeader(name: "Content-Type", value: contentType) + } + + /// Converts this object to a fully fleshed-out `URLRequest` + /// + /// - Throws: Any error in creating the request + /// - Returns: The URL request, ready to send to your server. + open func toURLRequest() throws -> URLRequest { + var request = URLRequest(url: self.graphQLEndpoint) + + for (fieldName, value) in additionalHeaders { + request.addValue(value, forHTTPHeaderField: fieldName) + } + + return request + } +} + +extension HTTPRequest: Equatable { + + public static func == (lhs: HTTPRequest, rhs: HTTPRequest) -> Bool { + lhs.graphQLEndpoint == rhs.graphQLEndpoint + && lhs.contextIdentifier == rhs.contextIdentifier + && lhs.additionalHeaders == rhs.additionalHeaders + && lhs.cachePolicy == rhs.cachePolicy + && lhs.operation.queryDocument == rhs.operation.queryDocument + } +} + +extension HTTPRequest: CustomDebugStringConvertible { + public var debugDescription: String { + var debugStrings = [String]() + debugStrings.append("HTTPRequest details:") + debugStrings.append("Endpoint: \(self.graphQLEndpoint)") + debugStrings.append("Additional Headers: [") + for (key, value) in self.additionalHeaders { + debugStrings.append("\t\(key): \(value),") + } + debugStrings.append("]") + debugStrings.append("Cache Policy: \(self.cachePolicy)") + debugStrings.append("Operation: \(self.operation)") + debugStrings.append("Context identifier: \(String(describing: self.contextIdentifier))") + return debugStrings.joined(separator: "\n\t") + } +} diff --git a/Sources/Apollo/HTTPResponse.swift b/Sources/Apollo/HTTPResponse.swift new file mode 100644 index 0000000000..b80e16f85c --- /dev/null +++ b/Sources/Apollo/HTTPResponse.swift @@ -0,0 +1,32 @@ +import Foundation + +/// Data about a response received by an HTTP request. +public class HTTPResponse { + + /// The `HTTPURLResponse` received from the URL loading system + public var httpResponse: HTTPURLResponse + + /// The raw data received from the URL loading system + public var rawData: Data + + /// [optional] The data as parsed into a `GraphQLResult`, which can eventually be returned to the UI. Will be nil if not yet parsed. + public var parsedResponse: GraphQLResult? + + /// [optional] The data as parsed into a `GraphQLResponse` for legacy caching purposes. If you're not using the `LegacyParsingInterceptor`, you probably shouldn't be using this property. + /// **NOTE:** This property will be removed when the transition to a Codable-based Codegen is complete. + public var legacyResponse: GraphQLResponse? = nil + + /// Designated initializer + /// + /// - Parameters: + /// - response: The `HTTPURLResponse` received from the server. + /// - rawData: The raw, unparsed data received from the server. + /// - parsedResponse: [optional] The response parsed into the `ParsedValue` type. Will be nil if not yet parsed, or if parsing failed. + public init(response: HTTPURLResponse, + rawData: Data, + parsedResponse: GraphQLResult?) { + self.httpResponse = response + self.rawData = rawData + self.parsedResponse = parsedResponse + } +} diff --git a/Sources/Apollo/InterceptorProvider.swift b/Sources/Apollo/InterceptorProvider.swift new file mode 100644 index 0000000000..067b28e6cd --- /dev/null +++ b/Sources/Apollo/InterceptorProvider.swift @@ -0,0 +1,120 @@ +import Foundation + +// MARK: - Basic protocol + +/// A protocol to allow easy creation of an array of interceptors for a given operation. +public protocol InterceptorProvider { + + /// Creates a new array of interceptors when called + /// + /// - Parameter operation: The operation to provide interceptors for + func interceptors(for operation: Operation) -> [ApolloInterceptor] + + /// Provides an additional error interceptor for any additional handling of errors + /// before returning to the UI, such as logging. + /// - Parameter operation: The oper + func additionalErrorInterceptor(for operation: Operation) -> ApolloErrorInterceptor? +} + +/// MARK: - Default Implementation + +public extension InterceptorProvider { + + func additionalErrorInterceptor(for operation: Operation) -> ApolloErrorInterceptor? { + return nil + } +} + +// MARK: - Default implementation for typescript codegen + +/// The default interceptor provider for typescript-generated code +open class LegacyInterceptorProvider: InterceptorProvider { + + private let client: URLSessionClient + private let store: ApolloStore + private let shouldInvalidateClientOnDeinit: Bool + + /// Designated initializer + /// + /// - Parameters: + /// - client: The `URLSessionClient` to use. Defaults to the default setup. + /// - shouldInvalidateClientOnDeinit: If the passed-in client should be invalidated when this interceptor provider is deinitialized. If you are recreating the `URLSessionClient` every time you create a new provider, you should do this to prevent memory leaks. Defaults to true, since by default we provide a `URLSessionClient` to new instances. + /// - store: The `ApolloStore` to use when reading from or writing to the cache. Defaults to the default initializer for ApolloStore. + public init(client: URLSessionClient = URLSessionClient(), + shouldInvalidateClientOnDeinit: Bool = true, + store: ApolloStore = ApolloStore()) { + self.client = client + self.shouldInvalidateClientOnDeinit = shouldInvalidateClientOnDeinit + self.store = store + } + + deinit { + if self.shouldInvalidateClientOnDeinit { + self.client.invalidate() + } + } + + open func interceptors(for operation: Operation) -> [ApolloInterceptor] { + return [ + MaxRetryInterceptor(), + LegacyCacheReadInterceptor(store: self.store), + NetworkFetchInterceptor(client: self.client), + ResponseCodeInterceptor(), + LegacyParsingInterceptor(cacheKeyForObject: self.store.cacheKeyForObject), + AutomaticPersistedQueryInterceptor(), + LegacyCacheWriteInterceptor(store: self.store), + ] + } + + open func additionalErrorInterceptor(for operation: Operation) -> ApolloErrorInterceptor? { + return nil + } +} + +// MARK: - Default implementation for swift codegen + + +/// The default interceptor provider for code generated with Swift Codegen™ +open class CodableInterceptorProvider: InterceptorProvider { + + private let client: URLSessionClient + private let shouldInvalidateClientOnDeinit: Bool + private let decoder: FlexDecoder + + /// Designated initializer + /// + /// - Parameters: + /// - client: The URLSessionClient to use. Defaults to the default setup. + /// - shouldInvalidateClientOnDeinit: If the passed-in client should be invalidated when this interceptor provider is deinitialized. If you are recreating the `URLSessionClient` every time you create a new provider, you should do this to prevent memory leaks. Defaults to true, since by default we provide a `URLSessionClient` to new instances. + /// - decoder: A `FlexibleDecoder` which can decode `Codable` objects. + public init(client: URLSessionClient = URLSessionClient(), + shouldInvalidateClientOnDeinit: Bool = true, + store: ApolloStore, + decoder: FlexDecoder) { + self.client = client + self.shouldInvalidateClientOnDeinit = shouldInvalidateClientOnDeinit + self.decoder = decoder + } + + deinit { + if self.shouldInvalidateClientOnDeinit { + self.client.invalidate() + } + } + + open func interceptors(for operation: Operation) -> [ApolloInterceptor] { + return [ + MaxRetryInterceptor(), + // Swift Codegen Phase 2: Add Cache Read interceptor + NetworkFetchInterceptor(client: self.client), + ResponseCodeInterceptor(), + AutomaticPersistedQueryInterceptor(), + CodableParsingInterceptor(decoder: self.decoder), + // Swift codegen Phase 2: Add Cache Write interceptor + ] + } + + open func additionalErrorInterceptor(for operation: Operation) -> ApolloErrorInterceptor? { + return nil + } +} diff --git a/Sources/Apollo/JSONRequest.swift b/Sources/Apollo/JSONRequest.swift new file mode 100644 index 0000000000..a707622a6e --- /dev/null +++ b/Sources/Apollo/JSONRequest.swift @@ -0,0 +1,117 @@ +import Foundation + +/// A request which sends JSON related to a GraphQL operation. +open class JSONRequest: HTTPRequest { + + public let requestBodyCreator: RequestBodyCreator + + public let autoPersistQueries: Bool + public let useGETForQueries: Bool + public let useGETForPersistedQueryRetry: Bool + public var isPersistedQueryRetry = false + + public let serializationFormat = JSONSerializationFormat.self + + /// Designated initializer + /// + /// - Parameters: + /// - operation: The GraphQL Operation to execute + /// - graphQLEndpoint: The endpoint to make a GraphQL request to + /// - contextIdentifier: [optional] A unique identifier for this request, to help with deduping cache hits for watchers. Defaults to `nil`. + /// - clientName: The name of the client to send with the `"apollographql-client-name"` header + /// - clientVersion: The version of the client to send with the `"apollographql-client-version"` header + /// - additionalHeaders: Any additional headers you wish to add by default to this request + /// - cachePolicy: The `CachePolicy` to use for this request. + /// - autoPersistQueries: `true` if Auto-Persisted Queries should be used. Defaults to `false`. + /// - useGETForQueries: `true` if Queries should use `GET` instead of `POST` for HTTP requests. Defaults to `false`. + /// - useGETForPersistedQueryRetry: `true` if when an Auto-Persisted query is retried, it should use `GET` instead of `POST` to send the query. Defaults to `false`. + /// - requestBodyCreator: An object conforming to the `RequestBodyCreator` protocol to assist with creating the request body. Defaults to the provided `ApolloRequestBodyCreator` implementation. + public init(operation: Operation, + graphQLEndpoint: URL, + contextIdentifier: UUID? = nil, + clientName: String, + clientVersion: String, + additionalHeaders: [String: String] = [:], + cachePolicy: CachePolicy = .default, + autoPersistQueries: Bool = false, + useGETForQueries: Bool = false, + useGETForPersistedQueryRetry: Bool = false, + requestBodyCreator: RequestBodyCreator = ApolloRequestBodyCreator()) { + self.autoPersistQueries = autoPersistQueries + self.useGETForQueries = useGETForQueries + self.useGETForPersistedQueryRetry = useGETForPersistedQueryRetry + self.requestBodyCreator = requestBodyCreator + + super.init(graphQLEndpoint: graphQLEndpoint, + operation: operation, + contextIdentifier: contextIdentifier, + contentType: "application/json", + clientName: clientName, + clientVersion: clientVersion, + additionalHeaders: additionalHeaders, + cachePolicy: cachePolicy) + } + + open var sendOperationIdentifier: Bool { + self.operation.operationIdentifier != nil + } + + open override func toURLRequest() throws -> URLRequest { + var request = try super.toURLRequest() + + let useGetMethod: Bool + let sendQueryDocument: Bool + let autoPersistQueries: Bool + switch operation.operationType { + case .query: + if isPersistedQueryRetry { + useGetMethod = self.useGETForPersistedQueryRetry + sendQueryDocument = true + autoPersistQueries = true + } else { + useGetMethod = self.useGETForQueries || (self.autoPersistQueries && self.useGETForPersistedQueryRetry) + sendQueryDocument = !self.autoPersistQueries + autoPersistQueries = self.autoPersistQueries + } + case .mutation: + useGetMethod = false + if isPersistedQueryRetry { + sendQueryDocument = true + autoPersistQueries = true + } else { + sendQueryDocument = !self.autoPersistQueries + autoPersistQueries = self.autoPersistQueries + } + default: + useGetMethod = false + sendQueryDocument = true + autoPersistQueries = false + } + + let body = self.requestBodyCreator.requestBody(for: operation, + sendOperationIdentifiers: self.sendOperationIdentifier, + sendQueryDocument: sendQueryDocument, + autoPersistQuery: autoPersistQueries) + + let httpMethod: GraphQLHTTPMethod = useGetMethod ? .GET : .POST + switch httpMethod { + case .GET: + let transformer = GraphQLGETTransformer(body: body, url: self.graphQLEndpoint) + if let urlForGet = transformer.createGetURL() { + request = URLRequest(url: urlForGet) + request.httpMethod = GraphQLHTTPMethod.GET.rawValue + } else { + throw GraphQLHTTPRequestError.serializedQueryParamsMessageError + } + case .POST: + do { + request.httpBody = try serializationFormat.serialize(value: body) + request.httpMethod = GraphQLHTTPMethod.POST.rawValue + } catch { + throw GraphQLHTTPRequestError.serializedBodyMessageError + } + } + + return request + } +} diff --git a/Sources/Apollo/LegacyCacheReadInterceptor.swift b/Sources/Apollo/LegacyCacheReadInterceptor.swift new file mode 100644 index 0000000000..4cf8060a48 --- /dev/null +++ b/Sources/Apollo/LegacyCacheReadInterceptor.swift @@ -0,0 +1,100 @@ +import Foundation + +/// An interceptor that reads data from the legacy cache for queries, following the `HTTPRequest`'s `cachePolicy`. +public class LegacyCacheReadInterceptor: ApolloInterceptor { + + private let store: ApolloStore + + /// Designated initializer + /// + /// - Parameter store: The store to use when reading from the cache. + public init(store: ApolloStore) { + self.store = store + } + + public func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) { + + switch request.operation.operationType { + case .mutation, + .subscription: + // Mutations and subscriptions don't need to hit the cache. + chain.proceedAsync(request: request, + response: response, + completion: completion) + case .query: + switch request.cachePolicy { + case .fetchIgnoringCacheCompletely, + .fetchIgnoringCacheData: + // Don't bother with the cache, just keep going + chain.proceedAsync(request: request, + response: response, + completion: completion) + case .returnCacheDataAndFetch: + self.fetchFromCache(for: request, chain: chain) { cacheFetchResult in + switch cacheFetchResult { + case .failure: + // Don't return a cache miss error, just keep going + break + case .success(let graphQLResult): + chain.returnValueAsync(for: request, + value: graphQLResult, + completion: completion) + } + + // In either case, keep going asynchronously + chain.proceedAsync(request: request, + response: response, + completion: completion) + } + case .returnCacheDataElseFetch: + self.fetchFromCache(for: request, chain: chain) { cacheFetchResult in + switch cacheFetchResult { + case .failure: + // Cache miss, proceed to network without returning error + chain.proceedAsync(request: request, + response: response, + completion: completion) + case .success(let graphQLResult): + // Cache hit! We're done. + chain.returnValueAsync(for: request, + value: graphQLResult, + completion: completion) + } + } + case .returnCacheDataDontFetch: + self.fetchFromCache(for: request, chain: chain) { cacheFetchResult in + switch cacheFetchResult { + case .failure(let error): + // Cache miss - don't hit the network, just return the error. + chain.handleErrorAsync(error, + request: request, + response: response, + completion: completion) + case .success(let result): + chain.returnValueAsync(for: request, + value: result, + completion: completion) + } + } + } + } + } + + private func fetchFromCache( + for request: HTTPRequest, + chain: RequestChain, + completion: @escaping (Result, Error>) -> Void) { + + self.store.load(query: request.operation) { loadResult in + guard chain.isNotCancelled else { + return + } + + completion(loadResult) + } + } +} diff --git a/Sources/Apollo/LegacyCacheWriteInterceptor.swift b/Sources/Apollo/LegacyCacheWriteInterceptor.swift new file mode 100644 index 0000000000..ba96ce7950 --- /dev/null +++ b/Sources/Apollo/LegacyCacheWriteInterceptor.swift @@ -0,0 +1,77 @@ +import Foundation + +/// An interceptor which writes data to the legacy cache, following the `HTTPRequest`'s `cachePolicy`. +public class LegacyCacheWriteInterceptor: ApolloInterceptor { + + public enum LegacyCacheWriteError: Error, LocalizedError { + case noResponseToParse + + public var errorDescription: String? { + switch self { + case .noResponseToParse: + return "The Legacy Cache Write Interceptor was called before a response was received to be parsed. Double-check the order of your interceptors." + } + } + } + + public let store: ApolloStore + + /// Designated initializer + /// + /// - Parameter store: The store to use when writing to the cache. + public init(store: ApolloStore) { + self.store = store + } + + public func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) { + + guard request.cachePolicy != .fetchIgnoringCacheCompletely else { + // If we're ignoring the cache completely, we're not writing to it. + chain.proceedAsync(request: request, + response: response, + completion: completion) + return + } + + guard + let createdResponse = response, + let legacyResponse = createdResponse.legacyResponse else { + chain.handleErrorAsync(LegacyCacheWriteError.noResponseToParse, + request: request, + response: response, + completion: completion) + return + } + + firstly { + try legacyResponse.parseResult(cacheKeyForObject: self.store.cacheKeyForObject) + }.andThen { [weak self] (result, records) in + guard let self = self else { + return + } + guard chain.isNotCancelled else { + return + } + + if let records = records { + self.store.publish(records: records, identifier: request.contextIdentifier) + .catch { error in + preconditionFailure(String(describing: error)) + } + } + + chain.proceedAsync(request: request, + response: createdResponse, + completion: completion) + }.catch { error in + chain.handleErrorAsync(error, + request: request, + response: response, + completion: completion) + } + } +} diff --git a/Sources/Apollo/LegacyParsingInterceptor.swift b/Sources/Apollo/LegacyParsingInterceptor.swift new file mode 100644 index 0000000000..9ac6c257ee --- /dev/null +++ b/Sources/Apollo/LegacyParsingInterceptor.swift @@ -0,0 +1,91 @@ +import Foundation + +/// An interceptor which parses code using the legacy parsing system. +public class LegacyParsingInterceptor: ApolloInterceptor { + + public enum LegacyParsingError: Error, LocalizedError { + case noResponseToParse + case couldNotParseToLegacyJSON(data: Data) + + public var errorDescription: String? { + switch self { + case .noResponseToParse: + return "The Codable Parsing Interceptor was called before a response was received to be parsed. Double-check the order of your interceptors." + case .couldNotParseToLegacyJSON(let data): + var errorStrings = [String]() + errorStrings.append("Could not parse data to legacy JSON format.") + if let dataString = String(bytes: data, encoding: .utf8) { + errorStrings.append("Data received as a String was:") + errorStrings.append(dataString) + } else { + errorStrings.append("Data of count \(data.count) also could not be parsed into a String.") + } + + return errorStrings.joined(separator: " ") + } + } + } + + public var cacheKeyForObject: CacheKeyForObject? + + /// Designated Initializer + public init(cacheKeyForObject: CacheKeyForObject? = nil) { + self.cacheKeyForObject = cacheKeyForObject + } + + public func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) { + + guard let createdResponse = response else { + chain.handleErrorAsync(LegacyParsingError.noResponseToParse, + request: request, + response: response, + completion: completion) + return + } + + do { + let deserialized = try? JSONSerializationFormat.deserialize(data: createdResponse.rawData) + let json = deserialized as? JSONObject + guard let body = json else { + throw LegacyParsingError.couldNotParseToLegacyJSON(data: createdResponse.rawData) + } + + let graphQLResponse = GraphQLResponse(operation: request.operation, body: body) + createdResponse.legacyResponse = graphQLResponse + + switch request.cachePolicy { + case .fetchIgnoringCacheCompletely: + // There is no cache, so we don't need to get any info on dependencies. Use fast parsing. + let fastResult = try graphQLResponse.parseResultFast() + createdResponse.parsedResponse = fastResult + chain.proceedAsync(request: request, + response: createdResponse, + completion: completion) + default: + graphQLResponse.parseResultWithCompletion(cacheKeyForObject: self.cacheKeyForObject) { parsingResult in + switch parsingResult { + case .failure(let error): + chain.handleErrorAsync(error, + request: request, + response: createdResponse, + completion: completion) + case .success(let (parsedResult, _)): + createdResponse.parsedResponse = parsedResult + chain.proceedAsync(request: request, + response: createdResponse, + completion: completion) + } + } + } + } catch { + chain.handleErrorAsync(error, + request: request, + response: response, + completion: completion) + } + } +} diff --git a/Sources/Apollo/MaxRetryInterceptor.swift b/Sources/Apollo/MaxRetryInterceptor.swift new file mode 100644 index 0000000000..986690c201 --- /dev/null +++ b/Sources/Apollo/MaxRetryInterceptor.swift @@ -0,0 +1,47 @@ +import Foundation + +/// An interceptor to enforce a maximum number of retries of any `HTTPRequest` +public class MaxRetryInterceptor: ApolloInterceptor { + + private let maxRetries: Int + private var hitCount = 0 + + public enum RetryError: Error, LocalizedError { + case hitMaxRetryCount(count: Int, operationName: String) + + public var errorDescription: String? { + switch self { + case .hitMaxRetryCount(let count, let operationName): + return "The maximum number of retries (\(count)) was hit without success for operation \"\(operationName)\"." + } + } + } + + /// Designated initializer. + /// + /// - Parameter maxRetriesAllowed: How many times a query can be retried, in addition to the initial attempt before + public init(maxRetriesAllowed: Int = 3) { + self.maxRetries = maxRetriesAllowed + } + + public func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) { + guard self.hitCount <= self.maxRetries else { + let error = RetryError.hitMaxRetryCount(count: self.maxRetries, + operationName: request.operation.operationName) + chain.handleErrorAsync(error, + request: request, + response: response, + completion: completion) + return + } + + self.hitCount += 1 + chain.proceedAsync(request: request, + response: response, + completion: completion) + } +} diff --git a/Sources/Apollo/NetworkFetchInterceptor.swift b/Sources/Apollo/NetworkFetchInterceptor.swift new file mode 100644 index 0000000000..4648a9a02c --- /dev/null +++ b/Sources/Apollo/NetworkFetchInterceptor.swift @@ -0,0 +1,61 @@ +import Foundation + +/// An interceptor which actually fetches data from the network. +public class NetworkFetchInterceptor: ApolloInterceptor, Cancellable { + let client: URLSessionClient + private var currentTask: URLSessionTask? + + /// Designated initializer. + /// + /// - Parameter client: The `URLSessionClient` to use to fetch data + public init(client: URLSessionClient) { + self.client = client + } + + public func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) { + + let urlRequest: URLRequest + do { + urlRequest = try request.toURLRequest() + } catch { + chain.handleErrorAsync(error, + request: request, + response: response, + completion: completion) + return + } + + self.currentTask = self.client.sendRequest(urlRequest) { result in + defer { + self.currentTask = nil + } + + guard chain.isNotCancelled else { + return + } + + switch result { + case .failure(let error): + chain.handleErrorAsync(error, + request: request, + response: response, + completion: completion) + case .success(let (data, httpResponse)): + let response = HTTPResponse(response: httpResponse, + rawData: data, + parsedResponse: nil) + chain.proceedAsync(request: request, + response: response, + completion: completion) + } + } + } + + public func cancel() { + self.currentTask?.cancel() + } +} diff --git a/Sources/Apollo/NetworkTransport.swift b/Sources/Apollo/NetworkTransport.swift index bfb126ee19..f66073be8e 100644 --- a/Sources/Apollo/NetworkTransport.swift +++ b/Sources/Apollo/NetworkTransport.swift @@ -9,9 +9,16 @@ public protocol NetworkTransport: class { /// /// - Parameters: /// - operation: The operation to send. + /// - cachePolicy: The `CachePolicy` to use making this request. + /// - contextIdentifier: [optional] A unique identifier for this request, to help with deduping cache hits for watchers. Defaults to `nil`. + /// - callbackQueue: The queue to call back on with the results. Should default to `.main`. /// - completionHandler: A closure to call when a request completes. On `success` will contain the response received from the server. On `failure` will contain the error which occurred. /// - Returns: An object that can be used to cancel an in progress request. - func send(operation: Operation, completionHandler: @escaping (_ result: Result, Error>) -> Void) -> Cancellable + func send(operation: Operation, + cachePolicy: CachePolicy, + contextIdentifier: UUID?, + callbackQueue: DispatchQueue, + completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable /// The name of the client to send as a header value. var clientName: String { get } @@ -89,7 +96,12 @@ public protocol UploadingNetworkTransport: NetworkTransport { /// - Parameters: /// - operation: The operation to send /// - files: An array of `GraphQLFile` objects to send. + /// - callbackQueue: The queue to call back on with the results. Should default to `.main`. /// - completionHandler: The completion handler to execute when the request completes or errors /// - Returns: An object that can be used to cancel an in progress request. - func upload(operation: Operation, files: [GraphQLFile], completionHandler: @escaping (_ result: Result, Error>) -> Void) -> Cancellable + func upload( + operation: Operation, + files: [GraphQLFile], + callbackQueue: DispatchQueue, + completionHandler: @escaping (Result,Error>) -> Void) -> Cancellable } diff --git a/Sources/Apollo/Parseable.swift b/Sources/Apollo/Parseable.swift new file mode 100644 index 0000000000..070abdb0e3 --- /dev/null +++ b/Sources/Apollo/Parseable.swift @@ -0,0 +1,27 @@ +import Foundation + +public enum ParseableError: Error { + case unexpectedType + case unsupportedInitializer + case notYetImplemented +} + +/// A protocol to represent anything that can be decoded by a `FlexibleDecoder` +public protocol Parseable { + + /// Required initializer + /// + /// - Parameters: + /// - data: The data to decode + /// - decoder: The decoder to use to decode it + init(from data: Data, decoder: T) throws +} + +// MARK: - Default implementation for Decodable + +public extension Parseable where Self: Decodable { + + init(from data: Data, decoder: T) throws { + self = try decoder.decode(Self.self, from: data) + } +} diff --git a/Sources/Apollo/RequestBodyCreator.swift b/Sources/Apollo/RequestBodyCreator.swift new file mode 100644 index 0000000000..cf3db4835e --- /dev/null +++ b/Sources/Apollo/RequestBodyCreator.swift @@ -0,0 +1,67 @@ +import Foundation +#if !COCOAPODS +import ApolloCore +#endif + +public protocol RequestBodyCreator { + /// Creates a `GraphQLMap` out of the passed-in operation + /// + /// - Parameters: + /// - operation: The operation to use + /// - sendOperationIdentifiers: Whether or not to send operation identifiers. Defaults to false. + /// - Returns: The created `GraphQLMap` + func requestBody(for operation: Operation, + sendOperationIdentifiers: Bool, + sendQueryDocument: Bool, + autoPersistQuery: Bool) -> GraphQLMap +} + +extension RequestBodyCreator { + /// Creates a `GraphQLMap` out of the passed-in operation + /// + /// - Parameters: + /// - operation: The operation to use + /// - sendOperationIdentifiers: Whether or not to send operation identifiers. Defaults to false. + /// - sendQueryDocument: Whether or not to send the full query document. Defaults to true. + /// - autoPersistQuery: Whether to use auto-persisted query information. Defaults to false. + /// - Returns: The created `GraphQLMap` + public func requestBody(for operation: Operation, + sendOperationIdentifiers: Bool = false, + sendQueryDocument: Bool = true, + autoPersistQuery: Bool = false) -> GraphQLMap { + var body: GraphQLMap = [ + "variables": operation.variables, + "operationName": operation.operationName, + ] + + if sendOperationIdentifiers { + guard let operationIdentifier = operation.operationIdentifier else { + preconditionFailure("To send operation identifiers, Apollo types must be generated with operationIdentifiers") + } + + body["id"] = operationIdentifier + } + + if sendQueryDocument { + body["query"] = operation.queryDocument + } + + if autoPersistQuery { + guard let operationIdentifier = operation.operationIdentifier else { + preconditionFailure("To enable `autoPersistQueries`, Apollo types must be generated with operationIdentifiers") + } + + body["extensions"] = [ + "persistedQuery" : ["sha256Hash": operationIdentifier, "version": 1] + ] + } + + return body + } +} + +// Helper struct to create requests independently of HTTP operations. +public struct ApolloRequestBodyCreator: RequestBodyCreator { + // Internal init methods cannot be used in public methods + public init() { } +} diff --git a/Sources/Apollo/RequestChain.swift b/Sources/Apollo/RequestChain.swift new file mode 100644 index 0000000000..9c4cbf798d --- /dev/null +++ b/Sources/Apollo/RequestChain.swift @@ -0,0 +1,192 @@ +import Foundation +#if !COCOAPODS +import ApolloCore +#endif + +/// A chain that allows a single network request to be created and executed. +public class RequestChain: Cancellable { + + public enum ChainError: Error, LocalizedError { + case invalidIndex(chain: RequestChain, index: Int) + case noInterceptors + + public var errorDescription: String? { + switch self { + case .noInterceptors: + return "No interceptors were provided to this chain. This is a developer error." + case .invalidIndex(_, let index): + return "`proceedAsync` was called for index \(index), which is out of bounds of the receiver for this chain. Double-check the order of your interceptors." + } + } + } + + private let interceptors: [ApolloInterceptor] + private var currentIndex: Int + private var callbackQueue: DispatchQueue + private var isCancelled = Atomic(false) + + /// Checks the underlying value of `isCancelled`. Set up like this for better readability in `guard` statements + public var isNotCancelled: Bool { + !self.isCancelled.value + } + + /// Something which allows additional error handling to occur when some kind of error has happened. + public var additionalErrorHandler: ApolloErrorInterceptor? + + /// Creates a chain with the given interceptor array. + /// + /// - Parameters: + /// - interceptors: The array of interceptors to use. + /// - callbackQueue: The `DispatchQueue` to call back on when an error or result occurs. Defauls to `.main`. + public init(interceptors: [ApolloInterceptor], + callbackQueue: DispatchQueue = .main) { + self.interceptors = interceptors + self.callbackQueue = callbackQueue + self.currentIndex = 0 + } + + /// Kicks off the request from the beginning of the interceptor array. + /// + /// - Parameters: + /// - request: The request to send. + /// - completion: The completion closure to call when the request has completed. + public func kickoff( + request: HTTPRequest, + completion: @escaping (Result, Error>) -> Void) { + assert(self.currentIndex == 0, "The interceptor index should be zero when calling this method") + + guard let firstInterceptor = self.interceptors.first else { + handleErrorAsync(ChainError.noInterceptors, + request: request, + response: nil, + completion: completion) + return + } + + firstInterceptor.interceptAsync(chain: self, + request: request, + response: nil, + completion: completion) + } + + /// Proceeds to the next interceptor in the array. + /// + /// - Parameters: + /// - request: The in-progress request object + /// - response: [optional] The in-progress response object, if received yet + /// - completion: The completion closure to call when data has been processed and should be returned to the UI. + public func proceedAsync( + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) { + guard self.isNotCancelled else { + // Do not proceed, this chain has been cancelled. + return + } + + let nextIndex = self.currentIndex + 1 + if self.interceptors.indices.contains(nextIndex) { + self.currentIndex = nextIndex + let interceptor = self.interceptors[self.currentIndex] + + interceptor.interceptAsync(chain: self, + request: request, + response: response, + completion: completion) + } else { + if let result = response?.parsedResponse { + // We got to the end of the chain with a parsed response. Yay! Return it. + self.returnValueAsync(for: request, + value: result, + completion: completion) + } else { + // We got to the end of the chain and no parsed response is there, there needs to be more processing. + self.handleErrorAsync(ChainError.invalidIndex(chain: self, index: nextIndex), + request: request, + response: response, + completion: completion) + } + } + } + + /// Cancels the entire chain of interceptors. + public func cancel() { + self.isCancelled.value = true + + // If an interceptor adheres to `Cancellable`, it should have its in-flight work cancelled as well. + for interceptor in self.interceptors { + if let cancellableInterceptor = interceptor as? Cancellable { + cancellableInterceptor.cancel() + } + } + } + + /// Restarts the request starting from the first inteceptor. + /// + /// - Parameters: + /// - request: The request to retry + /// - completion: The completion closure to call when the request has completed. + public func retry( + request: HTTPRequest, + completion: @escaping (Result, Error>) -> Void) { + + guard self.isNotCancelled else { + // Don't retry something that's been cancelled. + return + } + + self.currentIndex = 0 + self.kickoff(request: request, completion: completion) + } + + /// Handles the error by returning it on the appropriate queue, or by applying an additional error interceptor if one has been provided. + /// + /// - Parameters: + /// - error: The error to handle + /// - request: The request, as far as it has been constructed. + /// - response: The response, as far as it has been constructed. + /// - completion: The completion closure to call when work is complete. + public func handleErrorAsync( + _ error: Error, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) { + guard self.isNotCancelled else { + return + } + + guard let additionalHandler = self.additionalErrorHandler else { + self.callbackQueue.async { + completion(.failure(error)) + } + return + } + + + additionalHandler.handleErrorAsync(error: error, + chain: self, + request: request, + response: response, + completion: completion) + } + + /// Handles a resulting value by returning it on the appropriate queue. + /// + /// - Parameters: + /// - request: The request, as far as it has been constructed. + /// - value: The value to be returned + /// - completion: The completion closure to call when work is complete. + public func returnValueAsync( + for request: HTTPRequest, + value: GraphQLResult, + completion: @escaping (Result, Error>) -> Void) { + + guard self.isNotCancelled else { + return + } + + self.callbackQueue.async { + completion(.success(value)) + } + } +} diff --git a/Sources/Apollo/RequestChainNetworkTransport.swift b/Sources/Apollo/RequestChainNetworkTransport.swift new file mode 100644 index 0000000000..ec30ba4550 --- /dev/null +++ b/Sources/Apollo/RequestChainNetworkTransport.swift @@ -0,0 +1,139 @@ +import Foundation + +/// An implementation of `NetworkTransport` which creates a `RequestChain` object +/// for each item sent through it. +open class RequestChainNetworkTransport: NetworkTransport { + + let interceptorProvider: InterceptorProvider + + /// The GraphQL endpoint URL to use. + public let endpointURL: URL + + /// Any additional headers that should be automatically added to every request. + public private(set) var additionalHeaders: [String: String] + + /// Set to `true` if Automatic Persisted Queries should be used to send a query hash instead of the full query body by default. + public let autoPersistQueries: Bool + + /// Set to `true` if you want to use `GET` instead of `POST` for queries, for example to take advantage of a CDN. + public let useGETForQueries: Bool + + /// Set to `true` to use `GET` instead of `POST` for a retry of a persisted query. + public let useGETForPersistedQueryRetry: Bool + + /// The `RequestBodyCreator` object to use to build your `URLRequest`. + public var requestBodyCreator: RequestBodyCreator + + /// Designated initializer + /// + /// - Parameters: + /// - interceptorProvider: The interceptor provider to use when constructing chains for a request + /// - endpointURL: The GraphQL endpoint URL to use. + /// - additionalHeaders: Any additional headers that should be automatically added to every request. Defaults to an empty dictionary. + /// - autoPersistQueries: Pass `true` if Automatic Persisted Queries should be used to send a query hash instead of the full query body by default. Defaults to `false`. + /// - requestBodyCreator: The `RequestBodyCreator` object to use to build your `URLRequest`. Defaults to the providedd `ApolloRequestBodyCreator` implementation. + /// - useGETForQueries: Pass `true` if you want to use `GET` instead of `POST` for queries, for example to take advantage of a CDN. Defaults to `false`. + /// - useGETForPersistedQueryRetry: Pass `true` to use `GET` instead of `POST` for a retry of a persisted query. Defaults to `false`. + public init(interceptorProvider: InterceptorProvider, + endpointURL: URL, + additionalHeaders: [String: String] = [:], + autoPersistQueries: Bool = false, + requestBodyCreator: RequestBodyCreator = ApolloRequestBodyCreator(), + useGETForQueries: Bool = false, + useGETForPersistedQueryRetry: Bool = false) { + self.interceptorProvider = interceptorProvider + self.endpointURL = endpointURL + + self.additionalHeaders = additionalHeaders + self.autoPersistQueries = autoPersistQueries + self.requestBodyCreator = requestBodyCreator + self.useGETForQueries = useGETForQueries + self.useGETForPersistedQueryRetry = useGETForPersistedQueryRetry + } + + /// Constructs a default (ie, non-multipart) GraphQL request. + /// + /// Override this method if you need to use a custom subclass of `HTTPRequest`. + /// + /// - Parameters: + /// - operation: The operation to create the request for + /// - cachePolicy: The `CachePolicy` to use when creating the request + /// - contextIdentifier: [optional] A unique identifier for this request, to help with deduping cache hits for watchers. Should default to `nil`. + /// - Returns: The constructed request. + open func constructRequest( + for operation: Operation, + cachePolicy: CachePolicy, + contextIdentifier: UUID? = nil) -> HTTPRequest { + JSONRequest(operation: operation, + graphQLEndpoint: self.endpointURL, + contextIdentifier: contextIdentifier, + clientName: self.clientName, + clientVersion: self.clientVersion, + additionalHeaders: additionalHeaders, + cachePolicy: cachePolicy, + autoPersistQueries: self.autoPersistQueries, + useGETForQueries: self.useGETForQueries, + useGETForPersistedQueryRetry: self.useGETForPersistedQueryRetry, + requestBodyCreator: self.requestBodyCreator) + } + + // MARK: - NetworkTransport Conformance + + public var clientName = RequestChainNetworkTransport.defaultClientName + public var clientVersion = RequestChainNetworkTransport.defaultClientVersion + + public func send( + operation: Operation, + cachePolicy: CachePolicy = .default, + contextIdentifier: UUID? = nil, + callbackQueue: DispatchQueue = .main, + completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable { + + let interceptors = self.interceptorProvider.interceptors(for: operation) + let chain = RequestChain(interceptors: interceptors, callbackQueue: callbackQueue) + chain.additionalErrorHandler = self.interceptorProvider.additionalErrorInterceptor(for: operation) + let request = self.constructRequest(for: operation, + cachePolicy: cachePolicy, + contextIdentifier: contextIdentifier) + + chain.kickoff(request: request, completion: completionHandler) + return chain + } +} + +extension RequestChainNetworkTransport: UploadingNetworkTransport { + + /// Constructs an uploading (ie, multipart) GraphQL request + /// + /// Override this method if you need to use a custom subclass of `HTTPRequest`. + /// + /// - Parameters: + /// - operation: The operation to create a request for + /// - files: The files you wish to upload + /// - Returns: The created request. + open func constructUploadRequest( + for operation: Operation, + with files: [GraphQLFile]) -> HTTPRequest { + + UploadRequest(graphQLEndpoint: self.endpointURL, + operation: operation, + clientName: self.clientName, + clientVersion: self.clientVersion, + files: files, + requestBodyCreator: self.requestBodyCreator) + } + + public func upload( + operation: Operation, + files: [GraphQLFile], + callbackQueue: DispatchQueue = .main, + completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable { + + let request = self.constructUploadRequest(for: operation, with: files) + let interceptors = self.interceptorProvider.interceptors(for: operation) + let chain = RequestChain(interceptors: interceptors, callbackQueue: callbackQueue) + + chain.kickoff(request: request, completion: completionHandler) + return chain + } +} diff --git a/Sources/Apollo/RequestCreator.swift b/Sources/Apollo/RequestCreator.swift deleted file mode 100644 index 15a5039303..0000000000 --- a/Sources/Apollo/RequestCreator.swift +++ /dev/null @@ -1,163 +0,0 @@ -import Foundation -#if !COCOAPODS -import ApolloCore -#endif - -public protocol RequestCreator { - /// Creates a `GraphQLMap` out of the passed-in operation - /// - /// - Parameters: - /// - operation: The operation to use - /// - sendOperationIdentifiers: Whether or not to send operation identifiers. Defaults to false. - /// - Returns: The created `GraphQLMap` - func requestBody(for operation: Operation, - sendOperationIdentifiers: Bool, - sendQueryDocument: Bool, - autoPersistQuery: Bool) -> GraphQLMap - - /// Creates multi-part form data to send with a request - /// - /// - Parameters: - /// - operation: The operation to create the data for. - /// - files: An array of files to use. - /// - sendOperationIdentifiers: True if operation identifiers should be sent, false if not. - /// - serializationFormat: The format to use to serialize data. - /// - manualBoundary: [optional] A manual boundary to pass in. A default boundary will be used otherwise. - /// - Returns: The created form data - /// - Throws: Errors creating or loading the form data - func requestMultipartFormData(for operation: Operation, - files: [GraphQLFile], - sendOperationIdentifiers: Bool, - serializationFormat: JSONSerializationFormat.Type, - manualBoundary: String?) throws -> MultipartFormData -} - -extension RequestCreator { - /// Creates a `GraphQLMap` out of the passed-in operation - /// - /// - Parameters: - /// - operation: The operation to use - /// - sendOperationIdentifiers: Whether or not to send operation identifiers. Defaults to false. - /// - sendQueryDocument: Whether or not to send the full query document. Defaults to true. - /// - autoPersistQuery: Whether to use auto-persisted query information. Defaults to false. - /// - Returns: The created `GraphQLMap` - public func requestBody(for operation: Operation, - sendOperationIdentifiers: Bool = false, - sendQueryDocument: Bool = true, - autoPersistQuery: Bool = false) -> GraphQLMap { - var body: GraphQLMap = [ - "variables": operation.variables, - "operationName": operation.operationName, - ] - - if sendOperationIdentifiers { - guard let operationIdentifier = operation.operationIdentifier else { - preconditionFailure("To send operation identifiers, Apollo types must be generated with operationIdentifiers") - } - - body["id"] = operationIdentifier - } - - if sendQueryDocument { - body["query"] = operation.queryDocument - } - - if autoPersistQuery { - guard let operationIdentifier = operation.operationIdentifier else { - preconditionFailure("To enable `autoPersistQueries`, Apollo types must be generated with operationIdentifiers") - } - - body["extensions"] = [ - "persistedQuery" : ["sha256Hash": operationIdentifier, "version": 1] - ] - } - - return body - } - - /// Creates multi-part form data to send with a request - /// - /// - Parameters: - /// - operation: The operation to create the data for. - /// - files: An array of files to use. - /// - sendOperationIdentifiers: True if operation identifiers should be sent, false if not. - /// - serializationFormat: The format to use to serialize data. - /// - manualBoundary: [optional] A manual boundary to pass in. A default boundary will be used otherwise. - /// - Returns: The created form data - /// - Throws: Errors creating or loading the form data - public func requestMultipartFormData(for operation: Operation, - files: [GraphQLFile], - sendOperationIdentifiers: Bool, - serializationFormat: JSONSerializationFormat.Type, - manualBoundary: String?) throws -> MultipartFormData { - let formData: MultipartFormData - - if let boundary = manualBoundary { - formData = MultipartFormData(boundary: boundary) - } else { - formData = MultipartFormData() - } - - // Make sure all fields for files are set to null, or the server won't look - // for the files in the rest of the form data - let fieldsForFiles = Set(files.map { $0.fieldName }).sorted() - var fields = requestBody(for: operation, sendOperationIdentifiers: sendOperationIdentifiers) - var variables = fields["variables"] as? GraphQLMap ?? GraphQLMap() - for fieldName in fieldsForFiles { - if - let value = variables[fieldName], - let arrayValue = value as? [JSONEncodable] { - let arrayOfNils: [JSONEncodable?] = arrayValue.map { _ in nil } - variables.updateValue(arrayOfNils, forKey: fieldName) - } else { - variables.updateValue(nil, forKey: fieldName) - } - } - fields["variables"] = variables - - let operationData = try serializationFormat.serialize(value: fields) - formData.appendPart(data: operationData, name: "operations") - - // If there are multiple files for the same field, make sure to include them with indexes for the field. If there are multiple files for different fields, just use the field name. - var map = [String: [String]]() - var currentIndex = 0 - - var sortedFiles = [GraphQLFile]() - for fieldName in fieldsForFiles { - let filesForField = files.filter { $0.fieldName == fieldName } - if filesForField.count == 1 { - let firstFile = filesForField.first! - map["\(currentIndex)"] = ["variables.\(firstFile.fieldName)"] - sortedFiles.append(firstFile) - currentIndex += 1 - } else { - for (index, file) in filesForField.enumerated() { - map["\(currentIndex)"] = ["variables.\(file.fieldName).\(index)"] - sortedFiles.append(file) - currentIndex += 1 - } - } - } - - assert(sortedFiles.count == files.count, "Number of sorted files did not equal the number of incoming files - some field name has been left out.") - - let mapData = try serializationFormat.serialize(value: map) - formData.appendPart(data: mapData, name: "map") - - for (index, file) in sortedFiles.enumerated() { - formData.appendPart(inputStream: try file.generateInputStream(), - contentLength: file.contentLength, - name: "\(index)", - contentType: file.mimeType, - filename: file.originalName) - } - - return formData - } -} - -// Helper struct to create requests independently of HTTP operations. -public struct ApolloRequestCreator: RequestCreator { - // Internal init methods cannot be used in public methods - public init() { } -} diff --git a/Sources/Apollo/ResponseCodeInterceptor.swift b/Sources/Apollo/ResponseCodeInterceptor.swift new file mode 100644 index 0000000000..25c63a05de --- /dev/null +++ b/Sources/Apollo/ResponseCodeInterceptor.swift @@ -0,0 +1,59 @@ +import Foundation + +/// An interceptor to check the response code returned with a request. +public class ResponseCodeInterceptor: ApolloInterceptor { + + public enum ResponseCodeError: Error, LocalizedError { + case invalidResponseCode(response: HTTPURLResponse?, rawData: Data?) + + public var errorDescription: String? { + switch self { + case .invalidResponseCode(let response, let rawData): + var errorStrings = [String]() + if let code = response?.statusCode { + errorStrings.append("Received a \(code) error.") + } else { + errorStrings.append("Did not receive a valid status code.") + } + + if + let data = rawData, + let dataString = String(bytes: data, encoding: .utf8) { + errorStrings.append("Data returned as a String was:") + errorStrings.append(dataString) + } else { + errorStrings.append("Data was nil or could not be transformed into a string.") + } + + return errorStrings.joined(separator: " ") + } + } + } + + /// Designated initializer + public init() {} + + public func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) { + + + guard response?.httpResponse.apollo.isSuccessful == true else { + let error = ResponseCodeError.invalidResponseCode(response: response?.httpResponse, + + rawData: response?.rawData) + + chain.handleErrorAsync(error, + request: request, + response: response, + completion: completion) + return + } + + chain.proceedAsync(request: request, + response: response, + completion: completion) + } +} diff --git a/Sources/Apollo/UploadRequest.swift b/Sources/Apollo/UploadRequest.swift new file mode 100644 index 0000000000..f088b81ea2 --- /dev/null +++ b/Sources/Apollo/UploadRequest.swift @@ -0,0 +1,125 @@ +import Foundation + +/// A request class allowing for a multipart-upload request. +open class UploadRequest: HTTPRequest { + + public let requestBodyCreator: RequestBodyCreator + public let files: [GraphQLFile] + public let manualBoundary: String? + + public let serializationFormat = JSONSerializationFormat.self + + /// Designated Initializer + /// + /// - Parameters: + /// - graphQLEndpoint: The endpoint to make a GraphQL request to + /// - operation: The GraphQL Operation to execute + /// - clientName: The name of the client to send with the `"apollographql-client-name"` header + /// - clientVersion: The version of the client to send with the `"apollographql-client-version"` header + /// - additionalHeaders: Any additional headers you wish to add by default to this request. Defaults to an empty dictionary. + /// - files: The array of files to upload for all `Upload` parameters in the mutation. + /// - manualBoundary: [optional] A manual boundary to pass in. A default boundary will be used otherwise. Defaults to nil. + /// - requestBodyCreator: An object conforming to the `RequestBodyCreator` protocol to assist with creating the request body. Defaults to the provided `ApolloRequestBodyCreator` implementation. + public init(graphQLEndpoint: URL, + operation: Operation, + clientName: String, + clientVersion: String, + additionalHeaders: [String: String] = [:], + files: [GraphQLFile], + manualBoundary: String? = nil, + requestBodyCreator: RequestBodyCreator = ApolloRequestBodyCreator()) { + self.requestBodyCreator = requestBodyCreator + self.files = files + self.manualBoundary = manualBoundary + super.init(graphQLEndpoint: graphQLEndpoint, + operation: operation, + contentType: "multipart/form-data", + clientName: clientName, + clientVersion: clientVersion, + additionalHeaders: additionalHeaders) + } + + public override func toURLRequest() throws -> URLRequest { + let formData = try self.requestMultipartFormData() + self.updateContentType(to: "multipart/form-data; boundary=\(formData.boundary)") + var request = try super.toURLRequest() + request.httpBody = try formData.encode() + request.httpMethod = GraphQLHTTPMethod.POST.rawValue + + return request + } + + /// Creates the `MultipartFormData` object to use when creating the URL Request. + /// + /// This method follows the [GraphQL Multipart Request Spec](https://github.com/jaydenseric/graphql-multipart-request-spec) Override this method to use a different upload spec. + /// + /// - Throws: Any error arising from creating the form data + /// - Returns: The created form data + open func requestMultipartFormData() throws -> MultipartFormData { + let shouldSendOperationID = (self.operation.operationIdentifier != nil) + + let formData: MultipartFormData + + if let boundary = manualBoundary { + formData = MultipartFormData(boundary: boundary) + } else { + formData = MultipartFormData() + } + + // Make sure all fields for files are set to null, or the server won't look + // for the files in the rest of the form data + let fieldsForFiles = Set(files.map { $0.fieldName }).sorted() + var fields = self.requestBodyCreator.requestBody(for: operation, sendOperationIdentifiers: shouldSendOperationID) + var variables = fields["variables"] as? GraphQLMap ?? GraphQLMap() + for fieldName in fieldsForFiles { + if + let value = variables[fieldName], + let arrayValue = value as? [JSONEncodable] { + let arrayOfNils: [JSONEncodable?] = arrayValue.map { _ in nil } + variables.updateValue(arrayOfNils, forKey: fieldName) + } else { + variables.updateValue(nil, forKey: fieldName) + } + } + fields["variables"] = variables + + let operationData = try serializationFormat.serialize(value: fields) + formData.appendPart(data: operationData, name: "operations") + + // If there are multiple files for the same field, make sure to include them with indexes for the field. If there are multiple files for different fields, just use the field name. + var map = [String: [String]]() + var currentIndex = 0 + + var sortedFiles = [GraphQLFile]() + for fieldName in fieldsForFiles { + let filesForField = files.filter { $0.fieldName == fieldName } + if filesForField.count == 1 { + let firstFile = filesForField.first! + map["\(currentIndex)"] = ["variables.\(firstFile.fieldName)"] + sortedFiles.append(firstFile) + currentIndex += 1 + } else { + for (index, file) in filesForField.enumerated() { + map["\(currentIndex)"] = ["variables.\(file.fieldName).\(index)"] + sortedFiles.append(file) + currentIndex += 1 + } + } + } + + assert(sortedFiles.count == files.count, "Number of sorted files did not equal the number of incoming files - some field name has been left out.") + + let mapData = try serializationFormat.serialize(value: map) + formData.appendPart(data: mapData, name: "map") + + for (index, file) in sortedFiles.enumerated() { + formData.appendPart(inputStream: try file.generateInputStream(), + contentLength: file.contentLength, + name: "\(index)", + contentType: file.mimeType, + filename: file.originalName) + } + + return formData + } +} diff --git a/Sources/ApolloCodegenLib/FileFinder.swift b/Sources/ApolloCodegenLib/FileFinder.swift index 85076fdc37..b2e0158dc6 100644 --- a/Sources/ApolloCodegenLib/FileFinder.swift +++ b/Sources/ApolloCodegenLib/FileFinder.swift @@ -2,9 +2,15 @@ import Foundation public struct FileFinder { + #if compiler(>=5.3) public static func findParentFolder(from filePath: StaticString = #filePath) -> URL { self.findParentFolder(from: filePath.apollo.toString) } + #else + public static func findParentFolder(from filePath: StaticString = #file) -> URL { + self.findParentFolder(from: filePath.apollo.toString) + } + #endif public static func findParentFolder(from filePath: String) -> URL { let url = URL(fileURLWithPath: filePath) diff --git a/Sources/ApolloTestSupport/MockNetworkTransport.swift b/Sources/ApolloTestSupport/MockNetworkTransport.swift index ee1cb8566e..c2be0c91ef 100644 --- a/Sources/ApolloTestSupport/MockNetworkTransport.swift +++ b/Sources/ApolloTestSupport/MockNetworkTransport.swift @@ -1,25 +1,31 @@ -import Apollo +@testable import Apollo import Dispatch -public final class MockNetworkTransport: NetworkTransport { - public var body: JSONObject +public final class MockNetworkTransport: RequestChainNetworkTransport { - public var clientName = "MockNetworkTransport" - public var clientVersion = "mock_version" - - public init(body: JSONObject) { - self.body = body - } + private let mockClient: MockURLSessionClient - public func send(operation: Operation, completionHandler: @escaping (_ result: Result, Error>) -> Void) -> Cancellable { - DispatchQueue.global(qos: .default).async { - completionHandler(.success(GraphQLResponse(operation: operation, body: self.body))) - } - return MockTask() + public init(body: JSONObject, store: ApolloStore) { + let testURL = TestURL.mockServer.url + self.mockClient = MockURLSessionClient() + self.mockClient.data = try! JSONSerializationFormat.serialize(value: body) + self.mockClient.response = HTTPURLResponse(url: testURL, + statusCode: 200, + httpVersion: nil, + headerFields: nil) + let legacyProvider = LegacyInterceptorProvider(client: self.mockClient, + store: store) + super.init(interceptorProvider: legacyProvider, + endpointURL: TestURL.mockServer.url) + } + + public func updateBody(to body: JSONObject) { + self.mockClient.data = try! JSONSerializationFormat.serialize(value: body) } } private final class MockTask: Cancellable { func cancel() { + // no-op } } diff --git a/Sources/ApolloTestSupport/MockURLSession.swift b/Sources/ApolloTestSupport/MockURLSession.swift index a21b03a96b..5effc28f52 100644 --- a/Sources/ApolloTestSupport/MockURLSession.swift +++ b/Sources/ApolloTestSupport/MockURLSession.swift @@ -7,10 +7,11 @@ import Foundation import Apollo +import ApolloCore public final class MockURLSessionClient: URLSessionClient { - public private (set) var lastRequest: URLRequest? + public private (set) var lastRequest: Atomic = Atomic(nil) public var data: Data? public var response: HTTPURLResponse? @@ -19,7 +20,7 @@ public final class MockURLSessionClient: URLSessionClient { public override func sendRequest(_ request: URLRequest, rawTaskCompletionHandler: URLSessionClient.RawCompletion? = nil, completion: @escaping URLSessionClient.Completion) -> URLSessionTask { - self.lastRequest = request + self.lastRequest.value = request rawTaskCompletionHandler?(self.data, self.response, self.error) @@ -47,6 +48,6 @@ public final class MockURLSessionClient: URLSessionClient { private final class URLSessionDataTaskMock: URLSessionDataTask { override func resume() { - + // No-op } } diff --git a/Sources/ApolloTestSupport/TestURLs.swift b/Sources/ApolloTestSupport/TestURLs.swift new file mode 100644 index 0000000000..655f222bf6 --- /dev/null +++ b/Sources/ApolloTestSupport/TestURLs.swift @@ -0,0 +1,25 @@ +import Foundation + +/// URLs used in testing +public enum TestURL { + case mockServer + case starWarsServer + case starWarsWebSocket + case uploadServer + + public var url: URL { + let urlString: String + switch self { + case .starWarsServer: + urlString = "http://localhost:8080/graphql" + case .starWarsWebSocket: + urlString = "ws://localhost:8080/websocket" + case .uploadServer: + urlString = "http://localhost:4000" + case .mockServer: + urlString = "http://localhost/dummy_url" + } + + return URL(string: urlString)! + } +} diff --git a/Sources/ApolloWebSocket/SplitNetworkTransport.swift b/Sources/ApolloWebSocket/SplitNetworkTransport.swift index ccd3009ac5..7b9f6bb28b 100644 --- a/Sources/ApolloWebSocket/SplitNetworkTransport.swift +++ b/Sources/ApolloWebSocket/SplitNetworkTransport.swift @@ -1,14 +1,15 @@ +import Foundation #if !COCOAPODS import Apollo #endif /// A network transport that sends subscriptions using one `NetworkTransport` and other requests using another `NetworkTransport`. Ideal for sending subscriptions via a web socket but everything else via HTTP. public class SplitNetworkTransport { - private let httpNetworkTransport: UploadingNetworkTransport + private let uploadingNetworkTransport: UploadingNetworkTransport private let webSocketNetworkTransport: NetworkTransport public var clientName: String { - let httpName = self.httpNetworkTransport.clientName + let httpName = self.uploadingNetworkTransport.clientName let websocketName = self.webSocketNetworkTransport.clientName if httpName == websocketName { return httpName @@ -18,7 +19,7 @@ public class SplitNetworkTransport { } public var clientVersion: String { - let httpVersion = self.httpNetworkTransport.clientVersion + let httpVersion = self.uploadingNetworkTransport.clientVersion let websocketVersion = self.webSocketNetworkTransport.clientVersion if httpVersion == websocketVersion { return httpVersion @@ -30,10 +31,10 @@ public class SplitNetworkTransport { /// Designated initializer /// /// - Parameters: - /// - httpNetworkTransport: An `UploadingNetworkTransport` to use for non-subscription requests. Should generally be a `HTTPNetworkTransport` or something similar. + /// - uploadingNetworkTransport: An `UploadingNetworkTransport` to use for non-subscription requests. Should generally be a `RequestChainNetworkTransport` or something similar. /// - webSocketNetworkTransport: A `NetworkTransport` to use for subscription requests. Should generally be a `WebSocketTransport` or something similar. - public init(httpNetworkTransport: UploadingNetworkTransport, webSocketNetworkTransport: NetworkTransport) { - self.httpNetworkTransport = httpNetworkTransport + public init(uploadingNetworkTransport: UploadingNetworkTransport, webSocketNetworkTransport: NetworkTransport) { + self.uploadingNetworkTransport = uploadingNetworkTransport self.webSocketNetworkTransport = webSocketNetworkTransport } } @@ -42,11 +43,23 @@ public class SplitNetworkTransport { extension SplitNetworkTransport: NetworkTransport { - public func send(operation: Operation, completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable { + public func send(operation: Operation, + cachePolicy: CachePolicy, + contextIdentifier: UUID? = nil, + callbackQueue: DispatchQueue = .main, + completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable { if operation.operationType == .subscription { - return webSocketNetworkTransport.send(operation: operation, completionHandler: completionHandler) + return webSocketNetworkTransport.send(operation: operation, + cachePolicy: cachePolicy, + contextIdentifier: contextIdentifier, + callbackQueue: callbackQueue, + completionHandler: completionHandler) } else { - return httpNetworkTransport.send(operation: operation, completionHandler: completionHandler) + return uploadingNetworkTransport.send(operation: operation, + cachePolicy: cachePolicy, + contextIdentifier: contextIdentifier, + callbackQueue: callbackQueue, + completionHandler: completionHandler) } } } @@ -55,11 +68,14 @@ extension SplitNetworkTransport: NetworkTransport { extension SplitNetworkTransport: UploadingNetworkTransport { - public func upload(operation: Operation, - files: [GraphQLFile], - completionHandler: @escaping (_ result: Result, Error>) -> Void) -> Cancellable { - return httpNetworkTransport.upload(operation: operation, - files: files, - completionHandler: completionHandler) + public func upload( + operation: Operation, + files: [GraphQLFile], + callbackQueue: DispatchQueue = .main, + completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable { + return uploadingNetworkTransport.upload(operation: operation, + files: files, + callbackQueue: callbackQueue, + completionHandler: completionHandler) } } diff --git a/Sources/ApolloWebSocket/WebSocketTransport.swift b/Sources/ApolloWebSocket/WebSocketTransport.swift index 9f8b98adce..f4abfac697 100644 --- a/Sources/ApolloWebSocket/WebSocketTransport.swift +++ b/Sources/ApolloWebSocket/WebSocketTransport.swift @@ -30,7 +30,7 @@ public class WebSocketTransport { var websocket: ApolloWebSocketClient let error: Atomic = Atomic(nil) let serializationFormat = JSONSerializationFormat.self - private let requestCreator: RequestCreator + private let requestBodyCreator: RequestBodyCreator private final let protocols = ["graphql-ws"] @@ -104,7 +104,7 @@ public class WebSocketTransport { /// - Parameter reconnectionInterval: How long to wait before attempting to reconnect. Defaults to half a second. /// - Parameter allowSendingDuplicates: Allow sending duplicate messages. Important when reconnected. Defaults to true. /// - Parameter connectingPayload: [optional] The payload to send on connection. Defaults to an empty `GraphQLMap`. - /// - Parameter requestCreator: The request creator to use when serializing requests. Defaults to an `ApolloRequestCreator`. + /// - Parameter requestBodyCreator: The `RequestBodyCreator` to use when serializing requests. Defaults to an `ApolloRequestBodyCreator`. public init(request: URLRequest, clientName: String = WebSocketTransport.defaultClientName, clientVersion: String = WebSocketTransport.defaultClientVersion, @@ -113,13 +113,13 @@ public class WebSocketTransport { reconnectionInterval: TimeInterval = 0.5, allowSendingDuplicates: Bool = true, connectingPayload: GraphQLMap? = [:], - requestCreator: RequestCreator = ApolloRequestCreator()) { + requestBodyCreator: RequestBodyCreator = ApolloRequestBodyCreator()) { self.connectingPayload = connectingPayload self.sendOperationIdentifiers = sendOperationIdentifiers self.reconnect = Atomic(reconnect) self.reconnectionInterval = reconnectionInterval self.allowSendingDuplicates = allowSendingDuplicates - self.requestCreator = requestCreator + self.requestBodyCreator = requestBodyCreator self.websocket = WebSocketTransport.provider.init(request: request, protocols: protocols) self.clientName = clientName self.clientVersion = clientVersion @@ -270,7 +270,7 @@ public class WebSocketTransport { } func sendHelper(operation: Operation, resultHandler: @escaping (_ result: Result) -> Void) -> String? { - let body = requestCreator.requestBody(for: operation, sendOperationIdentifiers: self.sendOperationIdentifiers) + let body = requestBodyCreator.requestBody(for: operation, sendOperationIdentifiers: self.sendOperationIdentifiers) let sequenceNumber = "\(sequenceNumberCounter.increment())" guard let message = OperationMessage(payload: body, id: sequenceNumber).rawMessage else { @@ -342,10 +342,15 @@ public class WebSocketTransport { } } -// MARK: - HTTPNetworkTransport conformance +// MARK: - NetworkTransport conformance extension WebSocketTransport: NetworkTransport { - public func send(operation: Operation, completionHandler: @escaping (_ result: Result,Error>) -> Void) -> Cancellable { + public func send( + operation: Operation, + cachePolicy: CachePolicy, + contextIdentifier: UUID? = nil, + callbackQueue: DispatchQueue = .main, + completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable { if let error = self.error.value { completionHandler(.failure(error)) return EmptyCancellable() @@ -355,7 +360,12 @@ extension WebSocketTransport: NetworkTransport { switch result { case .success(let jsonBody): let response = GraphQLResponse(operation: operation, body: jsonBody) - completionHandler(.success(response)) + do { + let graphQLResult = try response.parseResultFast() + completionHandler(.success(graphQLResult)) + } catch { + completionHandler(.failure(error)) + } case .failure(let error): completionHandler(.failure(error)) } diff --git a/Tests/ApolloCacheDependentTests/FetchQueryTests.swift b/Tests/ApolloCacheDependentTests/FetchQueryTests.swift index 641f01b972..cbbc18ab5f 100644 --- a/Tests/ApolloCacheDependentTests/FetchQueryTests.swift +++ b/Tests/ApolloCacheDependentTests/FetchQueryTests.swift @@ -30,7 +30,7 @@ class FetchQueryTests: XCTestCase, CacheTesting { "__typename": "Human" ] ] - ]) + ], store: store) let client = ApolloClient(networkTransport: networkTransport, store: store) @@ -72,7 +72,7 @@ class FetchQueryTests: XCTestCase, CacheTesting { "__typename": "Human" ] ] - ]) + ], store: store) let client = ApolloClient(networkTransport: networkTransport, store: store) @@ -120,7 +120,7 @@ class FetchQueryTests: XCTestCase, CacheTesting { "__typename": "Human" ] ] - ]) + ], store: store) let client = ApolloClient(networkTransport: networkTransport, store: store) @@ -161,7 +161,7 @@ class FetchQueryTests: XCTestCase, CacheTesting { "__typename": "Human" ] ] - ]) + ], store: store) let client = ApolloClient(networkTransport: networkTransport, store: store) @@ -203,7 +203,7 @@ class FetchQueryTests: XCTestCase, CacheTesting { "__typename": "Human" ] ] - ]) + ], store: store) let client = ApolloClient(networkTransport: networkTransport, store: store) @@ -245,7 +245,7 @@ class FetchQueryTests: XCTestCase, CacheTesting { "__typename": "Human" ] ] - ]) + ], store: store) let client = ApolloClient(networkTransport: networkTransport, store: store) @@ -324,7 +324,7 @@ class FetchQueryTests: XCTestCase, CacheTesting { "__typename": "Human" ] ] - ]) + ], store: store) let client = ApolloClient(networkTransport: networkTransport, store: store) @@ -364,17 +364,17 @@ class FetchQueryTests: XCTestCase, CacheTesting { let query = HeroNameQuery() - let networkTransport = MockNetworkTransport(body: [ - "data": [ - "hero": [ - "name": "Luke Skywalker", - "__typename": "Human" - ] - ] - ]) - withCache { (cache) in let store = ApolloStore(cache: cache) + let networkTransport = MockNetworkTransport(body: [ + "data": [ + "hero": [ + "name": "Luke Skywalker", + "__typename": "Human" + ] + ] + ], store: store) + let client = ApolloClient(networkTransport: networkTransport, store: store) let expectation = self.expectation(description: "Fetching query") @@ -391,6 +391,8 @@ class FetchQueryTests: XCTestCase, CacheTesting { func testThreadedCache() throws { let cache = InMemoryNormalizedCache() + let store = ApolloStore(cache: cache) + let store2 = ApolloStore(cache: cache) let networkTransport1 = MockNetworkTransport(body: [ "data": [ @@ -404,7 +406,7 @@ class FetchQueryTests: XCTestCase, CacheTesting { ] ] ] - ]) + ], store: store) let networkTransport2 = MockNetworkTransport(body: [ "data": [ @@ -418,10 +420,7 @@ class FetchQueryTests: XCTestCase, CacheTesting { ] ] ] - ]) - - let store = ApolloStore(cache: cache) - let store2 = ApolloStore(cache: cache) + ], store: store2) let client1 = ApolloClient(networkTransport: networkTransport1, store: store) let client2 = ApolloClient(networkTransport: networkTransport2, store: store2) diff --git a/Tests/ApolloCacheDependentTests/SQLiteCacheTests.swift b/Tests/ApolloCacheDependentTests/SQLiteCacheTests.swift index f7650ae728..c559a208d8 100644 --- a/Tests/ApolloCacheDependentTests/SQLiteCacheTests.swift +++ b/Tests/ApolloCacheDependentTests/SQLiteCacheTests.swift @@ -49,3 +49,4 @@ class SQLiteWatchQueryTests: WatchQueryTests { SQLiteTestCacheProvider.self } } + diff --git a/Tests/ApolloCacheDependentTests/StarWarsServerCachingRoundtripTests.swift b/Tests/ApolloCacheDependentTests/StarWarsServerCachingRoundtripTests.swift index 2ee998bc0b..0573ff2f84 100644 --- a/Tests/ApolloCacheDependentTests/StarWarsServerCachingRoundtripTests.swift +++ b/Tests/ApolloCacheDependentTests/StarWarsServerCachingRoundtripTests.swift @@ -40,8 +40,11 @@ class StarWarsServerCachingRoundtripTests: XCTestCase, CacheTesting { private func fetchAndLoadFromStore(query: Query, setupClient: ((ApolloClient) -> Void)? = nil, completionHandler: @escaping (_ data: Query.Data) -> Void) { withCache { (cache) in - let network = HTTPNetworkTransport(url: URL(string: "http://localhost:8080/graphql")!) let store = ApolloStore(cache: cache) + let provider = LegacyInterceptorProvider(store: store) + let network = RequestChainNetworkTransport(interceptorProvider: provider, + endpointURL: TestURL.starWarsServer.url) + let client = ApolloClient(networkTransport: network, store: store) if let setupClient = setupClient { diff --git a/Tests/ApolloCacheDependentTests/StarWarsServerTests.swift b/Tests/ApolloCacheDependentTests/StarWarsServerTests.swift index 3345bd6d9b..3cd0b252e7 100644 --- a/Tests/ApolloCacheDependentTests/StarWarsServerTests.swift +++ b/Tests/ApolloCacheDependentTests/StarWarsServerTests.swift @@ -5,40 +5,49 @@ import StarWarsAPI protocol TestConfig { - func network() -> HTTPNetworkTransport + func network(store: ApolloStore) -> NetworkTransport } class DefaultConfig: TestConfig { - let transport = HTTPNetworkTransport(url: URL(string: "http://localhost:8080/graphql")!) - func network() -> HTTPNetworkTransport { - return transport + + func transport(with store: ApolloStore) -> NetworkTransport { + let provider = LegacyInterceptorProvider(store: store) + return RequestChainNetworkTransport(interceptorProvider: provider, + endpointURL: TestURL.starWarsServer.url) + } + + func network(store: ApolloStore) -> NetworkTransport { + return transport(with: store) } } class APQsConfig: TestConfig { - let transport = HTTPNetworkTransport(url: URL(string: "http://localhost:8080/graphql")!, - enableAutoPersistedQueries: true) - func network() -> HTTPNetworkTransport { - return transport + + func transport(with store: ApolloStore) -> NetworkTransport { + let provider = LegacyInterceptorProvider(store: store) + return RequestChainNetworkTransport(interceptorProvider: provider, + endpointURL: TestURL.starWarsServer.url, + autoPersistQueries: true) + } + + func network(store: ApolloStore) -> NetworkTransport { + return transport(with: store) } } -class APQsWithGetMethodConfig: TestConfig, HTTPNetworkTransportRetryDelegate{ +class APQsWithGetMethodConfig: TestConfig { - var alreadyRetried = false - func networkTransport(_ networkTransport: HTTPNetworkTransport, receivedError error: Error, for request: URLRequest, response: URLResponse?, continueHandler: @escaping (HTTPNetworkTransport.ContinueAction) -> Void) { - continueHandler(!alreadyRetried ? .retry : .fail(error)) - alreadyRetried = true + func transport(with store: ApolloStore) -> NetworkTransport { + let provider = LegacyInterceptorProvider(store: store) + return RequestChainNetworkTransport(interceptorProvider: provider, + endpointURL: TestURL.starWarsServer.url, + autoPersistQueries: true, + useGETForPersistedQueryRetry: true) } - func network() -> HTTPNetworkTransport { - let transport = HTTPNetworkTransport(url: URL(string: "http://localhost:8080/graphql")!, - enableAutoPersistedQueries: true, - useGETForPersistedQueryRetry: true) - transport.delegate = self - return transport + func network(store: ApolloStore) -> NetworkTransport { + return transport(with: store) } - } class StarWarsServerAPQsGetMethodTests: StarWarsServerTests { @@ -330,7 +339,7 @@ class StarWarsServerTests: XCTestCase, CacheTesting { withCache { (cache) in let store = ApolloStore(cache: cache) - let client = ApolloClient(networkTransport: config.network(), store: store) + let client = ApolloClient(networkTransport: config.network(store: store), store: store) let expectation = self.expectation(description: "Fetching query") @@ -363,7 +372,7 @@ class StarWarsServerTests: XCTestCase, CacheTesting { withCache { (cache) in let store = ApolloStore(cache: cache) - let client = ApolloClient(networkTransport: config.network(), store: store) + let client = ApolloClient(networkTransport: config.network(store: store), store: store) let expectation = self.expectation(description: "Performing mutation") diff --git a/Tests/ApolloCacheDependentTests/WatchQueryTests.swift b/Tests/ApolloCacheDependentTests/WatchQueryTests.swift index 30138520cb..7dde35f46c 100644 --- a/Tests/ApolloCacheDependentTests/WatchQueryTests.swift +++ b/Tests/ApolloCacheDependentTests/WatchQueryTests.swift @@ -20,7 +20,8 @@ class WatchQueryTests: XCTestCase, CacheTesting { ] ] - withCache(initialRecords: initialRecords) { (cache) in + withCache(initialRecords: initialRecords) { cache in + let store = ApolloStore(cache: cache) let networkTransport = MockNetworkTransport(body: [ "data": [ "hero": [ @@ -28,8 +29,7 @@ class WatchQueryTests: XCTestCase, CacheTesting { "__typename": "Droid" ] ] - ]) - let store = ApolloStore(cache: cache) + ], store: store) let client = ApolloClient(networkTransport: networkTransport, store: store) var verifyResult: GraphQLResultHandler @@ -68,6 +68,8 @@ class WatchQueryTests: XCTestCase, CacheTesting { watcher.refetch() waitForExpectations(timeout: 5, handler: nil) + + watcher.cancel() } } @@ -88,7 +90,8 @@ class WatchQueryTests: XCTestCase, CacheTesting { "QUERY_ROOT.hero.friends.2": ["__typename": "Human", "name": "Leia Organa"], ] - withCache(initialRecords: initialRecords) { (cache) in + withCache(initialRecords: initialRecords) { cache in + let store = ApolloStore(cache: cache) let networkTransport = MockNetworkTransport(body: [ "data": [ "hero": [ @@ -96,8 +99,7 @@ class WatchQueryTests: XCTestCase, CacheTesting { "__typename": "Droid" ] ] - ]) - let store = ApolloStore(cache: cache) + ], store: store) let client = ApolloClient(networkTransport: networkTransport, store: store) let query = HeroAndFriendsNamesQuery() @@ -175,7 +177,8 @@ class WatchQueryTests: XCTestCase, CacheTesting { "QUERY_ROOT.hero.friends.2": ["__typename": "Human", "name": "Leia Organa"], ] - withCache(initialRecords: initialRecords) { (cache) in + withCache(initialRecords: initialRecords) { cache in + let store = ApolloStore(cache: cache) let networkTransport = MockNetworkTransport(body: [ "data": [ "hero": [ @@ -187,8 +190,7 @@ class WatchQueryTests: XCTestCase, CacheTesting { ] ] ] - ]) - let store = ApolloStore(cache: cache) + ], store: store) let client = ApolloClient(networkTransport: networkTransport, store: store) let query = HeroAndFriendsNamesQuery() @@ -267,7 +269,8 @@ class WatchQueryTests: XCTestCase, CacheTesting { "QUERY_ROOT.hero.friends.2": ["__typename": "Human", "name": "Leia Organa"], ] - withCache(initialRecords: initialRecords) { (cache) in + withCache(initialRecords: initialRecords) { cache in + let store = ApolloStore(cache: cache) let networkTransport = MockNetworkTransport(body: [ "data": [ "hero": [ @@ -275,9 +278,8 @@ class WatchQueryTests: XCTestCase, CacheTesting { "__typename": "Droid" ] ] - ]) + ], store: store) - let store = ApolloStore(cache: cache) let client = ApolloClient(networkTransport: networkTransport, store: store) let query = HeroAndFriendsNamesQuery() @@ -339,7 +341,8 @@ class WatchQueryTests: XCTestCase, CacheTesting { ] ] - withCache(initialRecords: initialRecords) { (cache) in + withCache(initialRecords: initialRecords) { cache in + let store = ApolloStore(cache: cache) let networkTransport = MockNetworkTransport(body: [ "data": [ "hero": [ @@ -348,8 +351,7 @@ class WatchQueryTests: XCTestCase, CacheTesting { "__typename": "Human" ] ] - ]) - let store = ApolloStore(cache: cache) + ], store: store) let client = ApolloClient(networkTransport: networkTransport, store: store) client.store.cacheKeyForObject = { $0["id"] } @@ -411,7 +413,8 @@ class WatchQueryTests: XCTestCase, CacheTesting { "LO": ["__typename": "Human", "id": "LO", "name": "Leia Organa"], ] - withCache(initialRecords: initialRecords) { (cache) in + withCache(initialRecords: initialRecords) { cache in + let store = ApolloStore(cache: cache) let networkTransport = MockNetworkTransport(body: [ "data": [ "hero": [ @@ -424,8 +427,7 @@ class WatchQueryTests: XCTestCase, CacheTesting { ] ] ] - ]) - let store = ApolloStore(cache: cache) + ], store: store) let client = ApolloClient(networkTransport: networkTransport, store: store) client.store.cacheKeyForObject = { $0["id"] } @@ -500,9 +502,9 @@ class WatchQueryTests: XCTestCase, CacheTesting { "QUERY_ROOT.hero.friends.2": ["__typename": "Human", "name": "Leia Organa"], ] withCache(initialRecords: initialRecords) { (cache) in - let networkTransport = MockNetworkTransport(body: [:]) - let store = ApolloStore(cache: cache) + let networkTransport = MockNetworkTransport(body: [:], store: store) + let client = ApolloClient(networkTransport: networkTransport, store: store) let query = HeroAndFriendsNamesQuery() @@ -578,6 +580,8 @@ class WatchQueryTests: XCTestCase, CacheTesting { func testWatchedQueryDependentKeysAreUpdated() { withCache { cache in + let store = ApolloStore(cache: cache) + store.cacheKeyForObject = { $0["id"] } let networkTransport = MockNetworkTransport(body: [ "data": [ "hero": [ @@ -593,17 +597,13 @@ class WatchQueryTests: XCTestCase, CacheTesting { ] ] ] - ]) + ], store: store) - let store = ApolloStore(cache: cache) let client = ApolloClient(networkTransport: networkTransport, store: store) - client.store.cacheKeyForObject = { $0["id"] } - let query = HeroAndFriendsNamesWithIDsQuery() let hasPicardFriendExpecation = self.expectation(description: "Has friend named Jean-Luc Picard") let hasHanSoloFriendExpecation = self.expectation(description: "Has friend named Han Solo") let initialFetchExpectation = self.expectation(description: "Initial fetch") - var isInitialFetch = true var expectedDependentKeys = [ "0.__typename", "0.friends", @@ -615,12 +615,13 @@ class WatchQueryTests: XCTestCase, CacheTesting { "QUERY_ROOT.hero", ] - _ = client.watch(query: query) { result in + var fetchCount = 0 + let watcher = client.watch(query: query) { result in defer { - if isInitialFetch { - isInitialFetch = false + if fetchCount == 0 { initialFetchExpectation.fulfill() } + fetchCount += 1 } switch result { case .success(let graphQLResult): @@ -674,7 +675,7 @@ class WatchQueryTests: XCTestCase, CacheTesting { /// Send an update that updates friend #11 on a different query - networkTransport.body = [ + networkTransport.updateBody(to: [ "data": [ "hero": [ "id": "2", @@ -689,12 +690,14 @@ class WatchQueryTests: XCTestCase, CacheTesting { ] ] ] - ] + ]) /// This fetch should trigger our watcher on friend #11 client.fetch(query: HeroAndFriendsNamesWithIDsQuery(episode: .newhope), cachePolicy: .fetchIgnoringCacheData) self.wait(for: [hasHanSoloFriendExpecation], timeout: 1) + + watcher.cancel() } } } diff --git a/Tests/ApolloCodegenTests/ApolloSchemaTests.swift b/Tests/ApolloCodegenTests/ApolloSchemaTests.swift index 2f18b658c1..fb7552e2ad 100644 --- a/Tests/ApolloCodegenTests/ApolloSchemaTests.swift +++ b/Tests/ApolloCodegenTests/ApolloSchemaTests.swift @@ -7,19 +7,18 @@ // import XCTest +import ApolloTestSupport @testable import ApolloCodegenLib class ApolloSchemaTests: XCTestCase { - - private lazy var endpointURL = URL(string: "http://localhost:8080/graphql")! - + func testCreatingOptionsWithDefaultParameters() throws { let sourceRoot = CodegenTestHelper.sourceRootURL() - let options = ApolloSchemaOptions(endpointURL: self.endpointURL, + let options = ApolloSchemaOptions(endpointURL: TestURL.starWarsServer.url, outputFolderURL: sourceRoot) let expectedOutputURL = sourceRoot.appendingPathComponent("schema.json") - XCTAssertEqual(options.endpointURL, self.endpointURL) + XCTAssertEqual(options.endpointURL, TestURL.starWarsServer.url) XCTAssertEqual(options.outputURL, expectedOutputURL) XCTAssertNil(options.apiKey) XCTAssertTrue(options.headers.isEmpty) @@ -41,11 +40,11 @@ class ApolloSchemaTests: XCTestCase { let options = ApolloSchemaOptions(schemaFileName: "different_name", schemaFileType: .schemaDefinitionLanguage, apiKey: apiKey, - endpointURL: self.endpointURL, + endpointURL: TestURL.starWarsServer.url, headers: headers, outputFolderURL: sourceRoot) XCTAssertEqual(options.apiKey, apiKey) - XCTAssertEqual(options.endpointURL, self.endpointURL) + XCTAssertEqual(options.endpointURL, TestURL.starWarsServer.url) XCTAssertEqual(options.headers, headers) let expectedOutputURL = sourceRoot.appendingPathComponent("different_name.graphql") @@ -64,7 +63,7 @@ class ApolloSchemaTests: XCTestCase { func testDownloadingSchemaAsJSON() throws { let testOutputFolderURL = CodegenTestHelper.outputFolderURL() - let options = ApolloSchemaOptions(endpointURL: self.endpointURL, + let options = ApolloSchemaOptions(endpointURL: TestURL.starWarsServer.url, outputFolderURL: testOutputFolderURL) // Delete anything existing at the output URL @@ -98,7 +97,7 @@ class ApolloSchemaTests: XCTestCase { let testOutputFolderURL = CodegenTestHelper.outputFolderURL() let options = ApolloSchemaOptions(schemaFileType: .schemaDefinitionLanguage, - endpointURL: self.endpointURL, + endpointURL: TestURL.starWarsServer.url, outputFolderURL: testOutputFolderURL) // Delete anything existing at the output URL diff --git a/Tests/ApolloSQLiteTests/CachePersistenceTests.swift b/Tests/ApolloSQLiteTests/CachePersistenceTests.swift index bdf5422b76..9334edc352 100644 --- a/Tests/ApolloSQLiteTests/CachePersistenceTests.swift +++ b/Tests/ApolloSQLiteTests/CachePersistenceTests.swift @@ -21,7 +21,7 @@ class CachePersistenceTests: XCTestCase { "__typename": "Human" ] ] - ]) + ], store: store) let client = ApolloClient(networkTransport: networkTransport, store: store) let networkExpectation = self.expectation(description: "Fetching query from network") @@ -77,7 +77,7 @@ class CachePersistenceTests: XCTestCase { "__typename": "Human" ] ] - ]) + ], store: store) let client = ApolloClient(networkTransport: networkTransport, store: store) let networkExpectation = self.expectation(description: "Fetching query from network") diff --git a/Tests/ApolloTests/AutomaticPersistedQueriesTests.swift b/Tests/ApolloTests/AutomaticPersistedQueriesTests.swift index 2dfaa8f57a..a591d5807a 100644 --- a/Tests/ApolloTests/AutomaticPersistedQueriesTests.swift +++ b/Tests/ApolloTests/AutomaticPersistedQueriesTests.swift @@ -5,7 +5,7 @@ import StarWarsAPI class AutomaticPersistedQueriesTests: XCTestCase { - private final let endpoint = "http://localhost:8080/graphql" + private final let endpoint = TestURL.starWarsServer.url // MARK: - Helper Methods @@ -231,14 +231,22 @@ class AutomaticPersistedQueriesTests: XCTestCase { func testRequestBody() throws { let mockClient = MockURLSessionClient() - let network = HTTPNetworkTransport(url: URL(string: endpoint)!, client: mockClient) + let provider = LegacyInterceptorProvider(client: mockClient) + let network = RequestChainNetworkTransport(interceptorProvider: provider, + endpointURL: self.endpoint) + + let expectation = self.expectation(description: "Query sent") let query = HeroNameQuery() - let _ = network.send(operation: query) { _ in } + var lastRequest: URLRequest? + let _ = network.send(operation: query) { _ in + lastRequest = mockClient.lastRequest.value + expectation.fulfill() + } + self.wait(for: [expectation], timeout: 1) - let request = try XCTUnwrap(mockClient.lastRequest, - "last request should not be nil") + let request = try XCTUnwrap(lastRequest, "last request should not be nil") - XCTAssertEqual(request.url?.host, network.url.host) + XCTAssertEqual(request.url?.host, network.endpointURL.host) XCTAssertEqual(request.httpMethod, "POST") try self.validatePostBody(with: request, @@ -248,15 +256,23 @@ class AutomaticPersistedQueriesTests: XCTestCase { func testRequestBodyWithVariable() throws { let mockClient = MockURLSessionClient() - let network = HTTPNetworkTransport(url: URL(string: endpoint)!, client: mockClient) + let provider = LegacyInterceptorProvider(client: mockClient) + let network = RequestChainNetworkTransport(interceptorProvider: provider, + endpointURL: self.endpoint) + + let expectation = self.expectation(description: "Query sent") let query = HeroNameQuery(episode: .jedi) - let _ = network.send(operation: query) { _ in } + var lastRequest: URLRequest? + let _ = network.send(operation: query) { _ in + lastRequest = mockClient.lastRequest.value + expectation.fulfill() + } + self.wait(for: [expectation], timeout: 1) - let request = try XCTUnwrap(mockClient.lastRequest, - "last request should not be nil") - XCTAssertEqual(request.url?.host, network.url.host) + let request = try XCTUnwrap(lastRequest, "last request should not be nil") + XCTAssertEqual(request.url?.host, network.endpointURL.host) XCTAssertEqual(request.httpMethod, "POST") - + try validatePostBody(with: request, query: query, queryDocument: true) @@ -265,35 +281,49 @@ class AutomaticPersistedQueriesTests: XCTestCase { func testRequestBodyForAPQsWithVariable() throws { let mockClient = MockURLSessionClient() - let network = HTTPNetworkTransport(url: URL(string: endpoint)!, - client: mockClient, - enableAutoPersistedQueries: true) + let provider = LegacyInterceptorProvider(client: mockClient) + let network = RequestChainNetworkTransport(interceptorProvider: provider, + endpointURL: self.endpoint, + autoPersistQueries: true) + + let expectation = self.expectation(description: "Query sent") let query = HeroNameQuery(episode: .empire) - let _ = network.send(operation: query) { _ in } + var lastRequest: URLRequest? + let _ = network.send(operation: query) { _ in + lastRequest = mockClient.lastRequest.value + expectation.fulfill() + } + self.wait(for: [expectation], timeout: 1) - let request = try XCTUnwrap(mockClient.lastRequest, - "last request should not be nil") - - XCTAssertEqual(request.url?.host, network.url.host) + let request = try XCTUnwrap(lastRequest, "last request should not be nil") + + XCTAssertEqual(request.url?.host, network.endpointURL.host) XCTAssertEqual(request.httpMethod, "POST") try self.validatePostBody(with: request, query: query, persistedQuery: true) } - + func testMutationRequestBodyForAPQs() throws { let mockClient = MockURLSessionClient() - let network = HTTPNetworkTransport(url: URL(string: endpoint)!, - client: mockClient, - enableAutoPersistedQueries: true) + let provider = LegacyInterceptorProvider(client: mockClient) + let network = RequestChainNetworkTransport(interceptorProvider: provider, + endpointURL: self.endpoint, + autoPersistQueries: true) + + let expectation = self.expectation(description: "Mutation sent") let mutation = CreateAwesomeReviewMutation() - let _ = network.send(operation: mutation) { _ in } - - let request = try XCTUnwrap(mockClient.lastRequest, - "last request should not be nil") - - XCTAssertEqual(request.url?.host, network.url.host) + var lastRequest: URLRequest? + let _ = network.send(operation: mutation) { _ in + lastRequest = mockClient.lastRequest.value + expectation.fulfill() + } + self.wait(for: [expectation], timeout: 1) + + let request = try XCTUnwrap(lastRequest, "last request should not be nil") + + XCTAssertEqual(request.url?.host, network.endpointURL.host) XCTAssertEqual(request.httpMethod, "POST") try self.validatePostBody(with: request, @@ -303,16 +333,23 @@ class AutomaticPersistedQueriesTests: XCTestCase { func testQueryStringForAPQsUseGetMethod() throws { let mockClient = MockURLSessionClient() - let network = HTTPNetworkTransport(url: URL(string: endpoint)!, - client: mockClient, - enableAutoPersistedQueries: true, - useGETForPersistedQueryRetry: true) + let provider = LegacyInterceptorProvider(client: mockClient) + let network = RequestChainNetworkTransport(interceptorProvider: provider, + endpointURL: self.endpoint, + autoPersistQueries: true, + useGETForPersistedQueryRetry: true) + + let expectation = self.expectation(description: "Query sent") let query = HeroNameQuery() - let _ = network.send(operation: query) { _ in } + var lastRequest: URLRequest? + let _ = network.send(operation: query) { _ in + lastRequest = mockClient.lastRequest.value + expectation.fulfill() + } + self.wait(for: [expectation], timeout: 1) - let request = try XCTUnwrap(mockClient.lastRequest, - "last request should not be nil") - XCTAssertEqual(request.url?.host, network.url.host) + let request = try XCTUnwrap(lastRequest, "last request should not be nil") + XCTAssertEqual(request.url?.host, network.endpointURL.host) try self.validateUrlParams(with: request, query: query, @@ -321,17 +358,24 @@ class AutomaticPersistedQueriesTests: XCTestCase { func testQueryStringForAPQsUseGetMethodWithVariable() throws { let mockClient = MockURLSessionClient() - let network = HTTPNetworkTransport(url: URL(string: endpoint)!, - client: mockClient, - enableAutoPersistedQueries: true, - useGETForPersistedQueryRetry: true) + let provider = LegacyInterceptorProvider(client: mockClient) + let network = RequestChainNetworkTransport(interceptorProvider: provider, + endpointURL: self.endpoint, + autoPersistQueries: true, + useGETForPersistedQueryRetry: true) + + let expectation = self.expectation(description: "Query sent") let query = HeroNameQuery(episode: .empire) - let _ = network.send(operation: query) { _ in } + var lastRequest: URLRequest? + let _ = network.send(operation: query) { _ in + lastRequest = mockClient.lastRequest.value + expectation.fulfill() + } + self.wait(for: [expectation], timeout: 1) - let request = try XCTUnwrap(mockClient.lastRequest, - "last request should not be nil") - - XCTAssertEqual(request.url?.host, network.url.host) + let request = try XCTUnwrap(lastRequest, "last request should not be nil") + + XCTAssertEqual(request.url?.host, network.endpointURL.host) XCTAssertEqual(request.httpMethod, "GET") try self.validateUrlParams(with: request, @@ -341,16 +385,23 @@ class AutomaticPersistedQueriesTests: XCTestCase { func testUseGETForQueriesRequest() throws { let mockClient = MockURLSessionClient() - let network = HTTPNetworkTransport(url: URL(string: endpoint)!, - client: mockClient, - useGETForQueries: true) + let provider = LegacyInterceptorProvider(client: mockClient) + let network = RequestChainNetworkTransport(interceptorProvider: provider, + endpointURL: self.endpoint, + useGETForQueries: true) + + let expectation = self.expectation(description: "Query sent") let query = HeroNameQuery() - let _ = network.send(operation: query) { _ in } + var lastRequest: URLRequest? + let _ = network.send(operation: query) { _ in + lastRequest = mockClient.lastRequest.value + expectation.fulfill() + } + self.wait(for: [expectation], timeout: 1) - let request = try XCTUnwrap(mockClient.lastRequest, - "last request should not be nil") + let request = try XCTUnwrap(lastRequest, "last request should not be nil") - XCTAssertEqual(request.url?.host, network.url.host) + XCTAssertEqual(request.url?.host, network.endpointURL.host) XCTAssertEqual(request.httpMethod, "GET") try self.validateUrlParams(with: request, @@ -360,14 +411,22 @@ class AutomaticPersistedQueriesTests: XCTestCase { func testNotUseGETForQueriesRequest() throws { let mockClient = MockURLSessionClient() - let network = HTTPNetworkTransport(url: URL(string: endpoint)!, client: mockClient) + let provider = LegacyInterceptorProvider(client: mockClient) + let network = RequestChainNetworkTransport(interceptorProvider: provider, + endpointURL: self.endpoint) + + let expectation = self.expectation(description: "Query sent") let query = HeroNameQuery() - let _ = network.send(operation: query) { _ in } + var lastRequest: URLRequest? + let _ = network.send(operation: query) { _ in + lastRequest = mockClient.lastRequest.value + expectation.fulfill() + } + self.wait(for: [expectation], timeout: 1) - let request = try XCTUnwrap(mockClient.lastRequest, - "last request should not be nil") + let request = try XCTUnwrap(lastRequest, "last request should not be nil") - XCTAssertEqual(request.url?.host, network.url.host) + XCTAssertEqual(request.url?.host, network.endpointURL.host) XCTAssertEqual(request.httpMethod, "POST") try self.validatePostBody(with: request, @@ -377,16 +436,23 @@ class AutomaticPersistedQueriesTests: XCTestCase { func testNotUseGETForQueriesAPQsRequest() throws { let mockClient = MockURLSessionClient() - let network = HTTPNetworkTransport(url: URL(string: endpoint)!, - client: mockClient, - enableAutoPersistedQueries: true) + let provider = LegacyInterceptorProvider(client: mockClient) + let network = RequestChainNetworkTransport(interceptorProvider: provider, + endpointURL: self.endpoint, + autoPersistQueries: true) + + let expectation = self.expectation(description: "Query sent") let query = HeroNameQuery(episode: .empire) - let _ = network.send(operation: query) { _ in } + var lastRequest: URLRequest? + let _ = network.send(operation: query) { _ in + lastRequest = mockClient.lastRequest.value + expectation.fulfill() + } + self.wait(for: [expectation], timeout: 1) - let request = try XCTUnwrap(mockClient.lastRequest, - "last request should not be nil") - - XCTAssertEqual(request.url?.host, network.url.host) + let request = try XCTUnwrap(lastRequest, "last request should not be nil") + + XCTAssertEqual(request.url?.host, network.endpointURL.host) XCTAssertEqual(request.httpMethod, "POST") try self.validatePostBody(with: request, @@ -396,17 +462,24 @@ class AutomaticPersistedQueriesTests: XCTestCase { func testUseGETForQueriesAPQsRequest() throws { let mockClient = MockURLSessionClient() - let network = HTTPNetworkTransport(url: URL(string: endpoint)!, - client: mockClient, - useGETForQueries: true, - enableAutoPersistedQueries: true) + let provider = LegacyInterceptorProvider(client: mockClient) + let network = RequestChainNetworkTransport(interceptorProvider: provider, + endpointURL: self.endpoint, + autoPersistQueries: true, + useGETForQueries: true) + + let expectation = self.expectation(description: "Query sent") let query = HeroNameQuery(episode: .empire) - let _ = network.send(operation: query) { _ in } + var lastRequest: URLRequest? + let _ = network.send(operation: query) { _ in + lastRequest = mockClient.lastRequest.value + expectation.fulfill() + } + self.wait(for: [expectation], timeout: 1) - let request = try XCTUnwrap(mockClient.lastRequest, - "last request should not be nil") + let request = try XCTUnwrap(lastRequest, "last request should not be nil") - XCTAssertEqual(request.url?.host, network.url.host) + XCTAssertEqual(request.url?.host, network.endpointURL.host) XCTAssertEqual(request.httpMethod, "GET") try self.validateUrlParams(with: request, @@ -416,16 +489,24 @@ class AutomaticPersistedQueriesTests: XCTestCase { func testNotUseGETForQueriesAPQsGETRequest() throws { let mockClient = MockURLSessionClient() - let network = HTTPNetworkTransport(url: URL(string: endpoint)!, - client: mockClient, - enableAutoPersistedQueries: true, - useGETForPersistedQueryRetry: true) + let provider = LegacyInterceptorProvider(client: mockClient) + let network = RequestChainNetworkTransport(interceptorProvider: provider, + endpointURL: self.endpoint, + autoPersistQueries: true, + useGETForPersistedQueryRetry: true) + + let expectation = self.expectation(description: "Query sent") let query = HeroNameQuery(episode: .empire) - let _ = network.send(operation: query) { _ in } + var lastRequest: URLRequest? + let _ = network.send(operation: query) { _ in + lastRequest = mockClient.lastRequest.value + expectation.fulfill() + } + self.wait(for: [expectation], timeout: 1) + + let request = try XCTUnwrap(lastRequest, "last request should not be nil") - let request = try XCTUnwrap(mockClient.lastRequest, - "last request should not be nil") - XCTAssertEqual(request.url?.host, network.url.host) + XCTAssertEqual(request.url?.host, network.endpointURL.host) XCTAssertEqual(request.httpMethod, "GET") try self.validateUrlParams(with: request, diff --git a/Tests/ApolloTests/BlindRetryingTestInterceptor.swift b/Tests/ApolloTests/BlindRetryingTestInterceptor.swift new file mode 100644 index 0000000000..5e03b9b434 --- /dev/null +++ b/Tests/ApolloTests/BlindRetryingTestInterceptor.swift @@ -0,0 +1,31 @@ +// +// BlindRetryingTestInterceptor.swift +// ApolloTests +// +// Created by Ellen Shapiro on 8/19/20. +// Copyright © 2020 Apollo GraphQL. All rights reserved. +// + +import Foundation +import Apollo + +// An interceptor which blindly retries every time it receives a request. +class BlindRetryingTestInterceptor: ApolloInterceptor { + var hitCount = 0 + private(set) var hasBeenCancelled = false + + func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) { + self.hitCount += 1 + chain.retry(request: request, + completion: completion) + } + + // Purposely not adhering to `Cancellable` here to make sure non `Cancellable` interceptors don't have this called. + func cancel() { + self.hasBeenCancelled = true + } +} diff --git a/Tests/ApolloTests/CancellationHandlingInterceptor.swift b/Tests/ApolloTests/CancellationHandlingInterceptor.swift new file mode 100644 index 0000000000..726ce36349 --- /dev/null +++ b/Tests/ApolloTests/CancellationHandlingInterceptor.swift @@ -0,0 +1,35 @@ +// +// CancellationHandlingInterceptor.swift +// ApolloTests +// +// Created by Ellen Shapiro on 9/17/20. +// Copyright © 2020 Apollo GraphQL. All rights reserved. +// + +import Foundation +import Apollo + +class CancellationHandlingInterceptor: ApolloInterceptor, Cancellable { + private(set) var hasBeenCancelled = false + + func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) { + + guard !self.hasBeenCancelled else { + return + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + chain.proceedAsync(request: request, + response: response, + completion: completion) + } + } + + func cancel() { + self.hasBeenCancelled = true + } +} diff --git a/Tests/ApolloTests/GETTransformerTests.swift b/Tests/ApolloTests/GETTransformerTests.swift index 73d4a95b93..382b66218a 100644 --- a/Tests/ApolloTests/GETTransformerTests.swift +++ b/Tests/ApolloTests/GETTransformerTests.swift @@ -8,15 +8,16 @@ import XCTest @testable import Apollo +import ApolloTestSupport import StarWarsAPI class GETTransformerTests: XCTestCase { - private let requestCreator = ApolloRequestCreator() - private lazy var url = URL(string: "http://localhost:8080/graphql")! + private let requestBodyCreator = ApolloRequestBodyCreator() + private lazy var url = TestURL.starWarsServer.url func testEncodingQueryWithSingleParameter() { let operation = HeroNameQuery(episode: .empire) - let body = requestCreator.requestBody(for: operation, sendOperationIdentifiers: false) + let body = requestBodyCreator.requestBody(for: operation, sendOperationIdentifiers: false) let transformer = GraphQLGETTransformer(body: body, url: self.url) @@ -27,7 +28,7 @@ class GETTransformerTests: XCTestCase { func testEncodingQueryWithMoreThanOneParameterIncludingNonHashableValue() throws { let operation = HeroNameTypeSpecificConditionalInclusionQuery(episode: .jedi, includeName: true) - let body = requestCreator.requestBody(for: operation, sendOperationIdentifiers: false) + let body = requestBodyCreator.requestBody(for: operation, sendOperationIdentifiers: false) let transformer = GraphQLGETTransformer(body: body, url: self.url) @@ -194,7 +195,7 @@ class GETTransformerTests: XCTestCase { func testEncodingQueryWithNullDefaultParameter() { let operation = HeroNameQuery() - let body = requestCreator.requestBody(for: operation, sendOperationIdentifiers: false) + let body = requestBodyCreator.requestBody(for: operation, sendOperationIdentifiers: false) let transformer = GraphQLGETTransformer(body: body, url: self.url) diff --git a/Tests/ApolloTests/HTTPTransportTests.swift b/Tests/ApolloTests/HTTPTransportTests.swift deleted file mode 100644 index 2fbe545ae0..0000000000 --- a/Tests/ApolloTests/HTTPTransportTests.swift +++ /dev/null @@ -1,444 +0,0 @@ -// -// HTTPTransportTests.swift -// ApolloTests -// -// Created by Ellen Shapiro on 7/1/19. -// Copyright © 2019 Apollo GraphQL. All rights reserved. -// - -import XCTest -@testable import Apollo -import ApolloTestSupport -import StarWarsAPI -import ApolloTestSupport - -class HTTPTransportTests: XCTestCase { - - private var updatedHeaders: [String: String]? - private var shouldSend = true - - private var completedRequest: URLRequest? - private var completedData: Data? - private var completedResponse: URLResponse? - private var completedError: Error? - - private var shouldModifyURLInWillSend = false - private var retryCount = 0 - - private var graphQlErrors = [GraphQLError]() - - private lazy var url = URL(string: "http://localhost:8080/graphql")! - private lazy var networkTransport: HTTPNetworkTransport = { - let transport = HTTPNetworkTransport(url: self.url, - useGETForQueries: true) - transport.delegate = self - return transport - }() - - private func validateHeroNameQueryResponse(result: Result, Error>, - expectation: XCTestExpectation, - file: StaticString = #filePath, - line: UInt = #line) { - defer { - expectation.fulfill() - } - - switch result { - case .success(let graphQLResponse): - guard - let dictionary = graphQLResponse.body as? [String: AnyHashable], - let dataDict = dictionary["data"] as? [String: AnyHashable], - let heroDict = dataDict["hero"] as? [String: AnyHashable], - let name = heroDict["name"] as? String else { - XCTFail("No hero for you!", - file: file, - line: line) - return - } - - XCTAssertEqual(name, - "R2-D2", - file: file, - line: line) - case .failure(let error): - XCTFail("Unexpected response error: \(error)", - file: file, - line: line) - } - } - - func testPreflightDelegateTellingRequestNotToSend() { - self.shouldSend = false - - let expectation = self.expectation(description: "Send operation completed") - let cancellable = self.networkTransport.send(operation: HeroNameQuery(episode: .empire)) { result in - - defer { - expectation.fulfill() - } - - switch result { - case .success: - XCTFail("Expected error not received when telling delegate not to send!") - case .failure(let error): - switch error { - case GraphQLHTTPRequestError.cancelledByDelegate: - // Correct! - break - default: - XCTFail("Expected `cancelledByDelegate`, got \(error)") - } - } - } - - guard (cancellable as? EmptyCancellable) != nil else { - XCTFail("Wrong cancellable type returned!") - cancellable.cancel() - expectation.fulfill() - return - } - - // This should fail without hitting the network. - self.wait(for: [expectation], timeout: 1) - - // The request shouldn't have fired, so all these objects should be nil - XCTAssertNil(self.completedRequest) - XCTAssertNil(self.completedData) - XCTAssertNil(self.completedResponse) - XCTAssertNil(self.completedError) - XCTAssertEqual(self.retryCount, 0) - } - - func testPreflightDelgateModifyingRequest() { - self.updatedHeaders = ["Authorization": "Bearer HelloApollo"] - - let expectation = self.expectation(description: "Send operation completed") - let cancellable = self.networkTransport.send(operation: HeroNameQuery()) { result in - self.validateHeroNameQueryResponse(result: result, expectation: expectation) - } - - guard - let task = cancellable as? URLSessionTask, - let headers = task.currentRequest?.allHTTPHeaderFields else { - cancellable.cancel() - expectation.fulfill() - return - } - - XCTAssertEqual(headers["Authorization"], "Bearer HelloApollo") - - // This will come through after hitting the network. - self.wait(for: [expectation], timeout: 10) - - // We should have everything except an error since the request should have proceeded - XCTAssertNotNil(self.completedRequest) - XCTAssertNotNil(self.completedData) - XCTAssertNotNil(self.completedResponse) - XCTAssertNil(self.completedError) - XCTAssertEqual(self.retryCount, 0) - } - - func testPreflightDelegateNeitherModifyingOrStoppingRequest() { - let expectation = self.expectation(description: "Send operation completed") - let cancellable = self.networkTransport.send(operation: HeroNameQuery()) { result in - self.validateHeroNameQueryResponse(result: result, expectation: expectation) - } - - guard - let task = cancellable as? URLSessionTask, - let headers = task.currentRequest?.allHTTPHeaderFields else { - XCTFail("Couldn't access header fields!") - cancellable.cancel() - expectation.fulfill() - return - } - - XCTAssertNil(headers["Authorization"]) - - // This will come through after hitting the network. - self.wait(for: [expectation], timeout: 10) - - // We should have everything except an error since the request should have proceeded - XCTAssertNotNil(self.completedRequest) - XCTAssertNotNil(self.completedData) - XCTAssertNotNil(self.completedResponse) - XCTAssertNil(self.completedError) - XCTAssertEqual(self.retryCount, 0) - } - - func testRetryDelegateRetriesAfterUnsuccessfulAttempts() { - self.shouldModifyURLInWillSend = true - let expectation = self.expectation(description: "Send operation completed") - - let cancellable = self.networkTransport.send(operation: HeroNameQuery()) { result in - // This should have retried twice - the first time `shouldModifyURLInWillSend` shoud remain the same and it'll fail again. - XCTAssertEqual(self.retryCount, 2) - self.validateHeroNameQueryResponse(result: result, expectation: expectation) - } - - guard - let task = cancellable as? URLSessionTask, - let url = task.currentRequest?.url else { - XCTFail("Couldn't get url!") - cancellable.cancel() - expectation.fulfill() - return - } - - XCTAssertEqual(url, self.url) - - self.wait(for: [expectation], timeout: 10) - } - - func testRetryDelegateReturnsApolloError() throws { - class MockRetryDelegate: HTTPNetworkTransportRetryDelegate { - func networkTransport(_ networkTransport: HTTPNetworkTransport, - receivedError error: Error, - for request: URLRequest, - response: URLResponse?, - continueHandler: @escaping (HTTPNetworkTransport.ContinueAction) -> Void) { - continueHandler(.fail(error)) - } - } - - let mockRetryDelegate = MockRetryDelegate() - - let transport = HTTPNetworkTransport(url: URL(string: "http://localhost:8080/graphql_non_existant")!) - transport.delegate = mockRetryDelegate - - let expectationErrorResponse = self.expectation(description: "Send operation completed") - - let _ = transport.send(operation: HeroNameQuery()) { result in - switch result { - case .success: - XCTFail() - expectationErrorResponse.fulfill() - case .failure(let error): - XCTAssertTrue(error is GraphQLHTTPResponseError) - expectationErrorResponse.fulfill() - } - } - - wait(for: [expectationErrorResponse], timeout: 1) - } - - func testRetryDelegateReturnsCustomError() throws { - enum MockError: Error, Equatable { - case customError - } - - class MockRetryDelegate: HTTPNetworkTransportRetryDelegate { - func networkTransport(_ networkTransport: HTTPNetworkTransport, - receivedError error: Error, - for request: URLRequest, - response: URLResponse?, - continueHandler: @escaping (HTTPNetworkTransport.ContinueAction) -> Void) { - continueHandler(.fail(MockError.customError)) - } - } - - let mockRetryDelegate = MockRetryDelegate() - - let transport = HTTPNetworkTransport(url: URL(string: "http://localhost:8080/graphql_non_existant")!) - transport.delegate = mockRetryDelegate - - let expectationErrorResponse = self.expectation(description: "Send operation completed") - - let _ = transport.send(operation: HeroNameQuery()) { result in - switch result { - case .success: - XCTFail() - expectationErrorResponse.fulfill() - case .failure(let error): - XCTAssertTrue(error is MockError) - expectationErrorResponse.fulfill() - } - } - - wait(for: [expectationErrorResponse], timeout: 1) - } - - func testEquality() { - let identicalTransport = HTTPNetworkTransport(url: self.url, - client: self.networkTransport.client, - useGETForQueries: true) - XCTAssertEqual(self.networkTransport, identicalTransport) - - let nonIdenticalTransport = HTTPNetworkTransport(url: self.url, - client: self.networkTransport.client) - XCTAssertNotEqual(self.networkTransport, nonIdenticalTransport) - } - - func testErrorDelegateWithErrors() throws { - self.retryCount = 0 - self.graphQlErrors = [] - let query = HeroNameQuery() - // TODO: Replace this with once it is codable https://github.com/apollographql/apollo-ios/issues/467 - let body = ["errors": [["message": "Test graphql error"]]] - - let mockClient = MockURLSessionClient() - mockClient.response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil) - mockClient.data = try JSONSerialization.data(withJSONObject: body, options: .prettyPrinted) - let network = HTTPNetworkTransport(url: url, - client: mockClient) - network.delegate = self - let expectation = self.expectation(description: "Send operation completed") - - let _ = network.send(operation: query) { result in - switch result { - case .success: - expectation.fulfill() - case .failure: - break - } - } - - let request = try XCTUnwrap(mockClient.lastRequest, - "last request should not be nil") - - XCTAssertEqual(request.url?.host, network.url.host) - XCTAssertEqual(request.httpMethod, "POST") - - XCTAssertEqual(self.graphQlErrors.count, 1) - XCTAssertEqual(retryCount, 1) - wait(for: [expectation], timeout: 1) - } - - func testErrorDelegateWithNoErrors() throws { - self.retryCount = 0 - self.graphQlErrors = [] - let query = HeroNameQuery() - // TODO: Replace this with once it is codable https://github.com/apollographql/apollo-ios/issues/467 - let body = ["errors": []] - - let mockClient = MockURLSessionClient() - mockClient.response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil) - mockClient.data = try JSONSerialization.data(withJSONObject: body, options: .prettyPrinted) - let network = HTTPNetworkTransport(url: url, - client: mockClient) - network.delegate = self - let expectation = self.expectation(description: "Send operation completed") - - let _ = network.send(operation: query) { result in - switch result { - case .success: - expectation.fulfill() - case .failure: - break - } - } - - let request = try XCTUnwrap(mockClient.lastRequest, - "last request should not be nil") - - XCTAssertEqual(request.url?.host, network.url.host) - XCTAssertEqual(request.httpMethod, "POST") - XCTAssertEqual(self.retryCount, 0) - XCTAssertEqual(self.graphQlErrors.count, 0) - wait(for: [expectation], timeout: 1) - } - - func testClientNameAndVersionHeadersAreSent() throws { - let mockClient = MockURLSessionClient() - let network = HTTPNetworkTransport(url: self.url, - client: mockClient) - let query = HeroNameQuery(episode: .empire) - let _ = network.send(operation: query) { _ in } - - let request = try XCTUnwrap(mockClient.lastRequest, - "last request should not be nil") - - let clientName = try XCTUnwrap(request.value(forHTTPHeaderField: HTTPNetworkTransport.headerFieldNameApolloClientName), - "Client name on last request was nil!") - - XCTAssertFalse(clientName.isEmpty, "Client name was empty!") - XCTAssertEqual(clientName, network.clientName) - - let clientVersion = try XCTUnwrap(request.value(forHTTPHeaderField: HTTPNetworkTransport.headerFieldNameApolloClientVersion), - "Client version on last request was nil!") - - XCTAssertFalse(clientVersion.isEmpty, "Client version was empty!") - XCTAssertEqual(clientVersion, network.clientVersion) - } -} - -// MARK: - HTTPNetworkTransportPreflightDelegate - -extension HTTPTransportTests: HTTPNetworkTransportPreflightDelegate { - func networkTransport(_ networkTransport: HTTPNetworkTransport, shouldSend request: URLRequest) -> Bool { - return self.shouldSend - } - - func networkTransport(_ networkTransport: HTTPNetworkTransport, willSend request: inout URLRequest) { - if self.shouldModifyURLInWillSend { - // This undoes any changes to the URL done by the GET request, which will cause the request to fail. - request.url = self.url - } - - guard let headers = self.updatedHeaders else { - return - } - - headers.forEach { tuple in - let (key, value) = tuple - request.addValue(value, forHTTPHeaderField: key) - } - } -} - -// MARK: - HTTPNetworkTransportTaskCompletedDelegate - -extension HTTPTransportTests: HTTPNetworkTransportTaskCompletedDelegate { - - func networkTransport(_ networkTransport: HTTPNetworkTransport, - didCompleteRawTaskForRequest request: URLRequest, - withData data: Data?, - response: URLResponse?, - error: Error?) { - self.completedRequest = request - self.completedData = data - self.completedResponse = response - self.completedError = error - } -} - -// MARK: - HTTPNetworkTransportRetryDelegate - -extension HTTPTransportTests: HTTPNetworkTransportRetryDelegate { - - func networkTransport(_ networkTransport: HTTPNetworkTransport, - receivedError error: Error, - for request: URLRequest, - response: URLResponse?, - continueHandler: @escaping (HTTPNetworkTransport.ContinueAction) -> Void) { - guard let graphQLError = error as? GraphQLHTTPResponseError else { - continueHandler(.fail(error)) - return - } - - switch graphQLError.kind { - case .errorResponse: - self.retryCount += 1 - if retryCount > 1 { - self.shouldModifyURLInWillSend = false - } - continueHandler(.retry) - case .invalidResponse: - continueHandler(.fail(error)) - case .persistedQueryNotFound, - .persistedQueryNotSupported: - continueHandler(.fail(error)) - } - } -} - -// MARK: - HTTPNetworkTransportGraphQLErrorDelegate - -extension HTTPTransportTests: HTTPNetworkTransportGraphQLErrorDelegate { - func networkTransport(_ networkTransport: HTTPNetworkTransport, receivedGraphQLErrors errors: [GraphQLError], retryHandler: @escaping (Bool) -> Void) { - self.retryCount += 1 - let shouldRetry = retryCount == 2 - self.graphQlErrors = errors - retryHandler(shouldRetry) - } -} diff --git a/Tests/ApolloTests/InterceptorTests.swift b/Tests/ApolloTests/InterceptorTests.swift new file mode 100644 index 0000000000..3f31f2a645 --- /dev/null +++ b/Tests/ApolloTests/InterceptorTests.swift @@ -0,0 +1,281 @@ +// +// InterceptorTests.swift +// Apollo +// +// Created by Ellen Shapiro on 8/19/20. +// Copyright © 2020 Apollo GraphQL. All rights reserved. +// + +import XCTest +import Apollo +import ApolloTestSupport +import StarWarsAPI + +class InterceptorTests: XCTestCase { + + // MARK: - Retry Interceptor + + func testMaxRetryInterceptorErrorsAfterMaximumRetries() { + class TestProvider: InterceptorProvider { + let testInterceptor = BlindRetryingTestInterceptor() + let retryCount = 15 + func interceptors(for operation: Operation) -> [ApolloInterceptor] { + [ + MaxRetryInterceptor(maxRetriesAllowed: self.retryCount), + self.testInterceptor, + NetworkFetchInterceptor(client: MockURLSessionClient()), + ] + } + } + + let testProvider = TestProvider() + let network = RequestChainNetworkTransport(interceptorProvider: testProvider, + endpointURL: TestURL.mockServer.url) + + let expectation = self.expectation(description: "Reqeust sent") + + let operation = HeroNameQuery() + _ = network.send(operation: operation) { result in + defer { + expectation.fulfill() + } + + switch result { + case .success: + XCTFail("This should not have worked") + case .failure(let error): + switch error { + case MaxRetryInterceptor.RetryError.hitMaxRetryCount(let count, let operationName): + XCTAssertEqual(count, testProvider.retryCount) + // There should be one more hit than retries since it will be hit on the original call + XCTAssertEqual(testProvider.testInterceptor.hitCount, testProvider.retryCount + 1) + XCTAssertEqual(operationName, operation.operationName) + default: + XCTFail("Unexpected error type: \(error)") + } + } + } + + self.wait(for: [expectation], timeout: 1) + } + + func testRetryInterceptorDoesNotErrorIfRetriedFewerThanMaxTimes() { + class TestProvider: InterceptorProvider { + let testInterceptor = RetryToCountThenSucceedInterceptor(timesToCallRetry: 2) + let retryCount = 3 + + let mockClient: MockURLSessionClient = { + let client = MockURLSessionClient() + client.response = HTTPURLResponse(url: TestURL.mockServer.url, + statusCode: 200, + httpVersion: nil, + headerFields: nil) + let json = [ + "data": [ + "hero": [ + "name": "Luke Skywalker", + "__typename": "Human" + ] + ] + ] + let data = try! JSONSerializationFormat.serialize(value: json) + client.data = data + return client + }() + + func interceptors(for operation: Operation) -> [ApolloInterceptor] { + [ + MaxRetryInterceptor(maxRetriesAllowed: self.retryCount), + self.testInterceptor, + NetworkFetchInterceptor(client: self.mockClient), + LegacyParsingInterceptor(), + ] + } + } + + let testProvider = TestProvider() + let network = RequestChainNetworkTransport(interceptorProvider: testProvider, + endpointURL: TestURL.mockServer.url) + + let expectation = self.expectation(description: "Reqeust sent") + + let operation = HeroNameQuery() + _ = network.send(operation: operation) { result in + defer { + expectation.fulfill() + } + + switch result { + case .success(let graphQLResult): + XCTAssertEqual(graphQLResult.data?.hero?.name, "Luke Skywalker") + XCTAssertEqual(testProvider.testInterceptor.timesRetryHasBeenCalled, testProvider.testInterceptor.timesToCallRetry) + case .failure(let error): + XCTFail("Unexpected error: \(error.localizedDescription)") + } + } + + self.wait(for: [expectation], timeout: 1) + } + + // MARK: - Legacy Parsing Interceptor + + func testLegacyParsingInterceptorFailsWithEmptyData() { + class TestProvider: InterceptorProvider { + let mockClient: MockURLSessionClient = { + let client = MockURLSessionClient() + client.response = HTTPURLResponse(url: TestURL.mockServer.url, + statusCode: 200, + httpVersion: nil, + headerFields: nil) + client.data = Data() + return client + }() + + func interceptors(for operation: Operation) -> [ApolloInterceptor] { + [ + NetworkFetchInterceptor(client: self.mockClient), + LegacyParsingInterceptor(), + ] + } + } + + let network = RequestChainNetworkTransport(interceptorProvider: TestProvider(), + endpointURL: TestURL.mockServer.url) + + let expectation = self.expectation(description: "Reqeust sent") + + _ = network.send(operation: HeroNameQuery()) { result in + defer { + expectation.fulfill() + } + + switch result { + case .success: + XCTFail("This should not have succeeded") + case .failure(let error): + switch error { + case LegacyParsingInterceptor.LegacyParsingError.couldNotParseToLegacyJSON(let data): + XCTAssertTrue(data.isEmpty) + default: + XCTFail("Unexpected error type: \(error.localizedDescription)") + } + } + } + + self.wait(for: [expectation], timeout: 1) + } + + // MARK: - Response Code Interceptor + + func testResponseCodeInterceptorLetsAnyDataThroughWithValidResponseCode() { + class TestProvider: InterceptorProvider { + let mockClient: MockURLSessionClient = { + let client = MockURLSessionClient() + client.response = HTTPURLResponse(url: TestURL.mockServer.url, + statusCode: 200, + httpVersion: nil, + headerFields: nil) + client.data = Data() + return client + }() + + func interceptors(for operation: Operation) -> [ApolloInterceptor] { + [ + NetworkFetchInterceptor(client: self.mockClient), + ResponseCodeInterceptor(), + LegacyParsingInterceptor() + ] + } + } + + let network = RequestChainNetworkTransport(interceptorProvider: TestProvider(), + endpointURL: TestURL.mockServer.url) + + let expectation = self.expectation(description: "Reqeust sent") + + _ = network.send(operation: HeroNameQuery()) { result in + defer { + expectation.fulfill() + } + + switch result { + case .success: + XCTFail("This should not have succeeded") + case .failure(let error): + switch error { + case LegacyParsingInterceptor.LegacyParsingError.couldNotParseToLegacyJSON(let data): + XCTAssertTrue(data.isEmpty) + default: + XCTFail("Unexpected error type: \(error.localizedDescription)") + } + } + } + + self.wait(for: [expectation], timeout: 1) + } + + func testResponseCodeInterceptorDoesNotLetDataThroughWithInvalidResponseCode() { + class TestProvider: InterceptorProvider { + let mockClient: MockURLSessionClient = { + let client = MockURLSessionClient() + client.response = HTTPURLResponse(url: TestURL.mockServer.url, + statusCode: 401, + httpVersion: nil, + headerFields: nil) + let json = [ + "data": [ + "hero": [ + "name": "Luke Skywalker", + "__typename": "Human" + ] + ] + ] + let data = try! JSONSerializationFormat.serialize(value: json) + client.data = data + return client + }() + + func interceptors(for operation: Operation) -> [ApolloInterceptor] { + [ + NetworkFetchInterceptor(client: self.mockClient), + ResponseCodeInterceptor(), + LegacyParsingInterceptor(), + ] + } + } + + let network = RequestChainNetworkTransport(interceptorProvider: TestProvider(), + endpointURL: TestURL.mockServer.url) + + let expectation = self.expectation(description: "Reqeust sent") + + _ = network.send(operation: HeroNameQuery()) { result in + defer { + expectation.fulfill() + } + + switch result { + case .success: + XCTFail("This should not have succeeded") + case .failure(let error): + switch error { + case ResponseCodeInterceptor.ResponseCodeError.invalidResponseCode(response: let response, let rawData): + XCTAssertEqual(response?.statusCode, 401) + + guard + let data = rawData, + let dataString = String(bytes: data, encoding: .utf8) else { + XCTFail("Incorrect data returned with error") + return + } + + XCTAssertEqual(dataString, "{\"data\":{\"hero\":{\"__typename\":\"Human\",\"name\":\"Luke Skywalker\"}}}") + default: + XCTFail("Unexpected error type: \(error.localizedDescription)") + } + } + } + + self.wait(for: [expectation], timeout: 1) + } +} diff --git a/Tests/ApolloTests/MultipartFormData+Testing.swift b/Tests/ApolloTests/MultipartFormData+Testing.swift new file mode 100644 index 0000000000..b49fde370b --- /dev/null +++ b/Tests/ApolloTests/MultipartFormData+Testing.swift @@ -0,0 +1,14 @@ +import Foundation +@testable import Apollo + +extension MultipartFormData { + + func toTestString() throws -> String { + let encodedData = try self.encode() + let string = String(bytes: encodedData, encoding: .utf8)! + + // Replacing CRLF with new line as string literals uses new lines + return string.replacingOccurrences(of: MultipartFormData.CRLF, with: "\n") + } +} + diff --git a/Tests/ApolloTests/MultipartFormDataTests.swift b/Tests/ApolloTests/MultipartFormDataTests.swift new file mode 100644 index 0000000000..b8dbc8c8f0 --- /dev/null +++ b/Tests/ApolloTests/MultipartFormDataTests.swift @@ -0,0 +1,133 @@ +// +// MultipartFormDataTests.swift +// Apollo +// +// Created by Ellen Shapiro on 9/21/20. +// Copyright © 2020 Apollo GraphQL. All rights reserved. +// + +import XCTest +import Apollo + +class MultipartFormDataTests: XCTestCase { + func testSingleFile() throws { + let alphaFileUrl = TestFileHelper.fileURLForFile(named: "a", extension: "txt") + let alphaData = try Data(contentsOf: alphaFileUrl) + + let formData = MultipartFormData(boundary: "------------------------cec8e8123c05ba25") + try formData.appendPart(string: "{ \"query\": \"mutation ($file: Upload!) { singleUpload(file: $file) { id } }\", \"variables\": { \"file\": null } }", name: "operations") + try formData.appendPart(string: "{ \"0\": [\"variables.file\"] }", name: "map") + formData.appendPart(data: alphaData, name: "0", contentType: "text/plain", filename: "a.txt") + + let expectedString = """ +--------------------------cec8e8123c05ba25 +Content-Disposition: form-data; name="operations" + +{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) { id } }", "variables": { "file": null } } +--------------------------cec8e8123c05ba25 +Content-Disposition: form-data; name="map" + +{ "0": ["variables.file"] } +--------------------------cec8e8123c05ba25 +Content-Disposition: form-data; name="0"; filename="a.txt" +Content-Type: text/plain + +Alpha file content. + +--------------------------cec8e8123c05ba25-- +""" + + let stringToCompare = try formData.toTestString() + XCTAssertEqual(stringToCompare, expectedString) + } + + func testMultifileFile() throws { + let bravoFileUrl = TestFileHelper.fileURLForFile(named: "b", extension: "txt") + let charlieFileUrl = TestFileHelper.fileURLForFile(named: "c", extension: "txt") + + let bravoData = try Data(contentsOf: bravoFileUrl) + let charlieData = try Data(contentsOf: charlieFileUrl) + + let formData = MultipartFormData(boundary: "------------------------ec62457de6331cad") + try formData.appendPart(string: "{ \"query\": \"mutation($files: [Upload!]!) { multipleUpload(files: $files) { id } }\", \"variables\": { \"files\": [null, null] } }", name: "operations") + try formData.appendPart(string: "{ \"0\": [\"variables.files.0\"], \"1\": [\"variables.files.1\"] }", name: "map") + formData.appendPart(data: bravoData, name: "0", contentType: "text/plain", filename: "b.txt") + formData.appendPart(data: charlieData, name: "1", contentType: "text/plain", filename: "c.txt") + + let expectedString = """ +--------------------------ec62457de6331cad +Content-Disposition: form-data; name="operations" + +{ "query": "mutation($files: [Upload!]!) { multipleUpload(files: $files) { id } }", "variables": { "files": [null, null] } } +--------------------------ec62457de6331cad +Content-Disposition: form-data; name="map" + +{ "0": ["variables.files.0"], "1": ["variables.files.1"] } +--------------------------ec62457de6331cad +Content-Disposition: form-data; name="0"; filename="b.txt" +Content-Type: text/plain + +Bravo file content. + +--------------------------ec62457de6331cad +Content-Disposition: form-data; name="1"; filename="c.txt" +Content-Type: text/plain + +Charlie file content. + +--------------------------ec62457de6331cad-- +""" + let stringToCompare = try formData.toTestString() + XCTAssertEqual(stringToCompare, expectedString) + } + + func testBatchFile() throws { + let alphaFileUrl = TestFileHelper.fileURLForFile(named: "a", extension: "txt") + let bravoFileUrl = TestFileHelper.fileURLForFile(named: "b", extension: "txt") + let charlieFileUrl = TestFileHelper.fileURLForFile(named: "c", extension: "txt") + + let alphaData = try Data(contentsOf: alphaFileUrl) + let bravoData = try Data(contentsOf: bravoFileUrl) + let charlieData = try Data(contentsOf: charlieFileUrl) + + let formData = MultipartFormData(boundary: "------------------------627436eaefdbc285") + try formData.appendPart(string: "[{ \"query\": \"mutation ($file: Upload!) { singleUpload(file: $file) { id } }\", \"variables\": { \"file\": null } }, { \"query\": \"mutation($files: [Upload!]!) { multipleUpload(files: $files) { id } }\", \"variables\": { \"files\": [null, null] } }]", name: "operations") + try formData.appendPart(string: "{ \"0\": [\"0.variables.file\"], \"1\": [\"1.variables.files.0\"], \"2\": [\"1.variables.files.1\"] }", name: "map") + formData.appendPart(data: alphaData, name: "0", contentType: "text/plain", filename: "a.txt") + formData.appendPart(data: bravoData, name: "1", contentType: "text/plain", filename: "b.txt") + formData.appendPart(data: charlieData, name: "2", contentType: "text/plain", filename: "c.txt") + + let expectedString = """ +--------------------------627436eaefdbc285 +Content-Disposition: form-data; name="operations" + +[{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) { id } }", "variables": { "file": null } }, { "query": "mutation($files: [Upload!]!) { multipleUpload(files: $files) { id } }", "variables": { "files": [null, null] } }] +--------------------------627436eaefdbc285 +Content-Disposition: form-data; name="map" + +{ "0": ["0.variables.file"], "1": ["1.variables.files.0"], "2": ["1.variables.files.1"] } +--------------------------627436eaefdbc285 +Content-Disposition: form-data; name="0"; filename="a.txt" +Content-Type: text/plain + +Alpha file content. + +--------------------------627436eaefdbc285 +Content-Disposition: form-data; name="1"; filename="b.txt" +Content-Type: text/plain + +Bravo file content. + +--------------------------627436eaefdbc285 +Content-Disposition: form-data; name="2"; filename="c.txt" +Content-Type: text/plain + +Charlie file content. + +--------------------------627436eaefdbc285-- +""" + + let stringToCompare = try formData.toTestString() + XCTAssertEqual(stringToCompare, expectedString) + } +} diff --git a/Tests/ApolloTests/RequestBodyCreatorTests.swift b/Tests/ApolloTests/RequestBodyCreatorTests.swift new file mode 100644 index 0000000000..e19a084690 --- /dev/null +++ b/Tests/ApolloTests/RequestBodyCreatorTests.swift @@ -0,0 +1,33 @@ +// +// RequestBodyCreatorTests.swift +// ApolloTests +// +// Created by Kim de Vos on 16/07/2019. +// Copyright © 2019 Apollo GraphQL. All rights reserved. +// + +import XCTest +@testable import Apollo +import StarWarsAPI +import UploadAPI + +class RequestBodyCreatorTests: XCTestCase { + private let customRequestBodyCreator = TestCustomRequestBodyCreator() + private let apolloRequestBodyCreator = ApolloRequestBodyCreator() + + // MARK: - Tests + + func testRequestBodyWithApolloRequestBodyCreator() { + let query = HeroNameQuery() + let req = apolloRequestBodyCreator.requestBody(for: query, sendOperationIdentifiers: false) + + XCTAssertEqual(query.queryDocument, req["query"] as? String) + } + + func testRequestBodyWithCustomRequestBodyCreator() { + let query = HeroNameQuery() + let req = customRequestBodyCreator.requestBody(for: query, sendOperationIdentifiers: false) + + XCTAssertEqual(query.queryDocument, req["test_query"] as? String) + } +} diff --git a/Tests/ApolloTests/RequestChainTests.swift b/Tests/ApolloTests/RequestChainTests.swift new file mode 100644 index 0000000000..ed937ef38c --- /dev/null +++ b/Tests/ApolloTests/RequestChainTests.swift @@ -0,0 +1,276 @@ +// +// RequestChainTests.swift +// Apollo +// +// Created by Ellen Shapiro on 7/14/20. +// Copyright © 2020 Apollo GraphQL. All rights reserved. +// + +import XCTest +import Apollo +import ApolloTestSupport +import StarWarsAPI + +class RequestChainTests: XCTestCase { + + lazy var legacyClient: ApolloClient = { + let url = TestURL.starWarsServer.url + let provider = LegacyInterceptorProvider() + let transport = RequestChainNetworkTransport(interceptorProvider: provider, + endpointURL: url) + + return ApolloClient(networkTransport: transport) + }() + + func testLoading() { + let expectation = self.expectation(description: "loaded With legacy client") + legacyClient.fetch(query: HeroNameQuery()) { result in + switch result { + case .success(let graphQLResult): + XCTAssertEqual(graphQLResult.source, .server) + XCTAssertEqual(graphQLResult.data?.hero?.name, "R2-D2") + case .failure(let error): + XCTFail("Unexpected error: \(error)") + + } + expectation.fulfill() + } + + self.wait(for: [expectation], timeout: 10) + } + + func testInitialLoadFromNetworkAndSecondaryLoadFromCache() { + let initialLoadExpectation = self.expectation(description: "loaded With legacy client") + legacyClient.fetch(query: HeroNameQuery()) { result in + switch result { + case .success(let graphQLResult): + XCTAssertEqual(graphQLResult.source, .server) + XCTAssertEqual(graphQLResult.data?.hero?.name, "R2-D2") + case .failure(let error): + XCTFail("Unexpected error: \(error)") + + } + initialLoadExpectation.fulfill() + } + + self.wait(for: [initialLoadExpectation], timeout: 10) + + let secondLoadExpectation = self.expectation(description: "loaded With legacy client") + legacyClient.fetch(query: HeroNameQuery()) { result in + switch result { + case .success(let graphQLResult): + XCTAssertEqual(graphQLResult.source, .cache) + XCTAssertEqual(graphQLResult.data?.hero?.name, "R2-D2") + case .failure(let error): + XCTFail("Unexpected error: \(error)") + + } + secondLoadExpectation.fulfill() + } + + self.wait(for: [secondLoadExpectation], timeout: 10) + } + + func testEmptyInterceptorArrayReturnsCorrectError() { + class TestProvider: InterceptorProvider { + func interceptors(for operation: Operation) -> [ApolloInterceptor] { + [] + } + } + + let transport = RequestChainNetworkTransport(interceptorProvider: TestProvider(), + endpointURL: TestURL.mockServer.url) + let expectation = self.expectation(description: "kickoff failed") + _ = transport.send(operation: HeroNameQuery()) { result in + defer { + expectation.fulfill() + } + + switch result { + case .success: + XCTFail("This should not have succeeded") + case .failure(let error): + switch error { + case RequestChain.ChainError.noInterceptors: + // This is what we want. + break + default: + XCTFail("Incorrect error for no interceptors: \(error)") + } + } + } + + + self.wait(for: [expectation], timeout: 1) + } + + func testCancellingChainCallsCancelOnInterceptorsWhichImplementCancellableAndNotOnOnesThatDont() { + class TestProvider: InterceptorProvider { + let cancellationInterceptor = CancellationHandlingInterceptor() + let retryInterceptor = BlindRetryingTestInterceptor() + + func interceptors(for operation: Operation) -> [ApolloInterceptor] { + [ + self.cancellationInterceptor, + self.retryInterceptor + ] + } + } + + let provider = TestProvider() + let transport = RequestChainNetworkTransport(interceptorProvider: provider, + endpointURL: TestURL.mockServer.url) + let expectation = self.expectation(description: "Send succeeded") + expectation.isInverted = true + let cancellable = transport.send(operation: HeroNameQuery()) { _ in + XCTFail("This should not have gone through") + expectation.fulfill() + } + + cancellable.cancel() + XCTAssertTrue(provider.cancellationInterceptor.hasBeenCancelled) + XCTAssertFalse(provider.retryInterceptor.hasBeenCancelled) + self.wait(for: [expectation], timeout: 2) + } + + func testErrorInterceptorGetsCalledAfterAnErrorIsReceived() { + class ErrorInterceptor: ApolloErrorInterceptor { + var error: Error? = nil + + func handleErrorAsync( + error: Error, + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) { + + self.error = error + completion(.failure(error)) + } + } + + class TestProvider: InterceptorProvider { + let errorInterceptor = ErrorInterceptor() + func interceptors(for operation: Operation) -> [ApolloInterceptor] { + return [ + // An interceptor which will error without a response + AutomaticPersistedQueryInterceptor() + ] + } + + func additionalErrorInterceptor(for operation: Operation) -> ApolloErrorInterceptor? { + return self.errorInterceptor + } + } + + let provider = TestProvider() + let transport = RequestChainNetworkTransport(interceptorProvider: provider, + endpointURL: TestURL.mockServer.url, + autoPersistQueries: true) + + let expectation = self.expectation(description: "Hero name query complete") + _ = transport.send(operation: HeroNameQuery()) { result in + defer { + expectation.fulfill() + } + switch result { + case .success: + XCTFail("This should not have succeeded") + case .failure(let error): + switch error { + case AutomaticPersistedQueryInterceptor.APQError.noParsedResponse: + // This is what we want. + break + default: + XCTFail("Unexpected error: \(error)") + } + } + } + + self.wait(for: [expectation], timeout: 1) + + switch provider.errorInterceptor.error { + case .some(let error): + switch error { + case AutomaticPersistedQueryInterceptor.APQError.noParsedResponse: + // Again, this is what we expect. + break + default: + XCTFail("Unexpected error on the interceptor: \(error)") + } + case .none: + XCTFail("Error interceptor did not receive an error!") + } + } + + func testErrorInterceptorGetsCalledInLegacyInterceptorProviderSubclass() { + class ErrorInterceptor: ApolloErrorInterceptor { + var error: Error? = nil + + func handleErrorAsync( + error: Error, + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) { + + self.error = error + completion(.failure(error)) + } + } + + class TestProvider: LegacyInterceptorProvider { + let errorInterceptor = ErrorInterceptor() + + override func interceptors(for operation: Operation) -> [ApolloInterceptor] { + return [ + // An interceptor which will error without a response + AutomaticPersistedQueryInterceptor() + ] + } + + override func additionalErrorInterceptor(for operation: Operation) -> ApolloErrorInterceptor? { + return self.errorInterceptor + } + } + + let provider = TestProvider() + let transport = RequestChainNetworkTransport(interceptorProvider: provider, + endpointURL: TestURL.mockServer.url, + autoPersistQueries: true) + + let expectation = self.expectation(description: "Hero name query complete") + _ = transport.send(operation: HeroNameQuery()) { result in + defer { + expectation.fulfill() + } + switch result { + case .success: + XCTFail("This should not have succeeded") + case .failure(let error): + switch error { + case AutomaticPersistedQueryInterceptor.APQError.noParsedResponse: + // This is what we want. + break + default: + XCTFail("Unexpected error: \(error)") + } + } + } + + self.wait(for: [expectation], timeout: 1) + + switch provider.errorInterceptor.error { + case .some(let error): + switch error { + case AutomaticPersistedQueryInterceptor.APQError.noParsedResponse: + // Again, this is what we expect. + break + default: + XCTFail("Unexpected error on the interceptor: \(error)") + } + case .none: + XCTFail("Error interceptor did not receive an error!") + } + } +} diff --git a/Tests/ApolloTests/RequestCreatorTests.swift b/Tests/ApolloTests/RequestCreatorTests.swift deleted file mode 100644 index 18ea9c2ce5..0000000000 --- a/Tests/ApolloTests/RequestCreatorTests.swift +++ /dev/null @@ -1,441 +0,0 @@ -// -// MultipartFormDataTests.swift -// ApolloTests -// -// Created by Kim de Vos on 16/07/2019. -// Copyright © 2019 Apollo GraphQL. All rights reserved. -// - -import XCTest -@testable import Apollo -import StarWarsAPI -import UploadAPI - -class RequestCreatorTests: XCTestCase { - private let customRequestCreator = TestCustomRequestCreator() - private let apolloRequestCreator = ApolloRequestCreator() - - private func checkString(_ string: String, - includes expectedString: String, - file: StaticString = #filePath, - line: UInt = #line) { - XCTAssertTrue(string.contains(expectedString), - "Expected string:\n\n\(expectedString)\n\ndid not appear in string\n\n\(string)", - file: file, - line: line) - } - - private func string(from formData: MultipartFormData) throws -> String { - let encodedData = try formData.encode() - let string = String(bytes: encodedData, encoding: .utf8)! - - // Replacing CRLF with new line as string literals uses new lines - return string.replacingOccurrences(of: MultipartFormData.CRLF, with: "\n") - } - - private func fileURLForFile(named name: String, extension fileExtension: String) -> URL { - return TestFileHelper.testParentFolder() - .appendingPathComponent(name) - .appendingPathExtension(fileExtension) - } - - // MARK: - Tests - - func testSingleFile() throws { - let alphaFileUrl = self.fileURLForFile(named: "a", extension: "txt") - let alphaData = try Data(contentsOf: alphaFileUrl) - - let formData = MultipartFormData(boundary: "------------------------cec8e8123c05ba25") - try formData.appendPart(string: "{ \"query\": \"mutation ($file: Upload!) { singleUpload(file: $file) { id } }\", \"variables\": { \"file\": null } }", name: "operations") - try formData.appendPart(string: "{ \"0\": [\"variables.file\"] }", name: "map") - formData.appendPart(data: alphaData, name: "0", contentType: "text/plain", filename: "a.txt") - - let expectedString = """ ---------------------------cec8e8123c05ba25 -Content-Disposition: form-data; name="operations" - -{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) { id } }", "variables": { "file": null } } ---------------------------cec8e8123c05ba25 -Content-Disposition: form-data; name="map" - -{ "0": ["variables.file"] } ---------------------------cec8e8123c05ba25 -Content-Disposition: form-data; name="0"; filename="a.txt" -Content-Type: text/plain - -Alpha file content. - ---------------------------cec8e8123c05ba25-- -""" - - let stringToCompare = try self.string(from: formData) - XCTAssertEqual(stringToCompare, expectedString) - } - - func testMultifileFile() throws { - let bravoFileUrl = self.fileURLForFile(named: "b", extension: "txt") - let charlieFileUrl = self.fileURLForFile(named: "c", extension: "txt") - - let bravoData = try Data(contentsOf: bravoFileUrl) - let charlieData = try Data(contentsOf: charlieFileUrl) - - let formData = MultipartFormData(boundary: "------------------------ec62457de6331cad") - try formData.appendPart(string: "{ \"query\": \"mutation($files: [Upload!]!) { multipleUpload(files: $files) { id } }\", \"variables\": { \"files\": [null, null] } }", name: "operations") - try formData.appendPart(string: "{ \"0\": [\"variables.files.0\"], \"1\": [\"variables.files.1\"] }", name: "map") - formData.appendPart(data: bravoData, name: "0", contentType: "text/plain", filename: "b.txt") - formData.appendPart(data: charlieData, name: "1", contentType: "text/plain", filename: "c.txt") - - let expectedString = """ ---------------------------ec62457de6331cad -Content-Disposition: form-data; name="operations" - -{ "query": "mutation($files: [Upload!]!) { multipleUpload(files: $files) { id } }", "variables": { "files": [null, null] } } ---------------------------ec62457de6331cad -Content-Disposition: form-data; name="map" - -{ "0": ["variables.files.0"], "1": ["variables.files.1"] } ---------------------------ec62457de6331cad -Content-Disposition: form-data; name="0"; filename="b.txt" -Content-Type: text/plain - -Bravo file content. - ---------------------------ec62457de6331cad -Content-Disposition: form-data; name="1"; filename="c.txt" -Content-Type: text/plain - -Charlie file content. - ---------------------------ec62457de6331cad-- -""" - let stringToCompare = try self.string(from: formData) - XCTAssertEqual(stringToCompare, expectedString) - } - - func testBatchFile() throws { - let alphaFileUrl = self.fileURLForFile(named: "a", extension: "txt") - let bravoFileUrl = self.fileURLForFile(named: "b", extension: "txt") - let charlieFileUrl = self.fileURLForFile(named: "c", extension: "txt") - - let alphaData = try Data(contentsOf: alphaFileUrl) - let bravoData = try Data(contentsOf: bravoFileUrl) - let charlieData = try Data(contentsOf: charlieFileUrl) - - let formData = MultipartFormData(boundary: "------------------------627436eaefdbc285") - try formData.appendPart(string: "[{ \"query\": \"mutation ($file: Upload!) { singleUpload(file: $file) { id } }\", \"variables\": { \"file\": null } }, { \"query\": \"mutation($files: [Upload!]!) { multipleUpload(files: $files) { id } }\", \"variables\": { \"files\": [null, null] } }]", name: "operations") - try formData.appendPart(string: "{ \"0\": [\"0.variables.file\"], \"1\": [\"1.variables.files.0\"], \"2\": [\"1.variables.files.1\"] }", name: "map") - formData.appendPart(data: alphaData, name: "0", contentType: "text/plain", filename: "a.txt") - formData.appendPart(data: bravoData, name: "1", contentType: "text/plain", filename: "b.txt") - formData.appendPart(data: charlieData, name: "2", contentType: "text/plain", filename: "c.txt") - - let expectedString = """ ---------------------------627436eaefdbc285 -Content-Disposition: form-data; name="operations" - -[{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) { id } }", "variables": { "file": null } }, { "query": "mutation($files: [Upload!]!) { multipleUpload(files: $files) { id } }", "variables": { "files": [null, null] } }] ---------------------------627436eaefdbc285 -Content-Disposition: form-data; name="map" - -{ "0": ["0.variables.file"], "1": ["1.variables.files.0"], "2": ["1.variables.files.1"] } ---------------------------627436eaefdbc285 -Content-Disposition: form-data; name="0"; filename="a.txt" -Content-Type: text/plain - -Alpha file content. - ---------------------------627436eaefdbc285 -Content-Disposition: form-data; name="1"; filename="b.txt" -Content-Type: text/plain - -Bravo file content. - ---------------------------627436eaefdbc285 -Content-Disposition: form-data; name="2"; filename="c.txt" -Content-Type: text/plain - -Charlie file content. - ---------------------------627436eaefdbc285-- -""" - - let stringToCompare = try self.string(from: formData) - XCTAssertEqual(stringToCompare, expectedString) - } - - func testSingleFileWithApolloRequestCreator() throws { - let alphaFileUrl = self.fileURLForFile(named: "a", extension: "txt") - - let alphaFile = try GraphQLFile(fieldName: "file", - originalName: "a.txt", - mimeType: "text/plain", - fileURL: alphaFileUrl) - - let data = try apolloRequestCreator.requestMultipartFormData( - for: UploadOneFileMutation(file: alphaFile.originalName), - files: [alphaFile], - sendOperationIdentifiers: false, - serializationFormat: JSONSerializationFormat.self, - manualBoundary: "TEST.BOUNDARY" - ) - - let stringToCompare = try self.string(from: data) - - if JSONSerialization.dataCanBeSorted() { - let expectedString = """ ---TEST.BOUNDARY -Content-Disposition: form-data; name="operations" - -{"operationName":"UploadOneFile","query":"mutation UploadOneFile($file: Upload!) {\\n singleUpload(file: $file) {\\n __typename\\n id\\n path\\n filename\\n mimetype\\n }\\n}","variables":{"file":null}} ---TEST.BOUNDARY -Content-Disposition: form-data; name="map" - -{"0":["variables.file"]} ---TEST.BOUNDARY -Content-Disposition: form-data; name="0"; filename="a.txt" -Content-Type: text/plain - -Alpha file content. - ---TEST.BOUNDARY-- -""" - XCTAssertEqual(stringToCompare, expectedString) - } else { - // Operation parameters may be in weird order, so let's at least check that the files and single parameter got encoded properly. - let expectedEndString = """ ---TEST.BOUNDARY -Content-Disposition: form-data; name="map" - -{"0":["variables.file"]} ---TEST.BOUNDARY -Content-Disposition: form-data; name="0"; filename="a.txt" -Content-Type: text/plain - -Alpha file content. - ---TEST.BOUNDARY-- -""" - self.checkString(stringToCompare, includes: expectedEndString) - } - } - - func testMultipleFilesWithApolloRequestCreator() throws { - let alphaFileURL = self.fileURLForFile(named: "a", extension: "txt") - let alphaFile = try GraphQLFile(fieldName: "files", - originalName: "a.txt", - mimeType: "text/plain", - fileURL: alphaFileURL) - - let betaFileURL = self.fileURLForFile(named: "b", extension: "txt") - let betaFile = try GraphQLFile(fieldName: "files", - originalName: "b.txt", - mimeType: "text/plain", - fileURL: betaFileURL) - - let files = [alphaFile, betaFile] - let data = try apolloRequestCreator.requestMultipartFormData( - for: UploadMultipleFilesToTheSameParameterMutation(files: files.map { $0.originalName }), - files: files, - sendOperationIdentifiers: false, - serializationFormat: JSONSerializationFormat.self, - manualBoundary: "TEST.BOUNDARY" - ) - - let stringToCompare = try self.string(from: data) - - if JSONSerialization.dataCanBeSorted() { - let expectedString = """ ---TEST.BOUNDARY -Content-Disposition: form-data; name="operations" - -{"operationName":"UploadMultipleFilesToTheSameParameter","query":"mutation UploadMultipleFilesToTheSameParameter($files: [Upload!]!) {\\n multipleUpload(files: $files) {\\n __typename\\n id\\n path\\n filename\\n mimetype\\n }\\n}","variables":{"files":[null,null]}} ---TEST.BOUNDARY -Content-Disposition: form-data; name="map" - -{"0":["variables.files.0"],"1":["variables.files.1"]} ---TEST.BOUNDARY -Content-Disposition: form-data; name="0"; filename="a.txt" -Content-Type: text/plain - -Alpha file content. - ---TEST.BOUNDARY -Content-Disposition: form-data; name="1"; filename="b.txt" -Content-Type: text/plain - -Bravo file content. - ---TEST.BOUNDARY-- -""" - XCTAssertEqual(stringToCompare, expectedString) - } else { - // Query and operation parameters may be in weird order, so let's at least check that the files got encoded properly. - let endString = """ ---TEST.BOUNDARY -Content-Disposition: form-data; name="0"; filename="a.txt" -Content-Type: text/plain - -Alpha file content. - ---TEST.BOUNDARY -Content-Disposition: form-data; name="1"; filename="b.txt" -Content-Type: text/plain - -Bravo file content. - ---TEST.BOUNDARY-- -""" - self.checkString(stringToCompare, includes: endString) - } - } - - func testMultipleFilesWithMultipleFieldsWithApolloRequestCreator() throws { - let alphaFileURL = self.fileURLForFile(named: "a", extension: "txt") - let alphaFile = try GraphQLFile(fieldName: "uploads", - originalName: "a.txt", - mimeType: "text/plain", - fileURL: alphaFileURL) - - let betaFileURL = self.fileURLForFile(named: "b", extension: "txt") - let betaFile = try GraphQLFile(fieldName: "uploads", - originalName: "b.txt", - mimeType: "text/plain", - fileURL: betaFileURL) - - let charlieFileUrl = self.fileURLForFile(named: "c", extension: "txt") - let charlieFile = try GraphQLFile(fieldName: "secondField", - originalName: "c.txt", - mimeType: "text/plain", - fileURL: charlieFileUrl) - - let data = try apolloRequestCreator.requestMultipartFormData( - for: HeroNameQuery(), - files: [alphaFile, betaFile, charlieFile], - sendOperationIdentifiers: false, - serializationFormat: JSONSerializationFormat.self, - manualBoundary: "TEST.BOUNDARY" - ) - - let stringToCompare = try self.string(from: data) - - if JSONSerialization.dataCanBeSorted() { - let expectedString = """ - --TEST.BOUNDARY - Content-Disposition: form-data; name="operations" - - {"operationName":"HeroName","query":"query HeroName($episode: Episode) {\\n hero(episode: $episode) {\\n __typename\\n name\\n }\\n}","variables":{"episode":null,\"secondField\":null,\"uploads\":null}} - --TEST.BOUNDARY - Content-Disposition: form-data; name="map" - - {"0":["variables.secondField"],"1":["variables.uploads.0"],"2":["variables.uploads.1"]} - --TEST.BOUNDARY - Content-Disposition: form-data; name="0"; filename="c.txt" - Content-Type: text/plain - - Charlie file content. - - --TEST.BOUNDARY - Content-Disposition: form-data; name="1"; filename="a.txt" - Content-Type: text/plain - - Alpha file content. - - --TEST.BOUNDARY - Content-Disposition: form-data; name="2"; filename="b.txt" - Content-Type: text/plain - - Bravo file content. - - --TEST.BOUNDARY-- - """ - XCTAssertEqual(stringToCompare, expectedString) - } else { - // Query and operation parameters may be in weird order, so let's at least check that the files got encoded properly. - let endString = """ - --TEST.BOUNDARY - Content-Disposition: form-data; name="0"; filename="c.txt" - Content-Type: text/plain - - Charlie file content. - - --TEST.BOUNDARY - Content-Disposition: form-data; name="1"; filename="a.txt" - Content-Type: text/plain - - Alpha file content. - - --TEST.BOUNDARY - Content-Disposition: form-data; name="2"; filename="b.txt" - Content-Type: text/plain - - Bravo file content. - - --TEST.BOUNDARY-- - """ - self.checkString(stringToCompare, includes: endString) - } - } - - - func testRequestBodyWithApolloRequestCreator() { - let query = HeroNameQuery() - let req = apolloRequestCreator.requestBody(for: query, sendOperationIdentifiers: false) - - XCTAssertEqual(query.queryDocument, req["query"] as? String) - } - - // MARK: - Custom request creator tests - - func testSingleFileWithCustomRequestCreator() throws { - let alphaFileUrl = self.fileURLForFile(named: "a", extension: "txt") - - let alphaFile = try GraphQLFile(fieldName: "upload", - originalName: "a.txt", - mimeType: "text/plain", - fileURL: alphaFileUrl) - - let data = try customRequestCreator.requestMultipartFormData( - for: UploadOneFileMutation(file: alphaFile.originalName), - files: [alphaFile], - sendOperationIdentifiers: false, - serializationFormat: JSONSerializationFormat.self, - manualBoundary: "TEST.BOUNDARY" - ) - - let stringToCompare = try self.string(from: data) - - // Operation parameters may be in weird order, so let's at least check that the files and single parameter got encoded properly. - let expectedEndString = """ ---TEST.BOUNDARY -Content-Disposition: form-data; name="upload"; filename="a.txt" -Content-Type: text/plain - -Alpha file content. - ---TEST.BOUNDARY-- -""" - - let expectedQueryString = """ ---TEST.BOUNDARY -Content-Disposition: form-data; name="test_query" - -mutation UploadOneFile($file: Upload!) { - singleUpload(file: $file) { - __typename - id - path - filename - mimetype - } -} -""" - self.checkString(stringToCompare, includes: expectedEndString) - self.checkString(stringToCompare, includes: expectedQueryString) - } - - func testRequestBodyWithCustomRequestCreator() { - let query = HeroNameQuery() - let req = customRequestCreator.requestBody(for: query, sendOperationIdentifiers: false) - - XCTAssertEqual(query.queryDocument, req["test_query"] as? String) - } -} diff --git a/Tests/ApolloTests/RetryToCountThenSucceedInterceptor.swift b/Tests/ApolloTests/RetryToCountThenSucceedInterceptor.swift new file mode 100644 index 0000000000..8f31ebcae6 --- /dev/null +++ b/Tests/ApolloTests/RetryToCountThenSucceedInterceptor.swift @@ -0,0 +1,35 @@ +// +// RetryToCountThenSucceedInterceptor.swift +// ApolloTests +// +// Created by Ellen Shapiro on 8/19/20. +// Copyright © 2020 Apollo GraphQL. All rights reserved. +// + +import Foundation +import Apollo + +class RetryToCountThenSucceedInterceptor: ApolloInterceptor { + let timesToCallRetry: Int + var timesRetryHasBeenCalled = 0 + + init(timesToCallRetry: Int) { + self.timesToCallRetry = timesToCallRetry + } + + func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) { + if self.timesRetryHasBeenCalled < self.timesToCallRetry { + self.timesRetryHasBeenCalled += 1 + chain.retry(request: request, + completion: completion) + } else { + chain.proceedAsync(request: request, + response: response, + completion: completion) + } + } +} diff --git a/Tests/ApolloTests/String+IncludesForTesting.swift b/Tests/ApolloTests/String+IncludesForTesting.swift new file mode 100644 index 0000000000..64c2bbff34 --- /dev/null +++ b/Tests/ApolloTests/String+IncludesForTesting.swift @@ -0,0 +1,23 @@ +// +// String+IncludesForTesting.swift +// ApolloTests +// +// Created by Ellen Shapiro on 9/21/20. +// Copyright © 2020 Apollo GraphQL. All rights reserved. +// + +import ApolloCore +import Foundation +import XCTest + +extension ApolloExtension where Base == String { + + func checkIncludes(expectedString: String, + file: StaticString = #filePath, + line: UInt = #line) { + XCTAssertTrue(base.contains(expectedString), + "Expected string:\n\n\(expectedString)\n\ndid not appear in string\n\n\(base)", + file: file, + line: line) + } +} diff --git a/Tests/ApolloTests/TestCustomRequestBodyCreator.swift b/Tests/ApolloTests/TestCustomRequestBodyCreator.swift new file mode 100644 index 0000000000..45233b809f --- /dev/null +++ b/Tests/ApolloTests/TestCustomRequestBodyCreator.swift @@ -0,0 +1,30 @@ +// +// TestCustomRequestBodyCreator.swift +// Apollo +// +// Created by Kim de Vos on 02/10/2019. +// Copyright © 2019 Apollo GraphQL. All rights reserved. +// + +import Apollo + +struct TestCustomRequestBodyCreator: RequestBodyCreator { + public func requestBody(for operation: Operation, sendOperationIdentifiers: Bool) -> GraphQLMap { + var body: GraphQLMap = [ + "test_variables": operation.variables, + "test_operationName": operation.operationName, + ] + + if sendOperationIdentifiers { + guard let operationIdentifier = operation.operationIdentifier else { + preconditionFailure("To send operation identifiers, Apollo types must be generated with operationIdentifiers") + } + + body["test_id"] = operationIdentifier + } else { + body["test_query"] = operation.queryDocument + } + + return body + } +} diff --git a/Tests/ApolloTests/TestCustomRequestCreator.swift b/Tests/ApolloTests/TestCustomRequestCreator.swift deleted file mode 100644 index 97e2b85d33..0000000000 --- a/Tests/ApolloTests/TestCustomRequestCreator.swift +++ /dev/null @@ -1,62 +0,0 @@ -// -// TestCustomRequestCreator.swift -// Apollo -// -// Created by Kim de Vos on 02/10/2019. -// Copyright © 2019 Apollo GraphQL. All rights reserved. -// - -import Apollo - -struct TestCustomRequestCreator: RequestCreator { - public func requestBody(for operation: Operation, sendOperationIdentifiers: Bool) -> GraphQLMap { - var body: GraphQLMap = [ - "test_variables": operation.variables, - "test_operationName": operation.operationName, - ] - - if sendOperationIdentifiers { - guard let operationIdentifier = operation.operationIdentifier else { - preconditionFailure("To send operation identifiers, Apollo types must be generated with operationIdentifiers") - } - - body["test_id"] = operationIdentifier - } else { - body["test_query"] = operation.queryDocument - } - - return body - } - - public func requestMultipartFormData(for operation: Operation, - files: [GraphQLFile], - sendOperationIdentifiers: Bool, - serializationFormat: JSONSerializationFormat.Type, - manualBoundary: String?) throws -> MultipartFormData { - let formData: MultipartFormData - - if let boundary = manualBoundary { - formData = MultipartFormData(boundary: boundary) - } else { - formData = MultipartFormData() - } - - let fields = requestBody(for: operation, sendOperationIdentifiers: false) - for (name, data) in fields { - if let data = data as? GraphQLMap { - let data = try serializationFormat.serialize(value: data) - formData.appendPart(data: data, name: name) - } else if let data = data as? String { - try formData.appendPart(string: data, name: name) - } else { - try formData.appendPart(string: data.debugDescription, name: name) - } - } - - try files.forEach { - formData.appendPart(inputStream: try $0.generateInputStream(), contentLength: $0.contentLength, name: $0.fieldName, contentType: $0.mimeType, filename: $0.originalName) - } - - return formData - } -} diff --git a/Tests/ApolloTests/TestFileHelper.swift b/Tests/ApolloTests/TestFileHelper.swift index c2f4eb9dba..136a630d45 100644 --- a/Tests/ApolloTests/TestFileHelper.swift +++ b/Tests/ApolloTests/TestFileHelper.swift @@ -30,4 +30,10 @@ struct TestFileHelper { self.uploadServerFolder(from: file) .appendingPathComponent("uploads") } + + static func fileURLForFile(named name: String, extension fileExtension: String) -> URL { + return self.testParentFolder() + .appendingPathComponent(name) + .appendingPathExtension(fileExtension) + } } diff --git a/Tests/ApolloTests/UploadTests.swift b/Tests/ApolloTests/UploadTests.swift index 276f0118cb..a81eec83b4 100644 --- a/Tests/ApolloTests/UploadTests.swift +++ b/Tests/ApolloTests/UploadTests.swift @@ -1,12 +1,20 @@ import XCTest -import Apollo +@testable import Apollo +import ApolloTestSupport import UploadAPI +import StarWarsAPI class UploadTests: XCTestCase { - let uploadClientURL = URL(string: "http://localhost:4000")! + let uploadClientURL = TestURL.uploadServer.url - lazy var client = ApolloClient(url: self.uploadClientURL) + lazy var client: ApolloClient = { + let provider = LegacyInterceptorProvider() + let transport = RequestChainNetworkTransport(interceptorProvider: provider, + endpointURL: self.uploadClientURL) + + return ApolloClient(networkTransport: transport) + }() override static func tearDown() { // Recreate the uploads folder at the end of all tests in this suite to avoid having one billion files in there @@ -191,6 +199,225 @@ class UploadTests: XCTestCase { } self.wait(for: [expectation], timeout: 10) + } + + // MARK: - UploadRequest + + private func fileURLForFile(named name: String, extension fileExtension: String) -> URL { + return TestFileHelper.testParentFolder() + .appendingPathComponent(name) + .appendingPathExtension(fileExtension) + } + + func testSingleFileWithUploadRequest() throws { + let alphaFileUrl = self.fileURLForFile(named: "a", extension: "txt") + + let alphaFile = try GraphQLFile(fieldName: "file", + originalName: "a.txt", + mimeType: "text/plain", + fileURL: alphaFileUrl) + let operation = UploadOneFileMutation(file: alphaFile.originalName) + let uploadRequest = UploadRequest(graphQLEndpoint: TestURL.mockServer.url, + operation: operation, + clientName: "test", + clientVersion: "test", + files: [alphaFile], + manualBoundary: "TEST.BOUNDARY") + let formData = try uploadRequest.requestMultipartFormData() + let stringToCompare = try formData.toTestString() + + if JSONSerialization.dataCanBeSorted() { + let expectedString = """ +--TEST.BOUNDARY +Content-Disposition: form-data; name="operations" + +{"id":"c5d5919f77d9ba16a9689b6b0ad4b781cb05dc1dc4812623bf80f7c044c09533","operationName":"UploadOneFile","query":"mutation UploadOneFile($file: Upload!) {\\n singleUpload(file: $file) {\\n __typename\\n id\\n path\\n filename\\n mimetype\\n }\\n}","variables":{"file":null}} +--TEST.BOUNDARY +Content-Disposition: form-data; name="map" + +{"0":["variables.file"]} +--TEST.BOUNDARY +Content-Disposition: form-data; name="0"; filename="a.txt" +Content-Type: text/plain + +Alpha file content. + +--TEST.BOUNDARY-- +""" + XCTAssertEqual(stringToCompare, expectedString) + } else { + // Operation parameters may be in weird order, so let's at least check that the files and single parameter got encoded properly. + let expectedEndString = """ +--TEST.BOUNDARY +Content-Disposition: form-data; name="map" + +{"0":["variables.file"]} +--TEST.BOUNDARY +Content-Disposition: form-data; name="0"; filename="a.txt" +Content-Type: text/plain + +Alpha file content. + +--TEST.BOUNDARY-- +""" + stringToCompare.apollo.checkIncludes(expectedString: expectedEndString) + } + } + + func testMultipleFilesWithUploadRequest() throws { + let alphaFileURL = self.fileURLForFile(named: "a", extension: "txt") + let alphaFile = try GraphQLFile(fieldName: "files", + originalName: "a.txt", + mimeType: "text/plain", + fileURL: alphaFileURL) + + let betaFileURL = self.fileURLForFile(named: "b", extension: "txt") + let betaFile = try GraphQLFile(fieldName: "files", + originalName: "b.txt", + mimeType: "text/plain", + fileURL: betaFileURL) + + let files = [alphaFile, betaFile] + let operation = UploadMultipleFilesToTheSameParameterMutation(files: files.map { $0.originalName }) + let uploadRequest = UploadRequest(graphQLEndpoint: TestURL.mockServer.url, + operation: operation, + clientName: "test", + clientVersion: "test", + files: files, + manualBoundary: "TEST.BOUNDARY") + let multipartData = try uploadRequest.requestMultipartFormData() + let stringToCompare = try multipartData.toTestString() + + if JSONSerialization.dataCanBeSorted() { + let expectedString = """ +--TEST.BOUNDARY +Content-Disposition: form-data; name="operations" + +{"id":"88858c283bb72f18c0049dc85b140e72a4046f469fa16a8bf4bcf01c11d8a2b7","operationName":"UploadMultipleFilesToTheSameParameter","query":"mutation UploadMultipleFilesToTheSameParameter($files: [Upload!]!) {\\n multipleUpload(files: $files) {\\n __typename\\n id\\n path\\n filename\\n mimetype\\n }\\n}","variables":{"files":[null,null]}} +--TEST.BOUNDARY +Content-Disposition: form-data; name="map" + +{"0":["variables.files.0"],"1":["variables.files.1"]} +--TEST.BOUNDARY +Content-Disposition: form-data; name="0"; filename="a.txt" +Content-Type: text/plain + +Alpha file content. + +--TEST.BOUNDARY +Content-Disposition: form-data; name="1"; filename="b.txt" +Content-Type: text/plain + +Bravo file content. + +--TEST.BOUNDARY-- +""" + XCTAssertEqual(stringToCompare, expectedString) + } else { + // Query and operation parameters may be in weird order, so let's at least check that the files got encoded properly. + let endString = """ +--TEST.BOUNDARY +Content-Disposition: form-data; name="0"; filename="a.txt" +Content-Type: text/plain + +Alpha file content. + +--TEST.BOUNDARY +Content-Disposition: form-data; name="1"; filename="b.txt" +Content-Type: text/plain + +Bravo file content. + +--TEST.BOUNDARY-- +""" + stringToCompare.apollo.checkIncludes(expectedString: endString) + } + } + + func testMultipleFilesWithMultipleFieldsWithUploadRequest() throws { + let alphaFileURL = self.fileURLForFile(named: "a", extension: "txt") + let alphaFile = try GraphQLFile(fieldName: "uploads", + originalName: "a.txt", + mimeType: "text/plain", + fileURL: alphaFileURL) + + let betaFileURL = self.fileURLForFile(named: "b", extension: "txt") + let betaFile = try GraphQLFile(fieldName: "uploads", + originalName: "b.txt", + mimeType: "text/plain", + fileURL: betaFileURL) + + let charlieFileUrl = self.fileURLForFile(named: "c", extension: "txt") + let charlieFile = try GraphQLFile(fieldName: "secondField", + originalName: "c.txt", + mimeType: "text/plain", + fileURL: charlieFileUrl) + let uploadRequest = UploadRequest(graphQLEndpoint: TestURL.mockServer.url, + operation: HeroNameQuery(), + clientName: "test", + clientVersion: "test", + files: [alphaFile, betaFile, charlieFile], + manualBoundary: "TEST.BOUNDARY") + + let multipartData = try uploadRequest.requestMultipartFormData() + let stringToCompare = try multipartData.toTestString() + + if JSONSerialization.dataCanBeSorted() { + let expectedString = """ + --TEST.BOUNDARY + Content-Disposition: form-data; name="operations" + + {"id":"f6e76545cd03aa21368d9969cb39447f6e836a16717823281803778e7805d671","operationName":"HeroName","query":"query HeroName($episode: Episode) {\\n hero(episode: $episode) {\\n __typename\\n name\\n }\\n}","variables":{"episode":null,\"secondField\":null,\"uploads\":null}} + --TEST.BOUNDARY + Content-Disposition: form-data; name="map" + + {"0":["variables.secondField"],"1":["variables.uploads.0"],"2":["variables.uploads.1"]} + --TEST.BOUNDARY + Content-Disposition: form-data; name="0"; filename="c.txt" + Content-Type: text/plain + + Charlie file content. + + --TEST.BOUNDARY + Content-Disposition: form-data; name="1"; filename="a.txt" + Content-Type: text/plain + + Alpha file content. + + --TEST.BOUNDARY + Content-Disposition: form-data; name="2"; filename="b.txt" + Content-Type: text/plain + + Bravo file content. + + --TEST.BOUNDARY-- + """ + XCTAssertEqual(stringToCompare, expectedString) + } else { + // Query and operation parameters may be in weird order, so let's at least check that the files got encoded properly. + let endString = """ + --TEST.BOUNDARY + Content-Disposition: form-data; name="0"; filename="c.txt" + Content-Type: text/plain + + Charlie file content. + + --TEST.BOUNDARY + Content-Disposition: form-data; name="1"; filename="a.txt" + Content-Type: text/plain + + Alpha file content. + + --TEST.BOUNDARY + Content-Disposition: form-data; name="2"; filename="b.txt" + Content-Type: text/plain + + Bravo file content. + + --TEST.BOUNDARY-- + """ + stringToCompare.apollo.checkIncludes(expectedString: endString) + } } } diff --git a/Tests/ApolloWebsocketTests/MockWebSocket.swift b/Tests/ApolloWebsocketTests/MockWebSocket.swift index e478bb27ca..4425710b3f 100644 --- a/Tests/ApolloWebsocketTests/MockWebSocket.swift +++ b/Tests/ApolloWebsocketTests/MockWebSocket.swift @@ -1,5 +1,6 @@ import Starscream import Foundation +import ApolloTestSupport @testable import ApolloWebSocket class MockWebSocket: ApolloWebSocketClient { @@ -16,7 +17,7 @@ class MockWebSocket: ApolloWebSocketClient { } public init() { - self.request = URLRequest(url: URL(string: "http://localhost:8080")!) + self.request = URLRequest(url: TestURL.starWarsServer.url) } open func reportDidConnect() { diff --git a/Tests/ApolloWebsocketTests/MockWebSocketTests.swift b/Tests/ApolloWebsocketTests/MockWebSocketTests.swift index 9708515422..041164c451 100644 --- a/Tests/ApolloWebsocketTests/MockWebSocketTests.swift +++ b/Tests/ApolloWebsocketTests/MockWebSocketTests.swift @@ -1,5 +1,6 @@ import XCTest import Apollo +import ApolloTestSupport @testable import ApolloWebSocket import StarWarsAPI @@ -20,7 +21,7 @@ class MockWebSocketTests: XCTestCase { super.setUp() WebSocketTransport.provider = MockWebSocket.self - networkTransport = WebSocketTransport(request: URLRequest(url: URL(string: "http://localhost/dummy_url")!)) + networkTransport = WebSocketTransport(request: URLRequest(url: TestURL.mockServer.url)) client = ApolloClient(networkTransport: networkTransport!) } diff --git a/Tests/ApolloWebsocketTests/SplitNetworkTransportTests.swift b/Tests/ApolloWebsocketTests/SplitNetworkTransportTests.swift index b67c150521..83a72c79f7 100644 --- a/Tests/ApolloWebsocketTests/SplitNetworkTransportTests.swift +++ b/Tests/ApolloWebsocketTests/SplitNetworkTransportTests.swift @@ -8,49 +8,49 @@ import Foundation import XCTest import Apollo +import ApolloTestSupport @testable import ApolloWebSocket class SplitNetworkTransportTests: XCTestCase { - private let httpName = "TestHTTPNetworkTransport" - private let httpVersion = "TestHTTPNetworkTransportVersion" + private let mockTransportName = "TestMockNetworkTransport" + private let mockTransportVersion = "TestMockNetworkTransportVersion" private let webSocketName = "TestWebSocketTransport" private let webSocketVersion = "TestWebSocketTransportVersion" - private lazy var httpTransport: HTTPNetworkTransport = { - let url = URL(string: "http://localhost:8080/graphql")! - let transport = HTTPNetworkTransport(url: url) + private lazy var mockTransport: MockNetworkTransport = { + let transport = MockNetworkTransport(body: JSONObject(), + store: ApolloStore()) - transport.clientName = self.httpName - transport.clientVersion = self.httpVersion + transport.clientName = self.mockTransportName + transport.clientVersion = self.mockTransportVersion return transport }() private lazy var webSocketTransport: WebSocketTransport = { - let url = URL(string: "ws://localhost:8080/websocket")! - let request = URLRequest(url: url) + let request = URLRequest(url: TestURL.starWarsWebSocket.url) return WebSocketTransport(request: request, clientName: self.webSocketName, clientVersion: self.webSocketVersion) }() private lazy var splitTransport = SplitNetworkTransport( - httpNetworkTransport: self.httpTransport, + uploadingNetworkTransport: self.mockTransport, webSocketNetworkTransport: self.webSocketTransport ) func testGettingSplitClientNameWithDifferentNames() { let splitName = self.splitTransport.clientName XCTAssertTrue(splitName.hasPrefix("SPLIT_")) - XCTAssertTrue(splitName.contains(self.httpName)) + XCTAssertTrue(splitName.contains(self.mockTransportName)) XCTAssertTrue(splitName.contains(self.webSocketName)) } func testGettingSplitClientVersionWithDifferentVersions() { let splitVersion = self.splitTransport.clientVersion XCTAssertTrue(splitVersion.hasPrefix("SPLIT_")) - XCTAssertTrue(splitVersion.contains(self.httpVersion)) + XCTAssertTrue(splitVersion.contains(self.mockTransportVersion)) XCTAssertTrue(splitVersion.contains(self.webSocketVersion)) } @@ -58,7 +58,7 @@ class SplitNetworkTransportTests: XCTestCase { let splitName = "TestSplitClientName" self.webSocketTransport.clientName = splitName - self.httpTransport.clientName = splitName + self.mockTransport.clientName = splitName XCTAssertEqual(self.splitTransport.clientName, splitName) } @@ -67,7 +67,7 @@ class SplitNetworkTransportTests: XCTestCase { let splitVersion = "TestSplitClientVersion" self.webSocketTransport.clientVersion = splitVersion - self.httpTransport.clientVersion = splitVersion + self.mockTransport.clientVersion = splitVersion XCTAssertEqual(self.splitTransport.clientVersion, splitVersion) } diff --git a/Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift b/Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift index ec3c40cc5d..8c0451c44c 100644 --- a/Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift +++ b/Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift @@ -6,7 +6,6 @@ import StarWarsAPI import Starscream class StarWarsSubscriptionTests: XCTestCase { - let SERVER = "ws://localhost:8080/websocket" let concurrentQueue = DispatchQueue(label: "com.apollographql.testing", attributes: .concurrent) var client: ApolloClient! @@ -22,7 +21,7 @@ class StarWarsSubscriptionTests: XCTestCase { self.connectionStartedExpectation = self.expectation(description: "Web socket connected") WebSocketTransport.provider = ApolloWebSocket.self - webSocketTransport = WebSocketTransport(request: URLRequest(url: URL(string: SERVER)!)) + webSocketTransport = WebSocketTransport(request: URLRequest(url: TestURL.starWarsWebSocket.url)) webSocketTransport.delegate = self client = ApolloClient(networkTransport: webSocketTransport) @@ -393,7 +392,7 @@ class StarWarsSubscriptionTests: XCTestCase { func testConcurrentConnectAndCloseConnection() { WebSocketTransport.provider = MockWebSocket.self - let webSocketTransport = WebSocketTransport(request: URLRequest(url: URL(string: SERVER)!)) + let webSocketTransport = WebSocketTransport(request: URLRequest(url: TestURL.starWarsWebSocket.url)) let expectation = self.expectation(description: "Connection closed") expectation.expectedFulfillmentCount = 2 @@ -417,12 +416,14 @@ class StarWarsSubscriptionTests: XCTestCase { let reviewMutation = CreateAwesomeReviewMutation() // Send the mutations via a separate transport so they can still be sent when the websocket is disconnected - let httpTransport = HTTPNetworkTransport(url: URL(string: "http://localhost:8080/graphql")!) - let httpClient = ApolloClient(networkTransport: httpTransport) + let interceptorProvider = LegacyInterceptorProvider() + let alternateTransport = RequestChainNetworkTransport(interceptorProvider: interceptorProvider, + endpointURL: TestURL.starWarsServer.url) + let alternateClient = ApolloClient(networkTransport: alternateTransport) func sendReview() { let reviewSentExpectation = self.expectation(description: "review sent") - httpClient.perform(mutation: reviewMutation) { mutationResult in + alternateClient.perform(mutation: reviewMutation) { mutationResult in switch mutationResult { case .success: break diff --git a/Tests/ApolloWebsocketTests/StarWarsWebSocketTests.swift b/Tests/ApolloWebsocketTests/StarWarsWebSocketTests.swift index 479f180343..0e8747f21f 100755 --- a/Tests/ApolloWebsocketTests/StarWarsWebSocketTests.swift +++ b/Tests/ApolloWebsocketTests/StarWarsWebSocketTests.swift @@ -4,10 +4,7 @@ import ApolloTestSupport @testable import ApolloWebSocket import StarWarsAPI -// import StarWarsAPI - class StarWarsWebSocketTests: XCTestCase, CacheTesting { - let SERVER = "http://localhost:8080/websocket" var cacheType: TestCacheProvider.Type { InMemoryTestCacheProvider.self @@ -275,7 +272,7 @@ class StarWarsWebSocketTests: XCTestCase, CacheTesting { private func fetch(query: Query, completionHandler: @escaping (_ data: Query.Data) -> Void) { withCache { (cache) in - let network = WebSocketTransport(request: URLRequest(url: URL(string: SERVER)!)) + let network = WebSocketTransport(request: URLRequest(url: TestURL.starWarsWebSocket.url)) let store = ApolloStore(cache: cache) let client = ApolloClient(networkTransport: network, store: store) @@ -304,7 +301,7 @@ class StarWarsWebSocketTests: XCTestCase, CacheTesting { private func perform(mutation: Mutation, completionHandler: @escaping (_ data: Mutation.Data) -> Void) { withCache { (cache) in - let network = WebSocketTransport(request: URLRequest(url: URL(string: SERVER)!)) + let network = WebSocketTransport(request: URLRequest(url: TestURL.starWarsWebSocket.url)) let store = ApolloStore(cache: cache) let client = ApolloClient(networkTransport: network, store: store) diff --git a/Tests/ApolloWebsocketTests/WebSocketTransportTests.swift b/Tests/ApolloWebsocketTests/WebSocketTransportTests.swift index 3234155443..46ae7fc61c 100644 --- a/Tests/ApolloWebsocketTests/WebSocketTransportTests.swift +++ b/Tests/ApolloWebsocketTests/WebSocketTransportTests.swift @@ -1,15 +1,15 @@ import XCTest import Apollo +import ApolloTestSupport import Starscream @testable import ApolloWebSocket class WebSocketTransportTests: XCTestCase { - private let mockSocketURL = URL(string: "http://localhost/dummy_url")! private var webSocketTransport: WebSocketTransport! func testUpdateHeaderValues() { - var request = URLRequest(url: mockSocketURL) + var request = URLRequest(url: TestURL.mockServer.url) request.addValue("OldToken", forHTTPHeaderField: "Authorization") self.webSocketTransport = WebSocketTransport(request: request) @@ -22,7 +22,7 @@ class WebSocketTransportTests: XCTestCase { func testUpdateConnectingPayload() { WebSocketTransport.provider = MockWebSocket.self - self.webSocketTransport = WebSocketTransport(request: URLRequest(url: mockSocketURL), + self.webSocketTransport = WebSocketTransport(request: URLRequest(url: TestURL.mockServer.url), connectingPayload: ["Authorization": "OldToken"]) let mockWebSocketDelegate = MockWebSocketDelegate() diff --git a/docs/source/api/Apollo/README.md b/docs/source/api/Apollo/README.md index 39bafa60ab..1213103bca 100644 --- a/docs/source/api/Apollo/README.md +++ b/docs/source/api/Apollo/README.md @@ -3,7 +3,10 @@ ## Protocols - [ApolloClientProtocol](protocols/ApolloClientProtocol/) +- [ApolloErrorInterceptor](protocols/ApolloErrorInterceptor/) +- [ApolloInterceptor](protocols/ApolloInterceptor/) - [Cancellable](protocols/Cancellable/) +- [FlexibleDecoder](protocols/FlexibleDecoder/) - [GraphQLFragment](protocols/GraphQLFragment/) - [GraphQLInputValue](protocols/GraphQLInputValue/) - [GraphQLMapConvertible](protocols/GraphQLMapConvertible/) @@ -13,22 +16,19 @@ - [GraphQLSelection](protocols/GraphQLSelection/) - [GraphQLSelectionSet](protocols/GraphQLSelectionSet/) - [GraphQLSubscription](protocols/GraphQLSubscription/) -- [HTTPNetworkTransportDelegate](protocols/HTTPNetworkTransportDelegate/) -- [HTTPNetworkTransportGraphQLErrorDelegate](protocols/HTTPNetworkTransportGraphQLErrorDelegate/) -- [HTTPNetworkTransportPreflightDelegate](protocols/HTTPNetworkTransportPreflightDelegate/) -- [HTTPNetworkTransportRetryDelegate](protocols/HTTPNetworkTransportRetryDelegate/) -- [HTTPNetworkTransportTaskCompletedDelegate](protocols/HTTPNetworkTransportTaskCompletedDelegate/) +- [InterceptorProvider](protocols/InterceptorProvider/) - [JSONDecodable](protocols/JSONDecodable/) - [JSONEncodable](protocols/JSONEncodable/) - [Matchable](protocols/Matchable/) - [NetworkTransport](protocols/NetworkTransport/) - [NormalizedCache](protocols/NormalizedCache/) -- [RequestCreator](protocols/RequestCreator/) +- [Parseable](protocols/Parseable/) +- [RequestBodyCreator](protocols/RequestBodyCreator/) - [UploadingNetworkTransport](protocols/UploadingNetworkTransport/) ## Structs -- [ApolloRequestCreator](structs/ApolloRequestCreator/) +- [ApolloRequestBodyCreator](structs/ApolloRequestBodyCreator/) - [GraphQLBooleanCondition](structs/GraphQLBooleanCondition/) - [GraphQLError](structs/GraphQLError/) - [GraphQLError.Location](structs/GraphQLError.Location/) @@ -52,28 +52,50 @@ - [ApolloStore](classes/ApolloStore/) - [ApolloStore.ReadTransaction](classes/ApolloStore.ReadTransaction/) - [ApolloStore.ReadWriteTransaction](classes/ApolloStore.ReadWriteTransaction/) +- [AutomaticPersistedQueryInterceptor](classes/AutomaticPersistedQueryInterceptor/) +- [CodableInterceptorProvider](classes/CodableInterceptorProvider/) +- [CodableParsingInterceptor](classes/CodableParsingInterceptor/) - [EmptyCancellable](classes/EmptyCancellable/) - [GraphQLQueryWatcher](classes/GraphQLQueryWatcher/) - [GraphQLResponse](classes/GraphQLResponse/) -- [HTTPNetworkTransport](classes/HTTPNetworkTransport/) +- [HTTPRequest](classes/HTTPRequest/) +- [HTTPResponse](classes/HTTPResponse/) - [InMemoryNormalizedCache](classes/InMemoryNormalizedCache/) +- [JSONRequest](classes/JSONRequest/) - [JSONSerializationFormat](classes/JSONSerializationFormat/) +- [LegacyCacheReadInterceptor](classes/LegacyCacheReadInterceptor/) +- [LegacyCacheWriteInterceptor](classes/LegacyCacheWriteInterceptor/) +- [LegacyInterceptorProvider](classes/LegacyInterceptorProvider/) +- [LegacyParsingInterceptor](classes/LegacyParsingInterceptor/) +- [MaxRetryInterceptor](classes/MaxRetryInterceptor/) - [MultipartFormData](classes/MultipartFormData/) +- [NetworkFetchInterceptor](classes/NetworkFetchInterceptor/) +- [RequestChain](classes/RequestChain/) +- [RequestChainNetworkTransport](classes/RequestChainNetworkTransport/) +- [ResponseCodeInterceptor](classes/ResponseCodeInterceptor/) - [TaskData](classes/TaskData/) - [URLSessionClient](classes/URLSessionClient/) +- [UploadRequest](classes/UploadRequest/) ## Enums - [ApolloClient.ApolloClientError](enums/ApolloClient.ApolloClientError/) +- [AutomaticPersistedQueryInterceptor.APQError](enums/AutomaticPersistedQueryInterceptor.APQError/) - [CachePolicy](enums/CachePolicy/) +- [CodableParsingInterceptor.CodableParsingError](enums/CodableParsingInterceptor.CodableParsingError/) - [GraphQLFile.GraphQLFileError](enums/GraphQLFile.GraphQLFileError/) - [GraphQLHTTPRequestError](enums/GraphQLHTTPRequestError/) - [GraphQLHTTPResponseError.ErrorKind](enums/GraphQLHTTPResponseError.ErrorKind/) - [GraphQLOperationType](enums/GraphQLOperationType/) - [GraphQLOutputType](enums/GraphQLOutputType/) - [GraphQLResult.Source](enums/GraphQLResult.Source/) -- [HTTPNetworkTransport.ContinueAction](enums/HTTPNetworkTransport.ContinueAction/) - [JSONDecodingError](enums/JSONDecodingError/) +- [LegacyCacheWriteInterceptor.LegacyCacheWriteError](enums/LegacyCacheWriteInterceptor.LegacyCacheWriteError/) +- [LegacyParsingInterceptor.LegacyParsingError](enums/LegacyParsingInterceptor.LegacyParsingError/) +- [MaxRetryInterceptor.RetryError](enums/MaxRetryInterceptor.RetryError/) +- [ParseableError](enums/ParseableError/) +- [RequestChain.ChainError](enums/RequestChain.ChainError/) +- [ResponseCodeInterceptor.ResponseCodeError](enums/ResponseCodeInterceptor.ResponseCodeError/) - [URLSessionClient.URLSessionClientError](enums/URLSessionClient.URLSessionClientError/) ## Extensions @@ -90,20 +112,24 @@ - [GraphQLMutation](extensions/GraphQLMutation/) - [GraphQLOperation](extensions/GraphQLOperation/) - [GraphQLQuery](extensions/GraphQLQuery/) +- [GraphQLResult](extensions/GraphQLResult/) - [GraphQLSelectionSet](extensions/GraphQLSelectionSet/) - [GraphQLSubscription](extensions/GraphQLSubscription/) - [GraphQLVariable](extensions/GraphQLVariable/) -- [HTTPNetworkTransport](extensions/HTTPNetworkTransport/) +- [HTTPRequest](extensions/HTTPRequest/) - [Int](extensions/Int/) +- [InterceptorProvider](extensions/InterceptorProvider/) - [JSONDecodingError](extensions/JSONDecodingError/) - [JSONEncodable](extensions/JSONEncodable/) - [NetworkTransport](extensions/NetworkTransport/) - [Optional](extensions/Optional/) +- [Parseable](extensions/Parseable/) - [RawRepresentable](extensions/RawRepresentable/) - [Record](extensions/Record/) - [RecordSet](extensions/RecordSet/) - [Reference](extensions/Reference/) -- [RequestCreator](extensions/RequestCreator/) +- [RequestBodyCreator](extensions/RequestBodyCreator/) +- [RequestChainNetworkTransport](extensions/RequestChainNetworkTransport/) - [String](extensions/String/) - [URL](extensions/URL/) @@ -116,9 +142,11 @@ - [GraphQLID](typealiases/GraphQLID/) - [GraphQLMap](typealiases/GraphQLMap/) - [GraphQLResultHandler](typealiases/GraphQLResultHandler/) +- [JSONDecoder.Input](typealiases/JSONDecoder.Input/) - [JSONDecodingError.Base](typealiases/JSONDecodingError.Base/) - [JSONObject](typealiases/JSONObject/) - [JSONValue](typealiases/JSONValue/) +- [PropertyListDecoder.Input](typealiases/PropertyListDecoder.Input/) - [Record.Fields](typealiases/Record.Fields/) - [Record.Value](typealiases/Record.Value/) - [ResultMap](typealiases/ResultMap/) diff --git a/docs/source/api/Apollo/classes/ApolloStore.md b/docs/source/api/Apollo/classes/ApolloStore.md index b2ed7a7536..ddb7e71d3c 100644 --- a/docs/source/api/Apollo/classes/ApolloStore.md +++ b/docs/source/api/Apollo/classes/ApolloStore.md @@ -19,18 +19,18 @@ public var cacheKeyForObject: CacheKeyForObject? ### `init(cache:)` ```swift -public init(cache: NormalizedCache) +public init(cache: NormalizedCache = InMemoryNormalizedCache()) ``` > Designated initializer > -> - Parameter cache: An instance of `normalizedCache` to use to cache results. +> - Parameter cache: An instance of `normalizedCache` to use to cache results. Defaults to an `InMemoryNormalizedCache`. #### Parameters | Name | Description | | ---- | ----------- | -| cache | An instance of `normalizedCache` to use to cache results. | +| cache | An instance of `normalizedCache` to use to cache results. Defaults to an `InMemoryNormalizedCache`. | ### `clearCache(callbackQueue:completion:)` @@ -91,7 +91,7 @@ public func withinReadWriteTransaction(_ body: @escaping (ReadWriteTransactio ### `load(query:resultHandler:)` ```swift -public func load(query: Query, resultHandler: @escaping GraphQLResultHandler) +public func load(query: Operation, resultHandler: @escaping GraphQLResultHandler) ``` > Loads the results for the given query from the cache. diff --git a/docs/source/api/Apollo/classes/AutomaticPersistedQueryInterceptor.md b/docs/source/api/Apollo/classes/AutomaticPersistedQueryInterceptor.md new file mode 100644 index 0000000000..6771965002 --- /dev/null +++ b/docs/source/api/Apollo/classes/AutomaticPersistedQueryInterceptor.md @@ -0,0 +1,35 @@ +**CLASS** + +# `AutomaticPersistedQueryInterceptor` + +```swift +public class AutomaticPersistedQueryInterceptor: ApolloInterceptor +``` + +## Methods +### `init()` + +```swift +public init() +``` + +> Designated initializer + +### `interceptAsync(chain:request:response:completion:)` + +```swift +public func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) +``` + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| chain | The chain the interceptor is a part of. | +| request | The request, as far as it has been constructed | +| response | [optional] The response, if received | +| completion | The completion block to fire when data needs to be returned to the UI. | \ No newline at end of file diff --git a/docs/source/api/Apollo/classes/CodableInterceptorProvider.md b/docs/source/api/Apollo/classes/CodableInterceptorProvider.md new file mode 100644 index 0000000000..7a7e875051 --- /dev/null +++ b/docs/source/api/Apollo/classes/CodableInterceptorProvider.md @@ -0,0 +1,52 @@ +**CLASS** + +# `CodableInterceptorProvider` + +```swift +open class CodableInterceptorProvider: InterceptorProvider +``` + +> The default interceptor provider for code generated with Swift Codegen™ + +## Methods +### `init(client:shouldInvalidateClientOnDeinit:store:decoder:)` + +```swift +public init(client: URLSessionClient = URLSessionClient(), + shouldInvalidateClientOnDeinit: Bool = true, + store: ApolloStore, + decoder: FlexDecoder) +``` + +> Designated initializer +> +> - Parameters: +> - client: The URLSessionClient to use. Defaults to the default setup. +> - shouldInvalidateClientOnDeinit: If the passed-in client should be invalidated when this interceptor provider is deinitialized. If you are recreating the `URLSessionClient` every time you create a new provider, you should do this to prevent memory leaks. Defaults to true, since by default we provide a `URLSessionClient` to new instances. +> - decoder: A `FlexibleDecoder` which can decode `Codable` objects. + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| client | The URLSessionClient to use. Defaults to the default setup. | +| shouldInvalidateClientOnDeinit | If the passed-in client should be invalidated when this interceptor provider is deinitialized. If you are recreating the `URLSessionClient` every time you create a new provider, you should do this to prevent memory leaks. Defaults to true, since by default we provide a `URLSessionClient` to new instances. | +| decoder | A `FlexibleDecoder` which can decode `Codable` objects. | + +### `deinit` + +```swift +deinit +``` + +### `interceptors(for:)` + +```swift +open func interceptors(for operation: Operation) -> [ApolloInterceptor] +``` + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| operation | The operation to provide interceptors for | \ No newline at end of file diff --git a/docs/source/api/Apollo/classes/CodableParsingInterceptor.md b/docs/source/api/Apollo/classes/CodableParsingInterceptor.md new file mode 100644 index 0000000000..20709c7063 --- /dev/null +++ b/docs/source/api/Apollo/classes/CodableParsingInterceptor.md @@ -0,0 +1,33 @@ +**CLASS** + +# `CodableParsingInterceptor` + +```swift +public class CodableParsingInterceptor: ApolloInterceptor +``` + +## Methods +### `init(decoder:)` + +```swift +public init(decoder: FlexDecoder) +``` + +### `interceptAsync(chain:request:response:completion:)` + +```swift +public func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) +``` + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| chain | The chain the interceptor is a part of. | +| request | The request, as far as it has been constructed | +| response | [optional] The response, if received | +| completion | The completion block to fire when data needs to be returned to the UI. | \ No newline at end of file diff --git a/docs/source/api/Apollo/classes/GraphQLResponse.md b/docs/source/api/Apollo/classes/GraphQLResponse.md index 90a520efde..a9770c2771 100644 --- a/docs/source/api/Apollo/classes/GraphQLResponse.md +++ b/docs/source/api/Apollo/classes/GraphQLResponse.md @@ -3,7 +3,7 @@ # `GraphQLResponse` ```swift -public final class GraphQLResponse +public final class GraphQLResponse: Parseable ``` > Represents a GraphQL response received from a server. @@ -16,12 +16,32 @@ public let body: JSONObject ``` ## Methods +### `init(from:decoder:)` + +```swift +public init(from data: Foundation.Data, decoder: T) throws where T : FlexibleDecoder +``` + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| data | The data to decode | +| decoder | The decoder to use to decode it | + ### `init(operation:body:)` ```swift public init(operation: Operation, body: JSONObject) where Operation.Data == Data ``` +### `parseResultWithCompletion(cacheKeyForObject:completion:)` + +```swift +public func parseResultWithCompletion(cacheKeyForObject: CacheKeyForObject? = nil, + completion: (Result<(GraphQLResult, RecordSet?), Error>) -> Void) +``` + ### `parseErrorsOnlyFast()` ```swift diff --git a/docs/source/api/Apollo/classes/HTTPNetworkTransport.md b/docs/source/api/Apollo/classes/HTTPNetworkTransport.md deleted file mode 100644 index dde75eb846..0000000000 --- a/docs/source/api/Apollo/classes/HTTPNetworkTransport.md +++ /dev/null @@ -1,70 +0,0 @@ -**CLASS** - -# `HTTPNetworkTransport` - -```swift -public class HTTPNetworkTransport -``` - -> A network transport that uses HTTP POST requests to send GraphQL operations to a server, and that uses `URLSession` as the networking implementation. - -## Properties -### `delegate` - -```swift -public weak var delegate: HTTPNetworkTransportDelegate? -``` - -> A delegate which can conform to any or all of `HTTPNetworkTransportPreflightDelegate`, `HTTPNetworkTransportTaskCompletedDelegate`, and `HTTPNetworkTransportRetryDelegate`. - -### `clientName` - -```swift -public lazy var clientName = HTTPNetworkTransport.defaultClientName -``` - -### `clientVersion` - -```swift -public lazy var clientVersion = HTTPNetworkTransport.defaultClientVersion -``` - -## Methods -### `init(url:client:sendOperationIdentifiers:useGETForQueries:enableAutoPersistedQueries:useGETForPersistedQueryRetry:requestCreator:)` - -```swift -public init(url: URL, - client: URLSessionClient = URLSessionClient(), - sendOperationIdentifiers: Bool = false, - useGETForQueries: Bool = false, - enableAutoPersistedQueries: Bool = false, - useGETForPersistedQueryRetry: Bool = false, - requestCreator: RequestCreator = ApolloRequestCreator()) -``` - -> Creates a network transport with the specified server URL and session configuration. -> -> - Parameters: -> - url: The URL of a GraphQL server to connect to. -> - client: The client to handle URL Session calls. -> - sendOperationIdentifiers: Whether to send operation identifiers rather than full operation text, for use with servers that support query persistence. Defaults to false. -> - useGETForQueries: If query operation should be sent using GET instead of POST. Defaults to false. -> - enableAutoPersistedQueries: Whether to send persistedQuery extension. QueryDocument will be absent at 1st request, retry with QueryDocument if server respond PersistedQueryNotFound or PersistedQueryNotSupport. Defaults to false. -> - useGETForPersistedQueryRetry: Whether to retry persistedQuery request with HttpGetMethod. Defaults to false. - -#### Parameters - -| Name | Description | -| ---- | ----------- | -| url | The URL of a GraphQL server to connect to. | -| client | The client to handle URL Session calls. | -| sendOperationIdentifiers | Whether to send operation identifiers rather than full operation text, for use with servers that support query persistence. Defaults to false. | -| useGETForQueries | If query operation should be sent using GET instead of POST. Defaults to false. | -| enableAutoPersistedQueries | Whether to send persistedQuery extension. QueryDocument will be absent at 1st request, retry with QueryDocument if server respond PersistedQueryNotFound or PersistedQueryNotSupport. Defaults to false. | -| useGETForPersistedQueryRetry | Whether to retry persistedQuery request with HttpGetMethod. Defaults to false. | - -### `deinit` - -```swift -deinit -``` diff --git a/docs/source/api/Apollo/classes/HTTPRequest.md b/docs/source/api/Apollo/classes/HTTPRequest.md new file mode 100644 index 0000000000..1ccd8ad6e4 --- /dev/null +++ b/docs/source/api/Apollo/classes/HTTPRequest.md @@ -0,0 +1,112 @@ +**CLASS** + +# `HTTPRequest` + +```swift +open class HTTPRequest +``` + +> Encapsulation of all information about a request before it hits the network + +## Properties +### `graphQLEndpoint` + +```swift +open var graphQLEndpoint: URL +``` + +> The endpoint to make a GraphQL request to + +### `operation` + +```swift +open var operation: Operation +``` + +> The GraphQL Operation to execute + +### `additionalHeaders` + +```swift +open var additionalHeaders: [String: String] +``` + +> Any additional headers you wish to add by default to this request + +### `cachePolicy` + +```swift +public let cachePolicy: CachePolicy +``` + +> The `CachePolicy` to use for this request. + +### `contextIdentifier` + +```swift +public let contextIdentifier: UUID? +``` + +> [optional] A unique identifier for this request, to help with deduping cache hits for watchers. + +## Methods +### `init(graphQLEndpoint:operation:contextIdentifier:contentType:clientName:clientVersion:additionalHeaders:cachePolicy:)` + +```swift +public init(graphQLEndpoint: URL, + operation: Operation, + contextIdentifier: UUID? = nil, + contentType: String, + clientName: String, + clientVersion: String, + additionalHeaders: [String: String], + cachePolicy: CachePolicy = .default) +``` + +> Designated Initializer +> +> - Parameters: +> - graphQLEndpoint: The endpoint to make a GraphQL request to +> - operation: The GraphQL Operation to execute +> - contextIdentifier: [optional] A unique identifier for this request, to help with deduping cache hits for watchers. Defaults to `nil`. +> - contentType: The `Content-Type` header's value. Should usually be set for you by a subclass. +> - clientName: The name of the client to send with the `"apollographql-client-name"` header +> - clientVersion: The version of the client to send with the `"apollographql-client-version"` header +> - additionalHeaders: Any additional headers you wish to add by default to this request. +> - cachePolicy: The `CachePolicy` to use for this request. Defaults to the `.default` policy + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| graphQLEndpoint | The endpoint to make a GraphQL request to | +| operation | The GraphQL Operation to execute | +| contextIdentifier | [optional] A unique identifier for this request, to help with deduping cache hits for watchers. Defaults to `nil`. | +| contentType | The `Content-Type` header’s value. Should usually be set for you by a subclass. | +| clientName | The name of the client to send with the `"apollographql-client-name"` header | +| clientVersion | The version of the client to send with the `"apollographql-client-version"` header | +| additionalHeaders | Any additional headers you wish to add by default to this request. | +| cachePolicy | The `CachePolicy` to use for this request. Defaults to the `.default` policy | + +### `addHeader(name:value:)` + +```swift +open func addHeader(name: String, value: String) +``` + +### `updateContentType(to:)` + +```swift +open func updateContentType(to contentType: String) +``` + +### `toURLRequest()` + +```swift +open func toURLRequest() throws -> URLRequest +``` + +> Converts this object to a fully fleshed-out `URLRequest` +> +> - Throws: Any error in creating the request +> - Returns: The URL request, ready to send to your server. diff --git a/docs/source/api/Apollo/classes/HTTPResponse.md b/docs/source/api/Apollo/classes/HTTPResponse.md new file mode 100644 index 0000000000..1616887e46 --- /dev/null +++ b/docs/source/api/Apollo/classes/HTTPResponse.md @@ -0,0 +1,67 @@ +**CLASS** + +# `HTTPResponse` + +```swift +public class HTTPResponse +``` + +> Data about a response received by an HTTP request. + +## Properties +### `httpResponse` + +```swift +public var httpResponse: HTTPURLResponse +``` + +> The `HTTPURLResponse` received from the URL loading system + +### `rawData` + +```swift +public var rawData: Data +``` + +> The raw data received from the URL loading system + +### `parsedResponse` + +```swift +public var parsedResponse: GraphQLResult? +``` + +> [optional] The data as parsed into a `GraphQLResult`, which can eventually be returned to the UI. Will be nil if not yet parsed. + +### `legacyResponse` + +```swift +public var legacyResponse: GraphQLResponse? = nil +``` + +> [optional] The data as parsed into a `GraphQLResponse` for legacy caching purposes. If you're not using the `LegacyParsingInterceptor`, you probably shouldn't be using this property. +> **NOTE:** This property will be removed when the transition to a Codable-based Codegen is complete. + +## Methods +### `init(response:rawData:parsedResponse:)` + +```swift +public init(response: HTTPURLResponse, + rawData: Data, + parsedResponse: GraphQLResult?) +``` + +> Designated initializer +> +> - Parameters: +> - response: The `HTTPURLResponse` received from the server. +> - rawData: The raw, unparsed data received from the server. +> - parsedResponse: [optional] The response parsed into the `ParsedValue` type. Will be nil if not yet parsed, or if parsing failed. + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| response | The `HTTPURLResponse` received from the server. | +| rawData | The raw, unparsed data received from the server. | +| parsedResponse | [optional] The response parsed into the `ParsedValue` type. Will be nil if not yet parsed, or if parsing failed. | \ No newline at end of file diff --git a/docs/source/api/Apollo/classes/JSONRequest.md b/docs/source/api/Apollo/classes/JSONRequest.md new file mode 100644 index 0000000000..be7a01096c --- /dev/null +++ b/docs/source/api/Apollo/classes/JSONRequest.md @@ -0,0 +1,106 @@ +**CLASS** + +# `JSONRequest` + +```swift +open class JSONRequest: HTTPRequest +``` + +> A request which sends JSON related to a GraphQL operation. + +## Properties +### `requestBodyCreator` + +```swift +public let requestBodyCreator: RequestBodyCreator +``` + +### `autoPersistQueries` + +```swift +public let autoPersistQueries: Bool +``` + +### `useGETForQueries` + +```swift +public let useGETForQueries: Bool +``` + +### `useGETForPersistedQueryRetry` + +```swift +public let useGETForPersistedQueryRetry: Bool +``` + +### `isPersistedQueryRetry` + +```swift +public var isPersistedQueryRetry = false +``` + +### `serializationFormat` + +```swift +public let serializationFormat = JSONSerializationFormat.self +``` + +### `sendOperationIdentifier` + +```swift +open var sendOperationIdentifier: Bool +``` + +## Methods +### `init(operation:graphQLEndpoint:contextIdentifier:clientName:clientVersion:additionalHeaders:cachePolicy:autoPersistQueries:useGETForQueries:useGETForPersistedQueryRetry:requestBodyCreator:)` + +```swift +public init(operation: Operation, + graphQLEndpoint: URL, + contextIdentifier: UUID? = nil, + clientName: String, + clientVersion: String, + additionalHeaders: [String: String] = [:], + cachePolicy: CachePolicy = .default, + autoPersistQueries: Bool = false, + useGETForQueries: Bool = false, + useGETForPersistedQueryRetry: Bool = false, + requestBodyCreator: RequestBodyCreator = ApolloRequestBodyCreator()) +``` + +> Designated initializer +> +> - Parameters: +> - operation: The GraphQL Operation to execute +> - graphQLEndpoint: The endpoint to make a GraphQL request to +> - contextIdentifier: [optional] A unique identifier for this request, to help with deduping cache hits for watchers. Defaults to `nil`. +> - clientName: The name of the client to send with the `"apollographql-client-name"` header +> - clientVersion: The version of the client to send with the `"apollographql-client-version"` header +> - additionalHeaders: Any additional headers you wish to add by default to this request +> - cachePolicy: The `CachePolicy` to use for this request. +> - autoPersistQueries: `true` if Auto-Persisted Queries should be used. Defaults to `false`. +> - useGETForQueries: `true` if Queries should use `GET` instead of `POST` for HTTP requests. Defaults to `false`. +> - useGETForPersistedQueryRetry: `true` if when an Auto-Persisted query is retried, it should use `GET` instead of `POST` to send the query. Defaults to `false`. +> - requestBodyCreator: An object conforming to the `RequestBodyCreator` protocol to assist with creating the request body. Defaults to the provided `ApolloRequestBodyCreator` implementation. + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| operation | The GraphQL Operation to execute | +| graphQLEndpoint | The endpoint to make a GraphQL request to | +| contextIdentifier | [optional] A unique identifier for this request, to help with deduping cache hits for watchers. Defaults to `nil`. | +| clientName | The name of the client to send with the `"apollographql-client-name"` header | +| clientVersion | The version of the client to send with the `"apollographql-client-version"` header | +| additionalHeaders | Any additional headers you wish to add by default to this request | +| cachePolicy | The `CachePolicy` to use for this request. | +| autoPersistQueries | `true` if Auto-Persisted Queries should be used. Defaults to `false`. | +| useGETForQueries | `true` if Queries should use `GET` instead of `POST` for HTTP requests. Defaults to `false`. | +| useGETForPersistedQueryRetry | `true` if when an Auto-Persisted query is retried, it should use `GET` instead of `POST` to send the query. Defaults to `false`. | +| requestBodyCreator | An object conforming to the `RequestBodyCreator` protocol to assist with creating the request body. Defaults to the provided `ApolloRequestBodyCreator` implementation. | + +### `toURLRequest()` + +```swift +open override func toURLRequest() throws -> URLRequest +``` diff --git a/docs/source/api/Apollo/classes/LegacyCacheReadInterceptor.md b/docs/source/api/Apollo/classes/LegacyCacheReadInterceptor.md new file mode 100644 index 0000000000..533b0b7d43 --- /dev/null +++ b/docs/source/api/Apollo/classes/LegacyCacheReadInterceptor.md @@ -0,0 +1,45 @@ +**CLASS** + +# `LegacyCacheReadInterceptor` + +```swift +public class LegacyCacheReadInterceptor: ApolloInterceptor +``` + +> An interceptor that reads data from the legacy cache for queries, following the `HTTPRequest`'s `cachePolicy`. + +## Methods +### `init(store:)` + +```swift +public init(store: ApolloStore) +``` + +> Designated initializer +> +> - Parameter store: The store to use when reading from the cache. + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| store | The store to use when reading from the cache. | + +### `interceptAsync(chain:request:response:completion:)` + +```swift +public func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) +``` + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| chain | The chain the interceptor is a part of. | +| request | The request, as far as it has been constructed | +| response | [optional] The response, if received | +| completion | The completion block to fire when data needs to be returned to the UI. | \ No newline at end of file diff --git a/docs/source/api/Apollo/classes/LegacyCacheWriteInterceptor.md b/docs/source/api/Apollo/classes/LegacyCacheWriteInterceptor.md new file mode 100644 index 0000000000..cbdcbdf06b --- /dev/null +++ b/docs/source/api/Apollo/classes/LegacyCacheWriteInterceptor.md @@ -0,0 +1,52 @@ +**CLASS** + +# `LegacyCacheWriteInterceptor` + +```swift +public class LegacyCacheWriteInterceptor: ApolloInterceptor +``` + +> An interceptor which writes data to the legacy cache, following the `HTTPRequest`'s `cachePolicy`. + +## Properties +### `store` + +```swift +public let store: ApolloStore +``` + +## Methods +### `init(store:)` + +```swift +public init(store: ApolloStore) +``` + +> Designated initializer +> +> - Parameter store: The store to use when writing to the cache. + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| store | The store to use when writing to the cache. | + +### `interceptAsync(chain:request:response:completion:)` + +```swift +public func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) +``` + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| chain | The chain the interceptor is a part of. | +| request | The request, as far as it has been constructed | +| response | [optional] The response, if received | +| completion | The completion block to fire when data needs to be returned to the UI. | \ No newline at end of file diff --git a/docs/source/api/Apollo/classes/LegacyInterceptorProvider.md b/docs/source/api/Apollo/classes/LegacyInterceptorProvider.md new file mode 100644 index 0000000000..1aed3fc5b8 --- /dev/null +++ b/docs/source/api/Apollo/classes/LegacyInterceptorProvider.md @@ -0,0 +1,51 @@ +**CLASS** + +# `LegacyInterceptorProvider` + +```swift +open class LegacyInterceptorProvider: InterceptorProvider +``` + +> The default interceptor provider for typescript-generated code + +## Methods +### `init(client:shouldInvalidateClientOnDeinit:store:)` + +```swift +public init(client: URLSessionClient = URLSessionClient(), + shouldInvalidateClientOnDeinit: Bool = true, + store: ApolloStore = ApolloStore()) +``` + +> Designated initializer +> +> - Parameters: +> - client: The `URLSessionClient` to use. Defaults to the default setup. +> - shouldInvalidateClientOnDeinit: If the passed-in client should be invalidated when this interceptor provider is deinitialized. If you are recreating the `URLSessionClient` every time you create a new provider, you should do this to prevent memory leaks. Defaults to true, since by default we provide a `URLSessionClient` to new instances. +> - store: The `ApolloStore` to use when reading from or writing to the cache. Defaults to the default initializer for ApolloStore. + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| client | The `URLSessionClient` to use. Defaults to the default setup. | +| shouldInvalidateClientOnDeinit | If the passed-in client should be invalidated when this interceptor provider is deinitialized. If you are recreating the `URLSessionClient` every time you create a new provider, you should do this to prevent memory leaks. Defaults to true, since by default we provide a `URLSessionClient` to new instances. | +| store | The `ApolloStore` to use when reading from or writing to the cache. Defaults to the default initializer for ApolloStore. | + +### `deinit` + +```swift +deinit +``` + +### `interceptors(for:)` + +```swift +open func interceptors(for operation: Operation) -> [ApolloInterceptor] +``` + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| operation | The operation to provide interceptors for | \ No newline at end of file diff --git a/docs/source/api/Apollo/classes/LegacyParsingInterceptor.md b/docs/source/api/Apollo/classes/LegacyParsingInterceptor.md new file mode 100644 index 0000000000..cf52a3b9f3 --- /dev/null +++ b/docs/source/api/Apollo/classes/LegacyParsingInterceptor.md @@ -0,0 +1,44 @@ +**CLASS** + +# `LegacyParsingInterceptor` + +```swift +public class LegacyParsingInterceptor: ApolloInterceptor +``` + +> An interceptor which parses code using the legacy parsing system. + +## Properties +### `cacheKeyForObject` + +```swift +public var cacheKeyForObject: CacheKeyForObject? +``` + +## Methods +### `init(cacheKeyForObject:)` + +```swift +public init(cacheKeyForObject: CacheKeyForObject? = nil) +``` + +> Designated Initializer + +### `interceptAsync(chain:request:response:completion:)` + +```swift +public func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) +``` + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| chain | The chain the interceptor is a part of. | +| request | The request, as far as it has been constructed | +| response | [optional] The response, if received | +| completion | The completion block to fire when data needs to be returned to the UI. | \ No newline at end of file diff --git a/docs/source/api/Apollo/classes/MaxRetryInterceptor.md b/docs/source/api/Apollo/classes/MaxRetryInterceptor.md new file mode 100644 index 0000000000..46bf22352c --- /dev/null +++ b/docs/source/api/Apollo/classes/MaxRetryInterceptor.md @@ -0,0 +1,45 @@ +**CLASS** + +# `MaxRetryInterceptor` + +```swift +public class MaxRetryInterceptor: ApolloInterceptor +``` + +> An interceptor to enforce a maximum number of retries of any `HTTPRequest` + +## Methods +### `init(maxRetriesAllowed:)` + +```swift +public init(maxRetriesAllowed: Int = 3) +``` + +> Designated initializer. +> +> - Parameter maxRetriesAllowed: How many times a query can be retried, in addition to the initial attempt before + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| maxRetriesAllowed | How many times a query can be retried, in addition to the initial attempt before | + +### `interceptAsync(chain:request:response:completion:)` + +```swift +public func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) +``` + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| chain | The chain the interceptor is a part of. | +| request | The request, as far as it has been constructed | +| response | [optional] The response, if received | +| completion | The completion block to fire when data needs to be returned to the UI. | \ No newline at end of file diff --git a/docs/source/api/Apollo/classes/NetworkFetchInterceptor.md b/docs/source/api/Apollo/classes/NetworkFetchInterceptor.md new file mode 100644 index 0000000000..3a57cff884 --- /dev/null +++ b/docs/source/api/Apollo/classes/NetworkFetchInterceptor.md @@ -0,0 +1,51 @@ +**CLASS** + +# `NetworkFetchInterceptor` + +```swift +public class NetworkFetchInterceptor: ApolloInterceptor, Cancellable +``` + +> An interceptor which actually fetches data from the network. + +## Methods +### `init(client:)` + +```swift +public init(client: URLSessionClient) +``` + +> Designated initializer. +> +> - Parameter client: The `URLSessionClient` to use to fetch data + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| client | The `URLSessionClient` to use to fetch data | + +### `interceptAsync(chain:request:response:completion:)` + +```swift +public func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) +``` + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| chain | The chain the interceptor is a part of. | +| request | The request, as far as it has been constructed | +| response | [optional] The response, if received | +| completion | The completion block to fire when data needs to be returned to the UI. | + +### `cancel()` + +```swift +public func cancel() +``` diff --git a/docs/source/api/Apollo/classes/RequestChain.md b/docs/source/api/Apollo/classes/RequestChain.md new file mode 100644 index 0000000000..ba73d677d5 --- /dev/null +++ b/docs/source/api/Apollo/classes/RequestChain.md @@ -0,0 +1,172 @@ +**CLASS** + +# `RequestChain` + +```swift +public class RequestChain: Cancellable +``` + +> A chain that allows a single network request to be created and executed. + +## Properties +### `isNotCancelled` + +```swift +public var isNotCancelled: Bool +``` + +> Checks the underlying value of `isCancelled`. Set up like this for better readability in `guard` statements + +### `additionalErrorHandler` + +```swift +public var additionalErrorHandler: ApolloErrorInterceptor? +``` + +> Something which allows additional error handling to occur when some kind of error has happened. + +## Methods +### `init(interceptors:callbackQueue:)` + +```swift +public init(interceptors: [ApolloInterceptor], + callbackQueue: DispatchQueue = .main) +``` + +> Creates a chain with the given interceptor array. +> +> - Parameters: +> - interceptors: The array of interceptors to use. +> - callbackQueue: The `DispatchQueue` to call back on when an error or result occurs. Defauls to `.main`. + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| interceptors | The array of interceptors to use. | +| callbackQueue | The `DispatchQueue` to call back on when an error or result occurs. Defauls to `.main`. | + +### `kickoff(request:completion:)` + +```swift +public func kickoff( + request: HTTPRequest, + completion: @escaping (Result, Error>) -> Void) +``` + +> Kicks off the request from the beginning of the interceptor array. +> +> - Parameters: +> - request: The request to send. +> - completion: The completion closure to call when the request has completed. + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| request | The request to send. | +| completion | The completion closure to call when the request has completed. | + +### `proceedAsync(request:response:completion:)` + +```swift +public func proceedAsync( + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) +``` + +> Proceeds to the next interceptor in the array. +> +> - Parameters: +> - request: The in-progress request object +> - response: [optional] The in-progress response object, if received yet +> - completion: The completion closure to call when data has been processed and should be returned to the UI. + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| request | The in-progress request object | +| response | [optional] The in-progress response object, if received yet | +| completion | The completion closure to call when data has been processed and should be returned to the UI. | + +### `cancel()` + +```swift +public func cancel() +``` + +> Cancels the entire chain of interceptors. + +### `retry(request:completion:)` + +```swift +public func retry( + request: HTTPRequest, + completion: @escaping (Result, Error>) -> Void) +``` + +> Restarts the request starting from the first inteceptor. +> +> - Parameters: +> - request: The request to retry +> - completion: The completion closure to call when the request has completed. + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| request | The request to retry | +| completion | The completion closure to call when the request has completed. | + +### `handleErrorAsync(_:request:response:completion:)` + +```swift +public func handleErrorAsync( + _ error: Error, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) +``` + +> Handles the error by returning it on the appropriate queue, or by applying an additional error interceptor if one has been provided. +> +> - Parameters: +> - error: The error to handle +> - request: The request, as far as it has been constructed. +> - response: The response, as far as it has been constructed. +> - completion: The completion closure to call when work is complete. + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| error | The error to handle | +| request | The request, as far as it has been constructed. | +| response | The response, as far as it has been constructed. | +| completion | The completion closure to call when work is complete. | + +### `returnValueAsync(for:value:completion:)` + +```swift +public func returnValueAsync( + for request: HTTPRequest, + value: GraphQLResult, + completion: @escaping (Result, Error>) -> Void) +``` + +> Handles a resulting value by returning it on the appropriate queue. +> +> - Parameters: +> - request: The request, as far as it has been constructed. +> - value: The value to be returned +> - completion: The completion closure to call when work is complete. + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| request | The request, as far as it has been constructed. | +| value | The value to be returned | +| completion | The completion closure to call when work is complete. | \ No newline at end of file diff --git a/docs/source/api/Apollo/classes/RequestChainNetworkTransport.md b/docs/source/api/Apollo/classes/RequestChainNetworkTransport.md new file mode 100644 index 0000000000..22d485c8c0 --- /dev/null +++ b/docs/source/api/Apollo/classes/RequestChainNetworkTransport.md @@ -0,0 +1,155 @@ +**CLASS** + +# `RequestChainNetworkTransport` + +```swift +open class RequestChainNetworkTransport: NetworkTransport +``` + +> An implementation of `NetworkTransport` which creates a `RequestChain` object +> for each item sent through it. + +## Properties +### `endpointURL` + +```swift +public let endpointURL: URL +``` + +> The GraphQL endpoint URL to use. + +### `additionalHeaders` + +```swift +public private(set) var additionalHeaders: [String: String] +``` + +> Any additional headers that should be automatically added to every request. + +### `autoPersistQueries` + +```swift +public let autoPersistQueries: Bool +``` + +> Set to `true` if Automatic Persisted Queries should be used to send a query hash instead of the full query body by default. + +### `useGETForQueries` + +```swift +public let useGETForQueries: Bool +``` + +> Set to `true` if you want to use `GET` instead of `POST` for queries, for example to take advantage of a CDN. + +### `useGETForPersistedQueryRetry` + +```swift +public let useGETForPersistedQueryRetry: Bool +``` + +> Set to `true` to use `GET` instead of `POST` for a retry of a persisted query. + +### `requestBodyCreator` + +```swift +public var requestBodyCreator: RequestBodyCreator +``` + +> The `RequestBodyCreator` object to use to build your `URLRequest`. + +### `clientName` + +```swift +public var clientName = RequestChainNetworkTransport.defaultClientName +``` + +### `clientVersion` + +```swift +public var clientVersion = RequestChainNetworkTransport.defaultClientVersion +``` + +## Methods +### `init(interceptorProvider:endpointURL:additionalHeaders:autoPersistQueries:requestBodyCreator:useGETForQueries:useGETForPersistedQueryRetry:)` + +```swift +public init(interceptorProvider: InterceptorProvider, + endpointURL: URL, + additionalHeaders: [String: String] = [:], + autoPersistQueries: Bool = false, + requestBodyCreator: RequestBodyCreator = ApolloRequestBodyCreator(), + useGETForQueries: Bool = false, + useGETForPersistedQueryRetry: Bool = false) +``` + +> Designated initializer +> +> - Parameters: +> - interceptorProvider: The interceptor provider to use when constructing chains for a request +> - endpointURL: The GraphQL endpoint URL to use. +> - additionalHeaders: Any additional headers that should be automatically added to every request. Defaults to an empty dictionary. +> - autoPersistQueries: Pass `true` if Automatic Persisted Queries should be used to send a query hash instead of the full query body by default. Defaults to `false`. +> - requestBodyCreator: The `RequestBodyCreator` object to use to build your `URLRequest`. Defaults to the providedd `ApolloRequestBodyCreator` implementation. +> - useGETForQueries: Pass `true` if you want to use `GET` instead of `POST` for queries, for example to take advantage of a CDN. Defaults to `false`. +> - useGETForPersistedQueryRetry: Pass `true` to use `GET` instead of `POST` for a retry of a persisted query. Defaults to `false`. + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| interceptorProvider | The interceptor provider to use when constructing chains for a request | +| endpointURL | The GraphQL endpoint URL to use. | +| additionalHeaders | Any additional headers that should be automatically added to every request. Defaults to an empty dictionary. | +| autoPersistQueries | Pass `true` if Automatic Persisted Queries should be used to send a query hash instead of the full query body by default. Defaults to `false`. | +| requestBodyCreator | The `RequestBodyCreator` object to use to build your `URLRequest`. Defaults to the providedd `ApolloRequestBodyCreator` implementation. | +| useGETForQueries | Pass `true` if you want to use `GET` instead of `POST` for queries, for example to take advantage of a CDN. Defaults to `false`. | +| useGETForPersistedQueryRetry | Pass `true` to use `GET` instead of `POST` for a retry of a persisted query. Defaults to `false`. | + +### `constructRequest(for:cachePolicy:contextIdentifier:)` + +```swift +open func constructRequest( + for operation: Operation, + cachePolicy: CachePolicy, + contextIdentifier: UUID? = nil) -> HTTPRequest +``` + +> Constructs a default (ie, non-multipart) GraphQL request. +> +> Override this method if you need to use a custom subclass of `HTTPRequest`. +> +> - Parameters: +> - operation: The operation to create the request for +> - cachePolicy: The `CachePolicy` to use when creating the request +> - contextIdentifier: [optional] A unique identifier for this request, to help with deduping cache hits for watchers. Should default to `nil`. +> - Returns: The constructed request. + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| operation | The operation to create the request for | +| cachePolicy | The `CachePolicy` to use when creating the request | +| contextIdentifier | [optional] A unique identifier for this request, to help with deduping cache hits for watchers. Should default to `nil`. | + +### `send(operation:cachePolicy:contextIdentifier:callbackQueue:completionHandler:)` + +```swift +public func send( + operation: Operation, + cachePolicy: CachePolicy = .default, + contextIdentifier: UUID? = nil, + callbackQueue: DispatchQueue = .main, + completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable +``` + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| operation | The operation to send. | +| cachePolicy | The `CachePolicy` to use making this request. | +| contextIdentifier | [optional] A unique identifier for this request, to help with deduping cache hits for watchers. Defaults to `nil`. | +| callbackQueue | The queue to call back on with the results. Should default to `.main`. | +| completionHandler | A closure to call when a request completes. On `success` will contain the response received from the server. On `failure` will contain the error which occurred. | \ No newline at end of file diff --git a/docs/source/api/Apollo/classes/ResponseCodeInterceptor.md b/docs/source/api/Apollo/classes/ResponseCodeInterceptor.md new file mode 100644 index 0000000000..45ea6ee438 --- /dev/null +++ b/docs/source/api/Apollo/classes/ResponseCodeInterceptor.md @@ -0,0 +1,37 @@ +**CLASS** + +# `ResponseCodeInterceptor` + +```swift +public class ResponseCodeInterceptor: ApolloInterceptor +``` + +> An interceptor to check the response code returned with a request. + +## Methods +### `init()` + +```swift +public init() +``` + +> Designated initializer + +### `interceptAsync(chain:request:response:completion:)` + +```swift +public func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) +``` + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| chain | The chain the interceptor is a part of. | +| request | The request, as far as it has been constructed | +| response | [optional] The response, if received | +| completion | The completion block to fire when data needs to be returned to the UI. | \ No newline at end of file diff --git a/docs/source/api/Apollo/classes/UploadRequest.md b/docs/source/api/Apollo/classes/UploadRequest.md new file mode 100644 index 0000000000..5244824040 --- /dev/null +++ b/docs/source/api/Apollo/classes/UploadRequest.md @@ -0,0 +1,92 @@ +**CLASS** + +# `UploadRequest` + +```swift +open class UploadRequest: HTTPRequest +``` + +> A request class allowing for a multipart-upload request. + +## Properties +### `requestBodyCreator` + +```swift +public let requestBodyCreator: RequestBodyCreator +``` + +### `files` + +```swift +public let files: [GraphQLFile] +``` + +### `manualBoundary` + +```swift +public let manualBoundary: String? +``` + +### `serializationFormat` + +```swift +public let serializationFormat = JSONSerializationFormat.self +``` + +## Methods +### `init(graphQLEndpoint:operation:clientName:clientVersion:additionalHeaders:files:manualBoundary:requestBodyCreator:)` + +```swift +public init(graphQLEndpoint: URL, + operation: Operation, + clientName: String, + clientVersion: String, + additionalHeaders: [String: String] = [:], + files: [GraphQLFile], + manualBoundary: String? = nil, + requestBodyCreator: RequestBodyCreator = ApolloRequestBodyCreator()) +``` + +> Designated Initializer +> +> - Parameters: +> - graphQLEndpoint: The endpoint to make a GraphQL request to +> - operation: The GraphQL Operation to execute +> - clientName: The name of the client to send with the `"apollographql-client-name"` header +> - clientVersion: The version of the client to send with the `"apollographql-client-version"` header +> - additionalHeaders: Any additional headers you wish to add by default to this request. Defaults to an empty dictionary. +> - files: The array of files to upload for all `Upload` parameters in the mutation. +> - manualBoundary: [optional] A manual boundary to pass in. A default boundary will be used otherwise. Defaults to nil. +> - requestBodyCreator: An object conforming to the `RequestBodyCreator` protocol to assist with creating the request body. Defaults to the provided `ApolloRequestBodyCreator` implementation. + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| graphQLEndpoint | The endpoint to make a GraphQL request to | +| operation | The GraphQL Operation to execute | +| clientName | The name of the client to send with the `"apollographql-client-name"` header | +| clientVersion | The version of the client to send with the `"apollographql-client-version"` header | +| additionalHeaders | Any additional headers you wish to add by default to this request. Defaults to an empty dictionary. | +| files | The array of files to upload for all `Upload` parameters in the mutation. | +| manualBoundary | [optional] A manual boundary to pass in. A default boundary will be used otherwise. Defaults to nil. | +| requestBodyCreator | An object conforming to the `RequestBodyCreator` protocol to assist with creating the request body. Defaults to the provided `ApolloRequestBodyCreator` implementation. | + +### `toURLRequest()` + +```swift +public override func toURLRequest() throws -> URLRequest +``` + +### `requestMultipartFormData()` + +```swift +open func requestMultipartFormData() throws -> MultipartFormData +``` + +> Creates the `MultipartFormData` object to use when creating the URL Request. +> +> This method follows the [GraphQL Multipart Request Spec](https://github.com/jaydenseric/graphql-multipart-request-spec) Override this method to use a different upload spec. +> +> - Throws: Any error arising from creating the form data +> - Returns: The created form data diff --git a/docs/source/api/Apollo/enums/AutomaticPersistedQueryInterceptor.APQError.md b/docs/source/api/Apollo/enums/AutomaticPersistedQueryInterceptor.APQError.md new file mode 100644 index 0000000000..c2b3acbb61 --- /dev/null +++ b/docs/source/api/Apollo/enums/AutomaticPersistedQueryInterceptor.APQError.md @@ -0,0 +1,27 @@ +**ENUM** + +# `AutomaticPersistedQueryInterceptor.APQError` + +```swift +public enum APQError: LocalizedError +``` + +## Cases +### `noParsedResponse` + +```swift +case noParsedResponse +``` + +### `persistedQueryRetryFailed(operationName:)` + +```swift +case persistedQueryRetryFailed(operationName: String) +``` + +## Properties +### `errorDescription` + +```swift +public var errorDescription: String? +``` diff --git a/docs/source/api/Apollo/enums/CodableParsingInterceptor.CodableParsingError.md b/docs/source/api/Apollo/enums/CodableParsingInterceptor.CodableParsingError.md new file mode 100644 index 0000000000..aa4868d4a8 --- /dev/null +++ b/docs/source/api/Apollo/enums/CodableParsingInterceptor.CodableParsingError.md @@ -0,0 +1,21 @@ +**ENUM** + +# `CodableParsingInterceptor.CodableParsingError` + +```swift +public enum CodableParsingError: Error, LocalizedError +``` + +## Cases +### `noResponseToParse` + +```swift +case noResponseToParse +``` + +## Properties +### `errorDescription` + +```swift +public var errorDescription: String? +``` diff --git a/docs/source/api/Apollo/enums/GraphQLHTTPRequestError.md b/docs/source/api/Apollo/enums/GraphQLHTTPRequestError.md index 69247230da..2b17bc24d2 100644 --- a/docs/source/api/Apollo/enums/GraphQLHTTPRequestError.md +++ b/docs/source/api/Apollo/enums/GraphQLHTTPRequestError.md @@ -9,12 +9,6 @@ public enum GraphQLHTTPRequestError: Error, LocalizedError > An error which has occurred during the serialization of a request. ## Cases -### `cancelledByDelegate` - -```swift -case cancelledByDelegate -``` - ### `serializedBodyMessageError` ```swift diff --git a/docs/source/api/Apollo/enums/HTTPNetworkTransport.ContinueAction.md b/docs/source/api/Apollo/enums/HTTPNetworkTransport.ContinueAction.md deleted file mode 100644 index 92814c90bd..0000000000 --- a/docs/source/api/Apollo/enums/HTTPNetworkTransport.ContinueAction.md +++ /dev/null @@ -1,26 +0,0 @@ -**ENUM** - -# `HTTPNetworkTransport.ContinueAction` - -```swift -public enum ContinueAction -``` - -> The action to take when retrying - -## Cases -### `retry` - -```swift -case retry -``` - -> Directly retry the action - -### `fail(_:)` - -```swift -case fail(_ error: Error) -``` - -> Fail with the specified error. diff --git a/docs/source/api/Apollo/enums/LegacyCacheWriteInterceptor.LegacyCacheWriteError.md b/docs/source/api/Apollo/enums/LegacyCacheWriteInterceptor.LegacyCacheWriteError.md new file mode 100644 index 0000000000..351e8001e3 --- /dev/null +++ b/docs/source/api/Apollo/enums/LegacyCacheWriteInterceptor.LegacyCacheWriteError.md @@ -0,0 +1,21 @@ +**ENUM** + +# `LegacyCacheWriteInterceptor.LegacyCacheWriteError` + +```swift +public enum LegacyCacheWriteError: Error, LocalizedError +``` + +## Cases +### `noResponseToParse` + +```swift +case noResponseToParse +``` + +## Properties +### `errorDescription` + +```swift +public var errorDescription: String? +``` diff --git a/docs/source/api/Apollo/enums/LegacyParsingInterceptor.LegacyParsingError.md b/docs/source/api/Apollo/enums/LegacyParsingInterceptor.LegacyParsingError.md new file mode 100644 index 0000000000..e35d546717 --- /dev/null +++ b/docs/source/api/Apollo/enums/LegacyParsingInterceptor.LegacyParsingError.md @@ -0,0 +1,27 @@ +**ENUM** + +# `LegacyParsingInterceptor.LegacyParsingError` + +```swift +public enum LegacyParsingError: Error, LocalizedError +``` + +## Cases +### `noResponseToParse` + +```swift +case noResponseToParse +``` + +### `couldNotParseToLegacyJSON(data:)` + +```swift +case couldNotParseToLegacyJSON(data: Data) +``` + +## Properties +### `errorDescription` + +```swift +public var errorDescription: String? +``` diff --git a/docs/source/api/Apollo/enums/MaxRetryInterceptor.RetryError.md b/docs/source/api/Apollo/enums/MaxRetryInterceptor.RetryError.md new file mode 100644 index 0000000000..471d63e234 --- /dev/null +++ b/docs/source/api/Apollo/enums/MaxRetryInterceptor.RetryError.md @@ -0,0 +1,21 @@ +**ENUM** + +# `MaxRetryInterceptor.RetryError` + +```swift +public enum RetryError: Error, LocalizedError +``` + +## Cases +### `hitMaxRetryCount(count:operationName:)` + +```swift +case hitMaxRetryCount(count: Int, operationName: String) +``` + +## Properties +### `errorDescription` + +```swift +public var errorDescription: String? +``` diff --git a/docs/source/api/Apollo/enums/ParseableError.md b/docs/source/api/Apollo/enums/ParseableError.md new file mode 100644 index 0000000000..8ae7d21b6e --- /dev/null +++ b/docs/source/api/Apollo/enums/ParseableError.md @@ -0,0 +1,26 @@ +**ENUM** + +# `ParseableError` + +```swift +public enum ParseableError: Error +``` + +## Cases +### `unexpectedType` + +```swift +case unexpectedType +``` + +### `unsupportedInitializer` + +```swift +case unsupportedInitializer +``` + +### `notYetImplemented` + +```swift +case notYetImplemented +``` diff --git a/docs/source/api/Apollo/enums/RequestChain.ChainError.md b/docs/source/api/Apollo/enums/RequestChain.ChainError.md new file mode 100644 index 0000000000..70d501464f --- /dev/null +++ b/docs/source/api/Apollo/enums/RequestChain.ChainError.md @@ -0,0 +1,27 @@ +**ENUM** + +# `RequestChain.ChainError` + +```swift +public enum ChainError: Error, LocalizedError +``` + +## Cases +### `invalidIndex(chain:index:)` + +```swift +case invalidIndex(chain: RequestChain, index: Int) +``` + +### `noInterceptors` + +```swift +case noInterceptors +``` + +## Properties +### `errorDescription` + +```swift +public var errorDescription: String? +``` diff --git a/docs/source/api/Apollo/enums/ResponseCodeInterceptor.ResponseCodeError.md b/docs/source/api/Apollo/enums/ResponseCodeInterceptor.ResponseCodeError.md new file mode 100644 index 0000000000..aae6ca7d1a --- /dev/null +++ b/docs/source/api/Apollo/enums/ResponseCodeInterceptor.ResponseCodeError.md @@ -0,0 +1,21 @@ +**ENUM** + +# `ResponseCodeInterceptor.ResponseCodeError` + +```swift +public enum ResponseCodeError: Error, LocalizedError +``` + +## Cases +### `invalidResponseCode(response:rawData:)` + +```swift +case invalidResponseCode(response: HTTPURLResponse?, rawData: Data?) +``` + +## Properties +### `errorDescription` + +```swift +public var errorDescription: String? +``` diff --git a/docs/source/api/Apollo/extensions/ApolloClient.md b/docs/source/api/Apollo/extensions/ApolloClient.md index 2e0ef84616..3c0af9fd4b 100644 --- a/docs/source/api/Apollo/extensions/ApolloClient.md +++ b/docs/source/api/Apollo/extensions/ApolloClient.md @@ -16,7 +16,8 @@ public var cacheKeyForObject: CacheKeyForObject? ### `clearCache(callbackQueue:completion:)` ```swift -public func clearCache(callbackQueue: DispatchQueue = .main, completion: ((Result) -> Void)? = nil) +public func clearCache(callbackQueue: DispatchQueue = .main, + completion: ((Result) -> Void)? = nil) ``` #### Parameters @@ -26,12 +27,12 @@ public func clearCache(callbackQueue: DispatchQueue = .main, completion: ((Resul | callbackQueue | The queue to fall back on. Should default to the main queue. | | completion | [optional] A completion closure to execute when clearing has completed. Should default to nil. | -### `fetch(query:cachePolicy:context:queue:resultHandler:)` +### `fetch(query:cachePolicy:contextIdentifier:queue:resultHandler:)` ```swift @discardableResult public func fetch(query: Query, cachePolicy: CachePolicy = .returnCacheDataElseFetch, - context: UnsafeMutableRawPointer? = nil, + contextIdentifier: UUID? = nil, queue: DispatchQueue = DispatchQueue.main, resultHandler: GraphQLResultHandler? = nil) -> Cancellable ``` @@ -42,16 +43,15 @@ public func clearCache(callbackQueue: DispatchQueue = .main, completion: ((Resul | ---- | ----------- | | query | The query to fetch. | | cachePolicy | A cache policy that specifies when results should be fetched from the server and when data should be loaded from the local cache. | -| context | [optional] A context to use for the cache to work with results. Should default to nil. | | queue | A dispatch queue on which the result handler will be called. Defaults to the main queue. | +| contextIdentifier | [optional] A unique identifier for this request, to help with deduping cache hits for watchers. Should default to `nil`. | | resultHandler | [optional] A closure that is called when query results are available or when an error occurs. | -### `watch(query:cachePolicy:queue:resultHandler:)` +### `watch(query:cachePolicy:resultHandler:)` ```swift public func watch(query: Query, cachePolicy: CachePolicy = .returnCacheDataElseFetch, - queue: DispatchQueue = .main, resultHandler: @escaping GraphQLResultHandler) -> GraphQLQueryWatcher ``` @@ -60,17 +60,14 @@ public func watch(query: Query, | Name | Description | | ---- | ----------- | | query | The query to fetch. | -| fetchHTTPMethod | The HTTP Method to be used. | | cachePolicy | A cache policy that specifies when results should be fetched from the server or from the local cache. | -| queue | A dispatch queue on which the result handler will be called. Should default to the main queue. | | resultHandler | [optional] A closure that is called when query results are available or when an error occurs. | -### `perform(mutation:context:queue:resultHandler:)` +### `perform(mutation:queue:resultHandler:)` ```swift public func perform(mutation: Mutation, - context: UnsafeMutableRawPointer? = nil, - queue: DispatchQueue = DispatchQueue.main, + queue: DispatchQueue = .main, resultHandler: GraphQLResultHandler? = nil) -> Cancellable ``` @@ -79,15 +76,13 @@ public func perform(mutation: Mutation, | Name | Description | | ---- | ----------- | | mutation | The mutation to perform. | -| context | [optional] A context to use for the cache to work with results. Should default to nil. | | queue | A dispatch queue on which the result handler will be called. Defaults to the main queue. | | resultHandler | An optional closure that is called when mutation results are available or when an error occurs. | -### `upload(operation:context:files:queue:resultHandler:)` +### `upload(operation:files:queue:resultHandler:)` ```swift public func upload(operation: Operation, - context: UnsafeMutableRawPointer? = nil, files: [GraphQLFile], queue: DispatchQueue = .main, resultHandler: GraphQLResultHandler? = nil) -> Cancellable @@ -98,10 +93,9 @@ public func upload(operation: Operation, | Name | Description | | ---- | ----------- | | operation | The operation to send | -| context | [optional] A context to use for the cache to work with results. Should default to nil. | | files | An array of `GraphQLFile` objects to send. | | queue | A dispatch queue on which the result handler will be called. Should default to the main queue. | -| completionHandler | The completion handler to execute when the request completes or errors | +| completionHandler | The completion handler to execute when the request completes or errors. Note that an error will be returned If your `networkTransport` does not also conform to `UploadingNetworkTransport`. | ### `subscribe(subscription:queue:resultHandler:)` diff --git a/docs/source/api/Apollo/extensions/GraphQLResult.md b/docs/source/api/Apollo/extensions/GraphQLResult.md new file mode 100644 index 0000000000..ebfbf5f37d --- /dev/null +++ b/docs/source/api/Apollo/extensions/GraphQLResult.md @@ -0,0 +1,13 @@ +**EXTENSION** + +# `GraphQLResult` +```swift +extension GraphQLResult where Data: Decodable +``` + +## Methods +### `init(from:decoder:)` + +```swift +public init(from data: Foundation.Data, decoder: T) throws +``` diff --git a/docs/source/api/Apollo/extensions/HTTPNetworkTransport.md b/docs/source/api/Apollo/extensions/HTTPNetworkTransport.md deleted file mode 100644 index c5677f73d3..0000000000 --- a/docs/source/api/Apollo/extensions/HTTPNetworkTransport.md +++ /dev/null @@ -1,49 +0,0 @@ -**EXTENSION** - -# `HTTPNetworkTransport` -```swift -extension HTTPNetworkTransport: NetworkTransport -``` - -## Methods -### `send(operation:completionHandler:)` - -```swift -public func send(operation: Operation, completionHandler: @escaping (_ result: Result, Error>) -> Void) -> Cancellable -``` - -#### Parameters - -| Name | Description | -| ---- | ----------- | -| operation | The operation to send. | -| completionHandler | A closure to call when a request completes. On `success` will contain the response received from the server. On `failure` will contain the error which occurred. | - -### `upload(operation:files:completionHandler:)` - -```swift -public func upload(operation: Operation, - files: [GraphQLFile], - completionHandler: @escaping (_ result: Result, Error>) -> Void) -> Cancellable -``` - -#### Parameters - -| Name | Description | -| ---- | ----------- | -| operation | The operation to send | -| files | An array of `GraphQLFile` objects to send. | -| completionHandler | The completion handler to execute when the request completes or errors | - -### `==(_:_:)` - -```swift -public static func ==(lhs: HTTPNetworkTransport, rhs: HTTPNetworkTransport) -> Bool -``` - -#### Parameters - -| Name | Description | -| ---- | ----------- | -| lhs | A value to compare. | -| rhs | Another value to compare. | \ No newline at end of file diff --git a/docs/source/api/Apollo/extensions/HTTPRequest.md b/docs/source/api/Apollo/extensions/HTTPRequest.md new file mode 100644 index 0000000000..0322684185 --- /dev/null +++ b/docs/source/api/Apollo/extensions/HTTPRequest.md @@ -0,0 +1,27 @@ +**EXTENSION** + +# `HTTPRequest` +```swift +extension HTTPRequest: Equatable +``` + +## Properties +### `debugDescription` + +```swift +public var debugDescription: String +``` + +## Methods +### `==(_:_:)` + +```swift +public static func == (lhs: HTTPRequest, rhs: HTTPRequest) -> Bool +``` + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| lhs | A value to compare. | +| rhs | Another value to compare. | \ No newline at end of file diff --git a/docs/source/api/Apollo/extensions/InterceptorProvider.md b/docs/source/api/Apollo/extensions/InterceptorProvider.md new file mode 100644 index 0000000000..2fff879665 --- /dev/null +++ b/docs/source/api/Apollo/extensions/InterceptorProvider.md @@ -0,0 +1,19 @@ +**EXTENSION** + +# `InterceptorProvider` +```swift +public extension InterceptorProvider +``` + +## Methods +### `additionalErrorInterceptor(for:)` + +```swift +func additionalErrorInterceptor(for operation: Operation) -> ApolloErrorInterceptor? +``` + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| operation | The oper | \ No newline at end of file diff --git a/docs/source/api/Apollo/extensions/Parseable.md b/docs/source/api/Apollo/extensions/Parseable.md new file mode 100644 index 0000000000..74147279e9 --- /dev/null +++ b/docs/source/api/Apollo/extensions/Parseable.md @@ -0,0 +1,20 @@ +**EXTENSION** + +# `Parseable` +```swift +public extension Parseable where Self: Decodable +``` + +## Methods +### `init(from:decoder:)` + +```swift +init(from data: Data, decoder: T) throws +``` + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| data | The data to decode | +| decoder | The decoder to use to decode it | \ No newline at end of file diff --git a/docs/source/api/Apollo/extensions/RequestBodyCreator.md b/docs/source/api/Apollo/extensions/RequestBodyCreator.md new file mode 100644 index 0000000000..2988a24e0d --- /dev/null +++ b/docs/source/api/Apollo/extensions/RequestBodyCreator.md @@ -0,0 +1,34 @@ +**EXTENSION** + +# `RequestBodyCreator` +```swift +extension RequestBodyCreator +``` + +## Methods +### `requestBody(for:sendOperationIdentifiers:sendQueryDocument:autoPersistQuery:)` + +```swift +public func requestBody(for operation: Operation, + sendOperationIdentifiers: Bool = false, + sendQueryDocument: Bool = true, + autoPersistQuery: Bool = false) -> GraphQLMap +``` + +> Creates a `GraphQLMap` out of the passed-in operation +> +> - Parameters: +> - operation: The operation to use +> - sendOperationIdentifiers: Whether or not to send operation identifiers. Defaults to false. +> - sendQueryDocument: Whether or not to send the full query document. Defaults to true. +> - autoPersistQuery: Whether to use auto-persisted query information. Defaults to false. +> - Returns: The created `GraphQLMap` + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| operation | The operation to use | +| sendOperationIdentifiers | Whether or not to send operation identifiers. Defaults to false. | +| sendQueryDocument | Whether or not to send the full query document. Defaults to true. | +| autoPersistQuery | Whether to use auto-persisted query information. Defaults to false. | \ No newline at end of file diff --git a/docs/source/api/Apollo/extensions/RequestChainNetworkTransport.md b/docs/source/api/Apollo/extensions/RequestChainNetworkTransport.md new file mode 100644 index 0000000000..5c49424870 --- /dev/null +++ b/docs/source/api/Apollo/extensions/RequestChainNetworkTransport.md @@ -0,0 +1,50 @@ +**EXTENSION** + +# `RequestChainNetworkTransport` +```swift +extension RequestChainNetworkTransport: UploadingNetworkTransport +``` + +## Methods +### `constructUploadRequest(for:with:)` + +```swift +open func constructUploadRequest( + for operation: Operation, + with files: [GraphQLFile]) -> HTTPRequest +``` + +> Constructs an uploading (ie, multipart) GraphQL request +> +> Override this method if you need to use a custom subclass of `HTTPRequest`. +> +> - Parameters: +> - operation: The operation to create a request for +> - files: The files you wish to upload +> - Returns: The created request. + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| operation | The operation to create a request for | +| files | The files you wish to upload | + +### `upload(operation:files:callbackQueue:completionHandler:)` + +```swift +public func upload( + operation: Operation, + files: [GraphQLFile], + callbackQueue: DispatchQueue = .main, + completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable +``` + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| operation | The operation to send | +| files | An array of `GraphQLFile` objects to send. | +| callbackQueue | The queue to call back on with the results. Should default to `.main`. | +| completionHandler | The completion handler to execute when the request completes or errors | \ No newline at end of file diff --git a/docs/source/api/Apollo/extensions/RequestCreator.md b/docs/source/api/Apollo/extensions/RequestCreator.md deleted file mode 100644 index 4ae9ffa371..0000000000 --- a/docs/source/api/Apollo/extensions/RequestCreator.md +++ /dev/null @@ -1,65 +0,0 @@ -**EXTENSION** - -# `RequestCreator` -```swift -extension RequestCreator -``` - -## Methods -### `requestBody(for:sendOperationIdentifiers:sendQueryDocument:autoPersistQuery:)` - -```swift -public func requestBody(for operation: Operation, - sendOperationIdentifiers: Bool = false, - sendQueryDocument: Bool = true, - autoPersistQuery: Bool = false) -> GraphQLMap -``` - -> Creates a `GraphQLMap` out of the passed-in operation -> -> - Parameters: -> - operation: The operation to use -> - sendOperationIdentifiers: Whether or not to send operation identifiers. Defaults to false. -> - sendQueryDocument: Whether or not to send the full query document. Defaults to true. -> - autoPersistQuery: Whether to use auto-persisted query information. Defaults to false. -> - Returns: The created `GraphQLMap` - -#### Parameters - -| Name | Description | -| ---- | ----------- | -| operation | The operation to use | -| sendOperationIdentifiers | Whether or not to send operation identifiers. Defaults to false. | -| sendQueryDocument | Whether or not to send the full query document. Defaults to true. | -| autoPersistQuery | Whether to use auto-persisted query information. Defaults to false. | - -### `requestMultipartFormData(for:files:sendOperationIdentifiers:serializationFormat:manualBoundary:)` - -```swift -public func requestMultipartFormData(for operation: Operation, - files: [GraphQLFile], - sendOperationIdentifiers: Bool, - serializationFormat: JSONSerializationFormat.Type, - manualBoundary: String?) throws -> MultipartFormData -``` - -> Creates multi-part form data to send with a request -> -> - Parameters: -> - operation: The operation to create the data for. -> - files: An array of files to use. -> - sendOperationIdentifiers: True if operation identifiers should be sent, false if not. -> - serializationFormat: The format to use to serialize data. -> - manualBoundary: [optional] A manual boundary to pass in. A default boundary will be used otherwise. -> - Returns: The created form data -> - Throws: Errors creating or loading the form data - -#### Parameters - -| Name | Description | -| ---- | ----------- | -| operation | The operation to create the data for. | -| files | An array of files to use. | -| sendOperationIdentifiers | True if operation identifiers should be sent, false if not. | -| serializationFormat | The format to use to serialize data. | -| manualBoundary | [optional] A manual boundary to pass in. A default boundary will be used otherwise. | \ No newline at end of file diff --git a/docs/source/api/Apollo/protocols/ApolloClientProtocol.md b/docs/source/api/Apollo/protocols/ApolloClientProtocol.md index 618efd3dba..30a9862566 100644 --- a/docs/source/api/Apollo/protocols/ApolloClientProtocol.md +++ b/docs/source/api/Apollo/protocols/ApolloClientProtocol.md @@ -46,12 +46,12 @@ func clearCache(callbackQueue: DispatchQueue, completion: ((Result) | callbackQueue | The queue to fall back on. Should default to the main queue. | | completion | [optional] A completion closure to execute when clearing has completed. Should default to nil. | -### `fetch(query:cachePolicy:context:queue:resultHandler:)` +### `fetch(query:cachePolicy:contextIdentifier:queue:resultHandler:)` ```swift func fetch(query: Query, cachePolicy: CachePolicy, - context: UnsafeMutableRawPointer?, + contextIdentifier: UUID?, queue: DispatchQueue, resultHandler: GraphQLResultHandler?) -> Cancellable ``` @@ -61,8 +61,8 @@ func fetch(query: Query, > - Parameters: > - query: The query to fetch. > - cachePolicy: A cache policy that specifies when results should be fetched from the server and when data should be loaded from the local cache. -> - context: [optional] A context to use for the cache to work with results. Should default to nil. > - queue: A dispatch queue on which the result handler will be called. Defaults to the main queue. +> - contextIdentifier: [optional] A unique identifier for this request, to help with deduping cache hits for watchers. Should default to `nil`. > - resultHandler: [optional] A closure that is called when query results are available or when an error occurs. > - Returns: An object that can be used to cancel an in progress fetch. @@ -72,16 +72,15 @@ func fetch(query: Query, | ---- | ----------- | | query | The query to fetch. | | cachePolicy | A cache policy that specifies when results should be fetched from the server and when data should be loaded from the local cache. | -| context | [optional] A context to use for the cache to work with results. Should default to nil. | | queue | A dispatch queue on which the result handler will be called. Defaults to the main queue. | +| contextIdentifier | [optional] A unique identifier for this request, to help with deduping cache hits for watchers. Should default to `nil`. | | resultHandler | [optional] A closure that is called when query results are available or when an error occurs. | -### `watch(query:cachePolicy:queue:resultHandler:)` +### `watch(query:cachePolicy:resultHandler:)` ```swift func watch(query: Query, cachePolicy: CachePolicy, - queue: DispatchQueue, resultHandler: @escaping GraphQLResultHandler) -> GraphQLQueryWatcher ``` @@ -89,9 +88,7 @@ func watch(query: Query, > > - Parameters: > - query: The query to fetch. -> - fetchHTTPMethod: The HTTP Method to be used. > - cachePolicy: A cache policy that specifies when results should be fetched from the server or from the local cache. -> - queue: A dispatch queue on which the result handler will be called. Should default to the main queue. > - resultHandler: [optional] A closure that is called when query results are available or when an error occurs. > - Returns: A query watcher object that can be used to control the watching behavior. @@ -100,16 +97,13 @@ func watch(query: Query, | Name | Description | | ---- | ----------- | | query | The query to fetch. | -| fetchHTTPMethod | The HTTP Method to be used. | | cachePolicy | A cache policy that specifies when results should be fetched from the server or from the local cache. | -| queue | A dispatch queue on which the result handler will be called. Should default to the main queue. | | resultHandler | [optional] A closure that is called when query results are available or when an error occurs. | -### `perform(mutation:context:queue:resultHandler:)` +### `perform(mutation:queue:resultHandler:)` ```swift func perform(mutation: Mutation, - context: UnsafeMutableRawPointer?, queue: DispatchQueue, resultHandler: GraphQLResultHandler?) -> Cancellable ``` @@ -118,7 +112,6 @@ func perform(mutation: Mutation, > > - Parameters: > - mutation: The mutation to perform. -> - context: [optional] A context to use for the cache to work with results. Should default to nil. > - queue: A dispatch queue on which the result handler will be called. Defaults to the main queue. > - resultHandler: An optional closure that is called when mutation results are available or when an error occurs. > - Returns: An object that can be used to cancel an in progress mutation. @@ -128,15 +121,13 @@ func perform(mutation: Mutation, | Name | Description | | ---- | ----------- | | mutation | The mutation to perform. | -| context | [optional] A context to use for the cache to work with results. Should default to nil. | | queue | A dispatch queue on which the result handler will be called. Defaults to the main queue. | | resultHandler | An optional closure that is called when mutation results are available or when an error occurs. | -### `upload(operation:context:files:queue:resultHandler:)` +### `upload(operation:files:queue:resultHandler:)` ```swift func upload(operation: Operation, - context: UnsafeMutableRawPointer?, files: [GraphQLFile], queue: DispatchQueue, resultHandler: GraphQLResultHandler?) -> Cancellable @@ -146,22 +137,19 @@ func upload(operation: Operation, > > - Parameters: > - operation: The operation to send -> - context: [optional] A context to use for the cache to work with results. Should default to nil. > - files: An array of `GraphQLFile` objects to send. > - queue: A dispatch queue on which the result handler will be called. Should default to the main queue. -> - completionHandler: The completion handler to execute when the request completes or errors +> - completionHandler: The completion handler to execute when the request completes or errors. Note that an error will be returned If your `networkTransport` does not also conform to `UploadingNetworkTransport`. > - Returns: An object that can be used to cancel an in progress request. -> - Throws: If your `networkTransport` does not also conform to `UploadingNetworkTransport`. #### Parameters | Name | Description | | ---- | ----------- | | operation | The operation to send | -| context | [optional] A context to use for the cache to work with results. Should default to nil. | | files | An array of `GraphQLFile` objects to send. | | queue | A dispatch queue on which the result handler will be called. Should default to the main queue. | -| completionHandler | The completion handler to execute when the request completes or errors | +| completionHandler | The completion handler to execute when the request completes or errors. Note that an error will be returned If your `networkTransport` does not also conform to `UploadingNetworkTransport`. | ### `subscribe(subscription:queue:resultHandler:)` diff --git a/docs/source/api/Apollo/protocols/ApolloErrorInterceptor.md b/docs/source/api/Apollo/protocols/ApolloErrorInterceptor.md new file mode 100644 index 0000000000..0ec4337bf3 --- /dev/null +++ b/docs/source/api/Apollo/protocols/ApolloErrorInterceptor.md @@ -0,0 +1,40 @@ +**PROTOCOL** + +# `ApolloErrorInterceptor` + +```swift +public protocol ApolloErrorInterceptor +``` + +> An error interceptor called to allow further examination of error data when an error occurs in the chain. + +## Methods +### `handleErrorAsync(error:chain:request:response:completion:)` + +```swift +func handleErrorAsync( + error: Error, + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) +``` + +> Asynchronously handles the receipt of an error at any point in the chain. +> +> - Parameters: +> - error: The received error +> - chain: The chain the error was received on +> - request: The request, as far as it was constructed +> - response: [optional] The response, if one was received +> - completion: The completion closure to fire when the operation has completed. Note that if you call `retry` on the chain, you will not want to call the completion block in this method. + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| error | The received error | +| chain | The chain the error was received on | +| request | The request, as far as it was constructed | +| response | [optional] The response, if one was received | +| completion | The completion closure to fire when the operation has completed. Note that if you call `retry` on the chain, you will not want to call the completion block in this method. | \ No newline at end of file diff --git a/docs/source/api/Apollo/protocols/ApolloInterceptor.md b/docs/source/api/Apollo/protocols/ApolloInterceptor.md new file mode 100644 index 0000000000..2bde5267e4 --- /dev/null +++ b/docs/source/api/Apollo/protocols/ApolloInterceptor.md @@ -0,0 +1,37 @@ +**PROTOCOL** + +# `ApolloInterceptor` + +```swift +public protocol ApolloInterceptor: class +``` + +> A protocol to set up a chainable unit of networking work. + +## Methods +### `interceptAsync(chain:request:response:completion:)` + +```swift +func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) +``` + +> Called when this interceptor should do its work. +> +> - Parameters: +> - chain: The chain the interceptor is a part of. +> - request: The request, as far as it has been constructed +> - response: [optional] The response, if received +> - completion: The completion block to fire when data needs to be returned to the UI. + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| chain | The chain the interceptor is a part of. | +| request | The request, as far as it has been constructed | +| response | [optional] The response, if received | +| completion | The completion block to fire when data needs to be returned to the UI. | \ No newline at end of file diff --git a/docs/source/api/Apollo/protocols/FlexibleDecoder.md b/docs/source/api/Apollo/protocols/FlexibleDecoder.md new file mode 100644 index 0000000000..296f5b20ca --- /dev/null +++ b/docs/source/api/Apollo/protocols/FlexibleDecoder.md @@ -0,0 +1,14 @@ +**PROTOCOL** + +# `FlexibleDecoder` + +```swift +public protocol FlexibleDecoder +``` + +## Methods +### `decode(_:from:)` + +```swift +func decode(_ type: T.Type, from data: Data) throws -> T where T : Decodable +``` diff --git a/docs/source/api/Apollo/protocols/HTTPNetworkTransportDelegate.md b/docs/source/api/Apollo/protocols/HTTPNetworkTransportDelegate.md deleted file mode 100644 index 1e9bb8c8f5..0000000000 --- a/docs/source/api/Apollo/protocols/HTTPNetworkTransportDelegate.md +++ /dev/null @@ -1,9 +0,0 @@ -**PROTOCOL** - -# `HTTPNetworkTransportDelegate` - -```swift -public protocol HTTPNetworkTransportDelegate: class -``` - -> Empty base protocol to allow multiple sub-protocols to just use a single parameter. diff --git a/docs/source/api/Apollo/protocols/HTTPNetworkTransportGraphQLErrorDelegate.md b/docs/source/api/Apollo/protocols/HTTPNetworkTransportGraphQLErrorDelegate.md deleted file mode 100644 index 8d6f639829..0000000000 --- a/docs/source/api/Apollo/protocols/HTTPNetworkTransportGraphQLErrorDelegate.md +++ /dev/null @@ -1,40 +0,0 @@ -**PROTOCOL** - -# `HTTPNetworkTransportGraphQLErrorDelegate` - -```swift -public protocol HTTPNetworkTransportGraphQLErrorDelegate: HTTPNetworkTransportDelegate -``` - -> Methods which will be called after some kind of response has been received and it contains GraphQLErrors. - -## Methods -### `networkTransport(_:receivedGraphQLErrors:retryHandler:)` - -```swift -func networkTransport(_ networkTransport: HTTPNetworkTransport, - receivedGraphQLErrors errors: [GraphQLError], - retryHandler: @escaping (_ shouldRetry: Bool) -> Void) -``` - -> Called when response contains one or more GraphQL errors. -> -> NOTE: The mere presence of a GraphQL error does not necessarily mean a request failed! -> GraphQL is design to allow partial success/failures to return, so make sure -> you're validating the *type* of error you're getting in this before deciding whether to retry or not. -> -> ALSO NOTE: Don't just call the `retryHandler` with `true` all the time, or you can -> potentially wind up in an infinite loop of errors -> -> - Parameters: -> - networkTransport: The network transport which received the error -> - errors: The received GraphQL errors -> - retryHandler: A closure indicating whether the operation should be retried. Asyncrhonous to allow for re-authentication or other async operations to complete. - -#### Parameters - -| Name | Description | -| ---- | ----------- | -| networkTransport | The network transport which received the error | -| errors | The received GraphQL errors | -| retryHandler | A closure indicating whether the operation should be retried. Asyncrhonous to allow for re-authentication or other async operations to complete. | \ No newline at end of file diff --git a/docs/source/api/Apollo/protocols/HTTPNetworkTransportPreflightDelegate.md b/docs/source/api/Apollo/protocols/HTTPNetworkTransportPreflightDelegate.md deleted file mode 100644 index 2f0bde81f1..0000000000 --- a/docs/source/api/Apollo/protocols/HTTPNetworkTransportPreflightDelegate.md +++ /dev/null @@ -1,51 +0,0 @@ -**PROTOCOL** - -# `HTTPNetworkTransportPreflightDelegate` - -```swift -public protocol HTTPNetworkTransportPreflightDelegate: HTTPNetworkTransportDelegate -``` - -> Methods which will be called prior to a request being sent to the server. - -## Methods -### `networkTransport(_:shouldSend:)` - -```swift -func networkTransport(_ networkTransport: HTTPNetworkTransport, shouldSend request: URLRequest) -> Bool -``` - -> Called when a request is about to send, to validate that it should be sent. -> Good for early-exiting if your user is not logged in, for example. -> -> - Parameters: -> - networkTransport: The network transport which wants to send a request -> - request: The request, BEFORE it has been modified by `willSend` -> - Returns: True if the request should proceed, false if not. - -#### Parameters - -| Name | Description | -| ---- | ----------- | -| networkTransport | The network transport which wants to send a request | -| request | The request, BEFORE it has been modified by `willSend` | - -### `networkTransport(_:willSend:)` - -```swift -func networkTransport(_ networkTransport: HTTPNetworkTransport, willSend request: inout URLRequest) -``` - -> Called when a request is about to send. Allows last minute modification of any properties on the request, -> -> -> - Parameters: -> - networkTransport: The network transport which is about to send a request -> - request: The request, as an `inout` variable for modification - -#### Parameters - -| Name | Description | -| ---- | ----------- | -| networkTransport | The network transport which is about to send a request | -| request | The request, as an `inout` variable for modification | \ No newline at end of file diff --git a/docs/source/api/Apollo/protocols/HTTPNetworkTransportRetryDelegate.md b/docs/source/api/Apollo/protocols/HTTPNetworkTransportRetryDelegate.md deleted file mode 100644 index 104ccb19e8..0000000000 --- a/docs/source/api/Apollo/protocols/HTTPNetworkTransportRetryDelegate.md +++ /dev/null @@ -1,40 +0,0 @@ -**PROTOCOL** - -# `HTTPNetworkTransportRetryDelegate` - -```swift -public protocol HTTPNetworkTransportRetryDelegate: HTTPNetworkTransportDelegate -``` - -> Methods which will be called if an error is receieved at the network level. - -## Methods -### `networkTransport(_:receivedError:for:response:continueHandler:)` - -```swift -func networkTransport(_ networkTransport: HTTPNetworkTransport, - receivedError error: Error, - for request: URLRequest, - response: URLResponse?, - continueHandler: @escaping (_ action: HTTPNetworkTransport.ContinueAction) -> Void) -``` - -> Called when an error has been received after a request has been sent to the server to see if an operation should be retried or not. -> NOTE: Don't just call the `continueHandler` with `.retry` all the time, or you can potentially wind up in an infinite loop of errors -> -> - Parameters: -> - networkTransport: The network transport which received the error -> - error: The received error -> - request: The URLRequest which generated the error -> - response: [Optional] Any response received when the error was generated -> - continueHandler: A closure indicating whether the operation should be retried. Asyncrhonous to allow for re-authentication or other async operations to complete. - -#### Parameters - -| Name | Description | -| ---- | ----------- | -| networkTransport | The network transport which received the error | -| error | The received error | -| request | The URLRequest which generated the error | -| response | [Optional] Any response received when the error was generated | -| continueHandler | A closure indicating whether the operation should be retried. Asyncrhonous to allow for re-authentication or other async operations to complete. | \ No newline at end of file diff --git a/docs/source/api/Apollo/protocols/HTTPNetworkTransportTaskCompletedDelegate.md b/docs/source/api/Apollo/protocols/HTTPNetworkTransportTaskCompletedDelegate.md deleted file mode 100644 index 2782918d4d..0000000000 --- a/docs/source/api/Apollo/protocols/HTTPNetworkTransportTaskCompletedDelegate.md +++ /dev/null @@ -1,40 +0,0 @@ -**PROTOCOL** - -# `HTTPNetworkTransportTaskCompletedDelegate` - -```swift -public protocol HTTPNetworkTransportTaskCompletedDelegate: HTTPNetworkTransportDelegate -``` - -> Methods which will be called after some kind of response has been received to a `URLSessionTask`. - -## Methods -### `networkTransport(_:didCompleteRawTaskForRequest:withData:response:error:)` - -```swift -func networkTransport(_ networkTransport: HTTPNetworkTransport, - didCompleteRawTaskForRequest request: URLRequest, - withData data: Data?, - response: URLResponse?, - error: Error?) -``` - -> A callback to allow hooking in URL session responses for things like logging and examining headers. -> NOTE: This will call back on whatever thread the URL session calls back on, which is never the main thread. Call `DispatchQueue.main.async` before touching your UI! -> -> - Parameters: -> - networkTransport: The network transport that completed a task -> - request: The request which was completed by the task -> - data: [optional] Any data received. Passed through from `URLSession`. -> - response: [optional] Any response received. Passed through from `URLSession`. -> - error: [optional] Any error received. Passed through from `URLSession`. - -#### Parameters - -| Name | Description | -| ---- | ----------- | -| networkTransport | The network transport that completed a task | -| request | The request which was completed by the task | -| data | [optional] Any data received. Passed through from `URLSession`. | -| response | [optional] Any response received. Passed through from `URLSession`. | -| error | [optional] Any error received. Passed through from `URLSession`. | \ No newline at end of file diff --git a/docs/source/api/Apollo/protocols/InterceptorProvider.md b/docs/source/api/Apollo/protocols/InterceptorProvider.md new file mode 100644 index 0000000000..896a845187 --- /dev/null +++ b/docs/source/api/Apollo/protocols/InterceptorProvider.md @@ -0,0 +1,42 @@ +**PROTOCOL** + +# `InterceptorProvider` + +```swift +public protocol InterceptorProvider +``` + +> A protocol to allow easy creation of an array of interceptors for a given operation. + +## Methods +### `interceptors(for:)` + +```swift +func interceptors(for operation: Operation) -> [ApolloInterceptor] +``` + +> Creates a new array of interceptors when called +> +> - Parameter operation: The operation to provide interceptors for + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| operation | The operation to provide interceptors for | + +### `additionalErrorInterceptor(for:)` + +```swift +func additionalErrorInterceptor(for operation: Operation) -> ApolloErrorInterceptor? +``` + +> Provides an additional error interceptor for any additional handling of errors +> before returning to the UI, such as logging. +> - Parameter operation: The oper + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| operation | The oper | \ No newline at end of file diff --git a/docs/source/api/Apollo/protocols/NetworkTransport.md b/docs/source/api/Apollo/protocols/NetworkTransport.md index 93b0a51d72..92e997b3bd 100644 --- a/docs/source/api/Apollo/protocols/NetworkTransport.md +++ b/docs/source/api/Apollo/protocols/NetworkTransport.md @@ -26,10 +26,14 @@ var clientVersion: String > The version of the client to send as a header value. ## Methods -### `send(operation:completionHandler:)` +### `send(operation:cachePolicy:contextIdentifier:callbackQueue:completionHandler:)` ```swift -func send(operation: Operation, completionHandler: @escaping (_ result: Result, Error>) -> Void) -> Cancellable +func send(operation: Operation, + cachePolicy: CachePolicy, + contextIdentifier: UUID?, + callbackQueue: DispatchQueue, + completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable ``` > Send a GraphQL operation to a server and return a response. @@ -38,6 +42,9 @@ func send(operation: Operation, completionHandler: > > - Parameters: > - operation: The operation to send. +> - cachePolicy: The `CachePolicy` to use making this request. +> - contextIdentifier: [optional] A unique identifier for this request, to help with deduping cache hits for watchers. Defaults to `nil`. +> - callbackQueue: The queue to call back on with the results. Should default to `.main`. > - completionHandler: A closure to call when a request completes. On `success` will contain the response received from the server. On `failure` will contain the error which occurred. > - Returns: An object that can be used to cancel an in progress request. @@ -46,4 +53,7 @@ func send(operation: Operation, completionHandler: | Name | Description | | ---- | ----------- | | operation | The operation to send. | +| cachePolicy | The `CachePolicy` to use making this request. | +| contextIdentifier | [optional] A unique identifier for this request, to help with deduping cache hits for watchers. Defaults to `nil`. | +| callbackQueue | The queue to call back on with the results. Should default to `.main`. | | completionHandler | A closure to call when a request completes. On `success` will contain the response received from the server. On `failure` will contain the error which occurred. | \ No newline at end of file diff --git a/docs/source/api/Apollo/protocols/Parseable.md b/docs/source/api/Apollo/protocols/Parseable.md new file mode 100644 index 0000000000..c52ed78d7b --- /dev/null +++ b/docs/source/api/Apollo/protocols/Parseable.md @@ -0,0 +1,29 @@ +**PROTOCOL** + +# `Parseable` + +```swift +public protocol Parseable +``` + +> A protocol to represent anything that can be decoded by a `FlexibleDecoder` + +## Methods +### `init(from:decoder:)` + +```swift +init(from data: Data, decoder: T) throws +``` + +> Required initializer +> +> - Parameters: +> - data: The data to decode +> - decoder: The decoder to use to decode it + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| data | The data to decode | +| decoder | The decoder to use to decode it | \ No newline at end of file diff --git a/docs/source/api/Apollo/protocols/RequestBodyCreator.md b/docs/source/api/Apollo/protocols/RequestBodyCreator.md new file mode 100644 index 0000000000..d16929297b --- /dev/null +++ b/docs/source/api/Apollo/protocols/RequestBodyCreator.md @@ -0,0 +1,31 @@ +**PROTOCOL** + +# `RequestBodyCreator` + +```swift +public protocol RequestBodyCreator +``` + +## Methods +### `requestBody(for:sendOperationIdentifiers:sendQueryDocument:autoPersistQuery:)` + +```swift +func requestBody(for operation: Operation, + sendOperationIdentifiers: Bool, + sendQueryDocument: Bool, + autoPersistQuery: Bool) -> GraphQLMap +``` + +> Creates a `GraphQLMap` out of the passed-in operation +> +> - Parameters: +> - operation: The operation to use +> - sendOperationIdentifiers: Whether or not to send operation identifiers. Defaults to false. +> - Returns: The created `GraphQLMap` + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| operation | The operation to use | +| sendOperationIdentifiers | Whether or not to send operation identifiers. Defaults to false. | \ No newline at end of file diff --git a/docs/source/api/Apollo/protocols/RequestCreator.md b/docs/source/api/Apollo/protocols/RequestCreator.md deleted file mode 100644 index e7a6ac837d..0000000000 --- a/docs/source/api/Apollo/protocols/RequestCreator.md +++ /dev/null @@ -1,62 +0,0 @@ -**PROTOCOL** - -# `RequestCreator` - -```swift -public protocol RequestCreator -``` - -## Methods -### `requestBody(for:sendOperationIdentifiers:sendQueryDocument:autoPersistQuery:)` - -```swift -func requestBody(for operation: Operation, - sendOperationIdentifiers: Bool, - sendQueryDocument: Bool, - autoPersistQuery: Bool) -> GraphQLMap -``` - -> Creates a `GraphQLMap` out of the passed-in operation -> -> - Parameters: -> - operation: The operation to use -> - sendOperationIdentifiers: Whether or not to send operation identifiers. Defaults to false. -> - Returns: The created `GraphQLMap` - -#### Parameters - -| Name | Description | -| ---- | ----------- | -| operation | The operation to use | -| sendOperationIdentifiers | Whether or not to send operation identifiers. Defaults to false. | - -### `requestMultipartFormData(for:files:sendOperationIdentifiers:serializationFormat:manualBoundary:)` - -```swift -func requestMultipartFormData(for operation: Operation, - files: [GraphQLFile], - sendOperationIdentifiers: Bool, - serializationFormat: JSONSerializationFormat.Type, - manualBoundary: String?) throws -> MultipartFormData -``` - -> Creates multi-part form data to send with a request -> -> - Parameters: -> - operation: The operation to create the data for. -> - files: An array of files to use. -> - sendOperationIdentifiers: True if operation identifiers should be sent, false if not. -> - serializationFormat: The format to use to serialize data. -> - manualBoundary: [optional] A manual boundary to pass in. A default boundary will be used otherwise. -> - Returns: The created form data -> - Throws: Errors creating or loading the form data - -#### Parameters - -| Name | Description | -| ---- | ----------- | -| operation | The operation to create the data for. | -| files | An array of files to use. | -| sendOperationIdentifiers | True if operation identifiers should be sent, false if not. | -| serializationFormat | The format to use to serialize data. | -| manualBoundary | [optional] A manual boundary to pass in. A default boundary will be used otherwise. | \ No newline at end of file diff --git a/docs/source/api/Apollo/protocols/UploadingNetworkTransport.md b/docs/source/api/Apollo/protocols/UploadingNetworkTransport.md index fed197e834..299b4cebfd 100644 --- a/docs/source/api/Apollo/protocols/UploadingNetworkTransport.md +++ b/docs/source/api/Apollo/protocols/UploadingNetworkTransport.md @@ -9,10 +9,14 @@ public protocol UploadingNetworkTransport: NetworkTransport > A network transport which can also handle uploads of files. ## Methods -### `upload(operation:files:completionHandler:)` +### `upload(operation:files:callbackQueue:completionHandler:)` ```swift -func upload(operation: Operation, files: [GraphQLFile], completionHandler: @escaping (_ result: Result, Error>) -> Void) -> Cancellable +func upload( + operation: Operation, + files: [GraphQLFile], + callbackQueue: DispatchQueue, + completionHandler: @escaping (Result,Error>) -> Void) -> Cancellable ``` > Uploads the given files with the given operation. @@ -20,6 +24,7 @@ func upload(operation: Operation, files: [GraphQLFi > - Parameters: > - operation: The operation to send > - files: An array of `GraphQLFile` objects to send. +> - callbackQueue: The queue to call back on with the results. Should default to `.main`. > - completionHandler: The completion handler to execute when the request completes or errors > - Returns: An object that can be used to cancel an in progress request. @@ -29,4 +34,5 @@ func upload(operation: Operation, files: [GraphQLFi | ---- | ----------- | | operation | The operation to send | | files | An array of `GraphQLFile` objects to send. | +| callbackQueue | The queue to call back on with the results. Should default to `.main`. | | completionHandler | The completion handler to execute when the request completes or errors | \ No newline at end of file diff --git a/docs/source/api/Apollo/structs/ApolloRequestBodyCreator.md b/docs/source/api/Apollo/structs/ApolloRequestBodyCreator.md new file mode 100644 index 0000000000..1877e0a0db --- /dev/null +++ b/docs/source/api/Apollo/structs/ApolloRequestBodyCreator.md @@ -0,0 +1,14 @@ +**STRUCT** + +# `ApolloRequestBodyCreator` + +```swift +public struct ApolloRequestBodyCreator: RequestBodyCreator +``` + +## Methods +### `init()` + +```swift +public init() +``` diff --git a/docs/source/api/Apollo/structs/ApolloRequestCreator.md b/docs/source/api/Apollo/structs/ApolloRequestCreator.md deleted file mode 100644 index 9f533dd134..0000000000 --- a/docs/source/api/Apollo/structs/ApolloRequestCreator.md +++ /dev/null @@ -1,14 +0,0 @@ -**STRUCT** - -# `ApolloRequestCreator` - -```swift -public struct ApolloRequestCreator: RequestCreator -``` - -## Methods -### `init()` - -```swift -public init() -``` diff --git a/docs/source/api/Apollo/structs/GraphQLResult.md b/docs/source/api/Apollo/structs/GraphQLResult.md index a75f4cc7f7..4653afc6c6 100644 --- a/docs/source/api/Apollo/structs/GraphQLResult.md +++ b/docs/source/api/Apollo/structs/GraphQLResult.md @@ -3,7 +3,7 @@ # `GraphQLResult` ```swift -public struct GraphQLResult +public struct GraphQLResult: Parseable ``` > Represents the result of a GraphQL operation. @@ -42,6 +42,19 @@ public let source: Source > Source of data ## Methods +### `init(from:decoder:)` + +```swift +public init(from data: Foundation.Data, decoder: T) throws +``` + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| data | The data to decode | +| decoder | The decoder to use to decode it | + ### `init(data:extensions:errors:source:dependentKeys:)` ```swift diff --git a/docs/source/api/Apollo/typealiases/DidChangeKeysFunc.md b/docs/source/api/Apollo/typealiases/DidChangeKeysFunc.md index 8dd166ee96..78becdf477 100644 --- a/docs/source/api/Apollo/typealiases/DidChangeKeysFunc.md +++ b/docs/source/api/Apollo/typealiases/DidChangeKeysFunc.md @@ -3,5 +3,5 @@ # `DidChangeKeysFunc` ```swift -public typealias DidChangeKeysFunc = (Set, UnsafeMutableRawPointer?) -> Void +public typealias DidChangeKeysFunc = (Set, UUID?) -> Void ``` diff --git a/docs/source/api/Apollo/typealiases/JSONDecoder.Input.md b/docs/source/api/Apollo/typealiases/JSONDecoder.Input.md new file mode 100644 index 0000000000..1e4ae120d3 --- /dev/null +++ b/docs/source/api/Apollo/typealiases/JSONDecoder.Input.md @@ -0,0 +1,7 @@ +**TYPEALIAS** + +# `JSONDecoder.Input` + +```swift +public typealias Input = Data +``` diff --git a/docs/source/api/Apollo/typealiases/PropertyListDecoder.Input.md b/docs/source/api/Apollo/typealiases/PropertyListDecoder.Input.md new file mode 100644 index 0000000000..8d14b21bf4 --- /dev/null +++ b/docs/source/api/Apollo/typealiases/PropertyListDecoder.Input.md @@ -0,0 +1,7 @@ +**TYPEALIAS** + +# `PropertyListDecoder.Input` + +```swift +public typealias Input = Data +``` diff --git a/docs/source/api/ApolloWebSocket/classes/SplitNetworkTransport.md b/docs/source/api/ApolloWebSocket/classes/SplitNetworkTransport.md index 092c10ab1c..a02f04c6cd 100644 --- a/docs/source/api/ApolloWebSocket/classes/SplitNetworkTransport.md +++ b/docs/source/api/ApolloWebSocket/classes/SplitNetworkTransport.md @@ -22,21 +22,21 @@ public var clientVersion: String ``` ## Methods -### `init(httpNetworkTransport:webSocketNetworkTransport:)` +### `init(uploadingNetworkTransport:webSocketNetworkTransport:)` ```swift -public init(httpNetworkTransport: UploadingNetworkTransport, webSocketNetworkTransport: NetworkTransport) +public init(uploadingNetworkTransport: UploadingNetworkTransport, webSocketNetworkTransport: NetworkTransport) ``` > Designated initializer > > - Parameters: -> - httpNetworkTransport: An `UploadingNetworkTransport` to use for non-subscription requests. Should generally be a `HTTPNetworkTransport` or something similar. +> - uploadingNetworkTransport: An `UploadingNetworkTransport` to use for non-subscription requests. Should generally be a `RequestChainNetworkTransport` or something similar. > - webSocketNetworkTransport: A `NetworkTransport` to use for subscription requests. Should generally be a `WebSocketTransport` or something similar. #### Parameters | Name | Description | | ---- | ----------- | -| httpNetworkTransport | An `UploadingNetworkTransport` to use for non-subscription requests. Should generally be a `HTTPNetworkTransport` or something similar. | +| uploadingNetworkTransport | An `UploadingNetworkTransport` to use for non-subscription requests. Should generally be a `RequestChainNetworkTransport` or something similar. | | webSocketNetworkTransport | A `NetworkTransport` to use for subscription requests. Should generally be a `WebSocketTransport` or something similar. | \ No newline at end of file diff --git a/docs/source/api/ApolloWebSocket/classes/WebSocketTransport.md b/docs/source/api/ApolloWebSocket/classes/WebSocketTransport.md index 6428946ba9..8ba9bd2666 100644 --- a/docs/source/api/ApolloWebSocket/classes/WebSocketTransport.md +++ b/docs/source/api/ApolloWebSocket/classes/WebSocketTransport.md @@ -48,7 +48,7 @@ public var enableSOCKSProxy: Bool > Note: Will return `false` from the getter and no-op the setter for implementations that do not conform to `SOCKSProxyable`. ## Methods -### `init(request:clientName:clientVersion:sendOperationIdentifiers:reconnect:reconnectionInterval:allowSendingDuplicates:connectingPayload:requestCreator:)` +### `init(request:clientName:clientVersion:sendOperationIdentifiers:reconnect:reconnectionInterval:allowSendingDuplicates:connectingPayload:requestBodyCreator:)` ```swift public init(request: URLRequest, @@ -59,7 +59,7 @@ public init(request: URLRequest, reconnectionInterval: TimeInterval = 0.5, allowSendingDuplicates: Bool = true, connectingPayload: GraphQLMap? = [:], - requestCreator: RequestCreator = ApolloRequestCreator()) + requestBodyCreator: RequestBodyCreator = ApolloRequestBodyCreator()) ``` > Designated initializer @@ -72,7 +72,7 @@ public init(request: URLRequest, > - Parameter reconnectionInterval: How long to wait before attempting to reconnect. Defaults to half a second. > - Parameter allowSendingDuplicates: Allow sending duplicate messages. Important when reconnected. Defaults to true. > - Parameter connectingPayload: [optional] The payload to send on connection. Defaults to an empty `GraphQLMap`. -> - Parameter requestCreator: The request creator to use when serializing requests. Defaults to an `ApolloRequestCreator`. +> - Parameter requestBodyCreator: The `RequestBodyCreator` to use when serializing requests. Defaults to an `ApolloRequestBodyCreator`. #### Parameters @@ -86,7 +86,7 @@ public init(request: URLRequest, | reconnectionInterval | How long to wait before attempting to reconnect. Defaults to half a second. | | allowSendingDuplicates | Allow sending duplicate messages. Important when reconnected. Defaults to true. | | connectingPayload | [optional] The payload to send on connection. Defaults to an empty `GraphQLMap`. | -| requestCreator | The request creator to use when serializing requests. Defaults to an `ApolloRequestCreator`. | +| requestBodyCreator | The `RequestBodyCreator` to use when serializing requests. Defaults to an `ApolloRequestBodyCreator`. | ### `isConnected()` diff --git a/docs/source/api/ApolloWebSocket/extensions/SplitNetworkTransport.md b/docs/source/api/ApolloWebSocket/extensions/SplitNetworkTransport.md index 88e4ee9f13..58c960ddad 100644 --- a/docs/source/api/ApolloWebSocket/extensions/SplitNetworkTransport.md +++ b/docs/source/api/ApolloWebSocket/extensions/SplitNetworkTransport.md @@ -6,10 +6,14 @@ extension SplitNetworkTransport: NetworkTransport ``` ## Methods -### `send(operation:completionHandler:)` +### `send(operation:cachePolicy:contextIdentifier:callbackQueue:completionHandler:)` ```swift -public func send(operation: Operation, completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable +public func send(operation: Operation, + cachePolicy: CachePolicy, + contextIdentifier: UUID? = nil, + callbackQueue: DispatchQueue = .main, + completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable ``` #### Parameters @@ -17,14 +21,19 @@ public func send(operation: Operation, completionHa | Name | Description | | ---- | ----------- | | operation | The operation to send. | +| cachePolicy | The `CachePolicy` to use making this request. | +| contextIdentifier | [optional] A unique identifier for this request, to help with deduping cache hits for watchers. Defaults to `nil`. | +| callbackQueue | The queue to call back on with the results. Should default to `.main`. | | completionHandler | A closure to call when a request completes. On `success` will contain the response received from the server. On `failure` will contain the error which occurred. | -### `upload(operation:files:completionHandler:)` +### `upload(operation:files:callbackQueue:completionHandler:)` ```swift -public func upload(operation: Operation, - files: [GraphQLFile], - completionHandler: @escaping (_ result: Result, Error>) -> Void) -> Cancellable +public func upload( + operation: Operation, + files: [GraphQLFile], + callbackQueue: DispatchQueue = .main, + completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable ``` #### Parameters @@ -33,4 +42,5 @@ public func upload(operation: Operation, | ---- | ----------- | | operation | The operation to send | | files | An array of `GraphQLFile` objects to send. | +| callbackQueue | The queue to call back on with the results. Should default to `.main`. | | completionHandler | The completion handler to execute when the request completes or errors | \ No newline at end of file diff --git a/docs/source/api/ApolloWebSocket/extensions/WebSocketTransport.md b/docs/source/api/ApolloWebSocket/extensions/WebSocketTransport.md index 577d264475..88b7c9094c 100644 --- a/docs/source/api/ApolloWebSocket/extensions/WebSocketTransport.md +++ b/docs/source/api/ApolloWebSocket/extensions/WebSocketTransport.md @@ -6,10 +6,15 @@ extension WebSocketTransport: NetworkTransport ``` ## Methods -### `send(operation:completionHandler:)` +### `send(operation:cachePolicy:contextIdentifier:callbackQueue:completionHandler:)` ```swift -public func send(operation: Operation, completionHandler: @escaping (_ result: Result,Error>) -> Void) -> Cancellable +public func send( + operation: Operation, + cachePolicy: CachePolicy, + contextIdentifier: UUID? = nil, + callbackQueue: DispatchQueue = .main, + completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable ``` #### Parameters @@ -17,6 +22,9 @@ public func send(operation: Operation, completionHa | Name | Description | | ---- | ----------- | | operation | The operation to send. | +| cachePolicy | The `CachePolicy` to use making this request. | +| contextIdentifier | [optional] A unique identifier for this request, to help with deduping cache hits for watchers. Defaults to `nil`. | +| callbackQueue | The queue to call back on with the results. Should default to `.main`. | | completionHandler | A closure to call when a request completes. On `success` will contain the response received from the server. On `failure` will contain the error which occurred. | ### `websocketDidConnect(socket:)` diff --git a/docs/source/initialization.md b/docs/source/initialization.md index ec06b2cbc8..221acb080b 100644 --- a/docs/source/initialization.md +++ b/docs/source/initialization.md @@ -27,171 +27,293 @@ public init(networkTransport: NetworkTransport, The available implementations are: -- **`HTTPNetworkTransport`**, which has a number of configurable options and uses standard HTTP requests to communicate with the server +- **`RequestChainNetworkTransport`**, which passes a request through a chain of interceptors that can do work both before and after going to the network, and uses standard HTTP requests to communicate with the server - **`WebSocketTransport`**, which will send everything using a web socket. If you're using CocoaPods, make sure to install the `Apollo/WebSocket` sub-spec to access this. - **`SplitNetworkTransport`**, which will send subscription operations via a web socket and all other operations via HTTP. If you're using CocoaPods, make sure to install the `Apollo/WebSocket` sub-spec to access this. -### Using `HTTPNetworkTransport` +### Using `RequestChainNetworkTransport` -The initializer for `HTTPNetworkTransport` has several properties which can allow you to get better information and finer-grained control of your HTTP requests and responses: +The initializer for `RequestChainNetworkTransport` has several properties which can allow you to get better information and finer-grained control of your HTTP requests and responses: -- `client` allows you to pass in a [subclass of `URLSessionClient`](#the-urlsessionclient-class) to handle managing a background-compatible URL session, and set up anything which needs to be done for every single request without alteration. -- `sendOperationIdentifiers` allows you send operation identifiers along with your requests. **NOTE:** To send operation identifiers, Apollo types must be generated with `operationIdentifier`s or sending data will crash. Due to this restriction, this option defaults to `false`. -- `useGETForQueries` sends all requests of `query` type using `GET` instead of `POST`. This defaults to `false` to preserve existing behavior in older versions of the client. -- `delegate` Can conform to one or many of several sub-protocols for `HTTPNetworkTransportDelegate`, detailed below. +- `interceptorProvider`: The interceptor provider to use when constructing chains for a request. See below for details on interceptor providers. +- `endpointURL`: The GraphQL endpoint URL to use for all calls. +- `additionalHeaders`: Any additional headers that should be automatically added to every request. Defaults to an empty dictionary. +- `autoPersistQueries`: Pass `true` if [Automatic Persisted Queries](https://www.apollographql.com/docs/apollo-server/performance/apq/) should be used to send an operation's hash instead of the full operation body by default. **NOTE:** To use APQs, you need to make sure to generate your types with operation identifiers. In your Swift Script, make sure to pass a non-nil `operationIDsURL` to have this output. Due to this restriction, this option defaults to `false`. +- `requestCreator`: The `RequestCreator` object to use to build your `URLRequest`. Defaults to the provided `ApolloRequestCreator` implementation. +- `useGETForQueries`: Sends all requests of `query` and `mutation` types using `GET` instead of `POST`. This is mostly useful for large companies taking advantage of CDNs (Content Distribution Networks) that allow local caches instead of going all the way to your server for data which does not change often. This defaults to `false` to preserve existing behavior in older versions of the client. +- `useGETForPersistedQueryRetry`: Pass `true` to use `GET` instead of `POST` for a retry of a persisted query. Defaults to `false`. -### The URLSessionClient class +### How the `RequestChain` works -Since `URLSession` only supports use in the background using the delegate-based API, we have created our own `URLSessionClient` which handles the basics of setup for that. +A `RequestChain` is constructed using an array of interceptors, to be run in the order given, and handles calling back on a specified `DispatchQueue` after all work is complete. -One thing to be aware of: Because setting up a delegate is only possible in the initializer for `URLSession`, you can only pass in a `URLSessionConfiguration`, **not** an existing `URLSession`, to this class's initializer. +In each interceptor, work can be performed asynchronously on any thread. To move along to the next interceptor in the chain, call `proceedAsync`. -By default, instances of `URLSessionClient` use `URLSessionConfiguration.default` to set up their URL session, and instances of `HTTPNetworkTransport` use the default initializer for `URLSessionClient`. +By default, when the interceptor chain ends, if you have a parsed result available, this result will be returned to the caller. -The `URLSessionClient` class and most of its methods are `open` so you can subclass it if you need to override any of the delegate methods for the `URLSession` delegates we're using or you need to handle additional delegate scenarios. +If you want to directly return a value to the caller, call `returnValueAsync`. If you want to have the chain return an error, call `handleErrorAsync`. Both of these methods will call your completion block on the queue specified when creating the `RequestChain. -### Using `HTTPNetworkTransportDelegate` +Note that calling `returnValue` does **NOT** forbid calling `handleError` - or calling each more than once. For example, if you want to return data from the cache to the UI while a network fetch executes, you'd want to make sure that `returnValueAsync` was called twice. -This delegate includes several sub-protocols so that a single parameter can be passed no matter how many sub-protocols it conforms to. +The chain also includes a `retry` mechanism, which will go all the way back to the first interceptor in the chain, then start running through the interceptors again. -If you conform to a particular sub-protocol, you must implement all the methods in that sub-protocol, but we've tried to break things out in a sensible fashion. The sub-protocols are: +**IMPORTANT**: Do not call `retry` blindly. If your server is returning 500s or if the user has no internet, this will create an infinite loop of requests that are retrying (especially if you're not using something like the `MaxRetryInterceptor` to limit how many retries are made). This **will** kill your user's battery, and might also run up the bill on their data plan. Make sure to only request a retry when there's something your code can actually do about the problem! -#### `HTTPNetworkTransportPreflightDelegate` +In the `RequestChainNetworkTransport`, each request creates an individual request chain, and uses an `InterceptorProvider` -This protocol allows pre-flight validation of requests, the ability to bail out before modifying the request, and the ability to modify the `URLRequest` with things like additional headers. +### Setting up `ApolloInterceptor` chains with `InterceptorProvider` -The `shouldSend` method is called before any modifications are made by `willSend`. This allows you do things like check that you have an authentication token in your keychain, and if not, prevent the request from hitting the network. When you cancel a request in `shouldSend`, you will receive an error indicating the request was cancelled. +Every operation sent through a `RequestChainNetworkTransport` will be passed into an `InterceptorProvider` before going to the network. This protocol creates an array of interceptors for use by a single request chain based on the provided operation. -The `willSend` method is called with an `inout` parameter for the `URLRequest` which is about to be sent. There are several uses for this functionality. +There are two default implementations for this protocol provided: -The first is simple logging of the request that's about to go out. You could theoretically do this in `shouldSend`, but particularly if you're making any changes to the request, you'd probably want to do your logging after you've finished those changes. +- `LegacyInterceptorProvider` works with our existing parsing and caching system and tries to replicate the experience of using the old `HTTPNetworkTransport` as closely as possible. It takes a `URLSessionClient` and an `ApolloStore` to pass into the interceptors it uses. +- `CodableInterceptorProvider` is a **work in progress**, which is going to be for use with our [Swift Codegen Rewrite](https://github.com/apollographql/apollo-ios/projects/2), (which, I swear, will eventually be finished). It is not suitable for use at this time. It takes a `URLSessionClient`, a `FlexibleDecoder` (something can decode anything that conforms to `Decodable`). It does not support caching yet. -The most common usage is to modify the request headers. Note that when modifying request headers, you'll need to make a copy of any pre-existing headers before adding new ones. See the [Example Advanced Client Setup](#example-advanced-client-setup) for details. +If you wish to make your own `InterceptorProvider` instead of using the provided one, you can take advantage of several interceptors that are included in the library: -You can also make any other changes you need to the request, but be aware that going too crazy with this may lead to Unexpected Behavior™. +#### Pre-network +- `MaxRetryInterceptor` checks to make sure a query has not been tried more than a maximum number of times. +- `LegacyCacheReadInterceptor` reads from a provided `ApolloStore` based on the `cachePolicy`, and will return a resul if one is found. -#### `HTTPNetworkTransportTaskCompletedDelegate` +#### Network +- `NetworkFetchInterceptor` takes a `URLSessionClient` and uses it to send the prepared `HTTPRequest` (or subclass thereof) to the server. -This delegate allows you to peer in to the raw data returned to the `URLSession`. This is helpful both for logging what you're getting directly from your server and for grabbing any information out of the raw response, such as updated authentication tokens, which would be removed before parsing is completed. +#### Post-Network -#### `HTTPNetworkTransportRetryDelegate` +- `ResponseCodeInterceptor` checks to make sure a valid response status code has been returned. **NOTE**: Most errors at the GraphQL level are returned with a `200` status code and information in the `errors` array per the GraphQL Spec. This interceptor helps with things like server errors and errors that are returned by middleware. [This article on error handling in GraphQL](https://medium.com/@sachee/200-ok-error-handling-in-graphql-7ec869aec9bc) is a really helpful look at how and why these differences occur. +- `AutomaticPersistedQueryInterceptor` handles checking responses to see if an error is because an automatic persisted query failed, and the full operation needs to be resent to the server. +- `LegacyParsingInterceptor` parses code generated by our Typescript code generation. +- `LegacyCacheWriteInterceptor` writes to a provided `ApolloStore`. +- `CodableParsingError` is a **work in progress** which will parse `Codable` results form the Swift Codegen Rewrite. -This delegate allows you to asynchronously determine whether to retry your request. This is asynchronous to allow for things like re-authenticating your user. +### The URLSessionClient class -When you decide to retry, the `send` operation for your `GraphQLOperation` will be retried. This means you'll get brand new callbacks from `HTTPNetworkTransportPreflightDelegate` to update your headers again as if it was a totally new request. Therefore, the parameter for the completion closure is a simple `true`/`false` option: Pass `true` to retry, pass `false` to error out. +Since `URLSession` only supports use in the background using the delegate-based API, we have created our own `URLSessionClient` which handles the basics of setup for that. -**IMPORTANT**: Do not call `true` blindly in the completion closure. If your server is returning 500s or if the user has no internet, this will create an infinite loop of requests that are retrying. This **will** kill your user's battery, and might also run up the bill on their data plan. Make sure to only request a retry when there's something your code can actually do about the problem! +One thing to be aware of: Because setting up a delegate is only possible in the initializer for `URLSession`, you can only pass in a `URLSessionConfiguration`, **not** an existing `URLSession`, to this class's initializer. + +By default, instances of `URLSessionClient` use `URLSessionConfiguration.default` to set up their URL session, and instances of `HTTPNetworkTransport` use the default initializer for `URLSessionClient`. + +The `URLSessionClient` class and most of its methods are `open` so you can subclass it if you need to override any of the delegate methods for the `URLSession` delegates we're using or you need to handle additional delegate scenarios. ### Example Advanced Client Setup -Here's a sample of a singleton using an advanced client which handles all three sub-protocols. This code assumes you've got the following classes in your own code (these are **not** part of the Apollo library): +Here's a sample how to use an advanced client with some custom interceptors. This code assumes you've got the following classes in your own code (**these are not part of the Apollo library**): -- **`UserManager`** to check whether the user is logged in, perform associated checks on errors and responses to see if they need to reauthenticate, and perform reauthentication +- **`UserManager`** to check whether the user is logged in, perform associated checks on errors and responses to see if they need to renew their token, and perform that renewal - **`Logger`** to handle printing logs based on their level, and which supports `.debug`, `.error`, or `.always` log levels. -```swift -import Foundation -import Apollo +#### Example interceptors -// MARK: - Singleton Wrapper +##### Sample `UserManagementInteceptor` -class Network { - static let shared = Network() - - // Configure the network transport to use the singleton as the delegate. - private lazy var networkTransport: HTTPNetworkTransport = { - let transport = HTTPNetworkTransport(url: URL(string: "http://localhost:8080/graphql")!) - transport.delegate = self - return transport - }() +An interceptor which checks if the user is logged in and then renews the user's token if it is expired asynchronously before continuing the chain, using the above-mentioned `UserManager` class: + +```swift +class UserManagementInterceptor: ApolloInterceptor { - // Use the configured network transport in your Apollo client. - private(set) lazy var apollo = ApolloClient(networkTransport: self.networkTransport) + enum UserError: Error { + case noUserLoggedIn + } + + /// Helper function to add the token then move on to the next step + private func addTokenAndProceed( + _ token: Token, + to request: HTTPRequest, + chain: RequestChain, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) { + + request.addHeader(name: "Authentication", value: "Bearer: \(token.value)") + chain.proceedAsync(request: request, + response: response, + completion: completion) + } + + func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) { + + guard let token = UserManager.shared.token else { + // In this instance, no user is logged in, so we want to call + // the error handler, then return to prevent further work + chain.handleErrorAsync(UserError.noUserLoggedIn, + request: request, + response: response, + completion: completion) + return + } + + // If we've gotten here, there is a token! + if token.isExpired { + // Call an async method to renew the token + UserManager.shared.renewToken { [weak self] tokenRenewResult in + guard let self = self else { + return + } + + switch tokenRenewResult { + case .failure(let error): + // Pass the token renewal error up the chain, and do + // not proceed further. Note that you could also wrap this in a + // `UserError` if you want. + chain.handleErrorAsync(error, + request: request, + response: response, + completion: completion) + case .success(let token): + // Renewing worked! Add the token and move on + self.addTokenAndProceed(token, + to: request, + chain: chain, + response: response, + completion: completion) + } + } + } else { + // We don't need to wait for renewal, add token and move on + self.addTokenAndProceed(token, + to: request, + chain: chain, + response: response, + completion: completion) + } + } } +``` -// MARK: - Pre-flight delegate +##### Sample `RequestLoggingInterceptor` -extension Network: HTTPNetworkTransportPreflightDelegate { +An interceptor which logs the outgoing request using the above-mentioned `Logger` class, then moves on: - func networkTransport(_ networkTransport: HTTPNetworkTransport, - shouldSend request: URLRequest) -> Bool { - // If there's an authenticated user, send the request. If not, don't. - return UserManager.shared.hasAuthenticatedUser - } - - func networkTransport(_ networkTransport: HTTPNetworkTransport, - willSend request: inout URLRequest) { - - // Get the existing headers, or create new ones if they're nil - var headers = request.allHTTPHeaderFields ?? [String: String]() - - // Add any new headers you need - headers["Authorization"] = "Bearer \(UserManager.shared.currentAuthToken)" - - // Re-assign the updated headers to the request. - request.allHTTPHeaderFields = headers +```swift +class RequestLoggingInterceptor: ApolloInterceptor { - Logger.log(.debug, "Outgoing request: \(request)") - } + func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) { + + Logger.log(.debug, "Outgoing request: \(request)") + chain.proceedAsync(request: request, + response: response, + completion: completion) + } } +``` -// MARK: - Task Completed Delegate - -extension Network: HTTPNetworkTransportTaskCompletedDelegate { - func networkTransport(_ networkTransport: HTTPNetworkTransport, - didCompleteRawTaskForRequest request: URLRequest, - withData data: Data?, - response: URLResponse?, - error: Error?) { - Logger.log(.debug, "Raw task completed for request: \(request)") - - if let error = error { - Logger.log(.error, "Error: \(error)") - } +##### Sample `‌ResponseLoggingInterceptor` + +An interceptor using the above-mentioned `Logger` which logs the incoming response if it exists, and moves on. + +Note that this is an example of an interceptor which can both proceed **and** throw an error - we don't necessarily want to stop processing if this was set up in the wrong place, but we do want to know about it. + +```swift +class ResponseLoggingInterceptor: ApolloInterceptor { - if let response = response { - Logger.log(.debug, "Response: \(response)") - } else { - Logger.log(.error, "No URL Response received!") + enum ResponseLoggingError: Error { + case notYetReceived } - if let data = data { - Logger.log(.debug, "Data: \(String(describing: String(bytes: data, encoding: .utf8)))") - } else { - Logger.log(.error, "No data received!") + func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) { + + defer { + // Even if we can't log, we still want to keep going. + chain.proceedAsync(request: request, + response: response, + completion: completion) + } + + guard let receivedResponse = response else { + chain.handleErrorAsync(ResponseLoggingError.notYetReceived, + request: request, + response: response, + completion: completion) + return + } + + Logger.log(.debug, "HTTP Response: \(receivedResponse.httpResponse)") + + if let stringData = String(bytes: receivedResponse.rawData, encoding: .utf8) { + Logger.log(.debug, "Data: \(stringData)") + } else { + Logger.log(.error, "Could not convert data to string!") + } } - } } +``` -// MARK: - Retry Delegate +#### Example Custom Interceptor Provider -extension Network: HTTPNetworkTransportRetryDelegate { +This `InterceptorProvider` uses all of the interceptors that (as of this writing) are in the default `LegacyInterceptorProvider`, interspersed at the appropriate points with the sample interceptors created above: - func networkTransport(_ networkTransport: HTTPNetworkTransport, - receivedError error: Error, - for request: URLRequest, - response: URLResponse?, - continueHandler: @escaping (_ action: HTTPNetworkTransport.ContinueAction) -> Void) { - // Check if the error and/or response you've received are something that requires authentication - guard UserManager.shared.requiresReAuthentication(basedOn: error, response: response) else { - // This is not something this application can handle, do not retry. - continueHandler(.fail(error)) - return +``` +struct NetworkInterceptorProvider: InterceptorProvider { + + // These properties will remain the same throughout the life of the `InterceptorProvider`, even though they + // will be handed to different interceptors. + private let store: ApolloStore + private let client: URLSessionClient + + init(store: ApolloStore, + client: URLSessionClient) { + self.store = store + self.client = client } - // Attempt to re-authenticate asynchronously - UserManager.shared.reAuthenticate { (reAuthenticateError: Error?) in - // If re-authentication succeeded, try again. If it didn't, don't. - if let reAuthenticateError = reAuthenticateError { - continueHandler(.fail(reAuthenticateError)) // Will return re authenticate error to query callback - // or (depending what error you want to get to callback) - continueHandler(.fail(error)) // Will return original error - } else { - continueHandler(.retry) - } + func interceptors(for operation: Operation) -> [ApolloInterceptor] { + return [ + MaxRetryInterceptor(), + LegacyCacheReadInterceptor(store: self.store), + TokenAddingInterceptor(), + RequestLoggingInterceptor(), + NetworkFetchInterceptor(client: self.client), + ResponseLoggingInterceptor(), + ResponseCodeInterceptor(), + LegacyParsingInterceptor(cacheKeyForObject: self.store.cacheKeyForObject), + AutomaticPersistedQueryInterceptor(), + LegacyCacheWriteInterceptor(store: self.store) + ] } - } } ``` +#### Example Network Singleton Setup + +This is the equivalent of what you'd set up in the [Basic Client Creation](#basic-client-creation) section, and what you'd call into from your application. + +```swift +class Network { + static let shared = Network() + + private(set) lazy var apollo: ApolloClient = { + // The cache is necessary to set up the store, which we're going to hand to the provider + let cache = InMemoryNormalizedCache() + let store = ApolloStore(cache: cache) + + let client = URLSessionClient() + let provider = NetworkInterceptorProvider(store: store, client: client) + let url = URL(string: "https://apollo-fullstack-tutorial.herokuapp.com/")! + + let requestChainTransport = RequestChainNetworkTransport(interceptorProvider: provider, + endpointURL: url) + + + // Remember to give the store you already created to the client so it + // doesn't create one on its own + return ApolloClient(networkTransport: requestChainTransport, + store: store) + }() +} +``` + + An example of setting up a client which can handle web sockets and subscriptions is included in the [subscription documentation](subscriptions/#sample-subscription-supporting-initializer). diff --git a/docs/source/subscriptions.md b/docs/source/subscriptions.md index 7c87bfa2ad..247efb25dd 100644 --- a/docs/source/subscriptions.md +++ b/docs/source/subscriptions.md @@ -15,7 +15,7 @@ Once those operations are generated, you can use an instance of `ApolloClient` u There are two different classes which conform to the [`NetworkTransport` protocol](api/Apollo/protocols/NetworkTransport/) within the `ApolloWebSocket` library: - **`WebSocketTransport`** sends all operations over a web socket. -- **`SplitNetworkTransport`** hangs onto both a [`WebSocketTransport`](api/ApolloWebSocket/classes/WebSocketTransport/) instance and an [`UploadingNetworkTransport`](api/Apollo/protocols/UploadingNetworkTransport/) instance (usually [`HTTPNetworkTransport`](api/Apollo/classes/HTTPNetworkTransport/)) in order to create a single network transport that can use http for queries and mutations, and web sockets for subscriptions. +- **`SplitNetworkTransport`** hangs onto both a [`WebSocketTransport`](api/ApolloWebSocket/classes/WebSocketTransport/) instance and an [`UploadingNetworkTransport`](api/Apollo/protocols/UploadingNetworkTransport/) instance (usually [`RequestChainNetworkTransport`](api/Apollo/classes/RequestChainNetworkTransport/)) in order to create a single network transport that can use http for queries and mutations, and web sockets for subscriptions. Typically, you'll want to use `SplitNetworkTransport`, since this allows you to retain the single `NetworkTransport` setup and avoids any potential issues of using multiple client objects. diff --git a/docs/source/graphql_file_launchlist.png b/docs/source/tutorial/images/graphql_file_launchlist.png similarity index 100% rename from docs/source/graphql_file_launchlist.png rename to docs/source/tutorial/images/graphql_file_launchlist.png diff --git a/docs/source/tutorial/images/interceptor_breakpoint.png b/docs/source/tutorial/images/interceptor_breakpoint.png new file mode 100644 index 0000000000..a9338ad590 Binary files /dev/null and b/docs/source/tutorial/images/interceptor_breakpoint.png differ diff --git a/docs/source/tutorial/images/preflight_delegate_add_protocol_stubs.png b/docs/source/tutorial/images/preflight_delegate_add_protocol_stubs.png deleted file mode 100644 index 8d0cb71da5..0000000000 Binary files a/docs/source/tutorial/images/preflight_delegate_add_protocol_stubs.png and /dev/null differ diff --git a/docs/source/tutorial/images/preflight_delegate_breakpoint.png b/docs/source/tutorial/images/preflight_delegate_breakpoint.png deleted file mode 100644 index 63c14fc823..0000000000 Binary files a/docs/source/tutorial/images/preflight_delegate_breakpoint.png and /dev/null differ diff --git a/docs/source/tutorial/tutorial-introduction.md b/docs/source/tutorial/tutorial-introduction.md index 7ba1d616c4..b84e0e19e7 100644 --- a/docs/source/tutorial/tutorial-introduction.md +++ b/docs/source/tutorial/tutorial-introduction.md @@ -4,9 +4,9 @@ title: "0. Introduction" Welcome! This tutorial demonstrates adding the Apollo iOS SDK to an app to communicate with a GraphQL server. It is confirmed to work with the following tools: -- Xcode 11.5 -- Swift 5.2 -- Apollo iOS SDK 0.28.0 +- Xcode 12.0 +- Swift 5.3 +- Apollo iOS SDK 0.34.0 (BETA) The tutorial assumes that you're using a Mac with Xcode installed. It also assumes some prior experience with iOS development. diff --git a/docs/source/tutorial/tutorial-mutations.md b/docs/source/tutorial/tutorial-mutations.md index 69c63c92e0..d39ecb27fa 100644 --- a/docs/source/tutorial/tutorial-mutations.md +++ b/docs/source/tutorial/tutorial-mutations.md @@ -8,72 +8,95 @@ In this section, you'll learn how to build authenticated mutations and handle in Before you can book a trip, you need to be able to pass your authentication token along to the example server. To do that, let's dig a little deeper into how Apollo Client works. -The `ApolloClient` uses something called a `NetworkTransport` under the hood. By default, the client creates an `HTTPNetworkTransport` instance to handle talking over HTTP to your server. +The `ApolloClient` uses something called a `NetworkTransport` under the hood. By default, the client creates a `RequestChainNetworkTransport` instance to handle talking over HTTP to your server. -If you need to do anything before a request hits the wire but after Apollo has done most of the configuration for you, there's a delegate protocol called `HTTPNetworkTransportPreflightDelegate` that allows you to do that. +A `RequestChain` runs your request through an array of `ApolloInterceptor` objects which can mutate the request and/or check the cache before it hits the network, and then do additional work after a response is received from the network. -Open `Network.swift` and add an extension to conform to that delegate: +The `RequestChainNetworkTransport` uses an object that conforms to the `InterceptorProivder` protocol in order to create that array of interceptors for each operation it executes. There are a couple of providers that are set up by default, which return a fairly standard array of interceptors. -```swift:title=Network.swift -extension Network: HTTPNetworkTransportPreflightDelegate { +The nice thing is that you can also add your own interceptors to the chain anywhere you need to perform custom actions. In this case, you want to have an interceptor that will add -} -``` +First, create the new interceptor. Go to **File > New > File...** and create a new **Swift File**. Name it **TokenAddingInterceptor.swift**. Open that file, and add the -You'll get an error telling you that protocol stubs must be implemented, and asking you if you want to fix this. Click **Fix**. +```swift:title=TokenAddingInterceptor.swift +import Foundation +import Apollo -Do you wish to add protocol stubs with fix button +class TokenAddingInterceptor: ApolloInterceptor { + func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) { + + // TODO + } +} +``` -Two protocol methods will be added: `networkTransport(_:shouldSend:)` and `networkTransport(_:willSend:)`. +Next, import `KeychainSwift` at the top of the file so you can access the key you've just stored in the keychain. -The `shouldSend` method enables you to make sure a request should go out to the network at all. This is useful for things like checking that your user is logged in before trying to make a request. +```swift:title=TokenAddingInterceptor.swift +import KeychainSwift +``` -However, you're not going to use that functionality in this application. Update the method to have it return `true` all the time: +Then, replace the `TODO` within the `interceptAsync` method with code to get the token from the keychain, and add it to your headers if it exists: -```swift:title=Network.swift -func networkTransport(_ networkTransport: HTTPNetworkTransport, - shouldSend request: URLRequest) -> Bool { - return true -} +```swift:title=TokenAddingInterceptor.swift +let keychain = KeychainSwift() +if let token = keychain.get(LoginViewController.loginKeychainKey) { + request.addHeader(name: "Authorization", value: token) +} // else do nothing + +chain.proceedAsync(request: request, + response: response, + completion: completion) ``` -The `willSend` request is the last thing that can manipulate the request before it goes out to the network. Because the request is passed as an `inout` variable, you can manipulate its contents directly. - -Update the `willSend` method to add your token as the value for the `Authorization` header: +Next, since you're only adding one interceptor that can run at the very beginning of other interceptors, you can subclass the existing `LegacyInterceptorProvider` (which is the default interceptor provider). -```swift:title=Network.swift -func networkTransport(_ networkTransport: HTTPNetworkTransport, - willSend request: inout URLRequest) { - let keychain = KeychainSwift() - if let token = keychain.get(LoginViewController.loginKeychainKey) { - request.addValue(token, forHTTPHeaderField: "Authorization") - } // else do nothing -} -``` +Go to **File > New > File...** and create a new **Swift File**. Name it **NetworkInterceptorProvider.swift**. Add an initial Add code which inserts your `TokenAddingInterceptor` before the other interceptors provided by the `LegacyInterceptorProvider`: -Then, import `KeychainSwift` at the top of the file: +```swift:title=NetworkInterceptorProvider.swift +import Foundation +import Apollo -```swift:title=Network.swift -import KeychainSwift +class NetworkInterceptorProvider: LegacyInterceptorProvider { + override func interceptors(for operation: Operation) -> [ApolloInterceptor] { + var interceptors = super.interceptors(for: operation) + interceptors.insert(TokenAddingInterceptor(), at: 0) + return interceptors + } +} ``` -Next, you need to make sure that Apollo knows that this delegate exists. To do that, you need to do something that Apollo Client has thus far been doing for you under the hood: instantiating the `HTTPNetworkTransport`. +> Another way to do this would be to copy the interceptors provided by the `LegacyInterceptorProvider` (which are all public), and then place your interceptors in the points in the array where you want them. However, since in this case we can run this interceptor first, it's just as simple to subclass. -In the primary declaration of `Network`, update your `lazy var` to create this transport and set the `Network` object as its delegate, then pass it through to the `ApolloClient`: +Next, go back to your `Network` class. Replace the `ApolloClient` with an updated `lazy var` which creates the `RequestChainNetworkTransport` manually, using your custom interceptor provider: ```swift:title=Network.swift -private(set) lazy var apollo: ApolloClient = { - let httpNetworkTransport = HTTPNetworkTransport(url: URL(string: "https://apollo-fullstack-tutorial.herokuapp.com/")!) - httpNetworkTransport.delegate = self - return ApolloClient(networkTransport: httpNetworkTransport) -}() +class Network { + static let shared = Network() + + private(set) lazy var apollo: ApolloClient = { + let client = URLSessionClient() + let cache = InMemoryNormalizedCache() + let store = ApolloStore(cache: cache) + let provider = NetworkInterceptorProvider(client: client, store: store) + let url = URL(string: "https://apollo-fullstack-tutorial.herokuapp.com/")! + let transport = RequestChainNetworkTransport(interceptorProvider: provider, + endpointURL: url) + return ApolloClient(networkTransport: transport) + }() +} ``` +Now, go back to **TokenAddingInterceptor.swift**. Click on the line numbers to add a breakpoint at the line where you're instantiating the `Keychain`: -adding a breakpoint +adding a breakpoint -Build and run the application. Whenever a network request goes out, that breakpoint should now get hit. If you're logged in, your token will be sent to the server whenever you make a request. +Build and run the application. Whenever a network request goes out, that breakpoint should now get hit. If you're logged in, your token will be sent to the server whenever you make a request! ## Add Alert helper methods diff --git a/scripts/get-version.sh b/scripts/get-version.sh index 8d25d4b492..7bead58d8e 100755 --- a/scripts/get-version.sh +++ b/scripts/get-version.sh @@ -4,4 +4,4 @@ source "$(dirname "$0")/version-constants.sh" prefix="$VERSION_CONFIG_VAR = " version_config=$(cat $VERSION_CONFIG_FILE) -echo ${version_config:${#prefix}} +echo "${version_config:${#prefix}}"