├── .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 |
--------------------------------------------------------------------------------