├── .circleci └── config.yml ├── .gitattributes ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.yaml │ ├── feature_request.yaml │ └── question.yaml └── workflows │ ├── issue-close-user-survey.yml │ ├── pr-close.yml │ └── prometheus-project-add.yml ├── .gitignore ├── .gitleaks.toml ├── .gitmodules ├── .spi.yml ├── Apollo.podspec ├── ApolloTestSupport.podspec ├── CHANGELOG.md ├── CLI └── apollo-ios-cli.tar.gz ├── CONTRIBUTING.md ├── CodegenProposal.md ├── Design └── 3093-graphql-defer.md ├── LICENSE ├── Package.swift ├── Plugins └── InstallCLI │ └── InstallCLIPluginCommand.swift ├── README.md ├── ROADMAP.md ├── Sources ├── Apollo │ ├── AnyGraphQLResponse.swift │ ├── ApolloClient.swift │ ├── ApolloClientProtocol.swift │ ├── ApolloErrorInterceptor.swift │ ├── ApolloInterceptor.swift │ ├── ApolloStore.swift │ ├── Atomic.swift │ ├── AutomaticPersistedQueryInterceptor.swift │ ├── Bundle+Helpers.swift │ ├── CacheReadInterceptor.swift │ ├── CacheWriteInterceptor.swift │ ├── Cancellable.swift │ ├── Collection+Helpers.swift │ ├── Constants.swift │ ├── DataDict+Merging.swift │ ├── DataDictMapper.swift │ ├── DataLoader.swift │ ├── DefaultInterceptorProvider.swift │ ├── Dictionary+Helpers.swift │ ├── DispatchQueue+Optional.swift │ ├── Documentation.docc │ │ ├── Documentation.md │ │ └── Index.md │ ├── ExecutionSources │ │ ├── CacheDataExecutionSource.swift │ │ ├── NetworkResponseExecutionSource.swift │ │ └── SelectionSetModelExecutionSource.swift │ ├── FieldSelectionCollector.swift │ ├── GraphQLDependencyTracker.swift │ ├── GraphQLError.swift │ ├── GraphQLExecutionSource.swift │ ├── GraphQLExecutor.swift │ ├── GraphQLFile.swift │ ├── GraphQLGETTransformer.swift │ ├── GraphQLHTTPMethod.swift │ ├── GraphQLHTTPRequestError.swift │ ├── GraphQLQueryWatcher.swift │ ├── GraphQLResponse.swift │ ├── GraphQLResult.swift │ ├── GraphQLResultAccumulator.swift │ ├── GraphQLResultNormalizer.swift │ ├── GraphQLSelectionSetMapper.swift │ ├── HTTPRequest.swift │ ├── HTTPResponse.swift │ ├── HTTPURLResponse+Helpers.swift │ ├── InMemoryNormalizedCache.swift │ ├── IncrementalGraphQLResponse.swift │ ├── IncrementalGraphQLResult.swift │ ├── IncrementalJSONResponseParsingInterceptor.swift │ ├── InputValue+Evaluation.swift │ ├── InterceptorProvider.swift │ ├── InterceptorRequestChain.swift │ ├── JSONConverter.swift │ ├── JSONRequest.swift │ ├── JSONResponseParsingInterceptor.swift │ ├── JSONSerialization+Sorting.swift │ ├── JSONSerializationFormat.swift │ ├── MaxRetryInterceptor.swift │ ├── MultipartFormData.swift │ ├── MultipartResponseDeferParser.swift │ ├── MultipartResponseParsingInterceptor.swift │ ├── MultipartResponseSubscriptionParser.swift │ ├── NetworkFetchInterceptor.swift │ ├── NetworkTransport.swift │ ├── NormalizedCache.swift │ ├── PathComponent.swift │ ├── PossiblyDeferred.swift │ ├── Record.swift │ ├── RecordSet.swift │ ├── RequestBodyCreator.swift │ ├── RequestChain.swift │ ├── RequestChainNetworkTransport.swift │ ├── RequestClientMetadata.swift │ ├── RequestContext.swift │ ├── Resources │ │ └── PrivacyInfo.xcprivacy │ ├── ResponseCodeInterceptor.swift │ ├── ResponsePath.swift │ ├── SelectionSet+DictionaryIntializer.swift │ ├── SelectionSet+JSONInitializer.swift │ ├── TaskData.swift │ ├── URLSessionClient.swift │ └── UploadRequest.swift ├── ApolloAPI │ ├── AnyHashableConvertible.swift │ ├── CacheKeyInfo.swift │ ├── CacheReference.swift │ ├── DataDict.swift │ ├── Deferred.swift │ ├── Documentation.docc │ │ ├── Documentation.md │ │ ├── GraphQLEnum.md │ │ └── GraphQLNullable.md │ ├── FragmentProtocols.swift │ ├── GraphQLEnum.swift │ ├── GraphQLNullable.swift │ ├── GraphQLOperation.swift │ ├── InputValue.swift │ ├── JSON.swift │ ├── JSONDecodingError.swift │ ├── JSONStandardTypeConversions.swift │ ├── LocalCacheMutation.swift │ ├── ObjectData.swift │ ├── OutputTypeConvertible.swift │ ├── ParentType.swift │ ├── Resources │ │ └── PrivacyInfo.xcprivacy │ ├── ScalarTypes.swift │ ├── SchemaConfiguration.swift │ ├── SchemaMetadata.swift │ ├── SchemaTypes │ │ ├── EnumType.swift │ │ ├── InputObject.swift │ │ ├── Interface.swift │ │ ├── Object.swift │ │ └── Union.swift │ ├── Selection+Conditions.swift │ ├── Selection.swift │ └── SelectionSet.swift ├── ApolloSQLite │ ├── ApolloSQLiteDatabase.swift │ ├── Documentation.docc │ │ └── Documentation.md │ ├── JournalMode.swift │ ├── Resources │ │ └── PrivacyInfo.xcprivacy │ ├── SQLiteDatabase.swift │ ├── SQLiteNormalizedCache.swift │ └── SQLiteSerialization.swift ├── ApolloTestSupport │ ├── Field.swift │ ├── TestMock.swift │ └── TestMockSelectionSetMapper.swift └── ApolloWebSocket │ ├── DefaultImplementation │ ├── Compression.swift │ ├── SSLClientCertificate.swift │ ├── SSLSecurity.swift │ ├── WebSocket.swift │ └── WebSocketStream.swift │ ├── Documentation.docc │ └── Documentation.md │ ├── OperationMessage.swift │ ├── OperationMessageIdCreator.swift │ ├── Resources │ └── PrivacyInfo.xcprivacy │ ├── SplitNetworkTransport.swift │ ├── WebSocketClient.swift │ ├── WebSocketError.swift │ ├── WebSocketTask.swift │ └── WebSocketTransport.swift ├── Tests └── README.md ├── makefile └── scripts ├── cli-version-check.sh ├── download-cli.sh ├── get-version.sh └── version-constants.sh /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | secops: apollo/circleci-secops-orb@2.0.7 5 | 6 | workflows: 7 | security-scans: 8 | jobs: 9 | - secops/gitleaks: 10 | context: 11 | - platform-docker-ro 12 | - github-orb 13 | - secops-oidc 14 | git-base-revision: <<#pipeline.git.base_revision>><><> 15 | git-revision: << pipeline.git.revision >> 16 | - secops/semgrep: 17 | context: 18 | - secops-oidc 19 | - github-orb 20 | git-base-revision: <<#pipeline.git.base_revision>><><> 21 | fail-on-findings: true 22 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | docs/docc/Apollo.doccarchive/** linguist-generated=true -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by the Apollo SecOps team 2 | # Please customize this file as needed prior to merging. 3 | 4 | * @apollographql/client-swift 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yaml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Create a bug report 3 | labels: [ "bug", "needs investigation" ] 4 | body: 5 | - type: textarea 6 | attributes: 7 | label: Summary 8 | description: A clear and concise description of what the bug is. 9 | validations: 10 | required: true 11 | - type: input 12 | attributes: 13 | label: Version 14 | description: Make sure the bug is still happening on the latest version. Make sure you've read [`CHANGELOG.md`](https://github.com/apollographql/apollo-ios/blob/main/CHANGELOG.md) to check that a new version hasn't already addressed your problem. 15 | validations: 16 | required: true 17 | - type: textarea 18 | attributes: 19 | label: Steps to reproduce the behavior 20 | description: | 21 | Add context about the problem here. How it happened and how to reproduce it: 22 | - If your project is open source, a link to the project is greatly appreciated. 23 | - If not, please share as much information as possible to help to understand the problem: schema, queries, sample code, etc... 24 | - Add things that you already tried. 25 | validations: 26 | required: true 27 | - type: textarea 28 | attributes: 29 | label: Logs 30 | description: | 31 | Paste logs and full stacktrace here. 32 | You can also attach files by dragging them into the area. 33 | placeholder: This will be automatically formatted into code, so no need for backticks. 34 | render: shell 35 | validations: 36 | required: false 37 | - type: textarea 38 | attributes: 39 | label: Anything else? 40 | description: Links, references, more context, or anything that will give us more information about the issue you are encountering! 41 | validations: 42 | required: false 43 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yaml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest an idea for this project 3 | labels: [ "feature" ] 4 | body: 5 | - type: textarea 6 | attributes: 7 | label: Use case 8 | description: A clear and concise description of what the problem is. 9 | validations: 10 | required: true 11 | - type: textarea 12 | attributes: 13 | label: Describe the solution you'd like 14 | description: | 15 | A clear and concise description of what you want to happen. 16 | If you already have an idea of the API you would like, do not hesitate to add it to the issue. 17 | validations: 18 | required: false 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.yaml: -------------------------------------------------------------------------------- 1 | name: Question 2 | description: Ask a general question about the project 3 | labels: [ "question" ] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Please search the existing resources before asking your question: 9 | - [`CHANGELOG.md`](https://github.com/apollographql/apollo-ios/blob/main/CHANGELOG.md) 10 | - [Documentation](https://www.apollographql.com/docs/ios) 11 | - [GitHub issues](https://github.com/apollographql/apollo-ios/issues) 12 | - [StackOverflow](https://stackoverflow.com/questions/tagged/apollo-ios) 13 | - [Community forums](https://community.apollographql.com/tag/mobile) 14 | 15 | Please avoid duplicating issues and discussions. If you'd like a GitHub issue to be reopened, leave a message on the issue. 16 | - type: textarea 17 | attributes: 18 | label: Question 19 | placeholder: Please reconsider using this template and use one of the listed resources instead. 20 | validations: 21 | required: true 22 | -------------------------------------------------------------------------------- /.github/workflows/issue-close-user-survey.yml: -------------------------------------------------------------------------------- 1 | name: Issue Close User Survey 2 | 3 | on: 4 | issues: 5 | types: [closed] 6 | 7 | jobs: 8 | user-survey-comment: 9 | permissions: 10 | issues: write 11 | runs-on: ubuntu-latest 12 | if: github.repository == 'apollographql/apollo-ios' 13 | steps: 14 | - run: | 15 | if [ "$AUTHOR" == "MEMBER" ] && (( $COMMENTS == 0 )); then 16 | echo "Issue opened by member with no comments, skipping user survey." 17 | else 18 | gh issue comment "$NUMBER" --body "$BODY" 19 | fi 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | GH_REPO: ${{ github.repository }} 23 | NUMBER: ${{ github.event.issue.number }} 24 | COMMENTS: ${{ github.event.issue.comments }} 25 | AUTHOR: ${{ github.event.issue.author_association }} 26 | BODY: > 27 | Do you have any feedback for the maintainers? Please tell us by taking a [one-minute survey](https://docs.google.com/forms/d/e/1FAIpQLSczNDXfJne3ZUOXjk9Ursm9JYvhTh1_nFTDfdq3XBAFWCzplQ/viewform?usp=pp_url&entry.1170701325=Apollo+iOS&entry.204965213=GitHub+Issue). Your responses will help us understand Apollo iOS usage and allow us to serve you better. 28 | -------------------------------------------------------------------------------- /.github/workflows/pr-close.yml: -------------------------------------------------------------------------------- 1 | name: Close Pull Request 2 | 3 | on: 4 | pull_request_target: 5 | types: [opened] 6 | 7 | jobs: 8 | run: 9 | name: Close and Comment PR 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: superbrothers/close-pull-request@v3 13 | with: 14 | comment: "We do not accept PRs directly to the 'apollo-ios' repo. All development is done through the 'apollo-ios-dev' repo, please see the CONTRIBUTING guide for more information." -------------------------------------------------------------------------------- /.github/workflows/prometheus-project-add.yml: -------------------------------------------------------------------------------- 1 | name: Add newly opened issues to Prometheus Project 2 | on: 3 | issues: 4 | types: 5 | - opened 6 | 7 | jobs: 8 | add-to-project: 9 | name: Add issue to project 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Add to project 13 | uses: actions/add-to-project@v1.0.2 14 | with: 15 | project-url: https://github.com/orgs/apollographql/projects/21 16 | github-token: ${{ secrets.PROMETHEUS_PROJECT_ACCESS_SECRET }} 17 | - name: Set project variables 18 | if: ${{ success() }} 19 | uses: austenstone/project-update@v1 20 | with: 21 | project-number: 21 22 | item-id: ${{ github.event.number }} 23 | github-token: ${{ secrets.PROMETHEUS_PROJECT_ACCESS_SECRET }} 24 | organization: apollographql 25 | fields: Status,Project 26 | fields-value: Triage,Client (Swift) 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Build generated 2 | build/ 3 | DerivedData/ 4 | apollo-ios-cli 5 | !apollo-ios-cli/ 6 | 7 | ## Various settings 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata/ 17 | 18 | ## Other 19 | *.moved-aside 20 | *.xccheckout 21 | *.xcscmblueprint 22 | .DS_Store 23 | 24 | ## Visual Studio Code 25 | .vscode/launch.json 26 | 27 | ## Obj-C/Swift specific 28 | *.hmap 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | .build/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | **/Pods/ 50 | 51 | # Carthage 52 | # 53 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 54 | # Carthage/Checkouts 55 | 56 | Carthage/Build 57 | 58 | # Local Node Modules for Apollo CLI 59 | node_modules/ 60 | package-lock.json 61 | scripts/apollo 62 | scripts/apollo.tar.gz 63 | SwiftScripts/ApolloCLI 64 | Tests/ApolloCodegenTests/scripts 65 | Tests/ApolloCodegenTests/scripts directory 66 | Tests/ApolloCodegenTests/Schema 67 | Tests/ApolloCodegenTests/Output 68 | SwiftScripts/.build-** 69 | 70 | # Local Netlify folder 71 | .netlify 72 | 73 | # Generated js files we don't need committed 74 | Sources/ApolloCodegenLib/Frontend/dist/ 75 | 76 | # Local generated packages we don't need in the main project 77 | Sources/AnimalKingdomAPI/Generated/Package.swift 78 | Sources/AnimalKingdomAPI/Generated/Package.resolved 79 | Tests/TestCodeGenConfigurations/**/AnimalKingdomAPI/ 80 | Tests/TestCodeGenConfigurations/**/AnimalKingdomAPITestMocks 81 | Tests/TestCodeGenConfigurations/**/Package.resolved 82 | Tests/TestCodeGenConfigurations/**/Podfile.lock 83 | Tests/TestCodeGenConfigurations/**/TestMocks 84 | Tests/TestCodeGenConfigurations/EmbeddedInTarget-RelativeAbsolute/PackageOne/**/*.graphql.swift 85 | Tests/TestCodeGenConfigurations/EmbeddedInTarget-RelativeAbsolute/PackageTwo/Sources/PackageTwo 86 | !Tests/TestCodeGenConfigurations/**/SchemaConfiguration.swift 87 | !Tests/TestCodeGenConfigurations/Other-CustomTarget/AnimalKingdomAPI/AnimalKingdomAPI.h 88 | !Tests/TestCodeGenConfigurations/EmbeddedInTarget-RelativeAbsolute/PackageTwo/Sources/PackageTwo/PackageTwo.swift 89 | -------------------------------------------------------------------------------- /.gitleaks.toml: -------------------------------------------------------------------------------- 1 | # This file exists primarily to influence scheduled scans that Apollo runs of all repos in Apollo-managed orgs. 2 | # This is an Apollo-Internal link, but more information about these scans is available here: 3 | # https://apollographql.atlassian.net/wiki/spaces/SecOps/pages/81330213/Everything+Static+Application+Security+Testing#Scheduled-Scans.1 4 | # 5 | # Apollo is using Gitleaks (https://github.com/gitleaks/gitleaks) to run these scans. 6 | # However, this file is not something that Gitleaks natively consumes. This file is an 7 | # Apollo-convention. Prior to scanning a repo, Apollo merges 8 | # our standard Gitleaks configuration (which is largely just the Gitleaks-default config) with 9 | # this file if it exists in a repo. The combined config is then used to scan a repo. 10 | # 11 | # We did this because the natively-supported allowlisting functionality in Gitleaks didn't do everything we wanted 12 | # or wasn't as robust as we needed. For example, one of the allowlisting options offered by Gitleaks depends on the line number 13 | # on which a false positive secret exists to allowlist it. (https://github.com/gitleaks/gitleaks#gitleaksignore). 14 | # This creates a fairly fragile allowlisting mechanism. This file allows us to leverage the full capabilities of the Gitleaks rule syntax 15 | # to create allowlisting functionality. 16 | 17 | [[ rules ]] 18 | id = "high-entropy-base64" 19 | [ rules.allowlist ] 20 | commits = [ 21 | "2568a4c9921ccb04e8391200554bdd8897000fa6", 22 | 23 | ] 24 | 25 | [[ rules ]] 26 | id = "generic-api-key" 27 | [ rules.allowlist ] 28 | 29 | paths = [ 30 | # Allowlists a false positive detection at 31 | # https://github.com/apollographql/apollo-ios/blob/474554504e7e33cef2a71774f825d5b3947ff797/Tests/ApolloCodegenTests/TestHelpers/ASTMatchers.swift#L72 32 | # This was previously allowlisted via commit hash, but updating that rule 33 | # To support allowlisting false positive detections in the files below as well. 34 | '''Tests/ApolloCodegenTests/TestHelpers/ASTMatchers.swift''', 35 | 36 | # Allowlist the various high-entropy strings in xcscmblueprint files 37 | '''Apollo.xcodeproj/project.xcworkspace/xcshareddata/Apollo.xcscmblueprint$''', 38 | '''ApolloSQLite.xcodeproj/project.xcworkspace/xcshareddata/ApolloSQLite.xcscmblueprint$''', 39 | '''Apollo.xcworkspace/xcshareddata/Apollo.xcscmblueprint$''', 40 | ] 41 | 42 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "docs/renderer"] 2 | path = docs/renderer 3 | url = https://github.com/apollographql/swift-docc-render.git 4 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | 2 | version: 1 3 | builder: 4 | configs: 5 | - platform: ios 6 | scheme: Apollo 7 | - platform: macos-xcodebuild 8 | scheme: Apollo 9 | - platform: macos-xcodebuild-arm 10 | scheme: Apollo 11 | - platform: tvos 12 | scheme: Apollo 13 | - platform: watchos 14 | scheme: Apollo 15 | -------------------------------------------------------------------------------- /Apollo.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'Apollo' 3 | s.version = `scripts/get-version.sh` 4 | s.author = 'Apollo GraphQL' 5 | s.homepage = 'https://github.com/apollographql/apollo-ios' 6 | s.license = { :type => 'MIT', :file => 'LICENSE' } 7 | s.summary = "A GraphQL client for iOS, written in Swift." 8 | s.source = { :git => 'https://github.com/apollographql/apollo-ios.git', :tag => s.version } 9 | s.requires_arc = true 10 | s.swift_version = '5.6' 11 | s.default_subspecs = 'Core' 12 | s.ios.deployment_target = '12.0' 13 | s.osx.deployment_target = '10.14' 14 | s.tvos.deployment_target = '12.0' 15 | s.watchos.deployment_target = '5.0' 16 | s.visionos.deployment_target = '1.0' 17 | 18 | cli_binary_name = 'apollo-ios-cli' 19 | s.preserve_paths = [cli_binary_name] 20 | s.prepare_command = <<-CMD 21 | make unpack-cli 22 | CMD 23 | 24 | s.subspec 'Core' do |ss| 25 | ss.source_files = 'Sources/Apollo/**/*.swift','Sources/ApolloAPI/**/*.swift' 26 | ss.resource_bundles = {'Apollo' => ['Sources/Apollo/Resources/PrivacyInfo.xcprivacy']} 27 | end 28 | 29 | # Apollo provides exactly one persistent cache out-of-the-box, as a reasonable default choice for 30 | # those who require cache persistence. Third-party caches may use different storage mechanisms. 31 | s.subspec 'SQLite' do |ss| 32 | ss.source_files = 'Sources/ApolloSQLite/*.swift' 33 | ss.dependency 'Apollo/Core' 34 | ss.resource_bundles = { 35 | 'ApolloSQLite' => ['Sources/ApolloSQLite/Resources/PrivacyInfo.xcprivacy'] 36 | } 37 | end 38 | 39 | # Websocket and subscription support based on Starscream 40 | s.subspec 'WebSocket' do |ss| 41 | ss.source_files = 'Sources/ApolloWebSocket/**/*.swift' 42 | ss.dependency 'Apollo/Core' 43 | ss.resource_bundles = { 44 | 'ApolloWebSocket' => ['Sources/ApolloWebSocket/Resources/PrivacyInfo.xcprivacy'] 45 | } 46 | end 47 | 48 | end 49 | -------------------------------------------------------------------------------- /ApolloTestSupport.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | version = `scripts/get-version.sh` 3 | s.name = 'ApolloTestSupport' 4 | s.version = version 5 | s.author = 'Apollo GraphQL' 6 | s.homepage = 'https://github.com/apollographql/apollo-ios' 7 | s.license = { :type => 'MIT', :file => 'LICENSE' } 8 | s.summary = "TODO" 9 | s.source = { :git => 'https://github.com/apollographql/apollo-ios.git', :tag => s.version } 10 | s.requires_arc = true 11 | s.swift_version = '5.6' 12 | s.ios.deployment_target = '12.0' 13 | s.osx.deployment_target = '10.14' 14 | s.tvos.deployment_target = '12.0' 15 | s.watchos.deployment_target = '5.0' 16 | s.visionos.deployment_target = '1.0' 17 | 18 | s.source_files = 'Sources/ApolloTestSupport/*.swift' 19 | s.dependency 'Apollo', '= ' + version 20 | 21 | end 22 | -------------------------------------------------------------------------------- /CLI/apollo-ios-cli.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollographql/apollo-ios/b0007f917ae773fdaf4fe8222a324230eb2affe5/CLI/apollo-ios-cli.tar.gz -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | If you would like to contribute to this library, you should make your code modifications and pull requests in the the [apollo-ios-dev](https://github.com/apollographql/apollo-ios-dev) repository. For more information, see the contributor guide in the `apollo-ios-dev` repo [here](https://github.com/apollographql/apollo-ios-dev/tree/main/CONTRIBUTING.md) 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2022 Apollo Graph, Inc. (Formerly Meteor Development Group, Inc.) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.9 2 | // 3 | // The swift-tools-version declares the minimum version of Swift required to build this package. 4 | // Swift 5.9 is available from Xcode 15.0. 5 | 6 | 7 | import PackageDescription 8 | 9 | let package = Package( 10 | name: "Apollo", 11 | platforms: [ 12 | .iOS(.v12), 13 | .macOS(.v10_14), 14 | .tvOS(.v12), 15 | .watchOS(.v5), 16 | .visionOS(.v1), 17 | ], 18 | products: [ 19 | .library(name: "Apollo", targets: ["Apollo"]), 20 | .library(name: "ApolloAPI", targets: ["ApolloAPI"]), 21 | .library(name: "Apollo-Dynamic", type: .dynamic, targets: ["Apollo"]), 22 | .library(name: "ApolloSQLite", targets: ["ApolloSQLite"]), 23 | .library(name: "ApolloWebSocket", targets: ["ApolloWebSocket"]), 24 | .library(name: "ApolloTestSupport", targets: ["ApolloTestSupport"]), 25 | .plugin(name: "InstallCLI", targets: ["Install CLI"]) 26 | ], 27 | dependencies: [ 28 | ], 29 | targets: [ 30 | .target( 31 | name: "Apollo", 32 | dependencies: [ 33 | "ApolloAPI" 34 | ], 35 | resources: [ 36 | .copy("Resources/PrivacyInfo.xcprivacy") 37 | ], 38 | swiftSettings: [.enableUpcomingFeature("ExistentialAny")] 39 | ), 40 | .target( 41 | name: "ApolloAPI", 42 | dependencies: [], 43 | resources: [ 44 | .copy("Resources/PrivacyInfo.xcprivacy") 45 | ], 46 | swiftSettings: [.enableUpcomingFeature("ExistentialAny")] 47 | ), 48 | .target( 49 | name: "ApolloSQLite", 50 | dependencies: [ 51 | "Apollo", 52 | ], 53 | resources: [ 54 | .copy("Resources/PrivacyInfo.xcprivacy") 55 | ], 56 | swiftSettings: [.enableUpcomingFeature("ExistentialAny")] 57 | ), 58 | .target( 59 | name: "ApolloWebSocket", 60 | dependencies: [ 61 | "Apollo" 62 | ], 63 | resources: [ 64 | .copy("Resources/PrivacyInfo.xcprivacy") 65 | ], 66 | swiftSettings: [.enableUpcomingFeature("ExistentialAny")] 67 | ), 68 | .target( 69 | name: "ApolloTestSupport", 70 | dependencies: [ 71 | "Apollo", 72 | "ApolloAPI" 73 | ], 74 | swiftSettings: [.enableUpcomingFeature("ExistentialAny")] 75 | ), 76 | .plugin( 77 | name: "Install CLI", 78 | capability: .command( 79 | intent: .custom( 80 | verb: "apollo-cli-install", 81 | description: "Installs the Apollo iOS Command line interface."), 82 | permissions: [ 83 | .writeToPackageDirectory(reason: "Downloads and unzips the CLI executable into your project directory."), 84 | .allowNetworkConnections(scope: .all(ports: []), reason: "Downloads the Apollo iOS CLI executable from the GitHub Release.") 85 | ]), 86 | dependencies: [], 87 | path: "Plugins/InstallCLI" 88 | ) 89 | ] 90 | ) 91 | -------------------------------------------------------------------------------- /Plugins/InstallCLI/InstallCLIPluginCommand.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import PackagePlugin 3 | import os 4 | 5 | @main 6 | struct InstallCLIPluginCommand: CommandPlugin { 7 | 8 | enum Error: Swift.Error { 9 | case CannotDetermineXcodeVersion 10 | } 11 | 12 | func performCommand(context: PackagePlugin.PluginContext, arguments: [String]) async throws { 13 | let dependencies = context.package.dependencies 14 | try dependencies.forEach { dep in 15 | if dep.package.displayName == "Apollo" { 16 | let process = Process() 17 | let path = try context.tool(named: "sh").path 18 | process.executableURL = URL(fileURLWithPath: path.string) 19 | process.arguments = ["\(dep.package.directory)/scripts/download-cli.sh", context.package.directory.string] 20 | try process.run() 21 | process.waitUntilExit() 22 | } 23 | } 24 | } 25 | 26 | } 27 | 28 | #if canImport(XcodeProjectPlugin) 29 | import XcodeProjectPlugin 30 | 31 | extension InstallCLIPluginCommand: XcodeCommandPlugin { 32 | 33 | /// 👇 This entry point is called when operating on an Xcode project. 34 | func performCommand(context: XcodePluginContext, arguments: [String]) throws { 35 | let process = Process() 36 | let toolPath = try context.tool(named: "sh").path 37 | process.executableURL = URL(fileURLWithPath: toolPath.string) 38 | 39 | let downloadScriptPath = try downloadScriptPath(context: context) 40 | process.arguments = [downloadScriptPath, context.xcodeProject.directory.string] 41 | 42 | try process.run() 43 | process.waitUntilExit() 44 | } 45 | 46 | /// Used to get the location of the CLI download script. 47 | /// 48 | /// - Parameter context: Contextual information based on the plugin's stated intent and requirements. 49 | /// - Returns: The path to the download script used to fetch the CLI binary. 50 | private func downloadScriptPath(context: XcodePluginContext) throws -> String { 51 | let xcodeVersion = try xcodeVersion(context: context) 52 | let relativeScriptPath = "SourcePackages/checkouts/apollo-ios/scripts/download-cli.sh" 53 | let absoluteScriptPath: String 54 | 55 | if xcodeVersion.lexicographicallyPrecedes("16.3") { 56 | absoluteScriptPath = "\(context.pluginWorkDirectory)/../../../\(relativeScriptPath)" 57 | } else { 58 | absoluteScriptPath = "\(context.pluginWorkDirectory)/../../../../\(relativeScriptPath)" 59 | } 60 | 61 | return absoluteScriptPath 62 | } 63 | 64 | /// Used to get a string representation of Xcode in the current toolchain. 65 | /// 66 | /// - Parameter context: Contextual information based on the plugin's stated intent and requirements. 67 | /// - Returns: A string representation of the Xcode version. 68 | private func xcodeVersion(context: XcodePluginContext) throws -> String { 69 | let process = Process() 70 | let toolPath = try context.tool(named: "xcrun").path 71 | process.executableURL = URL(fileURLWithPath: toolPath.string) 72 | process.arguments = ["xcodebuild", "-version"] 73 | 74 | let outputPipe = Pipe() 75 | process.standardOutput = outputPipe 76 | 77 | try process.run() 78 | process.waitUntilExit() 79 | 80 | guard 81 | let outputData = try outputPipe.fileHandleForReading.readToEnd(), 82 | let output = String(data: outputData, encoding: .utf8) 83 | else { 84 | throw Error.CannotDetermineXcodeVersion 85 | } 86 | 87 | let xcodeVersionString = output.components(separatedBy: "\n")[0] 88 | guard !xcodeVersionString.isEmpty else { 89 | throw Error.CannotDetermineXcodeVersion 90 | } 91 | 92 | let versionString = xcodeVersionString 93 | .components(separatedBy: CharacterSet.decimalDigits.inverted) 94 | .compactMap({ $0.isEmpty ? nil : $0 }) 95 | .joined(separator: ".") 96 | 97 | return versionString 98 | } 99 | 100 | } 101 | #endif 102 | -------------------------------------------------------------------------------- /Sources/Apollo/AnyGraphQLResponse.swift: -------------------------------------------------------------------------------- 1 | #if !COCOAPODS 2 | import ApolloAPI 3 | #endif 4 | 5 | /// An abstract GraphQL response used for full and incremental responses. 6 | struct AnyGraphQLResponse { 7 | let body: JSONObject 8 | 9 | private let rootKey: CacheReference 10 | private let variables: GraphQLOperation.Variables? 11 | 12 | init( 13 | body: JSONObject, 14 | rootKey: CacheReference, 15 | variables: GraphQLOperation.Variables? 16 | ) { 17 | self.body = body 18 | self.rootKey = rootKey 19 | self.variables = variables 20 | } 21 | 22 | /// Call this function when you want to execute on an entire operation and its response data. 23 | /// This function should also be called to execute on the partial (initial) response of an 24 | /// operation with deferred selection sets. 25 | func execute< 26 | Accumulator: GraphQLResultAccumulator, 27 | Data: RootSelectionSet 28 | >( 29 | selectionSet: Data.Type, 30 | with accumulator: Accumulator 31 | ) throws -> Accumulator.FinalResult? { 32 | guard let dataEntry = body["data"] as? JSONObject else { 33 | return nil 34 | } 35 | 36 | return try executor.execute( 37 | selectionSet: Data.self, 38 | on: dataEntry, 39 | withRootCacheReference: rootKey, 40 | variables: variables, 41 | accumulator: accumulator 42 | ) 43 | } 44 | 45 | /// Call this function to execute on a specific selection set and its incremental response data. 46 | /// This is typically used when executing on deferred selections. 47 | func execute< 48 | Accumulator: GraphQLResultAccumulator, 49 | Operation: GraphQLOperation 50 | >( 51 | selectionSet: any Deferrable.Type, 52 | in operation: Operation.Type, 53 | with accumulator: Accumulator 54 | ) throws -> Accumulator.FinalResult? { 55 | guard let dataEntry = body["data"] as? JSONObject else { 56 | return nil 57 | } 58 | 59 | return try executor.execute( 60 | selectionSet: selectionSet, 61 | in: Operation.self, 62 | on: dataEntry, 63 | withRootCacheReference: rootKey, 64 | variables: variables, 65 | accumulator: accumulator 66 | ) 67 | } 68 | 69 | var executor: GraphQLExecutor { 70 | GraphQLExecutor(executionSource: NetworkResponseExecutionSource()) 71 | } 72 | 73 | func parseErrors() -> [GraphQLError]? { 74 | guard let errorsEntry = self.body["errors"] as? [JSONObject] else { 75 | return nil 76 | } 77 | 78 | return errorsEntry.map(GraphQLError.init) 79 | } 80 | 81 | func parseExtensions() -> JSONObject? { 82 | return self.body["extensions"] as? JSONObject 83 | } 84 | } 85 | 86 | // MARK: - Equatable Conformance 87 | 88 | extension AnyGraphQLResponse: Equatable { 89 | static func == (lhs: AnyGraphQLResponse, rhs: AnyGraphQLResponse) -> Bool { 90 | lhs.body == rhs.body && 91 | lhs.rootKey == rhs.rootKey && 92 | lhs.variables?._jsonEncodableObject._jsonValue == rhs.variables?._jsonEncodableObject._jsonValue 93 | } 94 | } 95 | 96 | // MARK: - Hashable Conformance 97 | 98 | extension AnyGraphQLResponse: Hashable { 99 | func hash(into hasher: inout Hasher) { 100 | hasher.combine(body) 101 | hasher.combine(rootKey) 102 | hasher.combine(variables?._jsonEncodableObject._jsonValue) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Sources/Apollo/ApolloErrorInterceptor.swift: -------------------------------------------------------------------------------- 1 | #if !COCOAPODS 2 | import ApolloAPI 3 | #endif 4 | 5 | /// An error interceptor called to allow further examination of error data when an error occurs in the chain. 6 | public protocol ApolloErrorInterceptor { 7 | 8 | /// Asynchronously handles the receipt of an error at any point in the chain. 9 | /// 10 | /// - Parameters: 11 | /// - error: The received error 12 | /// - chain: The chain the error was received on 13 | /// - request: The request, as far as it was constructed 14 | /// - response: [optional] The response, if one was received 15 | /// - 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. 16 | func handleErrorAsync( 17 | error: any Error, 18 | chain: any RequestChain, 19 | request: HTTPRequest, 20 | response: HTTPResponse?, 21 | completion: @escaping (Result, any Error>) -> Void) 22 | } 23 | -------------------------------------------------------------------------------- /Sources/Apollo/ApolloInterceptor.swift: -------------------------------------------------------------------------------- 1 | #if !COCOAPODS 2 | import ApolloAPI 3 | #endif 4 | 5 | /// A protocol to set up a chainable unit of networking work. 6 | public protocol ApolloInterceptor { 7 | 8 | /// Used to uniquely identify this interceptor from other interceptors in a request chain. 9 | /// 10 | /// Each operation request has it's own interceptor request chain so the interceptors do not 11 | /// need to be uniquely identifiable between each and every request, only unique between the 12 | /// list of interceptors in a single request. 13 | var id: String { get } 14 | 15 | /// Called when this interceptor should do its work. 16 | /// 17 | /// - Parameters: 18 | /// - chain: The chain the interceptor is a part of. 19 | /// - request: The request, as far as it has been constructed 20 | /// - response: [optional] The response, if received 21 | /// - completion: The completion block to fire when data needs to be returned to the UI. 22 | func interceptAsync( 23 | chain: any RequestChain, 24 | request: HTTPRequest, 25 | response: HTTPResponse?, 26 | completion: @escaping (Result, any Error>) -> Void) 27 | } 28 | -------------------------------------------------------------------------------- /Sources/Apollo/Atomic.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Wrapper for a value protected by an `NSLock` 4 | @propertyWrapper 5 | public class Atomic { 6 | private let lock = NSLock() 7 | private var _value: T 8 | 9 | /// Designated initializer 10 | /// 11 | /// - Parameter value: The value to begin with. 12 | public init(wrappedValue: T) { 13 | _value = wrappedValue 14 | } 15 | 16 | /// The current value. Read-only. To update the underlying value, use ``mutate(block:)``. 17 | /// 18 | /// Allowing the ``wrappedValue`` to be set using a setter can cause concurrency issues when 19 | /// mutating the value of a wrapped value type such as an `Array`. This is due to the copying of 20 | /// value types as described in [this article](https://www.donnywals.com/why-your-atomic-property-wrapper-doesnt-work-for-collection-types/). 21 | public var wrappedValue: T { 22 | get { 23 | lock.lock() 24 | defer { lock.unlock() } 25 | return _value 26 | } 27 | } 28 | 29 | public var projectedValue: Atomic { self } 30 | 31 | /// Mutates the underlying value within a lock. 32 | /// 33 | /// - Parameter block: The block executed to mutate the value. 34 | /// - Returns: The value returned by the block. 35 | public func mutate(block: (inout T) -> U) -> U { 36 | lock.lock() 37 | defer { lock.unlock() } 38 | return block(&_value) 39 | } 40 | } 41 | 42 | public extension Atomic where T : Numeric { 43 | 44 | /// Increments the wrapped `Int` atomically, adding +1 to the value. 45 | @discardableResult 46 | func increment() -> T { 47 | lock.lock() 48 | defer { lock.unlock() } 49 | 50 | _value += 1 51 | return _value 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/Apollo/AutomaticPersistedQueryInterceptor.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if !COCOAPODS 3 | import ApolloAPI 4 | #endif 5 | 6 | public struct AutomaticPersistedQueryInterceptor: ApolloInterceptor { 7 | 8 | public enum APQError: LocalizedError, Equatable { 9 | case noParsedResponse 10 | case persistedQueryNotFoundForPersistedOnlyQuery(operationName: String) 11 | case persistedQueryRetryFailed(operationName: String) 12 | 13 | public var errorDescription: String? { 14 | switch self { 15 | case .noParsedResponse: 16 | return "The Automatic Persisted Query Interceptor was called before a response was received. Double-check the order of your interceptors." 17 | case .persistedQueryRetryFailed(let operationName): 18 | return "Persisted query retry failed for operation \"\(operationName)\"." 19 | 20 | case .persistedQueryNotFoundForPersistedOnlyQuery(let operationName): 21 | return "The Persisted Query for operation \"\(operationName)\" was not found. The operation is a `.persistedOnly` operation and cannot be automatically persisted if it is not recognized by the server." 22 | 23 | } 24 | } 25 | } 26 | 27 | public var id: String = UUID().uuidString 28 | 29 | /// Designated initializer 30 | public init() {} 31 | 32 | public func interceptAsync( 33 | chain: any RequestChain, 34 | request: HTTPRequest, 35 | response: HTTPResponse?, 36 | completion: @escaping (Result, any Error>) -> Void) { 37 | 38 | guard let jsonRequest = request as? JSONRequest, 39 | jsonRequest.autoPersistQueries else { 40 | // Not a request that handles APQs, continue along 41 | chain.proceedAsync( 42 | request: request, 43 | response: response, 44 | interceptor: self, 45 | completion: completion 46 | ) 47 | return 48 | } 49 | 50 | guard let result = response?.parsedResponse else { 51 | // This is in the wrong order - this needs to be parsed before we can check it. 52 | chain.handleErrorAsync( 53 | APQError.noParsedResponse, 54 | request: request, 55 | response: response, 56 | completion: completion 57 | ) 58 | return 59 | } 60 | 61 | guard let errors = result.errors else { 62 | // No errors were returned so no retry is necessary, continue along. 63 | chain.proceedAsync( 64 | request: request, 65 | response: response, 66 | interceptor: self, 67 | completion: completion 68 | ) 69 | return 70 | } 71 | 72 | let errorMessages = errors.compactMap { $0.message } 73 | guard errorMessages.contains("PersistedQueryNotFound") else { 74 | // The errors were not APQ errors, continue along. 75 | chain.proceedAsync( 76 | request: request, 77 | response: response, 78 | interceptor: self, 79 | completion: completion 80 | ) 81 | return 82 | } 83 | 84 | guard !jsonRequest.isPersistedQueryRetry else { 85 | // We already retried this and it didn't work. 86 | chain.handleErrorAsync( 87 | APQError.persistedQueryRetryFailed(operationName: Operation.operationName), 88 | request: jsonRequest, 89 | response: response, 90 | completion: completion 91 | ) 92 | 93 | return 94 | } 95 | 96 | if Operation.operationDocument.definition == nil { 97 | chain.handleErrorAsync( 98 | APQError.persistedQueryNotFoundForPersistedOnlyQuery(operationName: Operation.operationName), 99 | request: jsonRequest, 100 | response: response, 101 | completion: completion 102 | ) 103 | 104 | return 105 | } 106 | 107 | // We need to retry this query with the full body. 108 | jsonRequest.isPersistedQueryRetry = true 109 | chain.retry(request: jsonRequest, completion: completion) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Sources/Apollo/Bundle+Helpers.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Bundle { 4 | 5 | /// Type-safe getter for info dictionary key objects 6 | /// 7 | /// - Parameter key: The key to try to grab an object for 8 | /// - Returns: The object of the desired type, or nil if it is not present or of the incorrect type. 9 | private func bundleValue(forKey key: String) -> T? { 10 | return object(forInfoDictionaryKey: key) as? T 11 | } 12 | 13 | /// The bundle identifier of this bundle, or nil if not present. 14 | var bundleIdentifier: String? { 15 | return self.bundleValue(forKey: String(kCFBundleIdentifierKey)) 16 | } 17 | 18 | /// The build number of this bundle (kCFBundleVersion) as a string, or nil if not present. 19 | var buildNumber: String? { 20 | return self.bundleValue(forKey: String(kCFBundleVersionKey)) 21 | } 22 | 23 | /// The short version string for this bundle, or nil if not present. 24 | var shortVersion: String? { 25 | return self.bundleValue(forKey: "CFBundleShortVersionString") 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/Apollo/CacheWriteInterceptor.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if !COCOAPODS 3 | import ApolloAPI 4 | #endif 5 | 6 | /// An interceptor which writes data to the cache, following the `HTTPRequest`'s `cachePolicy`. 7 | public struct CacheWriteInterceptor: ApolloInterceptor { 8 | 9 | public enum CacheWriteError: Error, LocalizedError { 10 | case noResponseToParse 11 | 12 | public var errorDescription: String? { 13 | switch self { 14 | case .noResponseToParse: 15 | return "The Cache Write Interceptor was called before a response was received to be parsed. Double-check the order of your interceptors." 16 | } 17 | } 18 | } 19 | 20 | public let store: ApolloStore 21 | public var id: String = UUID().uuidString 22 | 23 | /// Designated initializer 24 | /// 25 | /// - Parameter store: The store to use when writing to the cache. 26 | public init(store: ApolloStore) { 27 | self.store = store 28 | } 29 | 30 | public func interceptAsync( 31 | chain: any RequestChain, 32 | request: HTTPRequest, 33 | response: HTTPResponse?, 34 | completion: @escaping (Result, any Error>) -> Void) { 35 | 36 | guard !chain.isCancelled else { 37 | return 38 | } 39 | 40 | guard request.cachePolicy != .fetchIgnoringCacheCompletely else { 41 | // If we're ignoring the cache completely, we're not writing to it. 42 | chain.proceedAsync( 43 | request: request, 44 | response: response, 45 | interceptor: self, 46 | completion: completion 47 | ) 48 | return 49 | } 50 | 51 | guard let createdResponse = response else { 52 | chain.handleErrorAsync( 53 | CacheWriteError.noResponseToParse, 54 | request: request, 55 | response: response, 56 | completion: completion 57 | ) 58 | return 59 | } 60 | 61 | if let cacheRecords = createdResponse.cacheRecords { 62 | self.store.publish(records: cacheRecords, identifier: request.contextIdentifier) 63 | } 64 | 65 | chain.proceedAsync( 66 | request: request, 67 | response: createdResponse, 68 | interceptor: self, 69 | completion: completion 70 | ) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Sources/Apollo/Cancellable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// An object that can be used to cancel an in progress action. 4 | public protocol Cancellable: AnyObject { 5 | /// Cancel an in progress action. 6 | func cancel() 7 | } 8 | 9 | // MARK: - URL Session Conformance 10 | 11 | extension URLSessionTask: Cancellable {} 12 | 13 | // MARK: - Early-Exit Helper 14 | 15 | /// A class to return when we need to bail out of something which still needs to return `Cancellable`. 16 | public final class EmptyCancellable: Cancellable { 17 | 18 | // Needs to be public so this can be instantiated outside of the current framework. 19 | public init() {} 20 | 21 | public func cancel() { 22 | // Do nothing, an error occurred and there is nothing to cancel. 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/Apollo/Collection+Helpers.swift: -------------------------------------------------------------------------------- 1 | // MARK: - Unzipping 2 | // MARK: Arrays of tuples to tuples of arrays 3 | 4 | public func unzip(_ array: [(Element1?, Element2?)]) -> ([Element1], [Element2]) { 5 | var array1: [Element1] = [] 6 | var array2: [Element2] = [] 7 | 8 | for elements in array { 9 | if let element1 = elements.0 { array1.append(element1) } 10 | if let element2 = elements.1 { array2.append(element2) } 11 | } 12 | 13 | return (array1, array2) 14 | } 15 | 16 | public func unzip(_ array: [(Element1?, Element2?, Element3?)]) -> ([Element1], [Element2], [Element3]) { 17 | var array1: [Element1] = [] 18 | var array2: [Element2] = [] 19 | var array3: [Element3] = [] 20 | 21 | for elements in array { 22 | if let element1 = elements.0 { array1.append(element1) } 23 | if let element2 = elements.1 { array2.append(element2) } 24 | if let element3 = elements.2 { array3.append(element3) } 25 | } 26 | 27 | return (array1, array2, array3) 28 | } 29 | -------------------------------------------------------------------------------- /Sources/Apollo/Constants.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum Constants { 4 | public static let ApolloClientName = "apollo-ios" 5 | public static let ApolloClientVersion: String = "1.22.0" 6 | 7 | @available(*, deprecated, renamed: "ApolloClientVersion") 8 | public static let ApolloVersion: String = ApolloClientVersion 9 | } 10 | -------------------------------------------------------------------------------- /Sources/Apollo/DataDictMapper.swift: -------------------------------------------------------------------------------- 1 | #if !COCOAPODS 2 | import ApolloAPI 3 | #endif 4 | 5 | /// An accumulator that converts executed data to the correct values for use in a selection set. 6 | @_spi(Execution) 7 | public class DataDictMapper: GraphQLResultAccumulator { 8 | 9 | public let requiresCacheKeyComputation: Bool = false 10 | 11 | let handleMissingValues: HandleMissingValues 12 | 13 | public enum HandleMissingValues { 14 | case disallow 15 | case allowForOptionalFields 16 | /// Using this option will result in an unsafe `SelectionSet` that will crash 17 | /// when a required field that has missing data is accessed. 18 | case allowForAllFields 19 | } 20 | 21 | init(handleMissingValues: HandleMissingValues = .disallow) { 22 | self.handleMissingValues = handleMissingValues 23 | } 24 | 25 | public func accept(scalar: AnyHashable, info: FieldExecutionInfo) throws -> AnyHashable? { 26 | switch info.field.type.namedType { 27 | case let .scalar(decodable as any JSONDecodable.Type): 28 | // This will convert a JSON value to the expected value type. 29 | return try decodable.init(_jsonValue: scalar)._asAnyHashable 30 | default: 31 | preconditionFailure() 32 | } 33 | } 34 | 35 | public func accept(customScalar: AnyHashable, info: FieldExecutionInfo) throws -> AnyHashable? { 36 | switch info.field.type.namedType { 37 | case let .customScalar(decodable as any JSONDecodable.Type): 38 | // This will convert a JSON value to the expected value type, 39 | // which could be a custom scalar or an enum. 40 | return try decodable.init(_jsonValue: customScalar)._asAnyHashable 41 | default: 42 | preconditionFailure() 43 | } 44 | } 45 | 46 | public func acceptNullValue(info: FieldExecutionInfo) -> AnyHashable? { 47 | return DataDict._NullValue 48 | } 49 | 50 | public func acceptMissingValue(info: FieldExecutionInfo) throws -> AnyHashable? { 51 | switch handleMissingValues { 52 | case .allowForOptionalFields where info.field.type.isNullable: fallthrough 53 | case .allowForAllFields: 54 | return nil 55 | 56 | default: 57 | throw JSONDecodingError.missingValue 58 | } 59 | } 60 | 61 | public func accept(list: [AnyHashable?], info: FieldExecutionInfo) -> AnyHashable? { 62 | return list 63 | } 64 | 65 | public func accept(childObject: DataDict, info: FieldExecutionInfo) throws -> AnyHashable? { 66 | return childObject 67 | } 68 | 69 | public func accept( 70 | fieldEntry: AnyHashable?, 71 | info: FieldExecutionInfo 72 | ) -> (key: String, value: AnyHashable)? { 73 | guard let fieldEntry = fieldEntry else { return nil } 74 | return (info.responseKeyForField, fieldEntry) 75 | } 76 | 77 | public func accept( 78 | fieldEntries: [(key: String, value: AnyHashable)], 79 | info: ObjectExecutionInfo 80 | ) throws -> DataDict { 81 | return DataDict( 82 | data: .init(fieldEntries, uniquingKeysWith: { (_, last) in last }), 83 | fulfilledFragments: info.fulfilledFragments, 84 | deferredFragments: info.deferredFragments 85 | ) 86 | } 87 | 88 | public func finish(rootValue: DataDict, info: ObjectExecutionInfo) -> DataDict { 89 | return rootValue 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Sources/Apollo/DataLoader.swift: -------------------------------------------------------------------------------- 1 | final class DataLoader { 2 | public typealias BatchLoad = (Set) throws -> [Key: Value] 3 | private var batchLoad: BatchLoad 4 | 5 | private var cache: [Key: Result] = [:] 6 | private var pendingLoads: Set = [] 7 | 8 | public init(_ batchLoad: @escaping BatchLoad) { 9 | self.batchLoad = batchLoad 10 | } 11 | 12 | subscript(key: Key) -> PossiblyDeferred { 13 | if let cachedResult = cache[key] { 14 | return .immediate(cachedResult) 15 | } 16 | 17 | pendingLoads.insert(key) 18 | 19 | return .deferred { try self.load(key) } 20 | } 21 | 22 | private func load(_ key: Key) throws -> Value? { 23 | if let cachedResult = cache[key] { 24 | return try cachedResult.get() 25 | } 26 | 27 | assert(pendingLoads.contains(key)) 28 | 29 | let values = try batchLoad(pendingLoads) 30 | 31 | for key in pendingLoads { 32 | cache[key] = .success(values[key]) 33 | } 34 | 35 | pendingLoads.removeAll() 36 | 37 | return values[key] 38 | } 39 | 40 | func removeAll() { 41 | cache.removeAll() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/Apollo/DefaultInterceptorProvider.swift: -------------------------------------------------------------------------------- 1 | #if !COCOAPODS 2 | import ApolloAPI 3 | #endif 4 | 5 | /// The default interceptor provider for typescript-generated code 6 | open class DefaultInterceptorProvider: InterceptorProvider { 7 | 8 | private let client: URLSessionClient 9 | private let store: ApolloStore 10 | private let shouldInvalidateClientOnDeinit: Bool 11 | 12 | /// Designated initializer 13 | /// 14 | /// - Parameters: 15 | /// - client: The `URLSessionClient` to use. Defaults to the default setup. 16 | /// - 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. 17 | /// - store: The `ApolloStore` to use when reading from or writing to the cache. Make sure you pass the same store to the `ApolloClient` instance you're planning to use. 18 | public init(client: URLSessionClient = URLSessionClient(), 19 | shouldInvalidateClientOnDeinit: Bool = true, 20 | store: ApolloStore) { 21 | self.client = client 22 | self.shouldInvalidateClientOnDeinit = shouldInvalidateClientOnDeinit 23 | self.store = store 24 | } 25 | 26 | deinit { 27 | if self.shouldInvalidateClientOnDeinit { 28 | self.client.invalidate() 29 | } 30 | } 31 | 32 | open func interceptors( 33 | for operation: Operation 34 | ) -> [any ApolloInterceptor] { 35 | return [ 36 | MaxRetryInterceptor(), 37 | CacheReadInterceptor(store: self.store), 38 | NetworkFetchInterceptor(client: self.client), 39 | ResponseCodeInterceptor(), 40 | MultipartResponseParsingInterceptor(), 41 | jsonParsingInterceptor(for: operation), 42 | AutomaticPersistedQueryInterceptor(), 43 | CacheWriteInterceptor(store: self.store), 44 | ] 45 | } 46 | 47 | private func jsonParsingInterceptor(for operation: Operation) -> any ApolloInterceptor { 48 | if Operation.hasDeferredFragments { 49 | return IncrementalJSONResponseParsingInterceptor() 50 | 51 | } else { 52 | return JSONResponseParsingInterceptor() 53 | } 54 | } 55 | 56 | open func additionalErrorInterceptor(for operation: Operation) -> (any ApolloErrorInterceptor)? { 57 | return nil 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/Apollo/Dictionary+Helpers.swift: -------------------------------------------------------------------------------- 1 | public extension Dictionary { 2 | static func += (lhs: inout Dictionary, rhs: Dictionary) { 3 | lhs.merge(rhs) { (_, new) in new } 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /Sources/Apollo/DispatchQueue+Optional.swift: -------------------------------------------------------------------------------- 1 | import Dispatch 2 | 3 | extension DispatchQueue { 4 | 5 | static func performAsyncIfNeeded(on callbackQueue: DispatchQueue?, action: @escaping () -> Void) { 6 | if let callbackQueue = callbackQueue { 7 | // A callback queue was provided, perform the action on that queue 8 | callbackQueue.async { 9 | action() 10 | } 11 | } else { 12 | // Perform the action on the current queue 13 | action() 14 | } 15 | } 16 | 17 | static func returnResultAsyncIfNeeded(on callbackQueue: DispatchQueue?, 18 | action: ((Result) -> Void)?, 19 | result: Result) { 20 | if let action = action { 21 | self.performAsyncIfNeeded(on: callbackQueue) { 22 | action(result) 23 | } 24 | } else if case .failure(let error) = result { 25 | debugPrint("Apollo: Encountered failure result, but no completion handler was defined to handle it: \(error)") 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/Apollo/Documentation.docc/Documentation.md: -------------------------------------------------------------------------------- 1 | # ``Apollo`` 2 | 3 | A Strongly typed, Swift-first, GraphQL client. 4 | 5 | ## Overview 6 | 7 | The core Apollo client library. This library includes the networking client, normalized cache, and GraphQL executor. 8 | -------------------------------------------------------------------------------- /Sources/Apollo/Documentation.docc/Index.md: -------------------------------------------------------------------------------- 1 | # Apollo iOS 2 | 3 | @Metadata { 4 | @TechnologyRoot 5 | } 6 | 7 | API Reference Documentation for Apollo iOS. 8 | 9 | ## Overview 10 | 11 | Our API reference is automatically generated directly from the inline comments in our code. If you're contributing to Apollo, all you have to do is add inline documentation comments, conforming to [DocC formatting guidelines](https://developer.apple.com/documentation/xcode/writing-symbol-documentation-in-your-source-files), and they will appear here. 12 | 13 | See something missing in the documentation? Add inline documentation comments to the code, and open a pull request! 14 | 15 | To run the documentation generator, cd into the SwiftScripts folder and run 16 | 17 | ```bash 18 | swift run DocumentationGenerator 19 | ``` 20 | 21 | ## Libraries 22 | 23 | **[Apollo](/documentation/apollo)** 24 | 25 | The core Apollo client library. 26 | 27 | **[ApolloAPI](/documentation/apolloapi)** 28 | 29 | The internal models shared by the [``Apollo``](/documentation/apollo) client and the models generated by [``ApolloCodegenLib``](/documentation/apollocodegenlib) 30 | 31 | **[ApolloCodegenLib](/documentation/apollocodegenlib)** 32 | 33 | The code generation engine used to generate model objects for an application from a GraphQL schema and operation set. 34 | 35 | **[ApolloSQLite](/documentation/apollosqlite)** 36 | 37 | A [`NormalizedCache`](/documentation/apollo/normalizedcache) implementation backed by a `SQLite` database. 38 | 39 | **[ApolloWebSocket](/documentation/apollowebsocket)** 40 | 41 | A web socket network transport implementation that provides support for [`GraphQLSubscription`](/documentation/apolloapi/graphqlsubscription) operations over a web socket connection. 42 | 43 | **[ApolloPagination](/documentation/apollopagination)** 44 | 45 | A library that provides support for fetching and watching paginated queries with [``Apollo``](/documentation/apollo). 46 | -------------------------------------------------------------------------------- /Sources/Apollo/ExecutionSources/CacheDataExecutionSource.swift: -------------------------------------------------------------------------------- 1 | #if !COCOAPODS 2 | import ApolloAPI 3 | #endif 4 | 5 | /// A `GraphQLExecutionSource` configured to execute upon the data stored in a ``NormalizedCache``. 6 | /// 7 | /// Each object exposed by the cache is represented as a `Record`. 8 | struct CacheDataExecutionSource: GraphQLExecutionSource { 9 | typealias RawObjectData = Record 10 | typealias FieldCollector = CacheDataFieldSelectionCollector 11 | 12 | /// A `weak` reference to the transaction the cache data is being read from during execution. 13 | /// This transaction is used to resolve references to other objects in the cache during field 14 | /// value resolution. 15 | /// 16 | /// This property is `weak` to ensure there is not a retain cycle between the transaction and the 17 | /// execution pipeline. If the transaction has been deallocated, execution cannot continue 18 | /// against the cache data. 19 | weak var transaction: ApolloStore.ReadTransaction? 20 | 21 | /// Used to determine whether deferred selections within a selection set should be executed at the same 22 | /// time as the other selections. 23 | /// 24 | /// When executing on cache data all selections, including deferred, must be executed together because 25 | /// there is only a single response from the cache data. Any deferred selection that was cached will 26 | /// be returned in the response. 27 | var shouldAttemptDeferredFragmentExecution: Bool { true } 28 | 29 | init(transaction: ApolloStore.ReadTransaction) { 30 | self.transaction = transaction 31 | } 32 | 33 | func resolveField( 34 | with info: FieldExecutionInfo, 35 | on object: Record 36 | ) -> PossiblyDeferred { 37 | PossiblyDeferred { 38 | let value = try object[info.cacheKeyForField()] 39 | 40 | switch value { 41 | case let reference as CacheReference: 42 | return deferredResolve(reference: reference).map { $0 as AnyHashable } 43 | 44 | case let referenceList as [JSONValue]: 45 | return referenceList 46 | .enumerated() 47 | .deferredFlatMap { index, element in 48 | guard let cacheReference = element as? CacheReference else { 49 | return .immediate(.success(element)) 50 | } 51 | 52 | return self.deferredResolve(reference: cacheReference) 53 | .mapError { error in 54 | if !(error is GraphQLExecutionError) { 55 | return GraphQLExecutionError( 56 | path: info.responsePath.appending(String(index)), 57 | underlying: error 58 | ) 59 | } else { 60 | return error 61 | } 62 | }.map { $0 as AnyHashable } 63 | }.map { $0._asAnyHashable } 64 | 65 | default: 66 | return .immediate(.success(value)) 67 | } 68 | } 69 | } 70 | 71 | private func deferredResolve(reference: CacheReference) -> PossiblyDeferred { 72 | guard let transaction else { 73 | return .immediate(.failure(ApolloStore.Error.notWithinReadTransaction)) 74 | } 75 | 76 | return transaction.loadObject(forKey: reference.key) 77 | } 78 | 79 | func computeCacheKey( 80 | for object: Record, 81 | in schema: any SchemaMetadata.Type, 82 | inferredToImplementInterface interface: Interface? 83 | ) -> CacheKey? { 84 | return object.key 85 | } 86 | 87 | /// A wrapper around the `DefaultFieldSelectionCollector` that maps the `Record` object to it's 88 | /// `fields` representing the object's data. 89 | struct CacheDataFieldSelectionCollector: FieldSelectionCollector { 90 | static func collectFields( 91 | from selections: [Selection], 92 | into groupedFields: inout FieldSelectionGrouping, 93 | for object: Record, 94 | info: ObjectExecutionInfo 95 | ) throws { 96 | return try DefaultFieldSelectionCollector.collectFields( 97 | from: selections, 98 | into: &groupedFields, 99 | for: object.fields, 100 | info: info 101 | ) 102 | } 103 | } 104 | } 105 | 106 | 107 | -------------------------------------------------------------------------------- /Sources/Apollo/ExecutionSources/NetworkResponseExecutionSource.swift: -------------------------------------------------------------------------------- 1 | #if !COCOAPODS 2 | import ApolloAPI 3 | #endif 4 | 5 | /// A `GraphQLExecutionSource` configured to execute upon the JSON data from the network response 6 | /// for a GraphQL operation. 7 | @_spi(Execution) 8 | public struct NetworkResponseExecutionSource: GraphQLExecutionSource, CacheKeyComputingExecutionSource { 9 | public typealias RawObjectData = JSONObject 10 | public typealias FieldCollector = DefaultFieldSelectionCollector 11 | 12 | /// Used to determine whether deferred selections within a selection set should be executed at the same 13 | /// time as the other selections. 14 | /// 15 | /// When executing on a network response, deferred selections are not executed at the same time as the 16 | /// other selections because they are sent from the server as independent responses, are parsed 17 | /// sequentially, and the results are returned separately. 18 | public var shouldAttemptDeferredFragmentExecution: Bool { false } 19 | 20 | public init() {} 21 | 22 | public func resolveField( 23 | with info: FieldExecutionInfo, 24 | on object: JSONObject 25 | ) -> PossiblyDeferred { 26 | .immediate(.success(object[info.responseKeyForField])) 27 | } 28 | 29 | public func opaqueObjectDataWrapper(for rawData: JSONObject) -> ObjectData { 30 | ObjectData(_transformer: DataTransformer(), _rawData: rawData) 31 | } 32 | 33 | struct DataTransformer: _ObjectData_Transformer { 34 | func transform(_ value: AnyHashable) -> (any ScalarType)? { 35 | switch value { 36 | case let scalar as any ScalarType: 37 | return scalar 38 | case let customScalar as any CustomScalarType: 39 | return customScalar._jsonValue as? (any ScalarType) 40 | default: return nil 41 | } 42 | } 43 | 44 | func transform(_ value: AnyHashable) -> ObjectData? { 45 | switch value { 46 | case let object as JSONObject: 47 | return ObjectData(_transformer: self, _rawData: object) 48 | default: return nil 49 | } 50 | } 51 | 52 | func transform(_ value: AnyHashable) -> ListData? { 53 | switch value { 54 | case let list as [AnyHashable]: 55 | return ListData(_transformer: self, _rawData: list) 56 | default: return nil 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Sources/Apollo/ExecutionSources/SelectionSetModelExecutionSource.swift: -------------------------------------------------------------------------------- 1 | #if !COCOAPODS 2 | import ApolloAPI 3 | #endif 4 | 5 | /// A `GraphQLExecutionSource` designed for use when the data source is a generated model's 6 | /// `SelectionSet` data. 7 | struct SelectionSetModelExecutionSource: GraphQLExecutionSource, CacheKeyComputingExecutionSource { 8 | typealias RawObjectData = DataDict 9 | typealias FieldCollector = CustomCacheDataWritingFieldSelectionCollector 10 | 11 | var shouldAttemptDeferredFragmentExecution: Bool { false } 12 | 13 | func resolveField( 14 | with info: FieldExecutionInfo, 15 | on object: DataDict 16 | ) -> PossiblyDeferred { 17 | .immediate(.success(object._data[info.responseKeyForField])) 18 | } 19 | 20 | func opaqueObjectDataWrapper(for rawData: DataDict) -> ObjectData { 21 | ObjectData(_transformer: DataTransformer(), _rawData: rawData._data) 22 | } 23 | 24 | struct DataTransformer: _ObjectData_Transformer { 25 | func transform(_ value: AnyHashable) -> (any ScalarType)? { 26 | switch value { 27 | case let scalar as any ScalarType: 28 | return scalar 29 | case let customScalar as any CustomScalarType: 30 | return customScalar._jsonValue as? (any ScalarType) 31 | default: return nil 32 | } 33 | } 34 | 35 | func transform(_ value: AnyHashable) -> ObjectData? { 36 | switch value { 37 | case let object as DataDict: 38 | return ObjectData(_transformer: self, _rawData: object._data) 39 | default: return nil 40 | } 41 | } 42 | 43 | func transform(_ value: AnyHashable) -> ListData? { 44 | switch value { 45 | case let list as [AnyHashable]: 46 | return ListData(_transformer: self, _rawData: list) 47 | default: return nil 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/Apollo/GraphQLDependencyTracker.swift: -------------------------------------------------------------------------------- 1 | #if !COCOAPODS 2 | import ApolloAPI 3 | #endif 4 | 5 | final class GraphQLDependencyTracker: GraphQLResultAccumulator { 6 | 7 | let requiresCacheKeyComputation: Bool = true 8 | 9 | private var dependentKeys: Set = Set() 10 | 11 | func accept(scalar: JSONValue, info: FieldExecutionInfo) { 12 | dependentKeys.insert(info.cachePath.joined) 13 | } 14 | 15 | func accept(customScalar: JSONValue, info: FieldExecutionInfo) { 16 | dependentKeys.insert(info.cachePath.joined) 17 | } 18 | 19 | func acceptNullValue(info: FieldExecutionInfo) { 20 | dependentKeys.insert(info.cachePath.joined) 21 | } 22 | 23 | func acceptMissingValue(info: FieldExecutionInfo) throws -> () { 24 | dependentKeys.insert(info.cachePath.joined) 25 | } 26 | 27 | func accept(list: [Void], info: FieldExecutionInfo) { 28 | dependentKeys.insert(info.cachePath.joined) 29 | } 30 | 31 | func accept(childObject: Void, info: FieldExecutionInfo) { 32 | } 33 | 34 | func accept(fieldEntry: Void, info: FieldExecutionInfo) -> Void? { 35 | dependentKeys.insert(info.cachePath.joined) 36 | return () 37 | } 38 | 39 | func accept(fieldEntries: [Void], info: ObjectExecutionInfo) { 40 | } 41 | 42 | func finish(rootValue: Void, info: ObjectExecutionInfo) -> Set { 43 | return dependentKeys 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/Apollo/GraphQLError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if !COCOAPODS 3 | import ApolloAPI 4 | #endif 5 | 6 | /// Represents an error encountered during the execution of a GraphQL operation. 7 | /// 8 | /// - SeeAlso: [The Response Format section in the GraphQL specification](https://facebook.github.io/graphql/#sec-Response-Format) 9 | public struct GraphQLError: Error, Hashable { 10 | private let object: JSONObject 11 | 12 | public init(_ object: JSONObject) { 13 | self.object = object 14 | } 15 | 16 | init(_ message: String) { 17 | self.init(["message": message]) 18 | } 19 | 20 | /// GraphQL servers may provide additional entries as they choose to produce more helpful or machine‐readable errors. 21 | public subscript(key: String) -> Any? { 22 | return object[key] 23 | } 24 | 25 | /// A description of the error. 26 | public var message: String? { 27 | return self["message"] as? String 28 | } 29 | 30 | /// A list of locations in the requested GraphQL document associated with the error. 31 | public var locations: [Location]? { 32 | return (self["locations"] as? [JSONObject])?.compactMap(Location.init) 33 | } 34 | 35 | /// A path to the field that triggered the error, represented by an array of Path Entries. 36 | public var path: [PathEntry]? { 37 | return (self["path"] as? [JSONValue])?.compactMap(PathEntry.init) 38 | } 39 | 40 | /// A dictionary which services can use however they see fit to provide additional information in errors to clients. 41 | public var extensions: [String : Any]? { 42 | return self["extensions"] as? [String : Any] 43 | } 44 | 45 | /// Represents a location in a GraphQL document. 46 | public struct Location { 47 | /// The line number of a syntax element. 48 | public let line: Int 49 | /// The column number of a syntax element. 50 | public let column: Int 51 | 52 | init?(_ object: JSONObject) { 53 | guard let line = object["line"] as? Int, let column = object["column"] as? Int else { return nil } 54 | self.line = line 55 | self.column = column 56 | } 57 | } 58 | 59 | public typealias PathEntry = PathComponent 60 | } 61 | 62 | extension GraphQLError: CustomStringConvertible { 63 | public var description: String { 64 | return self.message ?? "GraphQL Error" 65 | } 66 | } 67 | 68 | extension GraphQLError: LocalizedError { 69 | public var errorDescription: String? { 70 | return self.description 71 | } 72 | } 73 | 74 | extension GraphQLError { 75 | func asJSONDictionary() -> [String: Any] { 76 | JSONConverter.convert(self) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Sources/Apollo/GraphQLExecutionSource.swift: -------------------------------------------------------------------------------- 1 | #if !COCOAPODS 2 | import ApolloAPI 3 | #endif 4 | 5 | /// A protocol representing a data source for GraphQL data to be executed upon by a 6 | /// `GraphQLExecutor`. 7 | /// 8 | /// Based on the source of execution data, the way we handle portions of the execution pipeline will 9 | /// be different. Each implementation of this protocol provides the necessary implementations for 10 | /// executing upon data from a specific source. 11 | @_spi(Execution) 12 | public protocol GraphQLExecutionSource { 13 | /// The type that represents each object in data from the source. 14 | associatedtype RawObjectData 15 | 16 | /// The type of `FieldSelectionCollector` used for the selection grouping step of 17 | /// GraphQL execution. 18 | associatedtype FieldCollector: FieldSelectionCollector 19 | 20 | /// Used to determine whether deferred selections within a selection set should be executed at the same 21 | /// time as the other selections. 22 | var shouldAttemptDeferredFragmentExecution: Bool { get } 23 | 24 | /// Resolves the value for given field on a data object from the source. 25 | /// 26 | /// Because data may be loaded from a database, these loads are batched for performance reasons. 27 | /// By returning a `PossiblyDeferred` wrapper, we allow `ApolloStore` to use a `DataLoader` that 28 | /// will defer loading the next batch of records from the cache until they are needed. 29 | /// 30 | /// - Returns: The value for the field represented by the `info` on the `object`. 31 | /// For a field with a scalar value, this should be a raw JSON value. 32 | /// For fields whose type is an object, this should be of the source's `ObjectData` type or 33 | /// a `CacheReference` that can be resolved by the source. 34 | func resolveField( 35 | with info: FieldExecutionInfo, 36 | on object: RawObjectData 37 | ) -> PossiblyDeferred 38 | 39 | /// Returns the cache key for an object to be used during GraphQL execution. 40 | /// - Parameters: 41 | /// - object: The data for the object from the source. 42 | /// - schema: The schema that the type the object data represents belongs to. 43 | /// - implementedInterface: An optional ``Interface`` that the object is 44 | /// inferred to implement. If the cache key is being resolved for a selection set with an 45 | /// interface as it's `__parentType`, you can infer the object must implement that interface. 46 | /// You should provide that interface to this parameter. 47 | /// - Returns: A cache key for normalizing the object in the cache. If `nil` is returned the 48 | /// object is assumed to be stored in the cache with no normalization. The executor will 49 | /// construct a cache key based on the object's path in its enclosing operation. 50 | func computeCacheKey( 51 | for object: RawObjectData, 52 | in schema: any SchemaMetadata.Type, 53 | inferredToImplementInterface implementedInterface: Interface? 54 | ) -> CacheKey? 55 | } 56 | 57 | /// A type of `GraphQLExecutionSource` that uses the user defined cache key computation 58 | /// defined in the ``SchemaConfiguration``. 59 | @_spi(Execution) 60 | public protocol CacheKeyComputingExecutionSource: GraphQLExecutionSource { 61 | /// A function that should return an `ObjectData` wrapper that performs and custom 62 | /// transformations required to transform the raw object data from the source into a consistent 63 | /// format to be exposed to the user's ``SchemaConfiguration/cacheKeyInfo(for:object:)`` function. 64 | func opaqueObjectDataWrapper(for: RawObjectData) -> ObjectData 65 | } 66 | 67 | extension CacheKeyComputingExecutionSource { 68 | @_spi(Execution) public func computeCacheKey( 69 | for object: RawObjectData, 70 | in schema: any SchemaMetadata.Type, 71 | inferredToImplementInterface implementedInterface: Interface? 72 | ) -> CacheKey? { 73 | let dataWrapper = opaqueObjectDataWrapper(for: object) 74 | return schema.cacheKey(for: dataWrapper, inferredToImplementInterface: implementedInterface) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Sources/Apollo/GraphQLFile.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A file which can be uploaded to a GraphQL server 4 | public struct GraphQLFile: Hashable { 5 | public let fieldName: String 6 | public let originalName: String 7 | public let mimeType: String 8 | public let data: Data? 9 | public let fileURL: URL? 10 | public let contentLength: UInt64 11 | 12 | public enum GraphQLFileError: Error, LocalizedError { 13 | case couldNotCreateInputStream 14 | case couldNotGetFileSize(fileURL: URL) 15 | 16 | public var errorDescription: String? { 17 | switch self { 18 | case .couldNotCreateInputStream: 19 | return "An input stream could not be created from either the passed-in file URL or data. Please check that you've passed at least one of these, and that for files you have proper permission to stream data." 20 | case .couldNotGetFileSize(let fileURL): 21 | return "Apollo could not get the file size for the file at \(fileURL). This likely indicates either a) The file is not at that URL or b) a permissions issue." 22 | } 23 | } 24 | } 25 | 26 | /// A convenience constant for declaring your mimetype is octet-stream. 27 | public static let octetStreamMimeType = "application/octet-stream" 28 | 29 | /// Convenience initializer for raw data 30 | /// 31 | /// - Parameters: 32 | /// - fieldName: The name of the field this file is being sent for 33 | /// - originalName: The original name of the file 34 | /// - mimeType: The mime type of the file to send to the server. Defaults to `GraphQLFile.octetStreamMimeType`. 35 | /// - data: The raw data to send for the file. 36 | public init(fieldName: String, 37 | originalName: String, 38 | mimeType: String = GraphQLFile.octetStreamMimeType, 39 | data: Data) { 40 | self.fieldName = fieldName 41 | self.originalName = originalName 42 | self.mimeType = mimeType 43 | self.data = data 44 | self.fileURL = nil 45 | self.contentLength = UInt64(data.count) 46 | } 47 | 48 | /// Throwing convenience initializer for files in the filesystem 49 | /// 50 | /// - Parameters: 51 | /// - fieldName: The name of the field this file is being sent for 52 | /// - originalName: The original name of the file 53 | /// - mimeType: The mime type of the file to send to the server. Defaults to `GraphQLFile.octetStreamMimeType`. 54 | /// - fileURL: The URL of the file to upload. 55 | /// - Throws: If the file's size could not be determined 56 | public init(fieldName: String, 57 | originalName: String, 58 | mimeType: String = GraphQLFile.octetStreamMimeType, 59 | fileURL: URL) throws { 60 | self.contentLength = try GraphQLFile.getFileSize(fileURL: fileURL) 61 | self.fieldName = fieldName 62 | self.originalName = originalName 63 | self.mimeType = mimeType 64 | self.data = nil 65 | self.fileURL = fileURL 66 | } 67 | 68 | /// Uses either the data or the file URL to create an 69 | /// `InputStream` that can be used to stream data into 70 | /// a multipart-form. 71 | /// 72 | /// - Returns: The created `InputStream`. 73 | /// - Throws: If an input stream could not be created from either data or a file URL. 74 | public func generateInputStream() throws -> InputStream { 75 | if let data = data { 76 | return InputStream(data: data) 77 | } else if let fileURL = fileURL, 78 | let inputStream = InputStream(url: fileURL) { 79 | return inputStream 80 | } else { 81 | throw GraphQLFileError.couldNotCreateInputStream 82 | } 83 | } 84 | 85 | private static func getFileSize(fileURL: URL) throws -> UInt64 { 86 | guard let fileSizeAttribute = try? FileManager.default.attributesOfItem(atPath: fileURL.path)[.size], 87 | let fileSize = fileSizeAttribute as? NSNumber else { 88 | throw GraphQLFileError.couldNotGetFileSize(fileURL: fileURL) 89 | } 90 | 91 | return fileSize.uint64Value 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Sources/Apollo/GraphQLGETTransformer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if !COCOAPODS 3 | import ApolloAPI 4 | #endif 5 | 6 | public struct GraphQLGETTransformer { 7 | 8 | let body: JSONEncodableDictionary 9 | let url: URL 10 | 11 | /// A helper for transforming a `JSONEncodableDictionary` that can be sent with a `POST` request into a URL with query parameters for a `GET` request. 12 | /// 13 | /// - Parameters: 14 | /// - body: The `JSONEncodableDictionary` to transform from the body of a `POST` request 15 | /// - url: The base url to append the query to. 16 | public init(body: JSONEncodableDictionary, url: URL) { 17 | self.body = body 18 | self.url = url 19 | } 20 | 21 | /// Creates the get URL. 22 | /// 23 | /// - Returns: [optional] The created get URL or nil if the provided information couldn't be used to access the appropriate parameters. 24 | public func createGetURL() -> URL? { 25 | guard var components = URLComponents(string: self.url.absoluteString) else { 26 | return nil 27 | } 28 | 29 | var queryItems: [URLQueryItem] = components.queryItems ?? [] 30 | 31 | do { 32 | _ = try self.body.sorted(by: {$0.key < $1.key}).compactMap({ arg in 33 | if let value = arg.value as? JSONEncodableDictionary { 34 | let data = try JSONSerialization.sortedData(withJSONObject: value._jsonValue) 35 | if let string = String(data: data, encoding: .utf8) { 36 | queryItems.append(URLQueryItem(name: arg.key, value: string)) 37 | } 38 | } else if let string = arg.value as? String { 39 | queryItems.append(URLQueryItem(name: arg.key, value: string)) 40 | } else if (arg.key != "variables") { 41 | assertionFailure() 42 | } 43 | }) 44 | } catch { 45 | return nil 46 | } 47 | 48 | if !queryItems.isEmpty { 49 | components.queryItems = queryItems 50 | } 51 | 52 | components.percentEncodedQuery = 53 | components.percentEncodedQuery?.replacingOccurrences(of: "+", with: "%2B") 54 | 55 | return components.url 56 | } 57 | } 58 | 59 | // MARK: - Hashable Conformance 60 | 61 | extension GraphQLGETTransformer: Hashable { 62 | public static func == (lhs: GraphQLGETTransformer, rhs: GraphQLGETTransformer) -> Bool { 63 | lhs.body._jsonValue == rhs.body._jsonValue && 64 | lhs.url == rhs.url 65 | } 66 | 67 | public func hash(into hasher: inout Hasher) { 68 | hasher.combine(body._jsonValue) 69 | hasher.combine(url) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Sources/Apollo/GraphQLHTTPMethod.swift: -------------------------------------------------------------------------------- 1 | /// Supported HTTP methods for Apollo 2 | enum GraphQLHTTPMethod: String, Hashable { 3 | case GET 4 | case POST 5 | } 6 | -------------------------------------------------------------------------------- /Sources/Apollo/GraphQLHTTPRequestError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// An error which has occurred during the serialization of a request. 4 | public enum GraphQLHTTPRequestError: Error, LocalizedError, Hashable { 5 | case serializedBodyMessageError 6 | case serializedQueryParamsMessageError 7 | 8 | public var errorDescription: String? { 9 | switch self { 10 | case .serializedBodyMessageError: 11 | return "JSONSerialization error: Error while serializing request's body" 12 | case .serializedQueryParamsMessageError: 13 | return "QueryParams error: Error while serializing variables as query parameters." 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/Apollo/GraphQLResponse.swift: -------------------------------------------------------------------------------- 1 | #if !COCOAPODS 2 | import ApolloAPI 3 | #endif 4 | 5 | /// Represents a complete GraphQL response received from a server. 6 | public final class GraphQLResponse { 7 | private let base: AnyGraphQLResponse 8 | 9 | public init( 10 | operation: Operation, 11 | body: JSONObject 12 | ) where Operation.Data == Data { 13 | self.base = AnyGraphQLResponse( 14 | body: body, 15 | rootKey: CacheReference.rootCacheReference(for: Operation.operationType), 16 | variables: operation.__variables 17 | ) 18 | } 19 | 20 | /// Parses the response into a `GraphQLResult` and a `RecordSet` depending on the cache policy. The result can be 21 | /// sent to a completion block for a request and the `RecordSet` can be merged into a local cache. 22 | /// 23 | /// - Returns: A tuple of a `GraphQLResult` and an optional `RecordSet`. 24 | /// 25 | /// - Parameter cachePolicy: Used to determine whether a cache `RecordSet` is returned. A cache policy that does 26 | /// not read or write to the cache will return a `nil` cache `RecordSet`. 27 | public func parseResult(withCachePolicy cachePolicy: CachePolicy) throws -> (GraphQLResult, RecordSet?) { 28 | switch cachePolicy { 29 | case .fetchIgnoringCacheCompletely: 30 | // There is no cache, so we don't need to get any info on dependencies. Use fast parsing. 31 | return (try parseResultFast(), nil) 32 | 33 | default: 34 | return try parseResult() 35 | } 36 | } 37 | 38 | /// Parses a response into a `GraphQLResult` and a `RecordSet`. The result can be sent to a completion block for a 39 | /// request and the `RecordSet` can be merged into a local cache. 40 | /// 41 | /// - Returns: A `GraphQLResult` and a `RecordSet`. 42 | public func parseResult() throws -> (GraphQLResult, RecordSet?) { 43 | let accumulator = zip( 44 | GraphQLSelectionSetMapper(), 45 | ResultNormalizerFactory.networkResponseDataNormalizer(), 46 | GraphQLDependencyTracker() 47 | ) 48 | let executionResult = try base.execute( 49 | selectionSet: Data.self, 50 | with: accumulator 51 | ) 52 | let result = makeResult(data: executionResult?.0, dependentKeys: executionResult?.2) 53 | 54 | return (result, executionResult?.1) 55 | } 56 | 57 | /// Parses a response into a `GraphQLResult` for use without the cache. This parsing does not 58 | /// create dependent keys or a `RecordSet` for the cache. 59 | /// 60 | /// This is faster than `parseResult()` and should be used when cache the response is not needed. 61 | public func parseResultFast() throws -> GraphQLResult { 62 | let accumulator = GraphQLSelectionSetMapper() 63 | let data = try base.execute( 64 | selectionSet: Data.self, 65 | with: accumulator 66 | ) 67 | 68 | return makeResult(data: data, dependentKeys: nil) 69 | } 70 | 71 | private func makeResult(data: Data?, dependentKeys: Set?) -> GraphQLResult { 72 | return GraphQLResult( 73 | data: data, 74 | extensions: base.parseExtensions(), 75 | errors: base.parseErrors(), 76 | source: .server, 77 | dependentKeys: dependentKeys 78 | ) 79 | } 80 | } 81 | 82 | // MARK: - Equatable Conformance 83 | 84 | extension GraphQLResponse: Equatable where Data: Equatable { 85 | public static func == (lhs: GraphQLResponse, rhs: GraphQLResponse) -> Bool { 86 | lhs.base == rhs.base 87 | } 88 | } 89 | 90 | // MARK: - Hashable Conformance 91 | 92 | extension GraphQLResponse: Hashable { 93 | public func hash(into hasher: inout Hasher) { 94 | hasher.combine(base) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Sources/Apollo/GraphQLResultNormalizer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if !COCOAPODS 3 | import ApolloAPI 4 | #endif 5 | 6 | enum ResultNormalizerFactory { 7 | 8 | static func selectionSetDataNormalizer() -> SelectionSetDataResultNormalizer { 9 | SelectionSetDataResultNormalizer() 10 | } 11 | 12 | static func networkResponseDataNormalizer() -> RawJSONResultNormalizer { 13 | RawJSONResultNormalizer() 14 | } 15 | } 16 | 17 | class BaseGraphQLResultNormalizer: GraphQLResultAccumulator { 18 | 19 | let requiresCacheKeyComputation: Bool = true 20 | 21 | private var records: RecordSet = [:] 22 | 23 | fileprivate init() {} 24 | 25 | final func accept(scalar: JSONValue, info: FieldExecutionInfo) -> JSONValue? { 26 | return scalar 27 | } 28 | 29 | func accept(customScalar: JSONValue, info: FieldExecutionInfo) -> JSONValue? { 30 | return customScalar 31 | } 32 | 33 | final func acceptNullValue(info: FieldExecutionInfo) -> JSONValue? { 34 | return NSNull() 35 | } 36 | 37 | final func acceptMissingValue(info: FieldExecutionInfo) -> JSONValue? { 38 | return nil 39 | } 40 | 41 | final func accept(list: [JSONValue?], info: FieldExecutionInfo) -> JSONValue? { 42 | return list 43 | } 44 | 45 | final func accept(childObject: CacheReference, info: FieldExecutionInfo) -> JSONValue? { 46 | return childObject 47 | } 48 | 49 | final func accept(fieldEntry: JSONValue?, info: FieldExecutionInfo) throws -> (key: String, value: JSONValue)? { 50 | guard let fieldEntry else { return nil } 51 | return (try info.cacheKeyForField(), fieldEntry) 52 | } 53 | 54 | final func accept( 55 | fieldEntries: [(key: String, value: JSONValue)], 56 | info: ObjectExecutionInfo 57 | ) throws -> CacheReference { 58 | let cachePath = info.cachePath.joined 59 | 60 | let object = JSONObject(fieldEntries, uniquingKeysWith: { (_, last) in last }) 61 | records.merge(record: Record(key: cachePath, object)) 62 | 63 | return CacheReference(cachePath) 64 | } 65 | 66 | final func finish(rootValue: CacheReference, info: ObjectExecutionInfo) throws -> RecordSet { 67 | return records 68 | } 69 | } 70 | 71 | final class RawJSONResultNormalizer: BaseGraphQLResultNormalizer {} 72 | 73 | final class SelectionSetDataResultNormalizer: BaseGraphQLResultNormalizer { 74 | override final func accept(customScalar: JSONValue, info: FieldExecutionInfo) -> JSONValue? { 75 | if let customScalar = customScalar as? (any JSONEncodable) { 76 | return customScalar._jsonValue 77 | } 78 | return customScalar 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Sources/Apollo/GraphQLSelectionSetMapper.swift: -------------------------------------------------------------------------------- 1 | #if !COCOAPODS 2 | import ApolloAPI 3 | #endif 4 | 5 | /// An accumulator that maps executed data to create a `SelectionSet`. 6 | @_spi(Execution) 7 | public final class GraphQLSelectionSetMapper: GraphQLResultAccumulator { 8 | 9 | let dataDictMapper: DataDictMapper 10 | 11 | public var requiresCacheKeyComputation: Bool { 12 | dataDictMapper.requiresCacheKeyComputation 13 | } 14 | 15 | public var handleMissingValues: DataDictMapper.HandleMissingValues { 16 | dataDictMapper.handleMissingValues 17 | } 18 | 19 | public init(handleMissingValues: DataDictMapper.HandleMissingValues = .disallow) { 20 | self.dataDictMapper = DataDictMapper(handleMissingValues: handleMissingValues) 21 | } 22 | 23 | public func accept(scalar: AnyHashable, info: FieldExecutionInfo) throws -> AnyHashable? { 24 | try dataDictMapper.accept(scalar: scalar, info: info) 25 | } 26 | 27 | public func accept(customScalar: AnyHashable, info: FieldExecutionInfo) throws -> AnyHashable? { 28 | try dataDictMapper.accept(customScalar: customScalar, info: info) 29 | } 30 | 31 | public func acceptNullValue(info: FieldExecutionInfo) -> AnyHashable? { 32 | return DataDict._NullValue 33 | } 34 | 35 | public func acceptMissingValue(info: FieldExecutionInfo) throws -> AnyHashable? { 36 | switch handleMissingValues { 37 | case .allowForOptionalFields where info.field.type.isNullable: fallthrough 38 | case .allowForAllFields: 39 | return nil 40 | 41 | default: 42 | throw JSONDecodingError.missingValue 43 | } 44 | } 45 | 46 | public func accept(list: [AnyHashable?], info: FieldExecutionInfo) -> AnyHashable? { 47 | return list 48 | } 49 | 50 | public func accept(childObject: DataDict, info: FieldExecutionInfo) throws -> AnyHashable? { 51 | return childObject 52 | } 53 | 54 | public func accept(fieldEntry: AnyHashable?, info: FieldExecutionInfo) -> (key: String, value: AnyHashable)? { 55 | guard let fieldEntry = fieldEntry else { return nil } 56 | return (info.responseKeyForField, fieldEntry) 57 | } 58 | 59 | public func accept( 60 | fieldEntries: [(key: String, value: AnyHashable)], 61 | info: ObjectExecutionInfo 62 | ) throws -> DataDict { 63 | return DataDict( 64 | data: .init(fieldEntries, uniquingKeysWith: { (_, last) in last }), 65 | fulfilledFragments: info.fulfilledFragments, 66 | deferredFragments: info.deferredFragments 67 | ) 68 | } 69 | 70 | public func finish(rootValue: DataDict, info: ObjectExecutionInfo) -> T { 71 | return T.init(_dataDict: rootValue) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Sources/Apollo/HTTPResponse.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if !COCOAPODS 3 | import ApolloAPI 4 | #endif 5 | 6 | /// Data about a response received by an HTTP request. 7 | public class HTTPResponse { 8 | 9 | /// The `HTTPURLResponse` received from the URL loading system 10 | public var httpResponse: HTTPURLResponse 11 | 12 | /// The raw data received from the URL loading system 13 | public var rawData: Data 14 | 15 | /// [optional] The data as parsed into a `GraphQLResult`, which can eventually be returned to the UI. Will be nil 16 | /// if not yet parsed. 17 | public var parsedResponse: GraphQLResult? 18 | 19 | /// [optional] The data as parsed into a `GraphQLResponse` for legacy caching purposes. If you're not using the 20 | /// `JSONResponseParsingInterceptor`, you probably shouldn't be using this property. 21 | @available(*, deprecated, message: "Do not use. This property will be removed in a future version.") 22 | public var legacyResponse: GraphQLResponse? { _legacyResponse } 23 | var _legacyResponse: GraphQLResponse? = nil 24 | 25 | /// A set of cache records from the response 26 | public var cacheRecords: RecordSet? 27 | 28 | /// Designated initializer 29 | /// 30 | /// - Parameters: 31 | /// - response: The `HTTPURLResponse` received from the server. 32 | /// - rawData: The raw, unparsed data received from the server. 33 | /// - parsedResponse: [optional] The response parsed into the `ParsedValue` type. Will be nil if not yet parsed, 34 | /// or if parsing failed. 35 | public init( 36 | response: HTTPURLResponse, 37 | rawData: Data, 38 | parsedResponse: GraphQLResult? 39 | ) { 40 | self.httpResponse = response 41 | self.rawData = rawData 42 | self.parsedResponse = parsedResponse 43 | } 44 | } 45 | 46 | // MARK: - Equatable Conformance 47 | 48 | extension HTTPResponse: Equatable where Operation.Data: Equatable { 49 | public static func == (lhs: HTTPResponse, rhs: HTTPResponse) -> Bool { 50 | lhs.httpResponse == rhs.httpResponse && 51 | lhs.rawData == rhs.rawData && 52 | lhs.parsedResponse == rhs.parsedResponse && 53 | lhs._legacyResponse == rhs._legacyResponse && 54 | lhs.cacheRecords == rhs.cacheRecords 55 | } 56 | } 57 | 58 | // MARK: - Hashable Conformance 59 | 60 | extension HTTPResponse: Hashable where Operation.Data: Hashable { 61 | public func hash(into hasher: inout Hasher) { 62 | hasher.combine(httpResponse) 63 | hasher.combine(rawData) 64 | hasher.combine(parsedResponse) 65 | hasher.combine(_legacyResponse) 66 | hasher.combine(cacheRecords) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Sources/Apollo/HTTPURLResponse+Helpers.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: Status extensions 4 | extension HTTPURLResponse { 5 | var isSuccessful: Bool { 6 | return (200..<300).contains(statusCode) 7 | } 8 | } 9 | 10 | // MARK: Multipart extensions 11 | extension HTTPURLResponse { 12 | /// Returns true if the `Content-Type` HTTP header contains the `multipart/mixed` MIME type. 13 | var isMultipart: Bool { 14 | return (allHeaderFields["Content-Type"] as? String)?.contains("multipart/mixed") ?? false 15 | } 16 | 17 | struct MultipartHeaderComponents { 18 | let media: String? 19 | let boundary: String? 20 | let `protocol`: String? 21 | 22 | init(media: String? = nil, boundary: String? = nil, protocol: String? = nil) { 23 | self.media = media 24 | self.boundary = boundary 25 | self.protocol = `protocol` 26 | } 27 | } 28 | 29 | /// Components of the `Content-Type` header specifically related to the `multipart` media type. 30 | var multipartHeaderComponents: MultipartHeaderComponents { 31 | guard let contentType = allHeaderFields["Content-Type"] as? String else { 32 | return MultipartHeaderComponents() 33 | } 34 | 35 | var media: String? = nil 36 | var boundary: String? = nil 37 | var `protocol`: String? = nil 38 | 39 | for component in contentType.components(separatedBy: ";") { 40 | let directive = component.trimmingCharacters(in: .whitespaces) 41 | 42 | if directive.starts(with: "multipart/") { 43 | media = directive.components(separatedBy: "/").last 44 | continue 45 | } 46 | 47 | if directive.starts(with: "boundary=") { 48 | if let markerEndIndex = directive.firstIndex(of: "=") { 49 | var startIndex = directive.index(markerEndIndex, offsetBy: 1) 50 | if directive[startIndex] == "\"" { 51 | startIndex = directive.index(after: startIndex) 52 | } 53 | var endIndex = directive.index(before: directive.endIndex) 54 | if directive[endIndex] == "\"" { 55 | endIndex = directive.index(before: endIndex) 56 | } 57 | 58 | boundary = String(directive[startIndex...endIndex]) 59 | } 60 | continue 61 | } 62 | 63 | if directive.contains("Spec=") { 64 | `protocol` = directive 65 | continue 66 | } 67 | } 68 | 69 | return MultipartHeaderComponents(media: media, boundary: boundary, protocol: `protocol`) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Sources/Apollo/InMemoryNormalizedCache.swift: -------------------------------------------------------------------------------- 1 | public final class InMemoryNormalizedCache: NormalizedCache { 2 | private var records: RecordSet 3 | 4 | public init(records: RecordSet = RecordSet()) { 5 | self.records = records 6 | } 7 | 8 | public func loadRecords(forKeys keys: Set) throws -> [CacheKey: Record] { 9 | return keys.reduce(into: [:]) { result, key in 10 | result[key] = records[key] 11 | } 12 | } 13 | 14 | public func removeRecord(for key: CacheKey) throws { 15 | records.removeRecord(for: key) 16 | } 17 | 18 | public func merge(records newRecords: RecordSet) throws -> Set { 19 | return records.merge(records: newRecords) 20 | } 21 | 22 | public func removeRecords(matching pattern: CacheKey) throws { 23 | records.removeRecords(matching: pattern) 24 | } 25 | 26 | public func clear() { 27 | records.clear() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/Apollo/IncrementalGraphQLResult.swift: -------------------------------------------------------------------------------- 1 | #if !COCOAPODS 2 | import ApolloAPI 3 | #endif 4 | 5 | /// Represents an incremental result received as part of a deferred incremental response. 6 | /// 7 | /// This is not a type exposed to users as a final result, it is an intermediate result that is 8 | /// merged into a final result. 9 | struct IncrementalGraphQLResult { 10 | /// This is the same label identifier passed to the `@defer` directive associated with the 11 | /// response. 12 | let label: String 13 | /// Allows for the association to a particular field in a GraphQL result. This will be a list of 14 | /// path segments starting at the root of the response and ending with the field to be associated 15 | /// with. 16 | let path: [PathComponent] 17 | /// The typed result data, or `nil` if an error was encountered that prevented a valid response. 18 | let data: (any SelectionSet)? 19 | /// A list of errors, or `nil` if the operation completed without encountering any errors. 20 | let errors: [GraphQLError]? 21 | /// A dictionary which services can use however they see fit to provide additional information to clients. 22 | let extensions: [String: AnyHashable]? 23 | 24 | let dependentKeys: Set? 25 | 26 | init( 27 | label: String, 28 | path: [PathComponent], 29 | data: (any SelectionSet)?, 30 | extensions: [String: AnyHashable]?, 31 | errors: [GraphQLError]?, 32 | dependentKeys: Set? 33 | ) { 34 | self.label = label 35 | self.path = path 36 | self.data = data 37 | self.extensions = extensions 38 | self.errors = errors 39 | self.dependentKeys = dependentKeys 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/Apollo/InputValue+Evaluation.swift: -------------------------------------------------------------------------------- 1 | #if !COCOAPODS 2 | import ApolloAPI 3 | #endif 4 | import Foundation 5 | 6 | /// A global function that formats the the cache key for a field on an object. 7 | /// 8 | /// `CacheKeyForField` represents the *key for a single field on an object* in a ``NormalizedCache``. 9 | /// **This is not the cache key that represents an individual entity in the cache.** 10 | /// 11 | /// - Parameters: 12 | /// - fieldName: The name of the field to return a cache key for. 13 | /// - arguments: The list of arguments used to compute the cache key. 14 | /// - Returns: A formatted `String` to be used as the key for the field on an object in a 15 | /// ``NormalizedCache``. 16 | func CacheKeyForField(named fieldName: String, arguments: JSONObject) -> String { 17 | let argumentsKey = orderIndependentKey(for: arguments) 18 | return argumentsKey.isEmpty ? fieldName : "\(fieldName)(\(argumentsKey))" 19 | } 20 | 21 | fileprivate func orderIndependentKey(for object: JSONObject) -> String { 22 | return object.sorted { $0.key < $1.key }.map { 23 | switch $0.value { 24 | case let object as JSONObject: 25 | return "[\($0.key):\(orderIndependentKey(for: object))]" 26 | case let array as [JSONObject]: 27 | return "\($0.key):[\(array.map { orderIndependentKey(for: $0) }.joined(separator: ","))]" 28 | case let array as [JSONValue]: 29 | return "\($0.key):[\(array.map { String(describing: $0.base) }.joined(separator: ", "))]" 30 | case is NSNull: 31 | return "\($0.key):null" 32 | default: 33 | return "\($0.key):\($0.value.base)" 34 | } 35 | }.joined(separator: ",") 36 | } 37 | 38 | extension Selection.Field { 39 | public func cacheKey(with variables: GraphQLOperation.Variables?) throws -> String { 40 | if let arguments = arguments { 41 | let argumentValues = try InputValue.evaluate(arguments, with: variables) 42 | return CacheKeyForField(named: name, arguments: argumentValues) 43 | } else { 44 | return name 45 | } 46 | } 47 | } 48 | 49 | extension InputValue { 50 | private func evaluate(with variables: GraphQLOperation.Variables?) throws -> JSONValue? { 51 | switch self { 52 | case let .variable(name): 53 | guard let value = variables?[name] else { 54 | throw GraphQLError("Variable \"\(name)\" was not provided.") 55 | } 56 | return value._jsonEncodableValue?._jsonValue 57 | 58 | case let .scalar(value): 59 | return value._jsonValue 60 | 61 | case let .list(array): 62 | return try InputValue.evaluate(array, with: variables) 63 | 64 | case let .object(dictionary): 65 | return try InputValue.evaluate(dictionary, with: variables) 66 | 67 | case .null: 68 | return NSNull() 69 | } 70 | } 71 | 72 | fileprivate static func evaluate( 73 | _ values: [InputValue], 74 | with variables: GraphQLOperation.Variables? 75 | ) throws -> [JSONValue] { 76 | try values.compactMap { try $0.evaluate(with: variables) } 77 | } 78 | 79 | fileprivate static func evaluate( 80 | _ values: [String: InputValue], 81 | with variables: GraphQLOperation.Variables? 82 | ) throws -> JSONObject { 83 | try values.compactMapValues { try $0.evaluate(with: variables) } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Sources/Apollo/InterceptorProvider.swift: -------------------------------------------------------------------------------- 1 | #if !COCOAPODS 2 | import ApolloAPI 3 | #endif 4 | 5 | // MARK: - Basic protocol 6 | 7 | /// A protocol to allow easy creation of an array of interceptors for a given operation. 8 | public protocol InterceptorProvider { 9 | 10 | /// Creates a new array of interceptors when called 11 | /// 12 | /// - Parameter operation: The operation to provide interceptors for 13 | func interceptors(for operation: Operation) -> [any ApolloInterceptor] 14 | 15 | /// Provides an additional error interceptor for any additional handling of errors 16 | /// before returning to the UI, such as logging. 17 | /// - Parameter operation: The operation to provide an additional error interceptor for 18 | func additionalErrorInterceptor(for operation: Operation) -> (any ApolloErrorInterceptor)? 19 | } 20 | 21 | /// MARK: - Default Implementation 22 | 23 | public extension InterceptorProvider { 24 | 25 | func additionalErrorInterceptor(for operation: Operation) -> (any ApolloErrorInterceptor)? { 26 | return nil 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/Apollo/JSONConverter.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if !COCOAPODS 3 | import ApolloAPI 4 | #endif 5 | 6 | public enum JSONConverter { 7 | 8 | /// Converts a ``SelectionSet`` into a basic JSON dictionary for use. 9 | /// 10 | /// - Returns: A `[String: Any]` JSON dictionary representing the ``SelectionSet``. 11 | public static func convert(_ selectionSet: some SelectionSet) -> [String: Any] { 12 | selectionSet.__data._data.mapValues(convert(value:)) 13 | } 14 | 15 | static func convert(_ dataDict: DataDict) -> [String: Any] { 16 | dataDict._data.mapValues(convert(value:)) 17 | } 18 | 19 | /// Converts a ``GraphQLResult`` into a basic JSON dictionary for use. 20 | /// 21 | /// - Returns: A `[String: Any]` JSON dictionary representing the ``GraphQLResult``. 22 | public static func convert(_ result: GraphQLResult) -> [String: Any] { 23 | result.asJSONDictionary() 24 | } 25 | 26 | /// Converts a ``GraphQLError`` into a basic JSON dictionary for use. 27 | /// 28 | /// - Returns: A `[String: Any]` JSON dictionary representing the ``GraphQLError``. 29 | public static func convert(_ error: GraphQLError) -> [String: Any] { 30 | var dict: [String: Any] = [:] 31 | if let message = error["message"] { dict["message"] = message } 32 | if let locations = error["locations"] { dict["locations"] = locations } 33 | if let path = error["path"] { dict["path"] = path } 34 | if let extensions = error["extensions"] { dict["extensions"] = extensions } 35 | return dict 36 | } 37 | 38 | private static func convert(value: Any) -> Any { 39 | var val: Any = value 40 | if let value = value as? DataDict { 41 | val = value._data 42 | } else if let value = value as? any CustomScalarType { 43 | val = value._jsonValue 44 | } 45 | if let dict = val as? [String: Any] { 46 | return dict.mapValues(convert) 47 | } else if let arr = val as? [Any] { 48 | return arr.map(convert) 49 | } 50 | return val 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/Apollo/JSONResponseParsingInterceptor.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if !COCOAPODS 3 | import ApolloAPI 4 | #endif 5 | 6 | /// An interceptor which parses JSON response data into a `GraphQLResult` and attaches it to the `HTTPResponse`. 7 | public struct JSONResponseParsingInterceptor: ApolloInterceptor { 8 | 9 | public enum JSONResponseParsingError: Error, LocalizedError { 10 | case noResponseToParse 11 | case couldNotParseToJSON(data: Data) 12 | 13 | public var errorDescription: String? { 14 | switch self { 15 | case .noResponseToParse: 16 | return "The Codable Parsing Interceptor was called before a response was received to be parsed. Double-check the order of your interceptors." 17 | case .couldNotParseToJSON(let data): 18 | var errorStrings = [String]() 19 | errorStrings.append("Could not parse data to JSON format.") 20 | if let dataString = String(bytes: data, encoding: .utf8) { 21 | errorStrings.append("Data received as a String was:") 22 | errorStrings.append(dataString) 23 | } else { 24 | errorStrings.append("Data of count \(data.count) also could not be parsed into a String.") 25 | } 26 | 27 | return errorStrings.joined(separator: " ") 28 | } 29 | } 30 | } 31 | 32 | public var id: String = UUID().uuidString 33 | 34 | public init() { } 35 | 36 | public func interceptAsync( 37 | chain: any RequestChain, 38 | request: HTTPRequest, 39 | response: HTTPResponse?, 40 | completion: @escaping (Result, any Error>) -> Void 41 | ) { 42 | guard let createdResponse = response else { 43 | chain.handleErrorAsync( 44 | JSONResponseParsingError.noResponseToParse, 45 | request: request, 46 | response: response, 47 | completion: completion 48 | ) 49 | return 50 | } 51 | 52 | do { 53 | guard 54 | let body = try? JSONSerializationFormat.deserialize(data: createdResponse.rawData) as? JSONObject 55 | else { 56 | throw JSONResponseParsingError.couldNotParseToJSON(data: createdResponse.rawData) 57 | } 58 | 59 | let graphQLResponse = GraphQLResponse(operation: request.operation, body: body) 60 | createdResponse._legacyResponse = graphQLResponse 61 | 62 | let (result, cacheRecords) = try graphQLResponse.parseResult(withCachePolicy: request.cachePolicy) 63 | createdResponse.parsedResponse = result 64 | createdResponse.cacheRecords = cacheRecords 65 | 66 | chain.proceedAsync( 67 | request: request, 68 | response: createdResponse, 69 | interceptor: self, 70 | completion: completion 71 | ) 72 | 73 | } catch { 74 | chain.handleErrorAsync( 75 | error, 76 | request: request, 77 | response: createdResponse, 78 | completion: completion 79 | ) 80 | } 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /Sources/Apollo/JSONSerialization+Sorting.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension JSONSerialization { 4 | 5 | /// Uses `sortedKeys` to create a stable representation of JSON objects. 6 | /// 7 | /// - Parameter object: The object to serialize 8 | /// - Returns: The serialized data 9 | /// - Throws: Errors related to the serialization of data. 10 | static func sortedData(withJSONObject object: Any) throws -> Data { 11 | return try self.data(withJSONObject: object, options: [.sortedKeys]) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/Apollo/JSONSerializationFormat.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if !COCOAPODS 3 | import ApolloAPI 4 | #endif 5 | 6 | public final class JSONSerializationFormat { 7 | public class func serialize(value: any JSONEncodable) throws -> Data { 8 | return try JSONSerialization.sortedData(withJSONObject: value._jsonValue) 9 | } 10 | 11 | public class func serialize(value: JSONObject) throws -> Data { 12 | return try JSONSerialization.sortedData(withJSONObject: value) 13 | } 14 | 15 | public class func deserialize(data: Data) throws -> JSONValue { 16 | return try JSONSerialization.jsonObject(with: data, options: []) as! AnyHashable 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/Apollo/MaxRetryInterceptor.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if !COCOAPODS 3 | import ApolloAPI 4 | #endif 5 | 6 | /// An interceptor to enforce a maximum number of retries of any `HTTPRequest` 7 | public class MaxRetryInterceptor: ApolloInterceptor { 8 | 9 | private let maxRetries: Int 10 | private var hitCount = 0 11 | 12 | public var id: String = UUID().uuidString 13 | 14 | public enum RetryError: Error, LocalizedError { 15 | case hitMaxRetryCount(count: Int, operationName: String) 16 | 17 | public var errorDescription: String? { 18 | switch self { 19 | case .hitMaxRetryCount(let count, let operationName): 20 | return "The maximum number of retries (\(count)) was hit without success for operation \"\(operationName)\"." 21 | } 22 | } 23 | } 24 | 25 | /// Designated initializer. 26 | /// 27 | /// - Parameter maxRetriesAllowed: How many times a query can be retried, in addition to the initial attempt before 28 | public init(maxRetriesAllowed: Int = 3) { 29 | self.maxRetries = maxRetriesAllowed 30 | } 31 | 32 | public func interceptAsync( 33 | chain: any RequestChain, 34 | request: HTTPRequest, 35 | response: HTTPResponse?, 36 | completion: @escaping (Result, any Error>) -> Void) { 37 | guard self.hitCount <= self.maxRetries else { 38 | let error = RetryError.hitMaxRetryCount( 39 | count: self.maxRetries, 40 | operationName: Operation.operationName 41 | ) 42 | 43 | chain.handleErrorAsync( 44 | error, 45 | request: request, 46 | response: response, 47 | completion: completion 48 | ) 49 | 50 | return 51 | } 52 | 53 | self.hitCount += 1 54 | chain.proceedAsync( 55 | request: request, 56 | response: response, 57 | interceptor: self, 58 | completion: completion 59 | ) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/Apollo/MultipartResponseDeferParser.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if !COCOAPODS 3 | import ApolloAPI 4 | #endif 5 | 6 | struct MultipartResponseDeferParser: MultipartResponseSpecificationParser { 7 | public enum ParsingError: Swift.Error, LocalizedError, Equatable { 8 | case unsupportedContentType(type: String) 9 | case cannotParseChunkData 10 | case cannotParsePayloadData 11 | 12 | public var errorDescription: String? { 13 | switch self { 14 | 15 | case let .unsupportedContentType(type): 16 | return "Unsupported content type: 'application/graphql-response+json' or 'application/json' are supported, received '\(type)'." 17 | case .cannotParseChunkData: 18 | return "The chunk data could not be parsed." 19 | case .cannotParsePayloadData: 20 | return "The payload data could not be parsed." 21 | } 22 | } 23 | } 24 | 25 | private enum DataLine { 26 | case contentHeader(directives: [String]) 27 | case json(object: JSONObject) 28 | case unknown 29 | 30 | init(_ value: String) { 31 | self = Self.parse(value) 32 | } 33 | 34 | private static func parse(_ dataLine: String) -> DataLine { 35 | if let directives = dataLine.parseContentTypeDirectives() { 36 | return .contentHeader(directives: directives) 37 | } 38 | 39 | if 40 | let data = dataLine.data(using: .utf8), 41 | let jsonObject = try? JSONSerializationFormat.deserialize(data: data) as? JSONObject 42 | { 43 | return .json(object: jsonObject) 44 | } 45 | 46 | return .unknown 47 | } 48 | } 49 | 50 | static let protocolSpec: String = "deferSpec=20220824" 51 | 52 | static func parse(_ chunk: String) -> Result { 53 | for dataLine in chunk.components(separatedBy: Self.dataLineSeparator.description) { 54 | switch DataLine(dataLine.trimmingCharacters(in: .newlines)) { 55 | case let .contentHeader(directives): 56 | guard directives.contains(where: { $0.isValidGraphQLContentType }) else { 57 | return .failure(ParsingError.unsupportedContentType(type: directives.joined(separator: ";"))) 58 | } 59 | 60 | case let .json(object): 61 | guard object.isPartialResponse || object.isIncrementalResponse else { 62 | return .failure(ParsingError.cannotParsePayloadData) 63 | } 64 | 65 | guard let serialized: Data = try? JSONSerializationFormat.serialize(value: object) else { 66 | return .failure(ParsingError.cannotParsePayloadData) 67 | } 68 | 69 | return .success(serialized) 70 | 71 | case .unknown: 72 | return .failure(ParsingError.cannotParseChunkData) 73 | } 74 | } 75 | 76 | return .success(nil) 77 | } 78 | } 79 | 80 | fileprivate extension JSONObject { 81 | var isPartialResponse: Bool { 82 | self.keys.contains("data") && self.keys.contains("hasNext") 83 | } 84 | 85 | var isIncrementalResponse: Bool { 86 | self.keys.contains("incremental") && self.keys.contains("hasNext") 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Sources/Apollo/MultipartResponseSubscriptionParser.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if !COCOAPODS 3 | import ApolloAPI 4 | #endif 5 | 6 | struct MultipartResponseSubscriptionParser: MultipartResponseSpecificationParser { 7 | public enum ParsingError: Swift.Error, LocalizedError, Equatable { 8 | case unsupportedContentType(type: String) 9 | case cannotParseChunkData 10 | case irrecoverableError(message: String?) 11 | case cannotParsePayloadData 12 | case cannotParseErrorData 13 | 14 | public var errorDescription: String? { 15 | switch self { 16 | 17 | case let .unsupportedContentType(type): 18 | return "Unsupported content type: 'application/graphql-response+json' or 'application/json' are supported, received '\(type)'." 19 | case .cannotParseChunkData: 20 | return "The chunk data could not be parsed." 21 | case let .irrecoverableError(message): 22 | return "An irrecoverable error occured: \(message ?? "unknown")." 23 | case .cannotParsePayloadData: 24 | return "The payload data could not be parsed." 25 | case .cannotParseErrorData: 26 | return "The error data could not be parsed." 27 | } 28 | } 29 | } 30 | 31 | private enum DataLine { 32 | case heartbeat 33 | case contentHeader(directives: [String]) 34 | case json(object: JSONObject) 35 | case unknown 36 | 37 | init(_ value: String) { 38 | self = Self.parse(value) 39 | } 40 | 41 | private static func parse(_ dataLine: String) -> DataLine { 42 | var contentTypeHeader: StaticString { "content-type:" } 43 | var heartbeat: StaticString { "{}" } 44 | 45 | if dataLine == heartbeat.description { 46 | return .heartbeat 47 | } 48 | 49 | if let directives = dataLine.parseContentTypeDirectives() { 50 | return .contentHeader(directives: directives) 51 | } 52 | 53 | if 54 | let data = dataLine.data(using: .utf8), 55 | let jsonObject = try? JSONSerializationFormat.deserialize(data: data) as? JSONObject 56 | { 57 | return .json(object: jsonObject) 58 | } 59 | 60 | return .unknown 61 | } 62 | } 63 | 64 | static let protocolSpec: String = "subscriptionSpec=1.0" 65 | 66 | static func parse(_ chunk: String) -> Result { 67 | for dataLine in chunk.components(separatedBy: Self.dataLineSeparator.description) { 68 | switch DataLine(dataLine.trimmingCharacters(in: .newlines)) { 69 | case .heartbeat: 70 | // Periodically sent by the router - noop 71 | break 72 | 73 | case let .contentHeader(directives): 74 | guard directives.contains(where: { $0.isValidGraphQLContentType }) else { 75 | return .failure(ParsingError.unsupportedContentType(type: directives.joined(separator: ";"))) 76 | } 77 | 78 | case let .json(object): 79 | if let errors = object.errors, !(errors is NSNull) { 80 | guard 81 | let errors = errors as? [JSONObject], 82 | let message = errors.first?["message"] as? String 83 | else { 84 | return .failure(ParsingError.cannotParseErrorData) 85 | } 86 | 87 | return .failure(ParsingError.irrecoverableError(message: message)) 88 | } 89 | 90 | if let payload = object.payload, !(payload is NSNull) { 91 | guard 92 | let payload = payload as? JSONObject, 93 | let data: Data = try? JSONSerializationFormat.serialize(value: payload) 94 | else { 95 | return .failure(ParsingError.cannotParsePayloadData) 96 | } 97 | 98 | return .success(data) 99 | } 100 | 101 | // 'errors' is optional because valid payloads don't have transport errors. 102 | // `errors` can be null because it's taken to be the same as optional. 103 | // `payload` is optional because the heartbeat message does not contain a payload field. 104 | // `payload` can be null such as in the case of a transport error or future use (TBD). 105 | return .success(nil) 106 | 107 | case .unknown: 108 | return .failure(ParsingError.cannotParseChunkData) 109 | } 110 | } 111 | 112 | return .success(nil) 113 | } 114 | } 115 | 116 | fileprivate extension JSONObject { 117 | var errors: JSONValue? { 118 | self["errors"] 119 | } 120 | 121 | var payload: JSONValue? { 122 | self["payload"] 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /Sources/Apollo/NetworkFetchInterceptor.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if !COCOAPODS 3 | import ApolloAPI 4 | #endif 5 | 6 | /// An interceptor which actually fetches data from the network. 7 | public class NetworkFetchInterceptor: ApolloInterceptor, Cancellable { 8 | let client: URLSessionClient 9 | @Atomic private var currentTask: URLSessionTask? 10 | 11 | public var id: String = UUID().uuidString 12 | 13 | /// Designated initializer. 14 | /// 15 | /// - Parameter client: The `URLSessionClient` to use to fetch data 16 | public init(client: URLSessionClient) { 17 | self.client = client 18 | } 19 | 20 | public func interceptAsync( 21 | chain: any RequestChain, 22 | request: HTTPRequest, 23 | response: HTTPResponse?, 24 | completion: @escaping (Result, any Error>) -> Void) { 25 | 26 | let urlRequest: URLRequest 27 | do { 28 | urlRequest = try request.toURLRequest() 29 | } catch { 30 | chain.handleErrorAsync( 31 | error, 32 | request: request, 33 | response: response, 34 | completion: completion 35 | ) 36 | return 37 | } 38 | 39 | let taskDescription = "\(Operation.operationType) \(Operation.operationName)" 40 | let task = self.client.sendRequest(urlRequest, taskDescription: taskDescription) { [weak self] result in 41 | guard let self = self else { 42 | return 43 | } 44 | 45 | defer { 46 | if Operation.operationType != .subscription { 47 | self.$currentTask.mutate { $0 = nil } 48 | } 49 | } 50 | 51 | guard !chain.isCancelled else { 52 | return 53 | } 54 | 55 | switch result { 56 | case .failure(let error): 57 | chain.handleErrorAsync( 58 | error, 59 | request: request, 60 | response: response, 61 | completion: completion 62 | ) 63 | 64 | case .success(let (data, httpResponse)): 65 | let response = HTTPResponse( 66 | response: httpResponse, 67 | rawData: data, 68 | parsedResponse: nil 69 | ) 70 | 71 | chain.proceedAsync( 72 | request: request, 73 | response: response, 74 | interceptor: self, 75 | completion: completion 76 | ) 77 | } 78 | } 79 | 80 | self.$currentTask.mutate { $0 = task } 81 | } 82 | 83 | public func cancel() { 84 | guard let task = self.currentTask else { 85 | return 86 | } 87 | 88 | task.cancel() 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Sources/Apollo/NormalizedCache.swift: -------------------------------------------------------------------------------- 1 | public protocol NormalizedCache: AnyObject, ReadOnlyNormalizedCache { 2 | 3 | /// Loads records corresponding to the given keys. 4 | /// 5 | /// - Parameters: 6 | /// - key: The cache keys to load data for 7 | /// - Returns: A dictionary of cache keys to records containing the records that have been found. 8 | func loadRecords(forKeys keys: Set) throws -> [CacheKey: Record] 9 | 10 | /// Merges a set of records into the cache. 11 | /// 12 | /// - Parameters: 13 | /// - records: The set of records to merge. 14 | /// - Returns: A set of keys corresponding to *fields* that have changed (i.e. QUERY_ROOT.Foo.myField). These are the same type of keys as are returned by RecordSet.merge(records:). 15 | func merge(records: RecordSet) throws -> Set 16 | 17 | /// Removes a record for the specified key. This method will only 18 | /// remove whole records, not individual fields. 19 | /// 20 | /// If you attempt to pass a cache key for a single field, this 21 | /// method will do nothing since it won't be able to locate a 22 | /// record to remove based on that key. 23 | /// 24 | /// This method does not support cascading delete - it will only 25 | /// remove the record for the specified key, and not any references to it or from it. 26 | /// 27 | /// - Parameters: 28 | /// - key: The cache key to remove the record for 29 | func removeRecord(for key: CacheKey) throws 30 | 31 | /// Removes records with keys that match the specified pattern. This method will only 32 | /// remove whole records, it does not perform cascading deletes. This means only the 33 | /// records with matched keys will be removed, and not any references to them. Key 34 | /// matching is case-insensitive. 35 | /// 36 | /// If you attempt to pass a cache path for a single field, this method will do nothing 37 | /// since it won't be able to locate a record to remove based on that path. 38 | /// 39 | /// - Note: This method can be very slow depending on the number of records in the cache. 40 | /// It is recommended that this method be called in a background queue. 41 | /// 42 | /// - Parameters: 43 | /// - pattern: The pattern that will be applied to find matching keys. 44 | func removeRecords(matching pattern: CacheKey) throws 45 | 46 | /// Clears all records. 47 | func clear() throws 48 | } 49 | 50 | /// A read-only view of a `NormalizedCache` for use within a `ReadTransaction`. 51 | public protocol ReadOnlyNormalizedCache: AnyObject { 52 | 53 | /// Loads records corresponding to the given keys. 54 | /// 55 | /// - Parameters: 56 | /// - key: The cache keys to load data for 57 | /// - Returns: A dictionary of cache keys to records containing the records that have been found. 58 | func loadRecords(forKeys keys: Set) throws -> [CacheKey: Record] 59 | 60 | } 61 | -------------------------------------------------------------------------------- /Sources/Apollo/PathComponent.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if !COCOAPODS 3 | import ApolloAPI 4 | #endif 5 | 6 | /// Represents a path in a GraphQL query. 7 | public enum PathComponent: Equatable { 8 | /// A String value for a field in a GraphQL query 9 | case field(String) 10 | /// An Int value for an index in a GraphQL List 11 | case index(Int) 12 | 13 | init?(_ value: JSONValue) { 14 | if let string = value as? String { 15 | self = .field(string) 16 | } else if let int = value as? Int { 17 | self = .index(int) 18 | } else { 19 | return nil 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/Apollo/Record.swift: -------------------------------------------------------------------------------- 1 | /// A cache key for a record. 2 | public typealias CacheKey = String 3 | 4 | /// A cache record. 5 | public struct Record: Hashable { 6 | public let key: CacheKey 7 | 8 | public typealias Value = AnyHashable 9 | public typealias Fields = [CacheKey: Value] 10 | public private(set) var fields: Fields 11 | 12 | public init(key: CacheKey, _ fields: Fields = [:]) { 13 | self.key = key 14 | self.fields = fields 15 | } 16 | 17 | public subscript(key: CacheKey) -> Value? { 18 | get { 19 | return fields[key] 20 | } 21 | set { 22 | fields[key] = newValue 23 | } 24 | } 25 | } 26 | 27 | extension Record: CustomStringConvertible { 28 | public var description: String { 29 | return "#\(key) -> \(fields)" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/Apollo/RecordSet.swift: -------------------------------------------------------------------------------- 1 | /// A set of cache records. 2 | public struct RecordSet: Hashable { 3 | public private(set) var storage: [CacheKey: Record] = [:] 4 | 5 | public init(records: S) where S.Iterator.Element == Record { 6 | insert(contentsOf: records) 7 | } 8 | 9 | public mutating func insert(_ record: Record) { 10 | storage[record.key] = record 11 | } 12 | 13 | public mutating func removeRecord(for key: CacheKey) { 14 | storage.removeValue(forKey: key) 15 | } 16 | 17 | public mutating func removeRecords(matching pattern: CacheKey) { 18 | for (key, _) in storage { 19 | if key.range(of: pattern, options: .caseInsensitive) != nil { 20 | storage.removeValue(forKey: key) 21 | } 22 | } 23 | } 24 | 25 | public mutating func clear() { 26 | storage.removeAll() 27 | } 28 | 29 | public mutating func insert(contentsOf records: S) where S.Iterator.Element == Record { 30 | for record in records { 31 | insert(record) 32 | } 33 | } 34 | 35 | public subscript(key: CacheKey) -> Record? { 36 | return storage[key] 37 | } 38 | 39 | public var isEmpty: Bool { 40 | return storage.isEmpty 41 | } 42 | 43 | public var keys: Set { 44 | return Set(storage.keys) 45 | } 46 | 47 | @discardableResult public mutating func merge(records: RecordSet) -> Set { 48 | var changedKeys: Set = Set() 49 | 50 | for (_, record) in records.storage { 51 | changedKeys.formUnion(merge(record: record)) 52 | } 53 | 54 | return changedKeys 55 | } 56 | 57 | @discardableResult public mutating func merge(record: Record) -> Set { 58 | if var oldRecord = storage.removeValue(forKey: record.key) { 59 | var changedKeys: Set = Set() 60 | 61 | for (key, value) in record.fields { 62 | if let oldValue = oldRecord.fields[key], oldValue == value { 63 | continue 64 | } 65 | oldRecord[key] = value 66 | changedKeys.insert([record.key, key].joined(separator: ".")) 67 | } 68 | storage[record.key] = oldRecord 69 | return changedKeys 70 | } else { 71 | storage[record.key] = record 72 | return Set(record.fields.keys.map { [record.key, $0].joined(separator: ".") }) 73 | } 74 | } 75 | } 76 | 77 | extension RecordSet: ExpressibleByDictionaryLiteral { 78 | public init(dictionaryLiteral elements: (CacheKey, Record.Fields)...) { 79 | self.init(records: elements.map { Record(key: $0.0, $0.1) }) 80 | } 81 | } 82 | 83 | extension RecordSet: CustomStringConvertible { 84 | public var description: String { 85 | return String(describing: Array(storage.values)) 86 | } 87 | } 88 | 89 | extension RecordSet: CustomDebugStringConvertible { 90 | public var debugDescription: String { 91 | return description 92 | } 93 | } 94 | 95 | extension RecordSet: CustomPlaygroundDisplayConvertible { 96 | public var playgroundDescription: Any { 97 | return description 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Sources/Apollo/RequestBodyCreator.swift: -------------------------------------------------------------------------------- 1 | #if !COCOAPODS 2 | import ApolloAPI 3 | #endif 4 | 5 | public protocol RequestBodyCreator { 6 | /// Creates a `JSONEncodableDictionary` out of the passed-in operation 7 | /// 8 | /// - Parameters: 9 | /// - operation: The operation to use 10 | /// - sendQueryDocument: Whether or not to send the full query document. Should default to `true`. 11 | /// - autoPersistQuery: Whether to use auto-persisted query information. Should default to `false`. 12 | /// - Returns: The created `JSONEncodableDictionary` 13 | func requestBody( 14 | for operation: Operation, 15 | sendQueryDocument: Bool, 16 | autoPersistQuery: Bool 17 | ) -> JSONEncodableDictionary 18 | } 19 | 20 | // MARK: - Default Implementation 21 | 22 | extension RequestBodyCreator { 23 | 24 | public func requestBody( 25 | for operation: Operation, 26 | sendQueryDocument: Bool, 27 | autoPersistQuery: Bool 28 | ) -> JSONEncodableDictionary { 29 | var body: JSONEncodableDictionary = [ 30 | "operationName": Operation.operationName, 31 | ] 32 | 33 | if let variables = operation.__variables { 34 | body["variables"] = variables._jsonEncodableObject 35 | } 36 | 37 | if sendQueryDocument { 38 | guard let document = Operation.definition?.queryDocument else { 39 | preconditionFailure("To send query documents, Apollo types must be generated with `OperationDefinition`s.") 40 | } 41 | body["query"] = document 42 | } 43 | 44 | if autoPersistQuery { 45 | guard let operationIdentifier = Operation.operationIdentifier else { 46 | preconditionFailure("To enable `autoPersistQueries`, Apollo types must be generated with operationIdentifiers") 47 | } 48 | 49 | body["extensions"] = [ 50 | "persistedQuery" : ["sha256Hash": operationIdentifier, "version": 1] 51 | ] 52 | } 53 | 54 | return body 55 | } 56 | } 57 | 58 | // Helper struct to create requests independently of HTTP operations. 59 | public struct ApolloRequestBodyCreator: RequestBodyCreator { 60 | // Internal init methods cannot be used in public methods 61 | public init() { } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/Apollo/RequestChain.swift: -------------------------------------------------------------------------------- 1 | #if !COCOAPODS 2 | import ApolloAPI 3 | #endif 4 | 5 | public protocol RequestChain: Cancellable { 6 | func kickoff( 7 | request: HTTPRequest, 8 | completion: @escaping (Result, any Error>) -> Void 9 | ) where Operation : GraphQLOperation 10 | 11 | @available(*, deprecated, renamed: "proceedAsync(request:response:interceptor:completion:)") 12 | func proceedAsync( 13 | request: HTTPRequest, 14 | response: HTTPResponse?, 15 | completion: @escaping (Result, any Error>) -> Void 16 | ) where Operation : GraphQLOperation 17 | 18 | func proceedAsync( 19 | request: HTTPRequest, 20 | response: HTTPResponse?, 21 | interceptor: any ApolloInterceptor, 22 | completion: @escaping (Result, any Error>) -> Void 23 | ) where Operation : GraphQLOperation 24 | 25 | func cancel() 26 | 27 | func retry( 28 | request: HTTPRequest, 29 | completion: @escaping (Result, any Error>) -> Void 30 | ) where Operation : GraphQLOperation 31 | 32 | func handleErrorAsync( 33 | _ error: any Error, 34 | request: HTTPRequest, 35 | response: HTTPResponse?, 36 | completion: @escaping (Result, any Error>) -> Void 37 | ) where Operation : GraphQLOperation 38 | 39 | func returnValueAsync( 40 | for request: HTTPRequest, 41 | value: GraphQLResult, 42 | completion: @escaping (Result, any Error>) -> Void 43 | ) where Operation : GraphQLOperation 44 | 45 | var isCancelled: Bool { get } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/Apollo/RequestClientMetadata.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if !COCOAPODS 3 | import ApolloAPI 4 | #endif 5 | 6 | extension JSONRequest { 7 | /// Adds client metadata to the request body in the `extensions` key. 8 | /// 9 | /// - Parameter body: The previously generated JSON body. 10 | func addEnhancedClientAwarenessExtension(to body: inout JSONEncodableDictionary) { 11 | _addEnhancedClientAwarenessExtension(to: &body) 12 | } 13 | } 14 | 15 | extension UploadRequest { 16 | /// Adds client metadata to the request body in the `extensions` key. 17 | /// 18 | /// - Parameter body: The previously generated JSON body. 19 | func addEnhancedClientAwarenessExtension(to body: inout JSONEncodableDictionary) { 20 | _addEnhancedClientAwarenessExtension(to: &body) 21 | } 22 | } 23 | 24 | fileprivate func _addEnhancedClientAwarenessExtension(to body: inout JSONEncodableDictionary) { 25 | let clientLibraryMetadata: JSONEncodableDictionary = [ 26 | "name": Constants.ApolloClientName, 27 | "version": Constants.ApolloClientVersion 28 | ] 29 | 30 | var extensions = body["extensions"] as? JSONEncodableDictionary ?? JSONEncodableDictionary() 31 | extensions["clientLibrary"] = clientLibraryMetadata 32 | 33 | body["extensions"] = extensions 34 | } 35 | -------------------------------------------------------------------------------- /Sources/Apollo/RequestContext.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if !COCOAPODS 3 | import ApolloAPI 4 | #endif 5 | 6 | /// A marker protocol to set up an object to pass through the request chain. 7 | /// 8 | /// Used to allow additional context-specific information to pass the length of the request chain. 9 | /// 10 | /// This allows the various interceptors to make modifications, or perform actions, with information 11 | /// that they cannot get just from the existing operation. It can be anything that conforms to this protocol. 12 | public protocol RequestContext {} 13 | 14 | /// A request context specialization protocol that specifies options for configuring the timeout of a `URLRequest`. 15 | /// 16 | /// A `RequestContext` object can conform to this protocol to provide a custom `requestTimeout` for an individual 17 | /// request. If the `RequestContext` for a request does not conform to this protocol, the default request timeout 18 | /// of `URLRequest` will be used. 19 | public protocol RequestContextTimeoutConfigurable: RequestContext { 20 | /// The timeout interval specifies the limit on the idle interval allotted to a request in the process of 21 | /// loading. This timeout interval is measured in seconds. 22 | /// 23 | /// The value of this property will be set as the `timeoutInterval` on the `URLRequest` created for this GraphQL request. 24 | var requestTimeout: TimeInterval { get } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/Apollo/Resources/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyCollectedDataTypes 6 | 7 | NSPrivacyAccessedAPITypes 8 | 9 | NSPrivacyTrackingDomains 10 | 11 | NSPrivacyTracking 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Sources/Apollo/ResponseCodeInterceptor.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if !COCOAPODS 3 | import ApolloAPI 4 | #endif 5 | 6 | /// An interceptor to check the response code returned with a request. 7 | public struct ResponseCodeInterceptor: ApolloInterceptor { 8 | 9 | public var id: String = UUID().uuidString 10 | 11 | public enum ResponseCodeError: Error, LocalizedError { 12 | case invalidResponseCode(response: HTTPURLResponse?, rawData: Data?) 13 | 14 | public var errorDescription: String? { 15 | switch self { 16 | case .invalidResponseCode(let response, let rawData): 17 | var errorStrings = [String]() 18 | if let code = response?.statusCode { 19 | errorStrings.append("Received a \(code) error.") 20 | } else { 21 | errorStrings.append("Did not receive a valid status code.") 22 | } 23 | 24 | if 25 | let data = rawData, 26 | let dataString = String(bytes: data, encoding: .utf8) { 27 | errorStrings.append("Data returned as a String was:") 28 | errorStrings.append(dataString) 29 | } else { 30 | errorStrings.append("Data was nil or could not be transformed into a string.") 31 | } 32 | 33 | return errorStrings.joined(separator: " ") 34 | } 35 | } 36 | 37 | public var graphQLError: GraphQLError? { 38 | switch self { 39 | case .invalidResponseCode(_, let rawData): 40 | if let jsonRawData = rawData, 41 | let jsonData = try? JSONSerialization.jsonObject(with: jsonRawData, options: .allowFragments) as? JSONObject { 42 | return GraphQLError(jsonData) 43 | } 44 | return nil 45 | } 46 | } 47 | } 48 | 49 | /// Designated initializer 50 | public init() {} 51 | 52 | public func interceptAsync( 53 | chain: any RequestChain, 54 | request: HTTPRequest, 55 | response: HTTPResponse?, 56 | completion: @escaping (Result, any Error>) -> Void) { 57 | 58 | 59 | guard response?.httpResponse.isSuccessful == true else { 60 | let error = ResponseCodeError.invalidResponseCode( 61 | response: response?.httpResponse, 62 | rawData: response?.rawData 63 | ) 64 | 65 | chain.handleErrorAsync( 66 | error, 67 | request: request, 68 | response: response, 69 | completion: completion 70 | ) 71 | return 72 | } 73 | 74 | chain.proceedAsync( 75 | request: request, 76 | response: response, 77 | interceptor: self, 78 | completion: completion 79 | ) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Sources/Apollo/ResponsePath.swift: -------------------------------------------------------------------------------- 1 | /// Represents a list of string components joined into a path using a reverse linked list. 2 | /// 3 | /// A response path is stored as a linked list because using an array turned out to be 4 | /// a performance bottleneck during decoding/execution. 5 | /// 6 | /// In order to optimize for calculation of a path string, `ResponsePath` does not allow insertion 7 | /// of components in the middle or at the beginning of the path. Components may only be appended to 8 | /// the end of an existing path. 9 | public struct ResponsePath: ExpressibleByArrayLiteral { 10 | public typealias Key = String 11 | 12 | private final class Node { 13 | let previous: Node? 14 | let key: Key 15 | 16 | init(previous: Node?, key: Key) { 17 | self.previous = previous 18 | self.key = key 19 | } 20 | 21 | lazy var joined: String = { 22 | if let previous = previous { 23 | return previous.joined + ".\(key)" 24 | } else { 25 | return key 26 | } 27 | }() 28 | 29 | lazy var components: [String] = { 30 | if let previous = previous { 31 | var components = previous.components 32 | components.append(key) 33 | return components 34 | } else { 35 | return [key] 36 | } 37 | }() 38 | } 39 | 40 | private var head: Node? 41 | public var joined: String { 42 | return head?.joined ?? "" 43 | } 44 | 45 | public func toArray() -> [String] { 46 | return head?.components ?? [] 47 | } 48 | 49 | public init(arrayLiteral segments: Key...) { 50 | for segment in segments { 51 | append(segment) 52 | } 53 | } 54 | 55 | public init(_ key: Key) { 56 | append(key) 57 | } 58 | 59 | public mutating func append(_ key: Key) { 60 | head = Node(previous: head, key: key) 61 | } 62 | 63 | public func appending(_ key: Key) -> ResponsePath { 64 | var copy = self 65 | copy.append(key) 66 | return copy 67 | } 68 | 69 | public var isEmpty: Bool { 70 | head == nil 71 | } 72 | 73 | public static func + (lhs: ResponsePath, rhs: Key) -> ResponsePath { 74 | lhs.appending(rhs) 75 | } 76 | 77 | public static func + (lhs: ResponsePath, rhs: ResponsePath) -> ResponsePath { 78 | lhs + rhs.toArray() 79 | } 80 | 81 | public static func + ( 82 | lhs: ResponsePath, rhs: T 83 | ) -> ResponsePath where T.Element == Key { 84 | var new = lhs 85 | for component in rhs { 86 | new.append(component) 87 | } 88 | return new 89 | } 90 | } 91 | 92 | extension ResponsePath: CustomStringConvertible { 93 | public var description: String { 94 | return joined 95 | } 96 | } 97 | 98 | extension ResponsePath: Hashable { 99 | public func hash(into hasher: inout Hasher) { 100 | hasher.combine(joined) 101 | } 102 | } 103 | 104 | extension ResponsePath: Equatable { 105 | static public func == (lhs: ResponsePath, rhs: ResponsePath) -> Bool { 106 | return lhs.joined == rhs.joined 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Sources/Apollo/SelectionSet+DictionaryIntializer.swift: -------------------------------------------------------------------------------- 1 | #if !COCOAPODS 2 | import ApolloAPI 3 | #endif 4 | 5 | public enum RootSelectionSetInitializeError: Error { 6 | case hasNonHashableValue 7 | } 8 | 9 | extension RootSelectionSet { 10 | /// Initializes a `SelectionSet` with a raw JSON response object. 11 | /// 12 | /// The process of converting a JSON response into `SelectionSetData` is done by using a 13 | /// `GraphQLExecutor` with a`GraphQLSelectionSetMapper` to parse, validate, and transform 14 | /// the JSON response data into the format expected by `SelectionSet`. 15 | /// 16 | /// - Parameters: 17 | /// - data: A dictionary representing a JSON response object for a GraphQL object. 18 | /// - variables: [Optional] The operation variables that would be used to obtain 19 | /// the given JSON response data. 20 | @_disfavoredOverload 21 | public init( 22 | data: [String: Any], 23 | variables: GraphQLOperation.Variables? = nil 24 | ) throws { 25 | let jsonObject = try Self.convertToAnyHashableValueDict(dict: data) 26 | try self.init(data: jsonObject, variables: variables) 27 | } 28 | 29 | /// Convert dictionary type [String: Any] to [String: AnyHashable] 30 | /// - Parameter dict: [String: Any] type dictionary 31 | /// - Returns: converted [String: AnyHashable] type dictionary 32 | private static func convertToAnyHashableValueDict(dict: [String: Any]) throws -> [String: AnyHashable] { 33 | var result = [String: AnyHashable]() 34 | 35 | for (key, value) in dict { 36 | if let arrayValue = value as? [Any] { 37 | result[key] = try convertToAnyHashableArray(array: arrayValue) 38 | } else { 39 | if let dictValue = value as? [String: Any] { 40 | result[key] = try convertToAnyHashableValueDict(dict: dictValue) 41 | } else if let hashableValue = value as? AnyHashable { 42 | result[key] = hashableValue 43 | } else { 44 | throw RootSelectionSetInitializeError.hasNonHashableValue 45 | } 46 | } 47 | } 48 | return result 49 | } 50 | 51 | /// Convert Any type Array type to AnyHashable type Array 52 | /// - Parameter array: Any type Array 53 | /// - Returns: AnyHashable type Array 54 | private static func convertToAnyHashableArray(array: [Any]) throws -> [AnyHashable] { 55 | var result: [AnyHashable] = [] 56 | for value in array { 57 | if let array = value as? [Any] { 58 | result.append(try convertToAnyHashableArray(array: array)) 59 | } else if let dict = value as? [String: Any] { 60 | result.append(try convertToAnyHashableValueDict(dict: dict)) 61 | } else if let hashable = value as? AnyHashable { 62 | result.append(hashable) 63 | } else { 64 | throw RootSelectionSetInitializeError.hasNonHashableValue 65 | } 66 | } 67 | return result 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Sources/Apollo/SelectionSet+JSONInitializer.swift: -------------------------------------------------------------------------------- 1 | #if !COCOAPODS 2 | import ApolloAPI 3 | #endif 4 | 5 | extension RootSelectionSet { 6 | 7 | /// Initializes a `SelectionSet` with a raw JSON response object. 8 | /// 9 | /// The process of converting a JSON response into `SelectionSetData` is done by using a 10 | /// `GraphQLExecutor` with a`GraphQLSelectionSetMapper` to parse, validate, and transform 11 | /// the JSON response data into the format expected by `SelectionSet`. 12 | /// 13 | /// - Parameters: 14 | /// - data: A dictionary representing a JSON response object for a GraphQL object. 15 | /// - variables: [Optional] The operation variables that would be used to obtain 16 | /// the given JSON response data. 17 | public init( 18 | data: JSONObject, 19 | variables: GraphQLOperation.Variables? = nil 20 | ) throws { 21 | let accumulator = GraphQLSelectionSetMapper( 22 | handleMissingValues: .allowForOptionalFields 23 | ) 24 | let executor = GraphQLExecutor(executionSource: NetworkResponseExecutionSource()) 25 | 26 | self = try executor.execute( 27 | selectionSet: Self.self, 28 | on: data, 29 | variables: variables, 30 | accumulator: accumulator 31 | ) 32 | } 33 | 34 | } 35 | 36 | extension Deferrable { 37 | 38 | /// Initializes a `Deferrable` `SelectionSet` with a raw JSON response object. 39 | /// 40 | /// The process of converting a JSON response into `SelectionSetData` is done by using a 41 | /// `GraphQLExecutor` with a`GraphQLSelectionSetMapper` to parse, validate, and transform 42 | /// the JSON response data into the format expected by the `Deferrable` `SelectionSet`. 43 | /// 44 | /// - Parameters: 45 | /// - data: A dictionary representing a JSON response object for a GraphQL object. 46 | /// - operation: The operation which contains `data`. 47 | /// - variables: [Optional] The operation variables that would be used to obtain 48 | /// the given JSON response data. 49 | init( 50 | data: JSONObject, 51 | in operation: any GraphQLOperation.Type, 52 | variables: GraphQLOperation.Variables? = nil 53 | ) throws { 54 | let accumulator = GraphQLSelectionSetMapper( 55 | handleMissingValues: .allowForOptionalFields 56 | ) 57 | let executor = GraphQLExecutor(executionSource: NetworkResponseExecutionSource()) 58 | 59 | self = try executor.execute( 60 | selectionSet: Self.self, 61 | in: operation, 62 | on: data, 63 | variables: variables, 64 | accumulator: accumulator 65 | ) 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /Sources/Apollo/TaskData.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A wrapper for data about a particular task handled by `URLSessionClient` 4 | public class TaskData { 5 | 6 | public let rawCompletion: URLSessionClient.RawCompletion? 7 | public let completionBlock: URLSessionClient.Completion 8 | private(set) var data: Data = Data() 9 | private(set) var response: HTTPURLResponse? = nil 10 | 11 | init(rawCompletion: URLSessionClient.RawCompletion?, 12 | completionBlock: @escaping URLSessionClient.Completion) { 13 | self.rawCompletion = rawCompletion 14 | self.completionBlock = completionBlock 15 | } 16 | 17 | func append(additionalData: Data) { 18 | self.data.append(additionalData) 19 | } 20 | 21 | func reset(data: Data?) { 22 | guard let data, !data.isEmpty else { 23 | self.data = Data() 24 | return 25 | } 26 | 27 | self.data = data 28 | } 29 | 30 | func responseReceived(response: URLResponse) { 31 | if let httpResponse = response as? HTTPURLResponse { 32 | self.response = httpResponse 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/ApolloAPI/AnyHashableConvertible.swift: -------------------------------------------------------------------------------- 1 | /// A helper protocol to enable `AnyHashable` conversion for types that do not have automatic 2 | /// `AnyHashable` conversion implemented. 3 | /// 4 | /// For most types that conform to `Hashable`, Swift automatically wraps them in an `AnyHashable` 5 | /// when they are cast to or stored in a property of the `AnyHashable` type. This does not happen 6 | /// automatically for containers with conditional `Hashable` conformance such as 7 | /// `Optional`, `Array`, and `Dictionary`. 8 | public protocol AnyHashableConvertible { 9 | /// Converts the type to an `AnyHashable`. 10 | var _asAnyHashable: AnyHashable { get } 11 | } 12 | 13 | extension AnyHashableConvertible where Self: Hashable { 14 | /// Converts the type to an `AnyHashable` by casting self. 15 | /// 16 | /// This utilizes Swift's automatic `AnyHashable` conversion functionality. 17 | @inlinable public var _asAnyHashable: AnyHashable { self } 18 | } 19 | 20 | extension AnyHashable: AnyHashableConvertible {} 21 | 22 | extension Optional: AnyHashableConvertible where Wrapped: Hashable {} 23 | 24 | extension Dictionary: AnyHashableConvertible where Key: Hashable, Value: Hashable {} 25 | 26 | extension Array: AnyHashableConvertible where Element: Hashable {} 27 | -------------------------------------------------------------------------------- /Sources/ApolloAPI/CacheReference.swift: -------------------------------------------------------------------------------- 1 | /// Represents a reference to a record for a GraphQL object in the cache. 2 | /// 3 | /// ``CacheReference`` is just a wrapper around a `String`. But when the value for a key in a cache 4 | /// `Record` is a `String`, we treat the string as the value. When the value for the key is a 5 | /// ``CacheReference``, the reference's ``key`` is the cache key for another referenced object 6 | /// that is the value. 7 | public struct CacheReference: Hashable { 8 | 9 | /// A CacheReference referencing the root query object. 10 | public static let RootQuery: CacheReference = CacheReference("QUERY_ROOT") 11 | 12 | /// A CacheReference referencing the root mutation object. 13 | public static let RootMutation: CacheReference = CacheReference("MUTATION_ROOT") 14 | 15 | /// A CacheReference referencing the root subscription object. 16 | public static let RootSubscription: CacheReference = CacheReference("SUBSCRIPTION_ROOT") 17 | 18 | /// Helper function that returns the cache's root ``CacheReference`` for the given 19 | /// ``GraphQLOperationType``. 20 | /// 21 | /// The Apollo `NormalizedCache` stores all objects that are not normalized 22 | /// (ie. don't have a unique cache key provided by the ``SchemaConfiguration``) 23 | /// with a ``CacheReference`` computed as the field path to the object from 24 | /// the root of the parent operation type. 25 | /// 26 | /// For example, given the operation: 27 | /// ```graphql 28 | /// query { 29 | /// animals { 30 | /// owner { 31 | /// name 32 | /// } 33 | /// } 34 | /// } 35 | /// ``` 36 | /// The ``CacheReference`` for the `owner` object of the third animal in the `animals` list would 37 | /// have a ``CacheReference/key`` of `"QUERY_ROOT.animals.2.owner`. 38 | /// 39 | /// - Parameter operationType: A ``GraphQLOperationType`` 40 | /// - Returns: The cache's root ``CacheReference`` for the given ``GraphQLOperationType`` 41 | public static func rootCacheReference( 42 | for operationType: GraphQLOperationType 43 | ) -> CacheReference { 44 | switch operationType { 45 | case .query: 46 | return RootQuery 47 | case .mutation: 48 | return RootMutation 49 | case .subscription: 50 | return RootSubscription 51 | } 52 | } 53 | 54 | /// The unique identifier for the referenced object. 55 | /// 56 | /// # See Also 57 | /// ``CacheKeyInfo`` 58 | public let key: String 59 | 60 | /// Designated Initializer 61 | /// 62 | /// - Parameters: 63 | /// - key: The unique identifier for the referenced object. 64 | public init(_ key: String) { 65 | self.key = key 66 | } 67 | 68 | } 69 | 70 | extension CacheReference: CustomStringConvertible { 71 | @inlinable public var description: String { 72 | return "-> #\(key)" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Sources/ApolloAPI/Deferred.swift: -------------------------------------------------------------------------------- 1 | public protocol Deferrable: SelectionSet { } 2 | 3 | /// Wraps a deferred selection set (either an inline fragment or fragment spread) to expose the 4 | /// fulfilled value as well as the fulfilled state through the projected value. 5 | @propertyWrapper 6 | public struct Deferred { 7 | public enum State: Equatable { 8 | /// The deferred selection set has not been received yet. 9 | case pending 10 | /// The deferred value can never be fulfilled, such as in the case of a type case mismatch. 11 | case notExecuted 12 | /// The deferred value has been received. 13 | case fulfilled(Fragment) 14 | } 15 | 16 | public init(_dataDict: DataDict) { 17 | __data = _dataDict 18 | } 19 | 20 | public var state: State { 21 | let fragment = ObjectIdentifier(Fragment.self) 22 | if __data._fulfilledFragments.contains(fragment) { 23 | return .fulfilled(Fragment.init(_dataDict: __data)) 24 | } 25 | else if __data._deferredFragments.contains(fragment) { 26 | return .pending 27 | } else { 28 | return .notExecuted 29 | } 30 | } 31 | 32 | private let __data: DataDict 33 | public var projectedValue: State { state } 34 | public var wrappedValue: Fragment? { 35 | guard case let .fulfilled(value) = state else { 36 | return nil 37 | } 38 | return value 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/ApolloAPI/Documentation.docc/Documentation.md: -------------------------------------------------------------------------------- 1 | # ``ApolloAPI`` 2 | 3 | The internal models shared by the [`Apollo`](/documentation/apollo) client and the models generated by [`ApolloCodegenLib`](/documentation/apollocodegenlib) 4 | 5 | ## Overview 6 | 7 | This library allows you to use your generated models without importing the full [``Apollo``](/documentation/apollo) client. The generated models import ``ApolloAPI`` and expose the necessary functionality. For most use cases, *you should not need to import ``ApolloAPI`` into your code.* 8 | 9 | ## Topics 10 | 11 | ### Schema Types 12 | 13 | - ``Object`` 14 | - ``Interface`` 15 | - ``Union`` 16 | - ``InputObject`` 17 | - ``EnumType`` 18 | 19 | ### Scalar Types 20 | 21 | - ``AnyScalarType`` 22 | - ``ScalarType`` 23 | - ``CustomScalarType`` 24 | 25 | ### JSON Conversion 26 | 27 | - ``JSONValue`` 28 | - ``JSONObject`` 29 | - ``JSONEncodable`` 30 | - ``JSONEncodableDictionary`` 31 | - ``JSONDecodable`` 32 | - ``JSONDecodingError`` 33 | -------------------------------------------------------------------------------- /Sources/ApolloAPI/Documentation.docc/GraphQLEnum.md: -------------------------------------------------------------------------------- 1 | # ``ApolloAPI/GraphQLEnum`` 2 | 3 | ## Topics 4 | 5 | ### Operators 6 | 7 | - ``==(_:_:)-n7qo`` 8 | - ``==(_:_:)-88en`` 9 | - ``==(_:_:)`` 10 | @Comment("Adds the ==(lhs: GraphQLEnum?, rhs: T) operator to the documentation page.") 11 | - ``!=(_:_:)-4co00`` 12 | - ``!=(_:_:)-9dudu`` 13 | - ``!=(_:_:)`` 14 | - ``~=(_:_:)`` 15 | -------------------------------------------------------------------------------- /Sources/ApolloAPI/Documentation.docc/GraphQLNullable.md: -------------------------------------------------------------------------------- 1 | # ``ApolloAPI/GraphQLNullable`` 2 | 3 | ## Topics 4 | 5 | ### Enumeration Cases 6 | 7 | - ``none`` 8 | - ``null`` 9 | - ``some(_:)`` 10 | 11 | ### Operators 12 | 13 | - ``__(_:_:)`` 14 | @Comment("Adds the ?? operator to the documentation page.") 15 | -------------------------------------------------------------------------------- /Sources/ApolloAPI/FragmentProtocols.swift: -------------------------------------------------------------------------------- 1 | // MARK: - Fragment 2 | 3 | /// A protocol representing a fragment that a ``SelectionSet`` object may be converted to. 4 | /// 5 | /// A ``SelectionSet`` can be converted to any ``Fragment`` included in it's 6 | /// `Fragments` object via its ``SelectionSet/fragments-swift.property`` property. 7 | public protocol Fragment: SelectionSet, Deferrable { 8 | /// The definition of the fragment in GraphQL syntax. 9 | static var fragmentDefinition: StaticString { get } 10 | } 11 | 12 | /// Extension providing default implementation for the ``Fragment`` protocol. 13 | extension Fragment { 14 | // Default implementation for the `fragmentDefinition` variable 15 | public static var fragmentDefinition: StaticString { 16 | return "" 17 | } 18 | } 19 | 20 | /// A protocol representing a container for the fragments on a generated ``SelectionSet``. 21 | /// 22 | /// A generated ``FragmentContainer`` includes generated properties for converting the 23 | /// ``SelectionSet`` into any generated ``Fragment`` that it includes. 24 | /// 25 | /// # Code Generation 26 | /// 27 | /// The ``FragmentContainer`` protocol is only conformed to by generated `Fragments` structs. 28 | /// Given a query: 29 | /// ```graphql 30 | /// fragment FragmentA on Animal { 31 | /// species 32 | /// } 33 | /// 34 | /// query { 35 | /// animals { 36 | /// ...FragmentA 37 | /// } 38 | /// } 39 | /// ``` 40 | /// The generated `Animal` ``SelectionSet`` will include the ``FragmentContainer``: 41 | /// ```swift 42 | /// public struct Animal: API.SelectionSet { 43 | /// // ... 44 | /// public struct Fragments: FragmentContainer { 45 | /// public let __data: DataDict 46 | /// public init(data: DataDict) { __data = data } 47 | /// 48 | /// public var fragmentA: FragmentA { _toFragment() } 49 | /// } 50 | /// } 51 | /// ``` 52 | /// 53 | /// # Converting a SelectionSet to a Fragment 54 | /// 55 | /// With the generated code above, you can conver the `Animal` ``SelectionSet`` to the generated 56 | /// `FragmentA` ``Fragment``: 57 | /// ```swift 58 | /// let fragmentA: FragmentA = animal.fragments.fragmentA 59 | /// ``` 60 | public protocol FragmentContainer { 61 | /// The data of the underlying GraphQL object represented by the parent ``SelectionSet`` 62 | var __data: DataDict { get } 63 | 64 | /// Designated Initializer 65 | /// - Parameter dataDict: The data of the underlying GraphQL object represented by the parent ``SelectionSet`` 66 | init(_dataDict: DataDict) 67 | } 68 | 69 | extension FragmentContainer { 70 | 71 | /// Converts a ``SelectionSet`` to a ``Fragment`` given a generic fragment type. 72 | /// 73 | /// > Warning: This function is not supported for use outside of generated call sites. 74 | /// Generated call sites are guaranteed by the GraphQL compiler to be safe. 75 | /// Unsupported usage may result in unintended consequences including crashes. 76 | /// 77 | /// - Returns: The ``Fragment`` the ``SelectionSet`` has been converted to 78 | @inlinable public func _toFragment() -> T { 79 | _convertToFragment() 80 | } 81 | 82 | @usableFromInline func _convertToFragment()-> T { 83 | return T.init(_dataDict: __data) 84 | } 85 | 86 | /// Converts a ``SelectionSet`` to a ``Fragment`` given a generic fragment type if the fragment 87 | /// was fulfilled. 88 | /// 89 | /// A fragment may not be fulfilled if it is condtionally included useing an `@include/@skip` 90 | /// directive. For more information on `@include/@skip` conditions, see ``Selection/Conditions`` 91 | /// 92 | /// > Warning: This function is not supported for use outside of generated call sites. 93 | /// Generated call sites are guaranteed by the GraphQL compiler to be safe. 94 | /// Unsupported usage may result in unintended consequences including crashes. 95 | /// 96 | /// - Returns: The ``Fragment`` the ``SelectionSet`` has been converted to, or `nil` if the 97 | /// fragment was not fulfilled. 98 | @inlinable public func _toFragment() -> T? { 99 | guard __data.fragmentIsFulfilled(T.self) else { return nil } 100 | return T.init(_dataDict: __data) 101 | } 102 | 103 | } 104 | 105 | /// A ``FragmentContainer`` to be used by ``SelectionSet``s that have no fragments. 106 | /// This is the default ``FragmentContainer`` for a ``SelectionSet`` that does not specify a 107 | /// `Fragments` type. 108 | public enum NoFragments {} 109 | -------------------------------------------------------------------------------- /Sources/ApolloAPI/InputValue.swift: -------------------------------------------------------------------------------- 1 | /// Represents an input value to an argument on a ``Selection/Field``'s ``Selection/Field/arguments``. 2 | /// 3 | /// # See Also 4 | /// [GraphQLSpec - Input Values](http://spec.graphql.org/October2021/#sec-Input-Values) 5 | public indirect enum InputValue { 6 | /// A direct input value, valid types are `String`, `Int` `Float` and `Bool`. 7 | /// For enum input values, the enum cases's `rawValue` as a `String` should be used. 8 | case scalar(any ScalarType) 9 | 10 | /// A variable input value to be evaluated using the operation's variables dictionary at runtime. 11 | /// See ``GraphQLOperation``. 12 | /// 13 | /// `.variable` should only be used as the value for an argument in a ``Selection/Field``. 14 | /// A `.variable` value should not be included in an operation's variables dictionary. See 15 | /// ``GraphQLOperation``. 16 | case variable(String) 17 | 18 | /// A GraphQL "`List`" input value. 19 | /// # See Also 20 | /// [GraphQLSpec - Input Values - List Value](http://spec.graphql.org/October2021/#sec-List-Value) 21 | case list([InputValue]) 22 | 23 | /// A GraphQL "`InputObject`" input value. Represented as a dictionary of input values. 24 | /// # See Also 25 | /// [GraphQLSpec - Input Values - Input Object Values](http://spec.graphql.org/October2021/#sec-Input-Object-Values) 26 | case object([String: InputValue]) 27 | 28 | /// A null input value. 29 | /// 30 | /// A null input value indicates an intentional inclusion of a value for a field argument as null. 31 | /// # See Also 32 | /// [GraphQLSpec - Input Values - Null Value](http://spec.graphql.org/October2021/#sec-Null-Value) 33 | case null 34 | } 35 | 36 | // MARK: - ExpressibleBy Literal Extensions 37 | 38 | extension InputValue: ExpressibleByStringLiteral { 39 | @inlinable public init(stringLiteral value: StringLiteralType) { 40 | self = .scalar(value) 41 | } 42 | } 43 | 44 | extension InputValue: ExpressibleByIntegerLiteral { 45 | @inlinable public init(integerLiteral value: IntegerLiteralType) { 46 | self = .scalar(value) 47 | } 48 | } 49 | 50 | extension InputValue: ExpressibleByFloatLiteral { 51 | @inlinable public init(floatLiteral value: FloatLiteralType) { 52 | self = .scalar(value) 53 | } 54 | } 55 | 56 | extension InputValue: ExpressibleByBooleanLiteral { 57 | @inlinable public init(booleanLiteral value: BooleanLiteralType) { 58 | self = .scalar(value) 59 | } 60 | } 61 | 62 | extension InputValue: ExpressibleByArrayLiteral { 63 | @inlinable public init(arrayLiteral elements: InputValue...) { 64 | self = .list(Array(elements.map { $0 })) 65 | } 66 | } 67 | 68 | extension InputValue: ExpressibleByDictionaryLiteral { 69 | @inlinable public init(dictionaryLiteral elements: (String, InputValue)...) { 70 | self = .object(Dictionary(elements.map{ ($0.0, $0.1) }, 71 | uniquingKeysWith: { (_, last) in last })) 72 | } 73 | } 74 | 75 | // MARK: Hashable Conformance 76 | 77 | extension InputValue: Hashable { 78 | public static func == (lhs: InputValue, rhs: InputValue) -> Bool { 79 | switch (lhs, rhs) { 80 | case let (.variable(lhsValue), .variable(rhsValue)), 81 | let (.scalar(lhsValue as String), .scalar(rhsValue as String)): 82 | return lhsValue == rhsValue 83 | case let (.scalar(lhsValue as Bool), .scalar(rhsValue as Bool)): 84 | return lhsValue == rhsValue 85 | case let (.scalar(lhsValue as Int), .scalar(rhsValue as Int)): 86 | return lhsValue == rhsValue 87 | case let (.scalar(lhsValue as Float), .scalar(rhsValue as Float)): 88 | return lhsValue == rhsValue 89 | case let (.scalar(lhsValue as Double), .scalar(rhsValue as Double)): 90 | return lhsValue == rhsValue 91 | case let (.list(lhsValue), .list(rhsValue)): 92 | return lhsValue.elementsEqual(rhsValue) 93 | case let (.object(lhsValue), .object(rhsValue)): 94 | return lhsValue.elementsEqual(rhsValue, by: { $0.key == $1.key && $0.value == $1.value }) 95 | case (.null, .null): 96 | return true 97 | default: return false 98 | } 99 | } 100 | 101 | public func hash(into hasher: inout Hasher) { 102 | hasher.combine(self) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Sources/ApolloAPI/JSON.swift: -------------------------------------------------------------------------------- 1 | /// Represents a value in a ``JSONObject`` 2 | /// 3 | /// Making ``JSONValue`` an `AnyHashable` enables comparing ``JSONObject``s 4 | /// in `Equatable` conformances. 5 | public typealias JSONValue = AnyHashable 6 | 7 | /// Represents a JSON Dictionary 8 | public typealias JSONObject = [String: JSONValue] 9 | 10 | /// Represents a Dictionary that can be converted into a ``JSONObject`` 11 | /// 12 | /// To convert to a ``JSONObject``: 13 | /// ```swift 14 | /// dictionary.compactMapValues { $0.jsonValue } 15 | /// ``` 16 | public typealias JSONEncodableDictionary = [String: any JSONEncodable] 17 | 18 | /// A protocol for a type that can be initialized from a ``JSONValue``. 19 | /// 20 | /// This is used to interoperate between the type-safe Swift models and the `JSON` in a 21 | /// GraphQL network response/request or the `NormalizedCache`. 22 | public protocol JSONDecodable: AnyHashableConvertible { 23 | 24 | /// Intializes the conforming type from a ``JSONValue``. 25 | /// 26 | /// > Important: For a type that conforms to both ``JSONEncodable`` and ``JSONDecodable``, 27 | /// the `jsonValue` passed to this initializer should be equal to the value returned by the 28 | /// initialized entity's ``JSONEncodable/jsonValue`` property. 29 | /// 30 | /// - Parameter value: The ``JSONValue`` to convert to the ``JSONDecodable`` type. 31 | /// 32 | /// - Throws: A ``JSONDecodingError`` if the `jsonValue` cannot be converted to the receiver's 33 | /// type. 34 | init(_jsonValue value: JSONValue) throws 35 | } 36 | 37 | /// A protocol for a type that can be converted into a ``JSONValue``. 38 | /// 39 | /// This is used to interoperate between the type-safe Swift models and the `JSON` in a 40 | /// GraphQL network response/request or the `NormalizedCache`. 41 | public protocol JSONEncodable { 42 | 43 | /// Converts the type into a ``JSONValue`` that can be sent in a GraphQL network request or 44 | /// stored in the `NormalizedCache`. 45 | /// 46 | /// > Important: For a type that conforms to both ``JSONEncodable`` and ``JSONDecodable``, 47 | /// the return value of this function, when passed to ``JSONDecodable/init(jsonValue:)`` should 48 | /// initialize a value equal to the receiver. 49 | var _jsonValue: JSONValue { get } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/ApolloAPI/JSONDecodingError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// An error thrown while decoding `JSON`. 4 | /// 5 | /// This error should be thrown when a ``JSONDecodable`` initialization fails. 6 | /// `GraphQLExecutor` and `ApolloStore` may also throw this error when decoding a `JSON` fails. 7 | public enum JSONDecodingError: Error, LocalizedError, Hashable { 8 | /// A value that is expected to be present is missing from the ``JSONObject``. 9 | case missingValue 10 | /// A value that is non-null has a `null`value. 11 | case nullValue 12 | /// A value in a ``JSONObject`` was not of the expected `JSON` type. 13 | /// (eg. An object instead of a list) 14 | case wrongType 15 | /// The `value` could not be converted to the expected type. 16 | /// 17 | /// This error is thrown when a ``JSONDecodable`` initialization fails for the expected type. 18 | case couldNotConvert(value: AnyHashable, to: Any.Type) 19 | 20 | public var errorDescription: String? { 21 | switch self { 22 | case .missingValue: 23 | return "Missing value" 24 | case .nullValue: 25 | return "Unexpected null value" 26 | case .wrongType: 27 | return "Wrong type" 28 | case .couldNotConvert(let value, let expectedType): 29 | return "Could not convert \"\(value)\" to \(expectedType)" 30 | } 31 | } 32 | 33 | public static func == (lhs: JSONDecodingError, rhs: JSONDecodingError) -> Bool { 34 | switch (lhs, rhs) { 35 | case (.missingValue, .missingValue), 36 | (.nullValue, .nullValue), 37 | (.wrongType, .wrongType): 38 | return true 39 | 40 | case let (.couldNotConvert(value: lhsValue, to: lhsType), 41 | .couldNotConvert(value: rhsValue, to: rhsType)): 42 | return lhsValue == rhsValue && lhsType == rhsType 43 | 44 | default: 45 | return false 46 | } 47 | } 48 | 49 | public func hash(into hasher: inout Hasher) { 50 | hasher.combine(self) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/ApolloAPI/LocalCacheMutation.swift: -------------------------------------------------------------------------------- 1 | public protocol LocalCacheMutation: AnyObject, Hashable { 2 | static var operationType: GraphQLOperationType { get } 3 | 4 | var __variables: GraphQLOperation.Variables? { get } 5 | 6 | associatedtype Data: MutableRootSelectionSet 7 | } 8 | 9 | public extension LocalCacheMutation { 10 | var __variables: GraphQLOperation.Variables? { 11 | return nil 12 | } 13 | 14 | func hash(into hasher: inout Hasher) { 15 | hasher.combine(__variables?._jsonEncodableValue?._jsonValue) 16 | } 17 | 18 | static func ==(lhs: Self, rhs: Self) -> Bool { 19 | lhs.__variables?._jsonEncodableValue?._jsonValue == rhs.__variables?._jsonEncodableValue?._jsonValue 20 | } 21 | } 22 | 23 | public protocol MutableSelectionSet: SelectionSet { 24 | var __data: DataDict { get set } 25 | } 26 | 27 | public extension MutableSelectionSet { 28 | @inlinable var __typename: String? { 29 | get { __data["__typename"] } 30 | set { __data["__typename"] = newValue } 31 | } 32 | } 33 | 34 | public extension MutableSelectionSet where Fragments: FragmentContainer { 35 | @inlinable var fragments: Fragments { 36 | get { Self.Fragments(_dataDict: __data) } 37 | _modify { 38 | var f = Self.Fragments(_dataDict: __data) 39 | yield &f 40 | self.__data._data = f.__data._data 41 | } 42 | } 43 | } 44 | 45 | public extension MutableSelectionSet where Self: InlineFragment { 46 | 47 | /// Function for mutating a conditional inline fragment on a mutable selection set. 48 | /// 49 | /// This function is the only supported way to mutate an inline fragment. Because setting the 50 | /// value for an inline fragment that was not already present would result in fatal data 51 | /// inconsistencies, inline fragments properties are get-only. However, mutating the properties of 52 | /// an inline fragment that has been fulfilled is allowed. This function enables the described 53 | /// functionality by checking if the fragment is fulfilled and, if so, calling the mutation body. 54 | /// 55 | /// - Parameters: 56 | /// - keyPath: The `KeyPath` to the inline fragment to mutate the properties of 57 | /// - transform: A closure used to apply mutations to the inline fragment's properties. 58 | /// - Returns: A `Bool` indicating if the fragment was fulfilled. 59 | /// If this returns `false`, the `transform` block will not be called. 60 | @discardableResult 61 | mutating func mutateIfFulfilled( 62 | _ keyPath: KeyPath, 63 | _ transform: (inout T) -> Void 64 | ) -> Bool where T.RootEntityType == Self.RootEntityType { 65 | guard var fragment = self[keyPath: keyPath] else { 66 | return false 67 | } 68 | 69 | transform(&fragment) 70 | self.__data = fragment.__data 71 | return true 72 | } 73 | } 74 | 75 | public protocol MutableRootSelectionSet: RootSelectionSet, MutableSelectionSet {} 76 | 77 | public extension MutableRootSelectionSet { 78 | 79 | /// Function for mutating a conditional inline fragment on a mutable selection set. 80 | /// 81 | /// This function is the only supported way to mutate an inline fragment. Because setting the 82 | /// value for an inline fragment that was not already present would result in fatal data 83 | /// inconsistencies, inline fragments properties are get-only. However, mutating the properties of 84 | /// an inline fragment that has been fulfilled is allowed. This function enables the described 85 | /// functionality by checking if the fragment is fulfilled and, if so, calling the mutation body. 86 | /// 87 | /// - Parameters: 88 | /// - keyPath: The `KeyPath` to the inline fragment to mutate the properties of 89 | /// - transform: A closure used to apply mutations to the inline fragment's properties. 90 | /// - Returns: A `Bool` indicating if the fragment was fulfilled. 91 | /// If this returns `false`, the `transform` block will not be called. 92 | @discardableResult 93 | mutating func mutateIfFulfilled( 94 | _ keyPath: KeyPath, 95 | _ transform: (inout T) -> Void 96 | ) -> Bool where T.RootEntityType == Self { 97 | guard var fragment = self[keyPath: keyPath] else { 98 | return false 99 | } 100 | 101 | transform(&fragment) 102 | self.__data = fragment.__data 103 | return true 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Sources/ApolloAPI/ObjectData.swift: -------------------------------------------------------------------------------- 1 | public protocol _ObjectData_Transformer { 2 | func transform(_ value: AnyHashable) -> (any ScalarType)? 3 | func transform(_ value: AnyHashable) -> ObjectData? 4 | func transform(_ value: AnyHashable) -> ListData? 5 | } 6 | 7 | /// An opaque wrapper for data representing a GraphQL object. This type wraps data from different 8 | /// sources, using a `_transformer` to ensure the raw data from different sources (which may be in 9 | /// different formats) can be consumed with a consistent API. 10 | public struct ObjectData { 11 | public let _transformer: any _ObjectData_Transformer 12 | public let _rawData: [String: AnyHashable] 13 | 14 | public init( 15 | _transformer: any _ObjectData_Transformer, 16 | _rawData: [String: AnyHashable] 17 | ) { 18 | self._transformer = _transformer 19 | self._rawData = _rawData 20 | } 21 | 22 | @inlinable public subscript(_ key: String) -> (any ScalarType)? { 23 | guard let rawValue = _rawData[key] else { return nil } 24 | var value: AnyHashable = rawValue 25 | 26 | // This check is based on AnyHashable using a canonical representation of the type-erased value so 27 | // instances wrapping the same value of any type compare as equal. Therefore while Int(1) and Int(0) 28 | // might be representable as Bool they will never equal Bool(true) nor Bool(false). 29 | if let boolVal = value as? Bool, value.isCanonicalBool { 30 | value = boolVal 31 | 32 | // Cast to `Int` to ensure we always use `Int` vs `Int32` or `Int64` for consistency and ScalarType casting 33 | } else if let intValue = value as? Int { 34 | value = intValue 35 | } 36 | 37 | return _transformer.transform(value) 38 | } 39 | 40 | @_disfavoredOverload 41 | @inlinable public subscript(_ key: String) -> ObjectData? { 42 | guard let value = _rawData[key] else { return nil } 43 | return _transformer.transform(value) 44 | } 45 | 46 | @_disfavoredOverload 47 | @inlinable public subscript(_ key: String) -> ListData? { 48 | guard let value = _rawData[key] else { return nil } 49 | return _transformer.transform(value) 50 | } 51 | 52 | } 53 | 54 | /// An opaque wrapper for data representing the value for a list field on a GraphQL object. 55 | /// This type wraps data from different sources, using a `_transformer` to ensure the raw data from 56 | /// different sources (which may be in different formats) can be consumed with a consistent API. 57 | public struct ListData { 58 | public let _transformer: any _ObjectData_Transformer 59 | public let _rawData: [AnyHashable] 60 | 61 | public init( 62 | _transformer: any _ObjectData_Transformer, 63 | _rawData: [AnyHashable] 64 | ) { 65 | self._transformer = _transformer 66 | self._rawData = _rawData 67 | } 68 | 69 | @inlinable public subscript(_ key: Int) -> (any ScalarType)? { 70 | var value: AnyHashable = _rawData[key] 71 | 72 | // This check is based on AnyHashable using a canonical representation of the type-erased value so 73 | // instances wrapping the same value of any type compare as equal. Therefore while Int(1) and Int(0) 74 | // might be representable as Bool they will never equal Bool(true) nor Bool(false). 75 | if let boolVal = value as? Bool, value.isCanonicalBool { 76 | value = boolVal 77 | 78 | // Cast to `Int` to ensure we always use `Int` vs `Int32` or `Int64` for consistency and ScalarType casting 79 | } else if let intValue = value as? Int { 80 | value = intValue 81 | } 82 | 83 | return _transformer.transform(value) 84 | } 85 | 86 | @_disfavoredOverload 87 | @inlinable public subscript(_ key: Int) -> ObjectData? { 88 | return _transformer.transform(_rawData[key]) 89 | } 90 | 91 | @_disfavoredOverload 92 | @inlinable public subscript(_ key: Int) -> ListData? { 93 | return _transformer.transform(_rawData[key]) 94 | } 95 | } 96 | 97 | extension AnyHashable { 98 | fileprivate static let boolTrue = AnyHashable(true) 99 | fileprivate static let boolFalse = AnyHashable(false) 100 | 101 | @usableFromInline var isCanonicalBool: Bool { 102 | self == Self.boolTrue || self == Self.boolFalse 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Sources/ApolloAPI/OutputTypeConvertible.swift: -------------------------------------------------------------------------------- 1 | public protocol OutputTypeConvertible { 2 | @inlinable static var _asOutputType: Selection.Field.OutputType { get } 3 | } 4 | 5 | extension String: OutputTypeConvertible { 6 | public static let _asOutputType: Selection.Field.OutputType = .nonNull(.scalar(String.self)) 7 | } 8 | extension Int: OutputTypeConvertible { 9 | public static let _asOutputType: Selection.Field.OutputType = .nonNull(.scalar(Int.self)) 10 | } 11 | extension Bool: OutputTypeConvertible { 12 | public static let _asOutputType: Selection.Field.OutputType = .nonNull(.scalar(Bool.self)) 13 | } 14 | extension Float: OutputTypeConvertible { 15 | public static let _asOutputType: Selection.Field.OutputType = .nonNull(.scalar(Float.self)) 16 | } 17 | extension Double: OutputTypeConvertible { 18 | public static let _asOutputType: Selection.Field.OutputType = .nonNull(.scalar(Double.self)) 19 | } 20 | 21 | extension Optional: OutputTypeConvertible where Wrapped: OutputTypeConvertible { 22 | @inlinable public static var _asOutputType: Selection.Field.OutputType { 23 | guard case let .nonNull(wrappedOutputType) = Wrapped._asOutputType else { 24 | return Wrapped._asOutputType 25 | } 26 | return wrappedOutputType 27 | } 28 | } 29 | 30 | extension Array: OutputTypeConvertible where Element: OutputTypeConvertible { 31 | @inlinable public static var _asOutputType: Selection.Field.OutputType { 32 | .nonNull(.list(Element._asOutputType)) 33 | } 34 | } 35 | 36 | extension RootSelectionSet { 37 | @inlinable public static var _asOutputType: Selection.Field.OutputType { 38 | .nonNull(.object(self)) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/ApolloAPI/ParentType.swift: -------------------------------------------------------------------------------- 1 | /// A protocol for a type that represents the `__parentType` of a ``SelectionSet``. 2 | /// 3 | /// A ``SelectionSet``'s `__parentType` is the type from the schema that the selection set is 4 | /// selected against. This type can be an ``Object``, ``Interface``, or ``Union``. 5 | public protocol ParentType { 6 | /// A helper function to determine if an ``Object`` of the given type can be converted to 7 | /// the receiver type. 8 | /// 9 | /// A type can be converted to an ``Interface`` type if and only if the type implements 10 | /// the interface. 11 | /// (ie. The ``Interface`` is contained in the ``Object``'s ``Object/implementedInterfaces``.) 12 | /// 13 | /// A type can be converted to a ``Union`` type if and only if the union includes the type. 14 | /// (ie. The ``Object`` Type is contained in the ``Union``'s ``Union/possibleTypes``.) 15 | /// 16 | /// - Parameter objectType: An ``Object`` type to determine conversion compatibility for 17 | /// - Returns: A `Bool` indicating if the type is compatible for conversion to the receiver type 18 | @inlinable func canBeConverted(from objectType: Object) -> Bool 19 | 20 | @inlinable var __typename: String { get } 21 | } 22 | 23 | extension Object: ParentType { 24 | @inlinable public func canBeConverted(from objectType: Object) -> Bool { 25 | objectType.typename == self.typename 26 | } 27 | 28 | @inlinable public var __typename: String { self.typename } 29 | } 30 | 31 | extension Interface: ParentType { 32 | @inlinable public func canBeConverted(from objectType: Object) -> Bool { 33 | objectType.implements(self) 34 | } 35 | 36 | @inlinable public var __typename: String { self.name } 37 | } 38 | 39 | extension Union: ParentType { 40 | @inlinable public func canBeConverted(from objectType: Object) -> Bool { 41 | possibleTypes.contains(where: { $0 == objectType }) 42 | } 43 | 44 | @inlinable public var __typename: String { self.name } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/ApolloAPI/Resources/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyCollectedDataTypes 6 | 7 | NSPrivacyAccessedAPITypes 8 | 9 | NSPrivacyTrackingDomains 10 | 11 | NSPrivacyTracking 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Sources/ApolloAPI/ScalarTypes.swift: -------------------------------------------------------------------------------- 1 | /// An abstract protocol that a GraphQL "`scalar`" type must conform to. 2 | /// 3 | /// # See Also 4 | /// [GraphQL Spec - Scalars](http://spec.graphql.org/October2021/#sec-Scalars) 5 | public protocol AnyScalarType: JSONEncodable, AnyHashableConvertible {} 6 | 7 | /// A protocol that represents any GraphQL "`scalar`" defined in the GraphQL Specification. 8 | /// 9 | /// Conforming types are: 10 | /// * `String` 11 | /// * `Int` 12 | /// * `Bool` 13 | /// * `Float` 14 | /// * `Double` 15 | /// 16 | /// # See Also 17 | /// [GraphQL Spec - Scalars](http://spec.graphql.org/October2021/#sec-Scalars) 18 | public protocol ScalarType: 19 | AnyScalarType, 20 | JSONDecodable, 21 | GraphQLOperationVariableValue {} 22 | 23 | extension String: ScalarType {} 24 | extension Int: ScalarType {} 25 | extension Bool: ScalarType {} 26 | extension Float: ScalarType {} 27 | extension Double: ScalarType {} 28 | 29 | /// A protocol a custom GraphQL "`scalar`" must conform to. 30 | /// 31 | /// Custom scalars defined in a schema are generated to conform to the ``CustomScalarType`` 32 | /// protocol. By default, these are generated as typealiases to `String`. You can edit the 33 | /// implementation of a custom scalar in the generated file. *Changes to generated custom scalar 34 | /// types will not be overwritten when running code generation again.* 35 | /// 36 | /// # See Also 37 | /// [GraphQL Spec - Scalars](http://spec.graphql.org/October2021/#sec-Scalars) 38 | public protocol CustomScalarType: 39 | AnyScalarType, 40 | JSONDecodable, 41 | OutputTypeConvertible, 42 | GraphQLOperationVariableValue 43 | {} 44 | 45 | extension CustomScalarType { 46 | @inlinable public static var _asOutputType: Selection.Field.OutputType { 47 | .nonNull(.customScalar(self)) 48 | } 49 | } 50 | 51 | extension Array: AnyScalarType where Array.Element: AnyScalarType & Hashable {} 52 | 53 | extension Optional: AnyScalarType where Wrapped: AnyScalarType & Hashable {} 54 | -------------------------------------------------------------------------------- /Sources/ApolloAPI/SchemaConfiguration.swift: -------------------------------------------------------------------------------- 1 | /// A protocol for an object used to provide custom configuration for a generated GraphQL schema. 2 | /// 3 | /// A ``SchemaConfiguration`` provides an entry point for customizing the cache key resolution 4 | /// for the types in the schema, which is used by `NormalizedCache` mechanisms. 5 | public protocol SchemaConfiguration { 6 | /// The entry point for configuring the cache key resolution 7 | /// for the types in the schema, which is used by `NormalizedCache` mechanisms. 8 | /// 9 | /// The default generated implementation always returns `nil`, disabling all cache normalization. 10 | /// 11 | /// Cache key resolution has a few notable quirks and limitations you should be aware of while 12 | /// implementing your cache key resolution function: 13 | /// 14 | /// 1. While the cache key for an object can use a field from another nested object, if the fields 15 | /// on the referenced object are changed in another operation, the cache key for the dependent 16 | /// object will not be updated. For nested objects that are not normalized with their own cache 17 | /// key, this will never occur, but if the nested object is an entity with its own cache key, it 18 | /// can be mutated independently. In that case, any other objects whose cache keys are dependent 19 | /// on the mutated entity will not be updated automatically. You must take care to update those 20 | /// entities manually with a cache mutation. 21 | /// 22 | /// 2. The `object` passed to this function represents data for an object in an specific operation 23 | /// model, not a type in your schema. This means that 24 | /// [aliased fields](https://spec.graphql.org/draft/#sec-Field-Alias) will be keyed on their 25 | /// alias name, not the name of the field on the schema type. 26 | /// 27 | /// 3. The `object` parameter of this function is an ``ObjectData`` struct that wraps the 28 | /// underlying object data. Because cache key resolution is performed both on raw JSON (from a 29 | /// network response) and `SelectionSet` model data (when writing to the cache directly), 30 | /// the underlying data will have different formats. The ``ObjectData`` wrapper is used to 31 | /// normalize this data to a consistent format in this context. 32 | /// 33 | /// # See Also 34 | /// ``CacheKeyInfo`` 35 | /// 36 | /// - Parameters: 37 | /// - type: The ``Object`` type of the response `object`. 38 | /// - object: The response object to resolve the cache key for. 39 | /// Represented as a ``ObjectData`` dictionary. 40 | /// - Returns: A ``CacheKeyInfo`` describing the computed cache key for the response object. 41 | static func cacheKeyInfo(for type: Object, object: ObjectData) -> CacheKeyInfo? 42 | 43 | } 44 | -------------------------------------------------------------------------------- /Sources/ApolloAPI/SchemaTypes/EnumType.swift: -------------------------------------------------------------------------------- 1 | /// A protocol that an enum in a generated GraphQL schema conforms to. 2 | /// 3 | /// When used as an input value or the value of a field in a generated ``SelectionSet``, each 4 | /// generated ``EnumType`` will be wrapped in a ``GraphQLEnum`` which provides support for handling 5 | /// unknown enum cases that were not included in the schema at the time of code generation. 6 | /// 7 | /// # See Also 8 | /// [GraphQLSpec - Enums](https://spec.graphql.org/draft/#sec-Enums) 9 | public protocol EnumType: 10 | RawRepresentable, 11 | CaseIterable, 12 | JSONEncodable, 13 | GraphQLOperationVariableValue 14 | where RawValue == String {} 15 | -------------------------------------------------------------------------------- /Sources/ApolloAPI/SchemaTypes/InputObject.swift: -------------------------------------------------------------------------------- 1 | /// An protocol for a struct that represents a GraphQL Input Object. 2 | /// 3 | /// # See Also 4 | /// [GraphQLSpec - Input Objects](https://spec.graphql.org/draft/#sec-Input-Objects) 5 | public protocol InputObject: GraphQLOperationVariableValue, JSONEncodable, Hashable { 6 | var __data: InputDict { get } 7 | } 8 | 9 | extension InputObject { 10 | public var _jsonValue: JSONValue { jsonEncodableValue?._jsonValue } 11 | public var jsonEncodableValue: (any JSONEncodable)? { __data._jsonEncodableValue } 12 | 13 | public static func == (lhs: Self, rhs: Self) -> Bool { 14 | lhs.__data == rhs.__data 15 | } 16 | 17 | public func hash(into hasher: inout Hasher) { 18 | hasher.combine(__data) 19 | } 20 | } 21 | 22 | /// A structure that wraps the underlying data dictionary used by `InputObject`s. 23 | public struct InputDict: GraphQLOperationVariableValue, Hashable { 24 | 25 | private var data: [String: any GraphQLOperationVariableValue] 26 | 27 | public init(_ data: [String: any GraphQLOperationVariableValue] = [:]) { 28 | self.data = data 29 | } 30 | 31 | public var _jsonEncodableValue: (any JSONEncodable)? { data._jsonEncodableObject } 32 | 33 | @_disfavoredOverload 34 | public subscript(key: String) -> T { 35 | get { data[key] as! T } 36 | set { data[key] = newValue } 37 | } 38 | 39 | public subscript(key: String) -> GraphQLNullable { 40 | get { 41 | if let value = data[key] { 42 | return value as! GraphQLNullable 43 | } 44 | 45 | return .none 46 | } 47 | set { data[key] = newValue } 48 | } 49 | 50 | public static func == (lhs: InputDict, rhs: InputDict) -> Bool { 51 | lhs.data._jsonEncodableValue?._jsonValue == rhs.data._jsonEncodableValue?._jsonValue 52 | } 53 | 54 | public func hash(into hasher: inout Hasher) { 55 | hasher.combine(data._jsonEncodableValue?._jsonValue) 56 | } 57 | 58 | } 59 | 60 | public protocol OneOfInputObject: InputObject { } 61 | -------------------------------------------------------------------------------- /Sources/ApolloAPI/SchemaTypes/Interface.swift: -------------------------------------------------------------------------------- 1 | /// Represents an `interface` type in a generated GraphQL schema. 2 | /// 3 | /// Each `interface` defined in the GraphQL schema will have an instance of ``Interface`` generated. 4 | /// 5 | /// # See Also 6 | /// [GraphQLSpec - Interfaces](https://spec.graphql.org/draft/#sec-Interfaces) 7 | public struct Interface: Hashable, Sendable { 8 | /// The name of the ``Interface`` in the GraphQL schema. 9 | public let name: String 10 | 11 | /// A list of fields used to uniquely identify an instance of an object implementing this interface. 12 | /// 13 | /// This is set by adding a `@typePolicy` directive to the schema. 14 | public let keyFields: [String]? 15 | 16 | /// A list of name for Objects that implement this Interface 17 | public let implementingObjects: [String] 18 | 19 | /// Designated Initializer 20 | /// 21 | /// - Parameter name: The name of the ``Interface`` in the GraphQL schema. 22 | public init( 23 | name: String, 24 | keyFields: [String]? = nil, 25 | implementingObjects: [String] 26 | ) { 27 | self.name = name 28 | if keyFields?.isEmpty == false { 29 | self.keyFields = keyFields 30 | } else { 31 | self.keyFields = nil 32 | } 33 | self.implementingObjects = implementingObjects 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/ApolloAPI/SchemaTypes/Object.swift: -------------------------------------------------------------------------------- 1 | /// Represents an object `type` in a generated GraphQL schema. 2 | /// 3 | /// Each `type` defined in the GraphQL schema will have an instance of ``Object`` generated. 4 | /// # See Also 5 | /// [GraphQLSpec - Objects](https://spec.graphql.org/draft/#sec-Objects) 6 | public struct Object: Hashable, Sendable { 7 | 8 | /// Designated Initializer 9 | /// 10 | /// - Parameters: 11 | /// - typename: The name of the type. 12 | /// - implementedInterfaces: A list of the interfaces implemented by the type. 13 | /// - keyFields: A list of field names that are used to uniquely identify an instance of this type. 14 | public init( 15 | typename: String, 16 | implementedInterfaces: [Interface], 17 | keyFields: [String]? = nil 18 | ) { 19 | self.typename = typename 20 | self._implementedInterfaces = implementedInterfaces 21 | if keyFields?.isEmpty == false { 22 | self.keyFields = keyFields 23 | } else { 24 | self.keyFields = nil 25 | } 26 | } 27 | 28 | private let _implementedInterfaces: [Interface] 29 | 30 | /// A list of the interfaces implemented by the type. 31 | @available(*, deprecated, message: "This property will be removed in version 2.0. To check if an Object implements an interface please use the 'implements(_)' function.") 32 | public var implementedInterfaces: [Interface] { 33 | return _implementedInterfaces 34 | } 35 | 36 | /// The name of the type. 37 | /// 38 | /// When an entity of the type is included in a GraphQL response its `__typename` field will 39 | /// match this value. 40 | public let typename: String 41 | 42 | /// A list of fields used to uniquely identify an instance of this object. 43 | /// 44 | /// This is set by adding a `@typePolicy` directive to the schema. 45 | public let keyFields: [String]? 46 | 47 | /// A helper function to determine if the receiver implements a given ``Interface`` Type. 48 | /// 49 | /// - Parameter interface: An ``Interface`` Type 50 | /// - Returns: A `Bool` indicating if the receiver implements the given ``Interface`` Type. 51 | public func implements(_ interface: Interface) -> Bool { 52 | interface.implementingObjects.contains(typename) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/ApolloAPI/SchemaTypes/Union.swift: -------------------------------------------------------------------------------- 1 | /// Represents a `union` type in a generated GraphQL schema. 2 | /// 3 | /// Each `union` defined in the GraphQL schema will have an instance of ``Union`` generated. 4 | /// 5 | /// # See Also 6 | /// [GraphQLSpec - Unions](https://spec.graphql.org/draft/#sec-Unions) 7 | public struct Union: Hashable, Sendable { 8 | /// The name of the ``Union`` in the GraphQL schema. 9 | public let name: String 10 | 11 | /// The ``Object`` types included in the `union`. 12 | public let possibleTypes: [Object] 13 | 14 | /// Designated Initializer 15 | /// 16 | /// - Parameters: 17 | /// - name: The name of the ``Union`` in the GraphQL schema. 18 | /// - possibleTypes: The ``Object`` types included in the `union`. 19 | public init(name: String, possibleTypes: [Object]) { 20 | self.name = name 21 | self.possibleTypes = possibleTypes 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /Sources/ApolloAPI/Selection+Conditions.swift: -------------------------------------------------------------------------------- 1 | public extension Selection { 2 | /// The conditions representing a group of `@include/@skip` directives. 3 | /// 4 | /// The conditions are a two-dimensional array of `Selection.Condition`s. 5 | /// The outer array represents groups of conditions joined together with a logical "or". 6 | /// Conditions in the same inner array are joined together with a logical "and". 7 | struct Conditions: Hashable { 8 | public let value: [[Condition]] 9 | 10 | public init(_ value: [[Condition]]) { 11 | self.value = value 12 | } 13 | 14 | public init(_ conditions: [Condition]...) { 15 | self.value = Array(conditions) 16 | } 17 | 18 | public init(_ condition: Condition) { 19 | self.value = [[condition]] 20 | } 21 | 22 | @inlinable public static func ||(_ lhs: Conditions, rhs: [Condition]) -> Conditions { 23 | var newValue = lhs.value 24 | newValue.append(rhs) 25 | return .init(newValue) 26 | } 27 | 28 | @inlinable public static func ||(_ lhs: Conditions, rhs: Condition) -> Conditions { 29 | lhs || [rhs] 30 | } 31 | } 32 | 33 | enum Condition: ExpressibleByStringLiteral, ExpressibleByBooleanLiteral, Hashable { 34 | case value(Bool) 35 | case variable(name: String, inverted: Bool) 36 | 37 | public init( 38 | variableName: String, 39 | inverted: Bool 40 | ) { 41 | self = .variable(name: variableName, inverted: inverted) 42 | } 43 | 44 | public init(stringLiteral value: StringLiteralType) { 45 | self = .variable(name: value, inverted: false) 46 | } 47 | 48 | public init(booleanLiteral value: BooleanLiteralType) { 49 | self = .value(value) 50 | } 51 | 52 | @inlinable public static func `if`(_ condition: StringLiteralType) -> Condition { 53 | .variable(name: condition, inverted: false) 54 | } 55 | 56 | @inlinable public static func `if`(_ condition: Condition) -> Condition { 57 | condition 58 | } 59 | 60 | @inlinable public static prefix func !(condition: Condition) -> Condition { 61 | switch condition { 62 | case let .value(value): 63 | return .value(!value) 64 | case let .variable(name, inverted): 65 | return .init(variableName: name, inverted: !inverted) 66 | } 67 | } 68 | 69 | @inlinable public static func &&(_ lhs: Condition, rhs: Condition) -> [Condition] { 70 | [lhs, rhs] 71 | } 72 | 73 | @inlinable public static func &&(_ lhs: [Condition], rhs: Condition) -> [Condition] { 74 | var newValue = lhs 75 | newValue.append(rhs) 76 | return newValue 77 | } 78 | 79 | @inlinable public static func ||(_ lhs: Condition, rhs: Condition) -> Conditions { 80 | .init([[lhs], [rhs]]) 81 | } 82 | 83 | @inlinable public static func ||(_ lhs: [Condition], rhs: Condition) -> Conditions { 84 | .init([lhs, [rhs]]) 85 | } 86 | 87 | } 88 | } 89 | 90 | // MARK: - Evaluation 91 | 92 | // MARK: Conditions - Or Group 93 | public extension Selection.Conditions { 94 | func evaluate(with variables: GraphQLOperation.Variables?) -> Bool { 95 | for andGroup in value { 96 | if andGroup.evaluate(with: variables) { 97 | return true 98 | } 99 | } 100 | return false 101 | } 102 | } 103 | 104 | // MARK: Conditions - And Group 105 | fileprivate extension Array where Element == Selection.Condition { 106 | func evaluate(with variables: GraphQLOperation.Variables?) -> Bool { 107 | for condition in self { 108 | if !condition.evaluate(with: variables) { 109 | return false 110 | } 111 | } 112 | return true 113 | } 114 | } 115 | 116 | // MARK: Conditions - Individual 117 | public extension Selection.Condition { 118 | func evaluate(with variables: GraphQLOperation.Variables?) -> Bool { 119 | switch self { 120 | case let .value(value): 121 | return value 122 | case let .variable(variableName, inverted): 123 | switch variables?[variableName] { 124 | case let boolValue as Bool: 125 | return inverted ? !boolValue : boolValue 126 | 127 | case let nullable as GraphQLNullable: 128 | let evaluated = nullable.unwrapped ?? false 129 | return inverted ? !evaluated : evaluated 130 | 131 | case .none: 132 | return false 133 | 134 | case let .some(wrapped): 135 | fatalError("Expected Bool for \(variableName), got \(wrapped)") 136 | } 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /Sources/ApolloSQLite/Documentation.docc/Documentation.md: -------------------------------------------------------------------------------- 1 | # ``ApolloSQLite`` 2 | 3 | A [`NormalizedCache`](/documentation/apollo/normalizedcache) implementation backed by a `SQLite` database. 4 | 5 | ## Overview 6 | 7 | ``SQLiteNormalizedCache`` is currently the primary supported mechanism for persisting Apollo cache data between application runs. 8 | -------------------------------------------------------------------------------- /Sources/ApolloSQLite/JournalMode.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum JournalMode: String { 4 | /// The rollback journal is deleted at the conclusion of each transaction. This is the default behaviour. 5 | case delete = "DELETE" 6 | /// Commits transactions by truncating the rollback journal to zero-length instead of deleting it. 7 | case truncate = "TRUNCATE" 8 | /// Prevents the rollback journal from being deleted at the end of each transaction. Instead, the header 9 | /// of the journal is overwritten with zeros. 10 | case persist = "PERSIST" 11 | /// Stores the rollback journal in volatile RAM. This saves disk I/O but at the expense of database 12 | /// safety and integrity. 13 | case memory = "MEMORY" 14 | /// Uses a write-ahead log instead of a rollback journal to implement transactions. The WAL journaling 15 | /// mode is persistent; after being set it stays in effect across multiple database connections and after 16 | /// closing and reopening the database. 17 | case wal = "WAL" 18 | /// Disables the rollback journal completely 19 | case off = "OFF" 20 | } 21 | -------------------------------------------------------------------------------- /Sources/ApolloSQLite/Resources/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyCollectedDataTypes 6 | 7 | NSPrivacyAccessedAPITypes 8 | 9 | NSPrivacyTrackingDomains 10 | 11 | NSPrivacyTracking 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Sources/ApolloSQLite/SQLiteDatabase.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if !COCOAPODS 3 | import Apollo 4 | #endif 5 | 6 | public struct DatabaseRow { 7 | let cacheKey: CacheKey 8 | let storedInfo: String 9 | 10 | public init(cacheKey: CacheKey, storedInfo: String) { 11 | self.cacheKey = cacheKey 12 | self.storedInfo = storedInfo 13 | } 14 | } 15 | 16 | public enum SQLiteError: Error, CustomStringConvertible { 17 | case execution(message: String) 18 | case open(path: String) 19 | case prepare(message: String) 20 | case step(message: String) 21 | 22 | public var description: String { 23 | switch self { 24 | case .execution(let message): 25 | return message 26 | case .open(let path): 27 | return "Failed to open SQLite database connection at path: \(path)" 28 | case .prepare(let message): 29 | return message 30 | case .step(let message): 31 | return message 32 | } 33 | } 34 | } 35 | 36 | public protocol SQLiteDatabase { 37 | 38 | init(fileURL: URL) throws 39 | 40 | func createRecordsTableIfNeeded() throws 41 | 42 | func selectRawRows(forKeys keys: Set) throws -> [DatabaseRow] 43 | 44 | func addOrUpdate(records: [(cacheKey: CacheKey, recordString: String)]) throws 45 | 46 | func deleteRecord(for cacheKey: CacheKey) throws 47 | 48 | func deleteRecords(matching pattern: CacheKey) throws 49 | 50 | func clearDatabase(shouldVacuumOnClear: Bool) throws 51 | 52 | @available(*, deprecated, renamed: "addOrUpdate(records:)") 53 | func addOrUpdateRecordString(_ recordString: String, for cacheKey: CacheKey) throws 54 | 55 | } 56 | 57 | extension SQLiteDatabase { 58 | 59 | public func addOrUpdateRecordString(_ recordString: String, for cacheKey: CacheKey) throws { 60 | try addOrUpdate(records: [(cacheKey, recordString)]) 61 | } 62 | 63 | } 64 | 65 | public extension SQLiteDatabase { 66 | 67 | static var tableName: String { 68 | "records" 69 | } 70 | 71 | static var idColumnName: String { 72 | "_id" 73 | } 74 | 75 | static var keyColumnName: String { 76 | "key" 77 | } 78 | 79 | static var recordColumName: String { 80 | "record" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Sources/ApolloSQLite/SQLiteSerialization.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if !COCOAPODS 3 | import Apollo 4 | import ApolloAPI 5 | #endif 6 | 7 | private let serializedReferenceKey = "$reference" 8 | 9 | enum SQLiteSerialization { 10 | static func serialize(fields: Record.Fields) throws -> Data { 11 | let jsonObject = try fields.compactMapValues(serialize(fieldValue:)) 12 | return try JSONSerialization.data(withJSONObject: jsonObject, options: []) 13 | } 14 | 15 | private static func serialize(fieldValue: Record.Value) throws -> Any { 16 | switch fieldValue { 17 | case let reference as CacheReference: 18 | return [serializedReferenceKey: reference.key] 19 | case let array as [Record.Value]: 20 | return try array.map { try serialize(fieldValue: $0) } 21 | default: 22 | return fieldValue 23 | } 24 | } 25 | 26 | static func deserialize(data: Data) throws -> Record.Fields { 27 | let object = try JSONSerialization.jsonObject(with: data, options: []) 28 | guard let jsonObject = object as? JSONObject else { 29 | throw SQLiteNormalizedCacheError.invalidRecordShape(object: object) 30 | } 31 | var fields = Record.Fields() 32 | for (key, value) in jsonObject { 33 | fields[key] = try deserialize(fieldJSONValue: value) 34 | } 35 | return fields 36 | } 37 | 38 | private static func deserialize(fieldJSONValue: JSONValue) throws -> Record.Value { 39 | switch fieldJSONValue { 40 | case let dictionary as JSONObject: 41 | guard let reference = dictionary[serializedReferenceKey] as? String else { 42 | return fieldJSONValue 43 | } 44 | return CacheReference(reference) 45 | case let array as [JSONValue]: 46 | return try array.map { try deserialize(fieldJSONValue: $0) } 47 | default: 48 | return fieldJSONValue 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/ApolloTestSupport/Field.swift: -------------------------------------------------------------------------------- 1 | #if !COCOAPODS 2 | import ApolloAPI 3 | #endif 4 | 5 | @propertyWrapper 6 | public struct Field { 7 | 8 | let key: StaticString 9 | 10 | public init(_ field: StaticString) { 11 | self.key = field 12 | } 13 | 14 | public var wrappedValue: Self { 15 | get { self } 16 | set { preconditionFailure() } 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /Sources/ApolloTestSupport/TestMockSelectionSetMapper.swift: -------------------------------------------------------------------------------- 1 | @_spi(Execution) import Apollo 2 | import Foundation 3 | 4 | /// An accumulator that converts data from a `Mock` to the correct values to create a `SelectionSet`. 5 | final class TestMockSelectionSetMapper: GraphQLResultAccumulator { 6 | 7 | var requiresCacheKeyComputation: Bool { underlyingMapper.requiresCacheKeyComputation } 8 | let underlyingMapper = GraphQLSelectionSetMapper(handleMissingValues: .allowForAllFields) 9 | 10 | func accept(scalar: AnyHashable, info: FieldExecutionInfo) throws -> AnyHashable? { 11 | return scalar 12 | } 13 | 14 | func accept(customScalar: AnyHashable, info: FieldExecutionInfo) throws -> AnyHashable? { 15 | return customScalar 16 | } 17 | 18 | func acceptNullValue(info: FieldExecutionInfo) -> AnyHashable? { 19 | return underlyingMapper.acceptNullValue(info: info) 20 | } 21 | 22 | func acceptMissingValue(info: FieldExecutionInfo) throws -> AnyHashable? { 23 | return try underlyingMapper.acceptMissingValue(info: info) 24 | } 25 | 26 | func accept(list: [AnyHashable?], info: FieldExecutionInfo) -> AnyHashable? { 27 | return underlyingMapper.accept(list: list, info: info) 28 | } 29 | 30 | func accept(childObject: DataDict, info: FieldExecutionInfo) throws -> AnyHashable? { 31 | return try underlyingMapper.accept(childObject: childObject, info: info) 32 | } 33 | 34 | func accept(fieldEntry: AnyHashable?, info: FieldExecutionInfo) -> (key: String, value: AnyHashable)? { 35 | return underlyingMapper.accept(fieldEntry: fieldEntry, info: info) 36 | } 37 | 38 | func accept( 39 | fieldEntries: [(key: String, value: AnyHashable)], 40 | info: ObjectExecutionInfo 41 | ) throws -> DataDict { 42 | return try underlyingMapper.accept(fieldEntries: fieldEntries, info: info) 43 | } 44 | 45 | func finish(rootValue: DataDict, info: ObjectExecutionInfo) -> T { 46 | return underlyingMapper.finish(rootValue: rootValue, info: info) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/ApolloWebSocket/DefaultImplementation/SSLClientCertificate.swift: -------------------------------------------------------------------------------- 1 | // Created by Tomasz Trela on 08/03/2018. 2 | // Copyright © 2018 Vluxe. All rights reserved. 3 | // Modified by Anthony Miller & Apollo GraphQL on 8/12/21 4 | // 5 | // This is a derived work derived from 6 | // Starscream (https://github.com/daltoniam/Starscream) 7 | // 8 | // Original Work License: http://www.apache.org/licenses/LICENSE-2.0 9 | // Derived Work License: https://github.com/apollographql/apollo-ios/blob/main/LICENSE 10 | 11 | import Foundation 12 | 13 | public struct SSLClientCertificateError: LocalizedError { 14 | public var errorDescription: String? 15 | 16 | init(errorDescription: String) { 17 | self.errorDescription = errorDescription 18 | } 19 | } 20 | 21 | public class SSLClientCertificate { 22 | internal let streamSSLCertificates: NSArray 23 | 24 | /** 25 | Convenience init. 26 | - parameter pkcs12Path: Path to pkcs12 file containing private key and X.509 ceritifacte (.p12) 27 | - parameter password: file password, see **kSecImportExportPassphrase** 28 | */ 29 | public convenience init(pkcs12Path: String, password: String) throws { 30 | let pkcs12Url = URL(fileURLWithPath: pkcs12Path) 31 | do { 32 | try self.init(pkcs12Url: pkcs12Url, password: password) 33 | } catch { 34 | throw error 35 | } 36 | } 37 | 38 | /** 39 | Designated init. For more information, see SSLSetCertificate() in Security/SecureTransport.h. 40 | - parameter identity: SecIdentityRef, see **kCFStreamSSLCertificates** 41 | - parameter identityCertificate: CFArray of SecCertificateRefs, see **kCFStreamSSLCertificates** 42 | */ 43 | public init(identity: SecIdentity, identityCertificate: SecCertificate) { 44 | self.streamSSLCertificates = NSArray(objects: identity, identityCertificate) 45 | } 46 | 47 | /** 48 | Convenience init. 49 | - parameter pkcs12Url: URL to pkcs12 file containing private key and X.509 ceritifacte (.p12) 50 | - parameter password: file password, see **kSecImportExportPassphrase** 51 | */ 52 | public convenience init(pkcs12Url: URL, password: String) throws { 53 | let importOptions = [kSecImportExportPassphrase as String : password] as CFDictionary 54 | do { 55 | try self.init(pkcs12Url: pkcs12Url, importOptions: importOptions) 56 | } catch { 57 | throw error 58 | } 59 | } 60 | 61 | /** 62 | Designated init. 63 | - parameter pkcs12Url: URL to pkcs12 file containing private key and X.509 ceritifacte (.p12) 64 | - parameter importOptions: A dictionary containing import options. A 65 | kSecImportExportPassphrase entry is required at minimum. Only password-based 66 | PKCS12 blobs are currently supported. See **SecImportExport.h** 67 | */ 68 | public init(pkcs12Url: URL, importOptions: CFDictionary) throws { 69 | do { 70 | let pkcs12Data = try Data(contentsOf: pkcs12Url) 71 | var rawIdentitiesAndCertificates: CFArray? 72 | let pkcs12CFData: CFData = pkcs12Data as CFData 73 | let importStatus = SecPKCS12Import(pkcs12CFData, importOptions, &rawIdentitiesAndCertificates) 74 | 75 | guard importStatus == errSecSuccess else { 76 | throw SSLClientCertificateError(errorDescription: "Error during 'SecPKCS12Import', see 'SecBase.h' - OSStatus: \(importStatus)") 77 | } 78 | guard let identitiyAndCertificate = (rawIdentitiesAndCertificates as? Array>)?.first else { 79 | throw SSLClientCertificateError(errorDescription: "Error - PKCS12 file is empty") 80 | } 81 | 82 | let identity = identitiyAndCertificate[kSecImportItemIdentity as String] as! SecIdentity 83 | var identityCertificate: SecCertificate? 84 | let copyStatus = SecIdentityCopyCertificate(identity, &identityCertificate) 85 | guard copyStatus == errSecSuccess else { 86 | throw SSLClientCertificateError(errorDescription: "Error during 'SecIdentityCopyCertificate', see 'SecBase.h' - OSStatus: \(copyStatus)") 87 | } 88 | self.streamSSLCertificates = NSArray(objects: identity, identityCertificate!) 89 | } catch { 90 | throw error 91 | } 92 | } 93 | } 94 | 95 | -------------------------------------------------------------------------------- /Sources/ApolloWebSocket/Documentation.docc/Documentation.md: -------------------------------------------------------------------------------- 1 | # ``ApolloWebSocket`` 2 | 3 | A web socket network transport implementation that provides support for [`GraphQLSubscription`](/documentation/apolloapi/graphqlsubscription) operations over a web socket connection. 4 | 5 | ## Overview 6 | 7 | To support subscriptions over web sockets, initialize your [`ApolloClient`](/documentation/apollo/apolloclient) with a ``SplitNetworkTransport``. 8 | -------------------------------------------------------------------------------- /Sources/ApolloWebSocket/OperationMessageIdCreator.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if !COCOAPODS 3 | import Apollo 4 | #endif 5 | 6 | public protocol OperationMessageIdCreator { 7 | func requestId() -> String 8 | } 9 | 10 | // MARK: - Default Implementation 11 | 12 | /// The default implementation of `OperationMessageIdCreator` that uses a sequential numbering scheme. 13 | public struct ApolloSequencedOperationMessageIdCreator: OperationMessageIdCreator { 14 | @Atomic private var sequenceNumberCounter: Int = 0 15 | 16 | /// Designated initializer. 17 | /// 18 | /// - Parameter startAt: The number from which the sequenced numbering scheme should start. 19 | public init(startAt sequenceNumber: Int = 1) { 20 | _sequenceNumberCounter = Atomic(wrappedValue: sequenceNumber) 21 | } 22 | 23 | /// Returns the number in the current sequence. Will be incremented when calling this method. 24 | public func requestId() -> String { 25 | let id = sequenceNumberCounter 26 | _ = $sequenceNumberCounter.increment() 27 | 28 | return "\(id)" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/ApolloWebSocket/Resources/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyCollectedDataTypes 6 | 7 | NSPrivacyAccessedAPITypes 8 | 9 | NSPrivacyTrackingDomains 10 | 11 | NSPrivacyTracking 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Sources/ApolloWebSocket/SplitNetworkTransport.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if !COCOAPODS 3 | import Apollo 4 | import ApolloAPI 5 | #endif 6 | 7 | /// 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. 8 | public class SplitNetworkTransport { 9 | private let uploadingNetworkTransport: any UploadingNetworkTransport 10 | private let webSocketNetworkTransport: any NetworkTransport 11 | 12 | public var clientName: String { 13 | let httpName = self.uploadingNetworkTransport.clientName 14 | let websocketName = self.webSocketNetworkTransport.clientName 15 | if httpName == websocketName { 16 | return httpName 17 | } else { 18 | return "SPLIT_HTTPNAME_\(httpName)_WEBSOCKETNAME_\(websocketName)" 19 | } 20 | } 21 | 22 | public var clientVersion: String { 23 | let httpVersion = self.uploadingNetworkTransport.clientVersion 24 | let websocketVersion = self.webSocketNetworkTransport.clientVersion 25 | if httpVersion == websocketVersion { 26 | return httpVersion 27 | } else { 28 | return "SPLIT_HTTPVERSION_\(httpVersion)_WEBSOCKETVERSION_\(websocketVersion)" 29 | } 30 | } 31 | 32 | /// Designated initializer 33 | /// 34 | /// - Parameters: 35 | /// - uploadingNetworkTransport: An `UploadingNetworkTransport` to use for non-subscription requests. Should generally be a `RequestChainNetworkTransport` or something similar. 36 | /// - webSocketNetworkTransport: A `NetworkTransport` to use for subscription requests. Should generally be a `WebSocketTransport` or something similar. 37 | public init(uploadingNetworkTransport: any UploadingNetworkTransport, webSocketNetworkTransport: any NetworkTransport) { 38 | self.uploadingNetworkTransport = uploadingNetworkTransport 39 | self.webSocketNetworkTransport = webSocketNetworkTransport 40 | } 41 | } 42 | 43 | // MARK: - NetworkTransport conformance 44 | 45 | extension SplitNetworkTransport: NetworkTransport { 46 | 47 | public func send(operation: Operation, 48 | cachePolicy: CachePolicy, 49 | contextIdentifier: UUID? = nil, 50 | context: (any RequestContext)? = nil, 51 | callbackQueue: DispatchQueue = .main, 52 | completionHandler: @escaping (Result, any Error>) -> Void) -> any Cancellable { 53 | if Operation.operationType == .subscription { 54 | return webSocketNetworkTransport.send(operation: operation, 55 | cachePolicy: cachePolicy, 56 | contextIdentifier: contextIdentifier, 57 | context: context, 58 | callbackQueue: callbackQueue, 59 | completionHandler: completionHandler) 60 | } else { 61 | return uploadingNetworkTransport.send(operation: operation, 62 | cachePolicy: cachePolicy, 63 | contextIdentifier: contextIdentifier, 64 | context: context, 65 | callbackQueue: callbackQueue, 66 | completionHandler: completionHandler) 67 | } 68 | } 69 | } 70 | 71 | // MARK: - UploadingNetworkTransport conformance 72 | 73 | extension SplitNetworkTransport: UploadingNetworkTransport { 74 | 75 | public func upload( 76 | operation: Operation, 77 | files: [GraphQLFile], 78 | context: (any RequestContext)?, 79 | callbackQueue: DispatchQueue = .main, 80 | completionHandler: @escaping (Result, any Error>) -> Void) -> any Cancellable { 81 | return uploadingNetworkTransport.upload(operation: operation, 82 | files: files, 83 | context: context, 84 | callbackQueue: callbackQueue, 85 | completionHandler: completionHandler) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Sources/ApolloWebSocket/WebSocketClient.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: - Client protocol 4 | 5 | /// Protocol allowing alternative implementations of websockets beyond `ApolloWebSocket`. 6 | public protocol WebSocketClient: AnyObject { 7 | 8 | /// The URLRequest used on connection. 9 | var request: URLRequest { get set } 10 | 11 | /// The delegate that will receive networking event updates for this websocket client. 12 | /// 13 | /// - Note: The `WebSocketTransport` will set itself as the delgate for the client. Consumers 14 | /// should set themselves as the delegate for the `WebSocketTransport` to observe events. 15 | var delegate: (any WebSocketClientDelegate)? { get set } 16 | 17 | /// `DispatchQueue` where the websocket client should call all delegate callbacks. 18 | var callbackQueue: DispatchQueue { get set } 19 | 20 | /// Connects to the websocket server. 21 | /// 22 | /// - Note: This should be implemented to connect the websocket on a background thread. 23 | func connect() 24 | 25 | /// Disconnects from the websocket server. 26 | func disconnect(forceTimeout: TimeInterval?) 27 | 28 | /// Writes ping data to the websocket. 29 | func write(ping: Data, completion: (() -> Void)?) 30 | 31 | /// Writes a string to the websocket. 32 | func write(string: String) 33 | 34 | } 35 | 36 | /// The delegate for a `WebSocketClient` to recieve notification of socket events. 37 | public protocol WebSocketClientDelegate: AnyObject { 38 | 39 | /// The websocket client has started a connection to the server. 40 | /// - Parameter socket: The `WebSocketClient` that sent the delegate event. 41 | func websocketDidConnect(socket: any WebSocketClient) 42 | 43 | /// The websocket client has disconnected from the server. 44 | /// - Parameters: 45 | /// - socket: The `WebSocketClient` that sent the delegate event. 46 | /// - error: An optional error if an error occured. 47 | func websocketDidDisconnect(socket: any WebSocketClient, error: (any Error)?) 48 | 49 | /// The websocket client received message text from the server 50 | /// - Parameters: 51 | /// - socket: The `WebSocketClient` that sent the delegate event. 52 | /// - text: The text received from the server. 53 | func websocketDidReceiveMessage(socket: any WebSocketClient, text: String) 54 | 55 | /// The websocket client received data from the server 56 | /// - Parameters: 57 | /// - socket: The `WebSocketClient` that sent the delegate event. 58 | /// - data: The data received from the server. 59 | func websocketDidReceiveData(socket: any WebSocketClient, data: Data) 60 | } 61 | -------------------------------------------------------------------------------- /Sources/ApolloWebSocket/WebSocketError.swift: -------------------------------------------------------------------------------- 1 | #if !COCOAPODS 2 | import ApolloAPI 3 | #endif 4 | import Foundation 5 | 6 | /// A structure for capturing problems and any associated errors from a `WebSocketTransport`. 7 | public struct WebSocketError: Error, LocalizedError { 8 | public enum ErrorKind { 9 | case errorResponse 10 | case networkError 11 | case unprocessedMessage(String) 12 | case serializedMessageError 13 | case neitherErrorNorPayloadReceived 14 | case upgradeError(code: Int) 15 | 16 | var description: String { 17 | switch self { 18 | case .errorResponse: 19 | return "Received error response" 20 | case .networkError: 21 | return "Websocket network error" 22 | case .unprocessedMessage(let message): 23 | return "Websocket error: Unprocessed message \(message)" 24 | case .serializedMessageError: 25 | return "Websocket error: Serialized message not found" 26 | case .neitherErrorNorPayloadReceived: 27 | return "Websocket error: Did not receive an error or a payload." 28 | case .upgradeError: 29 | return "Websocket error: Invalid HTTP upgrade." 30 | } 31 | } 32 | } 33 | 34 | /// The payload of the response. 35 | public let payload: JSONObject? 36 | 37 | /// The underlying error, or nil if one was not returned 38 | public let error: (any Error)? 39 | 40 | /// The kind of problem which occurred. 41 | public let kind: ErrorKind 42 | 43 | public var errorDescription: String? { 44 | return "\(self.kind.description). Error: \(String(describing: self.error))" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/ApolloWebSocket/WebSocketTask.swift: -------------------------------------------------------------------------------- 1 | #if !COCOAPODS 2 | import Apollo 3 | import ApolloAPI 4 | #endif 5 | import Foundation 6 | 7 | /// A task to wrap sending/canceling operations over a websocket. 8 | final class WebSocketTask: Cancellable { 9 | let sequenceNumber : String? 10 | let transport: WebSocketTransport 11 | 12 | /// Designated initializer 13 | /// 14 | /// - Parameter ws: The `WebSocketTransport` to use for this task 15 | /// - Parameter operation: The `GraphQLOperation` to use 16 | /// - Parameter completionHandler: A completion handler to fire when the operation has a result. 17 | init(_ ws: WebSocketTransport, 18 | _ operation: Operation, 19 | _ completionHandler: @escaping (_ result: Result) -> Void) { 20 | sequenceNumber = ws.sendHelper(operation: operation, resultHandler: completionHandler) 21 | transport = ws 22 | } 23 | 24 | public func cancel() { 25 | if let sequenceNumber = sequenceNumber { 26 | transport.unsubscribe(sequenceNumber) 27 | } 28 | } 29 | 30 | // Unsubscribes from further results from this task. 31 | public func unsubscribe() { 32 | cancel() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Tests/README.md: -------------------------------------------------------------------------------- 1 | All tests for the Apollo iOS code are located in the [Apollo iOS Dev](https://github.com/apollographql/apollo-ios-dev/tree/main/Tests) repo. -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | default: unpack-cli 2 | 3 | unpack-cli: 4 | (cd CLI && tar -xvf apollo-ios-cli.tar.gz -C ../) 5 | chmod +x apollo-ios-cli 6 | -------------------------------------------------------------------------------- /scripts/cli-version-check.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | directory=$(dirname "$0") 4 | projectDir="$directory/../CLI" 5 | 6 | APOLLO_VERSION=$(sh "$directory/get-version.sh") 7 | FILE_PATH="$projectDir/apollo-ios-cli.tar.gz" 8 | tar -xf "$FILE_PATH" 9 | CLI_VERSION=$(./apollo-ios-cli --version) 10 | 11 | echo "Comparing Apollo version $APOLLO_VERSION with CLI version $CLI_VERSION" 12 | 13 | if [ "$APOLLO_VERSION" = "$CLI_VERSION" ]; then 14 | echo "Success - matched!" 15 | else 16 | echo "Failed - mismatch!" 17 | exit 1 18 | fi 19 | -------------------------------------------------------------------------------- /scripts/download-cli.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script is intended for use only with the "InstallCLI" SPM plugin provided by Apollo iOS 4 | 5 | directory=$(dirname "$0") 6 | projectDir="$1" 7 | 8 | if [ -z "$projectDir" ]; 9 | then 10 | echo "Missing project directory path." >&2 11 | exit 1 12 | fi 13 | 14 | APOLLO_VERSION=$(sh "$directory/get-version.sh") 15 | DOWNLOAD_URL="https://www.github.com/apollographql/apollo-ios/releases/download/$APOLLO_VERSION/apollo-ios-cli.tar.gz" 16 | FILE_PATH="$projectDir/apollo-ios-cli.tar.gz" 17 | curl -L "$DOWNLOAD_URL" -s -o "$FILE_PATH" 18 | tar -xvf "$FILE_PATH" 19 | rm -f "$FILE_PATH" 20 | -------------------------------------------------------------------------------- /scripts/get-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | directory=$(dirname "$0") 4 | source "$directory/version-constants.sh" 5 | 6 | constantsFile=$(cat "$directory/../$APOLLO_CONSTANTS_FILE") 7 | currentVersion=$(echo $constantsFile | sed 's/^.*ApolloClientVersion: String = "\([^"]*\).*/\1/') 8 | echo $currentVersion 9 | -------------------------------------------------------------------------------- /scripts/version-constants.sh: -------------------------------------------------------------------------------- 1 | APOLLO_CONSTANTS_FILE="Sources/Apollo/Constants.swift" 2 | --------------------------------------------------------------------------------