├── .codeclimate.yml ├── .gitattributes ├── .github └── workflows │ ├── rebase.yml │ ├── release.yml │ ├── support-new-apollo.yml │ └── test.yml ├── .gitignore ├── .swiftlint.yml ├── ApolloDeveloperKit.podspec ├── ApolloDeveloperKit.schema.json ├── ApolloDeveloperKit.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── ApolloDeveloperKit.xcscheme ├── Cartfile ├── Cartfile.private ├── Cartfile.resolved ├── Example ├── API.swift ├── PostListViewController.graphql ├── PostTableViewCell.graphql ├── README.md ├── iOS │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ ├── Info.plist │ ├── Main.storyboard │ ├── PostListViewController.swift │ └── PostTableViewCell.swift ├── macOS │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Base.lproj │ │ └── Main.storyboard │ ├── Info.plist │ └── PostListViewController.swift └── schema.json ├── Gemfile ├── Gemfile.lock ├── InstallTests ├── ApolloDeveloperKitInstallTests.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Cartfile ├── Makefile ├── Podfile └── Sources ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources └── ApolloDeveloperKit │ ├── ApolloDebugServer.swift │ ├── ApolloDeveloperKit.h │ ├── Assets │ ├── bundle.js │ ├── favicon.png │ ├── index.html │ └── style.css │ ├── Background │ └── BackgroundTask.swift │ ├── Console │ ├── ConsoleDidWriteNotification.swift │ ├── ConsoleRedirection.swift │ ├── DarwinFileDescriptorDuplicator.swift │ └── FileDescriptorDuplicator.swift │ ├── DebuggableNetworkTransport.swift │ ├── DebuggableNormalizedCache.swift │ ├── DebuggableRequestChainNetworkTransport.swift │ ├── EventStreamMessage │ └── EventStreamMessage.swift │ ├── GraphQL │ ├── AnyGraphQLOperation.swift │ └── AnyGraphQLSelectionSet.swift │ ├── Info.plist │ ├── JSON │ ├── ErrorLike+Error.swift │ ├── GraphQLResult+JSONEncodable.swift │ ├── JSONCocoaTypeConversions.swift │ ├── Record+JSONEncodable.swift │ └── Reference+JSONEncodable.swift │ ├── Logger │ └── Logger.swift │ ├── Network │ └── Interceptor │ │ ├── DebugInitializeInterceptor.swift │ │ ├── DebuggableInterceptorProvider.swift │ │ └── DebuggableResultTranslateInterceptor.swift │ ├── Schema │ ├── Schema+EventStreamMessageConvertible.swift │ ├── Schema+JSONDecodable.swift │ ├── Schema+JSONEncodable.swift │ └── Schema.swift │ ├── Store │ ├── InMemoryOperationStore.swift │ ├── KeyedStack.swift │ ├── OperationStore.swift │ └── OperationStoreController.swift │ └── WebServer │ ├── AddressInfoError.swift │ ├── HTTPChunkedResponse.swift │ ├── HTTPConnection.swift │ ├── HTTPOutputStream.swift │ ├── HTTPOutputStreamSet.swift │ ├── HTTPRequestContext.swift │ ├── HTTPRequestMessage.swift │ ├── HTTPResponseMessage.swift │ ├── HTTPServer.swift │ ├── HTTPServerError.swift │ ├── InterfaceAddress.swift │ ├── InterfaceAddressIterator.swift │ ├── MIMEType.swift │ └── Socket.swift ├── Tests └── ApolloDeveloperKitTests │ ├── ApolloDebugServerLoadTests.swift │ ├── ApolloDebugServerTests.swift │ ├── ApolloDeveloperKitTests.swift │ ├── Background │ └── BackgroundTaskTests.swift │ ├── Console │ ├── ConsoleDidWriteNotificationTests.swift │ └── ConsoleRedirectionTests.swift │ ├── DebuggableNormalizedCacheTests.swift │ ├── DebuggableRequestChainNetworkTransportTests.swift │ ├── EventStreamMessage │ └── EventStreamMessageTests.swift │ ├── GraphQL │ ├── AnyGraphQLOperationTests.swift │ └── AnyGraphQLSelectionSetTests.swift │ ├── Info.plist │ ├── JSON │ ├── Record+JSONEncodableTests.swift │ ├── Reference+JSONEncodableTests.swift │ ├── Schema+JSONDecodableTests.swift │ └── Schema+JSONEncodableTests.swift │ ├── Network │ └── Interceptor │ │ ├── DebugInitializeInterceptorTests.swift │ │ ├── DebuggableInterceptorProviderTests.swift │ │ └── DebuggableResultTranslateInterceptorTests.swift │ ├── Store │ ├── InMemoryOperationStoreTests.swift │ ├── KeyedStackTests.swift │ └── OperationStoreControllerTests.swift │ ├── TestHelpers │ ├── GraphQLResult+AnyGraphQLOperation.swift │ ├── HTTPRequest+AnyGraphQLOperation.swift │ ├── HTTPResponse+AnyGraphQLOperation.swift │ ├── MockFileDescriptorDuplicator.swift │ ├── MockGraphQLOperations.swift │ ├── MockNetworkTransport.swift │ ├── URLSessionConfiguration+Test.swift │ ├── ifaddrs+Factory.swift │ └── sockaddr+Factory.swift │ └── WebServer │ ├── AddressInfoErrorTests.swift │ ├── HTTPChunkedResponseTests.swift │ ├── HTTPServerErrorTests.swift │ ├── HTTPServerTests.swift │ ├── InterfaceAddressIteratorTests.swift │ ├── InterfaceAddressTests.swift │ ├── MIMETypeTests.swift │ └── SocketTests.swift ├── bin ├── bump-version └── support-new-apollo ├── docs ├── Classes.html ├── Classes │ ├── ApolloDebugServer.html │ ├── DebugInitializeInterceptor.html │ ├── DebuggableInterceptorProvider.html │ ├── DebuggableNetworkTransport.html │ ├── DebuggableNormalizedCache.html │ ├── DebuggableRequestChainNetworkTransport.html │ ├── DebuggableResultTranslateInterceptor.html │ └── DebuggableResultTranslateInterceptor │ │ └── DebuggableResultTranslateError.html ├── Enums.html ├── Enums │ └── HTTPServerError.html ├── Extensions.html ├── Extensions │ ├── CFArray.html │ ├── CFBoolean.html │ ├── CFDictionary.html │ ├── CFNull.html │ ├── CFNumber.html │ ├── CFString.html │ ├── GraphQLResult.html │ ├── NSArray.html │ ├── NSDictionary.html │ ├── NSNull.html │ ├── NSNumber.html │ ├── NSString.html │ ├── Record.html │ └── Reference.html ├── Protocols.html ├── Protocols │ ├── DebuggableNetworkTransport.html │ └── DebuggableNetworkTransportDelegate.html ├── badge.svg ├── css │ ├── highlight.css │ └── jazzy.css ├── docsets │ ├── ApolloDeveloperKit.docset │ │ └── Contents │ │ │ ├── Info.plist │ │ │ └── Resources │ │ │ ├── Documents │ │ │ ├── Classes.html │ │ │ ├── Classes │ │ │ │ ├── ApolloDebugServer.html │ │ │ │ ├── DebugInitializeInterceptor.html │ │ │ │ ├── DebuggableInterceptorProvider.html │ │ │ │ ├── DebuggableNetworkTransport.html │ │ │ │ ├── DebuggableNormalizedCache.html │ │ │ │ ├── DebuggableRequestChainNetworkTransport.html │ │ │ │ ├── DebuggableResultTranslateInterceptor.html │ │ │ │ └── DebuggableResultTranslateInterceptor │ │ │ │ │ └── DebuggableResultTranslateError.html │ │ │ ├── Enums.html │ │ │ ├── Enums │ │ │ │ └── HTTPServerError.html │ │ │ ├── Extensions.html │ │ │ ├── Extensions │ │ │ │ ├── CFArray.html │ │ │ │ ├── CFBoolean.html │ │ │ │ ├── CFDictionary.html │ │ │ │ ├── CFNull.html │ │ │ │ ├── CFNumber.html │ │ │ │ ├── CFString.html │ │ │ │ ├── GraphQLResult.html │ │ │ │ ├── NSArray.html │ │ │ │ ├── NSDictionary.html │ │ │ │ ├── NSNull.html │ │ │ │ ├── NSNumber.html │ │ │ │ ├── NSString.html │ │ │ │ ├── Record.html │ │ │ │ └── Reference.html │ │ │ ├── Protocols.html │ │ │ ├── Protocols │ │ │ │ ├── DebuggableNetworkTransport.html │ │ │ │ └── DebuggableNetworkTransportDelegate.html │ │ │ ├── badge.svg │ │ │ ├── css │ │ │ │ ├── highlight.css │ │ │ │ └── jazzy.css │ │ │ ├── img │ │ │ │ ├── carat.png │ │ │ │ ├── dash.png │ │ │ │ ├── gh.png │ │ │ │ └── spinner.gif │ │ │ ├── index.html │ │ │ ├── js │ │ │ │ ├── jazzy.js │ │ │ │ ├── jazzy.search.js │ │ │ │ ├── jquery.min.js │ │ │ │ ├── lunr.min.js │ │ │ │ └── typeahead.jquery.js │ │ │ ├── search.json │ │ │ └── undocumented.json │ │ │ └── docSet.dsidx │ └── ApolloDeveloperKit.tgz ├── img │ ├── carat.png │ ├── dash.png │ ├── gh.png │ └── spinner.gif ├── index.html ├── js │ ├── jazzy.js │ ├── jazzy.search.js │ ├── jquery.min.js │ ├── lunr.min.js │ └── typeahead.jquery.js ├── search.json └── undocumented.json ├── package-lock.json ├── package.json ├── src ├── ApolloCachePretender.ts ├── ApolloClientPretender.ts ├── __tests__ │ ├── ApolloCachePretender.test.ts │ ├── ApolloClientPretender.test.ts │ └── IntegrationTests.test.ts ├── index.ts ├── schema.ts └── types │ ├── apollo-client-devtools │ ├── index.d.ts │ └── src │ │ ├── backend │ │ ├── broadcastQueries.d.ts │ │ ├── hook.d.ts │ │ ├── index.d.ts │ │ ├── links.d.ts │ │ └── typeDefs.d.ts │ │ └── bridge.d.ts │ └── eventsourcemock │ └── index.d.ts ├── tsconfig.json └── webpack.config.js /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | plugins: 3 | swiftlint: 4 | enabled: true 5 | exclude_patterns: 6 | - .build/ 7 | - Carthage/ 8 | - Example/ 9 | - Tests/ 10 | - docs/ 11 | - node_modules/ 12 | - vendor/ 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | Example/API.swift -diff linguist-generated 2 | Sources/ApolloDeveloperKit/Assets/bundle.js -diff linguist-generated 3 | Sources/ApolloDeveloperKit/Schema/Schema.swift -diff linguist-generated 4 | docs/** -diff linguist-documentation 5 | src/schema.ts -diff linguist-generated 6 | -------------------------------------------------------------------------------- /.github/workflows/rebase.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Automatic Rebase 3 | on: 4 | issue_comment: 5 | types: 6 | - created 7 | jobs: 8 | rebase: 9 | name: Rebase 10 | if: github.event.issue.pull_request != '' && contains(github.event.comment.body, 11 | '/rebase') && github.event.comment.author_association == 'OWNER' 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout the latest code 15 | uses: actions/checkout@v2 16 | with: 17 | token: "${{ secrets.PERSONAL_ACCESS_TOKEN }}" 18 | fetch-depth: 0 19 | - name: Automatic Rebase 20 | uses: cirrus-actions/rebase@1.4 21 | env: 22 | GITHUB_TOKEN: "${{ secrets.PERSONAL_ACCESS_TOKEN }}" 23 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Release 3 | on: 4 | release: 5 | types: 6 | - published 7 | jobs: 8 | publish-to-cocoapods: 9 | runs-on: macOS-10.15 10 | env: 11 | BUNDLE_JOBS: 4 12 | BUNDLE_RETRY: 3 13 | BUNDLE_WITHOUT: documentation:test 14 | COCOAPODS_TRUNK_TOKEN: "${{ secrets.COCOAPODS_TRUNK_TOKEN }}" 15 | DEVELOPER_DIR: "/Applications/Xcode_12.app/Contents/Developer" 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Install dependencies 19 | run: bundle install 20 | - name: Set up CocoaPods repository 21 | run: bundle exec pod setup 22 | - name: Publish to CocoaPods 23 | run: bundle exec pod trunk push --swift-version=5.3 --verbose ApolloDeveloperKit.podspec 24 | -------------------------------------------------------------------------------- /.github/workflows/support-new-apollo.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Support New Apollo 3 | on: 4 | repository_dispatch: 5 | types: 6 | - support-new-apollo 7 | jobs: 8 | support-new-apollo: 9 | if: endsWith(github.event.client_payload.url, '.0') 10 | runs-on: ubuntu-18.04 11 | steps: 12 | - name: Export version 13 | run: echo "::set-env name=APOLLO_VERSION::$(basename ${{ github.event.client_payload.url }})" 14 | - uses: actions/checkout@v2 15 | - name: Make changes 16 | run: ./bin/support-new-apollo "$APOLLO_VERSION" 17 | - name: Create pull request 18 | uses: peter-evans/create-pull-request@v3 19 | with: 20 | token: "${{ secrets.PERSONAL_ACCESS_TOKEN }}" 21 | commit-message: Support Apollo ${{ env.APOLLO_VERSION }} 22 | branch: support-apollo-${{ env.APOLLO_VERSION }} 23 | title: Support Apollo ${{ env.APOLLO_VERSION }} 24 | body: "${{ github.event.client_payload.url }}" 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | # Package.resolved 41 | # 42 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 43 | # hence it is not needed unless you have added a package configuration file to your project 44 | .swiftpm 45 | 46 | .build/ 47 | 48 | # Carthage 49 | # 50 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 51 | Carthage/Checkouts 52 | 53 | Carthage/Build 54 | 55 | /node_modules 56 | 57 | /.bundle 58 | /vendor/bundle 59 | 60 | InstallTests/ApolloDeveloperKitInstallTests.xcworkspace 61 | InstallTests/Cartfile.resolved 62 | InstallTests/Carthage 63 | InstallTests/Podfile.lock 64 | InstallTests/Pods 65 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - force_cast 3 | - force_try 4 | - identifier_name 5 | - line_length 6 | 7 | excluded: 8 | - .build/ 9 | - Carthage/ 10 | - Example/ 11 | - InstallTests/ 12 | - Tests/ 13 | - build/ 14 | - docs/ 15 | - node_modules/ 16 | - vendor/ 17 | -------------------------------------------------------------------------------- /ApolloDeveloperKit.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = "ApolloDeveloperKit" 3 | spec.version = "0.15.0" 4 | spec.summary = "Visual debugger for Apollo iOS GraphQL client" 5 | spec.description = <<-DESC 6 | ApolloDeveloperKit is an iOS library which works as a bridge between Apollo iOS client and Apollo Client Developer tools. 7 | This library adds an ability to watch the sent queries or mutations simultaneously, and also has the feature to request arbitrary operations from embedded GraphiQL console. 8 | DESC 9 | spec.homepage = "https://github.com/manicmaniac/ApolloDeveloperKit" 10 | spec.screenshots = "https://user-images.githubusercontent.com/1672393/92017937-6fcc7d00-ed8f-11ea-8611-baf3aef386cf.png" 11 | spec.documentation_url = "https://manicmaniac.github.io/ApolloDeveloperKit" 12 | spec.license = { :type => "MIT", :file => "LICENSE" } 13 | spec.authors = { "Ryosuke Ito" => "rito.0305@gmail.com" } 14 | spec.ios.deployment_target = '9.0' 15 | spec.osx.deployment_target = '10.10' 16 | spec.source = { :git => "https://github.com/manicmaniac/ApolloDeveloperKit.git", :tag => "#{spec.version}" } 17 | spec.source_files = "Sources/ApolloDeveloperKit/**/*.swift" 18 | spec.resource = "Sources/ApolloDeveloperKit/Assets" 19 | spec.cocoapods_version = '>= 1.7.0' 20 | spec.swift_versions = ['5.0', '5.1', '5.2', '5.3'] 21 | spec.dependency "Apollo", ">= 0.34.0", "< 0.38.0" 22 | end 23 | -------------------------------------------------------------------------------- /ApolloDeveloperKit.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2019-09/schema", 3 | "type": "object", 4 | "properties": { 5 | "operation": { 6 | "$id": "#operation", 7 | "title": "Operation", 8 | "description": "GraphQL operation request passed from client to server.", 9 | "type": "object", 10 | "properties": { 11 | "query": { 12 | "type": "string" 13 | }, 14 | "operationIdentifier": { 15 | "type": "string" 16 | }, 17 | "operationName": { 18 | "type": "string" 19 | }, 20 | "variables": { 21 | "$ref": "#/definitions/variables" 22 | } 23 | }, 24 | "required": [ 25 | "query" 26 | ], 27 | "additionalProperties": false 28 | }, 29 | "stateChange": { 30 | "$id": "#stateChange", 31 | "title": "StateChange", 32 | "description": "State change event pushed from server to client.", 33 | "type": "object", 34 | "properties": { 35 | "state": { 36 | "type": "object", 37 | "properties": { 38 | "queries": { 39 | "type": "array", 40 | "items": { 41 | "title": "Query", 42 | "type": "object", 43 | "properties": { 44 | "document": { 45 | "type": "string" 46 | }, 47 | "variables": { 48 | "$ref": "#/definitions/variables" 49 | }, 50 | "previousVariables": { 51 | "$ref": "#/definitions/variables" 52 | }, 53 | "networkError": { 54 | "$ref": "#/definitions/error" 55 | }, 56 | "graphQLErrors": { 57 | "type": "array", 58 | "items": { 59 | "$ref": "#/definitions/error" 60 | } 61 | } 62 | }, 63 | "required": [ 64 | "document" 65 | ], 66 | "additionalProperties": false 67 | } 68 | }, 69 | "mutations": { 70 | "type": "array", 71 | "items": { 72 | "title": "Mutation", 73 | "type": "object", 74 | "properties": { 75 | "mutation": { 76 | "type": "string" 77 | }, 78 | "variables": { 79 | "$ref": "#/definitions/variables" 80 | }, 81 | "loading": { 82 | "type": "boolean" 83 | }, 84 | "error": { 85 | "$ref": "#/definitions/error" 86 | } 87 | }, 88 | "required": [ 89 | "mutation", 90 | "loading" 91 | ], 92 | "additionalProperties": false 93 | } 94 | } 95 | }, 96 | "required": [ 97 | "queries", 98 | "mutations" 99 | ], 100 | "additionalProperties": false 101 | }, 102 | "dataWithOptimisticResults": { 103 | "title": "DataWithOptimisticResults", 104 | "type": "object" 105 | } 106 | }, 107 | "required": [ 108 | "state", 109 | "dataWithOptimisticResults" 110 | ], 111 | "additionalProperties": false 112 | }, 113 | "consoleEvent": { 114 | "$id": "#consoleEvent", 115 | "title": "ConsoleEvent", 116 | "description": "Console event pushed from client to server.", 117 | "type": "object", 118 | "properties": { 119 | "type": { 120 | "title": "ConsoleEventType", 121 | "type": "string", 122 | "enum": [ 123 | "stdout", 124 | "stderr" 125 | ] 126 | }, 127 | "data": { 128 | "type": "string" 129 | } 130 | }, 131 | "required": [ 132 | "type", 133 | "data" 134 | ], 135 | "additionalProperties": false 136 | } 137 | }, 138 | "additionalProperties": false, 139 | "definitions": { 140 | "error": { 141 | "title": "ErrorLike", 142 | "description": "JavaScript error serialized to JSON.", 143 | "type": "object", 144 | "properties": { 145 | "name": { 146 | "type": "string" 147 | }, 148 | "message": { 149 | "type": "string" 150 | }, 151 | "fileName": { 152 | "type": "string" 153 | }, 154 | "lineNumber": { 155 | "type": "integer" 156 | }, 157 | "columnNumber": { 158 | "type": "integer" 159 | } 160 | }, 161 | "required": [ 162 | "name", 163 | "message" 164 | ], 165 | "additionalProperties": false 166 | }, 167 | "variables": { 168 | "title": "Variables", 169 | "type": "object", 170 | "additionalProperties": true 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /ApolloDeveloperKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ApolloDeveloperKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ApolloDeveloperKit.xcodeproj/xcshareddata/xcschemes/ApolloDeveloperKit.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 33 | 34 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 55 | 61 | 62 | 63 | 64 | 65 | 75 | 76 | 82 | 83 | 84 | 85 | 89 | 90 | 91 | 92 | 98 | 99 | 105 | 106 | 107 | 108 | 110 | 111 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /Cartfile: -------------------------------------------------------------------------------- 1 | github "apollographql/apollo-ios" ~> 0.34.0 2 | -------------------------------------------------------------------------------- /Cartfile.private: -------------------------------------------------------------------------------- 1 | github "daltoniam/Starscream" "3.1.1" 2 | github "stephencelis/SQLite.swift" "0.12.2" 3 | -------------------------------------------------------------------------------- /Cartfile.resolved: -------------------------------------------------------------------------------- 1 | github "apollographql/apollo-ios" "0.34.1" 2 | github "daltoniam/Starscream" "3.1.1" 3 | github "stephencelis/SQLite.swift" "0.12.2" 4 | -------------------------------------------------------------------------------- /Example/PostListViewController.graphql: -------------------------------------------------------------------------------- 1 | query AllPosts { 2 | posts { 3 | ...PostDetails 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /Example/PostTableViewCell.graphql: -------------------------------------------------------------------------------- 1 | fragment PostDetails on Post { 2 | id 3 | title 4 | votes 5 | author { 6 | firstName 7 | lastName 8 | } 9 | } 10 | 11 | mutation UpvotePost($postId: Int!) { 12 | upvotePost(postId: $postId) { 13 | id 14 | votes 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Example/README.md: -------------------------------------------------------------------------------- 1 | Example 2 | ======= 3 | 4 | macOS and iOS example apps for `ApolloDeveloperKit`. 5 | 6 | Prerequisites 7 | ------------- 8 | 9 | - Xcode (`>= 11.2.1`) 10 | - Carthage 11 | 12 | Installation 13 | ------------ 14 | 15 | 1. Install dependency via Carthage 16 | - You may want to modify `Cartfile.resolved` to use the preferred version of `Apollo` beforehand. 17 | - Run `carthage bootstrap --platform iOS` or `carthage bootstrap --platform macOS` 18 | 2. Open the root `ApolloDeveloperKit.xcodeproj`. 19 | 3. Ensure Xcode scheme to be set to `ApolloDeveloperKitExample-macOS` or `ApolloDeveloperKitExample-iOS`. 20 | 4. Run build 21 | 22 | Notes 23 | ----- 24 | 25 | ### Updating Schema 26 | 27 | To update schema, run the following command. 28 | 29 | ``` 30 | ../Carthage/Checkouts/apollo-ios/scripts/run-bundled-codegen.sh schema:download --endpoint=http://localhost:8080/graphql 31 | ../Carthage/Checkouts/apollo-ios/scripts/run-bundled-codegen.sh codegen:generate --includes=*.graphql --target=swift --localSchemaFile=schema.json API.swift 32 | ``` 33 | -------------------------------------------------------------------------------- /Example/iOS/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Apollo 3 | #if DEBUG 4 | import ApolloDeveloperKit 5 | #endif 6 | 7 | @UIApplicationMain 8 | class AppDelegate: UIResponder, UIApplicationDelegate { 9 | var window: UIWindow? 10 | private var apollo: ApolloClient! 11 | #if DEBUG 12 | private var server: ApolloDebugServer! 13 | #endif 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { 16 | // Change localhost to your machine's local IP address when running from a device 17 | let url = URL(string: "http://localhost:8080/graphql")! 18 | #if DEBUG 19 | let cache = DebuggableNormalizedCache(cache: InMemoryNormalizedCache()) 20 | let store = ApolloStore(cache: cache) 21 | let interceptorProvider = LegacyInterceptorProvider(store: store) 22 | let networkTransport = DebuggableRequestChainNetworkTransport(interceptorProvider: interceptorProvider, endpointURL: url) 23 | server = ApolloDebugServer(networkTransport: networkTransport, cache: cache) 24 | server.enableConsoleRedirection = true 25 | try! server.start(port: 8081) 26 | #else 27 | let cache = InMemoryNormalizedCache() 28 | let store = ApolloStore(cache: cache) 29 | let interceptorProvider = LegacyInterceptorProvider() 30 | let networkTransport = RequestChainNetworkTransport(interceptorProvider: interceptorProvider, endpointURL: url) 31 | #endif 32 | apollo = ApolloClient(networkTransport: networkTransport, store: store) 33 | apollo.cacheKeyForObject = { $0["id"] } 34 | let navigationController = window!.rootViewController as! UINavigationController 35 | let postListViewController = navigationController.topViewController as! PostListViewController 36 | postListViewController.apollo = apollo 37 | #if DEBUG 38 | postListViewController.serverURL = server.serverURL 39 | #endif 40 | postListViewController.delegate = self 41 | return true 42 | } 43 | } 44 | 45 | extension AppDelegate: PostListViewControllerDelegate { 46 | func postListViewControllerWantsToToggleConsoleRedirection(_ postListViewController: PostListViewController) { 47 | #if DEBUG 48 | server.enableConsoleRedirection = !server.enableConsoleRedirection 49 | #endif 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Example/iOS/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Example/iOS/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/iOS/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Example/iOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 0.15.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | NSAppTransportSecurity 24 | 25 | NSExceptionDomains 26 | 27 | localhost 28 | 29 | NSTemporaryExceptionAllowsInsecureHTTPLoads 30 | 31 | 32 | 33 | 34 | UIBackgroundModes 35 | 36 | processing 37 | 38 | UILaunchStoryboardName 39 | LaunchScreen 40 | UIMainStoryboardFile 41 | Main 42 | UIRequiredDeviceCapabilities 43 | 44 | armv7 45 | 46 | UISupportedInterfaceOrientations 47 | 48 | UIInterfaceOrientationPortrait 49 | UIInterfaceOrientationLandscapeLeft 50 | UIInterfaceOrientationLandscapeRight 51 | 52 | UISupportedInterfaceOrientations~ipad 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationPortraitUpsideDown 56 | UIInterfaceOrientationLandscapeLeft 57 | UIInterfaceOrientationLandscapeRight 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /Example/iOS/PostListViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Apollo 3 | 4 | protocol PostListViewControllerDelegate: class { 5 | func postListViewControllerWantsToToggleConsoleRedirection(_ postListViewController: PostListViewController) 6 | } 7 | 8 | class PostListViewController: UITableViewController { 9 | var apollo: ApolloClient! 10 | var serverURL: URL? 11 | weak var delegate: PostListViewControllerDelegate? 12 | 13 | var posts: [AllPostsQuery.Data.Post?]? { 14 | didSet { 15 | tableView.reloadData() 16 | } 17 | } 18 | 19 | // MARK: - View lifecycle 20 | 21 | override func viewDidLoad() { 22 | super.viewDidLoad() 23 | 24 | tableView.rowHeight = UITableView.automaticDimension 25 | tableView.estimatedRowHeight = 64 26 | let refreshControl = UIRefreshControl() 27 | refreshControl.addTarget(self, action: #selector(refreshControlDidChangeValue(_:)), for: .valueChanged) 28 | self.refreshControl = refreshControl 29 | } 30 | 31 | override func viewWillAppear(_ animated: Bool) { 32 | super.viewWillAppear(animated) 33 | 34 | loadData(completion: nil) 35 | } 36 | 37 | // MARK: - Data loading 38 | 39 | var watcher: GraphQLQueryWatcher? 40 | 41 | func loadData(completion: (() -> Void)?) { 42 | watcher = apollo.watch(query: AllPostsQuery()) { result in 43 | switch result { 44 | case .success(let response): 45 | self.posts = response.data?.posts 46 | case .failure(let error): 47 | NSLog("Error while fetching query: \(error.localizedDescription)") 48 | } 49 | completion?() 50 | } 51 | } 52 | 53 | // MARK: - UITableViewDataSource 54 | 55 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 56 | return posts?.count ?? 0 57 | } 58 | 59 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 60 | guard let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as? PostTableViewCell else { 61 | fatalError("Could not dequeue PostTableViewCell") 62 | } 63 | 64 | guard let post = posts?[indexPath.row] else { 65 | fatalError("Could not find post at row \(indexPath.row)") 66 | } 67 | 68 | cell.configure(with: post.fragments.postDetails) 69 | cell.delegate = self 70 | 71 | return cell 72 | } 73 | 74 | @IBAction private func actionButtonDidTouchUpInside(_ sender: UIBarButtonItem) { 75 | let alertController = UIAlertController(title: "IP address", message: serverURL?.absoluteString ?? "(unknown)", preferredStyle: .actionSheet) 76 | alertController.addAction(UIAlertAction(title: "Toggle console redirection", style: .default) { action in 77 | self.delegate?.postListViewControllerWantsToToggleConsoleRedirection(self) 78 | }) 79 | alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) 80 | present(alertController, animated: true, completion: nil) 81 | } 82 | 83 | @objc private func refreshControlDidChangeValue(_ sender: UIRefreshControl) { 84 | loadData { 85 | sender.endRefreshing() 86 | } 87 | } 88 | } 89 | 90 | extension PostListViewController: PostTableViewCellDelegate { 91 | func postTableViewCell(_ postTableViewCell: PostTableViewCell, didPerformUpvote postId: Int) { 92 | apollo.perform(mutation: UpvotePostMutation(postId: postId)) { result in 93 | if case .failure(let error) = result { 94 | NSLog("Error while attempting to upvote post: \(error.localizedDescription)") 95 | } 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Example/iOS/PostTableViewCell.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Apollo 3 | 4 | protocol PostTableViewCellDelegate: class { 5 | func postTableViewCell(_ postTableViewCell: PostTableViewCell, didPerformUpvote postId: Int) 6 | } 7 | 8 | class PostTableViewCell: UITableViewCell { 9 | var postId: Int? 10 | weak var delegate: PostTableViewCellDelegate? 11 | 12 | @IBOutlet weak var titleLabel: UILabel! 13 | @IBOutlet weak var bylineLabel: UILabel! 14 | @IBOutlet weak var votesLabel: UILabel! 15 | 16 | func configure(with post: PostDetails) { 17 | postId = post.id 18 | 19 | titleLabel?.text = post.title 20 | bylineLabel?.text = byline(for: post) 21 | votesLabel?.text = "\(post.votes ?? 0) votes" 22 | } 23 | 24 | @IBAction func upvote() { 25 | guard let postId = postId else { return } 26 | delegate?.postTableViewCell(self, didPerformUpvote: postId) 27 | print("Upvoted the post \(postId)") 28 | } 29 | } 30 | 31 | // We can define helper methods that take the generated data types as arguments 32 | 33 | func byline(for post: PostDetails) -> String? { 34 | if let author = post.author { 35 | return "by \(author.fullName)" 36 | } else { 37 | return nil 38 | } 39 | } 40 | 41 | // We can also extend the generated data types to add convenience properties and methods 42 | 43 | extension PostDetails.Author { 44 | var fullName: String { 45 | return [firstName, lastName].compactMap { $0 }.joined(separator: " ") 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Example/macOS/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // ApolloDeveloperKitExample-macOS 4 | // 5 | // Created by Ryosuke Ito on 11/14/19. 6 | // Copyright © 2019 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import Apollo 11 | #if DEBUG 12 | import ApolloDeveloperKit 13 | #endif 14 | 15 | @NSApplicationMain 16 | class AppDelegate: NSObject, NSApplicationDelegate { 17 | private var apollo: ApolloClient! 18 | #if DEBUG 19 | private var server: ApolloDebugServer! 20 | #endif 21 | 22 | func applicationDidFinishLaunching(_ aNotification: Notification) { 23 | let url = URL(string: "http://localhost:8080/graphql")! 24 | #if DEBUG 25 | let cache = DebuggableNormalizedCache(cache: InMemoryNormalizedCache()) 26 | let store = ApolloStore(cache: cache) 27 | let interceptorProvider = LegacyInterceptorProvider(store: store) 28 | let networkTransport = DebuggableRequestChainNetworkTransport(interceptorProvider: interceptorProvider, endpointURL: url) 29 | server = ApolloDebugServer(networkTransport: networkTransport, cache: cache) 30 | server.enableConsoleRedirection = true 31 | try! server.start(port: 8081) 32 | #else 33 | let cache = InMemoryNormalizedCache() 34 | let store = ApolloStore(cache: cache) 35 | let interceptorProvider = LegacyInterceptorProvider() 36 | let networkTransport = RequestChainNetworkTransport(interceptorProvider: interceptorProvider, endpointURL: url) 37 | #endif 38 | apollo = ApolloClient(networkTransport: networkTransport, store: store) 39 | apollo.cacheKeyForObject = { $0["id"] } 40 | let postListViewController = NSApplication.shared.windows.first!.contentViewController as! PostListViewController 41 | postListViewController.apollo = apollo 42 | #if DEBUG 43 | postListViewController.serverURL = server.serverURL 44 | #endif 45 | postListViewController.delegate = self 46 | postListViewController.loadData(completion: nil) 47 | } 48 | } 49 | 50 | extension AppDelegate: PostListViewControllerDelegate { 51 | func postListViewControllerWantsToToggleConsoleRedirection(_ postListViewController: PostListViewController) { 52 | #if DEBUG 53 | server.enableConsoleRedirection = !server.enableConsoleRedirection 54 | #endif 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Example/macOS/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "size" : "16x16", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "size" : "16x16", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "size" : "32x32", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "size" : "32x32", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "size" : "128x128", 26 | "scale" : "1x" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "size" : "128x128", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "size" : "256x256", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "size" : "256x256", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "size" : "512x512", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "size" : "512x512", 51 | "scale" : "2x" 52 | } 53 | ], 54 | "info" : { 55 | "version" : 1, 56 | "author" : "xcode" 57 | } 58 | } -------------------------------------------------------------------------------- /Example/macOS/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/macOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSAppTransportSecurity 6 | 7 | NSExceptionDomains 8 | 9 | localhost 10 | 11 | NSTemporaryExceptionAllowsInsecureHTTPLoads 12 | 13 | 14 | 15 | 16 | CFBundleDevelopmentRegion 17 | $(DEVELOPMENT_LANGUAGE) 18 | CFBundleExecutable 19 | $(EXECUTABLE_NAME) 20 | CFBundleIconFile 21 | 22 | CFBundleIdentifier 23 | $(PRODUCT_BUNDLE_IDENTIFIER) 24 | CFBundleInfoDictionaryVersion 25 | 6.0 26 | CFBundleName 27 | $(PRODUCT_NAME) 28 | CFBundlePackageType 29 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 30 | CFBundleShortVersionString 31 | 0.15.0 32 | CFBundleVersion 33 | 1 34 | LSMinimumSystemVersion 35 | $(MACOSX_DEPLOYMENT_TARGET) 36 | NSHumanReadableCopyright 37 | Copyright © 2019 Ryosuke Ito. All rights reserved. 38 | NSMainStoryboardFile 39 | Main 40 | NSPrincipalClass 41 | NSApplication 42 | NSSupportsAutomaticTermination 43 | 44 | NSSupportsSuddenTermination 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /Example/macOS/PostListViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PostListViewController.swift 3 | // ApolloDeveloperKitExample-macOS 4 | // 5 | // Created by Ryosuke Ito on 11/14/19. 6 | // Copyright © 2019 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import Apollo 11 | 12 | protocol PostListViewControllerDelegate: class { 13 | func postListViewControllerWantsToToggleConsoleRedirection(_ postListViewController: PostListViewController) 14 | } 15 | 16 | class PostListViewController: NSViewController { 17 | var apollo: ApolloClient! 18 | var serverURL: URL! 19 | weak var delegate: PostListViewControllerDelegate? 20 | @IBOutlet private weak var tableView: NSTableView! 21 | 22 | var posts: [AllPostsQuery.Data.Post?]? { 23 | didSet { 24 | tableView.reloadData() 25 | } 26 | } 27 | 28 | // MARK: - View lifecycle 29 | 30 | override func viewDidLoad() { 31 | super.viewDidLoad() 32 | } 33 | 34 | // MARK: - Data loading 35 | 36 | var watcher: GraphQLQueryWatcher? 37 | 38 | func loadData(completion: (() -> Void)?) { 39 | watcher = apollo.watch(query: AllPostsQuery()) { result in 40 | switch result { 41 | case .success(let response): 42 | self.posts = response.data?.posts 43 | case .failure(let error): 44 | NSLog("error while fetching query: \(error.localizedDescription)") 45 | } 46 | completion?() 47 | } 48 | } 49 | 50 | @IBAction func upvote(_ sender: NSButton) { 51 | let tableCellView = sender.superview as! NSTableCellView 52 | let row = tableView.row(for: tableCellView) 53 | guard let postId = posts?[row]?.fragments.postDetails.id else { return } 54 | apollo.perform(mutation: UpvotePostMutation(postId: postId)) { result in 55 | if case .failure(let error) = result { 56 | NSLog("Error while attempting to upvote post: \(error.localizedDescription)") 57 | } 58 | } 59 | print("Upvoted the post \(postId)") 60 | } 61 | } 62 | 63 | // MARK: - NSTableViewDataSource 64 | 65 | extension PostListViewController: NSTableViewDataSource { 66 | func numberOfRows(in tableView: NSTableView) -> Int { 67 | return posts?.count ?? 0 68 | } 69 | } 70 | 71 | // MARK: - NSTableViewDelegate 72 | 73 | extension PostListViewController: NSTableViewDelegate { 74 | func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { 75 | guard let identifier = tableColumn?.identifier, let post = posts?[row]?.fragments.postDetails else { return nil } 76 | let view = tableView.makeView(withIdentifier: identifier, owner: self) as! NSTableCellView 77 | switch identifier.rawValue { 78 | case "title": 79 | view.textField?.stringValue = post.title ?? "" 80 | case "byline": 81 | view.textField?.stringValue = byline(for: post) ?? "" 82 | case "votes": 83 | view.textField?.stringValue = "\(post.votes ?? 0) votes" 84 | default: 85 | return nil 86 | } 87 | return view 88 | } 89 | } 90 | 91 | // We can define helper methods that take the generated data types as arguments 92 | 93 | func byline(for post: PostDetails) -> String? { 94 | if let author = post.author { 95 | return "by \(author.fullName)" 96 | } else { 97 | return nil 98 | } 99 | } 100 | 101 | // We can also extend the generated data types to add convenience properties and methods 102 | 103 | extension PostDetails.Author { 104 | var fullName: String { 105 | return [firstName, lastName].compactMap { $0 }.joined(separator: " ") 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | group :development do 6 | gem 'cocoapods' 7 | end 8 | 9 | group :documentation do 10 | gem 'jazzy' 11 | end 12 | 13 | group :test do 14 | gem 'slather' 15 | gem 'xcpretty' 16 | end 17 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.2) 5 | activesupport (4.2.11.3) 6 | i18n (~> 0.7) 7 | minitest (~> 5.1) 8 | thread_safe (~> 0.3, >= 0.3.4) 9 | tzinfo (~> 1.1) 10 | algoliasearch (1.27.3) 11 | httpclient (~> 2.8, >= 2.8.3) 12 | json (>= 1.5.1) 13 | atomos (0.1.3) 14 | claide (1.0.3) 15 | clamp (1.3.2) 16 | cocoapods (1.9.3) 17 | activesupport (>= 4.0.2, < 5) 18 | claide (>= 1.0.2, < 2.0) 19 | cocoapods-core (= 1.9.3) 20 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 21 | cocoapods-downloader (>= 1.2.2, < 2.0) 22 | cocoapods-plugins (>= 1.0.0, < 2.0) 23 | cocoapods-search (>= 1.0.0, < 2.0) 24 | cocoapods-stats (>= 1.0.0, < 2.0) 25 | cocoapods-trunk (>= 1.4.0, < 2.0) 26 | cocoapods-try (>= 1.1.0, < 2.0) 27 | colored2 (~> 3.1) 28 | escape (~> 0.0.4) 29 | fourflusher (>= 2.3.0, < 3.0) 30 | gh_inspector (~> 1.0) 31 | molinillo (~> 0.6.6) 32 | nap (~> 1.0) 33 | ruby-macho (~> 1.4) 34 | xcodeproj (>= 1.14.0, < 2.0) 35 | cocoapods-core (1.9.3) 36 | activesupport (>= 4.0.2, < 6) 37 | algoliasearch (~> 1.0) 38 | concurrent-ruby (~> 1.1) 39 | fuzzy_match (~> 2.0.4) 40 | nap (~> 1.0) 41 | netrc (~> 0.11) 42 | typhoeus (~> 1.0) 43 | cocoapods-deintegrate (1.0.4) 44 | cocoapods-downloader (1.4.0) 45 | cocoapods-plugins (1.0.0) 46 | nap 47 | cocoapods-search (1.0.0) 48 | cocoapods-stats (1.1.0) 49 | cocoapods-trunk (1.5.0) 50 | nap (>= 0.8, < 2.0) 51 | netrc (~> 0.11) 52 | cocoapods-try (1.2.0) 53 | colored2 (3.1.2) 54 | concurrent-ruby (1.1.7) 55 | escape (0.0.4) 56 | ethon (0.12.0) 57 | ffi (>= 1.3.0) 58 | ffi (1.13.1) 59 | fourflusher (2.3.1) 60 | fuzzy_match (2.0.4) 61 | gh_inspector (1.1.3) 62 | httpclient (2.8.3) 63 | i18n (0.9.5) 64 | concurrent-ruby (~> 1.0) 65 | jazzy (0.13.5) 66 | cocoapods (~> 1.5) 67 | mustache (~> 1.1) 68 | open4 69 | redcarpet (~> 3.4) 70 | rouge (>= 2.0.6, < 4.0) 71 | sassc (~> 2.1) 72 | sqlite3 (~> 1.3) 73 | xcinvoke (~> 0.3.0) 74 | json (2.3.1) 75 | liferaft (0.0.6) 76 | mini_portile2 (2.5.3) 77 | minitest (5.14.1) 78 | molinillo (0.6.6) 79 | mustache (1.1.1) 80 | nanaimo (0.3.0) 81 | nap (1.1.0) 82 | netrc (0.11.0) 83 | nokogiri (1.11.7) 84 | mini_portile2 (~> 2.5.0) 85 | racc (~> 1.4) 86 | open4 (1.3.4) 87 | racc (1.5.2) 88 | redcarpet (3.5.1) 89 | rouge (2.0.7) 90 | ruby-macho (1.4.0) 91 | sassc (2.4.0) 92 | ffi (~> 1.9) 93 | slather (2.7.1) 94 | CFPropertyList (>= 2.2, < 4) 95 | activesupport 96 | clamp (~> 1.3) 97 | nokogiri (~> 1.11) 98 | xcodeproj (~> 1.7) 99 | sqlite3 (1.4.2) 100 | thread_safe (0.3.6) 101 | typhoeus (1.4.0) 102 | ethon (>= 0.9.0) 103 | tzinfo (1.2.7) 104 | thread_safe (~> 0.1) 105 | xcinvoke (0.3.0) 106 | liferaft (~> 0.0.6) 107 | xcodeproj (1.18.0) 108 | CFPropertyList (>= 2.3.3, < 4.0) 109 | atomos (~> 0.1.3) 110 | claide (>= 1.0.2, < 2.0) 111 | colored2 (~> 3.1) 112 | nanaimo (~> 0.3.0) 113 | xcpretty (0.3.0) 114 | rouge (~> 2.0.7) 115 | 116 | PLATFORMS 117 | ruby 118 | 119 | DEPENDENCIES 120 | cocoapods 121 | jazzy 122 | slather 123 | xcpretty 124 | 125 | BUNDLED WITH 126 | 2.1.4 127 | -------------------------------------------------------------------------------- /InstallTests/ApolloDeveloperKitInstallTests.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /InstallTests/ApolloDeveloperKitInstallTests.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /InstallTests/Cartfile: -------------------------------------------------------------------------------- 1 | github "apollographql/apollo-ios" "0.34.1" 2 | git ".." "HEAD" 3 | -------------------------------------------------------------------------------- /InstallTests/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all clean carthage carthage-clean cocoapods cocoapods-clean 2 | 3 | all: carthage cocoapods 4 | 5 | clean: carthage-clean cocoapods-clean 6 | 7 | carthage: Cartfile.resolved 8 | cd Carthage/Checkouts/apollo-ios && swift package resolve # Workaround for Carthage's timeout error while reading xcodeproj. 9 | carthage build --platform iOS --no-use-binaries --cache-builds --use-xcframeworks 10 | set -o pipefail && xcodebuild -project ApolloDeveloperKitInstallTests.xcodeproj -scheme ApolloDeveloperKitInstallTests-iOS-Carthage -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 11' build | xcpretty 11 | 12 | carthage-clean: 13 | $(RM) -R Carthage Cartfile.resolved 14 | 15 | cocoapods: 16 | pod install 17 | set -o pipefail && xcodebuild -workspace ApolloDeveloperKitInstallTests.xcworkspace -scheme ApolloDeveloperKitInstallTests-iOS-CocoaPods -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 11' build | xcpretty 18 | 19 | cocoapods-clean: 20 | pod deintegrate 21 | 22 | Cartfile.resolved: 23 | carthage update --no-build 24 | -------------------------------------------------------------------------------- /InstallTests/Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, 10 2 | 3 | target 'ApolloDeveloperKitInstallTests-iOS-CocoaPods' do 4 | use_frameworks! 5 | 6 | pod 'Apollo', '0.34.1' 7 | pod 'ApolloDeveloperKit', path: '../', configurations: ['Debug'] 8 | end 9 | -------------------------------------------------------------------------------- /InstallTests/Sources: -------------------------------------------------------------------------------- 1 | ../Example -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Meteor Development Group, Inc. 4 | Copyright (c) 2019 Ryosuke Ito 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "Apollo", 6 | "repositoryURL": "https://github.com/apollographql/apollo-ios.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "c076bc75fdd92144db9d5e6496a0eb57c4bac175", 10 | "version": "0.34.1" 11 | } 12 | }, 13 | { 14 | "package": "PathKit", 15 | "repositoryURL": "https://github.com/kylef/PathKit.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "73f8e9dca9b7a3078cb79128217dc8f2e585a511", 19 | "version": "1.0.0" 20 | } 21 | }, 22 | { 23 | "package": "Spectre", 24 | "repositoryURL": "https://github.com/kylef/Spectre.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "f79d4ecbf8bc4e1579fbd86c3e1d652fb6876c53", 28 | "version": "0.9.2" 29 | } 30 | }, 31 | { 32 | "package": "SQLite.swift", 33 | "repositoryURL": "https://github.com/stephencelis/SQLite.swift.git", 34 | "state": { 35 | "branch": null, 36 | "revision": "0a9893ec030501a3956bee572d6b4fdd3ae158a1", 37 | "version": "0.12.2" 38 | } 39 | }, 40 | { 41 | "package": "Starscream", 42 | "repositoryURL": "https://github.com/daltoniam/Starscream", 43 | "state": { 44 | "branch": null, 45 | "revision": "e6b65c6d9077ea48b4a7bdda8994a1d3c6969c8d", 46 | "version": "3.1.1" 47 | } 48 | }, 49 | { 50 | "package": "Stencil", 51 | "repositoryURL": "https://github.com/stencilproject/Stencil.git", 52 | "state": { 53 | "branch": null, 54 | "revision": "973e190edf5d09274e4a6bc2e636c86899ed84c3", 55 | "version": "0.14.1" 56 | } 57 | }, 58 | { 59 | "package": "swift-nio-zlib-support", 60 | "repositoryURL": "https://github.com/apple/swift-nio-zlib-support.git", 61 | "state": { 62 | "branch": null, 63 | "revision": "37760e9a52030bb9011972c5213c3350fa9d41fd", 64 | "version": "1.0.0" 65 | } 66 | } 67 | ] 68 | }, 69 | "version": 1 70 | } 71 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "ApolloDeveloperKit", 7 | platforms: [.iOS(.v9), .macOS(.v10_10)], 8 | products: [ 9 | .library( 10 | name: "ApolloDeveloperKit", 11 | targets: ["ApolloDeveloperKit"]) 12 | ], 13 | dependencies: [ 14 | .package(name: "Apollo", url: "https://github.com/apollographql/apollo-ios.git", "0.34.0"..<"0.35.0") 15 | ], 16 | targets: [ 17 | .target( 18 | name: "ApolloDeveloperKit", 19 | dependencies: ["Apollo"], 20 | resources: [.copy("Assets")]), 21 | .testTarget( 22 | name: "ApolloDeveloperKitTests", 23 | dependencies: ["ApolloDeveloperKit"], 24 | exclude: ["ApolloDeveloperKitTests.swift"]) 25 | ] 26 | ) 27 | -------------------------------------------------------------------------------- /Sources/ApolloDeveloperKit/ApolloDeveloperKit.h: -------------------------------------------------------------------------------- 1 | // 2 | // ApolloDeveloperKit.h 3 | // ApolloDeveloperKit 4 | // 5 | // Created by Ryosuke Ito on 6/14/19. 6 | // Copyright © 2019 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | @import Foundation; 10 | 11 | FOUNDATION_EXPORT double ApolloDeveloperKitVersionNumber; 12 | FOUNDATION_EXPORT const unsigned char ApolloDeveloperKitVersionString[]; 13 | -------------------------------------------------------------------------------- /Sources/ApolloDeveloperKit/Assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manicmaniac/ApolloDeveloperKit/c1c1642035f2274064c682081d0d0c141295821d/Sources/ApolloDeveloperKit/Assets/favicon.png -------------------------------------------------------------------------------- /Sources/ApolloDeveloperKit/Assets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ApolloDeveloperKit 7 | 8 | 9 | 10 | 11 | 12 |
13 |

ApolloDebugServer is running!

14 |

Usage is available on manicmaniac/ApolloDeveloperKit.

15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /Sources/ApolloDeveloperKit/Assets/style.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | } 4 | 5 | body { 6 | background: linear-gradient(179deg, #2c2f39 2%, #363944 14%, #32353d 100%); 7 | color: white; 8 | margin: 0; 9 | } 10 | 11 | main { 12 | text-align: center; 13 | padding: 0 16px; 14 | } 15 | 16 | a { 17 | color: lightblue; 18 | text-decoration: none; 19 | } 20 | 21 | a:hover { 22 | text-decoration: underline; 23 | } 24 | 25 | a:visited { 26 | color: lightblue; 27 | } 28 | -------------------------------------------------------------------------------- /Sources/ApolloDeveloperKit/Background/BackgroundTask.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BackgroundTask.swift 3 | // ApolloDeveloperKit 4 | // 5 | // Created by Ryosuke Ito on 11/12/19. 6 | // Copyright © 2019 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | #if os(iOS) 10 | import UIKit 11 | 12 | protocol BackgroundTaskExecutor { 13 | func beginBackgroundTask(withName name: String?, expirationHandler handler: (() -> Void)?) -> UIBackgroundTaskIdentifier 14 | func endBackgroundTask(_ identifier: UIBackgroundTaskIdentifier) 15 | } 16 | 17 | extension UIApplication: BackgroundTaskExecutor { 18 | // Already conformed. 19 | } 20 | 21 | final class BackgroundTask { 22 | private(set) var currentIdentifier: UIBackgroundTaskIdentifier 23 | private let executor: BackgroundTaskExecutor 24 | 25 | init(executor: BackgroundTaskExecutor = UIApplication.shared) { 26 | self.executor = executor 27 | self.currentIdentifier = .invalid 28 | } 29 | 30 | func beginBackgroundTaskIfPossible() { 31 | precondition(Thread.isMainThread) 32 | guard currentIdentifier == .invalid else { return } 33 | currentIdentifier = executor.beginBackgroundTask(withName: "com.github.manicmaniac.ApolloDeveloperKit.BackgroundTask") { 34 | self.executor.endBackgroundTask(self.currentIdentifier) 35 | self.currentIdentifier = .invalid 36 | } 37 | } 38 | } 39 | #else 40 | final class BackgroundTask { 41 | func beginBackgroundTaskIfPossible() { 42 | } 43 | } 44 | #endif 45 | -------------------------------------------------------------------------------- /Sources/ApolloDeveloperKit/Console/ConsoleDidWriteNotification.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConsoleDidWriteNotification.swift 3 | // ApolloDeveloperKit 4 | // 5 | // Created by Ryosuke Ito on 8/18/20. 6 | // Copyright © 2020 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Notification.Name { 12 | // `userInfo` will be ["data": Data, "destination": ConsoleRedirection.Destination] 13 | static let consoleDidWrite = Notification.Name("ADKConsoleDidWriteNotification") 14 | } 15 | 16 | struct ConsoleDidWriteNotification: RawRepresentable { 17 | private static let dataKey = "data" 18 | private static let destinationKey = "destination" 19 | 20 | let rawValue: Notification 21 | 22 | init(object: ConsoleRedirection, data: Data, destination: ConsoleRedirection.Destination) { 23 | self.rawValue = Notification(name: .consoleDidWrite, object: object, userInfo: [ 24 | ConsoleDidWriteNotification.dataKey: data, 25 | ConsoleDidWriteNotification.destinationKey: destination 26 | ]) 27 | } 28 | 29 | init?(rawValue: Notification) { 30 | guard rawValue.name == Notification.Name.consoleDidWrite else { return nil } 31 | self.rawValue = rawValue 32 | } 33 | 34 | var object: ConsoleRedirection { 35 | return rawValue.object as! ConsoleRedirection 36 | } 37 | 38 | var data: Data { 39 | return rawValue.userInfo![ConsoleDidWriteNotification.dataKey] as! Data 40 | } 41 | 42 | var destination: ConsoleRedirection.Destination { 43 | return rawValue.userInfo![ConsoleDidWriteNotification.destinationKey] as! ConsoleRedirection.Destination 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/ApolloDeveloperKit/Console/DarwinFileDescriptorDuplicator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DarwinFileDescriptorDuplicator.swift 3 | // ApolloDeveloperKit 4 | // 5 | // Created by Ryosuke Ito on 11/5/19. 6 | // Copyright © 2019 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import Darwin 10 | 11 | struct DarwinFileDescriptorDuplicator: FileDescriptorDuplicator { 12 | func dup(_ fildes: Int32) -> Int32 { 13 | return Darwin.dup(fildes) 14 | } 15 | 16 | func dup2(_ fildes: Int32, _ fildes2: Int32) -> Int32 { 17 | return Darwin.dup2(fildes, fildes2) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/ApolloDeveloperKit/Console/FileDescriptorDuplicator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileDescriptorDuplicator.swift 3 | // ApolloDeveloperKit 4 | // 5 | // Created by Ryosuke Ito on 11/5/19. 6 | // Copyright © 2019 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | /** 10 | * `FileDescriptorDuplicator` abstracts the interface of `dup` and `dup2`. 11 | * 12 | * This protocol is mainly for testing purposes. 13 | */ 14 | protocol FileDescriptorDuplicator { 15 | func dup(_ fildes: Int32) -> Int32 16 | @discardableResult func dup2(_ fildes: Int32, _ fildes2: Int32) -> Int32 17 | } 18 | -------------------------------------------------------------------------------- /Sources/ApolloDeveloperKit/DebuggableNetworkTransport.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DebuggableNetworkTransport.swift 3 | // ApolloDeveloperKit 4 | // 5 | // Created by Ryosuke Ito on 6/15/19. 6 | // Copyright © 2019 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import Apollo 10 | 11 | public protocol DebuggableNetworkTransportDelegate: class { 12 | func networkTransport(_ networkTransport: NetworkTransport, willSendOperation operation: Operation) 13 | func networkTransport(_ networkTransport: NetworkTransport, didSendOperation operation: Operation, result: Result, Error>) 14 | } 15 | 16 | /** 17 | * `DebuggableNetworkTransport` is a bridge between `ApolloDebugServer` and `ApolloClient`. 18 | * 19 | * You should instantiate both `ApolloDebugServer` and `ApolloClient` with the same instance of this class. 20 | */ 21 | public protocol DebuggableNetworkTransport: NetworkTransport { 22 | var delegate: DebuggableNetworkTransportDelegate? { get set } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/ApolloDeveloperKit/DebuggableNormalizedCache.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DebuggableNormalizedCache.swift 3 | // ApolloDeveloperKit 4 | // 5 | // Created by Ryosuke Ito on 6/15/19. 6 | // Copyright © 2019 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import Apollo 10 | import Foundation 11 | 12 | protocol DebuggableNormalizedCacheDelegate: class { 13 | func normalizedCache(_ normalizedCache: DebuggableNormalizedCache, didChangeRecords records: RecordSet) 14 | } 15 | 16 | /** 17 | * `DebuggableNormalizedCache` is a bridge between `ApolloDebugServer` and `ApolloStore`. 18 | * 19 | * You should instantiate both `ApolloDebugServer` and `ApolloStore` with the same instance of this class. 20 | */ 21 | public class DebuggableNormalizedCache { 22 | weak var delegate: DebuggableNormalizedCacheDelegate? 23 | private let cache: NormalizedCache 24 | private var cachedRecords: RecordSet 25 | private let recordLock = NSRecursiveLock() 26 | 27 | /** 28 | * Initializes the receiver with the underlying cache object. 29 | * 30 | * - Parameter cache: The underlying cache. 31 | */ 32 | public init(cache: NormalizedCache) { 33 | self.cache = cache 34 | self.cachedRecords = RecordSet() 35 | } 36 | 37 | func extract() -> [String: Any] { 38 | return cachedRecords.storage 39 | } 40 | 41 | private func notifyRecordChange() { 42 | delegate?.normalizedCache(self, didChangeRecords: self.cachedRecords) 43 | } 44 | } 45 | 46 | // MARK: NormalizedCache 47 | 48 | extension DebuggableNormalizedCache: NormalizedCache { 49 | public func loadRecords(forKeys keys: [CacheKey], callbackQueue: DispatchQueue?, completion: @escaping (Result<[Record?], Error>) -> Void) { 50 | cache.loadRecords(forKeys: keys, callbackQueue: callbackQueue, completion: completion) 51 | } 52 | 53 | public func merge(records: RecordSet, callbackQueue: DispatchQueue?, completion: @escaping (Result, Error>) -> Void) { 54 | recordLock.lock() 55 | cachedRecords.merge(records: records) 56 | notifyRecordChange() 57 | recordLock.unlock() 58 | cache.merge(records: records, callbackQueue: callbackQueue, completion: completion) 59 | } 60 | 61 | public func clear(callbackQueue: DispatchQueue?, completion: ((Result) -> Void)?) { 62 | recordLock.lock() 63 | cachedRecords.clear() 64 | notifyRecordChange() 65 | recordLock.unlock() 66 | cache.clear(callbackQueue: callbackQueue, completion: completion) 67 | } 68 | 69 | public func clearImmediately() throws { 70 | recordLock.lock() 71 | cachedRecords.clear() 72 | notifyRecordChange() 73 | recordLock.unlock() 74 | try cache.clearImmediately() 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Sources/ApolloDeveloperKit/DebuggableRequestChainNetworkTransport.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DebuggableRequestChainNetworkTransport.swift 3 | // ApolloDeveloperKit 4 | // 5 | // Created by Ryosuke Ito on 5/30/21. 6 | // Copyright © 2021 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import Apollo 10 | import Foundation 11 | 12 | open class DebuggableRequestChainNetworkTransport: RequestChainNetworkTransport, DebuggableNetworkTransport { 13 | public weak var delegate: DebuggableNetworkTransportDelegate? 14 | private let debuggableInterceptorProvider: DebuggableInterceptorProvider 15 | 16 | public override init(interceptorProvider: InterceptorProvider, 17 | endpointURL: URL, 18 | additionalHeaders: [String: String] = [:], 19 | autoPersistQueries: Bool = false, 20 | requestBodyCreator: RequestBodyCreator = ApolloRequestBodyCreator(), 21 | useGETForQueries: Bool = false, 22 | useGETForPersistedQueryRetry: Bool = false) { 23 | debuggableInterceptorProvider = DebuggableInterceptorProvider(interceptorProvider) 24 | super.init(interceptorProvider: debuggableInterceptorProvider, 25 | endpointURL: endpointURL, 26 | additionalHeaders: additionalHeaders, 27 | autoPersistQueries: autoPersistQueries, 28 | requestBodyCreator: requestBodyCreator, 29 | useGETForQueries: useGETForQueries, 30 | useGETForPersistedQueryRetry: useGETForPersistedQueryRetry) 31 | debuggableInterceptorProvider.delegate = self 32 | } 33 | } 34 | 35 | extension DebuggableRequestChainNetworkTransport: DebuggableInterceptorProviderDelegate { 36 | func interceptorProvider(_ interceptorProvider: InterceptorProvider, willSendOperation operation: Operation) where Operation: GraphQLOperation { 37 | delegate?.networkTransport(self, willSendOperation: operation) 38 | } 39 | 40 | func interceptorProvider(_ interceptorProvider: InterceptorProvider, didSendOperation operation: Operation, result: Result, Error>) where Operation: GraphQLOperation { 41 | delegate?.networkTransport(self, didSendOperation: operation, result: result) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/ApolloDeveloperKit/EventStreamMessage/EventStreamMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EventStreamMessage.swift 3 | // ApolloDeveloperKit 4 | // 5 | // Created by Ryosuke Ito on 8/29/20. 6 | // Copyright © 2020 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol EventStreamMessageConvertible { 12 | var message: EventStreamMessage { get } 13 | } 14 | 15 | struct EventStreamMessage: RawRepresentable { 16 | static let ping = EventStreamMessage(rawValue: ":\n\n")! 17 | 18 | let rawValue: String 19 | 20 | init?(rawValue: String) { 21 | guard rawValue.hasSuffix("\n\n") else { return nil } 22 | self.rawValue = rawValue 23 | } 24 | 25 | init(event: String? = nil, data: String? = nil, id: String? = nil, retry: Int? = nil) { 26 | var value = "" 27 | if let event = event { 28 | value.append("event: \(event)\n") 29 | } 30 | if let data = data { 31 | value.append(data.split(separator: "\n").map { "data: \($0)\n" }.joined()) 32 | } 33 | if let id = id { 34 | value.append("id: \(id)\n") 35 | } 36 | if let retry = retry { 37 | value.append("retry: \(retry)\n") 38 | } 39 | value.append("\n") 40 | self.rawValue = value 41 | } 42 | 43 | var rawData: Data { 44 | return Data(rawValue.utf8) 45 | } 46 | } 47 | 48 | // MARK: Equatable 49 | 50 | extension EventStreamMessage: Equatable { 51 | static func == (lhs: EventStreamMessage, rhs: EventStreamMessage) -> Bool { 52 | return lhs.rawValue == rhs.rawValue 53 | } 54 | } 55 | 56 | // MARK: Hashable 57 | 58 | extension EventStreamMessage: Hashable { 59 | func hash(into hasher: inout Hasher) { 60 | hasher.combine(rawValue) 61 | } 62 | } 63 | 64 | // MARK: CustomStringConvertible 65 | 66 | extension EventStreamMessage: CustomStringConvertible { 67 | var description: String { 68 | return rawValue 69 | } 70 | } 71 | 72 | // MARK: LosslessStringConvertible 73 | 74 | extension EventStreamMessage: LosslessStringConvertible { 75 | init?(_ description: String) { 76 | self.init(rawValue: description) 77 | } 78 | } 79 | 80 | // MARK: EventStreamMessageConvertible 81 | 82 | extension EventStreamMessage: EventStreamMessageConvertible { 83 | var message: EventStreamMessage { 84 | return self 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Sources/ApolloDeveloperKit/GraphQL/AnyGraphQLOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnyGraphQLOperation.swift 3 | // ApolloDeveloperKit 4 | // 5 | // Created by Ryosuke Ito on 6/23/19. 6 | // Copyright © 2019 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import Apollo 10 | import Foundation 11 | 12 | /** 13 | * `AnyGraphQLOperation` is the class representing any kind of GraphQL operation including query, mutation and subscription. 14 | */ 15 | final class AnyGraphQLOperation: GraphQLOperation { 16 | typealias Data = AnyGraphQLSelectionSet 17 | 18 | /** 19 | * The type of an actual operation. 20 | * 21 | * Always be a GraphQLOperationType.query even if it isn't a query. 22 | */ 23 | let operationType: GraphQLOperationType 24 | 25 | /** 26 | * The query document of an operation. 27 | */ 28 | let operationDefinition: String 29 | 30 | /** 31 | * The identifier of an operation. 32 | */ 33 | let operationIdentifier: String? 34 | 35 | /** 36 | * The name of an operation. 37 | */ 38 | let operationName: String 39 | 40 | /** 41 | * The query variables of an operation. 42 | */ 43 | let variables: GraphQLMap? 44 | 45 | /** 46 | * Initializes a AnyGraphQLOperation object. 47 | * 48 | * Any kind of operation is assumed as GraphQLOperationType.query even if it isn't a query. 49 | * It doesn't cause a problem for now because it matters only when an operation is saved, 50 | * and ApolloDeveloperKit won't save any kind of operation given from devtool's GraphiQL. * 51 | * 52 | * - Parameter operation: Operation object defined in JSON schema 53 | */ 54 | convenience init(operation: Operation) { 55 | self.init(operationType: .query, 56 | operationDefinition: operation.query, 57 | operationIdentifier: operation.operationIdentifier, 58 | operationName: operation.operationName ?? "", 59 | variables: operation.variables as? GraphQLMap) 60 | } 61 | 62 | convenience init(_ operation: Operation) where Operation: GraphQLOperation { 63 | self.init(operationType: operation.operationType, 64 | operationDefinition: operation.operationDefinition, 65 | operationIdentifier: operation.operationIdentifier, 66 | operationName: operation.operationName, 67 | variables: operation.variables) 68 | } 69 | 70 | private init(operationType: GraphQLOperationType, operationDefinition: String, operationIdentifier: String?, operationName: String, variables: GraphQLMap?) { 71 | self.operationType = operationType 72 | self.operationDefinition = operationDefinition 73 | self.operationIdentifier = operationIdentifier 74 | self.operationName = operationName 75 | self.variables = variables 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Sources/ApolloDeveloperKit/GraphQL/AnyGraphQLSelectionSet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnyGraphQLSelectionSet.swift 3 | // ApolloDeveloperKit 4 | // 5 | // Created by Ryosuke Ito on 6/24/19. 6 | // Copyright © 2019 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import Apollo 10 | 11 | /** 12 | * A type erasure class for `GraphQLSelectionSet`. 13 | */ 14 | struct AnyGraphQLSelectionSet: GraphQLSelectionSet { 15 | static let selections = [GraphQLSelection]() 16 | 17 | let resultMap: ResultMap 18 | 19 | init(unsafeResultMap: ResultMap) { 20 | self.resultMap = unsafeResultMap 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/ApolloDeveloperKit/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 0.15.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /Sources/ApolloDeveloperKit/JSON/ErrorLike+Error.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ErrorLike+Error.swift 3 | // ApolloDeveloperKit 4 | // 5 | // Created by Ryosuke Ito on 8/22/20. 6 | // Copyright © 2020 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | extension ErrorLike { 10 | init(error: Error) { 11 | self.init(columnNumber: nil, 12 | fileName: nil, 13 | lineNumber: nil, 14 | message: error.localizedDescription, 15 | name: String(describing: type(of: error))) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/ApolloDeveloperKit/JSON/GraphQLResult+JSONEncodable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GraphQLResult+JSONEncodable.swift 3 | // ApolloDeveloperKit 4 | // 5 | // Created by Ryosuke Ito on 10/28/20. 6 | // Copyright © 2020 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import Apollo 10 | import Foundation 11 | 12 | extension GraphQLResult: GraphQLInputValue, JSONEncodable where Data: GraphQLSelectionSet { 13 | public var jsonValue: JSONValue { 14 | if let data = data { 15 | return data.jsonObject 16 | } 17 | if let errors = errors { 18 | return errors 19 | } 20 | return NSNull() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/ApolloDeveloperKit/JSON/JSONCocoaTypeConversions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSONCocoaTypeConversions.swift 3 | // ApolloDeveloperKit 4 | // 5 | // Created by Ryosuke Ito on 11/20/19. 6 | // Copyright © 2019 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import Apollo 10 | import Foundation 11 | 12 | /** 13 | * `Apollo` has its own utility extensions to convert Swift standard types to JSON object and uses it widely in the project. 14 | * `ApolloDeveloperKit` borrows this feature in order to convert an object to JSON-compliant object, like converting cache contents to JSON. 15 | * However, I found `Apollo` sometimes utilizes Cocoa types like NSString which is not covered by the above utility extensions, 16 | * and when I use those utility extensions on them, `fatalError()` occurs because it doesn't conform to `JSONEncodable`. 17 | * So to avoid this problem, `ApolloDeveloperKit` have to prepare some more extensions for Cocoa types. 18 | */ 19 | protocol ExtendedJSONEncodable: JSONEncodable {} 20 | 21 | extension NSString: ExtendedJSONEncodable { 22 | public var jsonValue: JSONValue { 23 | return (self as String).jsonValue 24 | } 25 | } 26 | 27 | extension NSNumber: ExtendedJSONEncodable { 28 | public var jsonValue: JSONValue { 29 | switch CFGetTypeID(self) { 30 | case CFBooleanGetTypeID(): 31 | return boolValue.jsonValue 32 | case CFNumberGetTypeID(): 33 | return CFNumberIsFloatType(self) ? doubleValue.jsonValue : intValue.jsonValue 34 | default: 35 | fatalError("The underlying type of value must be CFBoolean or CFNumber") 36 | } 37 | } 38 | } 39 | 40 | extension NSDictionary: ExtendedJSONEncodable { 41 | public var jsonValue: JSONValue { 42 | return (self as [NSObject: AnyObject]).jsonValue 43 | } 44 | } 45 | 46 | extension NSArray: ExtendedJSONEncodable { 47 | public var jsonValue: JSONValue { 48 | return (self as [AnyObject]).jsonValue 49 | } 50 | } 51 | 52 | extension NSNull: ExtendedJSONEncodable { 53 | public var jsonValue: JSONValue { 54 | return self 55 | } 56 | } 57 | 58 | extension CFString: ExtendedJSONEncodable { 59 | public var jsonValue: JSONValue { 60 | return (self as NSString).jsonValue 61 | } 62 | } 63 | 64 | extension CFNumber: ExtendedJSONEncodable { 65 | public var jsonValue: JSONValue { 66 | return (self as NSNumber).jsonValue 67 | } 68 | } 69 | 70 | extension CFBoolean: ExtendedJSONEncodable { 71 | public var jsonValue: JSONValue { 72 | return CFBooleanGetValue(self).jsonValue 73 | } 74 | } 75 | 76 | extension CFDictionary: ExtendedJSONEncodable { 77 | public var jsonValue: JSONValue { 78 | return (self as NSDictionary).jsonValue 79 | } 80 | } 81 | 82 | extension CFArray: ExtendedJSONEncodable { 83 | public var jsonValue: JSONValue { 84 | return (self as NSArray).jsonValue 85 | } 86 | } 87 | 88 | extension CFNull: ExtendedJSONEncodable { 89 | public var jsonValue: JSONValue { 90 | return self as NSNull 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Sources/ApolloDeveloperKit/JSON/Record+JSONEncodable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Record+JSONEncodable.swift 3 | // ApolloDeveloperKit 4 | // 5 | // Created by Ryosuke Ito on 6/15/19. 6 | // Copyright © 2019 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import Apollo 10 | 11 | extension Record: JSONEncodable { 12 | public var jsonValue: JSONValue { 13 | return fields.mapValues { value -> JSONValue in 14 | if let value = value as? JSONEncodable { 15 | return value.jsonValue 16 | } 17 | // As we cannot cast some kind of Objective-C types such as `NSCFString` to JSONEncodable, 18 | // assume it as naturally JSON-encodable object. 19 | return value 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/ApolloDeveloperKit/JSON/Reference+JSONEncodable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Reference+JSONEncodable.swift 3 | // ApolloDeveloperKit 4 | // 5 | // Created by Ryosuke Ito on 6/29/19. 6 | // Copyright © 2019 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import Apollo 10 | 11 | extension Reference: JSONEncodable { 12 | public var jsonValue: JSONValue { 13 | return [ 14 | "generated": true, 15 | "id": key, 16 | "type": "id" 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/ApolloDeveloperKit/Logger/Logger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Logger.swift 3 | // ApolloDeveloperKit 4 | // 5 | // Created by Ryosuke Ito on 6/9/21. 6 | // Copyright © 2021 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import asl 11 | import os.log 12 | 13 | final class Logger { 14 | static let apollo = Logger(category: "apollo") 15 | static let http = Logger(category: "http") 16 | 17 | private let logger: Any 18 | private var isSuppressed = false 19 | 20 | init?(category: String) { 21 | guard ProcessInfo().environment.keys.contains("APOLLO_DEVELOPER_KIT_DIAGNOSTICS") else { 22 | return nil 23 | } 24 | let subsystem = "com.github.manicmaniac.ApolloDeveloperKit" 25 | if #available(macOS 10.12, iOS 10, *) { 26 | logger = OSLog(subsystem: subsystem, category: category) 27 | } else { 28 | logger = asl_new(UInt32(ASL_TYPE_MSG))! 29 | } 30 | } 31 | 32 | static func withSuppressing(_ logger: Logger?, body: () throws -> Void) rethrows { 33 | try withExtendedLifetime(logger) { 34 | logger?.isSuppressed = true 35 | defer { logger?.isSuppressed = false } 36 | try body() 37 | } 38 | } 39 | 40 | func debug(_ message: @autoclosure () -> String) { 41 | log(level: .debug, message()) 42 | } 43 | 44 | func info(_ message: @autoclosure () -> String) { 45 | log(level: .info, message()) 46 | } 47 | 48 | func error(_ message: @autoclosure () -> String) { 49 | log(level: .error, message()) 50 | } 51 | 52 | private func log(level: LogLevel, _ message: String) { 53 | if isSuppressed { return } 54 | if #available(macOS 10.12, iOS 10, *) { 55 | os_log("%@", log: logger as! OSLog, type: level.osLogType, message) 56 | } else { 57 | asl_vlog((logger as! asl_object_t), nil, level.aslLogLevel, "%@", getVaList([message])) 58 | } 59 | } 60 | 61 | } 62 | 63 | private enum LogLevel { 64 | case debug 65 | case info 66 | case error 67 | 68 | @available(macOS 10.12, iOS 10, *) 69 | var osLogType: OSLogType { 70 | switch self { 71 | case .debug: 72 | return .debug 73 | case .info: 74 | return .info 75 | case .error: 76 | return .error 77 | } 78 | } 79 | 80 | var aslLogLevel: Int32 { 81 | switch self { 82 | case .debug: 83 | return ASL_LEVEL_DEBUG 84 | case .info: 85 | return ASL_LEVEL_INFO 86 | case .error: 87 | return ASL_LEVEL_ERR 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Sources/ApolloDeveloperKit/Network/Interceptor/DebugInitializeInterceptor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotifyStartInterceptor.swift 3 | // ApolloDeveloperKit 4 | // 5 | // Created by Ryosuke Ito on 5/30/21. 6 | // Copyright © 2021 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import Apollo 10 | import Foundation 11 | 12 | protocol DebugInitializeInterceptorDelegate: class { 13 | func interceptor(_ interceptor: ApolloInterceptor, willSendOperation operation: Operation) where Operation: GraphQLOperation 14 | func interceptor(_ interceptor: ApolloInterceptor, didSendOperation operation: Operation, result: Result, Error>) where Operation: GraphQLOperation 15 | } 16 | 17 | /** 18 | * `DebugInitializeInterceptor` is an interceptor that notifies that an operation begins. 19 | * 20 | * `DebugInitializeInterceptor` is intended to be put before all the rest interceptors. 21 | * 22 | * Typically you don't need to use this class directly but if you want to assemble your custom interceptor chain, you need to put this class at the right place. 23 | */ 24 | public class DebugInitializeInterceptor: ApolloInterceptor { 25 | weak var delegate: DebugInitializeInterceptorDelegate? 26 | 27 | public func interceptAsync(chain: RequestChain, request: HTTPRequest, response: HTTPResponse?, completion: @escaping (Result, Error>) -> Void) where Operation: GraphQLOperation { 28 | delegate?.interceptor(self, willSendOperation: request.operation) 29 | chain.proceedAsync(request: request, 30 | response: response) { [weak self] result in 31 | if let self = self { 32 | self.delegate?.interceptor(self, didSendOperation: request.operation, result: result) 33 | } 34 | completion(result) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/ApolloDeveloperKit/Network/Interceptor/DebuggableInterceptorProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DebuggableInterceptorProvider.swift 3 | // ApolloDeveloperKit 4 | // 5 | // Created by Ryosuke Ito on 5/30/21. 6 | // Copyright © 2021 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import Apollo 10 | import Foundation 11 | 12 | protocol DebuggableInterceptorProviderDelegate: class { 13 | func interceptorProvider(_ interceptorProvider: InterceptorProvider, willSendOperation operation: Operation) where Operation: GraphQLOperation 14 | func interceptorProvider(_ interceptorProvider: InterceptorProvider, didSendOperation operation: Operation, result: Result, Error>) where Operation: GraphQLOperation 15 | } 16 | 17 | /** 18 | * `DebuggableInterceptorProvider` wraps another interceptor and provides interceptors configured for `ApolloDeveloperKit`. 19 | * 20 | * `DebuggableInterceptorProvider` prepends `DebugInitializeInterceptor` at first, then appends `DebuggableResultTranslateInterceptor` at last only when the operation comes from `ApolloDeveloperKit`. 21 | */ 22 | public class DebuggableInterceptorProvider: InterceptorProvider { 23 | weak var delegate: DebuggableInterceptorProviderDelegate? 24 | private let interceptorProvider: InterceptorProvider 25 | private let debugInitializeInterceptor: DebugInitializeInterceptor 26 | private let debuggableResultTranslateInterceptor = DebuggableResultTranslateInterceptor() 27 | 28 | public init(_ interceptorProvider: InterceptorProvider) { 29 | self.interceptorProvider = interceptorProvider 30 | debugInitializeInterceptor = DebugInitializeInterceptor() 31 | debugInitializeInterceptor.delegate = self 32 | } 33 | 34 | public func interceptors(for operation: Operation) -> [ApolloInterceptor] where Operation: GraphQLOperation { 35 | var interceptors = interceptorProvider.interceptors(for: operation) 36 | interceptors.insert(debugInitializeInterceptor, at: 0) 37 | if operation is AnyGraphQLOperation { 38 | interceptors.append(debuggableResultTranslateInterceptor) 39 | } 40 | return interceptors 41 | } 42 | 43 | public func additionalErrorInterceptor(for operation: Operation) -> ApolloErrorInterceptor? { 44 | return interceptorProvider.additionalErrorInterceptor(for: operation) 45 | } 46 | } 47 | 48 | extension DebuggableInterceptorProvider: DebugInitializeInterceptorDelegate { 49 | func interceptor(_ interceptor: ApolloInterceptor, willSendOperation operation: Operation) where Operation: GraphQLOperation { 50 | delegate?.interceptorProvider(self, willSendOperation: operation) 51 | } 52 | 53 | func interceptor(_ interceptor: ApolloInterceptor, didSendOperation operation: Operation, result: Result, Error>) where Operation: GraphQLOperation { 54 | delegate?.interceptorProvider(self, didSendOperation: operation, result: result) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/ApolloDeveloperKit/Network/Interceptor/DebuggableResultTranslateInterceptor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DebuggableResultTranslateInterceptor.swift 3 | // ApolloDeveloperKit 4 | // 5 | // Created by Ryosuke Ito on 5/30/21. 6 | // Copyright © 2021 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import Apollo 10 | import Foundation 11 | 12 | /** 13 | * `DebuggableResultTranslateInterceptor` is an interceptor that translates legacy response to GraphQLResult. 14 | * 15 | * Since Apollo 0.34.0, Apollo parses returned raw GraphQL response along `GraphQLSelectionSet.selections` in `LegacyParsingInterceptor.interceptAsync(chain:request:response:completion:)` and store the result in `HTTPResponse.parsedResponse`. 16 | * This change makes it difficult to query an arbitrary operation because `GraphQLSelectionSet.selections` cannot change its return value at runtime. 17 | * 18 | * However, at least for the time being we can use `HTTPResponse.legacyResponse`, which doesn't check `GraphQLSelectionSet.selections` instead of `HTTPResponse.parsedResponse`. 19 | * 20 | * `DebuggableResultTranslateInterceptor` is intended to be put after `LegacyParsingInterceptor` and it substitutes `HTTPResponse.legacyResponse` for `HTTPResponse.parsedResponse`, only when the operation comes from `ApolloDeveloperKit`. 21 | * 22 | * Typically you don't need to use this class directly but if you want to assemble your custom interceptor chain, you need to put this class at the right place. 23 | */ 24 | public class DebuggableResultTranslateInterceptor: ApolloInterceptor { 25 | /** 26 | * `DebuggableResultTranslateError` is an error kind thrown by `DebuggableResultTranslateInterceptor`. 27 | */ 28 | public enum DebuggableResultTranslateError: Error, LocalizedError { 29 | /** 30 | * Indicates a logic error that is caused by putting `DebuggableResultTranslateInterceptor` before parsing a response. 31 | */ 32 | case noResponseToTranslate 33 | 34 | public var errorDescription: String? { 35 | switch self { 36 | case .noResponseToTranslate: 37 | return "The Debuggable Result Translate Interceptor was called before a response was received to be parsed. Double-check the order of your interceptors." 38 | } 39 | } 40 | } 41 | 42 | public func interceptAsync(chain: RequestChain, request: HTTPRequest, response: HTTPResponse?, completion: @escaping (Result, Error>) -> Void) where Operation: GraphQLOperation { 43 | guard request.operation is AnyGraphQLOperation else { 44 | return chain.proceedAsync(request: request, response: response, completion: completion) 45 | } 46 | guard let createdResponse = response, let parsedResponse = createdResponse.parsedResponse, let legacyResponse = createdResponse.legacyResponse else { 47 | return chain.handleErrorAsync(DebuggableResultTranslateError.noResponseToTranslate, 48 | request: request, 49 | response: response, 50 | completion: completion) 51 | } 52 | let data = AnyGraphQLSelectionSet(unsafeResultMap: legacyResponse.body) 53 | let result = GraphQLResult(data: data as? Operation.Data, 54 | extensions: parsedResponse.extensions, 55 | errors: parsedResponse.errors, 56 | source: parsedResponse.source, 57 | dependentKeys: nil) 58 | createdResponse.parsedResponse = result 59 | chain.proceedAsync(request: request, response: createdResponse, completion: completion) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/ApolloDeveloperKit/Schema/Schema+EventStreamMessageConvertible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Schema+EventStreamMessageConvertible.swift 3 | // ApolloDeveloperKit 4 | // 5 | // Created by Ryosuke Ito on 8/29/20. 6 | // Copyright © 2020 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension ConsoleEvent: EventStreamMessageConvertible { 12 | var message: EventStreamMessage { 13 | return EventStreamMessage(event: String(describing: type), data: data) 14 | } 15 | } 16 | 17 | extension StateChange: EventStreamMessageConvertible { 18 | var message: EventStreamMessage { 19 | let data = try! JSONSerialization.data(withJSONObject: jsonValue) 20 | return EventStreamMessage(data: String(data: data, encoding: .utf8)) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/ApolloDeveloperKit/Schema/Schema+JSONDecodable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Schema+JSONDecodable.swift 3 | // ApolloDeveloperKit 4 | // 5 | // Created by Ryosuke Ito on 8/22/20. 6 | // Copyright © 2020 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import Apollo 10 | 11 | extension Operation: JSONDecodable { 12 | init(jsonValue value: JSONValue) throws { 13 | guard let jsonObject = value as? JSONObject, let query = jsonObject["query"] as? String else { 14 | throw JSONDecodingError.couldNotConvert(value: value, to: Operation.self) 15 | } 16 | self.operationIdentifier = jsonObject["operationIdentifier"] as? String 17 | self.operationName = jsonObject["operationName"] as? String 18 | self.query = query 19 | self.variables = jsonObject["variables"] as? [String: Any] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/ApolloDeveloperKit/Schema/Schema+JSONEncodable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Schema+JSONEncodable.swift 3 | // ApolloDeveloperKit 4 | // 5 | // Created by Ryosuke Ito on 8/22/20. 6 | // Copyright © 2020 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import Apollo 10 | 11 | extension StateChange: JSONEncodable { 12 | var jsonValue: JSONValue { 13 | return [ 14 | "dataWithOptimisticResults": dataWithOptimisticResults.jsonValue, 15 | "state": state.jsonValue 16 | ] 17 | } 18 | } 19 | 20 | extension State: JSONEncodable { 21 | var jsonValue: JSONValue { 22 | return [ 23 | "mutations": mutations.jsonValue, 24 | "queries": queries.jsonValue 25 | ] 26 | } 27 | } 28 | 29 | extension Mutation: JSONEncodable { 30 | var jsonValue: JSONValue { 31 | return [ 32 | "error": error.jsonValue, 33 | "loading": loading.jsonValue, 34 | "mutation": mutation.jsonValue, 35 | "variables": variables.jsonValue 36 | ] 37 | } 38 | } 39 | 40 | extension ErrorLike: JSONEncodable { 41 | var jsonValue: JSONValue { 42 | return [ 43 | "columnNumber": columnNumber.jsonValue, 44 | "fileName": fileName.jsonValue, 45 | "lineNumber": lineNumber.jsonValue, 46 | "message": message.jsonValue, 47 | "name": name.jsonValue 48 | ] 49 | } 50 | } 51 | 52 | extension Query: JSONEncodable { 53 | var jsonValue: JSONValue { 54 | return [ 55 | "document": document.jsonValue, 56 | "graphQLErrors": graphQLErrors.jsonValue, 57 | "networkError": networkError.jsonValue, 58 | "previousVariables": previousVariables.jsonValue, 59 | "variables": variables.jsonValue 60 | ] 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/ApolloDeveloperKit/Schema/Schema.swift: -------------------------------------------------------------------------------- 1 | // This file was generated from JSON Schema using quicktype, do not modify it directly. 2 | // To parse the JSON, add this file to your project and do: 3 | // 4 | // let schema = try Schema(json) 5 | 6 | import Foundation 7 | 8 | // MARK: - Schema 9 | struct Schema { 10 | /// Console event pushed from client to server. 11 | let consoleEvent: ConsoleEvent? 12 | /// GraphQL operation request passed from client to server. 13 | let operation: Operation? 14 | /// State change event pushed from server to client. 15 | let stateChange: StateChange? 16 | } 17 | 18 | /// Console event pushed from client to server. 19 | // MARK: - ConsoleEvent 20 | struct ConsoleEvent { 21 | let data: String 22 | let type: ConsoleEventType 23 | } 24 | 25 | enum ConsoleEventType { 26 | case stderr 27 | case stdout 28 | } 29 | 30 | /// GraphQL operation request passed from client to server. 31 | // MARK: - Operation 32 | struct Operation { 33 | let operationIdentifier, operationName: String? 34 | let query: String 35 | let variables: [String: Any?]? 36 | } 37 | 38 | /// State change event pushed from server to client. 39 | // MARK: - StateChange 40 | struct StateChange { 41 | let dataWithOptimisticResults: [String: Any?] 42 | let state: State 43 | } 44 | 45 | // MARK: - State 46 | struct State { 47 | let mutations: [Mutation] 48 | let queries: [Query] 49 | } 50 | 51 | // MARK: - Mutation 52 | struct Mutation { 53 | let error: ErrorLike? 54 | let loading: Bool 55 | let mutation: String 56 | let variables: [String: Any?]? 57 | } 58 | 59 | /// JavaScript error serialized to JSON. 60 | // MARK: - ErrorLike 61 | struct ErrorLike { 62 | let columnNumber: Int? 63 | let fileName: String? 64 | let lineNumber: Int? 65 | let message, name: String 66 | } 67 | 68 | // MARK: - Query 69 | struct Query { 70 | let document: String 71 | let graphQLErrors: [ErrorLike]? 72 | let networkError: ErrorLike? 73 | let previousVariables, variables: [String: Any?]? 74 | } 75 | -------------------------------------------------------------------------------- /Sources/ApolloDeveloperKit/Store/InMemoryOperationStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InMemoryOperationStore.swift 3 | // ApolloDeveloperKit 4 | // 5 | // Created by Ryosuke Ito on 2/10/20. 6 | // Copyright © 2020 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import Apollo 10 | import Foundation 11 | 12 | /** 13 | * `InMemoryOperationStore` stores queries and mutations in memory. 14 | * 15 | * The current implementation stores queries and mutations respectively in an ordered dictionary. 16 | * Since Swift doesn't have a data structure like a mutable ordered dictionary, it is implemented with separate arrays, 17 | * one of which stores keys and the other stores values. 18 | * 19 | * The design is strongly inspired by `QueryStore` and `MutationStore` of `apollo-client`. 20 | * 21 | * - Warning: All operations are thread-unsafe. 22 | * - SeeAlso: 23 | * [queries.ts](https://github.com/apollographql/apollo-client/blob/v2.6.8/packages/apollo-client/src/data/queries.ts) 24 | * [mutations.ts](https://github.com/apollographql/apollo-client/blob/v2.6.8/packages/apollo-client/src/data/mutations.ts)] 25 | */ 26 | final class InMemoryOperationStore: OperationStore { 27 | private var queries = KeyedStack() 28 | private var mutations = KeyedStack() 29 | 30 | var state: State { 31 | let mutations = self.mutations.map { _, mutation in 32 | Mutation(error: mutation.networkError.flatMap(ErrorLike.init(error:)), 33 | loading: mutation.isLoading, 34 | mutation: mutation.queryDocument, 35 | variables: mutation.variables) 36 | } 37 | let queries = self.queries.map { _, query in 38 | Query(document: query.queryDocument, 39 | graphQLErrors: query.graphQLErrors?.map(ErrorLike.init(error:)), 40 | networkError: query.networkError.flatMap(ErrorLike.init(error:)), 41 | previousVariables: nil, 42 | variables: query.variables) 43 | } 44 | return State(mutations: mutations, queries: queries) 45 | } 46 | 47 | func add(_ operation: Operation) where Operation: GraphQLOperation { 48 | switch operation.operationType { 49 | case .query: 50 | queries.push(OperationStoreValue(operation), for: ObjectIdentifier(operation)) 51 | case .mutation: 52 | mutations.push(OperationStoreValue(operation), for: ObjectIdentifier(operation)) 53 | case .subscription: 54 | break 55 | } 56 | } 57 | 58 | func setFailure(for operation: Operation, networkError: Error) where Operation: GraphQLOperation { 59 | setState(.failure(networkError: networkError), for: operation) 60 | } 61 | 62 | func setSuccess(for operation: Operation, graphQLErrors: [Error]) where Operation: GraphQLOperation { 63 | setState(.success(graphQLErrors: graphQLErrors), for: operation) 64 | } 65 | 66 | private func setState(_ state: OperationState, for operation: Operation) where Operation: GraphQLOperation { 67 | switch operation.operationType { 68 | case .query: 69 | queries[ObjectIdentifier(operation)]?.state = state 70 | case .mutation: 71 | mutations[ObjectIdentifier(operation)]?.state = state 72 | case .subscription: 73 | break 74 | } 75 | } 76 | } 77 | 78 | private struct OperationStoreValue { 79 | let queryDocument: String 80 | let variables: GraphQLMap? 81 | var state = OperationState.loading 82 | 83 | init(_ operation: Operation) where Operation: GraphQLOperation { 84 | self.queryDocument = operation.queryDocument 85 | self.variables = operation.variables 86 | } 87 | 88 | var isLoading: Bool { 89 | if case .loading = state { 90 | return true 91 | } 92 | return false 93 | } 94 | 95 | var networkError: Error? { 96 | if case .failure(let networkError) = state { 97 | return networkError 98 | } 99 | return nil 100 | } 101 | 102 | var graphQLErrors: [Error]? { 103 | if case .success(let graphQLErrors) = state { 104 | return graphQLErrors 105 | } 106 | return nil 107 | } 108 | } 109 | 110 | private enum OperationState { 111 | case loading 112 | case failure(networkError: Error) 113 | case success(graphQLErrors: [Error]) 114 | } 115 | -------------------------------------------------------------------------------- /Sources/ApolloDeveloperKit/Store/KeyedStack.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyedStack.swift 3 | // ApolloDeveloperKit 4 | // 5 | // Created by Ryosuke Ito on 8/25/20. 6 | // Copyright © 2020 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | /** 10 | * `KeyedStack` is kind of a stack of `Value`s keyed by `Key`. 11 | * 12 | * - Warning: Iterating over `KeyedStack` yields key-value pairs from the *bottom* of stack. 13 | */ 14 | struct KeyedStack where Key: Equatable { 15 | private var elements = [(Key, Value)]() 16 | 17 | mutating func push(_ value: Value, for key: Key) { 18 | elements.append((key, value)) 19 | } 20 | 21 | /** 22 | * Access the value associated with the given key for reading and writing. 23 | * 24 | * Since `KeyedStack` is a stack and doesn't guarantee uniqueness of keys, 25 | * the return value is the *first* found `Value` from the top of stack. 26 | */ 27 | subscript(key: Key) -> Value? { 28 | get { 29 | return index(for: key).flatMap { elements[$0].1 } 30 | } 31 | set { 32 | switch (index(for: key), newValue) { 33 | case (let index?, let newValue?): 34 | elements[index] = (key, newValue) 35 | case (let index?, nil): 36 | elements.remove(at: index) 37 | case (nil, let newValue?): 38 | push(newValue, for: key) 39 | case (nil, nil): 40 | break 41 | } 42 | } 43 | } 44 | 45 | private func index(for key: Key) -> Int? { 46 | return elements.lastIndex { $0.0 == key } 47 | } 48 | } 49 | 50 | extension KeyedStack: Sequence { 51 | func makeIterator() -> AnyIterator<(Key, Value)> { 52 | return AnyIterator(elements.makeIterator()) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/ApolloDeveloperKit/Store/OperationStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OperationStore.swift 3 | // ApolloDeveloperKit 4 | // 5 | // Created by Ryosuke Ito on 2/10/20. 6 | // Copyright © 2020 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import Apollo 10 | 11 | /** 12 | * `OperationStore` represents a data store for GraphQL operations. 13 | * 14 | * The interface is inspired by `QueryManager` of `apollo-client`. 15 | * 16 | * - SeeAlso: 17 | * [QueryManager.ts](https://github.com/apollographql/apollo-client/blob/v2.6.8/packages/apollo-client/src/core/QueryManager.ts) 18 | */ 19 | protocol OperationStore: class { 20 | var state: State { get } 21 | func add(_ operation: Operation) where Operation: GraphQLOperation 22 | func setFailure(for operation: Operation, networkError: Error) where Operation: GraphQLOperation 23 | func setSuccess(for operation: Operation, graphQLErrors: [Error]) where Operation: GraphQLOperation 24 | } 25 | -------------------------------------------------------------------------------- /Sources/ApolloDeveloperKit/Store/OperationStoreController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OperationStoreController.swift 3 | // ApolloDeveloperKit 4 | // 5 | // Created by Ryosuke Ito on 2/10/20. 6 | // Copyright © 2020 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import Apollo 10 | import Dispatch 11 | 12 | /** 13 | * `OperationStoreController` is a controller class of `OperationStore`. 14 | * 15 | * It owns a `OperationStore` and manipulates the store in a thread-safe manner, delegating `DebuggableNetworkTransport`. 16 | */ 17 | final class OperationStoreController { 18 | /** 19 | * A queue where operations perform. 20 | * 21 | * This property is only visible for testing purpose. 22 | */ 23 | let queue = DispatchQueue(label: "com.github.manicmaniac.ApolloDeveloperKit.OperationStoreController") 24 | private(set) var store: OperationStore 25 | 26 | init(store: OperationStore) { 27 | self.store = store 28 | } 29 | } 30 | 31 | // MARK: DebuggableNetworkTransportDelegate 32 | 33 | extension OperationStoreController: DebuggableNetworkTransportDelegate { 34 | func networkTransport(_ networkTransport: NetworkTransport, willSendOperation operation: Operation) where Operation: GraphQLOperation { 35 | queue.async(flags: .barrier) { [weak self] in 36 | self?.store.add(operation) 37 | } 38 | } 39 | 40 | func networkTransport(_ networkTransport: NetworkTransport, didSendOperation operation: Operation, result: Result, Error>) where Operation: GraphQLOperation { 41 | queue.async(flags: .barrier) { [weak self] in 42 | switch result { 43 | case .success(let graphQLResult): 44 | self?.store.setSuccess(for: operation, graphQLErrors: graphQLResult.errors ?? []) 45 | case .failure(let error): 46 | self?.store.setFailure(for: operation, networkError: error) 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/ApolloDeveloperKit/WebServer/AddressInfoError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AddressInfoError.swift 3 | // ApolloDeveloperKit 4 | // 5 | // Created by Ryosuke Ito on 9/20/20. 6 | // Copyright © 2020 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import Darwin 10 | 11 | struct AddressInfoError: Error { 12 | typealias Code = AddressInfoErrorCode 13 | 14 | let code: AddressInfoErrorCode 15 | 16 | init(_ code: AddressInfoErrorCode) { 17 | self.code = code 18 | } 19 | 20 | var localizedDescription: String { 21 | return String(cString: gai_strerror(code.rawValue)) 22 | } 23 | } 24 | 25 | struct AddressInfoErrorCode: RawRepresentable { 26 | var rawValue: Int32 27 | 28 | init?(rawValue: Int32) { 29 | guard (1...weakObjects() 13 | 14 | func insert(_ stream: HTTPOutputStream) { 15 | hashTable.add(stream) 16 | } 17 | 18 | func broadcast(data: Data) { 19 | for stream in self { 20 | stream.write(data: data) 21 | } 22 | Logger.http?.debug("Broadcasted \(data) to \(hashTable.count) streams.") 23 | } 24 | 25 | func makeIterator() -> AnyIterator { 26 | return AnyIterator(hashTable.allObjects.map { $0 as! HTTPOutputStream }.makeIterator()) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/ApolloDeveloperKit/WebServer/HTTPRequestMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPRequestMessage.swift 3 | // ApolloDeveloperKit 4 | // 5 | // Created by Ryosuke Ito on 8/18/20. 6 | // Copyright © 2020 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | final class HTTPRequestMessage { 12 | private let message: CFHTTPMessage 13 | 14 | init() { 15 | self.message = CFHTTPMessageCreateEmpty(kCFAllocatorDefault, true).takeRetainedValue() 16 | } 17 | 18 | var body: Data? { 19 | return CFHTTPMessageCopyBody(message)?.takeRetainedValue() as Data? 20 | } 21 | 22 | var version: String { 23 | return CFHTTPMessageCopyVersion(message).takeRetainedValue() as String 24 | } 25 | 26 | var isHeaderComplete: Bool { 27 | return CFHTTPMessageIsHeaderComplete(message) 28 | } 29 | 30 | var requestURL: URL? { 31 | return CFHTTPMessageCopyRequestURL(message)?.takeRetainedValue() as URL? 32 | } 33 | 34 | var requestMethod: String? { 35 | return CFHTTPMessageCopyRequestMethod(message)?.takeRetainedValue() as String? 36 | } 37 | 38 | func value(for headerField: String) -> String? { 39 | return CFHTTPMessageCopyHeaderFieldValue(message, headerField as CFString)?.takeRetainedValue() as String? 40 | } 41 | 42 | func append(_ data: Data) -> Bool { 43 | return data.withUnsafeBytes { (bytes: UnsafeRawBufferPointer) -> Bool in 44 | guard let baseAddress = bytes.bindMemory(to: UInt8.self).baseAddress else { 45 | // I think data.withUnsafeBytes doesn't pass a null pointer but just in case, ignore it. 46 | return true 47 | } 48 | return CFHTTPMessageAppendBytes(message, baseAddress, bytes.count) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/ApolloDeveloperKit/WebServer/HTTPResponseMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPResponseMessage.swift 3 | // ApolloDeveloperKit 4 | // 5 | // Created by Ryosuke Ito on 8/18/20. 6 | // Copyright © 2020 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | final class HTTPResponseMessage { 12 | private let message: CFHTTPMessage 13 | 14 | init(statusCode: Int, statusDescription: String? = nil, httpVersion: String) { 15 | self.message = CFHTTPMessageCreateResponse(kCFAllocatorDefault, statusCode, statusDescription as CFString?, httpVersion as CFString).takeRetainedValue() 16 | } 17 | 18 | func setBody(_ body: Data) { 19 | CFHTTPMessageSetBody(message, body as CFData) 20 | } 21 | 22 | func setValue(_ value: String, for headerField: String) { 23 | CFHTTPMessageSetHeaderFieldValue(message, headerField as CFString, value as CFString) 24 | } 25 | 26 | func serialize() -> Data? { 27 | return CFHTTPMessageCopySerializedMessage(message)?.takeRetainedValue() as Data? 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/ApolloDeveloperKit/WebServer/HTTPServerError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPServerError.swift 3 | // ApolloDeveloperKit 4 | // 5 | // Created by Ryosuke Ito on 7/13/19. 6 | // Copyright © 2019 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /** 12 | * `HTTPServerError` represents an error derives from an underlying HTTP server. 13 | */ 14 | public enum HTTPServerError: CustomNSError, LocalizedError { 15 | /// Thrown when multiple errors occurred while creating a new socket. 16 | case multipleSocketErrorOccurred([UInt16: Error]) 17 | case unsupportedBodyEncoding(String) 18 | 19 | public static let errorDomain = "HTTPServerErrorDomain" 20 | 21 | public var errorCode: Int { 22 | switch self { 23 | case .multipleSocketErrorOccurred: 24 | return 199 25 | case .unsupportedBodyEncoding: 26 | return 200 27 | } 28 | } 29 | 30 | public var errorDescription: String? { 31 | switch self { 32 | case .multipleSocketErrorOccurred: 33 | return "Multiple error occurred while creating socket(s)." 34 | case .unsupportedBodyEncoding(let encoding): 35 | return "Failed to parse the given HTTP body encoded in \(encoding)." 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/ApolloDeveloperKit/WebServer/InterfaceAddress.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InterfaceAddress.swift 3 | // ApolloDeveloperKit 4 | // 5 | // Created by Ryosuke Ito on 9/15/20. 6 | // Copyright © 2020 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import Darwin 10 | 11 | struct InterfaceAddress: RawRepresentable { 12 | var rawValue: ifaddrs 13 | 14 | init(rawValue: ifaddrs) { 15 | self.rawValue = rawValue 16 | } 17 | 18 | var name: String { 19 | return String(cString: rawValue.ifa_name) 20 | } 21 | 22 | var isUp: Bool { 23 | return (rawValue.ifa_flags & UInt32(IFF_UP)) != 0 24 | } 25 | 26 | var socketFamily: sa_family_t { 27 | return rawValue.ifa_addr.pointee.sa_family 28 | } 29 | 30 | var hostName: String? { 31 | var host = [CChar](repeating: 0, count: Int(NI_MAXHOST)) 32 | errno = 0 33 | guard getnameinfo(rawValue.ifa_addr, 34 | socklen_t(rawValue.ifa_addr.pointee.sa_len), 35 | &host, 36 | socklen_t(host.count), 37 | nil, 38 | 0, 39 | NI_NUMERICHOST | NI_NOFQDN) == 0 40 | else { return nil } 41 | return String(cString: host) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/ApolloDeveloperKit/WebServer/InterfaceAddressIterator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InterfaceAddressIterator.swift 3 | // ApolloDeveloperKit 4 | // 5 | // Created by Ryosuke Ito on 9/15/20. 6 | // Copyright © 2020 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | final class InterfaceAddressIterator: IteratorProtocol { 12 | private let initialPointer: UnsafeMutablePointer 13 | private let deallocator: (UnsafeMutablePointer) -> Void 14 | private var currentPointer: UnsafeMutablePointer? 15 | 16 | init(initialPointer: UnsafeMutablePointer, deallocator: @escaping (UnsafeMutablePointer) -> Void) { 17 | self.initialPointer = initialPointer 18 | self.deallocator = deallocator 19 | self.currentPointer = initialPointer 20 | } 21 | 22 | convenience init() throws { 23 | var initialPointer: UnsafeMutablePointer! 24 | errno = 0 25 | guard withUnsafeMutablePointer(to: &initialPointer, getifaddrs) != -1 else { 26 | throw POSIXError(POSIXErrorCode(rawValue: errno)!) 27 | } 28 | self.init(initialPointer: initialPointer, deallocator: freeifaddrs) 29 | } 30 | 31 | deinit { 32 | deallocator(initialPointer) 33 | } 34 | 35 | func next() -> InterfaceAddress? { 36 | guard let pointer = currentPointer else { return nil } 37 | defer { currentPointer = currentPointer?.pointee.ifa_next } 38 | return InterfaceAddress(rawValue: pointer.pointee) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/ApolloDeveloperKit/WebServer/MIMEType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MIMEType.swift 3 | // ApolloDeveloperKit 4 | // 5 | // Created by Ryosuke Ito on 9/29/19. 6 | // Copyright © 2019 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import CoreFoundation 10 | 11 | /** 12 | * `MIMEType` represents a very limited part of MIME types. 13 | * 14 | * - SeeAlso: https://www.iana.org/assignments/media-types/media-types.xhtml 15 | */ 16 | enum MIMEType: Equatable { 17 | case html(String.Encoding?) 18 | case javascript 19 | case json 20 | case css 21 | case plainText(String.Encoding?) 22 | case png 23 | case eventStream // https://html.spec.whatwg.org/multipage/iana.html#text/event-stream 24 | case octetStream 25 | 26 | init(pathExtension: String, encoding: String.Encoding?) { 27 | switch pathExtension { 28 | case "html": 29 | self = .html(encoding) 30 | case "js": 31 | self = .javascript 32 | case "json": 33 | self = .json 34 | case "css": 35 | self = .css 36 | case "txt": 37 | self = .plainText(encoding) 38 | case "png": 39 | self = .png 40 | default: 41 | self = .octetStream 42 | } 43 | } 44 | } 45 | 46 | // MARK: CustomStringConvertible 47 | 48 | extension MIMEType: CustomStringConvertible { 49 | var description: String { 50 | switch self { 51 | case .html(let encoding?): 52 | return "text/html; charset=\(encoding.ianaCharSetName)" 53 | case .html(nil): 54 | return "text/html" 55 | case .javascript: 56 | return "application/javascript" 57 | case .json: 58 | return "application/json" 59 | case .css: 60 | return "text/css" 61 | case .plainText(let encoding?): 62 | return "text/plain; charset=\(encoding.ianaCharSetName)" 63 | case .plainText(nil): 64 | return "text/plain" 65 | case .png: 66 | return "image/png" 67 | case .eventStream: 68 | return "text/event-stream" 69 | case .octetStream: 70 | return "application/octet-stream" 71 | } 72 | } 73 | } 74 | 75 | private extension String.Encoding { 76 | var ianaCharSetName: String { 77 | let cfStringEncoding = CFStringConvertNSStringEncodingToEncoding(rawValue) 78 | return CFStringConvertEncodingToIANACharSetName(cfStringEncoding) as String 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Tests/ApolloDeveloperKitTests/ApolloDebugServerLoadTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ApolloDebugServerLoadTests.swift 3 | // ApolloDeveloperKitTests 4 | // 5 | // Created by Ryosuke Ito on 11/9/19. 6 | // Copyright © 2019 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import Apollo 10 | import XCTest 11 | @testable import ApolloDeveloperKit 12 | 13 | class ApolloDebugServerLoadTests: XCTestCase { 14 | private var store: ApolloStore! 15 | private var client: ApolloClient! 16 | private var server: ApolloDebugServer! 17 | private var port = UInt16(0) 18 | private var session: URLSession! 19 | 20 | override func setUpWithError() throws { 21 | let cache = DebuggableNormalizedCache(cache: InMemoryNormalizedCache()) 22 | store = ApolloStore(cache: cache) 23 | let url = URL(string: "https://localhost/graphql")! 24 | let configuration = URLSessionConfiguration.test 25 | configuration.protocolClasses = [MockHTTPURLProtocol.self] 26 | let urlSessionClient = URLSessionClient(sessionConfiguration: configuration, callbackQueue: nil) 27 | let interceptorProvider = LegacyInterceptorProvider(client: urlSessionClient, 28 | shouldInvalidateClientOnDeinit: true, 29 | store: store) 30 | let networkTransport = DebuggableRequestChainNetworkTransport(interceptorProvider: interceptorProvider, endpointURL: url) 31 | client = ApolloClient(networkTransport: networkTransport, store: store) 32 | server = ApolloDebugServer(networkTransport: networkTransport, cache: cache, keepAliveInterval: 0.25) 33 | port = try server.start(randomPortIn: 49152...65535) 34 | session = URLSession(configuration: .test) 35 | } 36 | 37 | override func tearDown() { 38 | session.invalidateAndCancel() 39 | server.stop() 40 | } 41 | 42 | func testGetBundleJS_withMaximumNumberOfClients() { 43 | let url = server.serverURL!.appendingPathComponent("bundle.js") 44 | for index in (0..<8) { 45 | let expectation = self.expectation(description: "response should be received (\(index))") 46 | let task = session.dataTask(with: url) { data, response, error in 47 | defer { expectation.fulfill() } 48 | if let error = error { 49 | return XCTFail(String(describing: error)) 50 | } 51 | guard let response = response as? HTTPURLResponse else { 52 | return XCTFail("unexpected repsonse type") 53 | } 54 | XCTAssertEqual(response.statusCode, 200) 55 | XCTAssertEqual(response.allHeaderFields["Content-Type"] as? String, "application/javascript") 56 | guard let data = data else { 57 | fatalError("URLSession.dataTask(with:) must pass either of error or data") 58 | } 59 | XCTAssertFalse(data.isEmpty) 60 | } 61 | task.resume() 62 | } 63 | waitForExpectations(timeout: 10.0, handler: nil) 64 | } 65 | } 66 | 67 | private class MockHTTPURLProtocol: URLProtocol { 68 | private let httpVersion = "1.1" 69 | private let queue = OperationQueue() 70 | 71 | override class func canInit(with request: URLRequest) -> Bool { 72 | return true 73 | } 74 | 75 | override class func canInit(with task: URLSessionTask) -> Bool { 76 | return true 77 | } 78 | 79 | override class func canonicalRequest(for request: URLRequest) -> URLRequest { 80 | return request 81 | } 82 | 83 | override func startLoading() { 84 | guard !Thread.isMainThread else { 85 | return queue.addOperation { [weak self] in 86 | self?.startLoading() 87 | } 88 | } 89 | let headerFields = ["Content-Type": "text/plain; charset=utf-8"] 90 | let response = HTTPURLResponse(url: request.url!, statusCode: 500, httpVersion: httpVersion, headerFields: headerFields)! 91 | client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) 92 | client?.urlProtocol(self, didLoad: Data(HTTPURLResponse.localizedString(forStatusCode: 500).utf8)) 93 | client?.urlProtocolDidFinishLoading(self) 94 | } 95 | 96 | override func stopLoading() { 97 | queue.cancelAllOperations() 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Tests/ApolloDeveloperKitTests/ApolloDeveloperKitTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ApolloDeveloperKitTests.swift 3 | // ApolloDeveloperKitTests 4 | // 5 | // Created by Ryosuke Ito on 8/31/20. 6 | // Copyright © 2020 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import ApolloDeveloperKit 11 | 12 | class ApolloDeveloperKitTests: XCTestCase { 13 | func testApolloDeveloperKitVersionNumber() { 14 | let version = Bundle(identifier: "com.github.manicmaniac.ApolloDeveloperKit")!.object(forInfoDictionaryKey: kCFBundleVersionKey as String) as! String 15 | XCTAssertEqual(ApolloDeveloperKitVersionNumber, Double(version)) 16 | XCTAssertGreaterThan(ApolloDeveloperKitVersionNumber, 0) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Tests/ApolloDeveloperKitTests/Background/BackgroundTaskTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BackgroundTaskTests.swift 3 | // ApolloDeveloperKitTests 4 | // 5 | // Created by Ryosuke Ito on 3/1/20. 6 | // Copyright © 2020 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import ApolloDeveloperKit 11 | 12 | #if os(iOS) 13 | import UIKit 14 | 15 | class BackgroundTaskTests: XCTestCase { 16 | private var executor: MockBackgroundTaskExecutor! 17 | private var backgroundTask: BackgroundTask! 18 | 19 | override func setUp() { 20 | executor = MockBackgroundTaskExecutor() 21 | backgroundTask = BackgroundTask(executor: executor) 22 | } 23 | 24 | func testBeginBackgroundTaskIfPossible_whenTaskIsNotRunning() { 25 | backgroundTask.beginBackgroundTaskIfPossible() 26 | XCTAssertEqual(Set(executor.expirationHandlersByActiveTaskIdentifier.keys), [backgroundTask.currentIdentifier]) 27 | } 28 | 29 | func testBeginBackgroundTaskIfPossible_whenTaskIsAlreadyRunning() { 30 | backgroundTask.beginBackgroundTaskIfPossible() 31 | backgroundTask.beginBackgroundTaskIfPossible() 32 | XCTAssertEqual(Set(executor.expirationHandlersByActiveTaskIdentifier.keys), [backgroundTask.currentIdentifier]) 33 | } 34 | 35 | func testBeginBackgroundTaskIfPossible_thenTaskExpires() { 36 | backgroundTask.beginBackgroundTaskIfPossible() 37 | executor.expireBackgroundTask(backgroundTask.currentIdentifier) 38 | XCTAssertTrue(executor.expirationHandlersByActiveTaskIdentifier.isEmpty) 39 | XCTAssertEqual(backgroundTask.currentIdentifier, .invalid) 40 | } 41 | } 42 | 43 | private class MockBackgroundTaskExecutor: BackgroundTaskExecutor { 44 | var expirationHandlersByActiveTaskIdentifier = [UIBackgroundTaskIdentifier: () -> Void]() 45 | 46 | func beginBackgroundTask(withName name: String?, expirationHandler handler: (() -> Void)?) -> UIBackgroundTaskIdentifier { 47 | XCTAssertEqual(name, "com.github.manicmaniac.ApolloDeveloperKit.BackgroundTask") 48 | let taskIdentifier = generateNewTaskIdentifier() 49 | expirationHandlersByActiveTaskIdentifier[taskIdentifier] = handler ?? {} 50 | return taskIdentifier 51 | } 52 | 53 | func endBackgroundTask(_ identifier: UIBackgroundTaskIdentifier) { 54 | expirationHandlersByActiveTaskIdentifier[identifier] = nil 55 | } 56 | 57 | func expireBackgroundTask(_ identifier: UIBackgroundTaskIdentifier) { 58 | expirationHandlersByActiveTaskIdentifier[identifier]?() 59 | } 60 | 61 | private func generateNewTaskIdentifier() -> UIBackgroundTaskIdentifier { 62 | return UIBackgroundTaskIdentifier(rawValue: expirationHandlersByActiveTaskIdentifier.count + 1) 63 | } 64 | } 65 | #endif 66 | -------------------------------------------------------------------------------- /Tests/ApolloDeveloperKitTests/Console/ConsoleDidWriteNotificationTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConsoleDidWriteNotificationTests.swift 3 | // ApolloDeveloperKitTests 4 | // 5 | // Created by Ryosuke Ito on 8/25/20. 6 | // Copyright © 2020 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import ApolloDeveloperKit 11 | 12 | class ConsoleDidWriteNotificationTests: XCTestCase { 13 | private var consoleRedirection: ConsoleRedirection! 14 | 15 | override func setUp() { 16 | let notificationCenter = NotificationCenter() 17 | let duplicator = MockFileDescriptorDuplicator() 18 | self.consoleRedirection = ConsoleRedirection(standardOutputFileDescriptor: STDOUT_FILENO, 19 | standardErrorFileDescriptor: STDERR_FILENO, 20 | notificationCenter: notificationCenter, 21 | queue: .main, 22 | duplicator: duplicator) 23 | } 24 | 25 | func testInitWithObjectDataDestination() { 26 | let data = Data() 27 | let destination = ConsoleRedirection.Destination.standardOutput 28 | let notification = ConsoleDidWriteNotification(object: consoleRedirection, data: data, destination: destination) 29 | XCTAssert(notification.object === consoleRedirection) 30 | XCTAssertEqual(notification.data, data) 31 | XCTAssertEqual(notification.destination, destination) 32 | } 33 | 34 | func testInitWithRawValue() throws { 35 | let data = Data() 36 | let destination = ConsoleRedirection.Destination.standardOutput 37 | let rawValue = Notification(name: .consoleDidWrite, object: consoleRedirection, userInfo: [ 38 | "data": data, 39 | "destination": destination 40 | ]) 41 | let notification = try XCTUnwrap(ConsoleDidWriteNotification(rawValue: rawValue)) 42 | XCTAssert(notification.object === consoleRedirection) 43 | XCTAssertEqual(notification.data, data) 44 | XCTAssertEqual(notification.destination, destination) 45 | } 46 | 47 | func testInitWithRawValue_withInvalidRawValue() { 48 | let rawValue = Notification(name: Notification.Name("invalid")) 49 | let notification = ConsoleDidWriteNotification(rawValue: rawValue) 50 | XCTAssertNil(notification) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Tests/ApolloDeveloperKitTests/DebuggableRequestChainNetworkTransportTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DebuggableRequestChainNetworkTransportTests.swift 3 | // ApolloDeveloperKitTests 4 | // 5 | // Created by Ryosuke Ito on 5/31/21. 6 | // Copyright © 2019 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import Apollo 10 | import XCTest 11 | @testable import ApolloDeveloperKit 12 | 13 | class DebuggableRequestChainNetworkTransportTests: XCTestCase { 14 | private let url = URL(string: "https://localhost/graphql")! 15 | 16 | func testInterceptorProviderWillSendOperation() { 17 | let interceptorProvider = MockInterceptorProvider() 18 | let operation = MockGraphQLQuery() 19 | let networkTransport = DebuggableRequestChainNetworkTransport(interceptorProvider: interceptorProvider, endpointURL: url) 20 | let delegateHandler = DebuggableNetworkTransportDelegateHandler() 21 | let expectation = self.expectation(description: "The corresponding delegate method should be called.") 22 | delegateHandler.networkTransportWillSendOperation = { receivedNetworkTransport, receivedOperation in 23 | XCTAssert(receivedNetworkTransport === networkTransport) 24 | XCTAssertEqual(receivedOperation.operationType, operation.operationType) 25 | XCTAssertEqual(receivedOperation.operationName, operation.operationName) 26 | XCTAssertEqual(receivedOperation.operationDefinition, operation.operationDefinition) 27 | XCTAssertEqual(receivedOperation.operationIdentifier, operation.operationIdentifier) 28 | expectation.fulfill() 29 | } 30 | networkTransport.delegate = delegateHandler 31 | networkTransport.interceptorProvider(interceptorProvider, willSendOperation: operation) 32 | waitForExpectations(timeout: 0.5) 33 | } 34 | 35 | func testInterceptorProviderDidSendOperation_withSuccess() { 36 | let interceptorProvider = MockInterceptorProvider() 37 | let operation = MockGraphQLQuery() 38 | let networkTransport = DebuggableRequestChainNetworkTransport(interceptorProvider: interceptorProvider, endpointURL: url) 39 | let delegateHandler = DebuggableNetworkTransportDelegateHandler() 40 | let expectation = self.expectation(description: "The corresponding delegate method should be called.") 41 | delegateHandler.networkTransportDidSendOperation = { receivedNetworkTransport, receivedOperation, receivedResult in 42 | XCTAssert(receivedNetworkTransport === networkTransport) 43 | XCTAssertEqual(receivedOperation.operationType, operation.operationType) 44 | XCTAssertEqual(receivedOperation.operationName, operation.operationName) 45 | XCTAssertEqual(receivedOperation.operationDefinition, operation.operationDefinition) 46 | XCTAssertEqual(receivedOperation.operationIdentifier, operation.operationIdentifier) 47 | XCTAssertNotNil(try? receivedResult.get()) 48 | expectation.fulfill() 49 | } 50 | networkTransport.delegate = delegateHandler 51 | let graphQLResult = GraphQLResult(data: nil, 52 | extensions: nil, 53 | errors: nil, 54 | source: .server, 55 | dependentKeys: nil) 56 | networkTransport.interceptorProvider(interceptorProvider, didSendOperation: operation, result: .success(graphQLResult)) 57 | waitForExpectations(timeout: 0.5) 58 | } 59 | } 60 | 61 | private class MockInterceptorProvider: InterceptorProvider { 62 | func interceptors(for operation: Operation) -> [ApolloInterceptor] where Operation : GraphQLOperation { 63 | return [] 64 | } 65 | } 66 | 67 | private class DebuggableNetworkTransportDelegateHandler: DebuggableNetworkTransportDelegate { 68 | var networkTransportWillSendOperation: ((NetworkTransport, AnyGraphQLOperation) -> Void)? 69 | var networkTransportDidSendOperation: ((NetworkTransport, AnyGraphQLOperation, Result, Error>) -> Void)? 70 | 71 | func networkTransport(_ networkTransport: NetworkTransport, willSendOperation operation: Operation) where Operation : GraphQLOperation { 72 | networkTransportWillSendOperation?(networkTransport, AnyGraphQLOperation(operation)) 73 | } 74 | 75 | func networkTransport(_ networkTransport: NetworkTransport, didSendOperation operation: Operation, result: Result, Error>) where Operation : GraphQLOperation { 76 | networkTransportDidSendOperation?(networkTransport, AnyGraphQLOperation(operation), result.map(GraphQLResult.init(_:))) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Tests/ApolloDeveloperKitTests/EventStreamMessage/EventStreamMessageTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EventStreamMessageTests.swift 3 | // ApolloDeveloperKitTests 4 | // 5 | // Created by Ryosuke Ito on 8/29/20. 6 | // Copyright © 2020 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import ApolloDeveloperKit 11 | 12 | class EventStreamMessageTests: XCTestCase { 13 | func testPing() { 14 | XCTAssertEqual(EventStreamMessage.ping.rawValue, ":\n\n") 15 | } 16 | 17 | func testInitWithRawValue_withEmptyString() { 18 | XCTAssertNil(EventStreamMessage(rawValue: "")) 19 | } 20 | 21 | func testInitWithRawValue_withValidString() { 22 | let rawValue = "event: foo\ndata: bar\n\n" 23 | XCTAssertEqual(EventStreamMessage(rawValue: rawValue)?.rawValue, rawValue) 24 | } 25 | 26 | func testInitWithEvent() { 27 | XCTAssertEqual(EventStreamMessage(event: "foo").rawValue, "event: foo\n\n") 28 | } 29 | 30 | func testInitWithData() { 31 | XCTAssertEqual(EventStreamMessage(data: "foo").rawValue, "data: foo\n\n") 32 | } 33 | 34 | func testInitWithData_withMultilineString() { 35 | let string = """ 36 | foo 37 | bar 38 | baz 39 | """ 40 | let expectedRawValue = """ 41 | data: foo 42 | data: bar 43 | data: baz\n\n 44 | """ 45 | XCTAssertEqual(EventStreamMessage(data: string).rawValue, expectedRawValue) 46 | } 47 | 48 | func testInitWithId() { 49 | XCTAssertEqual(EventStreamMessage(id: "foo").rawValue, "id: foo\n\n") 50 | } 51 | 52 | func testInitWithRetry() { 53 | XCTAssertEqual(EventStreamMessage(retry: 42).rawValue, "retry: 42\n\n") 54 | } 55 | 56 | func testInitWithEventDataIdRetry() { 57 | let message = EventStreamMessage(event: "foo", 58 | data: "bar", 59 | id: "baz", 60 | retry: 42) 61 | let expectedRawValue = """ 62 | event: foo 63 | data: bar 64 | id: baz 65 | retry: 42\n\n 66 | """ 67 | XCTAssertEqual(message.rawValue, expectedRawValue) 68 | } 69 | 70 | func testDescription() { 71 | let message = EventStreamMessage(event: "foo", 72 | data: "bar", 73 | id: "baz", 74 | retry: 42) 75 | let expectedDescription = """ 76 | event: foo 77 | data: bar 78 | id: baz 79 | retry: 42\n\n 80 | """ 81 | XCTAssertEqual(String(describing: message), expectedDescription) 82 | } 83 | 84 | func testInitWithDescription() { 85 | let description = "event: foo\ndata: bar\n\n" 86 | XCTAssertEqual(EventStreamMessage(description)?.rawValue, description) 87 | } 88 | 89 | func testMessage() { 90 | XCTAssertEqual(EventStreamMessage.ping.message, EventStreamMessage.ping) 91 | } 92 | 93 | func testHash() { 94 | let message1 = EventStreamMessage(event: "foo", data: "bar") 95 | let message2 = EventStreamMessage(event: "foo", data: "bar") 96 | let message3 = EventStreamMessage(event: "baz", data: "qux") 97 | XCTAssertEqual(message1.hashValue, message2.hashValue) 98 | XCTAssertNotEqual(message2.hashValue, message3.hashValue) 99 | // Somehow the above code doesn't invoke `hash(into:)` but the below does. 100 | let messageSet: Set = [message1, message2, message3] 101 | XCTAssertEqual(messageSet.count, 2) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Tests/ApolloDeveloperKitTests/GraphQL/AnyGraphQLOperationTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnyGraphQLOperationTests.swift 3 | // ApolloDeveloperKitTests 4 | // 5 | // Created by Ryosuke Ito on 6/29/19. 6 | // Copyright © 2019 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import Apollo 10 | import XCTest 11 | @testable import ApolloDeveloperKit 12 | 13 | class AnyGraphQLOperationTests: XCTestCase { 14 | func testInitWithJSONObject_withValidJSONObject() throws { 15 | let jsonObject: JSONObject = [ 16 | "variables": [ 17 | "input": [ 18 | "string": "foo" as NSString, 19 | "integer": 42 as NSNumber, 20 | "float": 4.2 as NSNumber, 21 | "boolean": true as CFBoolean, 22 | "array": ["foo"] as NSArray, 23 | "null": NSNull() 24 | ] 25 | ], 26 | "operationName": NSNull(), 27 | "query": "query { posts { id } }" 28 | ] 29 | let operation = try Operation(jsonValue: jsonObject) 30 | let request = AnyGraphQLOperation(operation: operation) 31 | XCTAssertNil(request.operationIdentifier) 32 | XCTAssertEqual(request.operationType, .query) 33 | XCTAssertEqual(request.operationDefinition, "query { posts { id } }") 34 | XCTAssertEqual(request.variables?.count, 1) 35 | let input = try XCTUnwrap(request.variables?["input"] as? [String: Any]) 36 | XCTAssertEqual(input["string"] as? String, "foo") 37 | XCTAssertEqual(input["integer"] as? Int, 42) 38 | XCTAssertEqual(input["float"] as? Double, 4.2) 39 | XCTAssertEqual(input["boolean"] as? Bool, true) 40 | XCTAssertEqual(input["array"] as? [String], ["foo"]) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Tests/ApolloDeveloperKitTests/GraphQL/AnyGraphQLSelectionSetTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnyGraphQLSelectionSetTests.swift 3 | // ApolloDeveloperKitTests 4 | // 5 | // Created by Ryosuke Ito on 6/29/19. 6 | // Copyright © 2019 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import Apollo 10 | import XCTest 11 | @testable import ApolloDeveloperKit 12 | 13 | class AnyGraphQLSelectionSetTests: XCTestCase { 14 | func testSelections() { 15 | XCTAssertTrue(AnyGraphQLSelectionSet.selections.isEmpty) 16 | } 17 | 18 | func testInitWithUnsafeResultMap() { 19 | let resultMap = ["foo": "bar"] 20 | let selectionSet = AnyGraphQLSelectionSet(unsafeResultMap: resultMap) 21 | XCTAssertEqual(selectionSet.resultMap as? [String: String], resultMap) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Tests/ApolloDeveloperKitTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 0.15.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Tests/ApolloDeveloperKitTests/JSON/Record+JSONEncodableTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Record+JSONEncodableTests.swift 3 | // ApolloDeveloperKitTests 4 | // 5 | // Created by Ryosuke Ito on 6/29/19. 6 | // Copyright © 2019 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import Apollo 10 | import XCTest 11 | @testable import ApolloDeveloperKit 12 | 13 | class Record_JSONEncodableTests: XCTestCase { 14 | func testJSONValue_whenValueIsSwiftStandardType() throws { 15 | let record = Record(key: "key", [ 16 | "String": "foo", 17 | "Int": 42, 18 | "Double": 1.5, 19 | "Bool": true, 20 | "nil": nil as String? as Any, 21 | "[String]": ["foo", "bar"], 22 | "[[Int]]": [[0, 42]], 23 | "[String: String]": ["foo": "bar"] 24 | ]) 25 | let object = try XCTUnwrap(record.jsonValue as? [String: Any]) 26 | XCTAssertEqual(object["String"] as? String, "foo") 27 | XCTAssertEqual(object["Int"] as? Int, 42) 28 | XCTAssertEqual(object["Double"] as? Double, 1.5) 29 | XCTAssertEqual(object["Bool"] as? Bool, true) 30 | XCTAssertEqual(object["nil"] as? NSNull, NSNull()) 31 | XCTAssertEqual(object["[String]"] as? [String], ["foo", "bar"]) 32 | XCTAssertEqual(object["[[Int]]"] as? [[Int]], [[0, 42]]) 33 | XCTAssertEqual(object["[String: String]"] as? [String: String], ["foo": "bar"]) 34 | } 35 | 36 | func testJSONValue_whenValueIsFoundationClass() throws { 37 | let record = Record(key: "key", [ 38 | "NSString": "foo" as NSString, 39 | "NSNumber-Int": 42 as NSNumber, 40 | "NSNumber-Double": 1.5 as NSNumber, 41 | "NSNumber-Bool": true as NSNumber, 42 | "NSNull": NSNull(), 43 | "NSArray": ["foo" as NSString, "bar" as NSString] as NSArray, 44 | "NSArray>": [[0 as NSNumber, 42 as NSNumber] as NSArray] as NSArray, 45 | "NSDictionary": ["foo" as NSString: "bar" as NSString] as NSDictionary 46 | ]) 47 | let object = try XCTUnwrap(record.jsonValue as? [String: Any]) 48 | XCTAssertEqual(object["NSString"] as? NSString, "foo") 49 | XCTAssertEqual(object["NSNumber-Int"] as? NSNumber, 42) 50 | XCTAssertEqual(object["NSNumber-Double"] as? NSNumber, 1.5) 51 | XCTAssertEqual(object["NSNumber-Bool"] as? NSNumber, true) 52 | XCTAssertEqual(object["NSNull"] as? NSNull, NSNull()) 53 | XCTAssertEqual(object["NSArray"] as? NSArray, ["foo", "bar"]) 54 | XCTAssertEqual(object["NSArray>"] as? NSArray, [[0, 42]]) 55 | XCTAssertEqual(object["NSDictionary"] as? [String: String], ["foo": "bar"]) 56 | } 57 | 58 | func testJSONValue_whenValueIsCoreFoundationClass() throws { 59 | let record = Record(key: "key", [ 60 | "CFString": "foo" as CFString, 61 | "CFNumber-Int": 42 as CFNumber, 62 | "CFNumber-Double": 1.5 as CFNumber, 63 | "CFBoolean": true as CFBoolean, 64 | "CFNull": kCFNull!, 65 | "CFArray": ["foo" as CFString, "bar" as CFString] as CFArray, 66 | "CFArray>": [[0 as CFNumber, 42 as CFNumber] as CFArray] as CFArray, 67 | "CFDictionary": ["foo" as CFString: "bar" as CFString] as CFDictionary 68 | ]) 69 | let object = try XCTUnwrap(record.jsonValue as? [String: CFTypeRef]) 70 | XCTAssert(CFEqual(object["CFString"], "foo" as CFString)) 71 | XCTAssert(CFEqual(object["CFNumber-Int"], 42 as CFNumber)) 72 | XCTAssert(CFEqual(object["CFNumber-Double"], 1.5 as CFNumber)) 73 | XCTAssert(CFEqual(object["CFBoolean"], true as CFBoolean)) 74 | XCTAssert(CFEqual(object["CFNull"], kCFNull)) 75 | XCTAssert(CFEqual(object["CFArray"], ["foo" as CFString, "bar" as CFString] as CFArray)) 76 | XCTAssert(CFEqual(object["CFArray>"], [[0 as CFNumber, 42 as CFNumber] as CFArray] as CFArray)) 77 | XCTAssert(CFEqual(object["CFDictionary"], ["foo" as CFString: "bar" as CFString] as CFDictionary)) 78 | } 79 | 80 | func testJSONValue_whenValueIsHybridType() throws { 81 | let record = Record(key: "key", [ 82 | "[NSString]": ["foo" as NSString, "bar" as NSString], 83 | "[NSString: Int]": ["foo" as NSString: 42] 84 | ]) 85 | let object = try XCTUnwrap(record.jsonValue as? [String: Any]) 86 | XCTAssertEqual(object["[NSString]"] as? [NSString], ["foo", "bar"]) 87 | XCTAssertEqual(object["[NSString: Int]"] as? [NSString: Int], ["foo": 42]) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Tests/ApolloDeveloperKitTests/JSON/Reference+JSONEncodableTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Reference+JSONEncodableTests.swift 3 | // ApolloDeveloperKitTests 4 | // 5 | // Created by Ryosuke Ito on 6/29/19. 6 | // Copyright © 2019 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import Apollo 10 | import XCTest 11 | @testable import ApolloDeveloperKit 12 | 13 | class Reference_JSONEncodableTests: XCTestCase { 14 | func testJSONValue() throws { 15 | let reference = Reference(key: "foo") 16 | let object = try XCTUnwrap(reference.jsonValue as? [String: Any]) 17 | XCTAssertEqual(object["generated"] as? Bool, true) 18 | XCTAssertEqual(object["id"] as? String, "foo") 19 | XCTAssertEqual(object["type"] as? String, "id") 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/ApolloDeveloperKitTests/JSON/Schema+JSONDecodableTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Schema+JSONDecodableTests.swift 3 | // ApolloDeveloperKitTests 4 | // 5 | // Created by Ryosuke Ito on 8/22/20. 6 | // Copyright © 2020 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import Apollo 11 | @testable import ApolloDeveloperKit 12 | 13 | class Schema_JSONDecodableTests: XCTestCase { 14 | func testOperationInitWithJSONObject_withInvalidJSONObject() { 15 | let invalidJSONObject: JSONObject = [ 16 | "operationName": Set() 17 | ] 18 | XCTAssertThrowsError(try Operation(jsonValue: invalidJSONObject)) { error in 19 | guard case JSONDecodingError.couldNotConvert(value: let jsonObject, to: let type) = error else { 20 | return XCTFail() 21 | } 22 | XCTAssertEqual(jsonObject as? NSDictionary, invalidJSONObject as NSDictionary) 23 | XCTAssert(type is ApolloDeveloperKit.Operation.Type) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Tests/ApolloDeveloperKitTests/JSON/Schema+JSONEncodableTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Schema+JSONEncodableTests.swift 3 | // ApolloDeveloperKitTests 4 | // 5 | // Created by Ryosuke Ito on 8/22/20. 6 | // Copyright © 2020 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import ApolloDeveloperKit 11 | 12 | class Schema_JSONEncodableTests: XCTestCase { 13 | func testErrorLikeInitWithJSONValue() { 14 | let error = URLError(.badURL) 15 | let errorLike = ErrorLike(error: error) 16 | XCTAssertEqual(errorLike.message, error.localizedDescription) 17 | XCTAssertEqual(errorLike.name, "NSError") 18 | XCTAssertNil(errorLike.fileName) 19 | XCTAssertNil(errorLike.lineNumber) 20 | XCTAssertNil(errorLike.columnNumber) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Tests/ApolloDeveloperKitTests/Network/Interceptor/DebugInitializeInterceptorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DebugInitializeInterceptorTests.swift 3 | // ApolloDeveloperKitTests 4 | // 5 | // Created by Ryosuke Ito on 5/31/21. 6 | // Copyright © 2021 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import Apollo 10 | import XCTest 11 | @testable import ApolloDeveloperKit 12 | 13 | class DebugInitializeInterceptorTests: XCTestCase { 14 | func testInterceptAsync() { 15 | let interceptor = DebugInitializeInterceptor() 16 | let delegateHandler = DebugInitializeInterceptorDelegateHandler() 17 | interceptor.delegate = delegateHandler 18 | let operation = MockGraphQLQuery() 19 | let chain = RequestChain(interceptors: []) 20 | let url = URL(string: "https://localhost/graphql")! 21 | let request = HTTPRequest(graphQLEndpoint: url, 22 | operation: operation, 23 | contentType: "", 24 | clientName: "", 25 | clientVersion: "", 26 | additionalHeaders: [:]) 27 | let expectationForWillSendOperation = expectation(description: "interceptor(_:willSendOperation:) delegate method should be called.") 28 | var isInterceptorWillSendOperationCalled = false 29 | var isInterceptorDidSendOperationCalled = false 30 | delegateHandler.interceptorWillSendOperation = { receivedInterceptor, receivedOperation in 31 | isInterceptorWillSendOperationCalled = true 32 | XCTAssertFalse(isInterceptorDidSendOperationCalled) 33 | XCTAssert(receivedInterceptor === interceptor) 34 | XCTAssertEqual(receivedOperation.operationType, operation.operationType) 35 | XCTAssertEqual(receivedOperation.operationName, operation.operationName) 36 | XCTAssertEqual(receivedOperation.operationDefinition, operation.operationDefinition) 37 | XCTAssertEqual(receivedOperation.operationIdentifier, operation.operationIdentifier) 38 | expectationForWillSendOperation.fulfill() 39 | } 40 | let expectationForDidSendOperation = expectation(description: "interceptor(_:didSendOperation:result:) delegate method should be called.") 41 | delegateHandler.interceptorDidSendOperation = { receivedInterceptor, receivedOperation, receivedResult in 42 | isInterceptorDidSendOperationCalled = true 43 | XCTAssert(isInterceptorWillSendOperationCalled) 44 | XCTAssert(receivedInterceptor === interceptor) 45 | XCTAssertEqual(receivedOperation.operationType, operation.operationType) 46 | XCTAssertEqual(receivedOperation.operationName, operation.operationName) 47 | XCTAssertEqual(receivedOperation.operationDefinition, operation.operationDefinition) 48 | XCTAssertEqual(receivedOperation.operationIdentifier, operation.operationIdentifier) 49 | XCTAssertNil(try? receivedResult.get()) 50 | expectationForDidSendOperation.fulfill() 51 | } 52 | let expectationForCompletion = expectation(description: "completion callback should be called.") 53 | interceptor.interceptAsync(chain: chain, request: request, response: nil) { result in 54 | XCTAssert(isInterceptorWillSendOperationCalled) 55 | XCTAssert(isInterceptorDidSendOperationCalled) 56 | expectationForCompletion.fulfill() 57 | } 58 | waitForExpectations(timeout: 0.5) 59 | } 60 | } 61 | 62 | class DebugInitializeInterceptorDelegateHandler: DebugInitializeInterceptorDelegate { 63 | var interceptorWillSendOperation: ((ApolloInterceptor, AnyGraphQLOperation) -> Void)? 64 | var interceptorDidSendOperation: ((ApolloInterceptor, AnyGraphQLOperation, Result, Error>) -> Void)? 65 | 66 | func interceptor(_ interceptor: ApolloInterceptor, willSendOperation operation: Operation) where Operation : GraphQLOperation { 67 | interceptorWillSendOperation?(interceptor, AnyGraphQLOperation(operation)) 68 | } 69 | 70 | func interceptor(_ interceptor: ApolloInterceptor, didSendOperation operation: Operation, result: Result, Error>) where Operation : GraphQLOperation { 71 | interceptorDidSendOperation?(interceptor, AnyGraphQLOperation(operation), result.map(GraphQLResult.init(_:))) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Tests/ApolloDeveloperKitTests/Store/KeyedStackTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyedStackTests.swift 3 | // ApolloDeveloperKitTests 4 | // 5 | // Created by Ryosuke Ito on 8/25/20. 6 | // Copyright © 2020 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import ApolloDeveloperKit 11 | 12 | class KeyedStackTests: XCTestCase { 13 | func testPush() { 14 | var stack = KeyedStack() 15 | stack.push(0, for: "foo") 16 | stack.push(1, for: "bar") 17 | stack.push(2, for: "foo") 18 | let elements = Array(stack) 19 | XCTAssertEqual(elements.count, 3) 20 | XCTAssertEqual(elements[0].0, "foo") 21 | XCTAssertEqual(elements[0].1, 0) 22 | XCTAssertEqual(elements[1].0, "bar") 23 | XCTAssertEqual(elements[1].1, 1) 24 | XCTAssertEqual(elements[2].0, "foo") 25 | XCTAssertEqual(elements[2].1, 2) 26 | } 27 | 28 | func testSubscriptGet_whenKeyExists() { 29 | var stack = KeyedStack() 30 | stack.push(0, for: "foo") 31 | XCTAssertEqual(stack["foo"], 0) 32 | } 33 | 34 | func testSubscriptGet_whenKeyDoesNotExist() { 35 | let stack = KeyedStack() 36 | XCTAssertNil(stack["foo"]) 37 | } 38 | 39 | func testSubscriptGet_whenMultipleKeysExist() { 40 | var stack = KeyedStack() 41 | stack.push(0, for: "foo") 42 | stack.push(1, for: "foo") 43 | XCTAssertEqual(stack["foo"], 1) 44 | } 45 | 46 | func testSubscriptSet_whenKeyExists() { 47 | var stack = KeyedStack() 48 | stack.push(0, for: "foo") 49 | stack["foo"] = 1 50 | let elements = Array(stack) 51 | XCTAssertEqual(elements.count, 1) 52 | XCTAssertEqual(elements[0].0, "foo") 53 | XCTAssertEqual(elements[0].1, 1) 54 | } 55 | 56 | func testSubscriptSet_whenKeyDoesNotExist() { 57 | var stack = KeyedStack() 58 | stack.push(0, for: "foo") 59 | stack["bar"] = 1 60 | let elements = Array(stack) 61 | XCTAssertEqual(elements.count, 2) 62 | XCTAssertEqual(elements[0].0, "foo") 63 | XCTAssertEqual(elements[0].1, 0) 64 | XCTAssertEqual(elements[1].0, "bar") 65 | XCTAssertEqual(elements[1].1, 1) 66 | } 67 | 68 | func testSubscriptSet_whenMultipleKeysExist() { 69 | var stack = KeyedStack() 70 | stack.push(0, for: "foo") 71 | stack.push(1, for: "foo") 72 | stack["foo"] = 2 73 | let elements = Array(stack) 74 | XCTAssertEqual(elements.count, 2) 75 | XCTAssertEqual(elements[0].0, "foo") 76 | XCTAssertEqual(elements[0].1, 0) 77 | XCTAssertEqual(elements[1].0, "foo") 78 | XCTAssertEqual(elements[1].1, 2) 79 | } 80 | 81 | func testSubscriptSet_whenKeyExistsButValueIsNil() { 82 | var stack = KeyedStack() 83 | stack.push(0, for: "foo") 84 | stack["foo"] = nil 85 | let elements = Array(stack) 86 | XCTAssert(elements.isEmpty) 87 | } 88 | 89 | func testSubscriptSet_whenKeyDoesNotExistAndValueIsNil() { 90 | var stack = KeyedStack() 91 | stack.push(0, for: "foo") 92 | stack["bar"] = nil 93 | let elements = Array(stack) 94 | XCTAssertEqual(elements.count, 1) 95 | XCTAssertEqual(elements[0].0, "foo") 96 | XCTAssertEqual(elements[0].1, 0) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Tests/ApolloDeveloperKitTests/TestHelpers/GraphQLResult+AnyGraphQLOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GraphQLResult+AnyGraphQLOperation.swift 3 | // ApolloDeveloperKitTests 4 | // 5 | // Created by Ryosuke Ito on 6/3/21. 6 | // Copyright © 2021 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import Apollo 10 | @testable import ApolloDeveloperKit 11 | 12 | extension GraphQLResult where Data == AnyGraphQLOperation.Data { 13 | init(_ graphQLResult: GraphQLResult) where Data: GraphQLSelectionSet { 14 | self.init(data: try? graphQLResult.data.flatMap(AnyGraphQLOperation.Data.init(_:)), 15 | extensions: graphQLResult.extensions, 16 | errors: graphQLResult.errors, 17 | source: convert(source: graphQLResult.source), 18 | dependentKeys: nil) 19 | } 20 | } 21 | 22 | private func convert(source: GraphQLResult.Source) -> GraphQLResult.Source where Data: GraphQLSelectionSet { 23 | switch source { 24 | case .cache: 25 | return .cache 26 | case .server: 27 | return .server 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Tests/ApolloDeveloperKitTests/TestHelpers/HTTPRequest+AnyGraphQLOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPRequest+AnyGraphQLOperation.swift 3 | // ApolloDeveloperKitTests 4 | // 5 | // Created by Ryosuke Ito on 6/3/21. 6 | // Copyright © 2021 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import Apollo 10 | @testable import ApolloDeveloperKit 11 | 12 | extension HTTPRequest where Operation == AnyGraphQLOperation { 13 | convenience init(_ httpRequest: HTTPRequest) where Operation: GraphQLOperation { 14 | self.init(graphQLEndpoint: httpRequest.graphQLEndpoint, 15 | operation: AnyGraphQLOperation(httpRequest.operation), 16 | contentType: httpRequest.additionalHeaders["Content-Type"]!, 17 | clientName: httpRequest.additionalHeaders["apollographql-client-name"]!, 18 | clientVersion: httpRequest.additionalHeaders["apollographql-client-version"]!, 19 | additionalHeaders: httpRequest.additionalHeaders) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/ApolloDeveloperKitTests/TestHelpers/HTTPResponse+AnyGraphQLOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPResponse+AnyGraphQLOperation.swift 3 | // ApolloDeveloperKitTests 4 | // 5 | // Created by Ryosuke Ito on 6/3/21. 6 | // Copyright © 2021 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import Apollo 10 | @testable import ApolloDeveloperKit 11 | 12 | extension HTTPResponse where Operation == AnyGraphQLOperation { 13 | convenience init(_ httpResponse: HTTPResponse) where Operation: GraphQLOperation { 14 | self.init(response: httpResponse.httpResponse, 15 | rawData: httpResponse.rawData, 16 | parsedResponse: httpResponse.parsedResponse.flatMap(GraphQLResult.init(_:))) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Tests/ApolloDeveloperKitTests/TestHelpers/MockFileDescriptorDuplicator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockFileDescriptorDuplicator.swift 3 | // ApolloDeveloperKit 4 | // 5 | // Created by Ryosuke Ito on 8/25/20. 6 | // Copyright © 2020 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | @testable import ApolloDeveloperKit 10 | 11 | class MockFileDescriptorDuplicator: FileDescriptorDuplicator { 12 | private(set) var dupInvocationHistory = [Int32]() 13 | private(set) var dup2InvocationHistory = [(fildes: Int32, fildes2: Int32)]() 14 | 15 | func dup(_ fildes: Int32) -> Int32 { 16 | dupInvocationHistory.append(fildes) 17 | return fildes 18 | } 19 | 20 | func dup2(_ fildes: Int32, _ fildes2: Int32) -> Int32 { 21 | dup2InvocationHistory.append((fildes, fildes2)) 22 | return fildes2 23 | } 24 | 25 | func clearInvocationHistory() { 26 | dupInvocationHistory.removeAll() 27 | dup2InvocationHistory.removeAll() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Tests/ApolloDeveloperKitTests/TestHelpers/MockGraphQLOperations.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockGraphQLOperations.swift 3 | // ApolloDeveloperKitTests 4 | // 5 | // Created by Ryosuke Ito on 2/12/20. 6 | // Copyright © 2020 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import Apollo 10 | @testable import ApolloDeveloperKit 11 | 12 | class MockGraphQLQuery: GraphQLQuery { 13 | typealias Data = AnyGraphQLSelectionSet 14 | 15 | let operationDefinition = "query {}" 16 | let operationIdentifier = "MockGraphQLQuery 1" 17 | let operationName = "MockGraphQLQuery" 18 | } 19 | 20 | class MockGraphQLMutation: GraphQLMutation { 21 | typealias Data = AnyGraphQLSelectionSet 22 | 23 | let operationDefinition = "mutation {}" 24 | let operationIdentifier = "MockGraphQLMutation 1" 25 | let operationName = "MockGraphQLMutation" 26 | } 27 | 28 | class MockGraphQLSubscription: GraphQLSubscription { 29 | typealias Data = AnyGraphQLSelectionSet 30 | 31 | let operationDefinition = "subscription {}" 32 | let operationIdentifier = "MockGraphQLSubscription 1" 33 | let operationName = "MockGraphQLSubscription" 34 | } 35 | 36 | class MockGraphQLSelectionSet: GraphQLSelectionSet { 37 | static let selections = [GraphQLSelection]() 38 | let resultMap: ResultMap 39 | 40 | required init(unsafeResultMap: ResultMap) { 41 | self.resultMap = unsafeResultMap 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Tests/ApolloDeveloperKitTests/TestHelpers/MockNetworkTransport.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockNetworkTransport.swift 3 | // ApolloDeveloperKitTests 4 | // 5 | // Created by Ryosuke Ito on 2/13/20. 6 | // Copyright © 2020 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import Apollo 10 | import Foundation 11 | 12 | class MockNetworkTransport: NetworkTransport { 13 | var clientName = "clientName" 14 | var clientVersion = "clientVersion" 15 | 16 | private var results = ArraySlice>() 17 | 18 | var isResultsEmpty: Bool { 19 | return results.isEmpty 20 | } 21 | 22 | func append(response: GraphQLResponse) where Data: GraphQLSelectionSet { 23 | results.append(.success(response)) 24 | } 25 | 26 | func append(error: Error) { 27 | results.append(.failure(error)) 28 | } 29 | 30 | func send(operation: Operation, cachePolicy: CachePolicy, contextIdentifier: UUID?, callbackQueue: DispatchQueue, completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable where Operation : GraphQLOperation { 31 | let result = results.popFirst() 32 | switch result { 33 | case .success(let graphQLResult as GraphQLResult)?: 34 | completionHandler(.success(graphQLResult)) 35 | case .failure(let error)?: 36 | completionHandler(.failure(error)) 37 | case .success: 38 | fatalError("The type of the next response doesn't match the expected type.") 39 | case nil: 40 | fatalError("The number of invocation of send(operation:completionHandler:) exceeds the number of results.") 41 | } 42 | return MockCancellable() 43 | } 44 | } 45 | 46 | class MockCancellable: Cancellable { 47 | func cancel() { 48 | // do nothing 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Tests/ApolloDeveloperKitTests/TestHelpers/URLSessionConfiguration+Test.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLSessionConfiguration+Test.swift 3 | // ApolloDeveloperKitTests 4 | // 5 | // Created by Ryosuke Ito on 11/10/19. 6 | // Copyright © 2019 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension URLSessionConfiguration { 12 | static var test: URLSessionConfiguration { 13 | let configuration = URLSessionConfiguration.ephemeral 14 | configuration.httpMaximumConnectionsPerHost = 256 15 | configuration.requestCachePolicy = .reloadIgnoringLocalCacheData 16 | configuration.timeoutIntervalForRequest = 2 17 | return configuration 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Tests/ApolloDeveloperKitTests/TestHelpers/ifaddrs+Factory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ifaddrs+Factory.swift 3 | // ApolloDeveloperKitTests 4 | // 5 | // Created by Ryosuke Ito on 8/21/19. 6 | // Copyright © 2019 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import Darwin 10 | 11 | extension ifaddrs { 12 | init(name: UnsafeMutablePointer, flags: T, addr: UnsafeMutablePointer) { 13 | self.init(ifa_next: nil, 14 | ifa_name: name, 15 | ifa_flags: UInt32(flags), 16 | ifa_addr: addr, 17 | ifa_netmask: nil, 18 | ifa_dstaddr: nil, 19 | ifa_data: nil) 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /Tests/ApolloDeveloperKitTests/TestHelpers/sockaddr+Factory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // sockaddr+Factory.swift 3 | // ApolloDeveloperKitTests 4 | // 5 | // Created by Ryosuke Ito on 8/21/19. 6 | // Copyright © 2019 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import Darwin 10 | 11 | extension sockaddr { 12 | static func `in`(family: Int32, address: UInt32, port: Int) -> sockaddr { 13 | return unsafeBitCast(sockaddr_in(sin_len: __uint8_t(MemoryLayout.size), 14 | sin_family: sa_family_t(AF_INET), 15 | sin_port: in_port_t(port).bigEndian, 16 | sin_addr: in_addr(s_addr: address.bigEndian), 17 | sin_zero: (0, 0, 0, 0, 0, 0, 0, 0)), 18 | to: sockaddr.self) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Tests/ApolloDeveloperKitTests/WebServer/AddressInfoErrorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AddressInfoErrorTests.swift 3 | // ApolloDeveloperKitTests 4 | // 5 | // Created by Ryosuke Ito on 10/2/20. 6 | // Copyright © 2020 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import ApolloDeveloperKit 11 | 12 | class AddressInfoErrorTests: XCTestCase { 13 | func testInit_withInvalidValue() throws { 14 | XCTAssertNil(AddressInfoErrorCode(rawValue: 0)) 15 | XCTAssertNotNil(AddressInfoErrorCode(rawValue: 1)) 16 | XCTAssertNotNil(AddressInfoErrorCode(rawValue: EAI_MAX - 1)) 17 | XCTAssertNil(AddressInfoErrorCode(rawValue: EAI_MAX)) 18 | } 19 | 20 | func testCode() throws { 21 | let code = try XCTUnwrap(AddressInfoErrorCode(rawValue: EAI_NODATA)) 22 | let error = AddressInfoError(code) 23 | XCTAssertEqual(error.code.rawValue, code.rawValue) 24 | } 25 | 26 | func testLocalizedDescription() throws { 27 | let code = try XCTUnwrap(AddressInfoErrorCode(rawValue: EAI_NODATA)) 28 | let error = AddressInfoError(code) 29 | XCTAssertEqual(error.localizedDescription, "No address associated with nodename") 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Tests/ApolloDeveloperKitTests/WebServer/HTTPChunkedResponseTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPChunkedResponseTests.swift 3 | // ApolloDeveloperKitTests 4 | // 5 | // Created by Ryosuke Ito on 11/3/19. 6 | // Copyright © 2019 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import ApolloDeveloperKit 11 | 12 | class HTTPChunkedResponseTests: XCTestCase { 13 | func testData_withEmptyData() { 14 | let chunk = HTTPChunkedResponse(rawData: Data()) 15 | XCTAssertEqual(chunk.data, Data("0\r\n\r\n".utf8)) 16 | } 17 | 18 | func testData_withNonemptyData() { 19 | let chunk = HTTPChunkedResponse(rawData: Data("data: foo\n\n".utf8)) 20 | XCTAssertEqual(chunk.data, Data("b\r\ndata: foo\n\n\r\n".utf8)) 21 | } 22 | 23 | func testData_withEventStreamMessage() { 24 | let chunk = HTTPChunkedResponse(event: EventStreamMessage.ping) 25 | XCTAssertEqual(chunk.data, Data("3\r\n:\n\n\r\n".utf8)) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Tests/ApolloDeveloperKitTests/WebServer/HTTPServerErrorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPServerErrorTests.swift 3 | // ApolloDeveloperKitTests 4 | // 5 | // Created by Ryosuke Ito on 7/13/19. 6 | // Copyright © 2019 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import ApolloDeveloperKit 11 | 12 | class HTTPServerErrorTests: XCTestCase { 13 | 14 | func testErrorDomain() { 15 | XCTAssertEqual(HTTPServerError.errorDomain, "HTTPServerErrorDomain") 16 | } 17 | 18 | func testErrorCode() { 19 | XCTAssertEqual(HTTPServerError.multipleSocketErrorOccurred([:]).errorCode, 199) 20 | XCTAssertEqual(HTTPServerError.unsupportedBodyEncoding("chunked").errorCode, 200) 21 | } 22 | 23 | func testLocalizedDescription() { 24 | XCTAssertTrue((HTTPServerError.multipleSocketErrorOccurred([:]) as Error).localizedDescription.contains("Multiple")) 25 | XCTAssertTrue((HTTPServerError.unsupportedBodyEncoding("chunked") as Error).localizedDescription.contains("chunked")) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Tests/ApolloDeveloperKitTests/WebServer/InterfaceAddressIteratorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InterfaceAddressIteratorTests.swift 3 | // ApolloDeveloperKitTests 4 | // 5 | // Created by Ryosuke Ito on 8/20/19. 6 | // Copyright © 2019 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import ApolloDeveloperKit 11 | 12 | class InterfaceAddressIteratorTests: XCTestCase { 13 | private var interfaceAddressIterator: InterfaceAddressIterator! 14 | 15 | // We must manage memory by ourselves because they are out of ARC. 16 | private var socketAddress = sockaddr.in(family: AF_INET, address: INADDR_ANY, port: 80) 17 | private var en0Name = "en0".cString(using: .ascii)! 18 | private var en1Name = "en1".cString(using: .ascii)! 19 | private var en2Name = "en2".cString(using: .ascii)! 20 | private var en0: ifaddrs! 21 | private var en1: ifaddrs! 22 | private var en2: ifaddrs! 23 | 24 | override func setUp() { 25 | let flags = IFF_UP | IFF_BROADCAST | IFF_RUNNING | IFF_PROMISC | IFF_SIMPLEX | IFF_MULTICAST 26 | en0 = ifaddrs(name: &en0Name, flags: flags, addr: &socketAddress) 27 | en1 = ifaddrs(name: &en1Name, flags: flags, addr: &socketAddress) 28 | en2 = ifaddrs(name: &en2Name, flags: flags, addr: &socketAddress) 29 | withUnsafeMutablePointer(to: &en1) { en0.ifa_next = $0 } 30 | withUnsafeMutablePointer(to: &en2) { en1.ifa_next = $0 } 31 | self.interfaceAddressIterator = withUnsafeMutablePointer(to: &en0) { 32 | InterfaceAddressIterator(initialPointer: $0) { _ in } 33 | } 34 | } 35 | 36 | func testNext() { 37 | XCTAssertEqual("en0", interfaceAddressIterator.next()?.name) 38 | XCTAssertEqual("en1", interfaceAddressIterator.next()?.name) 39 | XCTAssertEqual("en2", interfaceAddressIterator.next()?.name) 40 | XCTAssertNil(interfaceAddressIterator.next()) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Tests/ApolloDeveloperKitTests/WebServer/InterfaceAddressTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InterfaceAddressTests.swift 3 | // ApolloDeveloperKitTests 4 | // 5 | // Created by Ryosuke Ito on 8/21/19. 6 | // Copyright © 2019 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import ApolloDeveloperKit 11 | 12 | class InterfaceAddressTests: XCTestCase { 13 | func testIsUp_whenTheInterfaceIsUp() { 14 | var name = "en0".cString(using: .ascii)! 15 | var socketAddress = sockaddr.in(family: AF_INET, address: INADDR_ANY, port: 80) 16 | let address = ifaddrs(name: &name, flags: IFF_UP, addr: &socketAddress) 17 | let interfaceAddress = InterfaceAddress(rawValue: address) 18 | XCTAssertTrue(interfaceAddress.isUp) 19 | } 20 | 21 | func testIsUp_whenTheInterfaceIsDown() { 22 | var name = "en0".cString(using: .ascii)! 23 | var socketAddress = sockaddr.in(family: AF_INET, address: INADDR_ANY, port: 80) 24 | let address = ifaddrs(name: &name, flags: 0, addr: &socketAddress) 25 | let interfaceAddress = InterfaceAddress(rawValue: address) 26 | XCTAssertFalse(interfaceAddress.isUp) 27 | } 28 | 29 | func testName() { 30 | var socketAddress = sockaddr.in(family: AF_INET, address: INADDR_ANY, port: 80) 31 | var name = "en0".cString(using: .ascii)! 32 | let address = ifaddrs(name: &name, flags: 0, addr: &socketAddress) 33 | let interfaceAddress = InterfaceAddress(rawValue: address) 34 | XCTAssertEqual("en0", interfaceAddress.name) 35 | } 36 | 37 | func testSocketFamily() { 38 | var name = "en0".cString(using: .ascii)! 39 | var socketAddress = sockaddr.in(family: AF_INET, address: INADDR_ANY, port: 80) 40 | let address = ifaddrs(name: &name, flags: 0, addr: &socketAddress) 41 | let interfaceAddress = InterfaceAddress(rawValue: address) 42 | XCTAssertEqual(socketAddress.sa_family, interfaceAddress.socketFamily) 43 | } 44 | 45 | func testIpv4Address_whenTheInterfaceAddressIsINADDR_ANY() { 46 | var name = "en0".cString(using: .ascii)! 47 | var socketAddress = sockaddr.in(family: AF_INET, address: INADDR_ANY, port: 80) 48 | let address = ifaddrs(name: &name, flags: 0, addr: &socketAddress) 49 | let interfaceAddress = InterfaceAddress(rawValue: address) 50 | XCTAssertEqual("0.0.0.0", interfaceAddress.hostName) 51 | } 52 | 53 | func testIpv4Address_whenTheInterfaceAddressIsINADDR_LOOPBACK() { 54 | var name = "en0".cString(using: .ascii)! 55 | var socketAddress = sockaddr.in(family: AF_INET, address: INADDR_LOOPBACK, port: 80) 56 | let address = ifaddrs(name: &name, flags: 0, addr: &socketAddress) 57 | let interfaceAddress = InterfaceAddress(rawValue: address) 58 | XCTAssertEqual("127.0.0.1", interfaceAddress.hostName) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Tests/ApolloDeveloperKitTests/WebServer/MIMETypeTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MIMETypeTests.swift 3 | // ApolloDeveloperKitTests 4 | // 5 | // Created by Ryosuke Ito on 11/1/19. 6 | // Copyright © 2019 Ryosuke Ito. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import ApolloDeveloperKit 11 | 12 | class MIMETypeTests: XCTestCase { 13 | func testInitWithPathExtensionEncoding_withHTML() { 14 | let mimeType = MIMEType(pathExtension: "html", encoding: .utf8) 15 | XCTAssertEqual(mimeType, .html(.utf8)) 16 | } 17 | 18 | func testInitWithPathExtensionEncoding_withJavaScript() { 19 | let mimeType = MIMEType(pathExtension: "js", encoding: nil) 20 | XCTAssertEqual(mimeType, .javascript) 21 | } 22 | 23 | func testInitWithPathExtensionEncoding_withJSON() { 24 | let mimeType = MIMEType(pathExtension: "json", encoding: nil) 25 | XCTAssertEqual(mimeType, .json) 26 | } 27 | 28 | func testInitWithPathExtensionEncoding_withCSS() { 29 | let mimeType = MIMEType(pathExtension: "css", encoding: nil) 30 | XCTAssertEqual(mimeType, .css) 31 | } 32 | 33 | func testInitWithPathExtensionEncoding_withPlainText() { 34 | let mimeType = MIMEType(pathExtension: "txt", encoding: .utf8) 35 | XCTAssertEqual(mimeType, .plainText(.utf8)) 36 | } 37 | 38 | func testInitWithPathExtensionEncoding_withPNG() { 39 | let mimeType = MIMEType(pathExtension: "png", encoding: nil) 40 | XCTAssertEqual(mimeType, .png) 41 | } 42 | 43 | func testInitWithPathExtensionEncoding_withUnknownFileExtension() { 44 | let mimeType = MIMEType(pathExtension: "zip", encoding: nil) 45 | XCTAssertEqual(mimeType, .octetStream) 46 | } 47 | 48 | func testDescription_withHTML() { 49 | XCTAssertEqual(String(describing: MIMEType.html(nil)), "text/html") 50 | } 51 | 52 | func testDescription_withHTMLSpecifyingCharacterSet() { 53 | XCTAssertEqual(String(describing: MIMEType.html(.utf8)), "text/html; charset=utf-8") 54 | } 55 | 56 | func testDescription_withJavaScript() { 57 | XCTAssertEqual(String(describing: MIMEType.javascript), "application/javascript") 58 | } 59 | 60 | func testDescription_withJSON() { 61 | XCTAssertEqual(String(describing: MIMEType.json), "application/json") 62 | } 63 | 64 | func testDescription_withCSS() { 65 | XCTAssertEqual(String(describing: MIMEType.css), "text/css") 66 | } 67 | 68 | func testDescription_withPlainText() { 69 | XCTAssertEqual(String(describing: MIMEType.plainText(nil)), "text/plain") 70 | } 71 | 72 | func testDescription_withPlainTextSpecifyingCharacterSet() { 73 | XCTAssertEqual(String(describing: MIMEType.plainText(.utf8)), "text/plain; charset=utf-8") 74 | } 75 | 76 | func testDescription_withPNG() { 77 | XCTAssertEqual(String(describing: MIMEType.png), "image/png") 78 | } 79 | 80 | func testDescription_withEventStream() { 81 | XCTAssertEqual(String(describing: MIMEType.eventStream), "text/event-stream") 82 | } 83 | 84 | func testDescription_withOctetStream() { 85 | XCTAssertEqual(String(describing: MIMEType.octetStream), "application/octet-stream") 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /bin/bump-version: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | if [ $# -ne 1 -o "$1" = '-h' -o "$1" = '--help' ]; then 6 | echo 'Usage: bump-version [-h|--help] ' >&2 7 | exit 1 8 | fi 9 | 10 | agvtool new-version "${1%.*}" 11 | agvtool new-marketing-version "$1" 12 | sed -i '' -E "/spec[.]version *=/s/[0-9]+[.][0-9]+[.][0-9]+/$1/" ApolloDeveloperKit.podspec 13 | sed -i '' -E "/pod 'ApolloDeveloperKit',/s/[0-9]+[.][0-9]+[.][0-9]+/$1/" README.md 14 | npm version --no-git-tag-version "$1" 15 | -------------------------------------------------------------------------------- /bin/support-new-apollo: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'fileutils' 4 | require 'rubygems/version' 5 | require 'tempfile' 6 | require 'yaml' 7 | 8 | def modify_file_inplace(path) 9 | tempfile = Tempfile.new(File.basename(path)) 10 | begin 11 | File.open(path, 'r') do |file| 12 | file.each_line do |line| 13 | tempfile.puts(yield(line)) 14 | end 15 | end 16 | tempfile.close 17 | FileUtils.mv(tempfile.path, path) 18 | ensure 19 | tempfile.close 20 | tempfile.unlink 21 | end 22 | end 23 | 24 | def git_working_directory_is_clean? 25 | `git status --untracked-files=no --porcelain`.empty? 26 | end 27 | 28 | def update_podspec(next_version) 29 | modify_file_inplace('ApolloDeveloperKit.podspec') do |line| 30 | (line =~ /spec\.dependency/) ? line.sub(/< \d+\.\d+\.\d+/, "< #{next_version}") : line 31 | end 32 | end 33 | 34 | def update_readme(next_version) 35 | modify_file_inplace('README.md') do |line| 36 | (line =~ /- \[Apollo iOS\]/) ? line.sub(/< \d+\.\d+\.\d+/, "< #{next_version}") : line 37 | end 38 | end 39 | 40 | def update_test_workflow(version) 41 | path = '.github/workflows/test.yml' 42 | yaml = YAML.load_file(path) 43 | matrix = yaml['jobs']['unit-test']['strategy']['matrix'] 44 | matrix['apollo'].unshift(version) 45 | includes = matrix['include'] 46 | # iOS test conditions 47 | new_params = includes[0].dup 48 | new_params['apollo'] = version 49 | includes.unshift(new_params) 50 | # MacOS test conditions 51 | new_params_index = includes.index { |item| item['sdk'] == 'macosx' } 52 | new_params = includes[new_params_index].dup 53 | new_params['apollo'] = version 54 | includes.insert(new_params_index, new_params) 55 | # Write to file 56 | File.open(path, 'w') do |file| 57 | YAML.dump(yaml, file, line_width: 200) 58 | end 59 | modify_file_inplace(path) do |line| 60 | line.sub(/^true:/, 'on:') 61 | end 62 | end 63 | 64 | if ARGV.size != 1 || %w(-h --help).include?(ARGV[0]) 65 | STDERR.puts 'Usage: support-new-apollo [-h|--help] ' 66 | exit(1) 67 | end 68 | 69 | raise 'Git working directory is not clean' unless git_working_directory_is_clean? 70 | 71 | version = Gem::Version.new(ARGV[0]) 72 | next_version = version.bump.to_s + '.0' 73 | 74 | update_podspec(next_version) 75 | update_readme(next_version) 76 | update_test_workflow(version.to_s) 77 | -------------------------------------------------------------------------------- /docs/badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | documentation 17 | 18 | 19 | documentation 20 | 21 | 22 | 67% 23 | 24 | 25 | 67% 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /docs/docsets/ApolloDeveloperKit.docset/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleIdentifier 6 | com.jazzy.apollodeveloperkit 7 | CFBundleName 8 | ApolloDeveloperKit 9 | DocSetPlatformFamily 10 | apollodeveloperkit 11 | isDashDocset 12 | 13 | dashIndexFilePath 14 | index.html 15 | isJavaScriptEnabled 16 | 17 | DashDocSetFamily 18 | dashtoc 19 | 20 | 21 | -------------------------------------------------------------------------------- /docs/docsets/ApolloDeveloperKit.docset/Contents/Resources/Documents/badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | documentation 17 | 18 | 19 | documentation 20 | 21 | 22 | 66% 23 | 24 | 25 | 66% 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /docs/docsets/ApolloDeveloperKit.docset/Contents/Resources/Documents/img/carat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manicmaniac/ApolloDeveloperKit/c1c1642035f2274064c682081d0d0c141295821d/docs/docsets/ApolloDeveloperKit.docset/Contents/Resources/Documents/img/carat.png -------------------------------------------------------------------------------- /docs/docsets/ApolloDeveloperKit.docset/Contents/Resources/Documents/img/dash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manicmaniac/ApolloDeveloperKit/c1c1642035f2274064c682081d0d0c141295821d/docs/docsets/ApolloDeveloperKit.docset/Contents/Resources/Documents/img/dash.png -------------------------------------------------------------------------------- /docs/docsets/ApolloDeveloperKit.docset/Contents/Resources/Documents/img/gh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manicmaniac/ApolloDeveloperKit/c1c1642035f2274064c682081d0d0c141295821d/docs/docsets/ApolloDeveloperKit.docset/Contents/Resources/Documents/img/gh.png -------------------------------------------------------------------------------- /docs/docsets/ApolloDeveloperKit.docset/Contents/Resources/Documents/img/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manicmaniac/ApolloDeveloperKit/c1c1642035f2274064c682081d0d0c141295821d/docs/docsets/ApolloDeveloperKit.docset/Contents/Resources/Documents/img/spinner.gif -------------------------------------------------------------------------------- /docs/docsets/ApolloDeveloperKit.docset/Contents/Resources/Documents/js/jazzy.js: -------------------------------------------------------------------------------- 1 | window.jazzy = {'docset': false} 2 | if (typeof window.dash != 'undefined') { 3 | document.documentElement.className += ' dash' 4 | window.jazzy.docset = true 5 | } 6 | if (navigator.userAgent.match(/xcode/i)) { 7 | document.documentElement.className += ' xcode' 8 | window.jazzy.docset = true 9 | } 10 | 11 | function toggleItem($link, $content) { 12 | var animationDuration = 300; 13 | $link.toggleClass('token-open'); 14 | $content.slideToggle(animationDuration); 15 | } 16 | 17 | function itemLinkToContent($link) { 18 | return $link.parent().parent().next(); 19 | } 20 | 21 | // On doc load + hash-change, open any targetted item 22 | function openCurrentItemIfClosed() { 23 | if (window.jazzy.docset) { 24 | return; 25 | } 26 | var $link = $(`a[name="${location.hash.substring(1)}"]`).nextAll('.token'); 27 | $content = itemLinkToContent($link); 28 | if ($content.is(':hidden')) { 29 | toggleItem($link, $content); 30 | } 31 | } 32 | 33 | $(openCurrentItemIfClosed); 34 | $(window).on('hashchange', openCurrentItemIfClosed); 35 | 36 | // On item link ('token') click, toggle its discussion 37 | $('.token').on('click', function(event) { 38 | if (window.jazzy.docset) { 39 | return; 40 | } 41 | var $link = $(this); 42 | toggleItem($link, itemLinkToContent($link)); 43 | 44 | // Keeps the document from jumping to the hash. 45 | var href = $link.attr('href'); 46 | if (history.pushState) { 47 | history.pushState({}, '', href); 48 | } else { 49 | location.hash = href; 50 | } 51 | event.preventDefault(); 52 | }); 53 | 54 | // Clicks on links to the current, closed, item need to open the item 55 | $("a:not('.token')").on('click', function() { 56 | if (location == this.href) { 57 | openCurrentItemIfClosed(); 58 | } 59 | }); 60 | 61 | // KaTeX rendering 62 | if ("katex" in window) { 63 | $($('.math').each( (_, element) => { 64 | katex.render(element.textContent, element, { 65 | displayMode: $(element).hasClass('m-block'), 66 | throwOnError: false, 67 | trust: true 68 | }); 69 | })) 70 | } 71 | -------------------------------------------------------------------------------- /docs/docsets/ApolloDeveloperKit.docset/Contents/Resources/Documents/js/jazzy.search.js: -------------------------------------------------------------------------------- 1 | $(function(){ 2 | var $typeahead = $('[data-typeahead]'); 3 | var $form = $typeahead.parents('form'); 4 | var searchURL = $form.attr('action'); 5 | 6 | function displayTemplate(result) { 7 | return result.name; 8 | } 9 | 10 | function suggestionTemplate(result) { 11 | var t = '
'; 12 | t += '' + result.name + ''; 13 | if (result.parent_name) { 14 | t += '' + result.parent_name + ''; 15 | } 16 | t += '
'; 17 | return t; 18 | } 19 | 20 | $typeahead.one('focus', function() { 21 | $form.addClass('loading'); 22 | 23 | $.getJSON(searchURL).then(function(searchData) { 24 | const searchIndex = lunr(function() { 25 | this.ref('url'); 26 | this.field('name'); 27 | this.field('abstract'); 28 | for (const [url, doc] of Object.entries(searchData)) { 29 | this.add({url: url, name: doc.name, abstract: doc.abstract}); 30 | } 31 | }); 32 | 33 | $typeahead.typeahead( 34 | { 35 | highlight: true, 36 | minLength: 3, 37 | autoselect: true 38 | }, 39 | { 40 | limit: 10, 41 | display: displayTemplate, 42 | templates: { suggestion: suggestionTemplate }, 43 | source: function(query, sync) { 44 | const lcSearch = query.toLowerCase(); 45 | const results = searchIndex.query(function(q) { 46 | q.term(lcSearch, { boost: 100 }); 47 | q.term(lcSearch, { 48 | boost: 10, 49 | wildcard: lunr.Query.wildcard.TRAILING 50 | }); 51 | }).map(function(result) { 52 | var doc = searchData[result.ref]; 53 | doc.url = result.ref; 54 | return doc; 55 | }); 56 | sync(results); 57 | } 58 | } 59 | ); 60 | $form.removeClass('loading'); 61 | $typeahead.trigger('focus'); 62 | }); 63 | }); 64 | 65 | var baseURL = searchURL.slice(0, -"search.json".length); 66 | 67 | $typeahead.on('typeahead:select', function(e, result) { 68 | window.location = baseURL + result.url; 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /docs/docsets/ApolloDeveloperKit.docset/Contents/Resources/docSet.dsidx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manicmaniac/ApolloDeveloperKit/c1c1642035f2274064c682081d0d0c141295821d/docs/docsets/ApolloDeveloperKit.docset/Contents/Resources/docSet.dsidx -------------------------------------------------------------------------------- /docs/docsets/ApolloDeveloperKit.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manicmaniac/ApolloDeveloperKit/c1c1642035f2274064c682081d0d0c141295821d/docs/docsets/ApolloDeveloperKit.tgz -------------------------------------------------------------------------------- /docs/img/carat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manicmaniac/ApolloDeveloperKit/c1c1642035f2274064c682081d0d0c141295821d/docs/img/carat.png -------------------------------------------------------------------------------- /docs/img/dash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manicmaniac/ApolloDeveloperKit/c1c1642035f2274064c682081d0d0c141295821d/docs/img/dash.png -------------------------------------------------------------------------------- /docs/img/gh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manicmaniac/ApolloDeveloperKit/c1c1642035f2274064c682081d0d0c141295821d/docs/img/gh.png -------------------------------------------------------------------------------- /docs/img/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manicmaniac/ApolloDeveloperKit/c1c1642035f2274064c682081d0d0c141295821d/docs/img/spinner.gif -------------------------------------------------------------------------------- /docs/js/jazzy.js: -------------------------------------------------------------------------------- 1 | window.jazzy = {'docset': false} 2 | if (typeof window.dash != 'undefined') { 3 | document.documentElement.className += ' dash' 4 | window.jazzy.docset = true 5 | } 6 | if (navigator.userAgent.match(/xcode/i)) { 7 | document.documentElement.className += ' xcode' 8 | window.jazzy.docset = true 9 | } 10 | 11 | function toggleItem($link, $content) { 12 | var animationDuration = 300; 13 | $link.toggleClass('token-open'); 14 | $content.slideToggle(animationDuration); 15 | } 16 | 17 | function itemLinkToContent($link) { 18 | return $link.parent().parent().next(); 19 | } 20 | 21 | // On doc load + hash-change, open any targetted item 22 | function openCurrentItemIfClosed() { 23 | if (window.jazzy.docset) { 24 | return; 25 | } 26 | var $link = $(`a[name="${location.hash.substring(1)}"]`).nextAll('.token'); 27 | $content = itemLinkToContent($link); 28 | if ($content.is(':hidden')) { 29 | toggleItem($link, $content); 30 | } 31 | } 32 | 33 | $(openCurrentItemIfClosed); 34 | $(window).on('hashchange', openCurrentItemIfClosed); 35 | 36 | // On item link ('token') click, toggle its discussion 37 | $('.token').on('click', function(event) { 38 | if (window.jazzy.docset) { 39 | return; 40 | } 41 | var $link = $(this); 42 | toggleItem($link, itemLinkToContent($link)); 43 | 44 | // Keeps the document from jumping to the hash. 45 | var href = $link.attr('href'); 46 | if (history.pushState) { 47 | history.pushState({}, '', href); 48 | } else { 49 | location.hash = href; 50 | } 51 | event.preventDefault(); 52 | }); 53 | 54 | // Clicks on links to the current, closed, item need to open the item 55 | $("a:not('.token')").on('click', function() { 56 | if (location == this.href) { 57 | openCurrentItemIfClosed(); 58 | } 59 | }); 60 | 61 | // KaTeX rendering 62 | if ("katex" in window) { 63 | $($('.math').each( (_, element) => { 64 | katex.render(element.textContent, element, { 65 | displayMode: $(element).hasClass('m-block'), 66 | throwOnError: false, 67 | trust: true 68 | }); 69 | })) 70 | } 71 | -------------------------------------------------------------------------------- /docs/js/jazzy.search.js: -------------------------------------------------------------------------------- 1 | $(function(){ 2 | var $typeahead = $('[data-typeahead]'); 3 | var $form = $typeahead.parents('form'); 4 | var searchURL = $form.attr('action'); 5 | 6 | function displayTemplate(result) { 7 | return result.name; 8 | } 9 | 10 | function suggestionTemplate(result) { 11 | var t = '
'; 12 | t += '' + result.name + ''; 13 | if (result.parent_name) { 14 | t += '' + result.parent_name + ''; 15 | } 16 | t += '
'; 17 | return t; 18 | } 19 | 20 | $typeahead.one('focus', function() { 21 | $form.addClass('loading'); 22 | 23 | $.getJSON(searchURL).then(function(searchData) { 24 | const searchIndex = lunr(function() { 25 | this.ref('url'); 26 | this.field('name'); 27 | this.field('abstract'); 28 | for (const [url, doc] of Object.entries(searchData)) { 29 | this.add({url: url, name: doc.name, abstract: doc.abstract}); 30 | } 31 | }); 32 | 33 | $typeahead.typeahead( 34 | { 35 | highlight: true, 36 | minLength: 3, 37 | autoselect: true 38 | }, 39 | { 40 | limit: 10, 41 | display: displayTemplate, 42 | templates: { suggestion: suggestionTemplate }, 43 | source: function(query, sync) { 44 | const lcSearch = query.toLowerCase(); 45 | const results = searchIndex.query(function(q) { 46 | q.term(lcSearch, { boost: 100 }); 47 | q.term(lcSearch, { 48 | boost: 10, 49 | wildcard: lunr.Query.wildcard.TRAILING 50 | }); 51 | }).map(function(result) { 52 | var doc = searchData[result.ref]; 53 | doc.url = result.ref; 54 | return doc; 55 | }); 56 | sync(results); 57 | } 58 | } 59 | ); 60 | $form.removeClass('loading'); 61 | $typeahead.trigger('focus'); 62 | }); 63 | }); 64 | 65 | var baseURL = searchURL.slice(0, -"search.json".length); 66 | 67 | $typeahead.on('typeahead:select', function(e, result) { 68 | window.location = baseURL + result.url; 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apollodeveloperkit", 3 | "version": "0.15.0", 4 | "description": "Visual debug your app, that is based on Apollo iOS", 5 | "repository": "https://github.com/manicmaniac/ApolloDeveloperKit", 6 | "author": "Ryosuke Ito ", 7 | "license": "MIT", 8 | "private": true, 9 | "module": "src/index.ts", 10 | "sideEffects": [ 11 | "src/index.ts" 12 | ], 13 | "scripts": { 14 | "build": "webpack", 15 | "lint": "eslint . --ext .ts", 16 | "test": "jest", 17 | "generate:type": "npm run generate:type:swift & npm run generate:type:typescript & wait", 18 | "generate:type:swift": "quicktype -l swift --just-types -t Schema -s schema -o Sources/ApolloDeveloperKit/Schema/Schema.swift ApolloDeveloperKit.schema.json", 19 | "generate:type:typescript": "quicktype -l typescript --just-types -t Schema -s schema -o src/schema.ts ApolloDeveloperKit.schema.json" 20 | }, 21 | "devDependencies": { 22 | "@types/jest": "^26.0.10", 23 | "@typescript-eslint/eslint-plugin": "^3.10.1", 24 | "@typescript-eslint/parser": "^3.10.1", 25 | "apollo-cache": "^1.3.5", 26 | "apollo-client": "^2.6.10", 27 | "apollo-client-devtools": "^2.3.1", 28 | "eslint": "^7.7.0", 29 | "eventsourcemock": "^2.0.0", 30 | "graphql": "^15.3.0", 31 | "jest": "^26.4.2", 32 | "quicktype": "^15.0.256", 33 | "ts-jest": "^26.3.0", 34 | "ts-loader": "^8.0.3", 35 | "typescript": "^4.0.2", 36 | "webpack": "^4.44.1", 37 | "webpack-cli": "^3.3.12" 38 | }, 39 | "eslintConfig": { 40 | "root": true, 41 | "parser": "@typescript-eslint/parser", 42 | "plugins": [ 43 | "@typescript-eslint" 44 | ], 45 | "extends": [ 46 | "eslint:recommended", 47 | "plugin:@typescript-eslint/recommended" 48 | ], 49 | "ignorePatterns": [ 50 | "Carthage", 51 | "node_modules", 52 | "src/schema.ts" 53 | ], 54 | "rules": { 55 | "@typescript-eslint/no-unused-vars": [ 56 | "warn", 57 | { 58 | "argsIgnorePattern": "^_" 59 | } 60 | ] 61 | } 62 | }, 63 | "jest": { 64 | "preset": "ts-jest/presets/js-with-ts", 65 | "testEnvironment": "jsdom", 66 | "transformIgnorePatterns": [ 67 | "node_modules/(?!(apollo-client-devtools)/)" 68 | ] 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/ApolloCachePretender.ts: -------------------------------------------------------------------------------- 1 | import type { Cache, Transaction } from 'apollo-cache' 2 | import { ApolloCache } from 'apollo-cache' 3 | 4 | type CacheObject = Record 5 | 6 | export default class ApolloCachePretender extends ApolloCache { 7 | private onExtract?: () => void 8 | 9 | constructor(onExtract?: () => void) { 10 | super() 11 | this.onExtract = onExtract 12 | } 13 | 14 | read(_query: Cache.ReadOptions): null { 15 | return null 16 | } 17 | 18 | write(_write: Cache.WriteOptions): void { 19 | // do nothing 20 | } 21 | 22 | diff(_query: Cache.DiffOptions): Cache.DiffResult { 23 | return {} 24 | } 25 | 26 | watch(_watch: Cache.WatchOptions): () => void { 27 | return () => { /* do nothing */} 28 | } 29 | 30 | evict(_query: Cache.EvictOptions): Cache.EvictionResult { 31 | return { success: true } 32 | } 33 | 34 | async reset(): Promise { 35 | // do nothing 36 | } 37 | 38 | restore(_serializedState: unknown): this { 39 | return this 40 | } 41 | 42 | extract(_optimistic = false): CacheObject { 43 | this.onExtract?.() 44 | return {} 45 | } 46 | 47 | removeOptimistic(_id: string): void { 48 | // do nothing 49 | } 50 | 51 | performTransaction(transaction: Transaction): void { 52 | transaction(this) 53 | } 54 | 55 | recordOptimisticTransaction(transaction: Transaction, _id: string): void { 56 | transaction(this) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/ApolloClientPretender.ts: -------------------------------------------------------------------------------- 1 | import type { FetchResult, Operation as LinkOperation } from 'apollo-link' 2 | import type { DataProxy } from 'apollo-cache' 3 | import type { StateChange as DevtoolsStateChange } from 'apollo-client-devtools' 4 | import type { ConsoleEvent, Operation, StateChange as DeveloperKitStateChange } from './schema' 5 | import assert from 'assert' 6 | import { parse } from 'graphql/language/parser' 7 | import { print } from 'graphql/language/printer' 8 | import { ApolloLink, fromPromise } from 'apollo-link' 9 | import ApolloCachePretender from './ApolloCachePretender' 10 | import { ConsoleEventType } from './schema' 11 | 12 | export default class ApolloClientPretender implements DataProxy { 13 | readonly version = '2.0.0' 14 | readonly link = new ApolloLink((operation) => fromPromise(requestOperation(operation))) 15 | readonly cache = new ApolloCachePretender(this.startListening.bind(this)) 16 | 17 | private devToolsHookCb?: (event: DevtoolsStateChange) => void 18 | private eventSource?: EventSource 19 | 20 | readQuery = this.cache.readQuery.bind(this.cache) 21 | readFragment = this.cache.readFragment.bind(this.cache) 22 | writeQuery = this.cache.writeQuery.bind(this.cache) 23 | writeFragment = this.cache.writeFragment.bind(this.cache) 24 | writeData = this.cache.writeData.bind(this.cache) 25 | 26 | startListening(): void { 27 | this.eventSource = new EventSource('/events') 28 | this.eventSource.onmessage = message => { 29 | const event = JSON.parse(message.data) as DeveloperKitStateChange 30 | const newEvent = translateApolloStateChangeEvent(event) 31 | this.devToolsHookCb?.(newEvent) 32 | } 33 | this.eventSource.addEventListener('stdout', event => onConsoleEventReceived(event)) 34 | this.eventSource.addEventListener('stderr', event => onConsoleEventReceived(event)) 35 | } 36 | 37 | stopListening(): void { 38 | this.eventSource?.close() 39 | } 40 | 41 | __actionHookForDevTools(cb: (event: DevtoolsStateChange) => void): void { 42 | this.devToolsHookCb = cb 43 | } 44 | } 45 | 46 | async function requestOperation(operation: LinkOperation): Promise { 47 | const body: Operation = { 48 | variables: operation.variables, 49 | operationName: operation.operationName, 50 | query: print(operation.query) 51 | } 52 | const options: RequestInit = { 53 | method: 'POST', 54 | headers: { 'Content-Type': 'application/json' }, 55 | body: JSON.stringify(body) 56 | } 57 | const response = await fetch('/request', options) 58 | if (!response.ok) { 59 | throw new Error(response.statusText) 60 | } 61 | return await response.json() 62 | } 63 | 64 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 65 | function isConsoleEvent(object: any): object is ConsoleEvent { 66 | return Object.values(ConsoleEventType).includes(object?.type) && ((typeof object?.data === 'string') || object?.data instanceof String) 67 | } 68 | 69 | const consoleEventTypeColorMap: Readonly> = Object.freeze({ 70 | 'stdout': 'cadetblue', 71 | 'stderr': 'tomato' 72 | }) 73 | 74 | function onConsoleEventReceived(event: Event): void { 75 | assert(isConsoleEvent(event)) 76 | console.log(`%c${event.data}`, `color: ${consoleEventTypeColorMap[event.type]}`) 77 | } 78 | 79 | function translateApolloStateChangeEvent(event: DeveloperKitStateChange): DevtoolsStateChange { 80 | return { 81 | ...event, 82 | state: { 83 | queries: event.state.queries.map(query => ({ 84 | ...query, 85 | document: parse(query.document) 86 | })), 87 | mutations: event.state.mutations.map(mutation => ({ 88 | ...mutation, 89 | mutation: parse(mutation.mutation) 90 | })) 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/__tests__/ApolloCachePretender.test.ts: -------------------------------------------------------------------------------- 1 | import type { DocumentNode } from 'graphql' 2 | import type { Transaction } from 'apollo-cache' 3 | import ApolloCachePretender from '../ApolloCachePretender' 4 | 5 | describe('ApolloCachePretender', () => { 6 | const query: DocumentNode = { 7 | kind: 'Document', 8 | definitions: [] 9 | } 10 | 11 | let cache: ApolloCachePretender 12 | const onExtract = jest.fn() 13 | 14 | beforeEach(() => { 15 | cache = new ApolloCachePretender(onExtract) 16 | onExtract.mockClear() 17 | }) 18 | 19 | describe('#extract', () => { 20 | it('returns some object', () => { 21 | expect(cache.extract()).toStrictEqual({}) 22 | }) 23 | 24 | it('invokes the callback', () => { 25 | cache.extract() 26 | expect(onExtract).toHaveBeenCalledTimes(1) 27 | }) 28 | }) 29 | 30 | describe('#read', () => { 31 | it('returns null', () => { 32 | expect(cache.read({ query, optimistic: true })).toBeNull() 33 | }) 34 | }) 35 | 36 | describe('#write', () => { 37 | it('does not throw any error', () => { 38 | expect(() => cache.write({ query, dataId: '', result: ''})).not.toThrow() 39 | }) 40 | }) 41 | 42 | describe('#diff', () => { 43 | it('returns empty object', () => { 44 | expect(cache.diff({ query, optimistic: true })).toMatchObject({}) 45 | }) 46 | }) 47 | 48 | describe('#watch', () => { 49 | it('returns empty thunk', () => { 50 | const callback = jest.fn() 51 | expect(cache.watch({ query, callback, optimistic: true })).toBeInstanceOf(Function) 52 | expect(callback).not.toHaveBeenCalled() 53 | }) 54 | }) 55 | 56 | describe('#evict', () => { 57 | it('tells success', () => { 58 | expect(cache.evict({ query })).toMatchObject({ success: true }) 59 | }) 60 | }) 61 | 62 | describe('#reset', () => { 63 | it('returns an empty promise', async () => { 64 | expect(async () => await cache.reset()).not.toThrow() 65 | }) 66 | }) 67 | 68 | describe('#restore', () => { 69 | it('returns itself', () => { 70 | expect(cache.restore(null)).toBe(cache) 71 | }) 72 | }) 73 | 74 | describe('#removeOptimistic', () => { 75 | it('does not throw any error', () => { 76 | expect(() => cache.removeOptimistic('')).not.toThrowError() 77 | }) 78 | }) 79 | 80 | describe('#performTransaction', () => { 81 | it('does not throw any error', () => { 82 | const transaction: Transaction = jest.fn() 83 | expect(() => cache.performTransaction(transaction)).not.toThrowError() 84 | expect(transaction).toHaveBeenCalledTimes(1) 85 | }) 86 | }) 87 | 88 | describe('#recordOptimisticTransaction', () => { 89 | it('does not throw any error', () => { 90 | const transaction: Transaction = jest.fn() 91 | expect(() => cache.recordOptimisticTransaction(transaction, '')).not.toThrowError() 92 | expect(transaction).toHaveBeenCalledTimes(1) 93 | }) 94 | }) 95 | }) 96 | -------------------------------------------------------------------------------- /src/__tests__/ApolloClientPretender.test.ts: -------------------------------------------------------------------------------- 1 | import type { DataProxy } from 'apollo-cache' 2 | import type { DocumentNode } from 'graphql' 3 | import EventSourceMock, { sources } from 'eventsourcemock' 4 | import { mocked } from 'ts-jest/utils' 5 | import ApolloClientPretender from '../ApolloClientPretender' 6 | import ApolloCachePretender from '../ApolloCachePretender' 7 | 8 | jest.mock('../ApolloCachePretender') 9 | jest.spyOn(console, 'log') 10 | 11 | describe('ApolloClientPretender', () => { 12 | const query: DocumentNode = { 13 | kind: 'Document', 14 | definitions: [] 15 | } 16 | 17 | let originalEventSource: typeof EventSource 18 | let client: ApolloClientPretender 19 | let cache: ApolloCachePretender 20 | 21 | beforeEach(() => { 22 | mocked(ApolloCachePretender).mockClear() 23 | mocked(console.log).mockClear() 24 | originalEventSource = window.EventSource 25 | window.EventSource = EventSourceMock as typeof EventSource 26 | client = new ApolloClientPretender() 27 | cache = mocked(ApolloCachePretender).mock.instances[0] 28 | }) 29 | 30 | afterEach(() => { 31 | window.EventSource = originalEventSource 32 | }) 33 | 34 | describe('#version', () => { 35 | it('is 2.0.0', () => { 36 | expect(client.version).toBe('2.0.0') 37 | }) 38 | }) 39 | 40 | describe('#cache', () => { 41 | it('returns its own cache', () => { 42 | expect(client.cache).toStrictEqual(cache) 43 | }) 44 | }) 45 | 46 | describe('#readQuery', () => { 47 | it('proxies method call to its own cache', () => { 48 | const options: DataProxy.Query = { query } 49 | client.readQuery(options) 50 | expect(cache.readQuery).toHaveBeenCalledWith(options) 51 | }) 52 | }) 53 | 54 | describe('#readFragment', () => { 55 | it('proxies method call to its own cache', () => { 56 | const options: DataProxy.Fragment = { 57 | id: '', 58 | fragment: query 59 | } 60 | client.readFragment(options) 61 | expect(cache.readFragment).toHaveBeenCalledWith(options) 62 | }) 63 | }) 64 | 65 | describe('#writeQuery', () => { 66 | it('proxies method call to its own cache', () => { 67 | const options: DataProxy.WriteQueryOptions = { 68 | query, 69 | data: '' 70 | } 71 | client.writeQuery(options) 72 | expect(cache.writeQuery).toHaveBeenCalledWith(options) 73 | }) 74 | }) 75 | 76 | describe('#writeFragment', () => { 77 | it('proxies method call to its own cache', () => { 78 | const options: DataProxy.WriteFragmentOptions = { 79 | id: '', 80 | fragment: query, 81 | data: '' 82 | } 83 | client.writeFragment(options) 84 | expect(cache.writeFragment).toHaveBeenCalledWith(options) 85 | }) 86 | }) 87 | 88 | describe('#writeData', () => { 89 | it('proxies method call to its own cache', () => { 90 | const options: DataProxy.WriteDataOptions = { data: '' } 91 | client.writeData(options) 92 | expect(cache.writeData).toHaveBeenCalledWith(options) 93 | }) 94 | }) 95 | 96 | describe('#startListening', () => { 97 | const hook = jest.fn() 98 | 99 | beforeEach(() => { 100 | hook.mockClear() 101 | client.__actionHookForDevTools(hook) 102 | client.startListening() 103 | }) 104 | 105 | afterEach(() => { 106 | client.stopListening() 107 | }) 108 | 109 | it('starts event source', () => { 110 | expect(sources['/events'].readyState).toBe(0) 111 | }) 112 | 113 | it('calls hook callback when it receives state change event', () => { 114 | const data = { 115 | state: { 116 | queries: [], 117 | mutations: [] 118 | }, 119 | dataWithOptimisticResults: {} 120 | } 121 | const event = { 122 | type: 'message', 123 | data: JSON.stringify(data) 124 | } as MessageEvent 125 | sources['/events'].emitMessage(event) 126 | expect(hook).toHaveBeenCalledWith(data) 127 | }) 128 | 129 | it('writes data to console when it receives stdout event', () => { 130 | const event = { 131 | type: 'stdout', 132 | data: 'blah', 133 | } as MessageEvent 134 | sources['/events'].emit('stdout', event) 135 | expect(console.log).toHaveBeenCalledWith('%cblah', 'color: cadetblue') 136 | }) 137 | 138 | it('writes data to console when it receives stderr event', () => { 139 | const event = { 140 | type: 'stderr', 141 | data: 'blah', 142 | } as MessageEvent 143 | sources['/events'].emit('stderr', event) 144 | expect(console.log).toHaveBeenCalledWith('%cblah', 'color: tomato') 145 | }) 146 | }) 147 | 148 | describe('#stopListening', () => { 149 | it('stops event source', () => { 150 | client.startListening() 151 | client.stopListening() 152 | expect(sources['/events'].readyState).toBe(2) 153 | }) 154 | }) 155 | }) 156 | -------------------------------------------------------------------------------- /src/__tests__/IntegrationTests.test.ts: -------------------------------------------------------------------------------- 1 | import type { /* global */ } from 'apollo-client-devtools' 2 | import Bridge from 'apollo-client-devtools/src/bridge' 3 | import { initBackend } from 'apollo-client-devtools/src/backend' 4 | import { installHook } from 'apollo-client-devtools/src/backend/hook' 5 | import ApolloClientPretender from '../ApolloClientPretender' 6 | 7 | describe('integration', () => { 8 | let bridge: Bridge 9 | 10 | beforeAll(done => { 11 | window.__APOLLO_CLIENT__ = new ApolloClientPretender() 12 | bridge = new Bridge({ 13 | listen(fn) { 14 | const listener = (evt: MessageEvent) => { 15 | fn(evt.data.payload) 16 | } 17 | window.addEventListener('message', listener) 18 | }, 19 | send(payload) { 20 | window.postMessage({ payload }, '*') 21 | } 22 | }) 23 | installHook(window, 'test') 24 | setTimeout(done, 1000) // Wait until hook finds ApolloClient 25 | }) 26 | 27 | describe('#initBackend', () => { 28 | it('emits `ready` event', done => { 29 | bridge.addListener('ready', (message: string) => { 30 | expect(message).toBe('2.0.0') 31 | done() 32 | }) 33 | initBackend(bridge, window.__APOLLO_DEVTOOLS_GLOBAL_HOOK__, window.localStorage) 34 | }) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { /* global */ } from 'apollo-client-devtools' 2 | import ApolloClientPretender from "./ApolloClientPretender" 3 | 4 | window.__APOLLO_CLIENT__ = new ApolloClientPretender() 5 | -------------------------------------------------------------------------------- /src/schema.ts: -------------------------------------------------------------------------------- 1 | export interface Schema { 2 | /** 3 | * Console event pushed from client to server. 4 | */ 5 | consoleEvent?: ConsoleEvent; 6 | /** 7 | * GraphQL operation request passed from client to server. 8 | */ 9 | operation?: Operation; 10 | /** 11 | * State change event pushed from server to client. 12 | */ 13 | stateChange?: StateChange; 14 | } 15 | 16 | /** 17 | * Console event pushed from client to server. 18 | */ 19 | export interface ConsoleEvent { 20 | data: string; 21 | type: ConsoleEventType; 22 | } 23 | 24 | export enum ConsoleEventType { 25 | Stderr = "stderr", 26 | Stdout = "stdout", 27 | } 28 | 29 | /** 30 | * GraphQL operation request passed from client to server. 31 | */ 32 | export interface Operation { 33 | operationIdentifier?: string; 34 | operationName?: string; 35 | query: string; 36 | variables?: { [key: string]: any }; 37 | } 38 | 39 | /** 40 | * State change event pushed from server to client. 41 | */ 42 | export interface StateChange { 43 | dataWithOptimisticResults: { [key: string]: any }; 44 | state: State; 45 | } 46 | 47 | export interface State { 48 | mutations: Mutation[]; 49 | queries: Query[]; 50 | } 51 | 52 | export interface Mutation { 53 | error?: ErrorLike; 54 | loading: boolean; 55 | mutation: string; 56 | variables?: { [key: string]: any }; 57 | } 58 | 59 | /** 60 | * JavaScript error serialized to JSON. 61 | */ 62 | export interface ErrorLike { 63 | columnNumber?: number; 64 | fileName?: string; 65 | lineNumber?: number; 66 | message: string; 67 | name: string; 68 | } 69 | 70 | export interface Query { 71 | document: string; 72 | graphQLErrors?: ErrorLike[]; 73 | networkError?: ErrorLike; 74 | previousVariables?: { [key: string]: any }; 75 | variables?: { [key: string]: any }; 76 | } 77 | -------------------------------------------------------------------------------- /src/types/apollo-client-devtools/index.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for apollo-client-devtools 2.2.5 2 | // Project: https://github.com/apollographql/apollo-client-devtools 3 | // Definitions by: Ryosuke Ito 4 | // TypeScript Version: 3.5.2 5 | 6 | import { Hook } from 'apollo-client-devtools/src/backend/hook' 7 | import { DocumentNode } from 'apollo-link' 8 | 9 | declare global { 10 | interface Window { 11 | __APOLLO_CLIENT__: unknown 12 | __APOLLO_DEVTOOLS_GLOBAL_HOOK__: Hook 13 | } 14 | } 15 | 16 | type Variables = Record 17 | 18 | type CacheStorage = Record 19 | 20 | type Query = { 21 | document: DocumentNode, 22 | variables?: Variables, 23 | previousVariables?: Variables, 24 | networkError?: Error, 25 | graphQLErrors?: Error[] 26 | } 27 | 28 | type Mutation = { 29 | mutation: DocumentNode, 30 | variables?: Variables, 31 | loading: boolean, 32 | error?: Error 33 | } 34 | 35 | export type StateChange = { 36 | state: { 37 | queries: Query[], 38 | mutations: Mutation[] 39 | }, 40 | dataWithOptimisticResults: CacheStorage 41 | } 42 | -------------------------------------------------------------------------------- /src/types/apollo-client-devtools/src/backend/broadcastQueries.d.ts: -------------------------------------------------------------------------------- 1 | import { Hook } from './hook' 2 | import Bridge from '../bridge' 3 | 4 | export const initBroadCastEvents: (hook: Hook, bridge: Bridge) => void 5 | -------------------------------------------------------------------------------- /src/types/apollo-client-devtools/src/backend/hook.d.ts: -------------------------------------------------------------------------------- 1 | export function installHook(window: Window, devToolsVersion: string): void 2 | 3 | export interface Hook { 4 | ApolloClient: unknown 5 | actionLog: string[] 6 | devToolsVersion: string 7 | on(event: string, fn: (...args: unknown[]) => void): void 8 | once(event: string, fn: (...args: unknown[]) => void): void 9 | off(event: string, fn: (...args: unknown[]) => void): void 10 | emit(event: string): void 11 | } 12 | -------------------------------------------------------------------------------- /src/types/apollo-client-devtools/src/backend/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Hook } from './hook' 2 | import Bridge from '../bridge' 3 | 4 | export const sendBridgeReady: () => void 5 | export const initBackend: (bridge: Bridge, hook: Hook, storage: Storage) => void 6 | -------------------------------------------------------------------------------- /src/types/apollo-client-devtools/src/backend/links.d.ts: -------------------------------------------------------------------------------- 1 | import { Hook } from './hook' 2 | import Bridge from '../bridge' 3 | 4 | export const initLinkEvents: (hook: Hook, bridge: Bridge) => void 5 | -------------------------------------------------------------------------------- /src/types/apollo-client-devtools/src/backend/typeDefs.d.ts: -------------------------------------------------------------------------------- 1 | type TypeDefs = string | Record 2 | 3 | export interface Schema { 4 | definition: string, 5 | directives: string 6 | } 7 | 8 | export function buildSchemasFromTypeDefs(typeDefs: TypeDefs): [Schema] 9 | -------------------------------------------------------------------------------- /src/types/apollo-client-devtools/src/bridge.d.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events' 2 | 3 | export type Message = string | { event: string, payload: unknown } 4 | 5 | export interface Wall { 6 | listen(fn: (message: Message) => void): void 7 | send(message: Message): void 8 | } 9 | 10 | export default class Bridge extends EventEmitter { 11 | constructor(wall: Wall) 12 | send(event: string, payload: unknown): void 13 | log(message: string): void 14 | } 15 | -------------------------------------------------------------------------------- /src/types/eventsourcemock/index.d.ts: -------------------------------------------------------------------------------- 1 | import type { EventEmitter } from 'events' 2 | 3 | type EventSourceConfigurationType = { 4 | withCredentials: boolean 5 | } 6 | 7 | type ReadyStateType = 0 | 1 | 2 8 | 9 | declare const defaultOptions: { 10 | withCredentials: false 11 | } 12 | 13 | export const sources: Record 14 | 15 | export default class EventSource { 16 | static CONNECTING: ReadyStateType 17 | static OPEN: ReadyStateType 18 | static CLOSED: ReadyStateType 19 | 20 | CONNECTING: ReadyStateType 21 | OPEN: ReadyStateType 22 | CLOSED: ReadyStateType 23 | 24 | __emitter: EventEmitter 25 | onerror: ((this: EventSource, ev: Event) => unknown) | null 26 | onmessage: ((this: EventSource, ev: MessageEvent) => unknown) | null 27 | onopen: ((this: EventSource, ev: Event) => unknown) | null 28 | readyState: ReadyStateType 29 | url: string 30 | withCredentials: boolean 31 | 32 | constructor( 33 | url: string, 34 | configuration?: EventSourceInit 35 | ) 36 | 37 | addEventListener(eventName: string, listener: (ev: Event) => void): void 38 | removeEventListener(eventName: string, listener: (ev: Event) => void): void 39 | close(): void 40 | emit(eventName: string, messageEvent?: MessageEvent): void 41 | emitError(error: Event): void 42 | emitOpen(): void 43 | emitMessage(message: MessageEvent): void 44 | 45 | // Actually missing 46 | dispatchEvent(event: Event): boolean 47 | } 48 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "esModuleInterop": true, 5 | "module": "es2015", 6 | "target": "es2015", 7 | "moduleResolution": "node", 8 | "strict": true, 9 | "baseUrl": "src/types" 10 | }, 11 | "files": [ 12 | "src/index.ts", 13 | "src/__tests__/IntegrationTests.test.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | mode: 'production', 5 | module: { 6 | rules: [ 7 | { 8 | test: /\.ts$/, 9 | use: 'ts-loader', 10 | exclude: /node_modules/ 11 | } 12 | ] 13 | }, 14 | resolve: { 15 | extensions: ['.ts', '.js'] 16 | }, 17 | output: { 18 | filename: 'bundle.js', 19 | path: path.resolve(__dirname, 'Sources/ApolloDeveloperKit/Assets') 20 | } 21 | } 22 | --------------------------------------------------------------------------------